ES6 + 核心语法全解析(极简可运行代码 + 避坑 + 快速回顾)
前言
写 JS 久了,常发现 let/const 的作用域、箭头函数的 this 指向容易混淆,模板字符串换行也常踩坑。这里整理了 10 个高频核心语法的「问题 + 思路 + 极简例子」,每个都能直接跑,方便随时唤醒记忆。
一、核心思路 / 概念
ES6(ECMAScript 2015)及后续版本是 JavaScript 的重大升级,核心是解决旧语法痛点 + 简化代码。比如用 let/const 解决 var 的全局/函数作用域混乱问题,用箭头函数简化回调写法并固定 this 指向,用解构/扩展运算符快速操作数组/对象,用 Promise/Async-Await 解决回调地狱。所有语法都围绕'写更少的代码,做更多的事',且完全兼容日常开发。
二、实操步骤 + 例子
2.1 语法 1:let/const(替代 var,解决作用域问题)
问题
var 只有全局/函数作用域,容易变量提升、重复声明导致 bug(比如循环里的变量泄露)。
核心思路
let 是块级作用域变量(可重新赋值),const 是块级作用域常量(引用不可变),两者都不能重复声明、不存在变量提升。
极简可运行例子
// 对比 var 的问题(var 无块级作用域)
var a = 1;
if (true) {
var a = 2; // 覆盖全局 a
}
console.log(a); // 输出 2(坑:本意是块内变量,却改了全局)
// let 的正确用法(块级作用域)
let b = 1;
if (true) {
let b = 2; // 块内新变量,不影响外部
}
console.log(b); // 输出 1(符合预期)
// const 的正确用法(引用不可变)
const PI = 3.14;
// PI = 3.1415; // ❌ 报错:不能重新赋值(核心坑点)
const user = { name: "张三" };
user.name = "李四"; // ✅ 允许改属性(const 只锁引用,不锁值)
console.log(user.name); // 输出李四
2.2 语法 2:箭头函数(简化回调 + 固定 this 指向)
问题
传统函数的 this 指向随调用场景变化(比如定时器里 this 指向 window),回调函数写法繁琐。
核心思路
箭头函数无自身 this(继承外层作用域的 this),语法更简洁,适合简单回调/纯函数。
极简可运行例子
// 传统函数的 this 坑
const person = {
name: "张三",
sayName: function () {
setTimeout(function () {
console.log(this.name); // 输出 undefined(this 指向 window)
}, 100);
},
};
person.sayName();
// 箭头函数解决 this 问题
const person2 = {
name: "张三",
sayName: function () {
setTimeout(() => {
console.log(this.name); // 输出张三(继承外层 this)
}, 100);
},
};
person2.sayName();
// 箭头函数简写规则
const add = (a, b) => a + b; // 单返回值可省略{}和 return
console.log(add(1, 2)); // 输出 3
const fn = a => a * 2; // 单参数可省略()
console.log(fn(3)); // 输出 6
2.3 语法 3:模板字符串(解决字符串拼接繁琐)
问题
传统字符串拼接需要 + 号,多行字符串需要 \n 转义,易出错。
核心思路
用反引号 ` 包裹字符串,${变量/表达式}插值,支持原生多行,无需转义。
极简可运行例子
// 传统拼接的麻烦
const name = "张三";
const age = 20;
const str1 = "姓名:" + name + ",年龄:" + age + "\n多行字符串";
console.log(str1);
// 模板字符串简化
const str2 = `姓名:${name},年龄:${age} 多行字符串(原生换行)表达式:${1 + 2}`;
console.log(str2); // 输出:姓名:张三,年龄:20 换行 多行字符串 换行 表达式:3
2.4 语法 4:解构赋值(快速提取数组/对象数据)
问题
从数组/对象中取数据需要逐个赋值,代码冗余(比如 const name = user.name; const age = user.age;)。
核心思路
按数组/对象的结构'拆解',直接提取所需数据,支持默认值、重命名。
极简可运行例子
// 数组解构
const arr = [1, 2, 3];
const [a, b] = arr; // 按顺序提取
console.log(a, b); // 输出 1 2
const [x, , z] = arr; // 跳过第二个元素
console.log(x, z); // 输出 1 3
// 对象解构(按属性名,不按顺序)
const user = { name: "张三", age: 20 };
const { name, age } = user; // 直接提取
console.log(name, age); // 输出张三 20
const { name: userName } = user; // 重命名(避免变量冲突)
console.log(userName); // 输出张三
// 函数参数解构(高频用法)
function printUser({ name, age = 18 }) { // 设置默认值
console.log(`${name},${age}岁`);
}
printUser(user); // 输出张三,20 岁
printUser({ name: "李四" }); // 输出李四,18 岁(用默认值)
2.5 语法 5:扩展运算符(快速合并/复制数组/对象)
问题
传统合并数组用 concat,复制数组用 slice,合并对象用 Object.assign,写法繁琐。
核心思路
用 ... 展开数组/对象,实现一键合并、复制(浅拷贝),替代传统方法。
极简可运行例子
// 数组操作
const arr1 = [1, 2];
const arr2 = [3, 4];
const mergeArr = [...arr1, ...arr2]; // 合并数组
console.log(mergeArr); // 输出 [1,2,3,4]
const copyArr = [...arr1]; // 复制数组(浅拷贝)
console.log(copyArr); // 输出 [1,2]
// 对象操作
const obj1 = { a: 1, b: 2 };
const obj2 = { ...obj1, b: 3, c: 4 }; // 合并 + 覆盖属性
console.log(obj2); // 输出 {a:1,b:3,c:4}
const copyObj = { ...obj1 }; // 复制对象(浅拷贝)
console.log(copyObj); // 输出 {a:1,b:2}
2.6 语法 6:对象简写(减少冗余代码)
问题
对象属性名和变量名相同时,需要重复写 key: value;方法需要写 function 关键字。
核心思路
变量名 = 属性名时可省略 key:,方法可省略 function,支持动态属性名。
极简可运行例子
// 属性简写
const name = "张三";
const age = 20;
const user = { name, age }; // 等价于{name: name, age: age}
console.log(user); // 输出 {name: "张三", age: 20}
// 方法简写
const obj = {
sayHi() { // 等价于 sayHi: function() {}
console.log("Hi");
}
};
obj.sayHi(); // 输出 Hi
// 动态属性名
const key = "status";
const obj2 = {
[key]: "active", // 动态属性名:status
[`get_${key}`]() { // 动态方法名:get_status
return this[key];
}
};
console.log(obj2.status); // 输出 active
console.log(obj2.get_status()); // 输出 active
2.7 语法 7:Promise/Async-Await(解决回调地狱)
问题
多层异步回调(比如先请求用户数据,再请求用户订单)会形成'回调地狱',代码嵌套深、难维护。
核心思路
Promise 将异步操作封装为对象,用 then/catch 链式调用;Async-Await 是 Promise 的语法糖,用同步写法写异步代码。
极简可运行例子
// 模拟异步请求(比如接口调用)
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ name: "张三" }); // 成功返回数据
// reject(new Error("请求失败")); // 失败返回错误
}, 1000);
});
}
// Promise 链式调用(替代回调嵌套)
fetchData()
.then(data => {
console.log(data); // 输出 {name: "张三"}
return data.name;
})
.then(name => console.log(name)) // 输出张三
.catch(error => console.error(error)); // 捕获错误
// Async-Await(同步写法)
async function loadData() {
try {
const data = await fetchData(); // 等待异步完成
console.log(data); // 输出 {name: "张三"}
} catch (error) {
console.error(error); // 捕获错误
}
}
loadData();
2.8 语法 8:Set/Map(解决数组去重/对象键限制)
问题
数组去重需要遍历 + 判断,对象的键只能是字符串/Symbol,无法用对象做键。
核心思路
Set 是唯一值集合(一键去重),Map 是键值对集合(键可以是任意类型)。
极简可运行例子
// Set 去重(高频用法)
const arr = [1, 2, 2, 3];
const uniqueArr = [...new Set(arr)]; // 数组去重
console.log(uniqueArr); // 输出 [1,2,3]
// Map 使用(键可以是对象)
const map = new Map();
const objKey = { id: 1 };
map.set(objKey, "张三"); // 对象作为键
console.log(map.get(objKey)); // 输出张三
console.log(map.has(objKey)); // 输出 true
2.9 语法 9:Class 类(简化原型链继承)
问题
传统原型链继承写法繁琐(比如 Person.prototype.sayHi = function() {}),易出错。
核心思路
Class 是原型链的语法糖,用 class/constructor/extends/super 实现面向对象编程,更贴近传统语言。
极简可运行例子
// 定义基础类
class Person {
constructor(name, age) { // 构造函数(初始化属性)
this.name = name;
this.age = age;
}
sayHi() { // 实例方法
console.log(`Hi, ${this.name}`);
}
}
const p = new Person("张三", 20);
p.sayHi(); // 输出 Hi, 张三
// 继承
class Student extends Person {
constructor(name, age, grade) {
super(name, age); // 调用父类构造函数
this.grade = grade;
}
sayGrade() {
console.log(`${this.name}的年级:${this.grade}`);
}
}
const s = new Student("李四", 18, "高三");
s.sayHi(); // 输出 Hi, 李四
s.sayGrade(); // 输出李四的年级:高三
2.10 语法 10:模块化(export/import,解决全局变量污染)
问题
多个 JS 文件共用变量时会造成全局污染,代码无法隔离复用。
核心思路
用 export 导出模块内的变量/函数/类,用 import 导入到其他文件,实现代码隔离和复用。
极简可运行例子
// 创建模块文件(module.js)
export const PI = 3.14; // 命名导出
export function add(a, b) {
return a + b;
}
export default function mul(a, b) { // 默认导出(一个模块只能有一个)
return a * b;
}
// 导入模块(app.js)
// import mul, { PI, add } from './module.js';
// console.log(PI); // 输出 3.14
// console.log(add(1, 2)); // 输出 3
// console.log(mul(2, 3)); // 输出 6
三、踩坑记录 + 解决方案
- ❌ 坑 1:
const定义对象后改属性报错 → ✅ 解决:const锁的是'引用地址',不是值,对象/数组属性可正常修改,只有重新赋值(user = {})才会报错; - ❌ 坑 2: 箭头函数里用
arguments→ ✅ 解决: 箭头函数无arguments对象,需用剩余参数...args替代; - ❌ 坑 3: 模板字符串里的换行/空格会被保留 → ✅ 解决: 如果不需要空格,可把代码写在一行,或用
trim()去除首尾空格; - ❌ 坑 4: 解构赋值时变量名写错 → ✅ 解决: 对象解构按'属性名'匹配,数组解构按'顺序'匹配,写错会得到
undefined; - ❌ 坑 5: 扩展运算符深拷贝对象 → ✅ 解决: 扩展运算符是浅拷贝,嵌套对象/数组需用
JSON.parse(JSON.stringify())或专门的深拷贝方法; - ❌ 坑 6:
Async-Await未用try/catch捕获错误 → ✅ 解决:await的异步错误必须用try/catch捕获,否则会静默失败。
四、总结
- 核心价值: ES6+ 的所有语法都是'解决旧痛点 + 简化代码',优先用
let/const替代var,用箭头函数简化回调,用解构/扩展运算符操作数据,用Async-Await写异步代码; - 高频用法: 日常开发中
let/const、模板字符串、解构/扩展运算符、Async-Await是使用频率最高的 5 个语法,必须掌握; - 避坑关键: 记住
const的'引用不可变'、箭头函数的this继承规则、扩展运算符的浅拷贝特性,能避免 80% 的 ES6 使用坑; - 适用场景: 这些语法完全适配 uni-app、Vue/React 等前端框架,直接复制例子就能在项目中使用。
ES6+ 核心语法综合练习题(题目 + 答案合一版)
一、综合场景题(3 道,侧重实战综合应用)
场景题 1:异步数据处理与重组(综合箭头函数/解构/扩展运算符/Async-Await/Set)
题目需求
模拟前端请求「用户列表」和「用户订单列表」两个异步接口,完成以下操作:
- 用
Async-Await并行请求两个接口(模拟接口用Promise封装,延迟 1 秒返回数据); - 合并用户数据与订单数据,为每个用户添加
orders字段(匹配userId); - 过滤出「有订单的用户」,并对用户的订单金额去重后求和;
- 最终返回格式:
[{ id: 1, name: "张三", orders: [/* 该用户订单 */], totalAmount: 去重后总金额 }, ...]。
模拟接口数据
// 模拟用户列表接口
function fetchUsers() {
return new Promise(resolve => setTimeout(() => {
resolve([
{ id: 1, name: "张三", age: 20 },
{ id: 2, name: "李四", age: 22 },
{ id: 3, name: "王五", age: 25 }
]);
}, 1000));
}
// 模拟订单列表接口
function fetchOrders() {
return new Promise(resolve => setTimeout(() => {
resolve([
{ orderId: 1, userId: 1, amount: 100 },
{ orderId: 2, userId: 1, amount: 200 },
{ orderId: 3, userId: 1, amount: 200 }, // 重复金额
{ orderId: 4, userId: 2, amount: 150 },
{ orderId: 5, userId: 3, amount: 0 } // 金额为 0
]);
}, 1000));
}
题目要求
- 必须使用
Async-Await处理异步,禁止使用then/catch链式调用; - 订单金额去重必须用
Set实现; - 合并数据时需用解构/扩展运算符简化代码;
- 过滤条件:订单金额 > 0 且有订单的用户。
参考答案
async function handleUserAndOrders() {
// 并行请求两个接口,数组解构接收结果
const [users, orders] = await Promise.all([fetchUsers(), fetchOrders()]);
// 处理用户数据,过滤无有效订单的用户
const result = users.map(user => {
// 筛选当前用户的有效订单(userId 匹配 + 金额>0)
const userOrders = orders.filter(order => order.userId === user.id && order.amount > 0);
if (userOrders.length === 0) return null; // 无有效订单返回 null,后续过滤
// Set 实现金额去重,扩展运算符转数组后求和
const uniqueAmounts = new Set(userOrders.map(order => order.amount));
const totalAmount = [...uniqueAmounts].reduce((sum, amount) => sum + amount, 0);
// 扩展运算符合并用户基础数据,添加订单和总金额字段
return { ...user, orders: userOrders, totalAmount };
}).filter(Boolean); // 过滤 null 值,保留有有效订单的用户
return result;
}
// 测试调用
handleUserAndOrders().then(res => console.log(res));
// 预期输出:
// [
// { id: 1, name: "张三", age: 20, orders: [{ orderId:1, userId:1, amount:100 }, { orderId:2, userId:1, amount:200 }, { orderId:3, userId:1, amount:200 }], totalAmount: 300 },
// { id: 2, name: "李四", age: 22, orders: [{ orderId:4, userId:2, amount:150 }], totalAmount: 150 }
// ]
场景题 2:购物车类封装(综合 Class/Map/ 对象简写/模板字符串/解构)
题目需求
封装一个 ShoppingCart 类,实现购物车核心功能:
- 用
Map存储商品(键:商品 id,值:{id, name, price, count}); - 实现方法:
addGoods(goods):添加商品(若已存在则累加数量,商品格式:{id, name, price});removeGoods(goodsId):删除指定 id 的商品;getTotalPrice():计算购物车商品总价(价格×数量求和);getCartInfo():返回购物车信息字符串,格式:"购物车:商品 1(数量:2,单价:100)、商品 2(数量:1,单价:200),总价:400"。
- 类的方法需使用「对象简写语法」,字符串拼接需用模板字符串。
测试用例
const cart = new ShoppingCart();
cart.addGoods({ id: 1, name: "手机", price: 2000 });
cart.addGoods({ id: 1, name: "手机", price: 2000 }); // 数量累加为 2
cart.addGoods({ id: 2, name: "耳机", price: 300 });
console.log(cart.getTotalPrice()); // 预期输出:4300
console.log(cart.getCartInfo()); // 预期输出:"购物车:手机(数量:2,单价:2000)、耳机(数量:1,单价:300),总价:4300"
cart.removeGoods(2);
console.log(cart.getCartInfo()); // 预期输出:"购物车:手机(数量:2,单价:2000),总价:4000"
参考答案
class ShoppingCart {
constructor() {
// 初始化 Map,键为商品 id,值为商品详细信息
this.goodsMap = new Map();
}
// 添加商品:对象简写语法,解构取值简化代码
addGoods(goods) {
const { id, name, price } = goods;
if (this.goodsMap.has(id)) {
// 商品已存在,累加数量,扩展运算符保留原有属性
const oldGoods = this.goodsMap.get(id);
this.goodsMap.set(id, { ...oldGoods, count: oldGoods.count + 1 });
} else {
// 商品不存在,新增,数量默认 1
this.goodsMap.set(id, { id, name, price, count: 1 });
}
}
// 删除商品:根据 id 删除 Map 中的键值对
removeGoods(goodsId) {
this.goodsMap.delete(goodsId);
}
// 计算总价:遍历 Map 值,累加价格×数量
getTotalPrice() {
let total = 0;
for (const goods of this.goodsMap.values()) {
total += goods.price * goods.count;
}
return total;
}
// 获取购物车信息:模板字符串拼接,扩展运算符转数组后遍历
getCartInfo() {
const goodsStr = [...this.goodsMap.values()].map(goods => {
return `${goods.name}(数量:${goods.count},单价:${goods.price})`;
}).join("、");
const totalPrice = this.getTotalPrice();
return `购物车:${goodsStr},总价:${totalPrice}`;
}
}
// 测试用例执行
const cart = new ShoppingCart();
cart.addGoods({ id: 1, name: "手机", price: 2000 });
cart.addGoods({ id: 1, name: "手机", price: 2000 });
cart.addGoods({ id: 2, name: "耳机", price: 300 });
console.log(cart.getTotalPrice()); // 4300
console.log(cart.getCartInfo()); // 购物车:手机(数量:2,单价:2000)、耳机(数量:1,单价:300),总价:4300
cart.removeGoods(2);
console.log(cart.getCartInfo()); // 购物车:手机(数量:2,单价:2000),总价:4000
场景题 3:模块化工具函数封装(综合模块化/解构/扩展运算符/const/ 箭头函数)
题目需求
封装一个 formatUtil.js 工具模块,实现以下功能:
- 导出常量
DEFAULT_FORMAT(值:{ date: "YYYY-MM-DD", money: "¥0.00" }); - 导出命名函数
formatDate:接收日期对象/时间戳,返回指定格式的日期字符串(默认用DEFAULT_FORMAT.date); - 导出命名函数
formatMoney:接收数字,返回指定格式的金额字符串(默认用DEFAULT_FORMAT.money); - 导出默认函数
formatData:接收数据对象({date, money}),合并默认配置,返回格式化后的对象; - 题目要求:
- 所有变量用
const声明,函数用箭头函数; - 函数参数需用解构 + 默认值简化代码;
- 合并配置时需用扩展运算符。
- 所有变量用
调用示例(app.js)
import formatData, { formatDate, formatMoney, DEFAULT_FORMAT } from './formatUtil.js';
console.log(formatDate(new Date(2026, 0, 27))); // 预期输出:"2026-01-27"
console.log(formatMoney(100)); // 预期输出:"¥100.00"
console.log(formatData({ date: new Date(2026, 0, 27), money: 200 })); // 预期输出:{ date: "2026-01-27", money: "¥200.00" }
console.log(formatData({ money: 300 }, { money: "$0.00" })); // 预期输出:{ date: "YYYY-MM-DD", money: "$300.00" }
参考答案
formatUtil.js 模块代码
// 常量声明:所有变量用 const,符合要求
const DEFAULT_FORMAT = { date: "YYYY-MM-DD", money: "¥0.00" };
// 格式化日期:箭头函数,参数默认值,处理日期对象/时间戳
const formatDate = (date, format = DEFAULT_FORMAT.date) => {
const d = new Date(date);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, "0"); // 补 0 为 2 位
const day = String(d.getDate()).padStart(2, "0"); // 替换格式符为实际日期
return format.replace("YYYY", year).replace("MM", month).replace("DD", day);
};
// 格式化金额:箭头函数,参数默认值,数字补两位小数
const formatMoney = (num, format = DEFAULT_FORMAT.money) => {
const fixedNum = Number(num).toFixed(2); // 确保是两位小数
return format.replace("0.00", fixedNum);
};
// 默认导出:格式化数据,箭头函数,解构 + 默认值,扩展运算符合并配置
const formatData = (data = {}, customFormat = {}) => {
// 合并默认配置和自定义配置,自定义配置优先级更高
const finalFormat = { ...DEFAULT_FORMAT, ...customFormat };
const { date, money } = data;
// 按需格式化日期和金额,未传则返回配置默认值
return {
date: date ? formatDate(date, finalFormat.date) : finalFormat.date,
money: money ? formatMoney(money, finalFormat.money) : finalFormat.money
};
};
// 命名导出 + 默认导出,符合模块化规范
export { DEFAULT_FORMAT, formatDate, formatMoney };
export default formatData;
app.js 调用代码
import formatData, { formatDate, formatMoney, DEFAULT_FORMAT } from './formatUtil.js';
// 测试调用
console.log(formatDate(new Date(2026, 0, 27))); // 2026-01-27
console.log(formatMoney(100)); // ¥100.00
console.log(formatData({ date: new Date(2026, 0, 27), money: 200 })); // { date: '2026-01-27', money: '¥200.00' }
console.log(formatData({ money: 300 }, { money: "$0.00" })); // { date: 'YYYY-MM-DD', money: '$300.00' }
二、问答题(5 道,侧重理解与避坑)
问答题 1
题目:let/const与var的核心区别有哪些?为什么实际开发中推荐优先使用const而非let?
参考答案:
- 核心区别
- 作用域:
var是函数/全局作用域,let/const是块级作用域({}包裹的区域,如 if/for/while); - 变量提升:
var存在变量提升(可先使用后声明),let/const存在暂时性死区(声明前使用会报错,无变量提升); - 重复声明:
var允许同一作用域内重复声明,let/const不允许; - 初始值:
const声明时必须初始化,var/let可后续赋值; - 全局属性:
var声明的全局变量会挂载到window/global上,let/const不会。
- 作用域:
- 优先使用 const 的原因
- 语义化更强:
const明确表示变量引用不可变,代码可读性更高,其他开发者能快速知道该变量不会被重新赋值; - 减少意外 bug:避免开发中无意修改变量值,若后续发现需要修改,再将
const改为let即可,属于'最小权限原则'; - 符合函数式编程思想:强调数据不可变,减少副作用,让代码更稳定、易维护;
- 对引用类型友好:
const仅锁引用地址,不锁值(对象/数组属性可正常修改),不影响日常使用。
- 语义化更强:
问答题 2
题目:箭头函数和普通函数的核心差异体现在哪些方面?请列举至少 3 点,并说明哪些场景绝对不能使用箭头函数(举例说明)。
参考答案:
- 核心差异(至少 3 点)
this指向:箭头函数无自身 this,继承外层作用域的 this;普通函数的 this 指向调用者(谁调用指向谁,全局调用指向 window/undefined);arguments对象:箭头函数无 arguments 对象,需用剩余参数...args替代;普通函数有 arguments 对象(存储实参的类数组);- 构造函数:箭头函数不能作为构造函数(不能用 new 调用,无 prototype 原型属性);普通函数可以作为构造函数;
- 原型属性:箭头函数没有 prototype 属性;普通函数有 prototype 属性,可通过原型扩展方法;
- 绑定方式:箭头函数的 this一旦确定无法修改(call/apply/bind 无法改变);普通函数的 this 可通过 call/apply/bind 手动修改。
- 绝对不能使用箭头函数的场景(举例)
- 生成器函数:箭头函数不能使用 yield 关键字,无法作为生成器函数。
- 需要动态 this 的场景(如事件回调、DOM 操作):箭头函数的 this 无法指向触发事件的 DOM 元素;
const btn = document.querySelector('button'); btn.onclick = () => { console.log(this) }; // this 指向 window,而非按钮元素- 类的实例方法/原型方法:箭头函数会导致每个实例创建新的函数,浪费内存,且 this 不指向实例;
class Person { name = "张三"; sayName = () => { console.log(this.name) }; // 每个实例都有一个 sayName,浪费内存 }- 对象的方法:箭头函数的 this 继承全局,而非对象本身,导致取值失败;
const obj = { name: "张三", sayName: () => { console.log(this.name) } // this 指向 window,输出 undefined }; obj.sayName();
问答题 3
题目:解构赋值是 ES6 的高频用法,请列举 3 个解构赋值的典型业务场景,并说明:解构对象时如果目标属性不存在,如何避免获取到undefined?
参考答案:
- 解构赋值的 3 个典型业务场景
- 补充:嵌套数据解构(如 JSON 数据、复杂对象)、遍历解构(如 Map/数组的键值对遍历)也属于高频场景。
- 解构对象避免获取到 undefined 的方法
- 解构前判断对象是否存在:先判断源对象是否为 null/undefined,再解构,避免基础报错;
const user = null; const { name } = user || {}; // user 为 null,用空对象替代,name 取 undefined- 可选链 + 默认值结合:先通过可选链判断属性是否存在,不存在则用 ||/?? 设置默认值;
const user = { name: "张三" }; const age = user?.info?.age ?? 0; // 可选链判断,空值合并运算符设置默认值- 嵌套解构的默认值:若解构嵌套对象,需为外层对象也设置默认值,避免外层属性不存在时报错;
// 错误:info 为 undefined,解构 info.age 会报错 const { info: { age = 0 } } = { name: "张三" }; // 正确:为 info 设置默认空对象,避免报错 const { info: { age = 0 } = {} } = { name: "张三" };- 设置属性默认值:解构时直接为属性设置默认值,属性不存在时使用默认值;
const { name = "未知用户", age = 0 } = { name: "张三" }; // age 不存在,取默认值 0- 数组解构与变量交换:无需临时变量,快速交换两个变量的值,适合排序、交换场景;
let a = 1, b = 2; [a, b] = [b, a]; // 交换 a 和 b 的值,a=2,b=1- 接口数据解构:快速提取接口返回的核心数据,简化代码,避免多次点语法取值;
// 接口返回数据 const res = { code: 200, data: { list: [1,2,3], total: 3 }, msg: "成功" }; // 解构提取核心数据 const { code, data: { list, total } } = res;- 函数参数解构:简化多参数的取值,支持默认值,替代繁琐的参数对象取值;
// 传统写法 function fn(options) { const name = options.name; const age = options.age; } // 解构写法 function fn({ name, age = 18 }) { console.log(name, age) } fn({ name: "张三" });
问答题 4
题目:扩展运算符(...)实现的对象/数组拷贝是浅拷贝,请解释「浅拷贝」的定义,并说明深拷贝的 3 种实现方式,以及每种方式的优缺点。
参考答案:
-
原理:将对象转为 JSON 字符串,再转回新的对象,实现内存地址的完全隔离;
-
优点:写法简单,无需额外依赖,适合大部分简单场景;
-
缺点:① 无法拷贝函数、正则表达式、Date 对象、undefined、Symbol;② 无法处理循环引用(对象自身引用/互相引用),会直接报错;③ 会丢失对象的原型链,拷贝后变为普通对象。
-
原理:递归遍历对象的每一层属性,若为引用类型(对象/数组)则继续递归拷贝,若为基本类型则直接赋值;
-
优点:可自定义拷贝规则,支持函数、Date、正则等类型,可处理循环引用,保留原型链;
-
原理:库内部封装了完善的递归拷贝逻辑,处理了所有边界情况(类型判断、循环引用、原型链等);
-
优点:成熟稳定,支持所有数据类型,处理了循环引用/原型链/特殊对象,无需自己写代码,开发效率高;
-
缺点:若仅需简单深拷贝,引入库会增加项目体积(可按需引入,如
import cloneDeep from 'lodash/cloneDeep')。
缺点:代码复杂,需要处理各种边界情况(如 null/undefined/ 正则/Date/ 循环引用),开发成本高;
function deepClone(obj, map = new WeakMap()) {
if (obj === null || typeof obj !== 'object') return obj;
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
if (map.has(obj)) return map.get(obj); // 处理循环引用
const cloneObj = new obj.constructor(); // 保留原型链
map.set(obj, cloneObj);
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
cloneObj[key] = deepClone(obj[key], map);
}
}
return cloneObj;
}
深拷贝的 3 种实现方式及优缺点
方式 1:JSON.parse(JSON.stringify(obj))(简易深拷贝)
方式 2:手写递归深拷贝
方式 3:第三方库实现(如 Lodash 的_.cloneDeep)
浅拷贝的定义浅拷贝仅复制对象/数组的第一层属性,对于嵌套的对象/数组,不会递归复制,而是直接复制其内存引用地址。修改拷贝后对象的嵌套属性,原对象的嵌套属性也会被修改,因为二者指向同一块内存空间。
const obj1 = { a: 1, b: { c: 2 } };
const obj2 = { ...obj1 }; // 浅拷贝
obj2.b.c = 3;
console.log(obj1.b.c); // 输出 3,原对象被修改
问答题 5
题目:Async-Await的本质是什么?它和Promise的关系是什么?使用Async-Await时,为什么必须用try/catch捕获错误?如果不捕获会有什么问题?
参考答案:
- Async-Await 的本质
Async-Await是ES7 推出的语法糖,底层完全基于Promise实现,其核心作用是将异步代码以同步的写法呈现,解决了 Promise 链式调用(then/catch)的嵌套问题,让异步代码的可读性和维护性更高。 - Async-Await 与 Promise 的关系
async修饰的函数,返回值一定是 Promise 对象(无论函数内返回什么,都会被包装为 Promise.resolve (返回值));await关键字只能在 async 函数中使用,且后面必须跟 Promise 对象(若跟非 Promise 值,会被包装为 Promise.resolve (值));await的作用是等待 Promise 的状态变为 fulfilled(成功),并返回 Promise 的 resolve 值;若 Promise 状态为 rejected(失败),则会抛出错误;- Async-Await 是 Promise 的上层封装,不能脱离 Promise 独立使用,所有 Async-Await 的异步操作,最终都要通过 Promise 实现。
- 必须用 try/catch 捕获错误的原因当
await后面的 Promise 对象状态变为 rejected(如接口请求失败、异步操作报错)时,会抛出一个错误,该错误不会像普通 Promise 那样通过 catch 捕获,而是会静默中断async 函数的执行,若不通过 try/catch 捕获,错误会被'吞噬',导致后续代码无法执行,且无法定位问题。- 错误无法被感知:错误不会抛到全局,控制台仅会显示一个未捕获的 Promise 错误,难以定位错误的具体位置和原因;
- 全局报错:若多个 async 函数嵌套,未捕获的错误会向上传递,最终导致全局未捕获错误,严重时会导致页面/应用崩溃;
- 异步状态无法处理:无法对失败的异步操作做兜底处理(如接口请求失败后提示用户、刷新数据)。
代码执行中断:async 函数中,await 抛出错误后,错误位置后的代码会完全停止执行,导致业务逻辑中断;
async function fn() {
await Promise.reject(new Error("请求失败"));
console.log("后续代码"); // 不会执行,因为前面抛出了错误
}
fn();
不捕获错误的问题补充:除了 try/catch,也可通过在 await 后加 Promise.catch () 捕获单个异步错误:
async function fn() {
const res = await fetchData().catch(err => {
console.error(err);
return null; // 兜底返回 null,避免代码中断
});
console.log(res);
}

