1. JS的数据类型,如何判断js的数据类型?
数据类型有:Number,String,Boolean,Undefined,Null,Object,Array
其中,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. 事件循环
事件循环的主要工作流程如下:
- 执行同步任务(Synchronous Tasks): 当 JavaScript 引擎开始执行代码时,会首先执行当前调用栈中的所有同步任务,直到调用栈为空。
- 执行微任务(Microtasks): 在每个宏任务执行完毕之后,会立即执行所有微任务队列中的任务。微任务包括 Promise.then()、MutationObserver 和 process.nextTick(Node.js 中)等。
- 执行宏任务(Macrotasks): 在微任务执行完毕之后,会从宏任务队列中选择一个任务来执行。宏任务包括 setTimeout、setInterval、requestAnimationFrame、I/O 操作等。
- 等待新的任务: 一旦当前宏任务执行完毕,事件循环会检查是否有新的宏任务需要执行。如果有,事件循环会继续执行宏任务队列中的下一个任务,否则将继续等待新的任务加入。