JavaScript 展开运算符深度解析:从基础到 2026 年工程化实践

在日常的 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);
       
  • 使用 Immer:在 React 开发中,Immer 库利用 Proxy 技术,让你看起来像是直接修改了对象,实际上是在生成不可变的新结构。这与展开运算符的理念不谋而合,但功能更强大。

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 方法,多用用 ...,并在处理深层数据时保持警惕。你会发现代码的可读性会有质的飞跃。继续编码,继续探索!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。如需转载,请注明文章出处豆丁博客和来源网址。https://shluqu.cn/30706.html
点赞
0.00 平均评分 (0% 分数) - 0