ES6 引入的 ... 运算符可能是 JavaScript 中最“一符多用”的语法。它根据出现的位置不同,分别扮演“扩展”(spread)和“剩余”(rest)两种角色。理解这两种用途,能极大简化数组、对象和函数参数的常见操作。
11.2.1 扩展运算符(Spread)
扩展运算符将一个可迭代对象(如数组、字符串、Set、Map)或对象展开为逗号分隔的序列。它只能用在函数调用、数组字面量或对象字面量中。
数组展开
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
// 合并数组(等价于 const merged = arr1.concat(arr2))
const merged = [...arr1, ...arr2]; // [1,2,3,4,5,6]
// 浅拷贝数组
const copy = [...arr1]; // [1,2,3],与 arr1 不同引用
// 将数组元素作为独立参数传给函数
console.log(...arr1); // 相当于 console.log(1, 2, 3)
Math.max(...arr1); // 相当于 Math.max(1, 2, 3)
对象展开(ES2018)
对象展开将源对象自身的可枚举属性浅拷贝到目标对象中。
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 copy = { ...obj1 }; // { a:1, b:2 }
// 添加或覆盖属性
const updated = { ...obj1, b: 99, e: 5 }; // { a:1, b:99, e:5 }
字符串展开
字符串也是可迭代对象,... 可以将其拆分为字符数组。
const chars = [..."hello"]; // ['h','e','l','l','o']
实际场景
- 不可变数据更新:在 React/Vue 中更新状态时,常用展开避免直接修改原对象。
- 函数参数展开:将数组元素作为参数传入函数。
- 类数组转换:
[...arguments]或[...nodeList]将类数组转为真正的数组。
11.2.2 剩余运算符(Rest)
剩余运算符将多个独立元素收集到一个数组中。它只出现在函数参数定义和解构赋值的左侧。
函数剩余参数
将不确定数量的参数收集为数组,彻底取代 arguments 对象。
function sum(...numbers) {
return numbers.reduce((acc, cur) => acc + cur, 0);
}
sum(1, 2, 3); // 6
sum(10, 20); // 30
sum(); // 0(空数组)
优势:
- 剩余参数是真正的数组,可以直接使用数组方法,无需
Array.prototype.slice.call(arguments)。 - 箭头函数中没有
arguments对象,剩余参数是唯一选择。 - 可以和其他命名参数结合,但必须放在最后。
function greet(greeting, ...names) {
return `${greeting}, ${names.join(' and ')}`;
}
greet('Hello', 'Alice', 'Bob'); // "Hello, Alice and Bob"
解构剩余参数
在数组或对象解构时,将未匹配的剩余元素收集到新变量中。
const [first, second, ...others] = [1, 2, 3, 4, 5];
console.log(first); // 1
console.log(second); // 2
console.log(others); // [3,4,5]
const { a, b, ...rest } = { a: 1, b: 2, c: 3, d: 4 };
console.log(a); // 1
console.log(rest); // { c:3, d:4 }
对象解构中的 ...rest 会收集所有未被提取的自身可枚举属性,形成一个新对象。常用于分离出某些已知属性。
11.2.3 区分扩展与剩余的秘诀
关键看 ... 出现在哪里、做什么:
- 扩展(Spread):放在“值的提供方”——函数调用中的实参列表、数组/对象字面量内部。将整体拆成个体。
- 剩余(Rest):放在“值的收集方”——函数形参定义、解构赋值左侧。将多个个体合成一个数组/对象。
// 扩展:在函数调用时展开数组
const nums = [1, 2, 3];
Math.max(...nums);
// 剩余:在函数定义时收集参数
function logAll(...args) { console.log(args); }
11.2.4 注意事项
- 浅拷贝本质:无论数组还是对象的展开,都只做一层浅拷贝。如果源对象中包含引用类型(嵌套对象、数组),拷贝后的对象仍共享同一引用。
const original = { a: 1, b: { c: 2 } };
const copy = { ...original };
copy.b.c = 99;
console.log(original.b.c); // 99,两个对象的 b 指向同一个内部对象
处理深层嵌套数据时,应使用深拷贝库(如 lodash.cloneDeep)或手动递归拷贝。
- 只能展开可迭代对象:数组、字符串、Map、Set、arguments 等拥有
Symbol.iterator的对象才可以在数组字面量或函数调用中使用...展开。对象展开依赖的是{...obj}语法,它基于对象自身的可枚举属性,而非迭代协议。
- 剩余参数必须位于最后:
function fn(a, ...rest, b) {}会直接报错,rest必须是最后一个形参。
- 解构中的
...操作优先级:剩余元素收集会将剩余值放入一个新数组/对象中,不影响前面的解构。
掌握扩展与剩余运算符,你的代码将从繁琐的 concat、slice、apply 中解放出来,变得更加简洁和可读。