在 JavaScript 生态中,有很多 “看似简单却暗藏玄机” 的语法,扩展运算符(...,三个连续的点)就是其中之一。它诞生于 ES6 标准,凭借简洁的语法和灵活的功能,迅速成为开发者处理数组、对象、函数参数的 “利器”。但不少人对它的理解只停留在 “复制数组”,却忽略了它在复杂场景下的妙用。今天这篇文章,我们就从基础到进阶,全面拆解扩展运算符的用法、原理与避坑指南。
一、先搞懂:扩展运算符到底是什么?
扩展运算符(Spread Operator)的核心作用,是 **“将一个可迭代对象(Iterable)的元素‘展开’成独立的个体”**,或者 “将多个独立的值‘收集’成一个对象 / 数组”(注:后者更偏向 “剩余参数”,但语法与扩展运算符一致,后文会区分)。
首先要明确:不是所有场景都能使用扩展运算符,它只能作用于 “可迭代对象”—— 即实现了 Symbol.iterator 接口的数据类型,比如:
- 数组(Array)
- 字符串(String)
- 集合(Set/Map)
- arguments 对象(函数的类数组参数集合)
- NodeList(DOM 元素集合,如
document.querySelectorAll的返回值)
而普通对象(Object)本身不是可迭代对象,但 ES2018 标准为对象新增了扩展运算符支持,允许我们 “展开” 对象的键值对(这是一个特殊场景,需要单独注意)。
二、基础用法:3 个最常用的场景
扩展运算符的入门门槛很低,掌握以下 3 个场景,就能覆盖 80% 的日常开发需求。
1. 处理数组:复制、合并与解构
数组是扩展运算符最经典的应用场景,解决了传统数组操作(如 concat、slice)语法繁琐的问题。
(1)浅复制数组
传统复制数组需要用 slice(0) 或 concat(),而扩展运算符一行代码就能搞定:
javascript
运行
// 原数组
const arr1 = [1, 2, 3];
// 扩展运算符复制(浅复制)
const arr2 = [...arr1];
console.log(arr2); // [1, 2, 3]
console.log(arr1 === arr2); // false(新数组,地址不同)
⚠️ 注意:这是浅复制!如果数组中包含引用类型(如对象、子数组),复制的只是引用地址,修改子元素会影响原数组:
javascript
运行
const arr1 = [1, { name: "张三" }];
const arr2 = [...arr1];
arr2[1].name = "李四";
console.log(arr1[1].name); // 李四(原数组的子对象被修改)
如果需要深复制,需结合 JSON.parse(JSON.stringify()) 或 structuredClone()(针对复杂类型)。
(2)合并数组
无需再用 arr1.concat(arr2, arr3),扩展运算符可以直观地合并多个数组:
javascript
运行
const arr1 = [1, 2];
const arr2 = [3, 4];
const arr3 = [5];
// 合并为新数组
const mergedArr = [...arr1, ...arr2, ...arr3];
console.log(mergedArr); // [1, 2, 3, 4, 5]
// 也可以在合并时插入新元素
const mergedArrWithNew = [0, ...arr1, "中间值", ...arr2];
console.log(mergedArrWithNew); // [0, 1, 2, "中间值", 3, 4]
(3)数组解构赋值
在解构数组时,扩展运算符可以 “收集” 剩余的元素,形成一个新数组(这里更偏向 “剩余参数” 的用法,但语法一致):
javascript
运行
const [first, second, ...rest] = [1, 2, 3, 4, 5];
console.log(first); // 1
console.log(second); // 2
console.log(rest); // [3, 4, 5](剩余元素被收集)
⚠️ 注意:剩余元素必须放在解构的最后一位,否则会报错:
javascript
运行
const [first, ...rest, last] = [1, 2, 3, 4]; // SyntaxError: Rest element must be last element
2. 处理对象:复制与合并(ES2018+)
ES2018 后,扩展运算符支持作用于普通对象,核心是 “展开对象的可枚举属性(Enumerable Properties)”,常用于对象的复制与合并。
(1)浅复制对象
替代传统的 Object.assign({}, obj),语法更简洁:
javascript
运行
const obj1 = { name: "张三", age: 20 };
// 扩展运算符复制(浅复制)
const obj2 = { ...obj1 };
console.log(obj2); // { name: "张三", age: 20 }
console.log(obj1 === obj2); // false(新对象,地址不同)
同样需要注意浅复制问题:如果对象的属性是引用类型(如子对象、数组),复制的是引用地址:
javascript
运行
const obj1 = { name: "张三", hobbies: ["篮球", "游戏"] };
const obj2 = { ...obj1 };
obj2.hobbies.push("读书");
console.log(obj1.hobbies); // ["篮球", "游戏", "读书"](原对象的子数组被修改)
(2)合并对象
合并多个对象时,后面的对象会覆盖前面重复的属性(这一点与 Object.assign 一致):
javascript
运行
const obj1 = { name: "张三", age: 20 };
const obj2 = { age: 22, gender: "男" };
const obj3 = { city: "北京" };
// 合并对象:age 属性被 obj2 覆盖
const mergedObj = { ...obj1, ...obj2, ...obj3 };
console.log(mergedObj);
// { name: "张三", age: 22, gender: "男", city: "北京" }
这个特性非常适合 “默认配置 + 自定义配置” 的场景,比如函数参数配置:
javascript
运行
// 默认配置
const defaultConfig = { timeout: 1000, method: "GET" };
// 自定义配置(覆盖 timeout)
const customConfig = { timeout: 3000 };
// 合并配置:自定义配置优先级更高
const finalConfig = { ...defaultConfig, ...customConfig };
console.log(finalConfig); // { timeout: 3000, method: "GET" }
3. 处理函数参数:简化 arguments
在函数调用时,扩展运算符可以将数组 “展开” 成独立的参数,替代传统的 apply 方法。
比如,我们要调用 Math.max() 求数组中的最大值,传统写法需要用 Math.max.apply(null, arr),而扩展运算符可以直接写:
javascript
运行
const numbers = [10, 5, 20, 8];
// 传统写法:apply 展开数组
const max1 = Math.max.apply(null, numbers);
// 扩展运算符写法:更简洁
const max2 = Math.max(...numbers);
console.log(max1, max2); // 20 20
再比如,函数需要多个参数时,直接展开数组作为参数:
javascript
运行
// 函数:计算三个数的和
function sum(a, b, c) {
return a + b + c;
}
const nums = [1, 2, 3];
console.log(sum(...nums)); // 6(等价于 sum(1, 2, 3))
三、进阶用法:这些场景能提升效率
掌握基础后,扩展运算符在一些复杂场景下能大幅简化代码,比如处理集合、DOM 元素,甚至与 React 结合。
1. 处理可迭代对象:Set、Map、字符串
扩展运算符支持所有可迭代对象,因此可以轻松将 Set/Map/ 字符串转换为数组。
(1)Set 转数组(去重)
Set 本身是 “无重复元素的集合”,结合扩展运算符可以快速实现数组去重:
javascript
运行
const arrWithDuplicates = [1, 2, 2, 3, 3, 3];
// Set 去重后转数组
const uniqueArr = [...new Set(arrWithDuplicates)];
console.log(uniqueArr); // [1, 2, 3]
(2)Map 转数组
Map 的每个元素是 [key, value] 形式的数组,扩展运算符可以直接展开:
javascript
运行
const map = new Map([
["name", "张三"],
["age", 20]
]);
// Map 转数组
const mapArr = [...map];
console.log(mapArr); // [["name", "张三"], ["age", 20]]
// 只取 key 或 value
const keys = [...map.keys()]; // ["name", "age"]
const values = [...map.values()]; // ["张三", 20]
(3)字符串转数组
传统字符串转数组用 split(''),扩展运算符也能实现,且对 Unicode 字符更友好(比如 emoji):
javascript
运行
const str = "hello";
const strArr1 = [...str];
const strArr2 = str.split('');
console.log(strArr1); // ["h", "e", "l", "l", "o"]
console.log(strArr2); // ["h", "e", "l", "l", "o"]
// 对 emoji 友好(split 可能会拆分 emoji 的 Unicode 码点)
const emojiStr = "😀😂";
console.log([...emojiStr]); // ["😀", "😂"]
console.log(emojiStr.split('')); // ["\uD83D", "\uDE00", "\uD83D", "\uDE02"](错误拆分)
2. 与 React 结合:传递 props 与状态更新
在 React 开发中,扩展运算符是 “传递多个 props” 和 “更新状态(尤其是对象 / 数组状态)” 的常用工具。
(1)批量传递 props
如果组件需要多个 props,无需逐个传递,用扩展运算符批量传递:
javascript
运行
// 父组件
const user = { name: "张三", age: 20, gender: "男" };
// 批量传递 props:等价于 <User name={user.name} age={user.age} gender={user.gender} />
<User {...user} />
// 也可以在批量传递后覆盖某个 prop
<User {...user} age={22} /> // age 最终为 22
(2)更新数组 / 对象状态
React 状态(useState)是不可变的,更新数组 / 对象时不能直接修改原状态,需要创建新对象 / 数组。扩展运算符可以轻松实现:
javascript
运行
// 1. 更新数组状态(添加元素)
const [list, setList] = useState([1, 2, 3]);
// 错误:直接修改原数组
// list.push(4);
// setList(list);
// 正确:用扩展运算符创建新数组
setList([...list, 4]); // 新增元素到末尾
setList([0, ...list]); // 新增元素到开头
// 2. 更新对象状态(修改属性)
const [user, setUser] = useState({ name: "张三", age: 20 });
// 正确:用扩展运算符创建新对象,覆盖属性
setUser({ ...user, age: 21 });
四、避坑指南:这些错误别再犯
扩展运算符虽然好用,但如果不注意细节,很容易踩坑。以下是 3 个最常见的错误场景:
1. 混淆 “扩展运算符” 与 “剩余参数”
扩展运算符(...)在不同场景下有不同含义,最容易混淆的是 “扩展” 和 “剩余”:
- 扩展(Spread):将可迭代对象 “拆分成独立元素”,用于数组 / 对象字面量、函数调用。例:
const newArr = [...oldArr];(拆分成元素后组成新数组) - 剩余(Rest):将多个独立值 “收集成一个数组”,用于函数参数、数组解构。例:
function sum(...args) { return args.reduce((a,b)=>a+b) }(收集参数成数组args)
简单区分:放在 “赋值号左边” 或 “函数参数” 中是剩余,放在 “赋值号右边” 或 “函数调用” 中是扩展。
2. 试图扩展 “不可迭代对象”
扩展运算符只能作用于可迭代对象,如果对普通对象(ES2018 前)、null、undefined 使用,会报错:
javascript
运行
// 错误:ES2018 前不支持对象扩展(现在支持,但需注意环境)
const obj = { name: "张三" };
const newArr = [...obj]; // TypeError: obj is not iterable(旧环境)
// 错误:null/undefined 不可迭代
const arr1 = [...null]; // TypeError: null is not iterable
const arr2 = [...undefined]; // TypeError: undefined is not iterable
3. 忽略 “浅复制” 的局限性
前面多次提到,扩展运算符实现的是 “浅复制”,如果原对象 / 数组包含嵌套的引用类型(如子对象、子数组),修改新对象的嵌套属性会影响原对象。
解决浅复制问题的方案:
- 简单场景:用
JSON.parse(JSON.stringify(original))(但不支持函数、Symbol、循环引用)。 - 复杂场景:用
structuredClone(original)(支持循环引用、Blob、RegExp 等,但不支持函数)。 - 生产环境:用 Lodash 的
_.cloneDeep(original)(最稳定,支持所有类型)。
五、总结:扩展运算符的核心价值
回顾全文,扩展运算符的核心价值在于 **“用简洁的语法替代繁琐的传统操作”**:
- 替代
slice/concat处理数组,替代Object.assign处理对象。 - 替代
apply传递函数参数,避免Math.max.apply(null, arr)这样的 “反直觉” 写法。 - 简化可迭代对象(Set/Map/ 字符串)与数组的转换,提升代码可读性。
它不是 “银弹”,但却是 “效率工具”—— 掌握它的用法和边界,能让你的 JavaScript 代码更简洁、更优雅。最后,建议你在实际项目中多尝试,比如用它重构旧的 concat/apply 代码,慢慢就能体会到它的便利~

9490

被折叠的 条评论
为什么被折叠?



