在 JavaScript 的开发世界里,‘…‘(三个点符号)无疑是我们每天都在使用且最为强大的语法特性之一。根据上下文的不同,我们习惯称之为扩展运算符或剩余参数。虽然这个语法早已存在多年,但在 2026 年的今天,随着 AI 辅助编程的普及和前端工程化的深度演进,理解其底层原理、性能边界以及在复杂系统中的最佳实践,变得比以往任何时候都重要。在这篇文章中,我们将不仅回顾基础用法,还会结合现代开发范式,探讨如何在 AI 时代更优雅、更高效地运用它。
扩展与收集:JavaScript 数据流动的哲学
首先,让我们通过一个直观的例子来快速区分这两种核心用法。理解“展开”与“收集”的对立统一,是掌握 JavaScript 数据处理哲学的第一步。
// 1. 扩展运算符: 将数组或对象 "展开" 为独立的元素或属性
const sourceArray = [ 10, 20, 30 ];
const targetArray = [ ...sourceArray, 40 ];
// targetArray => [ 10, 20, 30, 40 ]
// 2. 剩余参数: 将独立的元素 "收集" 到一个数组中
const numbers = [ 10, 20, 30, 40, 50 ];
const [ first, ...restOfThem ] = numbers;
// first => 10, restOfThem => [ 20, 30, 40, 50 ]
这种看似简单的语法,实际上是现代函数式编程和状态管理的基础。让我们深入挖掘一下它在不同场景下的表现。
1. 不可变数据流与 React/Vue 状态更新
在现代前端开发中(无论是 React 的 Hooks 还是 Vue 3.5+ 的 Composition API),保持状态的不可变性是核心原则。我们经常需要更新一个庞大状态树中的某个小角落,而不能直接修改原对象。
浅拷贝的艺术与陷阱
扩展运算符是我们实现“原地更新感,实则新对象”的首选工具。
// 场景:更新用户配置
const userState = {
id: 1,
name: ‘Alice‘,
preferences: { theme: ‘dark‘, language: ‘en‘ } // 嵌套对象
};
// 我们想要更新 name,保留其他属性
const updatedState = {
...userState, // 第一步:展开旧对象的所有属性
name: ‘Alice Updated‘ // 第二步:覆盖同名属性(注意顺序)
};
// preferences 属性未被修改,且引用未变
console.log(userState.preferences === updatedState.preferences); // true
专家提示:这种操作极其高效,因为它只复制了对象的第一层引用。然而,这也带来了一个经典陷阱:浅拷贝问题。如果我们试图修改 preferences.theme,单纯使用扩展运算符会失效。
// 错误示范:深层更新失败
const badUpdate = {
...userState,
preferences: { theme: ‘light‘ } // 这直接替换了整个 preferences 对象,丢失了 language!
};
应对深层嵌套:2026 年的解决方案
在 2026 年,面对深层嵌套的状态,我们不再手动编写繁琐的“扩展地狱”代码。我们有更优雅的选择:
- Immer (业界标准):让我们像写 mutable 代码一样写 immutable 逻辑,由库来负责结构共享。
- 原生 structuredClone:当我们需要彻底的深拷贝且不关心引用关系时,现代浏览器和 Node.js 均已支持此 API。
// 使用 Immer 解决深层更新问题
import { produce } from ‘immer‘;
const nextState = produce(userState, draft => {
// 在这里可以随意“修改”,Immer 会生成新的不可变状态
draft.preferences.theme = ‘light‘;
});
// 使用原生 structuredClone (深拷贝,无引用共享)
// 注意:这比浅拷贝慢,仅用于需要完全隔离数据的场景
const deepCloneState = structuredClone(userState);
2. AI 辅助编程时代的代码可读性
随着 Cursor、Windsurf 和 GitHub Copilot 等工具的普及,我们编写代码的方式发生了变化。AI 模型是基于海量的优秀代码训练出来的,它们对扩展运算符有着特殊的“偏好”。
为什么 AI 更喜欢 ‘…‘ 而不是 Object.assign?
当我们使用 INLINECODE1f586537 时,AI 编程助手能瞬间理解我们的意图是“合并与继承”。而 INLINECODE1a009f6e 虽然功能相似,但在语义上略显模糊(它倾向于修改目标对象)。
// AI 友好写法
const config = {
...baseConfig,
...userConfig, // 用户配置覆盖基础配置
timestamp: Date.now()
};
实战建议:在 AI 协作开发中,尽量保持代码的“显式意图”。使用扩展运算符进行解构赋值(如 const { id, ...rest } = obj)能帮助 AI 更准确地推断类型,从而生成更精准的单元测试。
3. 函数设计中的剩余参数
在设计公共 API 或工具函数时,灵活的参数传递机制至关重要。剩余参数不仅让函数签名更干净,还解决了 arguments 对象(类数组)无法直接使用数组方法的痛点。
构建灵活的数据管道
让我们来看一个我们在最近的一个微前端项目中,如何利用剩余参数构建一个灵活的事件总线。
// 场景:一个类型安全的日志记录器
// 它接受一个标签和任意数量的消息片段
function logEvent(tag, ...messages) {
// messages 是一个真正的数组!
// 我们可以直接使用 map, join, filter 等高阶函数
const formattedMessage = messages.join(‘ | ‘);
// 在 2026 年,我们可能会直接将这个发送到可观测性平台
console.log(`[${tag}]: ${formattedMessage}`);
}
logEvent(‘ERROR‘, ‘Database Timeout‘, ‘Port: 5432‘, { retry: 3 });
// Output: [ERROR]: Database Timeout | Port: 5432 | [object Object]
智能解构与属性剥离
在处理 API 响应或组件 Props 时,我们经常需要剔除敏感字段,将剩余的安全数据传递给下游。
// 场景:剥离敏感信息,仅保留公开 Profile
const rawUserData = {
userId: ‘u_123‘,
passwordHash: ‘super_secret_hash‘, // 必须移除
email: ‘[email protected]‘,
bio: ‘Frontend Engineer‘
};
// 利用解构和剩余参数,一行代码完成清洗
const { passwordHash, ...publicProfile } = rawUserData;
// 此时 publicProfile 不包含 passwordHash
console.log(publicProfile);
// Output: { userId: ‘u_123‘, email: ‘[email protected]‘, bio: ‘Frontend Engineer‘ }
4. 性能优化与边缘计算考量
作为经验丰富的开发者,我们不能只关注代码的优雅性,必须时刻警惕性能陷阱。在 2026 年,随着应用逻辑向边缘节点迁移,内存限制变得尤为严格。
调用栈溢出风险
一个鲜为人知的事实是:扩展运算符在作为函数参数传递时,是受到引擎参数数量限制的。
// 危险操作:展开超大规模数组
const massiveArray = new Array(200000).fill(0); // 20 万个元素
try {
// 这里的 ...massiveArray 会尝试在栈上展开 20 万个参数
// 在大多数 JS 引擎中,这会直接导致 "RangeError: Maximum call stack size exceeded"
console.log(Math.max(...massiveArray));
} catch (e) {
console.error(‘栈溢出!‘, e.message);
}
// 正确做法:循环处理或使用 apply (如果支持)
// 或者使用不限制参数数量的现代 API
边缘环境中的内存压力
在 Cloudflare Workers 或 Vercel Edge Functions 等边缘环境中,CPU 和内存极其珍贵。过度的对象展开会产生大量的临时对象,导致频繁的垃圾回收(GC)。
// 边缘环境下的反模式
// 假设我们有一个 5MB 的配置对象
const bigConfig = await fetchRemoteConfig();
// 每次请求都这样展开,会产生 5MB 的新对象,迅速撑爆边缘内存
const requestConfig = { ...bigConfig, reqId: crypto.randomUUID() };
// 推荐做法:按需拷贝,或者仅在顶层进行小范围扩展
// 保持大对象的引用不变,只附加小数据
const requestConfig = Object.assign({ reqId: crypto.randomUUID() }, bigConfig);
5. 高级应用:推断与元组
在 2026 年,TypeScript 已经成为大型项目的标配。扩展运算符在泛型推断中扮演着关键角色,特别是处理“元组”类型时。
让我们思考一个场景:我们需要编写一个高阶函数,它能完美记住传入参数的类型。
// 泛型 Args 捕获传入的参数类型为一个元组
declare function createEventHandler(
...args: Args
): (callback: (...args: Args) => void) => void;
// 实际使用
const handler = createEventHandler(
‘app_ready‘,
{ version: 2.0 },
[true, false]
);
// TypeScript 完美推断出了 callback 的参数类型
// 你无需手动写泛型,IDE 和 AI 都能感知到这三个参数的具体类型
handler((event, config, flags) => {
// event: string, config: { version: number }, flags: boolean[]
console.log(config.version.toFixed(2)); // 类型安全
});
专家提示:利用剩余参数捕获“泛型元组”,是编写类型安全的高阶组件和库函数的核心技巧。如果你使用传统的 INLINECODE5be33f93 或者将参数作为普通对象传递,TypeScript 的推断往往会在第一层泛型处“断链”,导致类型被降级为 INLINECODE5ff59d92 或 unknown。
结语
回顾这篇文章,我们探讨了从基础的数组展开到复杂的状态管理,再到 2026 年 AI 时代的工程化实践。‘…‘ 不仅仅是一个语法糖,它代表了 JavaScript 处理集合数据的灵活性。
在我们的日常工作中,掌握它不仅要会用,更要知道何时不用(比如深层拷贝、超大规模数组或边缘计算环境)。随着 AI 辅助编程的普及,编写清晰、符合语义的代码(如使用 ... 表达不可变更新意图)变得比单纯的技巧更重要。希望这些分享能帮助你在未来的项目中,构建出既优雅又健壮的系统。