在日常的 JavaScript 开发中,我们经常需要处理数组和对象的数据操作。你是否遇到过需要合并两个数组,或者在不修改原对象的情况下添加新属性的情况?又或者你是否曾为函数参数数量不确定而感到头疼?
在 JavaScript (ES6) 引入的众多特性中,有一个非常小巧但功能强大的语法糖——三个点 (...)。这就是我们常说的“展开/剩余运算符”。它不仅能极大地简化我们的代码,还能让数据的处理逻辑变得更加清晰和直观。随着我们步入 2026 年,在 AI 辅助编程和高度组件化的前端架构下,理解这一特性的底层原理变得比以往任何时候都重要。
在这篇文章中,我们将深入探讨这三个点背后的魔法。我们将一起学习它如何作为“展开运算符”来拆解数据,以及如何作为“剩余参数”来收集数据。通过丰富的实战代码示例和深度解析,你将彻底掌握这一现代 JavaScript 开发中不可或缺的工具。
什么是展开运算符?
首先,让我们来看看展开运算符。
JavaScript 的展开运算符允许一个可迭代对象(如数组)或对象字面量在期望接收多个参数或元素的地方被“展开”。简单来说,它就像是把一个打包好的盒子拆开,将其中的内容一个个拿出来放到新的地方。
核心语法
它的语法非常简洁,就是三个点 ... 后面跟上一个变量名:
let newArray = [...oldArray];
场景一:数组的合并与克隆
在过去,我们合并数组通常需要使用 concat() 方法,而克隆数组则比较复杂。现在,有了展开运算符,这些操作变得异常轻松。
示例 1:基础的数组合并
在这个例子中,我们不再需要调用函数,而是直接“展开”数组的内容。
// 使用展开运算符合并两个数组
let arr1 = [4, 5];
let arr2 = [8, 9, 10];
// 将 arr1 和 arr2 的元素依次展开到新数组中
let mergedArr = [...arr1, ...arr2];
console.log(mergedArr);
// 输出: [ 4, 5, 8, 9, 10 ]
深度解析:
这里,INLINECODE2eceac78 将其元素 INLINECODEba1b0e81 放入新数组,紧接着 INLINECODE39b920b9 将 INLINECODE91359127 放入。这种写法不仅代码量少,而且语义非常清晰——“把这个数组里的东西放进去”。
场景二:对象的扩展与不可变更新
展开运算符不仅适用于数组,也适用于对象(在 ES2018 中被正式引入)。这在现代前端开发(如 React 状态管理)中至关重要,因为它让我们能轻松实现“不可变数据更新”。
示例 2:对象属性的克隆与添加
假设我们有一个用户信息对象,我们想要创建一个新对象,包含原有信息但增加一个城市属性,同时不修改原始对象。
const obj1 = { name: "Amit", age: 22 };
// 克隆 obj1 的所有属性,并添加新的 city 属性
const newObject = { ...obj1, city: "Uttarakhand" };
console.log(newObject);
// 输出: { name: ‘Amit‘, age: 22, city: ‘Uttarakhand‘ }
console.log(obj1);
// 输出: { name: ‘Amit‘, age: 22 } (原对象未受影响)
实用见解:
如果你在开发中遵循函数式编程理念,你需要保证数据不被直接修改。使用 { ...original, newProp: value } 是更新对象状态的最佳实践。特别是在 2026 年的 React Server Components 和 Next.js 架构中,这种模式确保了数据流的单向性和可预测性。
示例 3:处理对象属性的覆盖
当展开的对象和后续属性有重名时,后面的属性会覆盖前面的。这对于处理默认值非常有用。
const defaultSettings = { theme: ‘light‘, notifications: true };
const userSettings = { theme: ‘dark‘ };
// userSettings 会覆盖 defaultSettings 中的同名属性
const finalSettings = { ...defaultSettings, ...userSettings };
console.log(finalSettings);
// 输出: { theme: ‘dark‘, notifications: true }
场景三:将类数组对象转为真数组
这是一个非常实用的技巧。你可能在处理 DOM 操作时遇到过 INLINECODE1fa3bd5b 的结果,它看起来像数组,但并没有 INLINECODEbf1bbbe1 或 filter 方法。
const domNodes = document.querySelectorAll(‘div‘);
// 这是一个 NodeList,不是真正的 Array
// 使用展开运算符将其转化为真正的数组
const nodesArray = [...domNodes];
// 现在我们可以使用数组方法了
nodesArray.forEach(node => console.log(node.nodeType));
2026 前端架构下的深度应用
随着我们进入 2026 年,前端开发的格局已经发生了巨大的变化。展开运算符不再仅仅是简化数组的工具,它成为了我们构建高性能、AI 辅助应用的基础砖块。让我们看看在最新的技术趋势中,我们是如何运用它的。
1. AI 原生开发与状态不可变性
在使用 Cursor、Windsurf 或 GitHub Copilot 等 AI IDE 进行“氛围编程”时,AI 模型通常倾向于生成具有极高可读性和确定性的代码。展开运算符因其声名式(Declarative)的特性,成为了 AI 生成不可变状态更新的首选。
实战案例:复杂状态的局部更新
假设我们正在为一个协作型 Notion 类工具编写状态管理逻辑。我们需要更新一个嵌套在深层对象中的评论,而不影响其他数据。
// 初始状态:一篇文档的元数据
const documentState = {
id: ‘doc_2026_001‘,
title: ‘未来技术展望‘,
collaborators: [‘Alice‘, ‘Bob‘],
metadata: {
lastEdited: ‘2026-05-20‘,
views: 1024,
settings: {
isPublic: false,
allowComments: true
}
}
};
// 场景:我们需要将 isPublic 改为 true,并保留其他所有属性
// 在以前,我们可能会使用 lodash 的 merge 或者复杂的深拷贝逻辑
// 在现代工程中,我们推荐使用显式的展开结构
const nextState = {
...documentState, // 第一层展开
metadata: {
...documentState.metadata, // 第二层展开,保留未修改的 lastEdited 和 views
settings: {
...documentState.metadata.settings, // 第三层展开
isPublic: true // 这里覆盖旧值
}
}
};
// 验证:原对象保持不变
console.log(documentState.metadata.settings.isPublic); // 输出: false
// 验证:新对象已更新
console.log(nextState.metadata.settings.isPublic); // 输出: true
为什么这在 2026 年很重要?
当我们与 AI 编程助手结对时,这种显式的层级结构能让 AI 更容易理解我们的意图。如果你使用 Object.assign 或者深拷贝库,AI 可能难以推断出哪些字段是被有意修改的。而上面的代码,哪怕是对非人类来说,也是一目了然的:
- 复制外层。
- 复制并进入
metadata。 - 复制并进入
settings。 - 修改
isPublic。
2. 边缘计算与数据序列化
随着边缘计算的普及,越来越多的数据在边缘节点进行处理。展开运算符在数据的快速打包和解包中扮演了关键角色。但在处理 Serverless 函数或 Edge Runtime 时,我们必须格外小心。
工程陷阱:只读对象的不可变性
在现代框架(如 Next.js 14+)中,从 INLINECODE4c135d31 或 INLINECODE7d3b606f 获取的对象通常是只读的。尝试直接展开并修改它们可能会导致运行时错误。
// 错误示范:在 Edge Runtime 中尝试修改 props
function Page({ params }) {
// 如果 params 是只读的,直接展开会报错或产生意外副作用
// const newParams = { ...params, id: ‘new_id‘ };
// 正确做法:先确保将其序列化为普通对象
const safeParams = JSON.parse(JSON.stringify(params));
const newParams = { ...safeParams, id: ‘new_id‘ };
return {newParams.id};
}
在这个例子中,我们使用了 JSON.parse(JSON.stringify()) 来打破引用链。虽然在极高频率调用下会有性能损耗,但在大多数边缘路由预处理的场景下,这是保证数据完全隔离的最稳妥方法之一。
什么是剩余参数?
理解了“展开”之后,我们来看看它的“镜像”操作——“剩余参数”。
JavaScript 的剩余参数同样使用三个点 ... 表示,但它的作用恰恰相反:它将多个不确定的参数收集到一个数组中。这在定义函数时特别有用,尤其是当我们不知道会传进来多少个参数的时候。
核心语法
剩余参数通常放在函数参数列表的最后:
function myFunction(a, b, ...theRest) {
// theRest 是一个包含剩余所有参数的数组
}
场景一:处理可变数量的函数参数
想象一下,我们要写一个求和函数,如果不使用剩余参数,我们可能需要依赖老式的 arguments 对象(类数组,不易用),或者限制参数的数量。
示例 4:通用的求和函数
这里,我们创建了一个 sumFunction,无论你传入1个数字还是100个数字,它都能完美处理。
function sumFunction(...numbers) {
// numbers 现在是一个纯数组
return numbers.reduce((total, num) => total + num, 0);
}
console.log(sumFunction(1, 2, 3));
// 输出: 6
console.log(sumFunction(1, 2, 3, 4, 5));
// 输出: 15
深度解析:
在这个例子中,INLINECODE56df95e6 告诉 JavaScript 引擎:“把所有传进来的参数都装进 INLINECODE5a819632 这个数组里”。这比使用隐式的 arguments 对象要清晰得多,因为它明确指出了函数的意图。
场景二:配合解构赋值使用
这是剩余参数最优雅的用法之一。当你想要从一个数组或对象中提取前几个特定的值,而把剩下的打包起来时,它非常方便。
示例 5:数组解构与剩余部分收集
我们来看看这个例子。我们只关心第一个和第二个值,剩下的我们并不想逐个处理,而是想作为一个整体保留。
let [first, second, ...rest] = [1, 2, 3, 4, 5];
console.log(first);
// 输出: 1
console.log(second);
// 输出: 2
console.log(rest);
// 输出: [3, 4, 5]
示例 6:对象解构与剩余属性
同样的逻辑也适用于对象。这在处理配置项时非常有用。
const user = {
id: 101,
username: ‘jdoe‘,
email: ‘[email protected]‘,
password: ‘secret123‘,
age: 30
};
// 提取 id 和 username,其他的放入 userDetails
const { id, username, ...userDetails } = user;
console.log(userDetails);
// 输出: { email: ‘[email protected]‘, password: ‘secret123‘, age: 30 }
实战中的最佳实践与高级陷阱
虽然 ... 运算符非常强大,但在实际工程中,我们需要注意一些细节,以避免踩坑。特别是在 2026 年,应用复杂度极高,细微的错误可能导致难以调试的 Bug。
1. 浅拷贝陷阱与深拷贝策略
展开运算符进行的是“浅拷贝”。这意味着它只复制对象或数组的第一层属性。如果你的对象嵌套了其他对象,内部的嵌套对象仍然是通过引用共享的。
故障排查:奇怪的同步 Bug
我们在最近的一个企业级仪表盘项目中遇到了这样一个 Bug:修改了副本中的某个深层数据,结果 UI 上原本应该锁定的“原始数据”也跟着变了。
const original = {
name: ‘GFG‘,
details: {
type: ‘Portal‘
}
};
const copy = { ...original };
// 修改嵌套对象的属性
copy.details.type = ‘Blog‘;
console.log(original.details.type);
// 输出: ‘Blog‘ (原对象也被修改了!)
2026 解决方案:
对于简单的数据,使用展开运算符。但对于复杂的配置对象或 Redux 状态树,我们建议:
- 使用
structuredClone():这是现代浏览器原生的深拷贝 API,性能极佳且支持循环引用。
const deepCopy = structuredClone(original);
2. 性能考量与大数据处理
在处理大型数组时,展开运算符可能会带来性能开销。因为每次展开都会创建一个新的数组并在内存中复制元素。
// 假设 bigArray 包含 10 万个元素
const bigArray = [...new Array(100000)].map((_, i) => i);
// 这会创建一个新的 10 万元素的数组,如果频繁执行,可能会影响性能
const copy = [...bigArray];
优化建议:
在处理大量数据(如 WebGL 顶点数据、大型 CSV 解析)时,尽量避免频繁的浅拷贝。如果数据不需要不可变性,直接操作原数组。如果必须不可变,考虑使用 Immutable.js 或直接使用 TypedArray(如 Float32Array),这在现代 Web 图形和高性能计算应用中至关重要。
3. 识别身份:展开 vs 剩余
到现在,你可能会问:“它们长得一模一样,我怎么知道什么时候是‘展开’,什么时候是‘剩余’呢?”
这里有一个简单的判断法则:
- 如果是剩余参数: 它通常出现在函数定义的参数列表末尾,或者是赋值操作(解构)的左边(
let { ...rest } = obj)。它的作用是收集。
口诀:* “剩下的全归我。”
- 如果是展开运算符: 它通常出现在函数调用的参数中(INLINECODEb085dc6a),或者是数组/对象字面量中(INLINECODEd02898b1)。它的作用是拆解。
口诀:* “把我拆开散出去。”
总结
JavaScript 中的这三个点 ... 是一个多面手。在 2026 年及未来的开发中,随着 TypeScript 类型系统的普及和 AI 辅助编码的常态化,写出语义清晰、结构明确的代码比以往任何时候都重要。
掌握展开和剩余运算符不仅能让你的代码更加简洁,更是你理解现代 JavaScript 框架(React, Vue, Svelte)核心原理的第一把钥匙。
让我们回顾一下关键点:
- 展开: 用于数组合并、对象克隆、函数参数展开。它是“将结构拆解为独立部分”的过程。
- 剩余: 用于函数参数收集、解构赋值。它是“将独立部分组合为结构(数组)”的过程。
- 区分法则: 看
...是出现在等号/函数定义的左边(收集/剩余),还是右边(拆解/展开)。
在接下来的开发中,试着替换掉旧的 INLINECODE30382486 或 INLINECODE5a4a9c24 方法,多用用 ...,并在处理深层数据时保持警惕。你会发现代码的可读性会有质的飞跃。继续编码,继续探索!