ES6 面试题及详细答案 80题 (62-80)-- 模块化与其他特性
《前后端面试题
》专栏集合了前后端各个知识模块的面试题,包括html,javascript,css,vue,react,java,Openlayers,leaflet,cesium,mapboxGL,threejs,nodejs,mangoDB,SQL,Linux… 。
文章目录
- 一、本文面试题目录
- 62. ES6模块与CommonJS模块的区别是什么?
- 63. 如何使用export导出模块成员?有哪些导出方式?
- 64. import命令的作用是什么?如何导入模块?
- 基本用法:
- 65. 什么是默认导出(export default)?与命名导出有何区别?
- 与命名导出的区别:
- 66. 如何实现模块的动态导入(dynamic import)?
- 基本用法:
- 与`async/await`结合(更简洁):
- 应用场景:
- 67. 模块的循环依赖问题如何解决?
- 1. ES6模块的自然处理
- 2. CommonJS的处理与解决
- 通用原则:
- 68. 什么是迭代器(Iterator)?它有什么作用?
- 迭代器的结构:
- 作用:
- 69. 可迭代对象(Iterable)有哪些?如何判断一个对象是否可迭代?
- 常见的可迭代对象:
- 判断对象是否可迭代:
- 70. for...of循环与for...in循环的区别是什么?
- 71. 什么是Proxy?它可以实现哪些功能?
- 基本语法:
- 可实现的功能:
- 72. Reflect对象的作用是什么?它与Object方法有何区别?
- 作用:
- 与Object方法的区别:
- 73. ES6的新增数据结构中,哪些可以用于解决内存泄漏问题?为什么?
- 弱引用的特性:
- 应用场景:
- 74. 什么是对象的可枚举性(enumerable)?如何判断属性是否可枚举?
- 特性说明:
- 判断属性是否可枚举:
- 75. 什么是Promise的穿透?如何避免?
- 示例:
- 如何避免:
- 76. Generator函数如何实现异步操作?
- 实现原理:
- 示例(手动执行):
- 自动执行(使用co模块):
- 77. Generator函数与Promise结合使用有什么优势?
- 78. 什么是数组的迭代方法?ES6新增了哪些?
- ES6新增的数组迭代方法:
- 79. 什么是对象的浅拷贝和深拷贝?ES6中如何实现?
- 浅拷贝(Shallow Copy):
- 深拷贝(Deep Copy):
- ES6中实现方式:
- 80. async/await相比Promise有哪些优势?
- 二、80道ES6 面试题目录列表
一、本文面试题目录
62. ES6模块与CommonJS模块的区别是什么?
ES6模块(ESM)和CommonJS(CJS)是JavaScript的两种模块规范,核心区别如下:
特性 | ES6模块 | CommonJS模块 |
---|---|---|
语法 | 使用import /export | 使用require() /module.exports |
加载时机 | 编译时静态分析(预加载) | 运行时动态加载 |
输出方式 | 输出值的引用(动态绑定) | 输出值的拷贝(静态绑定) |
this指向 | 模块顶层this 为undefined | 模块顶层this 指向当前模块 |
运行环境 | 浏览器和Node.js(需配置) | 主要用于Node.js |
循环依赖 | 处理更优(基于引用) | 可能导致未完成的模块 |
异步加载 | 支持import() 动态异步加载 | 同步加载(阻塞执行) |
示例:
// ES6模块(导出)
export const a = 1;
export function fn() {}// ES6模块(导入)
import { a, fn } from './module.js';// CommonJS(导出)
module.exports = { a: 1, fn() {} };// CommonJS(导入)
const { a, fn } = require('./module.js');
63. 如何使用export导出模块成员?有哪些导出方式?
export
用于将模块内的成员暴露给外部,主要有以下导出方式:
-
命名导出(单个导出)
直接在声明前加export
:export const name = "模块"; export function greet() { return "Hello"; } export class MyClass {}
-
命名导出(集中导出)
先声明成员,再集中导出:const age = 18; function add(a, b) { return a + b; }export { age, add }; // 注意用大括号包裹
-
导出时重命名
使用as
关键字重命名导出的成员:const a = 10; export { a as num, add as sum }; // 外部需用num和sum导入
-
默认导出(export default)
每个模块只能有一个默认导出,用于导出模块的主要成员:// 方式1:直接导出 export default function() { return "默认导出"; }// 方式2:先声明后导出 const defaultObj = { name: "默认" }; export default defaultObj;
注意:命名导出和默认导出可同时使用,但默认导出只能有一个。
64. import命令的作用是什么?如何导入模块?
import
命令用于从其他模块导入成员,实现模块间的依赖管理。
基本用法:
-
导入命名导出的成员
使用{}
指定要导入的成员名,需与导出时一致:// 导入单个或多个成员 import { name, greet } from './module.js';// 导入时重命名(避免冲突) import { name as moduleName, greet as sayHello } from './module.js';
-
导入默认导出的成员
无需大括号,可自定义名称:import myFunc from './module.js'; // 导入默认导出的函数 import defaultObj from './other.js'; // 导入默认导出的对象
-
同时导入命名成员和默认成员
import defaultFunc, { name, age } from './module.js';
-
导入整个模块(命名空间导入)
使用* as
将模块所有成员导入为一个对象:import * as Module from './module.js'; console.log(Module.name); // 访问命名导出成员 Module.greet(); // 调用命名导出方法
注意:
import
命令具有提升性,会被提升到模块顶部执行。- 浏览器中使用时,脚本标签需添加
type="module"
:<script type="module" src="main.js"></script>
65. 什么是默认导出(export default)?与命名导出有何区别?
默认导出(export default) 是模块的主要导出方式,用于指定模块的默认成员,每个模块只能有一个默认导出。
与命名导出的区别:
特性 | 默认导出(export default) | 命名导出(export) |
---|---|---|
数量限制 | 每个模块只能有1个 | 每个模块可以有多个 |
导入语法 | 无需大括号,可自定义名称 | 必须用{} ,且名称需与导出一致(或用as 重命名) |
导出场景 | 模块主要功能(如一个核心函数/类) | 模块的次要功能或多个成员 |
示例 | export default function() {} | export const a = 1; |
导入示例 | import myFunc from './m.js' | import { a } from './m.js' |
示例:
// 模块 file.js
// 默认导出(1个)
export default class User {constructor(name) { this.name = name; }
}// 命名导出(多个)
export const version = "1.0";
export function formatName(name) { return name.toUpperCase(); }// 导入模块
import User, { version, formatName } from './file.js';
const user = new User("Alice");
console.log(version); // "1.0"
66. 如何实现模块的动态导入(dynamic import)?
动态导入(import()
)是ES2020引入的特性,允许在运行时异步加载模块,返回一个Promise。
基本用法:
// 动态导入模块,返回Promise
import('./module.js').then((module) => {// 模块加载成功,使用导出的成员console.log(module.name);module.greet();}).catch((error) => {// 处理加载失败console.error("模块加载失败:", error);});
与async/await
结合(更简洁):
async function loadModule() {try {const module = await import('./module.js');module.doSomething();} catch (error) {console.error(error);}
}loadModule();
应用场景:
-
按需加载:只在需要时加载模块(如点击按钮后),减少初始加载时间。
document.getElementById('btn').addEventListener('click', async () => {const module = await import('./heavy-module.js');module.execute(); });
-
条件加载:根据不同条件加载不同模块。
async function loadFeature(feature) {let module;if (feature === 'chart') {module = await import('./chart.js');} else {module = await import('./table.js');}module.render(); }
注意:动态导入返回的是模块对象的Promise,与静态import
的编译时加载不同。
67. 模块的循环依赖问题如何解决?
模块循环依赖指A模块依赖B模块,同时B模块依赖A模块的情况。ES6模块和CommonJS通过不同机制处理,解决方式如下:
1. ES6模块的自然处理
ES6模块基于“引用绑定”,循环依赖时:
- 模块在加载过程中会先创建一个“未完成”的模块实例。
- 依赖方可以访问已声明的成员,未声明的成员暂时为
undefined
。
解决关键:将依赖使用放在成员声明之后。
示例:
// a.js
import { b } from './b.js';
export const a = 1;
console.log('a.js中b的值:', b); // 可访问b(此时b可能为undefined,取决于执行顺序)// b.js
import { a } from './a.js';
export const b = 2;
console.log('b.js中a的值:', a); // 可访问a
2. CommonJS的处理与解决
CommonJS基于“值拷贝”,循环依赖时可能获取到未完成的模块导出。
解决方式:
- 将依赖引用延迟到函数内部(避免模块加载时立即访问)。
// a.js const { b } = require('./b.js'); exports.a = 1; // 延迟访问b exports.logB = () => console.log(b);// b.js const { a, logB } = require('./a.js'); exports.b = 2; console.log(a); // undefined(此时a尚未导出) logB(); // 调用时a已导出,可正常访问b
通用原则:
- 减少模块间的耦合,避免不必要的循环依赖。
- 将共享逻辑提取到第三方模块,打破循环。
- 延迟使用依赖(如在函数中访问,而非模块顶层)。
68. 什么是迭代器(Iterator)?它有什么作用?
迭代器(Iterator) 是ES6引入的一种接口机制,为各种数据结构提供统一的遍历方式。
迭代器的结构:
迭代器是一个对象,必须包含next()
方法,该方法返回一个对象{ value: 当前值, done: 是否完成 }
:
value
:当前遍历到的值。done
:布尔值,true
表示遍历结束。
作用:
- 统一遍历接口:让不同数据结构(数组、Set、Map等)可以用相同的方式遍历。
- 支持
for...of
循环:所有可迭代对象(实现迭代器接口)都能被for...of
遍历。 - 惰性计算:迭代器按需生成值,适合处理大数据或无限序列。
示例:自定义迭代器
// 生成1-5的迭代器
const createIterator = () => {let count = 1;return {next() {if (count <= 5) {return { value: count++, done: false };}return { value: undefined, done: true };}};
};const iterator = createIterator();
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
// ... 直到done为true
69. 可迭代对象(Iterable)有哪些?如何判断一个对象是否可迭代?
可迭代对象(Iterable) 是指实现了迭代器接口([Symbol.iterator]
方法)的数据结构。
常见的可迭代对象:
- 原生数据结构:数组(Array)、字符串(String)、Set、Map。
- 类数组对象: arguments、NodeList。
- 自定义实现
[Symbol.iterator]
的对象。
示例:
// 数组是可迭代对象
for (const item of [1, 2, 3]) { console.log(item); }// 字符串是可迭代对象
for (const char of "hello") { console.log(char); }// Set是可迭代对象
for (const val of new Set([1, 2])) { console.log(val); }
判断对象是否可迭代:
检查对象是否有[Symbol.iterator]
属性,且该属性是函数:
function isIterable(obj) {return obj != null && typeof obj[Symbol.iterator] === 'function';
}console.log(isIterable([1, 2])); // true(数组)
console.log(isIterable("test")); // true(字符串)
console.log(isIterable({})); // false(普通对象默认不可迭代)
自定义可迭代对象:
const myIterable = {items: [10, 20, 30],[Symbol.iterator]() {let index = 0;return {next: () => {if (index < this.items.length) {return { value: this.items[index++], done: false };}return { done: true };}};}
};// 可被for...of遍历
for (const item of myIterable) {console.log(item); // 10 → 20 → 30
}
70. for…of循环与for…in循环的区别是什么?
for...of
和for...in
是两种不同的循环方式,核心区别如下:
特性 | for…of循环 | for…in循环 |
---|---|---|
遍历对象 | 可迭代对象(数组、Set、Map等) | 普通对象(遍历键名) |
遍历内容 | 遍历值(value) | 遍历键名(key) |
适用场景 | 遍历数据结构的元素 | 遍历对象的属性 |
是否遍历原型链 | 不遍历 | 会遍历继承的可枚举属性 |
数组索引 | 不关注索引,直接取元素 | 遍历数组索引(字符串类型) |
示例 | for (const item of arr) {} | for (const key in obj) {} |
示例:
const arr = [10, 20, 30];
const obj = { a: 1, b: 2 };// for...of遍历数组的值
for (const item of arr) {console.log(item); // 10 → 20 → 30
}// for...in遍历对象的键名
for (const key in obj) {console.log(key); // "a" → "b"
}// for...in遍历数组(不推荐,会遍历索引和可能的扩展属性)
Array.prototype.extra = "test";
for (const key in arr) {console.log(key); // "0" → "1" → "2" → "extra"(继承的属性)
}
最佳实践:
- 遍历数组、Set、Map等用
for...of
。 - 遍历普通对象的属性用
for...in
,且通常配合hasOwnProperty
过滤继承属性:for (const key in obj) {if (obj.hasOwnProperty(key)) {console.log(key); // 只输出自身属性} }
71. 什么是Proxy?它可以实现哪些功能?
Proxy 是ES6引入的特性,用于创建一个对象的代理,从而实现对对象的拦截和自定义操作(如属性访问、赋值、删除等)。
基本语法:
const proxy = new Proxy(target, handler);
target
:被代理的目标对象。handler
:拦截器对象,定义各种操作的拦截方法。
可实现的功能:
-
属性访问拦截(get)
拦截对象属性的读取操作。const obj = { name: "Alice" }; const proxy = new Proxy(obj, {get(target, prop) {if (prop in target) {return target[prop];}return `属性${prop}不存在`; // 自定义不存在属性的返回值} }); console.log(proxy.name); // "Alice" console.log(proxy.age); // "属性age不存在"
-
属性赋值拦截(set)
拦截对象属性的赋值操作,可用于数据验证。const user = { age: 0 }; const userProxy = new Proxy(user, {set(target, prop, value) {if (prop === "age" && (value < 0 || value > 150)) {throw new Error("年龄必须在0-150之间");}target[prop] = value;return true; // 表示赋值成功} }); userProxy.age = 25; // 成功 // userProxy.age = 200; // 报错
-
其他拦截操作
deleteProperty
:拦截属性删除。has
:拦截in
运算符。apply
:拦截函数调用。construct
:拦截new
操作符。
示例(拦截函数调用):
const sum = (a, b) => a + b;
const sumProxy = new Proxy(sum, {apply(target, thisArg, args) {console.log(`调用sum(${args[0]}, ${args[1]})`);return target(...args) * 2; // 拦截并修改返回值}
});
console.log(sumProxy(1, 2)); // 输出:调用sum(1, 2) → 6(原结果3×2)
应用场景:数据验证、日志记录、权限控制、虚拟DOM实现等。
72. Reflect对象的作用是什么?它与Object方法有何区别?
Reflect 是ES6引入的内置对象,提供了一系列操作对象的方法,与Object
的部分方法功能相似,但设计更统一。
作用:
- 统一对象操作接口:将对象的操作(如属性访问、赋值、删除等)集中到
Reflect
对象上。 - 替代
Object
的部分方法:提供更合理的返回值和参数设计。 - 配合Proxy使用:
Reflect
方法的参数与Proxy拦截器的参数一致,便于在拦截中调用原始操作。
与Object方法的区别:
特性 | Reflect | Object |
---|---|---|
返回值 | 操作是否成功(如Reflect.set() 返回布尔值) | 通常返回操作后的对象或undefined |
参数顺序 | 目标对象在前,属性名在后(如Reflect.get(obj, prop) ) | 部分方法参数顺序不同(如Object.getOwnPropertyDescriptor(obj, prop) ) |
函数式风格 | 全部为函数方法(如Reflect.has(obj, prop) 替代prop in obj ) | 混合函数方法和运算符 |
报错处理 | 操作失败返回false (不报错) | 操作失败可能抛出错误 |
与Proxy配合 | 完美匹配Proxy拦截器的参数,便于转发操作 | 需手动处理参数适配 |
示例:
const obj = { name: "Bob" };// 1. 属性赋值
console.log(Reflect.set(obj, "age", 20)); // true(操作成功)
console.log(obj.age); // 20// 2. 属性检查(替代in运算符)
console.log(Reflect.has(obj, "name")); // true// 3. 与Proxy配合
const proxy = new Proxy(obj, {get(target, prop) {// 用Reflect转发原始操作return Reflect.get(target, prop);}
});
73. ES6的新增数据结构中,哪些可以用于解决内存泄漏问题?为什么?
ES6中WeakSet和WeakMap可用于解决内存泄漏问题,核心原因是它们对对象的引用是弱引用。
弱引用的特性:
- 不会阻止垃圾回收(GC):若对象仅被WeakSet/WeakMap引用,且无其他强引用,该对象会被GC回收,对应的键/值也会从集合中自动移除。
- 无法枚举:没有
size
属性和遍历方法,避免依赖可能被回收的元素。
应用场景:
-
临时存储DOM节点
避免删除DOM节点后,因仍被Set/Map引用而无法回收:const wm = new WeakMap(); const div = document.createElement('div'); wm.set(div, '临时数据'); // 弱引用DOM节点// 当div被移除且无其他引用时,会被GC回收,wm中对应的键值对也会消失
-
缓存对象数据
缓存不会阻止原对象被回收:const cache = new WeakMap(); function getCache(obj) {if (!cache.has(obj)) {cache.set(obj, computeData(obj)); // 缓存计算结果}return cache.get(obj); }
相比之下,Set和Map对对象的引用是强引用,即使对象在外部无引用,只要被Set/Map引用就不会被回收,可能导致内存泄漏。
74. 什么是对象的可枚举性(enumerable)?如何判断属性是否可枚举?
可枚举性(enumerable) 是对象属性的一个特性,决定该属性是否会被for...in
循环、Object.keys()
等方法遍历到。
特性说明:
- 可枚举属性:会被遍历方法(如
for...in
、Object.keys()
)包含。 - 不可枚举属性:不会被遍历方法包含(如对象的
toString
、hasOwnProperty
等继承属性)。
默认情况下:
- 通过对象字面量定义的属性是可枚举的。
- 通过
Object.defineProperty
定义的属性默认不可枚举。
判断属性是否可枚举:
使用Object.prototype.propertyIsEnumerable(prop)
方法:
const obj = { a: 1 };
// 自定义属性默认可枚举
console.log(obj.propertyIsEnumerable('a')); // true// 继承的属性不可枚举
console.log(obj.propertyIsEnumerable('toString')); // false// 通过defineProperty定义不可枚举属性
Object.defineProperty(obj, 'b', {value: 2,enumerable: false // 显式设置不可枚举
});
console.log(obj.propertyIsEnumerable('b')); // false
console.log(Object.keys(obj)); // ['a'](b未被枚举)
应用场景:筛选对象的可枚举属性,或定义内部使用的不可枚举属性(避免被意外遍历)。
75. 什么是Promise的穿透?如何避免?
Promise的穿透指当then()
方法未传入回调函数(或传入非函数值)时,Promise会将结果“穿透”到下一个then()
的现象。
示例:
Promise.resolve(1).then() // 未传回调.then(2) // 传入非函数.then((value) => {console.log(value); // 输出1(结果穿透到此处)});
原理:then()
方法期望接收函数参数,若未传入,会默认生成一个“透传”函数((value) => value
),将结果传递给下一个then()
。
如何避免:
-
始终传入函数作为回调,即使是空函数:
Promise.resolve(1).then(() => {}) // 显式传入空函数,阻止穿透.then((value) => {console.log(value); // undefined(不再穿透)});
-
使用
catch()
集中处理错误,避免错误穿透到意外的then()
:Promise.reject(new Error("出错")).then() // 错误穿透.catch((error) => {console.log(error.message); // 输出"出错"(正确捕获)});
穿透本身不是bug,而是Promise的设计特性,合理利用可简化代码(如跳过中间处理),但需注意避免意外行为。
76. Generator函数如何实现异步操作?
Generator函数通过yield
暂停执行和next()
恢复执行的特性,可将异步操作以同步的形式编写,实现异步流程控制。
实现原理:
- 将异步操作包装成返回Promise的函数。
- 在Generator函数中用
yield
调用异步操作,暂停等待结果。 - 通过自动执行器(如
co
模块)或手动调用next()
,在异步操作完成后恢复执行,并将结果传入。
示例(手动执行):
// 异步操作函数
function fetchData(url) {return new Promise((resolve) => {setTimeout(() => {resolve(`数据:${url}`);}, 1000);});
}// Generator函数处理异步流程
function* asyncGenerator() {console.log("开始请求");const data1 = yield fetchData("url1"); // 暂停,等待异步结果console.log(data1);const data2 = yield fetchData("url2"); // 继续执行,等待下一个结果console.log(data2);return "完成";
}// 手动执行Generator
const gen = asyncGenerator();
gen.next().value // 启动第一个异步操作.then((result1) => {return gen.next(result1).value; // 将结果传入,执行到下一个yield}).then((result2) => {gen.next(result2); // 完成最后一步});
自动执行(使用co模块):
const co = require('co');
co(asyncGenerator).then((result) => {console.log(result); // "完成"
});
Generator函数让异步代码的逻辑更清晰,避免了Promise的链式调用嵌套。
77. Generator函数与Promise结合使用有什么优势?
Generator函数与Promise结合,结合了两者的优势,成为更优雅的异步编程方案:
-
同步化的异步代码
Generator的yield
可暂停等待Promise完成,让异步代码看起来像同步代码,可读性更高:// 纯Promise(链式调用) fetchData1().then(data1 => {return fetchData2(data1);}).then(data2 => {return fetchData3(data2);});// Generator+Promise(同步风格) function* gen() {const data1 = yield fetchData1();const data2 = yield fetchData2(data1);const data3 = yield fetchData3(data2); }
-
统一的错误处理
可通过try/catch
捕获所有yield
的Promise错误,无需在每个then()
后加catch()
:function* gen() {try {const data1 = yield fetchData1(); // 若失败,直接进入catchconst data2 = yield fetchData2(data1);} catch (error) {console.error("出错:", error); // 统一处理所有错误} }
-
灵活的流程控制
可在next()
方法中传入参数,动态调整异步流程:function* gen() {const action = yield prompt("请输入操作"); // 等待用户输入if (action === "save") {yield saveData(); // 根据输入执行不同异步操作} else {yield loadData();} }
-
天然支持迭代
适合处理序列异步操作(如分页加载数据):function* loadPages() {let page = 1;while (true) {const data = yield fetchPage(page); // 循环加载分页数据if (!data) break;page++;} }
这种组合是async/await
的前身,async/await
可视为Generator+Promise的语法糖。
78. 什么是数组的迭代方法?ES6新增了哪些?
数组的迭代方法指用于遍历数组并对元素执行操作的方法,通常接收回调函数,返回新数组或其他值(不修改原数组)。
ES6新增的数组迭代方法:
-
Array.prototype.find()
返回数组中第一个满足回调函数条件的元素,无则返回undefined
。const numbers = [10, 20, 30, 40]; const found = numbers.find(num => num > 25); console.log(found); // 30
-
Array.prototype.findIndex()
返回数组中第一个满足条件的元素的索引,无则返回-1
。const index = numbers.findIndex(num => num > 25); console.log(index); // 2(30的索引)
-
Array.prototype.includes()
判断数组是否包含指定值,返回布尔值(与indexOf
相比,可识别NaN
)。console.log(numbers.includes(20)); // true console.log(numbers.includes(50)); // falseconst arr = [NaN]; console.log(arr.includes(NaN)); // true(indexOf(NaN)返回-1)
-
Array.prototype.flat()
将嵌套数组“扁平化”,返回新数组(默认扁平化一层)。const nested = [1, [2, [3]]]; console.log(nested.flat()); // [1, 2, [3]](扁平化一层) console.log(nested.flat(2)); // [1, 2, 3](扁平化两层)
-
Array.prototype.flatMap()
先对每个元素执行map
,再对结果执行flat(1)
(相当于map
+flat
的组合)。const words = ["hello world", "es6 features"]; const letters = words.flatMap(word => word.split(" ")); console.log(letters); // ["hello", "world", "es6", "features"]
这些方法增强了数组的遍历和处理能力,使代码更简洁。
79. 什么是对象的浅拷贝和深拷贝?ES6中如何实现?
浅拷贝(Shallow Copy):
只复制对象的顶层属性,若属性值是对象(引用类型),则复制的是引用(修改拷贝后的对象会影响原对象)。
深拷贝(Deep Copy):
完全复制对象的所有层级,包括嵌套对象,拷贝后与原对象完全独立(修改互不影响)。
ES6中实现方式:
-
浅拷贝:
-
扩展运算符(
...
):const obj = { a: 1, b: { c: 2 } }; const shallowCopy = { ...obj }; shallowCopy.b.c = 3; console.log(obj.b.c); // 3(嵌套对象受影响)
-
Object.assign()
:const shallowCopy = Object.assign({}, obj);
-
数组浅拷贝:
const arr = [1, [2, 3]]; const arrCopy = [...arr]; // 或 arr.slice()
-
-
深拷贝:
-
简易实现(JSON方法):
适用于无函数、Date
、RegExp
等特殊类型的对象。const obj = { a: 1, b: { c: 2 } }; const deepCopy = JSON.parse(JSON.stringify(obj)); deepCopy.b.c = 3; console.log(obj.b.c); // 2(不受影响)
-
自定义递归函数:
处理特殊类型(需递归复制嵌套对象)。function deepClone(target) {if (typeof target !== 'object' || target === null) {return target; // 非对象直接返回}let clone = Array.isArray(target) ? [] : {};for (const key in target) {if (target.hasOwnProperty(key)) {clone[key] = deepClone(target[key]); // 递归拷贝}}return clone; }
-
使用
structuredClone()
(ES2022):
原生支持深拷贝,支持大部分内置类型。const deepCopy = structuredClone(obj);
-
80. async/await相比Promise有哪些优势?
async/await
是基于 Promise 的语法糖,它在保留 Promise 异步特性的同时,解决了 Promise 链式调用的一些痛点,主要优势如下:
-
代码结构更接近同步逻辑,可读性更高
Promise 依赖链式调用(then()
),多层嵌套层级较深时会形成“回调链”;而async/await
用同步代码的写法表达异步流程,逻辑更直观。// Promise 链式调用 fetchUser().then(user => {return fetchPosts(user.id).then(posts => {return fetchComments(posts[0].id).then(comments => {console.log(comments);});});});// async/await 写法 async function getComments() {const user = await fetchUser();const posts = await fetchPosts(user.id);const comments = await fetchComments(posts[0].id);console.log(comments); }
-
错误处理更统一、简洁
Promise 需要在每个then()
后单独处理错误(或在链尾用catch()
);async/await
可通过try/catch
捕获所有异步操作的错误,包括同步代码错误。// Promise 错误处理 fetchData().then(data => process(data)).catch(err => console.error("获取数据失败:", err)).then(result => saveResult(result)).catch(err => console.error("保存数据失败:", err));// async/await 错误处理 async function handleData() {try {const data = await fetchData();const result = await process(data);await saveResult(result);} catch (err) {console.error("操作失败:", err); // 统一捕获所有步骤的错误} }
-
流程控制更灵活
在异步流程中插入条件判断、循环等逻辑时,async/await
比 Promise 更自然,无需嵌套then()
或使用额外变量。async function loadResources(needExtra) {const base = await loadBase();if (needExtra) {const extra = await loadExtra(); // 条件性异步操作return { ...base, ...extra };}return base; }
-
调试体验更友好
调试async/await
代码时,可以直接在await
语句处断点,变量作用域清晰;而 Promise 的then()
回调函数会形成独立作用域,调试时变量查看更复杂。 -
返回值处理更直接
async/await
中,await
直接返回 Promise 的结果值,无需通过then()
的回调参数获取;而 Promise 需要嵌套回调才能处理上一步的结果。
总之,async/await
并未替代 Promise,而是在其基础上提供了更优雅的语法,让异步代码的编写和维护成本显著降低,是目前 JavaScript 异步编程的最佳实践。
二、80道ES6 面试题目录列表
文章序号 | ES6 80道 |
---|---|
1 | ES6面试题及详细答案80道(01-05) |
2 | ES6面试题及详细答案80道(06-12) |
3 | ES6面试题及详细答案80道(13-21) |
4 | ES6面试题及详细答案80道(22-32) |
5 | ES6面试题及详细答案80道(33-40) |
6 | ES6面试题及详细答案80道(41-54) |
7 | ES6面试题及详细答案80道(55-61) |
8 | ES6面试题及详细答案80道(62-80) |