扩展语法 (...)

**扩展运算符 (...)** 允许将可迭代对象(例如数组或字符串)扩展到期望零个或多个参数(对于函数调用)或元素(对于数组字面量)的位置。在对象字面量中,扩展运算符枚举对象的属性并将键值对添加到正在创建的对象中。

扩展运算符看起来与剩余运算符完全相同。从某种程度上来说,扩展运算符与剩余运算符相反。扩展运算符将数组“扩展”成其元素,而剩余运算符收集多个元素并将它们“压缩”成单个元素。请参阅剩余参数剩余属性

试一试

语法

js
myFunction(a, ...iterableObj, b)
[1, ...iterableObj, '4', 'five', 6]
{ ...obj, key: 'value' }

描述

当需要将对象或数组中的所有元素包含在新数组或对象中,或者应在函数调用的参数列表中逐一应用时,可以使用扩展运算符。

虽然语法看起来相同,但它们具有略微不同的语义。

只有可迭代值,如ArrayString,才能在数组字面量和参数列表中扩展。许多对象不可迭代,包括所有缺乏Symbol.iterator方法的普通对象

js
const obj = { key1: "value1" };
const array = [...obj]; // TypeError: obj is not iterable

另一方面,在对象字面量中扩展会枚举该值的自有属性。对于典型的数组,所有索引都是可枚举的自有属性,因此数组可以扩展到对象中。

js
const array = [1, 2, 3];
const obj = { ...array }; // { 0: 1, 1: 2, 2: 3 }

所有基本类型都可以在对象中扩展。只有字符串具有可枚举的自有属性,扩展其他任何内容都不会在新对象上创建属性。

js
const obj = { ...true, ..."test", ...10 };
// { '0': 't', '1': 'e', '2': 's', '3': 't' }

在函数调用中使用扩展运算符时,请注意可能超出 JavaScript 引擎的参数长度限制。有关更多详细信息,请参阅Function.prototype.apply()

示例

在函数调用中扩展

替换 apply()

在希望将数组的元素用作函数的参数的情况下,通常使用Function.prototype.apply()

js
function myFunction(x, y, z) {}
const args = [0, 1, 2];
myFunction.apply(null, args);

使用扩展运算符,上述内容可以写成

js
function myFunction(x, y, z) {}
const args = [0, 1, 2];
myFunction(...args);

参数列表中的任何参数都可以使用扩展运算符,并且可以多次使用扩展运算符。

js
function myFunction(v, w, x, y, z) {}
const args = [0, 1];
myFunction(-1, ...args, 2, ...[3]);

对 new 运算符应用

使用new调用构造函数时,无法**直接**使用数组和apply(),因为apply()调用目标函数而不是构造它,这意味着(除其他事项外)new.target将为undefined。但是,由于扩展运算符,数组可以轻松地与new一起使用

js
const dateFields = [1970, 0, 1]; // 1 Jan 1970
const d = new Date(...dateFields);

在数组字面量中扩展

更强大的数组字面量

在没有扩展运算符的情况下,数组字面量语法不再足以使用现有数组作为其中一部分来创建新数组。相反,必须使用组合方法(包括push()splice()concat()等)使用命令式代码。使用扩展运算符,这变得更加简洁

js
const parts = ["shoulders", "knees"];
const lyrics = ["head", ...parts, "and", "toes"];
//  ["head", "shoulders", "knees", "and", "toes"]

就像参数列表中的扩展一样,...可以在数组字面量的任何位置使用,并且可以使用多次。

复制数组

您可以使用扩展运算符创建数组的浅拷贝。每个数组元素都保留其标识,而不会被复制。

js
const arr = [1, 2, 3];
const arr2 = [...arr]; // like arr.slice()

arr2.push(4);
// arr2 becomes [1, 2, 3, 4]
// arr remains unaffected

扩展运算符在复制数组时有效地深入一层。因此,它可能不适合复制多维数组。对于Object.assign()也是如此——JavaScript 中没有本机操作可以进行深克隆。Web API 方法structuredClone()允许深拷贝某些受支持类型的值。有关更多详细信息,请参阅浅拷贝

js
const a = [[1], [2], [3]];
const b = [...a];

b.shift().shift();
// 1

// Oh no! Now array 'a' is affected as well:
console.log(a);
// [[], [2], [3]]

连接数组的更好方法

Array.prototype.concat()通常用于将数组连接到现有数组的末尾。在没有扩展运算符的情况下,这将完成如下操作:

js
let arr1 = [0, 1, 2];
const arr2 = [3, 4, 5];

// Append all items from arr2 onto arr1
arr1 = arr1.concat(arr2);

使用扩展运算符,这将变成

js
let arr1 = [0, 1, 2];
const arr2 = [3, 4, 5];

arr1 = [...arr1, ...arr2];
// arr1 is now [0, 1, 2, 3, 4, 5]

Array.prototype.unshift()通常用于在现有数组的开头插入一组值。在没有扩展运算符的情况下,这将完成如下操作:

js
const arr1 = [0, 1, 2];
const arr2 = [3, 4, 5];

//  Prepend all items from arr2 onto arr1
Array.prototype.unshift.apply(arr1, arr2);
console.log(arr1); // [3, 4, 5, 0, 1, 2]

使用扩展运算符,这将变成

js
let arr1 = [0, 1, 2];
const arr2 = [3, 4, 5];

arr1 = [...arr2, ...arr1];
console.log(arr1); // [3, 4, 5, 0, 1, 2]

注意:unshift()不同,这会创建一个新的arr1,而不是就地修改原始arr1数组。

有条件地向数组添加值

您可以使用条件运算符使数组字面量中的元素存在或不存在,具体取决于条件。

js
const isSummer = false;
const fruits = ["apple", "banana", ...(isSummer ? ["watermelon"] : [])];
// ['apple', 'banana']

当条件为false时,我们扩展一个空数组,这样就不会向最终数组添加任何内容。请注意,这与以下内容不同

js
const fruits = ["apple", "banana", isSummer ? "watermelon" : undefined];
// ['apple', 'banana', undefined]

在这种情况下,当isSummerfalse时,会添加一个额外的undefined元素,并且诸如Array.prototype.map()之类的方法将访问此元素。

在对象字面量中扩展

复制和合并对象

您可以使用扩展运算符将多个对象合并成一个新对象。

js
const obj1 = { foo: "bar", x: 42 };
const obj2 = { bar: "baz", y: 13 };

const mergedObj = { ...obj1, ...obj2 };
// { foo: "bar", x: 42, bar: "baz", y: 13 }

单个扩展会创建原始对象的浅拷贝(但不会创建不可枚举的属性,也不会复制原型),类似于复制数组

js
const clonedObj = { ...obj1 };
// { foo: "bar", x: 42 }

覆盖属性

当一个对象扩展到另一个对象中,或者当多个对象扩展到一个对象中时,并且遇到名称相同的属性,则该属性将采用最后分配的值,同时保留其最初设置的位置。

js
const obj1 = { foo: "bar", x: 42 };
const obj2 = { foo: "baz", y: 13 };

const mergedObj = { x: 41, ...obj1, ...obj2, y: 9 }; // { x: 42, foo: "baz", y: 9 }

有条件地向对象添加属性

您可以使用条件运算符使对象字面量中的元素存在或不存在,具体取决于条件。

js
const isSummer = false;
const fruits = {
  apple: 10,
  banana: 5,
  ...(isSummer ? { watermelon: 30 } : {}),
};
// { apple: 10, banana: 5 }

当条件为false时,结果是一个空对象,因此不会将任何内容扩展到最终对象中。请注意,这与以下情况不同。

js
const fruits = {
  apple: 10,
  banana: 5,
  watermelon: isSummer ? 30 : undefined,
};
// { apple: 10, banana: 5, watermelon: undefined }

在这种情况下,watermelon 属性始终存在,并且会被诸如 Object.keys() 之类的方法访问。

由于基本类型也可以扩展到对象中,并且观察到所有 假值 都没有可枚举的属性,因此您可以简单地使用 逻辑与 运算符。

js
const isSummer = false;
const fruits = {
  apple: 10,
  banana: 5,
  ...(isSummer && { watermelon: 30 }),
};

在这种情况下,如果isSummer是任何假值,则不会在fruits对象上创建任何属性。

与Object.assign()比较

请注意,Object.assign() 可用于修改对象,而扩展语法则不能。

js
const obj1 = { foo: "bar", x: 42 };
Object.assign(obj1, { x: 1337 });
console.log(obj1); // { foo: "bar", x: 1337 }

此外,Object.assign() 会触发目标对象上的设置器,而扩展语法不会。

js
const objectAssign = Object.assign(
  {
    set foo(val) {
      console.log(val);
    },
  },
  { foo: 1 },
);
// Logs "1"; objectAssign.foo is still the original setter

const spread = {
  set foo(val) {
    console.log(val);
  },
  ...{ foo: 1 },
};
// Nothing is logged; spread.foo is 1

您不能通过简单的扩展来重新实现 Object.assign() 函数。

js
const obj1 = { foo: "bar", x: 42 };
const obj2 = { foo: "baz", y: 13 };
const merge = (...objects) => ({ ...objects });

const mergedObj1 = merge(obj1, obj2);
// { 0: { foo: 'bar', x: 42 }, 1: { foo: 'baz', y: 13 } }

const mergedObj2 = merge({}, obj1, obj2);
// { 0: {}, 1: { foo: 'bar', x: 42 }, 2: { foo: 'baz', y: 13 } }

在上面的示例中,扩展语法的工作方式与预期不符:由于剩余参数,它将参数的数组扩展到对象字面量中。这是一个使用扩展语法实现merge的示例,其行为类似于 Object.assign(),除了它不会触发设置器,也不会修改任何对象。

js
const obj1 = { foo: "bar", x: 42 };
const obj2 = { foo: "baz", y: 13 };
const merge = (...objects) =>
  objects.reduce((acc, cur) => ({ ...acc, ...cur }));

const mergedObj1 = merge(obj1, obj2);
// { foo: 'baz', x: 42, y: 13 }

规范

规范
ECMAScript 语言规范
# prod-SpreadElement
ECMAScript 语言规范
# prod-ArgumentList
ECMAScript 语言规范
# prod-PropertyDefinition

浏览器兼容性

BCD 表格仅在浏览器中加载

另请参阅