2024前端面试题总结:JavaScript篇完整版_箭头函数的this指向(1)

const user1 = { name: “Alice”, age: 30 };
const user2 = { name: “Bob”, age: 25 };
userMap.set(user1, “User data for Alice”);
userMap.set(user2, “User data for Bob”);

// 记录操作顺序
const logMap = new Map();
logMap.set(“1”, “First action”);
logMap.set(“2”, “Second action”);
logMap.set(“3”, “Third action”);

// 避免命名冲突
const moduleAMap = new Map();
const moduleBMap = new Map();
moduleAMap.set(“data”, “Module A data”);
moduleBMap.set(“data”, “Module B data”);


`WeakMap 的应用场景:`


1. 私有数据存储:WeakMap 可以用于存储对象的私有数据,因为键是弱引用,不会阻止对象被垃圾回收。
2. 元数据存储:WeakMap 可以用于存储对象的元数据,这些元数据不会影响对象的生命周期。



// 私有数据存储
const privateDataMap = new WeakMap();
class MyClass {
constructor() {
privateDataMap.set(this, { privateValue: 42 });
}
getPrivateValue() {
return privateDataMap.get(this).privateValue;
}
}

// 元数据存储
const metadataMap = new WeakMap();
function trackMetadata(obj, metadata) {
metadataMap.set(obj, metadata);
}



### 什么是类数组,如何转换成真正的数组?


类数组(Array-like)是一种类似于数组的对象,它具有一些数组的特性,例如有一系列的数字索引和 length 属性,但它并不是真正的数组,缺少数组的方法和属性。  
 常见的类数组包括函数的 arguments 对象、DOM 元素列表(如 NodeList)、字符串等。虽然类数组在某些情况下可以像数组一样被访问和操作,但由于缺少数组的方法,有时可能需要将其转换为真正的数组。  
 以下是如何将类数组转换为真正的数组的几种方法:  
`使用 Array.from():`  
 Array.from() 方法可以从类数组对象或可迭代对象创建一个新的数组。它将类数组的值复制到一个新的数组中,并返回这个新数组。



// 类数组对象
function convertToArray() {
const argsArray = Array.from(arguments);
console.log(Array.isArray(argsArray)); // true
return argsArray;
}

convertToArray(1, 2, 3); // [1, 2, 3]


`使用 Array.prototype.slice.call():`  
 slice() 方法可以用于截取数组的一部分,当将 slice() 应用于类数组对象时,可以将其转换为真正的数组。



// 类数组对象
const nodeList = document.querySelectorAll(“p”);
const nodeListArray = Array.prototype.slice.call(nodeList);
console.log(Array.isArray(nodeListArray)); // true


`使用扩展运算符:`  
 扩展运算符可以将一个可迭代对象转换为多个参数,然后通过 Array.from() 或直接使用 [] 创建新数组。



// 类数组对象
const nodeList = document.querySelectorAll(“p”);
const nodeListArray = […nodeList];
console.log(Array.isArray(nodeListArray)); // true


`使用 concat() 方法:`  
 通过将类数组对象与一个空数组(或其他数组)连接,可以创建一个新数组,实现类数组转换。



// 类数组对象
const nodeList = document.querySelectorAll(“p”);
const nodeListArray = [].concat(nodeList);
console.log(Array.isArray(nodeListArray)); // true


#### **前端面试题库 (**面试必备)****推荐:★★★★★****


地址:[前端面试题库](https://bbs.csdn.net/topics/618166371)


### 操作数组有哪些方法?


javaScript 数组有许多原生的方法,用于对数组进行操作、遍历、修改等。以下是一些常见的数组原生方法:


1. push(item1, item2, ...):向数组末尾添加一个或多个元素,并返回新数组的长度。
2. pop():移除并返回数组的最后一个元素。
3. unshift(item1, item2, ...):向数组开头添加一个或多个元素,并返回新数组的长度。
4. shift():移除并返回数组的第一个元素。
5. concat(array1, array2, ...):合并多个数组或值,返回一个新的合并后的数组。
6. slice(start, end):截取数组的一部分,返回一个新数组,不修改原数组。
7. splice(start, deleteCount, item1, item2, ...):从数组中删除、插入或替换元素,修改原数组,并返回被删除的元素组成的数组。
8. join(separator):将数组的所有元素转换为字符串,使用指定的分隔符连接。
9. indexOf(item, fromIndex):查找指定元素在数组中首次出现的位置,返回索引值,如果不存在则返回 -1。
10. lastIndexOf(item, fromIndex):查找指定元素在数组中最后一次出现的位置,返回索引值,如果不存在则返回 -1。
11. forEach(callback(item, index, array)):遍历数组的每个元素,执行回调函数。
12. map(callback(item, index, array)):创建一个新数组,其中的元素是对原数组每个元素执行回调函数的结果。
13. filter(callback(item, index, array)):创建一个新数组,包含满足回调函数条件的元素。
14. reduce(callback(accumulator, item, index, array), initialValue):从左到右依次处理数组的元素,将结果累积为单个值。
15. reduceRight(callback(accumulator, item, index, array), initialValue):从右到左依次处理数组的元素,将结果累积为单个值。
16. some(callback(item, index, array)):检测数组中是否有至少一个元素满足回调函数条件,返回布尔值。
17. every(callback(item, index, array)):检测数组中的所有元素是否都满足回调函数条件,返回布尔值。
18. find(callback(item, index, array)):返回数组中第一个满足回调函数条件的元素。
19. findIndex(callback(item, index, array)):返回数组中第一个满足回调函数条件的元素的索引,如果不存在则返回 -1。
20. sort(compareFunction(a, b)):对数组元素进行排序,可传入比较函数用于指定排序规则。
21. reverse():反转数组的元素顺序,修改原数组。
22. fill(value, start, end):将数组的一部分元素替换为指定的值,修改原数组。
23. includes(item, fromIndex):检测数组是否包含指定元素,返回布尔值。
24. isArray(obj):检测一个对象是否为数组,返回布尔值。


### 常见的DOM操作有哪些?


DOM(Document Object Model)操作是指通过 JavaScript 对网页中的 HTML 元素和内容进行增删改查的操作。以下是一些常见的 DOM 操作以及相应的示例:  
`获取元素:`


1. getElementById(id):根据元素的 id 获取元素。
2. getElementsByClassName(className):根据类名获取一组元素。
3. getElementsByTagName(tagName):根据标签名获取一组元素。
4. querySelector(selector):根据选择器获取第一个匹配的元素。
5. querySelectorAll(selector):根据选择器获取所有匹配的元素。



// 获取元素并修改其内容
let heading = document.getElementById(“myHeading”);
heading.textContent = “Hello, DOM!”;


`创建和插入元素:`


1. createElement(tagName):创建一个新的元素节点。
2. appendChild(node):将一个节点添加为另一个节点的子节点。
3. insertBefore(newNode, referenceNode):在指定节点之前插入一个新节点。



// 创建并插入新元素
let newParagraph = document.createElement(“p”);
newParagraph.textContent = “This is a new paragraph.”;
document.body.appendChild(newParagraph);


`修改元素属性和样式:`


1. setAttribute(name, value):设置元素的属性值。
2. getAttribute(name):获取元素的属性值。
3. style.property = value:设置元素的样式属性。



// 修改元素的属性和样式
let image = document.getElementById(“myImage”);
image.setAttribute(“src”, “new-image.jpg”);
image.style.width = “200px”;


`移除元素:`  
 removeChild(node):从父节点中移除指定的子节点。



// 移除元素
let paragraphToRemove = document.getElementById(“paragraphToRemove”);
paragraphToRemove.parentNode.removeChild(paragraphToRemove);


`事件处理:`


1. addEventListener(eventType, callback):为元素添加事件监听器。
2. removeEventListener(eventType, callback):移除元素的事件监听器。



// 添加事件监听器
let button = document.getElementById(“myButton”);
button.addEventListener(“click”, function() {
alert(“Button clicked!”);
});



### for...in和for...of有什么区别?


for...in 和 for...of 都是用于迭代(遍历)数据结构中的元素,但它们在使用方式和应用场景上有一些区别。  
`for...in 循环:`


1. 用于遍历对象的可枚举属性。
2. 返回的是属性名(键名)。
3. 可能会遍历到对象原型链上的属性。
4. 不适合遍历数组或类数组对象,因为会遍历到额外的非数字索引属性和原型链上的属性。



const obj = { a: 1, b: 2, c: 3 };

for (const key in obj) {
console.log(key); // 输出:a, b, c
}


`for...of 循环:`


1. 用于遍历可迭代对象(如数组、字符串、Set、Map 等)的值。
2. 返回的是集合中的值,而不是属性名。
3. 不会遍历对象的属性,只能遍历实际值。
4. 可以直接遍历数组等数据结构,适合循环迭代。



const arr = [1, 2, 3];

for (const value of arr) {
console.log(value); // 输出:1, 2, 3
}


`区别总结:`


1. for...in 遍历对象属性,返回属性名。
2. for...of 遍历可迭代对象的值,返回实际值。
3. for...in 可能会遍历到原型链上的属性,适合遍历对象属性。
4. for...of 不会遍历原型链上的属性,适合遍历实际值。


### 常见的位运算符有哪些?其计算规则是什么?


`按位与(&):`  
 计算规则:对两个操作数的每个对应位执行逻辑 AND 操作,结果中的每个位都取决于两个操作数的对应位是否都为 1。  
 示例:a & b 返回一个值,其中每个位都是 a 和 b 对应位的 AND 结果。  
`按位或(|):`  
 计算规则:对两个操作数的每个对应位执行逻辑 OR 操作,结果中的每个位都取决于两个操作数的对应位是否有至少一个为 1。  
 示例:a | b 返回一个值,其中每个位都是 a 和 b 对应位的 OR 结果。  
`按位异或(^):`  
 计算规则:对两个操作数的每个对应位执行逻辑 XOR 操作,结果中的每个位都取决于两个操作数的对应位是否不同。  
 示例:a ^ b 返回一个值,其中每个位都是 a 和 b 对应位的 XOR 结果。  
`按位非(~):`  
 计算规则:对操作数的每个位执行逻辑 NOT 操作,即对每个位取反,1 变为 0,0 变为 1。  
 示例:~a 返回一个值,其中每个位都是 a 每个位取反的结果。  
`左移(<<):`  
 计算规则:将操作数的二进制位向左移动指定的位数,右侧用 0 填充。  
 示例:a << b 返回一个值,其中 a 的二进制位向左移动 b 位。  
`右移(>>):`  
 计算规则:将操作数的二进制位向右移动指定的位数,左侧用符号位的值填充。  
 示例:a >> b 返回一个值,其中 a 的二进制位向右移动 b 位。  
`无符号右移(>>>):`  
 计算规则:将操作数的二进制位向右移动指定的位数,左侧用 0 填充。  
 示例:a >>> b 返回一个值,其中 a 的二进制位向右移动 b 位,左侧用 0 填充。


### 解释性语言和编译型语言的区别是什么?


解释性语言和编译型语言是两种不同的编程语言类型,它们的主要区别在于代码的执行方式和编译过程:  
**解释性语言:**


1. 执行方式:解释性语言的代码在运行时逐行被解释器翻译成机器代码,然后立即执行。代码是一边翻译一边执行,不需要先进行显式的编译过程。
2. 执行效率:由于每次运行都需要进行解释,解释性语言通常比较慢。解释性语言更注重开发速度和动态性,适用于一些快速原型开发和脚本编写场景。
3. 跨平台性:解释性语言的代码通常可以在不同平台上直接运行,不需要重新编译。
4. 例子:`Python、JavaScript、Ruby、PHP`是一些常见的解释性语言。


**编译型语言:**


1. 编译过程:编译型语言的代码需要在运行之前先通过编译器进行一次完整的编译,将源代码翻译成目标平台的机器代码。这个编译过程产生一个可执行文件,运行时不需要再次编译。
2. 执行效率:由于代码已经编译成机器代码,编译型语言的执行速度通常较快。适合开发需要高性能的应用程序。
3. 跨平台性:编译型语言的代码需要为每个不同的目标平台进行编译,因此需要重新编译适应不同的操作系统和硬件。
4. 例子:`C、C++、Rust、Go`是一些常见的编译型语言。


**总结区别:**


1. 解释性语言在运行时逐行解释并执行代码,执行速度较慢,适合开发快速原型和脚本。
2. 编译型语言在运行之前先编译成机器代码,执行速度较快,适合开发需要高性能的应用。
3. 解释性语言跨平台性较好,无需重新编译。
4. 编译型语言需要为每个平台重新编译,较难实现跨平台。


## JavaScript高级知识



### 什么是原型,原型链?


JavaScript 中的原型(prototype)和原型链(prototype chain)是理解对象和继承机制的关键概念。  
`原型(prototype):`  
 每个 JavaScript 对象都有一个关联的原型对象,它是一个普通对象,包含一些共享的属性和方法。当你访问对象的属性或方法时,如果对象本身没有这个属性或方法,JavaScript 引擎会沿着对象的原型链去查找。对象可以通过 prototype 属性来访问它的原型对象。  
`原型链(prototype chain):`  
 原型链是由一系列连接的原型对象组成的链。当访问对象的属性或方法时,如果对象本身没有找到,引擎会沿着原型链向上查找,直到找到该属性或方法或者到达原型链的顶端(Object.prototype)。这样的搜索路径就构成了原型链。  
 例如:



class Person {
constructor(name, age) {
this.name = name
this.age = age
}
sayHello() {
console.log(‘hello’)
}
}

const person = new Person(‘John’, 20)

console.log(Person.prototype)
console.log(person.proto)

const person2 = Object.create(person)
console.log(person2.proto.proto)
person2.sayHello()


执行结果如下  
  


![image.png](https://img-blog.csdnimg.cn/img_convert/4547ed91b57678619b374c328af31ed9.webp?x-oss-process=image/format,png)


  
 这里的`Person`类就存在自己的原型`prototype`,`prototype`是一个对象,拥有`Person`的`constructor和sayHello`方法  
 使用`Person`创建出一个实例`person`此时`person`上并没有`sayHello`方法,但是`person`的`__proto__`指向`Person`的`prototype`,当在当前对象上找不到`sayHello`方法时,js就会沿着`__proto__`的指向向上寻找  
 后面我们用`Object.create(person)`方法创建一个对象`person2`,此时该对象是空对象,但是该对象的`__proto__`指向`person`,而`person`的`__proto__`又指向`Person`的`prototype`,因此`person2`也可以调用`sayHello`方法  
 这种沿着`__proto__`指向一致向上寻找,便形成一个原型链,需要注意的时原型链的最顶端都是`null`



#### **前端面试题库 (**面试必备)****推荐:★★★★★****


地址:[前端面试题库](https://bbs.csdn.net/topics/618166371)


### 什么是闭包,有什么作用?


闭包是一种在函数内部创建的函数,它可以访问其包含函数(外部函数)的变量和参数,即使外部函数已经执行完毕。换句话说,闭包是一个函数加上该函数创建时所能访问的作用域,它可以"捕获"这个作用域中的变量,并在函数执行后继续保持对这些变量的访问能力。  
**闭包通常由以下两个主要部分组成:**


1. 内部函数(嵌套函数):在外部函数内部定义的函数,形成了闭包。
2. 外部函数的作用域:内部函数可以访问和"捕获"外部函数的作用域中的变量和参数。


**闭包的作用:**


1. `保护数据:`通过闭包,可以将一些数据限制在函数的作用域内,避免全局污染,只暴露必要的接口。
2. `保存状态:`闭包可以用于保存函数执行过程中的状态,例如计数器、缓存等,使得状态在函数调用之间得以保持。
3. `实现模块化:`通过闭包,可以实现类似于模块的封装,将一组相关的功能组织在一起,避免命名冲突和变量泄露。
4. `实现回调和异步操作:`JavaScript 中的回调函数和异步操作通常需要使用闭包来捕获外部函数的状态和数据。
5. `创建私有变量:`通过闭包可以模拟私有变量,使得只有内部函数能够访问和修改某些数据。


**以下是一个闭包的简单示例:**



function outerFunction() {
let outerVariable = “I am from outer function”;

function innerFunction() {
console.log(outerVariable); // 内部函数访问了外部函数的变量
}

return innerFunction; // 返回内部函数作为闭包
}

let closure = outerFunction(); // 调用外部函数,返回内部函数
closure(); // 调用内部函数,输出 “I am from outer function”


在这个例子中,innerFunction 是一个闭包,它可以访问外部函数 outerFunction 中的变量 outerVariable。当我们调用外部函数并将其返回的内部函数赋值给 closure 变量后,closure 成为了一个保存了 outerVariable 的闭包,可以在之后的调用中继续访问这个变量。


### JavaScript中的作用域是什么,有什么作用?


JavaScript 中的作用域(scope)是指变量和函数在代码中可访问的范围。作用域规定了在何处以及如何查找变量。作用域的概念在 JavaScript 中是非常重要的,因为它影响着变量的可见性和生命周期。  
**作用域的类型:**


1. `全局作用域:`在代码的最外层定义的变量和函数,可以在代码的任何位置被访问,称为全局作用域。
2. `局部作用域:`在函数内部定义的变量和函数,只能在函数内部被访问,称为局部作用域(也称为函数作用域)。


**作用域的作用:**


1. `隔离变量:`作用域可以将变量限制在特定的代码块或函数内部,防止变量之间发生命名冲突。
2. `封装和信息隐藏:`通过使用函数作用域,可以将一些变量隐藏在函数内部,避免了外部代码直接访问和修改这些变量,实现了信息隐藏和封装。
3. `变量的生命周期:`作用域也影响了变量的生命周期,变量在进入作用域时被创建,在离开作用域时被销毁。这有助于内存管理和垃圾回收。



// 全局作用域
let globalVariable = “I am global”;

function myFunction() {
// 局部作用域
let localVariable = “I am local”;
console.log(localVariable); // 可以访问局部变量
console.log(globalVariable); // 可以访问全局变量
}

console.log(globalVariable); // 可以在全局范围内访问
// console.log(localVariable); // 无法在全局范围内访问局部变量


在这个示例中,globalVariable 是一个全局变量,可以在代码的任何地方访问。localVariable 是在函数内部定义的局部变量,只能在函数内部访问。  
 作用域帮助我们组织代码、封装功能、限制变量的可见性,并且有助于避免命名冲突,使得代码更加模块化和可维护。


### 如何理解JavaScript中的执行上下文?


执行上下文(Execution Context)是 JavaScript 中一个重要的概念,它是代码在执行过程中的环境,包含了当前代码执行所需的所有信息,如变量、函数、作用域等。每当 JavaScript 代码运行时,都会创建一个新的执行上下文。理解执行上下文对于理解代码的执行流程和作用域非常关键。  
**执行上下文可以分为三种类型:**


1. `全局执行上下文:`整个脚本的最外层环境,在代码执行之前被创建。在浏览器中,全局执行上下文通常是 window 对象。
2. `函数执行上下文:`每次调用函数时,都会创建一个新的函数执行上下文,用于管理函数内部的变量、参数和执行流程。
3. `Eval 函数执行上下文:`使用 eval 函数执行的代码会在一个特殊的执行上下文中运行。


**执行上下文包含的重要信息:**


1. `变量环境(Variable Environment):`包含了变量和函数声明,以及外部环境的引用。在函数执行上下文中,这部分称为函数的“作用域链”。
2. `词法环境(Lexical Environment):`类似于变量环境,但是在函数执行上下文中,它会随着函数嵌套的改变而变化,支持闭包。
3. `this 值:`指向当前执行上下文的上下文对象。
4. `外部引用:`指向包含当前执行上下文的外部执行上下文。


**执行上下文的生命周期:**


1. `创建阶段:`在进入代码块之前,执行上下文会被创建。这时会创建变量对象(VO),初始化函数参数、变量和函数声明。
2. `执行阶段:`在进入代码块时,代码会按顺序执行。在这个阶段,变量赋值和函数调用等操作会被执行。
3. `销毁阶段:`执行完代码后,执行上下文会被销毁,函数的局部变量等会被释放。


**示例:**



function greet(name) {
var message = "Hello, " + name;
console.log(message);
}

greet(“Alice”); // 调用函数,创建函数执行上下文


在这个示例中,当调用 greet("Alice") 时,会创建一个新的函数执行上下文用于执行 greet 函数内部的代码。这个执行上下文包含了 name 参数和 message 变量,以及其他所需的信息。一旦函数执行完毕,这个执行上下文将会被销毁。


### 什么是Ajax,如何实现一个简单的Ajax请求


Ajax(Asynchronous JavaScript and XML) 是一种用于在不刷新整个页面的情况下,通过 JavaScript 在后台与服务器进行数据交换的技术。通过 Ajax,可以实现异步加载数据、动态更新页面内容,从而提升用户体验。  
**Ajax 的工作流程:**


1. 创建 XMLHttpRequest 对象:通过 JavaScript 创建一个 XMLHttpRequest 对象,用于在后台与服务器进行通信。
2. 发送请求:使用 XMLHttpRequest 对象发送请求,可以是 GET 或 POST 请求,传递参数或数据给服务器。
3. 接收响应:服务器处理请求后,返回数据或响应,JavaScript 通过监听事件来获取响应数据。
4. 更新页面:根据接收到的响应数据,使用 JavaScript 动态更新页面内容,而不需要刷新整个页面。


**以下是一个简单的 Ajax 请求的示例,使用原生的 XMLHttpRequest 对象:**



Ajax Example

Load Data


在这个示例中,点击 "Load Data" 按钮会触发一个 Ajax 请求,获取 `https://jsonplaceholder.typicode.com/posts/1` 地址返回的数据,并将其中的标题显示在页面上。`XMLHttpRequest` 对象用于创建和管理请求,`onreadystatechange` 事件监听响应状态的变化,`readyState` 表示请求状态,`status` 表示响应状态码,`responseText` 存储响应内容。


### 如何使用Promise封装Ajax请求


使用 Promise 封装 Ajax 请求可以让代码更具可读性和可维护性,同时还可以更好地处理异步操作的结果。以下是一个使用 Promise 封装 Ajax 请求的示例,使用原生的 XMLHttpRequest 对象:



function makeAjaxRequest(url, method) {
return new Promise(function(resolve, reject) {
let xhr = new XMLHttpRequest();
xhr.open(method, url, true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
resolve(xhr.responseText); // 请求成功,将响应数据传递给 resolve 函数
} else {
reject(new Error("Request failed with status: " + xhr.status)); // 请求失败,将错误信息传递给 reject 函数
}
}
};
xhr.send();
});
}

// 使用封装的函数
makeAjaxRequest(“https://jsonplaceholder.typicode.com/posts/1”, “GET”)
.then(function(response) {
console.log(“Response:”, response);
})
.catch(function(error) {
console.error(“Error:”, error);
});


在这个示例中,makeAjaxRequest 函数返回一个 Promise 对象,它将一个回调函数传递给 XMLHttpRequest 的 onreadystatechange 事件处理程序。当请求完成时,如果状态码为 200,则调用 resolve 函数并传递响应数据,否则调用 reject 函数并传递错误信息。  
 通过使用 Promise,你可以使用 .then() 方法处理成功的响应,使用 .catch() 方法处理失败的情况,使得代码逻辑更加清晰和可控。封装了 Promise 的 Ajax 请求可以更好地管理异步操作,使得代码更具可读性和可维护性。不过需要注意的是,现代的 Web 开发中,许多人更倾向于使用基于 Promise 的库(如 axios)来进行网络请求,因为它们提供了更便捷和强大的功能。


### Ajax,axios,fetch之间有什么区别


Ajax、axios 和 fetch 都是用于在前端实现网络请求的工具或技术,但它们在用法和功能上有一些区别。  
`Ajax:`


1. Ajax(Asynchronous JavaScript and XML)是一种异步通信技术,使用原生的 JavaScript 和 XMLHttpRequest 对象来实现在不刷新整个页面的情况下与服务器交换数据。
2. Ajax 可以用于发送各种类型的请求(GET、POST 等),并接收服务器返回的数据,然后通过 JavaScript 更新页面内容。
3. 原生的 Ajax 代码较为繁琐,需要处理不同浏览器的兼容性。


`axios:`


1. axios 是一个基于 Promise 的 HTTP 客户端,用于在浏览器和 Node.js 中发送 HTTP 请求。
2. axios 提供了简洁的 API,并且处理了很多底层细节,如请求和响应的转换、拦截器、错误处理等。
3. axios 可以在浏览器环境和 Node.js 环境中使用,支持请求取消和并发请求管理等功能。


`fetch:`


1. fetch 是 Web API 中的一部分,是浏览器原生提供的一个用于发起网络请求的方法。
2. fetch 提供了一种基于 Promise 的简单和现代的方式来发送请求和处理响应。
3. 与 axios 不同,fetch 不会自动将请求失败(例如 HTTP 错误状态码)视为错误,需要手动检查响应状态。


**区别总结:**


1. Ajax 是一种通信技术,主要通过 XMLHttpRequest 对象实现。
2. axios 是一个第三方库,提供了基于 Promise 的 API,用于简化 HTTP 请求的处理。
3. fetch 是浏览器原生提供的网络请求 API,也使用了 Promise,但处理响应状态的方式略有不同。
4. axios 和 fetch 提供了更现代、更简洁的 API,且能处理一些附加功能,如拦截器、取消请求等。


### JavaScript中的this是什么?


在 JavaScript 中,this 是一个特殊关键字,它在不同的上下文中指向不同的值。this 的值取决于代码在何处被调用,以及调用方式。  
**this 的指向可以分为以下几种情况:**  
`全局上下文中的 this:`  
 在全局上下文(没有嵌套在任何函数内部)中,this 指向全局对象,通常在浏览器环境中是 window 对象,在 Node.js 环境中是 global 对象。  
`函数中的 this:`


1. 在函数中,this 的值取决于函数是如何被调用的。常见的情况包括:
2. 在函数中,直接使用 this,其值取决于函数的调用方式。
3. 在函数中,作为对象的方法被调用,this 指向调用该方法的对象。
4. 使用 call()、apply() 或 bind() 方法来明确指定函数执行时的 this。


`箭头函数中的 this:`  
 箭头函数没有自己的 this 值,它会继承外部函数的 this 值。箭头函数的 this 始终指向包含它的最近的非箭头函数的 this 值,即词法作用域的 this。  
`DOM 事件处理函数中的 this:`  
 在处理 DOM 事件时,事件处理函数的 this 默认指向触发事件的 DOM 元素。但是,如果使用箭头函数作为事件处理函数,this 将保持其外部函数的 this 值。  
**示例:**



console.log(this); // 全局上下文中,指向全局对象(浏览器环境中是 window)

function sayHello() {
console.log(this); // 在函数中,取决于调用方式
}

const obj = {
method: function() {
console.log(this); // 在对象的方法中,指向调用方法的对象(obj)
}
};

const arrowFunction = () => {
console.log(this); // 箭头函数中,继承外部函数的 this 值
};

sayHello(); // 函数中,取决于调用方式
obj.method(); // 对象的方法中,指向调用方法的对象(obj)
arrowFunction(); // 箭头函数中,继承外部函数的 this 值



### call,apply,bind函数如何使用,有何区别?


call、apply 和 bind 是 JavaScript 中用于显式设置函数执行上下文(this 值)的方法。它们的主要作用是在调用函数时,将指定的对象作为函数的上下文来执行函数。这些方法的区别在于参数传递的方式和执行时间。  
`call 方法:`


1. call 方法允许你将一个对象作为参数传递给函数,并将该对象作为函数的上下文(this 值)来调用函数。
2. 除了第一个参数,后续的参数是传递给函数的实际参数。
3. 函数会立即执行。


**示例:**



function greet(name) {
console.log(Hello, ${name}! I am ${this.name}.);
}

const person = { name: “Alice” };
greet.call(person, “Bob”); // 输出:Hello, Bob! I am Alice.


`apply 方法:`


1. apply 方法与 call 方法类似,但参数是以数组形式传递的。
2. 函数会立即执行。


**示例:**



function greet(name) {
console.log(Hello, ${name}! I am ${this.name}.);
}

const person = { name: “Alice” };
greet.apply(person, [“Bob”]); // 输出:Hello, Bob! I am Alice.


`bind 方法:`


1. bind 方法返回一个新函数,该新函数的上下文被绑定到指定的对象,但不会立即执行。
2. 返回的新函数可以稍后被调用,调用新函数时将使用绑定的上下文。


**示例:**



function greet(name) {
console.log(Hello, ${name}! I am ${this.name}.);
}

const person = { name: “Alice” };
const boundGreet = greet.bind(person);
boundGreet(“Bob”); // 输出:Hello, Bob! I am Alice.


`区别总结:`


1. call 和 apply 是用于立即执行函数并传递参数的方法,其中 apply 接受参数的形式是数组。
2. bind 是用于创建一个新函数,该函数会在稍后的调用中使用绑定的上下文。


这些方法通常用于在特定上下文中执行函数,例如在事件处理程序、回调函数或特定环境中的函数调用。


### 如何自己实现call,apply,bind函数?


`实现 call 函数:`



Function.prototype.myCall = function(context, …args) {
context = context || window; // 如果未传入上下文,则使用全局对象
const uniqueKey = Symbol(); // 使用一个唯一的键来避免覆盖原有属性

context[uniqueKey] = this; // 将当前函数设置为上下文的属性
const result = contextuniqueKey; // 调用函数并传递参数

delete context[uniqueKey]; // 删除添加的属性
return result; // 返回函数执行的结果
};

function greet(name) {
console.log(Hello, ${name}! I am ${this.name}.);
}

const person = { name: “Alice” };
greet.myCall(person, “Bob”); // 输出:Hello, Bob! I am Alice.


`实现 apply 函数:`



Function.prototype.myApply = function(context, args) {
context = context || window;
const uniqueKey = Symbol();

context[uniqueKey] = this;
const result = contextuniqueKey;

delete context[uniqueKey];
return result;
};

function greet(name) {
console.log(Hello, ${name}! I am ${this.name}.);
}

const person = { name: “Alice” };
greet.myApply(person, [“Bob”]); // 输出:Hello, Bob! I am Alice.


`实现 bind 函数:`



Function.prototype.myBind = function(context, …args1) {
const originalFunction = this;
return function(…args2) {
context = context || window;
const combinedArgs = args1.concat(args2); // 合并传入的参数
return originalFunction.apply(context, combinedArgs);
};
};

function greet(name) {
console.log(Hello, ${name}! I am ${this.name}.);
}

const person = { name: “Alice” };
const boundGreet = greet.myBind(person);
boundGreet(“Bob”); // 输出:Hello, Bob! I am Alice.


需要注意,使用 `Symbol` 来确保添加到上下文中的属性不会与现有属性冲突。这只是一种简化的实现,实际的 call、apply 和 bind 方法还包括更多的错误处理和边界情况考虑。


### 什么是异步编程,有何作用?


异步编程是一种编程范式,用于处理在执行过程中需要等待的操作,如网络请求、文件读写、数据库查询等。在传统的同步编程中,代码会按顺序一行一行执行,如果遇到需要等待的操作,整个程序可能会被阻塞,直到操作完成。而异步编程允许程序在执行等待操作的同时继续执行其他任务,以提高程序的效率和响应性。  
**异步编程的作用有以下几点:**


1. `提高响应性:`在等待长时间操作(如网络请求)时,程序不会被阻塞,用户仍然可以与界面交互,提升用户体验。
2. `优化资源利用:`在等待 IO 操作时,CPU 不会被空闲浪费,可以继续执行其他任务,充分利用系统资源。
3. `降低延迟:`在需要等待的操作完成后,程序可以立即继续执行,而不需要等待整个操作序列完成。
4. `并行处理:`异步编程使得程序能够同时执行多个任务,从而更有效地利用多核处理器。


**在 JavaScript 中,异步编程通常使用以下方式来实现:**


1. `回调函数:`将需要在操作完成后执行的代码封装在一个回调函数中,并在操作完成后调用回调函数。
2. `Promise:`通过 Promise 对象封装异步操作,并通过链式调用 .then() 方法处理操作完成后的结果。
3. `async/await:`使用 async 和 await 关键字来编写更具同步风格的异步代码,使代码更易读和维护。
4. `事件监听:`将需要在操作完成时执行的代码绑定到事件,当事件触发时执行相应的代码。


异步编程对于处理现代 Web 应用中的网络请求、数据库查询、文件操作等任务非常重要。它能够提高应用的性能、响应性和用户体验,同时还有助于避免程序因等待操作而阻塞。


### 什么是Promise,如何使用?


Promise 是 JavaScript 中处理异步操作的一种机制,它表示一个异步操作的最终完成(或失败),并可以在操作完成后进行处理。Promise 提供了一种更优雅和结构化的方式来处理回调地狱(Callback Hell),使异步代码更加可读和可维护。  
**一个 Promise 可以处于以下三个状态之一:**


1. `Pending(进行中):`初始状态,表示异步操作正在进行中。
2. `Fulfilled(已完成):`表示异步操作已成功完成,结果值可用。
3. `Rejected(已拒绝):`表示异步操作失败,错误信息可用。


**使用 Promise 的基本流程如下:**


1. 创建一个 Promise 对象,传入一个执行函数,该函数接收两个参数:resolve 和 reject。
2. 在执行函数中执行异步操作,并根据操作结果调用 resolve 或 reject。


**以下是一个简单的使用 Promise 的示例:**



function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
const data = { message: “Data fetched successfully” };
if (data) {
resolve(data); // 成功时调用 resolve 并传递数据
} else {
reject(new Error(“Failed to fetch data”)); // 失败时调用 reject 并传递错误信息
}
}, 1000);
});
}

// 使用 Promise
fetchData()
.then((result) => {
console.log(result.message); // 处理成功的情况
})
.catch((error) => {
console.error(error.message); // 处理失败的情况
});


在这个示例中,fetchData 函数返回一个 Promise 对象。通过 .then() 方法处理异步操作成功的情况,通过 .catch() 方法处理异步操作失败的情况。


### 如何自己实现一个Promise?


实现一个完整的 Promise 并不是一件简单的事情,因为 Promise 的内部需要处理异步操作的状态管理、回调函数的注册和调用、错误处理等复杂逻辑。以下是一个简化版本的 Promise 实现,用于演示其基本原理:



class MyPromise {
constructor(executor) {
this.state = “pending”; // 初始状态
this.value = undefined; // 保存异步操作的结果
this.callbacks = []; // 存储成功和失败回调

const resolve = (value) => {
  if (this.state === "pending") {
    this.state = "fulfilled"; // 成功状态
    this.value = value;

    this.callbacks.forEach(callback => callback.onFulfilled(value));
  }
};

const reject = (reason) => {
  if (this.state === "pending") {
    this.state = "rejected"; // 失败状态
    this.value = reason;

    this.callbacks.forEach(callback => callback.onRejected(reason));
  }
};

try {
  executor(resolve, reject);
} catch (error) {
  reject(error);
}

}

then(onFulfilled, onRejected) {
if (this.state === “fulfilled”) {
onFulfilled(this.value);
} else if (this.state === “rejected”) {
onRejected(this.value);
} else if (this.state === “pending”) {
this.callbacks.push({ onFulfilled, onRejected });
}
}
}

// 使用自己实现的 Promise
const promise = new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve(“Promise resolved”);
// reject(“Promise rejected”);
}, 1000);
});

promise.then(
value => console.log(“Fulfilled:”, value),
reason => console.error(“Rejected:”, reason)
);


这只是一个简化的 Promise 实现,它涵盖了 pending、fulfilled 和 rejected 状态以及 then 方法的基本逻辑。实际的 Promise 还需要考虑更多的细节,如链式调用、错误处理、异步操作的处理等。要在生产环境中使用,建议使用原生的 Promise 或第三方库来获得更完善和稳定的功能。


### 什么是async/await,如何使用?


async/await 是 JavaScript 中用于处理异步操作的一种现代化的语法特性。它使得异步代码的编写和理解更接近于同步代码,使得异步操作的流程更加清晰和可读。async/await 是建立在 Promise 之上的一种语法糖,用于更方便地处理 Promise 的链式调用。  
`async:`


1. async 是一个关键字,用于定义一个函数,该函数返回一个 Promise 对象。
2. async 函数内部可以包含 await 表达式,用于暂停函数的执行,直到 await 后的表达式完成并返回结果。


`await:`


1. await 是一个关键字,只能在 async 函数内部使用。
2. await 后面跟着一个返回 Promise 对象的表达式,该表达式可以是异步函数调用、Promise 对象等。
3. 在 await 后的表达式完成之前,函数的执行会被暂停,直到 Promise 对象状态变为 fulfilled。


使用 async/await 的基本示例:



function fetchData() {
return new Promise(resolve => {
setTimeout(() => {
resolve(“Data fetched successfully”);
}, 1000);
});
}

async function fetchAndPrintData() {
try {
const data = await fetchData(); // 等待异步操作完成
console.log(data);
} catch (error) {
console.error(“Error:”, error);
}
}

fetchAndPrintData(); // 调用 async 函数


在这个示例中,fetchAndPrintData 是一个 async 函数,内部使用 await 暂停了执行,直到 fetchData 异步操作完成。使用 try/catch 来处理异步操作的结果或错误。


### async/await和Promise有何区别,有何优势?


async/await 和 Promise 都是用于处理异步操作的方式,但它们在语法和使用上有一些区别,以及一些优势和劣势。  
**区别:**  
`语法:`


1. Promise 使用链式调用的方式,通过 .then() 和 .catch() 来处理异步操作的结果和错误。
2. async/await 使用更类似同步代码的语法,通过 async 关键字定义异步函数,内部使用 await 暂停执行,并使用 try/catch 处理错误。


`返回值:`


1. Promise 的 .then() 和 .catch() 方法返回的仍然是 Promise 对象,允许进行链式调用。
2. async/await 中,await 后的表达式返回的是 Promise 的结果,而 async 函数本身返回的也是一个 Promise 对象。


**优势:**  
`可读性:`  
 async/await 的语法更接近同步代码,使异步操作的流程更加清晰和易读,减少了回调地狱(Callback Hell)的问题。  
`错误处理:`  
 在 Promise 中,错误处理需要使用 .catch() 方法,而 async/await 可以使用 try/catch 来处理错误,使得错误处理更加直观和统一。  
`调试:`  
 async/await 在调试时更容易跟踪代码执行流程,因为它更接近同步代码的写法。  
`链式调用:`  
 Promise 的链式调用对于一系列的异步操作很有用,但可能会造成代码深度嵌套,难以维护。  
 async/await 也可以链式调用,但代码的可读性更高,不易产生回调地狱。  
`并发:`  
 使用 Promise.all() 可以同时处理多个 Promise,在 async/await 中可以使用 Promise.all() 实现类似的功能。  
**选择使用时的考虑:**


1. 如果代码中已经广泛使用了 Promise,并且不打算重构,那么继续使用 Promise 可能更合适。
2. 如果希望代码更易读、易于调试,并且要处理复杂的异步操作流程,那么可以考虑使用 async/await。


需要注意的是,async/await 内部仍然基于 Promise,因此它们并不是互斥的,而是可以相互配合使用的。


### 怎么理解JavaScript中的面向对象?


JavaScript 中的面向对象(Object-Oriented)编程是一种编程范式,它将程序的组织方式从简单的函数和数据结构转变为更加模块化和抽象化的方式。在面向对象编程中,程序被组织为一组对象,每个对象都具有属性(数据)和方法(函数),这些对象可以相互交互和协作,从而构建出更复杂的系统。  
**面向对象编程的主要概念包括:**


1. `类(Class):`类是对象的蓝图或模板,定义了对象的属性和方法。类描述了对象的特征和行为,可以看作是一个抽象的概念。
2. `对象(Object):`对象是类的实例,具有类定义的属性和方法。对象是面向对象编程中的基本单位,代表现实世界中的实体。
3. `封装(Encapsulation):`封装是将数据和操作封装在一个对象中,隐藏对象的内部细节,只暴露必要的接口供外部使用。
4. `继承(Inheritance):`继承是通过创建一个新的类来扩展已有的类,子类继承了父类的属性和方法,并可以添加自己的属性和方法。
5. `多态(Polymorphism):`多态允许不同的类实现相同的接口或方法,并可以以相同的方式进行调用,提高了代码的灵活性和可复用性。


在 JavaScript 中,虽然它是一门基于原型的编程语言,但也支持面向对象编程。通过使用构造函数、原型链、类、继承等特性,可以在 JavaScript 中实现面向对象的编程风格。ES6 引入了更现代的类和继承语法,使得 JavaScript 的面向对象编程更加清晰和直观。  
 以下是一个简单的 JavaScript 面向对象编程的示例:



// 定义一个类
class Animal {
constructor(name) {
this.name = name;
}

speak() {
console.log(${this.name} makes a sound.);
}
}

// 定义一个子类继承自 Animal
class Dog extends Animal {
speak() {
console.log(${this.name} barks.);
}
}

// 创建对象并调用方法
const dog = new Dog(“Buddy”);
dog.speak(); // 输出:Buddy barks.


#### **前端面试题库 (**面试必备)****推荐:★★★★★****


地址:[前端面试题库](https://bbs.csdn.net/topics/618166371)


### JavaScript中的垃圾回收机制是什么?


JavaScript 中的垃圾回收(Garbage Collection)是一种自动管理内存的机制,用于检测和回收不再使用的内存,以防止内存泄漏和资源浪费。在 JavaScript 中,开发人员不需要手动释放内存,因为垃圾回收机制会自动处理不再使用的内存对象。  
**JavaScript 中的垃圾回收主要基于以下两个概念:**  
`引用计数垃圾回收:`


1. 引用计数是一种最简单的垃圾回收机制,它通过维护每个对象的引用计数来判断对象是否可达。
2. 当一个对象被引用时,它的引用计数加一;当一个对象的引用被移除时,它的引用计数减一。
3. 如果一个对象的引用计数变为零,说明没有任何引用指向它,该对象不再可达,垃圾回收机制会将其回收。


`标记-清除垃圾回收:`


1. 标记-清除是一种更高级的垃圾回收算法,通过判断对象是否可达来确定对象是否应该回收。
2. 垃圾回收器首先从根对象(如全局对象、活动函数的局部变量等)开始,标记所有可达对象。
3. 之后,垃圾回收器会遍历所有对象,清除没有被标记为可达的对象,将其内存回收。


需要注意的是,引用计数垃圾回收在解决循环引用(两个对象相互引用)方面存在问题,因为即使对象之间相互引用,它们的引用计数也不会变为零。而标记-清除垃圾回收则能够处理循环引用。  
 现代 JavaScript 引擎通常使用基于标记-清除的垃圾回收机制。例如,V8 引擎(Chrome、Node.js)采用了分代垃圾回收策略,将对象分为新生代和老生代,以更高效地进行垃圾回收。垃圾回收是一种重要的机制,它确保在代码执行期间不会出现内存泄漏问题,提高了应用的性能和稳定性。


### JavaScript有哪些情况会导致内存泄漏?


JavaScript 中的内存泄漏是指程序中已不再使用的内存没有被正确释放,导致内存占用不断增加,最终可能导致应用程序的性能下降甚至崩溃。以下是一些可能导致内存泄漏的常见情况:  
`循环引用:`  
 当两个或多个对象彼此相互引用,而且没有其他对象引用它们,就会形成循环引用。这会阻止垃圾回收器回收这些对象,导致内存泄漏。  
`未清理的定时器和事件监听:`  
 如果使用 setTimeout、setInterval 或 addEventListener 注册了定时器或事件监听器,但在不再需要时未进行清理,这些定时器和监听器将继续持有对象的引用,阻止垃圾回收。  
`闭包:`  
 闭包可以使函数内部的变量在函数执行结束后仍然被引用,导致这些变量的内存无法释放。特别是在循环中创建闭包时,可能会导致内存泄漏。  
`未使用的全局变量:`  
 如果创建了全局变量,但没有及时销毁或解除引用,这些变量将继续占用内存,即使在不再需要它们的情况下也是如此。  
`DOM 元素引用:`  
 在 JavaScript 中,保留对 DOM 元素的引用,即使这些元素从页面中删除,也会导致内存泄漏。必须确保在不需要 DOM 元素时解除引用。  
`大量缓存:`  
 缓存数据和对象可以提高性能,但如果缓存不受限制地增长,会导致内存泄漏。必须定期清理缓存,删除不再需要的数据。  
`忘记释放资源:`  
 例如,在使用了底层资源(如数据库连接、文件句柄等)后,没有显式地关闭或释放这些资源,可能导致资源泄漏。  
`循环引用的事件处理器:`  
 如果在 DOM 元素上注册了事件处理器,而这些事件处理器引用了其他对象,且这些对象又引用了相同的 DOM 元素,可能会导致循环引用,阻止垃圾回收。  
 为了避免内存泄漏,开发者应该注意适时释放不再需要的资源、解除引用,确保定时器和事件监听器的正确清理,以及注意循环引用的情况。使用浏览器的开发者工具和内存分析工具可以帮助识别内存泄漏问题。


### 什么是防抖,节流函数,如何实现?


防抖(Debouncing)和节流(Throttling)是两种常用的优化技术,用于限制频繁触发的事件的执行次数,从而提升性能和用户体验。


1. `防抖`是指在事件触发后,等待一段时间(例如 200 毫秒),如果在这段时间内没有再次触发事件,那么执行相应的操作。如果在等待时间内又触发了事件,那么等待时间会被重置。
2. `节流`是指在事件触发后,一段时间内只执行一次相应的操作。无论触发多少次事件,都会在每个固定的时间间隔内执行一次。


**防抖的实现:**



function debounce(func, delay) {
let timerId;
return function (…args) {
clearTimeout(timerId);
timerId = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}

// 使用示例
const debouncedFn = debounce(() => {
console.log(‘Debounced function called’);
}, 200);

window.addEventListener(‘scroll’, debouncedFn);


**节流的实现:**



function throttle(func, delay) {
let lastTime = 0;
return function (…args) {
const currentTime = new Date().getTime();
if (currentTime - lastTime >= delay) {
func.apply(this, args);
lastTime = currentTime;
}
};
}

// 使用示例
const throttledFn = throttle(() => {
console.log(‘Throttled function called’);
}, 200);

window.addEventListener(‘scroll’, throttledFn);



### 浅拷贝,深拷贝是什么?如何实现一个深拷贝函数?


浅拷贝和深拷贝是两种不同的对象复制方式,涉及到对象的引用关系。下面分别对浅拷贝和深拷贝进行解释:


1. `浅拷贝:`在浅拷贝中,创建一个新对象,然后将原始对象的属性值复制到新对象中。如果属性值是基本类型,那么直接复制其值;如果属性值是对象或数组等引用类型,那么只是复制其引用,新对象和原始对象共享这些引用。浅拷贝只复制对象的一层属性。
2. `深拷贝:`在深拷贝中,递归地复制一个对象及其嵌套的所有属性,直到所有嵌套的属性都是基本类型为止。这样创建了一个完全独立的对象副本,原始对象和新对象之间没有任何引用关系。


**以下是一个简单的深拷贝函数的示例:**



function deepClone(obj) {
if (obj === null || typeof obj !== ‘object’) {
return obj;
}

let clone = Array.isArray(obj) ? [] : {};

for (let key in obj) {
if (obj.hasOwnProperty(key)) {
clone[key] = deepClone(obj[key]);
}
}

return clone;
}

const originalObj = {
a: 1,
b: { c: 2 },
d: [3, 4]
};

const copiedObj = deepClone(originalObj);
console.log(copiedObj); // 深拷贝后的新对象
console.log(copiedObj === originalObj); // false,新对象和原始对象无关联
console.log(copiedObj.b === originalObj.b); // false,嵌套对象也是深拷贝


需要注意的是,上述深拷贝函数只是一个简单的实现示例,可能在某些特定情况下存在性能问题。在实际项目中,可以使用成熟的深拷贝工具库,如 lodash 的 cloneDeep 方法,来处理深拷贝的需求。


### 深拷贝如何解决循环引用的问题?


深拷贝是在创建一个对象的副本时,递归地复制其所有嵌套属性和子对象。在深拷贝过程中,如果对象存在循环引用,即某个对象引用了自身或与其他对象形成了循环引用关系,那么简单的递归拷贝可能会导致无限循环,甚至造成内存溢出。  
**为了解决循环引用问题,你可以在深拷贝过程中使用一些策略。以下是一种常见的方法**


1. 使用一个缓存对象来跟踪已经被拷贝的对象,以及它们在新对象中的引用。这可以防止无限递归。
2. 在每次拷贝一个对象之前,先检查缓存对象,如果该对象已经被拷贝,则直接返回缓存中的引用,而不是递归拷贝。


**下面是一个使用JavaScript实现深拷贝并解决循环引用问题的示例**



function deepCopyWithCycles(obj, cache = new WeakMap()) {
if (obj === null || typeof obj !== ‘object’) {
return obj;
}

if (cache.has(obj)) {
    return cache.get(obj);
}

if (obj instanceof Date) {
    return new Date(obj);
}

if (obj instanceof RegExp) {
    return new RegExp(obj);
}

const copy = Array.isArray(obj) ? [] : {};

cache.set(obj, copy);

for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
        copy[key] = deepCopyWithCycles(obj[key], cache);
    }
}

return copy;

}

const objA = {
name: ‘Object A’
};

const objB = {
name: ‘Object B’
};

objA.circularRef = objA;
objB.circularRef = objA;

const copiedObjA = deepCopyWithCycles(objA);

console.log(copiedObjA.name); // 输出: Object A
console.log(copiedObjA.circularRef.name); // 输出: Object A
console.log(copiedObjA.circularRef === copiedObjA); // 输出: true
console.log(copiedObjA.circularRef === copiedObjA.circularRef.circularRef); // 输出: true


在上面的示例中,deepCopyWithCycles 函数使用了一个 WeakMap 来缓存已经拷贝的对象,以及它们在新对象中的引用。这样,即使对象存在循环引用,也可以正确地处理。同时,这个函数也考虑了处理特殊类型如 Date 和 RegExp。


### 什么是事件委托,为什么要使用事件委托?


事件委托(Event Delegation)是一种在 Web 开发中常用的事件处理技术,它通过将事件监听器绑定到父元素而不是每个子元素上,以达到优化性能、简化代码和处理动态内容的目的。  
 事件委托的原理是利用了事件冒泡机制。当子元素上的事件被触发时,该事件会向上冒泡到父元素,父元素可以捕获并处理这个事件。通过在父元素上监听事件,可以捕获到子元素触发的事件,从而实现对子元素的事件处理。  
**事件委托的优势和原因:**


1. `减少事件监听器的数量:`当页面上存在大量的子元素时,为每个子元素都绑定事件监听器会增加内存占用和性能开销。使用事件委托可以减少事件监听器的数量,只需在父元素上绑定一个监听器。
2. `动态添加的元素也能被处理:`对于通过 JavaScript 动态添加到页面的元素,无需再次绑定事件监听器,因为它们是父元素的后代,事件会冒泡到父元素。
3. `性能优化:`由于事件监听器较少,减少了事件冒泡的层级,可以提升页面的响应速度和性能。
4. `方便维护:`在有大量相似子元素的情况下,使用事件委托可以简化代码,使代码更易维护和理解。


### 如何实现数组扁平化?


1. 递归方法:使用递归遍历数组的每个元素,如果元素是数组,则递归处理;如果是基本类型,则添加到结果数组中。



function flattenArray(arr) {
const result = [];
for (const item of arr) {
if (Array.isArray(item)) {
result.push(…flattenArray(item));
} else {
result.push(item);
}
}
return result;
}

const nestedArray = [1, [2, 3, [4, 5]], 6];
const flattenedArray = flattenArray(nestedArray);
console.log(flattenedArray); // [1, 2, 3, 4, 5, 6]



### 文末
篇幅有限没有列举更多的前端面试题,小编把整理的前端大厂面试题PDF分享出来,一共有269页

![](https://img-blog.csdnimg.cn/img_convert/04273c2f50e58b6822663c5b2f379206.png)  

![](https://img-blog.csdnimg.cn/img_convert/081b5236ebd9ff24af676bcd4a51c99e.png)

**[开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】](https://bbs.csdn.net/topics/618166371)**
  • 21
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值