原文: 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"]
在上面的片段中,我们使用 ...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”
在上面的片段中,我们使用扩展运算符(...
)将 ["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"]
在上面的片段中,注意 myBio
的调用向函数传递了五个参数。
换句话说,"Oluwatobi"
和 "Sofela"
被分配到 firstName
和 lastName
参数中。
同时,剩余运算符将其余的参数("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"
printMyName()
返回了一个语法错误,因为我们在一个有 rest 参数的函数中使用了 “use strict”
指令。
但假设你需要你的函数处于严格模式,同时也使用 rest 参数,在这种情况下,你可以把 “use strict”
指令写在函数的外面。
下面是一个例子:
// 在你的函数外定义一个 "use strict" 指令:
"use strict";
// 定义一个函数,具有一个 rest 参数
function printMyName(...value) {
return value;
}
// 调用 printMyName 函数,同时传递两个参数给它
printMyName("Oluwatobi", "Sofela");
// 调用将返回:
["Oluwatobi", "Sofela"]
注意:只有当整个脚本或包围的范围都处于严格模式时,才可以在你的函数外添加 “use strict”
指令。
现在我们知道了 rest 是如何在函数中工作的,我们可以谈谈它是如何在解构赋值中工作的。
rest 运算符如何在解构赋值中工作
rest 运算符通常被用作解构赋值最后一个变量的前缀。
下面是一个例子:
// 定义一个具有两个常规变量和一个 rest 变量的解构数组:
const [firstName, lastName, ...otherInfo] = [
"Oluwatobi", "Sofela", "CodeSweetly", "Web Developer", "Male"
];
// 调用 otherInfo 变量:
console.log(otherInfo);
// 调用将返回:
["CodeSweetly", "Web Developer", "Male"]
剩余运算符(...
)指示计算机将用户提供的其余数值添加到一个数组中。然后,它将该数组分配给 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"}
在上面的片段中,注意到 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." ]
上面的片段使用 spread(...
)将 myName
数组复制到 aboutMe
中。
注意:
- 对
myName
的修改不会反映在aboutMe
中,因为myName
中的所有值都是基本数据类型。因此,扩展运算符只是简单地将myName
的内容复制并粘贴到aboutMe
中,而没有创建任何对原始数组的引用。 - 正如 @nombrekeff 在这里的评论中提到的,扩展操作符只做浅层复制。因此,请记住,假设
myName
包含任何非原始值,计算机会在myName
和aboutMe
之间创建一个引用。更多关于扩展运算符如何处理原始值和非原始值的信息,请参见信息 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" ]
在上面的片段中,我们在一个数组字面对象([...]
)中使用了扩展语法(...
),将 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
在上面的片段中,我们使用 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
这是另一个例子:
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"
扩展运算符示例 4:spread 在对象字面量中的作用
const myNames = ["Oluwatobi", "Sofela"];
const bio = { ...myNames, runs: "codesweetly.com" };
console.log(bio);
// 调用将返回:
{ 0: "Oluwatobi", 1: "Sofela", runs: "codesweetly.com" }
在上面的片段中,我们在 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" };
上面的片段使用扩展运算符将 myName
的内容克隆到 bio
对象中。
注意:
- 扩展运算符只能扩展可迭代对象的值。
- 只有当一个对象(或其原型链中的任何对象)有一个带有 @@iterator 键的属性时,它才是可迭代的。
- Array、TypedArray、String、Map 和 Set 都是内置的可迭代类型,因为它们默认都有
@@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" };
请注意,扩展运算符并没有将 myName
的 firstName
属性复制到 bio
对象中,因为 bio
已经包含了 firstName
属性。
信息 3:当在包含非原始数据的对象上使用 spread 时,要注意它是如何工作的!
假设你在一个只包含原始值的对象(或数组)上使用扩展符,计算机不会在原始对象和复制的对象之间创建任何引用。
例如,考虑下面这段代码:
const myName = ["Sofela", "is", "my"];
const aboutMe = ["Oluwatobi", ...myName, "name."];
console.log(aboutMe);
// 调用将返回:
["Oluwatobi", "Sofela", "is", "my", "name."]
请注意,myName
中的每个项目都是一个原始值。因此,当我们使用 spread 运算符将 myName
克隆到 aboutMe
时,计算机并没有在两个数组之间创建任何引用。
因此,你对 myName
的任何改动都不会反映在 aboutMe
中,反之亦然。
例如,让我们为 myName
添加更多的内容:
myName.push("real");
现在,让我们检查一下 myName
和 aboutMe
现在的状态:
console.log(myName); // ["Sofela", "is", "my", "real"]
console.log(aboutMe); // ["Oluwatobi", "Sofela", "is", "my", "name."]
请注意,myName
的更新内容并没有反映在 aboutMe
中--因为 spread 在原始数组和复制的数组之间没有创建引用。
如果 myName
包含非原始值呢?
假设 myName
包含非基本值,在这种情况下,spread 运算符将在原本的非原始值和克隆的非原始值之间创建一个引用。
下面是一个例子:
const myName = [["Sofela", "is", "my"]];
const aboutMe = ["Oluwatobi", ...myName, "name."];
console.log(aboutMe);
// 调用将返回:
[ "Oluwatobi", ["Sofela", "is", "my"], "name." ]
请注意,myName
包含一个非原始值。
因此,使用 spread 运算符将 myName
的内容克隆到 aboutMe
中,导致计算机在两个数组之间创建一个引用。
因此,你对 myName
的副本所做的任何改动都会反映在 aboutMe
的版本中,反之亦然。
例如,让我们为 myName
添加更多的内容:
myName[0].push("real");
现在,让我们检查一下 myName
和 aboutMe
的当前状态:
console.log(myName); // [["Sofela", "is", "my", "real"]]
console.log(aboutMe); // ["Oluwatobi", ["Sofela", "is", "my", "real"], "name."]
注意,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" }
在上面的片段中,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" } }
在上面的片段中,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 insidemyName
orbio
with a new object. For instance, doingmyName.fullName = { firstName: "Tobi", lastName: "Sofela" }
would disconnect the pointer betweenmyName
andbio
. - 你可以通过用一个新的对象替换
myName
或bio
里面的fullName
对象来切断两个对象之间的引用。例如,myName.fullName = { firstName: "Tobi", lastName: "Sofela" }
将断开myName
和bio
之间的指针。
总结
本文讨论了 rest 和 spread 运算符之间的区别。我们还使用了一些例子来说明每个运算符是如何工作的。
谢谢你阅读本文!