原文: JavaScript Rest vs Spread Operator – What’s the Difference?

JavaScript 使用三个点(...)来表示 rest(剩余运算符)和 spread(扩展运算符),但是这两个运算符是不一样的。

rest 和 spread 的主要区别在于,rest 运算符将一些特定的用户提供的值的其余部分放入一个 JavaScript 数组中,而 spread 语法将迭代器扩展为单个元素。

例如,考虑一下这段代码,它使用 rest 将一些值放到一个数组中:

// 使用 rest 将其余的特定用户提供的值放入一个数组:
function myBio(firstName, lastName, ...otherInfo) { 
  return otherInfo;
}

// 调用 myBio 函数,同时向其传递五个参数:
myBio("Oluwatobi", "Sofela", "CodeSweetly", "Web Developer", "Male");

// 调用将返回:
["CodeSweetly", "Web Developer", "Male"]

在 StackBlitz 编辑器中尝试

在上面的片段中,我们使用 ...otherInfo rest 参数将 "CodeSweetly""Web Developer""Male" 放入一个数组。

现在,考虑一下这个扩展运算符的例子:

// 定义一个具有三个参数的函数:
function myBio(firstName, lastName, company) { 
  return `${firstName} ${lastName} runs ${company}`;
}

// 使用 spread 将一个数组的项扩展为单个参数:
myBio(...["Oluwatobi", "Sofela", "CodeSweetly"]);

// 调用将返回:
“Oluwatobi Sofela runs CodeSweetly”

在 StackBlitzit 中尝试

在上面的片段中,我们使用扩展运算符(...)将 ["Oluwatobi", "Sofela", "CodeSweetly"] 的内容扩展到 myBio() 的参数中。

如果你还不了解剩余运算符或扩展运算符,不要担心。这篇文章已经为你准备好了!

在下面的章节中,我们将讨论剩余运算符和扩展运算符如何在 JavaScript 中工作。

那么,话不多说,让我们开始学习剩余运算符。

剩余运算符到底是什么

剩余运算符是用来把一些特定的用户提供的值的其余部分放入一个 JavaScript 数组。

举例来说,下面是 rest 的语法:

...yourValues

上面的片段中的三个点(...)代表剩余运算符。

剩余运算符后面的文字是指你希望放在数组中的值。你只能在一个函数定义中的最后一个参数之前使用它。

为了更好地理解这个语法,让我们看看rest是如何在JavaScript函数中工作的。

剩余运算符在函数中是如何工作的

在 JavaScript 函数中,rest 被用作函数的最后一个参数的前缀。

下面是一个例子:

// 定义一个有两个常规参数和一个 rest 参数的函数
function myBio(firstName, lastName, ...otherInfo) { 
  return otherInfo;
}

剩余运算符(...)指示计算机将用户提供的任何 otherInfo (参数)加入一个数组中。然后,将该数组分配给 otherInfo  参数。

因此,我们称 ...otherInfo 为 rest 参数。

注意:参数是你可以通过调用器传递给一个函数的参数的可选值。

下面是另一个例子:

// 定义一个有两个常规参数和一个 rest 参数的函数
function myBio(firstName, lastName, ...otherInfo) { 
  return otherInfo;
}

// 调用 myBio 函数,同时向其参数传递五个参数:
myBio("Oluwatobi", "Sofela", "CodeSweetly", "Web Developer", "Male");

// 调用将返回:
["CodeSweetly", "Web Developer", "Male"]

StackBlitz 中尝试

在上面的片段中,注意 myBio 的调用向函数传递了五个参数。

换句话说,"Oluwatobi""Sofela" 被分配到 firstNamelastName 参数中。

同时,剩余运算符将其余的参数("CodeSweetly""Web Developer""Male")放入一个数组,并将该数组分配给 otherInfo 参数。

因此,myBio() 函数正确地返回 ["CodeSweetly", "Web Developer", "Male"] 作为 otherInfo 剩余参数的内容。

注意!你不能在包含 rest 参数的函数中使用 “use strict” 指令

请记住,你不能在任何包含 rest 参数、默认参数或解构赋值参数的函数中使用 "use strict" 指令。否则,计算机将抛出一个语法错误

例如,考虑下面这个例子:

// 定义一个具有 rest 参数的函数
function printMyName(...value) {
  "use strict";
  return value;
}

// 上面的定义将返回:
"Uncaught SyntaxError: Illegal 'use strict' directive in function with non-simple parameter list"

CodeSandbox 中尝试

printMyName() 返回了一个语法错误,因为我们在一个有 rest 参数的函数中使用了 “use strict” 指令。

但假设你需要你的函数处于严格模式,同时也使用 rest 参数,在这种情况下,你可以把 “use strict” 指令写在函数的外面。

下面是一个例子:

// 在你的函数外定义一个 "use strict" 指令:
"use strict";

// 定义一个函数,具有一个 rest 参数
function printMyName(...value) {
  return value;
}

// 调用 printMyName 函数,同时传递两个参数给它
printMyName("Oluwatobi", "Sofela");

// 调用将返回:
["Oluwatobi", "Sofela"]

CodeSandbox 中尝试

注意:只有当整个脚本或包围的范围都处于严格模式时,才可以在你的函数外添加 “use strict” 指令。

现在我们知道了 rest 是如何在函数中工作的,我们可以谈谈它是如何在解构赋值中工作的。

rest 运算符如何在解构赋值中工作

rest 运算符通常被用作解构赋值最后一个变量的前缀。

下面是一个例子:

// 定义一个具有两个常规变量和一个 rest 变量的解构数组:
const [firstName, lastName, ...otherInfo] = [
  "Oluwatobi", "Sofela", "CodeSweetly", "Web Developer", "Male"
];

// 调用 otherInfo 变量:
console.log(otherInfo); 

// 调用将返回:
["CodeSweetly", "Web Developer", "Male"]

StackBlitz 中尝试

剩余运算符(...)指示计算机将用户提供的其余数值添加到一个数组中。然后,它将该数组分配给 otherInfo 变量。

因此,你可以把 ...otherInfo 称为 rest 变量。

下面是另一个例子:

// 定义一个具有两个常规变量和一个 rest 变量的解构对象:
const { firstName, lastName, ...otherInfo } = {
  firstName: "Oluwatobi",
  lastName: "Sofela", 
  companyName: "CodeSweetly",
  profession: "Web Developer",
  gender: "Male"
}

// 调用 otherInfo 变量:
console.log(otherInfo);

// 调用将返回:
{companyName: "CodeSweetly", profession: "Web Developer", gender: "Male"}

StackBlitz 中尝试

在上面的片段中,注意到 rest 运算符给 otherInfo 变量分配了一个属性对象,而不是一个数组。

换句话说,只要你在一个解构对象中使用 rest,rest 运算符就会产生一个属性对象。

然而,如果你在一个解构数组或函数中使用 rest,该运算符将产生一个数组字面量。

在我们结束对 rest 的讨论之前,你应该知道 JavaScript 参数和 rest 参数之间的一些区别。所以,下面我们来谈谈这个问题。

Arguments vs Rest Parameters 有什么区别

下面是 JavaScript 参数(Arguments)和 rest 参数(rest parameter)的一些区别:

差异 1:arguments 对象是一个类似数组的对象--而不是一个真正的数组!

请记住,JavaScript 的 arguments 对象不是一个真正的数组。它是一个类似数组的对象,不具备普通 JavaScript 数组的全面功能。

rest 参数则是一个真正的数组对象。因此,你可以对它使用所有的数组方法。

因此,例如,你可以在 rest 参数上调用 sort()map()forEach()pop() 方法,但是你不能在 arguments 对象上做同样的事情。

差异 2:你不能在箭头函数中使用 arguments 对象

arguments 对象在箭头函数中是不可用的,但你可以在所有函数中使用 rest 参数--包括箭头函数。

差异 3:让 rest 成为你的首选

最好是使用 rest 参数而不是 arguments 对象--特别是在编写 ES6 兼容代码时。

现在我们知道 rest 是如何工作的,让我们讨论一下扩展运算符,这样我们就可以看到区别了。

什么是扩展运算符,它在 JavaScript 中是如何工作的

扩展运算符(...)可以帮助你将迭代对象扩展为单个元素。

spread 语法在数组字面量、函数调用和初始化属性对象中起作用,将可迭代对象的值扩展到单独的项目中。因此,它做了与 rest 运算符相反的事情。

注意:扩展运算符只有在数组字面量、函数调用或初始化属性对象中使用时才有效。

那么,这到底是什么意思?让我们通过一些例子来看看。

扩展运算符示例 1:在数组字面量中的作用

const myName = ["Sofela", "is", "my"];
const aboutMe = ["Oluwatobi", ...myName, "name."];

console.log(aboutMe);

// 调用将返回:
[ "Oluwatobi", "Sofela", "is", "my", "name." ]

StackBlitz 中尝试

上面的片段使用 spread(...)将 myName 数组复制到 aboutMe 中。

注意:

  • myName 的修改不会反映在 aboutMe 中,因为 myName 中的所有值都是基本数据类型。因此,扩展运算符只是简单地将 myName 的内容复制并粘贴到 aboutMe 中,而没有创建任何对原始数组的引用。
  • 正如 @nombrekeff 在这里的评论中提到的,扩展操作符只做浅层复制。因此,请记住,假设 myName 包含任何非原始值,计算机会在 myNameaboutMe 之间创建一个引用。更多关于扩展运算符如何处理原始值和非原始值的信息,请参见信息 3。
  • 假设我们没有使用 spread 语法来复制 myName 的内容。例如,如果我们写了 const aboutMe = ["Oluwatobi", myName, "name."],在这种情况下,计算机会分配一个引用回给 myName。因此,原始数组中的任何变化都会反映在重复的数组中。

扩展运算符示例 2:如何使用 spread 将一个字符串转换为单个数组项目

const myName = "Oluwatobi Sofela";

console.log([...myName]);

// 调用将返回:
[ "O", "l", "u", "w", "a", "t", "o", "b", "i", " ", "S", "o", "f", "e", "l", "a" ]

StackBlitz 中尝试

在上面的片段中,我们在一个数组字面对象([...])中使用了扩展语法(...),将 myName 的字符串值扩展为单个项目。

因此,"Oluwatobi Sofela" 被扩展为 [ "O", "l", "u", "w", "a", "t", "o", "b", "i", " ", "S", "o", "f", "e", "l", "a" ]

扩展运算符示例 3:spread 操作符在函数调用中的作用

const numbers = [1, 3, 5, 7];

function addNumbers(a, b, c, d) {
  return a + b + c + d;
}

console.log(addNumbers(...numbers));

// 调用将返回:
16

StackBlitz 中尝试

在上面的片段中,我们使用 spread 语法将 numbers 数组的内容扩展到 addNumbers() 的参数中。

假设 numbers 数组有四个以上的项目,在这种情况下,计算机将只使用前四项作为 addNumbers() 的参数,而忽略其他的。

下面是一个例子:

const numbers = [1, 3, 5, 7, 10, 200, 90, 59];

function addNumbers(a, b, c, d) {
  return a + b + c + d;
}

console.log(addNumbers(...numbers));

// 调用将返回:
16

StackBlitz 中尝试

这是另一个例子

const myName = "Oluwatobi Sofela";

function spellName(a, b, c) {
  return a + b + c;
}

console.log(spellName(...myName));      // returns: "Olu"

console.log(spellName(...myName[3]));   // returns: "wundefinedundefined"

console.log(spellName([...myName]));    // returns: "O,l,u,w,a,t,o,b,i, ,S,o,f,e,l,aundefinedundefined"

console.log(spellName({...myName}));    // returns: "[object Object]undefinedundefined"

StackBlitz 中尝试

扩展运算符示例 4:spread 在对象字面量中的作用

const myNames = ["Oluwatobi", "Sofela"];
const bio = { ...myNames, runs: "codesweetly.com" };

console.log(bio);

// 调用将返回:
{ 0: "Oluwatobi", 1: "Sofela", runs: "codesweetly.com" }

StackBlitz 中尝试

在上面的片段中,我们在 bio 对象中使用了 spread,将 myNames 的值扩展为单个属性。

关于扩展运算符,你需要知道什么

每当你选择使用扩展运算符时,请记住这三个基本信息。

信息 1:扩展运算符不能扩展对象字面的值

由于属性对象不是一个可迭代的对象,你不能使用扩展运算符来扩展其值。

然而,你可以使用扩展运算符将属性从一个对象克隆到另一个对象。

这是一个例子

const myName = { firstName: "Oluwatobi", lastName: "Sofela" };
const bio = { ...myName, website: "codesweetly.com" };

console.log(bio);

// 调用将返回:
{ firstName: "Oluwatobi", lastName: "Sofela", website: "codesweetly.com" };

StackBlitz 中尝试

上面的片段使用扩展运算符将 myName 的内容克隆到 bio 对象中。

注意:

  • 扩展运算符只能扩展可迭代对象的值。
  • 只有当一个对象(或其原型链中的任何对象)有一个带有 @@iterator 键的属性时,它才是可迭代的。
  • ArrayTypedArrayStringMapSet 都是内置的可迭代类型,因为它们默认都有 @@iterator 属性。
  • 一个属性对象不是一个可迭代的数据类型,因为它默认没有 @@iterator 属性。
  • 你可以通过在属性对象上添加 @@iterator 来使其成为可迭代对象。

信息 2:扩展运算符不会克隆相同的属性

假设你使用扩展运算符将属性从对象 A 克隆到对象 B 中,并且假设对象 B 包含与对象 A 中相同的属性。

这里有一个例子:

const myName = { firstName: "Tobi", lastName: "Sofela" };
const bio = { ...myName, firstName: "Oluwatobi", website: "codesweetly.com" };

console.log(bio);

// 调用将返回:
{ firstName: "Oluwatobi", lastName: "Sofela", website: "codesweetly.com" };

StackBlitz 中尝试

请注意,扩展运算符并没有将 myNamefirstName 属性复制到 bio 对象中,因为 bio 已经包含了 firstName 属性。

信息 3:当在包含非原始数据的对象上使用 spread 时,要注意它是如何工作的!

假设你在一个只包含原始值的对象(或数组)上使用扩展符,计算机不会在原始对象和复制的对象之间创建任何引用。

例如,考虑下面这段代码:

const myName = ["Sofela", "is", "my"];
const aboutMe = ["Oluwatobi", ...myName, "name."];

console.log(aboutMe);

// 调用将返回:
["Oluwatobi", "Sofela", "is", "my", "name."]

StackBlitz 中尝试

请注意,myName 中的每个项目都是一个原始值。因此,当我们使用 spread 运算符将 myName 克隆到 aboutMe 时,计算机并没有在两个数组之间创建任何引用。

因此,你对 myName 的任何改动都不会反映在 aboutMe 中,反之亦然。

例如,让我们为 myName 添加更多的内容:

myName.push("real");

现在,让我们检查一下 myNameaboutMe 现在的状态:

console.log(myName); // ["Sofela", "is", "my", "real"]

console.log(aboutMe); // ["Oluwatobi", "Sofela", "is", "my", "name."]

StackBlitz 中尝试

请注意,myName 的更新内容并没有反映在 aboutMe 中--因为 spread 在原始数组和复制的数组之间没有创建引用。

如果 myName 包含非原始值呢?

假设 myName 包含非基本值,在这种情况下,spread 运算符将在原本的非原始值和克隆的非原始值之间创建一个引用。

下面是一个例子:

const myName = [["Sofela", "is", "my"]];
const aboutMe = ["Oluwatobi", ...myName, "name."];

console.log(aboutMe);

// 调用将返回:
[ "Oluwatobi", ["Sofela", "is", "my"], "name." ]

StackBlitz 中尝试

请注意,myName 包含一个非原始值。

因此,使用 spread 运算符将 myName 的内容克隆到 aboutMe 中,导致计算机在两个数组之间创建一个引用。

因此,你对 myName 的副本所做的任何改动都会反映在 aboutMe 的版本中,反之亦然。

例如,让我们为 myName 添加更多的内容:

myName[0].push("real");

现在,让我们检查一下 myNameaboutMe 的当前状态:

console.log(myName); // [["Sofela", "is", "my", "real"]]

console.log(aboutMe); // ["Oluwatobi", ["Sofela", "is", "my", "real"], "name."]

StackBlitz 中尝试

注意,myName 的更新内容反映在 aboutMe 中--因为 spread 运算符在原始数组和重复的数组之间创建了一个引用。

下面是另一个例子:

const myName = { firstName: "Oluwatobi", lastName: "Sofela" };
const bio = { ...myName };

myName.firstName = "Tobi";

console.log(myName); // { firstName: "Tobi", lastName: "Sofela" }

console.log(bio); // { firstName: "Oluwatobi", lastName: "Sofela" }

StackBlitz 中尝试

在上面的片段中,myName 的更新没有反映在 bio 中,因为我们在一个只包含原始值的对象上使用了 spread 运算符。

注意:开发者会称 myName 为浅层对象,因为它只包含原始项目。

这里还有一个例子:

const myName = { 
  fullName: { firstName: "Oluwatobi", lastName: "Sofela" }
};

const bio = { ...myName };

myName.fullName.firstName = "Tobi";

console.log(myName); // { fullName: { firstName: "Tobi", lastName: "Sofela" } }

console.log(bio); // { fullName: { firstName: "Tobi", lastName: "Sofela" } }

StackBlitz 中尝试

在上面的片段中,myName 的更新反映在 bio 中,因为我们在一个包含非原始值的对象上使用了 spread 运算符。

注意:

  • 我们称 myName 为深层对象,因为它包含一个非原始项目。
  • 当你在克隆一个对象到另一个对象时创建引用,你会做浅拷贝。例如,...myName 产生了 myName 对象的浅拷贝,因为无论你在一个对象中做什么改动,都会反映在另一个对象中。
  • 当你克隆对象而不创建引用时,你会进行深度拷贝。例如,我可以通过 const bio = JSON.parse(JSON.stringify(myName))myName 深度复制到 bio。通过这样做,计算机将把 myName 克隆到 bio 中而不创建任何引用。
  • You can break off the reference between the two objects by replacing the fullName object inside myName or bio with a new object. For instance, doing myName.fullName = { firstName: "Tobi", lastName: "Sofela" } would disconnect the pointer between myName and bio.
  • 你可以通过用一个新的对象替换 myNamebio 里面的 fullName 对象来切断两个对象之间的引用。例如,myName.fullName = { firstName: "Tobi", lastName: "Sofela" } 将断开 myNamebio 之间的指针。

总结

本文讨论了 rest 和 spread 运算符之间的区别。我们还使用了一些例子来说明每个运算符是如何工作的。

谢谢你阅读本文!