从零搞懂 ES6 展开运算符:不只是...那么简单
你有没有写过这样的代码?
const result = Array.prototype.slice.call(arguments);或者为了合并两个数组,翻出文档查concat的用法?又或者在 React 组件里为了一键透传所有 props,绞尽脑汁地手动列出来?
如果你点头了——恭喜,这篇文章就是为你准备的。我们不讲术语堆砌,也不照搬 MDN 文档,而是用实战视角重新认识那个看似简单、实则威力巨大的语法糖:展开运算符(Spread Operator)。
它不是什么高深莫测的新技术,但却是现代 JavaScript 开发中出现频率最高、最容易被低估的工具之一。掌握它,你的代码会变得更干净、更安全、更具函数式风格。
什么是展开运算符?先别急着看定义
我们先来看一个最直观的例子:
const nums = [1, 2, 3]; console.log(nums); // [1, 2, 3] console.log(...nums); // 1 2 3注意第二行输出的结果:1 2 3,不再是数组,而是三个独立的参数。
换句话说,...做了一件事:把“打包”的数据“拆开”。
这个“拆”的过程,就是“展开”。
它的正式名字叫Spread Syntax(展开语法),写成...variable,作用是将可迭代对象或可枚举对象中的每一项“拉平”成单独的值,插入到当前表达式中。
✅ 支持类型包括:数组、字符串、Set、Map、arguments、NodeList、TypedArray,以及普通对象(ES2018 起支持)。
❌ 不支持null和undefined,否则会报错。
它是怎么工作的?背后没有魔法
JavaScript 引擎内部有一套叫做Iterator Protocol(迭代协议)的机制。任何实现了[Symbol.iterator]()方法的对象,都可以被for...of遍历,也就可以被...展开。
比如数组:
[1, 2, 3][Symbol.iterator](); // 返回一个迭代器所以当你写:
Math.max(...[1, 5, 3]);引擎实际上做了这件事:
Math.max(1, 5, 3); // 相当于直接传参整个过程由语言层面自动完成,无需手动循环,既高效又语义清晰。
真正强大的地方:7 个高频使用场景
与其死记硬背语法,不如直接上战场。下面这些例子,全来自真实项目中的高频痛点。
场景一:数组合并与复制 —— 再也不用.concat()了
传统方式:
const arr1 = [1, 2]; const arr2 = [3, 4]; const merged = arr1.concat(arr2); // [1,2,3,4]ES6 写法:
const merged = [...arr1, ...arr2]; // 更直观,还能加新元素 const extended = [...arr1, 0, ...arr2]; // [1,2,0,3,4]复制数组呢?
const copy = [...arr1]; // 比 slice() 还简洁这招在 React 或 Redux 中尤其重要:永远不要直接修改 state。通过展开创建新数组,实现不可变更新(immutability),让状态变化可追踪、可调试。
⚠️ 注意:这是浅拷贝!如果数组里有对象,改子项仍会影响原数据:
js const nested = [{ name: 'Alice' }]; const copy = [...nested]; copy[0].name = 'Bob'; console.log(nested[0].name); // 'Bob' → 原数组也被改了!
深层嵌套需配合递归或库(如 immer),但大多数情况下,浅拷贝已足够。
场景二:字符串也能展开?对,而且比 split 更靠谱
你想把"hello"变成['h','e','l','l','o'],可能会这么写:
"hello".split(''); // ['h','e','l','l','o']但遇到 emoji 呢?
"👨👩👧".split(''); // 结果可能是一堆乱码字符,因为 emoji 是组合符号而展开运算符能正确识别 Unicode 复合字符(在支持环境下):
[..."👨👩👧"]; // ['👨👩👧'] → 正确识别为一个整体所以在做密码强度检测、输入过滤、文本分析时,优先考虑[...str]而非split('')。
场景三:函数传参新姿势 —— 告别.apply(null, args)
以前想把数组当参数传给函数,得靠apply:
function sum(a, b, c) { return a + b + c; } const args = [1, 2, 3]; sum.apply(null, args); // 6现在只需一行:
sum(...args); // 6,干净利落尤其调用内置函数时特别方便:
Math.max(...[1, 8, 5, 9]); // 9 Math.min(...[1, 8, 5, 9]); // 1⚠️ 小心陷阱:JavaScript 引擎对函数参数数量有限制(通常约 65536)。超大数组别硬来,改用
reduce:
js const max = largeArr.reduce((a, b) => Math.max(a, b), -Infinity);
场景四:对象克隆与合并 ——{...obj}比Object.assign更优雅
要复制一个对象,老办法是:
const user = { name: 'Alice', age: 25 }; const clone = Object.assign({}, user);现在可以:
const clone = { ...user }; // 一样的效果,更短合并配置项也很常见:
const defaults = { theme: 'light', lang: 'zh', timeout: 5000 }; const userPrefs = { lang: 'en', theme: 'dark' }; const config = { ...defaults, ...userPrefs }; // { theme: 'dark', lang: 'en', timeout: 5000 }后出现的属性覆盖前面的,逻辑清晰,适合默认配置 + 用户自定义的场景。
🔍 注意细节:
- 只复制自身可枚举属性,原型链上的不会被带上;
-Symbol类型的 key 不会被展开;
- 同样是浅拷贝,对象内的引用仍然共享。
场景五:和剩余参数(Rest Parameters)搭档,写出灵活 API
展开运算符有个好兄弟叫剩余参数(Rest),长得一样...,但用途相反:它是“收拢”而不是“展开”。
function log(prefix, ...messages) { messages.forEach(msg => console.log(`[${prefix}] ${msg}`)); } log('INFO', '启动中...', '加载资源'); // 输出: // [INFO] 启动中... // [INFO] 加载资源这里...messages把剩下的参数收集成数组,再配合forEach使用。
更进一步,我们可以封装一个带时间戳的日志装饰器:
const withTimestamp = (fn, prefix) => (...args) => { fn(prefix, new Date().toISOString(), ...args); }; const timedLog = withTimestamp(console.log, 'TIMESTAMP'); timedLog('应用启动'); // TIMESTAMP 2025-04-05T10:00:00Z 应用启动看到没?...args接收所有参数,...args又把它展开传递下去——形成完美闭环。
这类模式广泛用于中间件、埋点、权限拦截等需要动态处理参数的场景。
场景六:DOM NodeList 转数组?一行搞定
你一定遇到过这个问题:
const divs = document.querySelectorAll('div'); divs.map; // undefined!因为它不是真正的数组querySelectorAll返回的是NodeList,类数组但不能用map、filter。
传统解法:
const arrayDivs = Array.prototype.slice.call(divs);现在只需要:
const arrayDivs = [...divs];然后就能愉快地链式操作了:
const wideActiveDivIds = [...document.querySelectorAll('.active')] .filter(div => div.offsetWidth > 100) .map(div => div.id);这种写法在 SPA 动态渲染、表单校验、动画控制中极为常见。
场景七:数组去重神器 ——Set+ 展开 = 一行解决
要去掉重复元素,你会怎么写?
const unique = Array.from(new Set([1, 2, 2, 3, 3, 4])); // [1,2,3,4]其实还可以更短:
const unique = [...new Set([1, 2, 2, 3, 3, 4])];利用Set自动去重的特性,再通过展开还原为数组。目前公认最简洁的去重方案。
适用于标签管理、选项去重、历史记录清理等功能模块。
在现代前端架构中扮演什么角色?
展开运算符早已渗透进各个开发层级:
| 架构层 | 典型用法 |
|---|---|
| 视图层 | React/Vue 中 props 扩展、class 名拼接 |
| 状态管理 | Redux 中 state 更新,确保不可变性 |
| 工具函数 | 替代 Lodash 的部分功能,减少依赖 |
| 请求封装 | 动态构造 headers、query 参数 |
特别是在 React 中,几乎每天都在用:
const Button = ({ className, ...props }) => ( <button className={`btn ${className}`} {...props} // 所有其他属性自动透传 /> );这种“属性收集 + 展开透传”的模式,已经成为高级组件设计的标准范式。
实战建议:怎么用才不容易踩坑?
✅ 推荐做法
- 优先使用展开代替老方法
js // 好 const newArr = [...arr]; const newObj = { ...obj };
- 结合解构使用,提升代码表达力
js const [first, ...rest] = [1, 2, 3, 4]; console.log(first); // 1 console.log(rest); // [2, 3, 4]
- Redux reducer 中的安全更新
js case 'ADD_USER': return { ...state, users: [...state.users, action.payload] };
❌ 避免这些错误
- 误以为是深拷贝
js const obj = { user: { name: 'Alice' } }; const copy = { ...obj }; copy.user.name = 'Bob'; console.log(obj.user.name); // Bob → 出乎意料!
- 在超大数组上滥用展开
js Math.max(...hugeArray); // 可能栈溢出
- 在对象展开时忽略 Symbol 和不可枚举属性
js const obj = { [Symbol('id')]: 123 }; console.log({ ...obj }); // {} → 看不见 Symbol!
写到最后
展开运算符不是一个炫技的语法,而是一个真正提升开发效率、增强代码健壮性的实用工具。
它让你少写几行代码的同时,也让逻辑更清晰、副作用更可控。尤其是在 React、Vue、Redux 这些强调不可变性和组件复用的框架中,它几乎是每日必用的存在。
更重要的是,它代表了一种思维方式的转变:从命令式到声明式,从手动操作到抽象表达。
下次当你想调用concat、slice、apply的时候,停下来想想:能不能用...一行搞定?
也许你会发现,JavaScript 没那么难,只是我们一直用老方法写着新代码。
如果你正在学习 ES6,不妨把今天这七个场景抄一遍、跑一遍、改一遍。等哪天你在代码评审中看到别人还在用Array.prototype.slice.call(arguments),你可以微微一笑:
“兄弟,试试
...args?”