1. 引言:为什么现代 JS 离不开解构与展开
ES6 引入的解构赋值(Destructuring Assignment)与展开运算符(Spread Operator),是日常写 JavaScript 时出现频率最高的语法糖之一。它们本身不改变程序的能力边界,却能显著减少样板代码,让「取值、传参、合并、复制」这些高频操作从五六行压缩到一行。
本文围绕五个核心点展开:数组解构、对象解构、默认值、剩余参数、展开复制,并给出可直接复制运行的示例与常见坑位提醒。
全文的知识脉络可以先用一张图串起来:
flowchart TD
A["解构赋值与展开运算符"] --> B["解构:怎么优雅地取"]
A --> C["展开:怎么优雅地合与复制"]
B --> B1["数组解构:按位置取值"]
B --> B2["对象解构:按属性名取值"]
B --> B3["默认值:仅 undefined 生效"]
B --> B4["剩余语法:收集剩下的部分"]
C --> C1["浅复制与合并"]
C --> C2["浅复制边界与 structuredClone"]
B1 --> D["常见坑位清单"]
B2 --> D
B3 --> D
B4 --> D
C1 --> D
C2 --> D
2. 数组解构
数组解构按位置取值,本质是调用迭代器,因此任何可迭代对象(数组、字符串、Set、Map、生成器)都能被解构。
const arr = [1, 2, 3, 4, 5];
const [first, second] = arr;
console.log(first, second); // 1 2
// 跳过元素:用空位占位
const [, , third] = arr;
console.log(third); // 3
// 交换变量,无需中间变量
let a = 1, b = 2;
[a, b] = [b, a];
console.log(a, b); // 2 1
// 字符串同样可解构
const [c1, c2] = 'JS';
console.log(c1, c2); // J S
嵌套数组解构时,保持左右结构一致即可:
const matrix = [[1, 2], [3, 4]];
const [[x1, y1], [x2, y2]] = matrix;
console.log(x1, y1, x2, y2); // 1 2 3 4
3. 对象解构
对象解构按属性名取值,与顺序无关,这是它和数组解构最本质的区别。
const user = {
id: 1,
name: 'Alice',
profile: { city: 'Shanghai', age: 28 }
};
// 基础解构
const { name, id } = user;
console.log(name, id); // Alice 1
// 重命名:原属性名: 新变量名
const { name: userName } = user;
console.log(userName); // Alice
// 嵌套解构
const { profile: { city } } = user;
console.log(city); // Shanghai
函数参数解构是对象解构最实用的场景之一,它让「配置对象」式的调用既清晰又不必在函数体内反复写 options.xxx:
function createButton({ text = '按钮', type = 'primary', disabled = false } = {}) {
return `<button type="${type}" ${disabled ? 'disabled' : ''}>${text}</button>`;
}
console.log(createButton({ text: '提交' }));
// <button type="primary" >提交</button>
注意参数末尾的 = {}:它保证调用方不传参数时不会抛 Cannot destructure property ... of undefined。
4. 默认值
默认值只在解构结果为 undefined 时生效,null、0、''、false 都不会触发默认值——这是最容易踩的坑。
const [p = 10] = [];
console.log(p); // 10
const [q = 10] = [null];
console.log(q); // null,不是 10
const { count = 0 } = { count: undefined };
console.log(count); // 0
const { flag = true } = { flag: false };
console.log(flag); // false
默认值可以是表达式,且惰性求值:只有真正需要时才执行。
function getDefault() {
console.log('计算默认值');
return 100;
}
const { value = getDefault() } = { value: 1 };
// 不会打印「计算默认值」,因为 value 已有值
console.log(value); // 1
默认值还可以引用前面已解构出的变量:
const { width, height = width } = { width: 300 };
console.log(width, height); // 300 300
5. 剩余参数与剩余属性
剩余(Rest)语法用 ... 收集「剩下的部分」,在函数参数中叫剩余参数,在解构中叫剩余属性。
// 函数剩余参数:必须是最后一个形参
function sum(first, ...rest) {
return rest.reduce((acc, cur) => acc + cur, first);
}
console.log(sum(1, 2, 3, 4)); // 10
// 数组剩余元素
const [head, ...tail] = [1, 2, 3, 4];
console.log(head, tail); // 1 [2, 3, 4]
// 对象剩余属性:常用于「剔除某些字段」
const { password, ...safeUser } = { id: 1, name: 'Alice', password: '123456' };
console.log(safeUser); // { id: 1, name: 'Alice' }
arguments 与剩余参数的区别值得记住:arguments 是类数组对象、不是真正的数组,且包含所有实参;剩余参数是真正的数组,只包含未被前面形参匹配的部分,并且箭头函数中同样可用。
6. 展开运算符与复制
展开运算符用 ... 把可迭代对象「摊开」,最常见的用途是浅复制与合并。
// 数组浅复制
const source = [1, 2, 3];
const copy = [...source];
console.log(copy); // [1, 2, 3]
console.log(copy === source); // false
// 数组合并
const merged = [...source, 4, 5];
console.log(merged); // [1, 2, 3, 4, 5]
// 对象浅复制与覆盖
const base = { a: 1, b: 2 };
const next = { ...base, b: 3, c: 4 };
console.log(next); // { a: 1, b: 3, c: 4 }
// 函数调用时展开
console.log(Math.max(...source)); // 3
6.1 浅复制的边界
展开复制只做一层复制,嵌套对象仍然是引用共享:
const original = { user: { name: 'Alice' } };
const shallow = { ...original };
shallow.user.name = 'Bob';
console.log(original.user.name); // Bob,原对象被改动了
需要深复制时,可用 structuredClone(现代浏览器与 Node 17+ 支持):
const deep = structuredClone(original);
deep.user.name = 'Carol';
console.log(original.user.name); // Bob,原对象不受影响
6.2 展开与解构的取舍
- 需要「按位置取值」→ 数组解构;
- 需要「按名字取值」→ 对象解构;
- 需要「收集剩余」→ 剩余语法;
- 需要「生成新对象/新数组」→ 展开复制。
把上面的取舍整理成一张决策流程图,写代码时按图索骥即可:
flowchart TD
S["拿到一份数据,要做什么?"] --> Q1{"按位置取值?"}
Q1 -->|"是"| R1["数组解构 const [a, b] = arr"]
Q1 -->|"否"| Q2{"按名字取值?"}
Q2 -->|"是"| R2["对象解构 const { name } = obj"]
Q2 -->|"否"| Q3{"要收集剩余部分?"}
Q3 -->|"是"| R3["剩余语法 ...rest"]
Q3 -->|"否"| Q4{"要生成新对象或新数组?"}
Q4 -->|"是"| R4["展开复制 [...arr] / {...obj}"]
R4 --> Q5{"存在嵌套引用?"}
Q5 -->|"是"| R5["structuredClone 深复制"]
Q5 -->|"否"| R6["浅复制即可"]
7. 常见坑位清单
| 场景 | 错误写法 | 正确写法 | 说明 |
|---|---|---|---|
解构 null/undefined | const { a } = null | const { a } = obj ?? {} | 会抛 TypeError |
| 默认值误判 | const { n = 1 } = { n: null } | 显式判断 n ?? 1 | null 不触发默认值 |
| 剩余参数位置 | function f(...rest, last) | function f(first, ...rest) | 剩余参数必须在最后 |
| 对象展开覆盖 | { ...a, ...b } 顺序写反 | 后写的覆盖先写的 | 顺序决定优先级 |
| 深复制误用 | { ...obj } 改嵌套 | structuredClone(obj) | 展开只复制一层 |
8. 小结
解构赋值解决的是「怎么优雅地取」,展开运算符解决的是「怎么优雅地合与复制」。把默认值、剩余语法与它们组合起来,绝大多数数据搬运代码都能压缩到一行,同时保持可读性。真正需要警惕的只有两点:默认值对 null 不生效,以及展开复制永远是浅复制。