前端面试问题汇总 - JS篇

1. JS的数据类型,如何判断js的数据类型?

        数据类型有:NumberStringBooleanUndefinedNullObjectArray

        其中,Number,String,Boolean,Undefined等基本数据可以直接用 typeof 进行判定。但对于对象类型(如数组和null),typeof通常不能准确判断,它会简单地返回 "object"。为了更准确地判断数据类型,可以使用instanceof运算符来判断对象是否为某个构造函数的实例。对于数组和null,可以使用Array.isArray()方法来判断是否为数组使用== null来判断是否为null

2. 数组去重方法有哪些?

  • 新建数组,循环遍历去重
  • 借助Set数据类型,[...new Set(array)] 或  Array.from(new Set(arr))
  • 新建对象,遍历数组,在转回新的数组

3. 深拷贝和浅拷贝区别?如何实现深拷贝?

        深拷贝和浅拷贝的区别在于是否真正获取了一个对象的复制实体,而不是引用。只针对Object和Array这样的引用数据类型。

        浅拷贝仅仅是复制指向的内存地址,如果原地址中对象被改变,那么浅拷贝出来的对象也会相应改变。

        深拷贝是在计算机中开辟一块新的内存地址用于存放复制的对象。

4. 说一下防抖和节流,分别如何实现?

  • 防抖的原理是在事件触发后等待一段时间,如果在这段时间内没有再次触发事件,则执行该事件,否则重新等待一段时间。适用于用户输入(如搜索框输入)等频繁触发的事件。
// 封装
function debounce(func, delay) {
  let timerId;
  return function(...args) {
    clearTimeout(timerId);
    timerId = setTimeout(() => {
      func.apply(this, args);
    }, delay);
  };
}
// 引用示例const debouncedFunction = debounce(function() {
  console.log('Debounced function executed');
}, 300);
// 触发事件
debouncedFunction(); // 不会立即执行
debouncedFunction(); // 不会立即执行
// 等待300毫秒后执行
// 如果在300毫秒内再次触发,则重新等待300毫秒
  • 节流的原理是规定一个单位时间,在这个单位时间内只能执行一次事件,如果在这个单位时间内触发多次事件,只有一次会生效。适用于用户频繁操作,但是我们希望限制其触发频率的场景,比如滚动加载、拖拽等。
// 封装
function throttle(func, delay) {
  let canRun = true;
  return function(...args) {
    if (!canRun) return;
    canRun = false;
    setTimeout(() => {
      func.apply(this, args);
      canRun = true;
    }, delay);
  };
}
// 引用示例
const throttledFunction = throttle(function() {
  console.log('Throttled function executed');
}, 300);
// 触发事件
throttledFunction(); // 立即执行
throttledFunction(); // 在300毫秒内再次触发,不会执行

5. 闭包是什么?如何实现?使用场景有哪些?

        闭包(Closure)是指在函数内部创建另一个函数时,内部函数可以访问其外部函数作用域的变量,即使外部函数已经执行结束并返回了,这个内部函数仍然可以访问和修改外部函数的变量。闭包使得函数可以保留对自己定义时所处环境的引用,这样它就可以在其定义的作用域之外执行。

        使用场景:封装私有变量模块化开发延迟执行

6. 闭包优缺点分别有哪些针对闭包缺点有什么解决方案

闭包的优点:

  • 访问外部变量: 闭包可以访问函数外部作用域的变量,使得函数能够访问其创建时所处的环境。
  • 保护变量: 闭包可以帮助保护函数内部的变量,防止外部代码对其进行意外修改,提高了代码的安全性。
  • 实现私有变量和方法: 闭包可以模拟私有变量和方法,使得外部无法直接访问,从而实现了封装。
  • 保存状态: 闭包可以保存函数执行时的状态,使得函数在多次调用之间保持状态的连续性。

闭包的缺点:

  • 内存泄漏: 如果闭包中引用了外部函数的变量,而这个闭包又被长期引用(比如被存储在全局变量或定时器中),可能会导致外部函数的变量无法被释放造成内存泄漏
  • 性能损耗: 使用闭包会增加内存和 CPU 的开销,因为闭包会捕获外部变量的引用,导致函数的作用域链变长,查找变量时需要遍历更多的作用域。

解决闭包缺点的方法:

  • 手动释放引用: 在不再需要使用闭包时,手动释放对外部变量的引用,以便垃圾回收器能够回收相关的内存。
  • 减少闭包的使用范围: 尽量减少闭包的作用域范围,避免将闭包存储在全局变量中或长期引用,以减少内存占用和性能损耗。
  • 使用模块化: 将功能模块化,通过模块化的方式管理变量和方法,可以降低闭包的使用频率,从而减少潜在的内存泄漏问题。
  • 避免滥用闭包: 在设计和编写代码时,避免过度依赖闭包,只在必要的情况下使用闭包,以免造成性能问题和内存泄漏。

7. 什么是JS原型?原型链是什么?

        JS 原型是一个普通的对象,它包含了一组属性和方法,可以被其他对象共享。每个对象都有一个关联的原型对象,可以通过 __proto__ 属性来访问它的原型对象。

        原型链是由对象的原型对象构成的链式结构。当 JavaScript 查找对象的属性或方法时,如果当前对象本身没有该属性或方法,则会沿着原型链向上查找,直到找到相应的属性或方法,或者到达原型链的顶端(即 Object.prototype)。

        原型链的顶端是 Object.prototype,它是所有 JavaScript 对象的根原型对象。

8. 作用域是什么?

        作用域(Scope)是指在程序中定义变量的区域,确定了变量的可访问性和生命周期。

        作用域主要有全局作用域(Global Scope)、局部作用域(Local Scope)两种。

9. 数组的常用方法有哪些?

  • push(el_1, ..., el_N): 将一个或多个元素添加到数组的末尾,并返回数组的新长度
  • pop(): 删除数组的最后一个元素,并返回该元素的值
  • shift(): 删除数组的第一个元素,并返回该元素的值,同时将数组的长度减 1
  • unshift(el_1, ..., el_N): 将一个或多个元素添加到数组的开头,并返回数组的新长度
  • concat(arr_1, ..., arr_N): 返回一个新数组,该数组是由当前数组和其他数组或值连接而成的
  • join(''): 将数组中的所有元素连接成一个字符串,使用指定的分隔符来分隔元素
  • slice(start, end): 返回一个新数组,该数组包含从原始数组中指定开始到结束(不包含)的部分
  • splice(start, deleteCount, item1, ..., itemN): 从数组中添加/删除项目,返回被删除的项目
  • indexOf(query, fromIndex): 返回数组中第一个匹配指定值的索引,如果没有找到则返回 -1
  • lastIndexOf(query, fromIndex): 返回数组中最后一个匹配指定值的索引,如果没有找到则返回 -1
  • forEach((el, index, arr) => {}, thisArg): 对数组的每个元素执行提供的函数
  • map((el, index, arr) => {}, thisArg): 创建一个新数组,该数组的每个元素都是原始数组的对应元素上调用回调函数的结果
  • filter((el, index, arr) => {}, thisArg): 创建一个新数组,包含原始数组中所有通过提供的测试函数的元素
  • reduce((accumulator, currentValue, currentIndex, array) => {}, initialValue): 对数组中的每个元素执行一个提供的函数(从左到右),将结果汇总为单个值
  • find((el, index, arr) => {}, thisArg): 返回数组中满足提供的测试函数的第一个元素的值,如果没有找到则返回 undefined
  • findIndex((el, index, arr) => {}, thisArg): 返回数组中满足提供的测试函数的第一个元素的索引,如果没有找到则返回 -1
  • every((el, index, arr) => {}, thisArg): 测试数组的所有元素是否都通过了指定函数的测试
  • some((el, index, arr) => {}, thisArg): 测试数组的某些元素是否通过了指定函数的测试

10. 对象的常用方法有哪些?

  • Object.keys(obj): 返回一个包含对象所有可枚举属性的数组
  • Object.values(obj): 返回一个包含对象所有可枚举属性值的数组
  • Object.entries(obj): 返回一个包含对象所有可枚举属性键值对的数组,每个键值对表示为 [key, value] 的形式
  • Object.assign(target, source1, source2, ...): 将一个或多个源对象的属性复制到目标对象,并返回目标对象
  • Object.create(proto, propertiesObject): 使用指定的原型对象和属性来创建一个新对象
  • Object.defineProperty(obj, prop, descriptor): 定义对象的一个新属性,或者修改现有属性的特性
  • Object.defineProperties(obj, properties): 定义或修改对象的多个属性的特性
  • Object.getOwnPropertyDescriptor(obj, prop): 返回指定对象上一个自有属性对应的属性描述符
  • Object.getOwnPropertyNames(obj): 返回一个包含指定对象所有自身属性名称的数组
  • Object.freeze(obj): 冻结一个对象,防止对对象进行修改
  • Object.is(value1, value2): 判断两个值是否严格相等,类似于 === 运算符
  • Object.seal(obj): 封闭一个对象,防止添加新属性并将所有现有属性标记为不可配置
  • Object.setPrototypeOf(obj, prototype): 设置一个对象的原型(即 __proto__ 属性)为另一个对象或 null
  • Object.getOwnPropertySymbols(obj): 返回一个包含指定对象自有的所有 Symbol 属性的数组
  • Object.fromEntries(entries): 将一个键值对的列表转换为一个对象

11. 0.1+0.2等于0.3吗?为什么?如何解决?

        0.30000000000000004。这是因为 JavaScript 使用 IEEE 754 浮点数标准来表示数字,而浮点数在计算机中是以二进制形式存储的,有时无法精确地表示十进制小数

12. 如何改变一个函数的上下文?

        要改变一个函数的上下文,可以使用 bind()、call() 或 apply() 方法来实现。这些方法允许您显式地指定函数在调用时应该使用的上下文(即 this 值)。

  • bind() : 创建一个新的函数,该函数与原始函数具有相同的代码和作用域,但是在调用时,其 this 值被绑定到指定的对象
  • call() : 调用函数,并且可以指定函数内部 this 的值,以及其他参数以逗号分隔的形式传递给函数
  • apply() : 与 call() 方法类似,但是接收一个参数数组,而不是一系列单独的参数

13. 如何做全局错误统一处理?

        可以通过window.onerror注册全局错误处理函数并进行处理来实现全局错误的统一处理

14. 如何理解js是单线程的

        JavaScript 是单线程的意味着在任何给定的时刻,JavaScript 引擎只能执行一个任务

        在 JavaScript 中,这个单线程被称为主线程(也称为 UI 线程),它负责执行所有的 JavaScript 代码、处理事件、执行 DOM 操作等。这意味着,当 JavaScript 代码正在执行时,其他任务(如用户输入、定时器事件、HTTP 请求等)必须等待

        这种单线程模型的主要原因是为了简化开发,并减少在多线程编程中可能出现的复杂性和竞态条件。JavaScript 最初是作为浏览器中处理用户交互的脚本语言而设计的,因此,使用单线程模型可以避免多个线程同时修改页面状态引发的问题,如数据竞争、死锁等。

        虽然 JavaScript 引擎是单线程的,但是浏览器环境提供了一些机制来处理并发任务,比如事件循环(Event Loop)和异步编程模型。通过事件循环,JavaScript 可以在等待异步操作完成时继续执行其他任务,以保持页面的响应性和流畅性。

        因此,虽然 JavaScript 是单线程的,但通过异步编程模型和事件循环机制,可以在单线程中实现并发执行任务的效果,从而更好地满足用户交互和应用程序的需求。

15. 事件循环

事件循环的主要工作流程如下:

  1. 执行同步任务(Synchronous Tasks): 当 JavaScript 引擎开始执行代码时,会首先执行当前调用栈中的所有同步任务,直到调用栈为空。
  2. 执行微任务(Microtasks): 在每个宏任务执行完毕之后,会立即执行所有微任务队列中的任务。微任务包括 Promise.then()MutationObserver 和 process.nextTick(Node.js 中)等。
  3. 执行宏任务(Macrotasks): 在微任务执行完毕之后,会从宏任务队列中选择一个任务来执行。宏任务包括 setTimeoutsetIntervalrequestAnimationFrameI/O 操作等。
  4. 等待新的任务: 一旦当前宏任务执行完毕,事件循环会检查是否有新的宏任务需要执行。如果有,事件循环会继续执行宏任务队列中的下一个任务,否则将继续等待新的任务加入。
  • 9
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值