解构赋值与展开运算符:现代 JS 提效技巧

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、生成器)都能被解构。

js 复制代码
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

嵌套数组解构时,保持左右结构一致即可:

js 复制代码
const matrix = [[1, 2], [3, 4]];
const [[x1, y1], [x2, y2]] = matrix;
console.log(x1, y1, x2, y2); // 1 2 3 4

3. 对象解构

对象解构按属性名取值,与顺序无关,这是它和数组解构最本质的区别。

js 复制代码
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:

js 复制代码
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 都不会触发默认值------这是最容易踩的坑。

js 复制代码
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

默认值可以是表达式,且惰性求值:只有真正需要时才执行。

js 复制代码
function getDefault() {
  console.log('计算默认值');
  return 100;
}

const { value = getDefault() } = { value: 1 };
// 不会打印「计算默认值」,因为 value 已有值
console.log(value); // 1

默认值还可以引用前面已解构出的变量:

js 复制代码
const { width, height = width } = { width: 300 };
console.log(width, height); // 300 300

5. 剩余参数与剩余属性

剩余(Rest)语法用 ... 收集「剩下的部分」,在函数参数中叫剩余参数,在解构中叫剩余属性。

js 复制代码
// 函数剩余参数:必须是最后一个形参
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. 展开运算符与复制

展开运算符用 ... 把可迭代对象「摊开」,最常见的用途是浅复制与合并。

js 复制代码
// 数组浅复制
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 浅复制的边界

展开复制只做一层复制,嵌套对象仍然是引用共享:

js 复制代码
const original = { user: { name: 'Alice' } };
const shallow = { ...original };

shallow.user.name = 'Bob';
console.log(original.user.name); // Bob,原对象被改动了

需要深复制时,可用 structuredClone(现代浏览器与 Node 17+ 支持):

js 复制代码
const deep = structuredClone(original);
deep.user.name = 'Carol';
console.log(original.user.name); // Bob,原对象不受影响

6.2 展开与解构的取舍

  • 需要「按位置取值」→ 数组解构;
  • 需要「按名字取值」→ 对象解构;
  • 需要「收集剩余」→ 剩余语法;
  • 需要「生成新对象/新数组」→ 展开复制。

把上面的取舍整理成一张决策流程图,写代码时按图索骥即可:

flowchart TD S[&#34;拿到一份数据,要做什么?&#34;] --> Q1{&#34;按位置取值?&#34;} Q1 -->|&#34;是&#34;| R1[&#34;数组解构 const [a, b] = arr&#34;] Q1 -->|&#34;否&#34;| Q2{&#34;按名字取值?&#34;} Q2 -->|&#34;是&#34;| R2[&#34;对象解构 const { name } = obj&#34;] Q2 -->|&#34;否&#34;| Q3{&#34;要收集剩余部分?&#34;} Q3 -->|&#34;是&#34;| R3[&#34;剩余语法 ...rest&#34;] Q3 -->|&#34;否&#34;| Q4{&#34;要生成新对象或新数组?&#34;} Q4 -->|&#34;是&#34;| R4[&#34;展开复制 [...arr] / {...obj}&#34;] R4 --> Q5{&#34;存在嵌套引用?&#34;} Q5 -->|&#34;是&#34;| R5[&#34;structuredClone 深复制&#34;] Q5 -->|&#34;否&#34;| R6[&#34;浅复制即可&#34;]

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 不生效,以及展开复制永远是浅复制。

相关推荐
2603_965896621 小时前
JavaScript 原型与原型链
开发语言·前端·javascript
晓得迷路了2 小时前
栗子前端技术周刊第 152 期 - Rspack 2.3、TypeScript 7.1 Beta、Bun 1.4.4...
前端·javascript·css
jktantan11 小时前
Vue keep-alive 切标签后滚动位置回顶?因为 onDeactivated 里读到的 scrollTop 是 0
前端·javascript·vue.js
总要学点什么12 小时前
第89篇:三维特效-螺旋圆效果——让螺旋臂卷起一圈圈外扩的涟漪
开发语言·javascript·ecmascript·webgl·cesium·空间分析·1024程序员节
前端精髓13 小时前
调用openrouter的免费API模型
java·前端·javascript
螺丝钉tina19 小时前
[Playwright] [02]常用命令与调试:Debug UI、Mode和 Trace Viewer
开发语言·javascript·测试工具·ui·自动化
开开心心就好20 小时前
竖式除法出题工具,整除余数混合模式
运维·服务器·前端·javascript·ocr·perl·1024程序员节
এ慕ོ冬℘゜20 小时前
JS 事件循环、宏任务微任务:单线程底层原理 + 执行顺序
开发语言·javascript·ecmascript
web打印社区20 小时前
Lodop排障总纲-常见报错一张表
开发语言·前端·javascript·chrome·websocket