一文吃透 JavaScript ES6 扩展运算符:从基础用法到进阶实战

在 JavaScript 生态中,有很多 “看似简单却暗藏玄机” 的语法,扩展运算符(...,三个连续的点)就是其中之一。它诞生于 ES6 标准,凭借简洁的语法和灵活的功能,迅速成为开发者处理数组、对象、函数参数的 “利器”。但不少人对它的理解只停留在 “复制数组”,却忽略了它在复杂场景下的妙用。今天这篇文章,我们就从基础到进阶,全面拆解扩展运算符的用法、原理与避坑指南。

一、先搞懂:扩展运算符到底是什么?

扩展运算符(Spread Operator)的核心作用,是 **“将一个可迭代对象(Iterable)的元素‘展开’成独立的个体”**,或者 “将多个独立的值‘收集’成一个对象 / 数组”(注:后者更偏向 “剩余参数”,但语法与扩展运算符一致,后文会区分)。

首先要明确:不是所有场景都能使用扩展运算符,它只能作用于 “可迭代对象”—— 即实现了 Symbol.iterator 接口的数据类型,比如:

  • 数组(Array)
  • 字符串(String)
  • 集合(Set/Map)
  • arguments 对象(函数的类数组参数集合)
  • NodeList(DOM 元素集合,如 document.querySelectorAll 的返回值)

而普通对象(Object)本身不是可迭代对象,但 ES2018 标准为对象新增了扩展运算符支持,允许我们 “展开” 对象的键值对(这是一个特殊场景,需要单独注意)。

二、基础用法:3 个最常用的场景

扩展运算符的入门门槛很低,掌握以下 3 个场景,就能覆盖 80% 的日常开发需求。

1. 处理数组:复制、合并与解构

数组是扩展运算符最经典的应用场景,解决了传统数组操作(如 concatslice)语法繁琐的问题。

(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 前)、nullundefined 使用,会报错:

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 代码,慢慢就能体会到它的便利~

评论
成就一亿技术人!
拼手气红包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、付费专栏及课程。

余额充值