← 返回题目列表

展开运算符和剩余参数有什么区别?

高频 简单 第 4 / 28 题 更新于 2026/07/27
ES6展开运算符剩余参数

简化版

... 在不同位置含义不同:用在函数形参或解构左侧是收集剩余值,叫剩余参数;用在调用、数组、对象字面量里是展开已有值。一个是“收起来”,一个是“摊开来”。

详细版

剩余参数:

function sum(...nums) {
  return nums.reduce((a, b) => a + b, 0);
}

数组展开:

const arr = [1, 2];
const next = [...arr, 3];

对象展开:

const user = { name: 'Tom' };
const copy = { ...user, age: 18 };

注意对象和数组展开都是浅拷贝,只复制第一层;嵌套对象仍然共享引用。

完整版教学

一、同一个符号看位置

... 不是固定叫展开运算符。它在收集位置叫 rest,在展开位置叫 spread。

const [first, ...rest] = list; // 收集
fn(...args); // 展开

判断方法很简单:在赋值左边或形参里,多半是收集;在值表达式里,多半是展开。

二、替代 arguments

剩余参数是真数组,比 arguments 更好用:

function log(level, ...messages) {
  messages.forEach(msg => console.log(level, msg));
}

它还能和普通参数配合,但必须放在最后。

三、对象展开的覆盖规则

对象展开按从左到右覆盖:

const options = { ...defaults, ...userOptions };

后面的同名属性覆盖前面。配置合并中非常常见。

四、浅拷贝风险

const a = { nested: { x: 1 } };
const b = { ...a };
b.nested.x = 2;
console.log(a.nested.x); // 2

展开只复制引用,不复制深层对象。需要深拷贝时不能只靠 ...

五、面试追问与工程落地

展开运算符常被追问“对象展开和 Object.assign 是否完全一样”。它们很接近,都会做浅拷贝并触发 getter,但在语义和细节上不是完全等价。对象展开创建新对象更常用于不可变更新,Object.assign 可以指定目标对象,可能直接修改第一个参数。

还会问“展开能不能复制 Symbol 属性”。对象展开会复制自身可枚举属性,包括可枚举的 Symbol key;但不会复制不可枚举属性,也不会复制原型链上的属性。

工程里对象展开常用于合并配置,但要注意覆盖顺序。{ ...defaults, ...options } 是用户配置覆盖默认值,反过来就会让默认值覆盖用户输入。这个顺序错误在线上非常隐蔽。

六、区分迭代展开、属性复制与参数收集

数组字面量和函数调用中的展开要求右侧可迭代,因此 [...null] 会抛出 TypeError;对象字面量展开走的是自有可枚举属性复制,{...null} 则直接忽略空值。剩余参数把实参尾部收集成真正的数组,而对象剩余属性会排除已经取走的键。

const source = {
  get value() { return 2 },
  [Symbol.for('tag')]: 'ok'
}
const copy = { ...source }
console.log(copy.value) // 2,复制的是 getter 求出的值
console.log(Object.getOwnPropertySymbols(copy).length) // 1
写法输入协议结果
[...items]可迭代协议新数组元素
fn(...items)可迭代协议多个实参
{...source}自有可枚举字符串和 Symbol 属性新对象属性
function f(...args)剩余实参真数组

对象展开与 Object.assign({}, source) 都是浅层复制并会读取源 getter,但写入目标的细节并不完全相同:对象展开创建数据属性,而 Object.assign 使用普通赋值语义,可能触发目标对象已有的 setter。两者都不会保留源属性描述符,也不会递归复制嵌套对象。

若对象有 2 层嵌套,{ ...state } 只新建最外层 1 个对象,内层引用仍共享。更新状态时应沿被修改路径逐层复制,或使用明确的不可变更新工具,不能把浅拷贝当深拷贝。

判断 ... 的第一步永远是看语法位置:形参和赋值模式中是 rest,调用和字面量中是 spread,随后再判断它使用迭代协议还是属性复制。

七、常见误区与追问

  • 误区:展开运算符可以完成深拷贝。 它只复制一层,嵌套对象、数组和函数仍共享引用。
  • 误区:对象展开只复制字符串键。 自有、可枚举的 Symbol 键也会被复制。
  • 误区:对象展开和 Object.assign 在任何情况下都等价。 二者目标属性的写入语义不同,遇到 setter 时可能产生不同结果。
  • 追问:为什么数组能展开字符串却不能展开普通对象? 字符串实现了可迭代协议,普通对象默认没有 Symbol.iterator
  • 追问:展开 null 在数组和对象中结果一样吗? 不一样;数组展开要求可迭代值并报错,对象展开会忽略 nullundefined
  • 追问:多个对象展开时同名属性取哪个? 按从左到右写入,后出现的属性覆盖先前属性,但这仍是浅层覆盖。
  • 追问:rest 参数比 arguments 好在哪里? rest 是真正数组、只收集未命名的剩余实参,并且适用于箭头函数。

八、加强记忆

... 的口诀是“左边收,右边放”。形参和解构里收集剩余值,调用和字面量里展开已有值;同时别忘了它只是浅拷贝。

看到三个点先确认它位于模式还是表达式,再确认走迭代协议还是属性复制,绝大多数边界题都可据此推导。