ES6+ 里的 “万能三个点”:原来 ... 能搞定数组、对象和函数!

在 JavaScript 中,三个点(... 是 扩展运算符(Spread Operator) 和 剩余参数(Rest Parameters) 的共同语法符号,核心区别在于 使用场景和作用方向

  • 扩展运算符(Spread):"展开" 可迭代对象(数组、字符串、对象等),将其元素 / 属性分散到新的上下文(如数组、函数参数)中。
  • 剩余参数(Rest):"收集" 函数多余的参数或数组 / 对象解构时的剩余部分,合并为一个数组 / 对象。

下面分场景详细解析,结合示例说明用法、注意事项和边界情况。

一、扩展运算符(Spread Operator):展开元素 / 属性

核心作用:将可迭代对象(Iterable)拆分为独立元素,或 将对象的可枚举属性拆分为键值对,用于创建新数组、新对象或传递函数参数。

1. 场景 1:操作数组(最常用)

(1)创建数组副本(浅拷贝)

避免直接赋值导致的引用传递问题:

const arr1 = [1, 2, 3];
const arr2 = [...arr1]; // 等价于 arr1.slice(),创建浅拷贝
arr2.push(4);
console.log(arr1); // [1,2,3](原数组不受影响)
console.log(arr2); // [1,2,3,4]
(2)合并多个数组

替代 concat(),语法更简洁,支持任意位置插入:

const arrA = [1, 2];
const arrB = [3, 4];
const arrC = [0, ...arrA, 2.5, ...arrB, 5]; // [0,1,2,2.5,3,4,5]
(3)将数组转为函数参数

替代 apply(),直接传递数组元素作为独立参数:

const nums = [10, 20, 30];
// 需求:求数组最大值(Math.max 不接收数组,需独立参数)
const max = Math.max(...nums); // 等价于 Math.max(10,20,30) → 30

// 自定义函数示例
function sum(a, b, c) {
  return a + b + c;
}
sum(...nums); // 60
(4)展开类数组 / 可迭代对象

支持所有实现 Iterable 接口的对象(字符串、Set、Map、NodeList 等):

// 字符串 → 字符数组
const str = "abc";
const strArr = [...str]; // ['a','b','c']

// Set → 数组(自动去重)
const set = new Set([1, 2, 2, 3]);
const setArr = [...set]; // [1,2,3]

// NodeList → 数组(DOM 元素集合)
const lis = document.querySelectorAll("li");
const lisArr = [...lis]; // 转为数组,可使用数组方法(forEach、map 等)

2. 场景 2:操作对象(ES2018 新增)

核心作用:复制对象的可枚举属性,用于创建对象副本、合并对象,或覆盖属性。

(1)创建对象副本(浅拷贝)

替代 Object.assign({}, obj),语法更简洁:

const user = { name: "张三", age: 20 };
const userCopy = { ...user }; // 浅拷贝
userCopy.age = 21;
console.log(user); // { name: "张三", age: 20 }(原对象不受影响)
(2)合并多个对象

后面对象的属性会覆盖前面的同名属性:

const obj1 = { a: 1, b: 2 };
const obj2 = { b: 3, c: 4 };
const mergedObj = { ...obj1, ...obj2 }; // { a:1, b:3, c:4 }(obj2 的 b 覆盖 obj1 的 b)
(3)扩展对象时添加新属性

可在展开后直接添加 / 修改属性,灵活性更高:

const base = { x: 1, y: 2 };
const extended = {
  ...base, // 复制原有属性
  z: 3, // 新增属性
  y: 20 // 修改原有属性
};
console.log(extended); // { x:1, y:20, z:3 }
注意:对象扩展的限制
  • 仅复制 自身可枚举属性(继承的属性、不可枚举属性不会复制);
  • 浅拷贝:如果对象属性是引用类型(数组、对象等),仅复制引用,修改会影响原对象:
    const obj = { info: { age: 20 } };
    const copy = { ...obj };
    copy.info.age = 21;
    console.log(obj.info.age); // 21(原对象被修改,因为 info 是引用类型)
    

3. 场景 3:与解构赋值结合

在数组 / 对象解构时,用扩展运算符提取剩余元素(本质是「剩余参数」的一种应用,见下文):

// 数组解构:提取前两个元素,剩余元素合并为 newArr
const [a, b, ...newArr] = [1, 2, 3, 4, 5];
console.log(a); // 1,b:2,newArr: [3,4,5]

// 对象解构:提取 name 属性,剩余属性合并为 restObj
const { name, ...restObj } = { name: "李四", age: 25, gender: "男" };
console.log(name); // "李四",restObj: { age:25, gender:"男" }
解构时的注意事项
  • 扩展运算符必须放在 解构模式的最后一位,否则报错:
    const [a, ...b, c] = [1,2,3,4]; // SyntaxError: Rest element must be last in destructuring pattern
    
  • 对象解构时,扩展运算符收集的是 自身可枚举属性,不包含继承属性。

二、剩余参数(Rest Parameters):收集多余元素

核心作用:将函数的多余参数 或 解构时的剩余部分 合并为一个数组(数组解构)/ 对象(对象解构)。注意:剩余参数仅在 函数参数列表 或 解构赋值 中使用,且必须是最后一个元素。

1. 场景 1:函数的剩余参数

替代 arguments 对象,更清晰地收集函数的可变参数(ES6 推荐用法)。

(1)基本用法:收集所有多余参数
// 需求:求任意个数字的和
function sum(...nums) {
  // nums 是一个数组,包含所有传入的参数
  return nums.reduce((total, num) => total + num, 0);
}

sum(1); // 1(nums: [1])
sum(1, 2, 3); // 6(nums: [1,2,3])
sum(5, 10, 15, 20); // 50(nums: [5,10,15,20])
(2)与固定参数结合使用

剩余参数必须放在固定参数之后:

// 固定参数 name,剩余参数 args 收集后续所有参数
function greet(name, ...args) {
  console.log(`Hello, ${name}!`);
  console.log("额外参数:", args);
}

greet("王五", 28, "北京", "程序员"); 
// Hello, 王五!
// 额外参数:[28, "北京", "程序员"]
(3)替代 arguments 对象

arguments 是类数组对象,需手动转为数组;剩余参数直接是数组,更易用:

// ES5 用 arguments(繁琐)
function oldSum() {
  return Array.from(arguments).reduce((a, b) => a + b, 0);
}

// ES6 用剩余参数(简洁)
function newSum(...nums) {
  return nums.reduce((a, b) => a + b, 0);
}
注意:剩余参数的限制
  • 一个函数只能有一个剩余参数;
  • 剩余参数必须是函数参数列表的最后一个;
  • 箭头函数不支持 arguments,但可使用剩余参数:
    const arrowSum = (...nums) => nums.reduce((a, b) => a + b, 0);
    

2. 场景 2:数组 / 对象解构中的剩余参数

本质是扩展运算符的「反向应用」,将解构后剩余的元素合并为一个数组 / 对象(前文已举例,此处补充细节):

// 数组解构:剩余参数必须是最后一个
const [first, ...rest] = [10, 20, 30, 40];
console.log(rest); // [20, 30, 40]

// 对象解构:剩余参数收集剩余的自身可枚举属性
const person = { name: "赵六", age: 30, address: { city: "上海" } };
const { address, ...personRest } = person;
console.log(personRest); // { name: "赵六", age: 30 }

三、扩展运算符 vs 剩余参数:核心区别

特性扩展运算符(Spread)剩余参数(Rest)
作用方向展开(拆分)可迭代对象 / 对象属性收集(合并)多余元素 / 参数
使用场景数组 / 对象创建、函数参数传递函数参数列表、数组 / 对象解构
结果类型多个独立元素 / 键值对一个数组(函数 / 数组解构)或对象(对象解构)
位置要求无(可在任意位置展开)必须是最后一个元素(函数参数 / 解构中)
语法本质「分散」操作「聚合」操作

记忆口诀

  • 看到 ... 后面是「可迭代对象 / 对象」→ 扩展运算符(展开);
  • 看到 ... 后面是「变量名」→ 剩余参数(收集)。

四、常见误区与边界情况

1. 扩展 null/undefined

  • 数组 / 函数参数中展开 null/undefined:不会报错,会被忽略(因为它们不是可迭代对象);
  • 对象中展开 null/undefined:同样被忽略,不会复制任何属性。
// 数组中展开 null/undefined
const arr = [1, ...null, 2, ...undefined];
console.log(arr); // [1, 2]

// 函数参数中展开 null/undefined
Math.max(...null, 10); // 10(null 被忽略)

// 对象中展开 null/undefined
const obj = { ...null, a: 1, ...undefined };
console.log(obj); // { a: 1 }

2. 扩展不可迭代对象(数组 / 函数参数场景)

如果在数组或函数参数中展开 非可迭代对象(如数字、布尔值),会报错:

const arr = [1, ...2]; // TypeError: 2 is not iterable
Math.max(...3); // TypeError: 3 is not iterable

3. 对象扩展 vs Object.assign ()

两者功能类似(浅拷贝、合并对象),但有细微区别:

  • Object.assign() 会触发目标对象的 setter 方法,... 扩展不会;
  • ... 扩展仅复制自身可枚举属性,与 Object.assign() 一致;
  • 语法上,... 更简洁,支持中间插入新属性。

4. 数组扩展 vs 数组方法(slice/concat)

  • 浅拷贝:[...arr] 等价于 arr.slice()
  • 合并数组:[...a, ...b] 等价于 a.concat(b),但 ... 支持更灵活的拼接(如中间插入元素)。

五、总结

... 是 JavaScript 中极具灵活性的语法,核心分为两类用法:

  1. 扩展运算符:用于「展开」数据(数组、字符串、对象等),简化拷贝、合并、参数传递逻辑;
  2. 剩余参数:用于「收集」多余元素(函数参数、解构剩余部分),替代 arguments,让代码更清晰。

掌握其使用场景和边界情况,能大幅提升代码简洁性和可读性(尤其在处理数组、对象和可变参数时)。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

canjun_wen

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值