JavaScript 展开运算符(Spread Operator)完全指南

JavaScript 展开运算符(Spread Operator)完全指南

深入理解...的语法、行为和各种边界情况

目录

  • 一、基础概念
  • 二、对象展开
  • 三、数组展开
  • 四、条件展开详解
  • 五、对象 vs 数组的行为差异
  • 六、常见坑点与最佳实践
  • 七、完整参考表

一、基础概念

展开运算符(...)允许一个表达式在期望多个元素(数组)或多个参数(函数调用)或多个属性(对象)的位置展开。

两种使用场景

javascript

// 1. 对象展开 - 复制对象属性 const obj = { ...source }; // 2. 数组展开 - 展开可迭代元素 const arr = [...source];

二、对象展开

2.1 基本用法

javascript

// 复制对象 const source = { name: '张三', age: 18 }; const copy = { ...source }; // 结果:{ name: '张三', age: 18 } // 合并对象 const obj1 = { a: 1, b: 2 }; const obj2 = { c: 3, d: 4 }; const merged = { ...obj1, ...obj2 }; // 结果:{ a: 1, b: 2, c: 3, d: 4 } // 覆盖属性(后面的覆盖前面的) const base = { name: '张三', age: 18 }; const update = { age: 20, city: '北京' }; const result = { ...base, ...update }; // 结果:{ name: '张三', age: 20, city: '北京' }

2.2 各种值的展开行为

javascript

// ✅ 普通对象 const obj = { name: '张三' }; { ...obj } // { name: '张三' } // ✅ 空对象 const empty = {}; { ...empty } // {} // ✅ 数组展开为对象(索引变属性名) const arr = [1, 2, 3]; { ...arr } // { '0': 1, '1': 2, '2': 3 } // ✅ 字符串展开为对象 const str = 'abc'; { ...str } // { '0': 'a', '1': 'b', '2': 'c' } // ✅ null 和 undefined(被忽略) { ...null } // {} (不报错) { ...undefined } // {} (不报错) // ✅ 数字和布尔值 { ...123 } // {} (数字没有可枚举属性) { ...true } // {} (布尔没有可枚举属性) // ❌ 注意:在对象字面量外部不能直接展开 // ...null; // 语法错误

2.3 对象展开的规范行为

根据 ECMAScript 规范,对象展开时:

  1. nullundefined:直接跳过(不处理)
  2. 原始类型(stringnumberboolean):先包装成对象,再复制可枚举属性
  3. 普通对象:复制所有可枚举属性

javascript

// 规范示例 const obj = { ...null, // 跳过 ...undefined, // 跳过 ...'abc', // { '0': 'a', '1': 'b', '2': 'c' } ...123, // {} (无属性) ...true, // {} (无属性) ...{ x: 1 } // { x: 1 } }; // 最终结果:{ '0': 'a', '1': 'b', '2': 'c', x: 1 }

三、数组展开

3.1 基本用法

javascript

// 复制数组 const source = [1, 2, 3]; const copy = [...source]; // 结果:[1, 2, 3] // 合并数组 const arr1 = [1, 2]; const arr2 = [3, 4]; const merged = [...arr1, ...arr2]; // 结果:[1, 2, 3, 4] // 添加元素 const arr = [1, 2]; const newArr = [0, ...arr, 3]; // 结果:[0, 1, 2, 3]

3.2 各种值的展开行为

javascript

// ✅ 普通数组 const arr = [1, 2, 3]; [...arr] // [1, 2, 3] // ✅ 空数组 const empty = []; [...empty] // [] // ✅ 字符串(可迭代) const str = 'abc'; [...str] // ['a', 'b', 'c'] // ✅ Set(可迭代) const set = new Set([1, 2, 3]); [...set] // [1, 2, 3] // ✅ Map(可迭代,返回键值对数组) const map = new Map([['a', 1], ['b', 2]]); [...map] // [['a', 1], ['b', 2]] // ❌ null 和 undefined(报错) [...null] // TypeError: null is not iterable [...undefined] // TypeError: undefined is not iterable // ❌ 普通对象(不可迭代) const obj = { name: '张三' }; [...obj] // TypeError: obj is not iterable // ❌ 数字(不可迭代) [...123] // TypeError: 123 is not iterable // ❌ 布尔值(不可迭代) [...true] // TypeError: true is not iterable

3.3 数组展开的要求

数组展开要求值必须是可迭代对象(Iterable)

  • ✅ Array
  • ✅ String
  • ✅ Set
  • ✅ Map
  • ✅ NodeList(DOM 元素集合)
  • ✅ arguments 对象
  • ❌ 普通对象
  • ❌ null / undefined
  • ❌ Number / Boolean

四、条件展开详解

4.1 核心语法

javascript

...(condition && { data })

这是短路求值+展开运算符的组合使用。

4.2 执行步骤

javascript

// 代码 ...(data && { data }) // 步骤1:计算 data && { data } // 如果 data 是真值 → 返回 { data } // 如果 data 是假值 → 返回 data 本身 // 步骤2:展开步骤1的结果 // 如果是对象 → 展开属性 // 如果是假值(null/undefined)→ 在对象中忽略

4.3 各种值的行为

data 值data && { data }结果展开结果
{ id: 1 }{ data: { id: 1 } }✅ 添加 data 字段
[1, 2, 3]{ data: [1, 2, 3] }✅ 添加 data 字段
'hello'{ data: 'hello' }✅ 添加 data 字段
true{ data: true }✅ 添加 data 字段
nullnull⏭️ 被忽略
undefinedundefined⏭️ 被忽略
00⏭️ 被忽略(⚠️ 可能是问题)
''''⏭️ 被忽略(⚠️ 可能是问题)
falsefalse⏭️ 被忽略(⚠️ 可能是问题)
NaNNaN⏭️ 被忽略(⚠️ 可能是问题)

4.4 实际应用示例

javascript

// 当前代码 function sendJsonResponse(res, statusCode, message, data = null) { return res.status(statusCode).json({ message, ...(data && { data }) }); } // 测试 sendJsonResponse(res, 200, '成功', { id: 1 }); // 返回:{ message: '成功', data: { id: 1 } } sendJsonResponse(res, 200, '成功', null); // 返回:{ message: '成功' } ✅ 干净,没有 data: null sendJsonResponse(res, 200, '成功', 0); // 返回:{ message: '成功' } ⚠️ 但你可能想要 data: 0

4.5 ⚠️ 注意:假值陷阱

javascript

// 问题:0、''、false 是有效数据,但会被当作"无数据" const data = 0; ...(data && { data }) // 不会添加 data 字段 // 解决方案1:只排除 null 和 undefined ...(data != null && { data }) // 或 ...(data !== null && data !== undefined && { data }) // 解决方案2:使用空值合并运算符 ...(data ?? { data }) // 注意:这会在 data 为 null/undefined 时添加 {} // 但这样会添加空对象,不推荐

五、对象 vs 数组的行为差异

5.1 核心区别

特性对象展开{...}数组展开[...]
null 和 undefined✅ 被忽略,不报错❌ 报错(不可迭代)
普通对象✅ 复制属性❌ 报错(不可迭代)
数组✅ 索引变属性名✅ 展开元素
字符串✅ 索引变属性名✅ 展开字符
数字/布尔✅ 无属性,不报错❌ 报错(不可迭代)
Set/Map❌ 报错(普通对象行为)✅ 展开元素

5.2 对比示例

javascript

// 对象展开 - 宽容 const objResult = { ...null, // ✅ 跳过 ...undefined, // ✅ 跳过 ...'abc', // ✅ { '0': 'a', '1': 'b', '2': 'c' } ...[1, 2], // ✅ { '0': 1, '1': 2 } ...123, // ✅ {} }; // 结果:{ '0': 1, '1': 2, '2': 'c' } // 数组展开 - 严格 try { const arrResult = [ ...null, // ❌ TypeError ...undefined, // ❌ TypeError ...'abc', // ✅ 'a', 'b', 'c' ...[1, 2], // ✅ 1, 2 ...123, // ❌ TypeError ]; } catch (e) { console.log('数组展开报错!'); }

5.3 安全的数组条件展开

javascript

// 方案1:使用空数组默认值 const arr = [...(data ?? [])]; // 方案2:先判断再展开 const arr = data ? [...data] : []; // 方案3:Array.from(更安全) const arr = Array.from(data ?? []); // 方案4:concat const arr = [].concat(data ?? []); // 方案5:条件判断 const arr = []; if (data && Array.isArray(data)) { arr.push(...data); }

六、常见坑点与最佳实践

6.1 坑点清单

❌ 坑1:假值被忽略

javascript

// 问题 const data = 0; ...(data && { data }) // data 字段丢失 // 解决 ...(data != null && { data })
❌ 坑2:数组展开 null

javascript

// 问题 const arr = [...data]; // data 为 null 时报错 // 解决 const arr = [...(data ?? [])];
❌ 坑3:对象展开非普通对象

javascript

// 问题 const obj = { ...new Date() }; // 会复制日期对象的所有属性 // 结果:{ toString: [Function], toDateString: [Function], ... } // 解决:明确只复制需要的属性 const { year, month } = new Date();
❌ 坑4:浅拷贝问题

javascript

// 问题:展开只做浅拷贝 const original = { user: { name: '张三' } }; const copy = { ...original }; copy.user.name = '李四'; console.log(original.user.name); // '李四'(被修改了!) // 解决:深拷贝 const deepCopy = JSON.parse(JSON.stringify(original)); // 或使用 structuredClone(Node 17+) const clone = structuredClone(original);

6.2 最佳实践

✅ 推荐1:条件展开只排除 null/undefined

javascript

// 好的做法 ...(data != null && { data }) // 而不是 ...(data && { data })
✅ 推荐2:数组展开使用空值合并

javascript

// 好的做法 const arr = [...(items ?? [])]; // 而不是 const arr = [...items]; // items 为 null 时报错
✅ 推荐3:对象展开保持纯净

javascript

// 好的做法:使用展开创建新对象 const updateUser = (user, updates) => ({ ...user, ...updates, updatedAt: new Date() }); // 而不是:直接修改原对象 const updateUser = (user, updates) => { Object.assign(user, updates); // ❌ 修改了原对象 return user; };
✅ 推荐4:使用展开进行不可变更新

javascript

// 添加属性 const newState = { ...state, newProp: 'value' }; // 更新属性 const newState = { ...state, prop: 'newValue' }; // 删除属性 const { deleteProp, ...newState } = state; // 更新嵌套属性 const newState = { ...state, user: { ...state.user, age: 20 } };

七、完整参考表

7.1 对象展开参考表

示例结果是否报错
普通对象{ ...{a:1} }{ a: 1 }✅ 否
空对象{ ...{} }{}✅ 否
数组{ ...[1,2] }{ '0': 1, '1': 2 }✅ 否
字符串{ ...'abc' }{ '0': 'a', '1': 'b', '2': 'c' }✅ 否
null{ ...null }{}✅ 否
undefined{ ...undefined }{}✅ 否
数字{ ...123 }{}✅ 否
布尔值{ ...true }{}✅ 否
Set{ ...new Set() }{}✅ 否
Date{ ...new Date() }复制所有属性✅ 否

7.2 数组展开参考表

示例结果是否报错
数组[...[1,2]][1, 2]✅ 否
空数组[...[]][]✅ 否
字符串[...'abc']['a', 'b', 'c']✅ 否
Set[...new Set([1,2])][1, 2]✅ 否
Map[...new Map([['a',1]])][['a', 1]]✅ 否
null[...null]-❌ TypeError
undefined[...undefined]-❌ TypeError
普通对象[...{a:1}]-❌ TypeError
数字[...123]-❌ TypeError
布尔值[...true]-❌ TypeError

7.3 条件展开参考表

条件值表达式对象展开结果数组展开结果
{ data: 1 }...(value && { data })✅ 添加 data❌ 报错
null...(value && { data })⏭️ 忽略❌ 报错
undefined...(value && { data })⏭️ 忽略❌ 报错
0...(value && { data })⏭️ 忽略❌ 报错
''...(value && { data })⏭️ 忽略❌ 报错
false...(value && { data })⏭️ 忽略❌ 报错

八、总结

  1. 对象展开null/undefined宽容,在对象字面量中会被静默忽略
  2. 数组展开要求值必须可迭代,null/undefined会直接报错
  3. 条件展开...(data && { data })是一种优雅的条件添加属性的方式
  4. 注意假值陷阱0''false会被当作"无数据"
  5. 推荐使用data != null来精确判断是否添加字段

javascript

// 最终推荐写法 function sendJsonResponse(res, statusCode, message, data = null) { return res.status(statusCode).json({ message, ...(data != null && { data }) // 只有 null/undefined 时不添加 }); }

参考资料

  • MDN: Spread syntax
  • ECMAScript Specification
  • JavaScript 展开运算符的深度解析

📝 本文由 [小新1688] 整理,欢迎分享和讨论!

如果觉得有用,请给个 Star ⭐ 或分享给更多开发者~