JavaScript使用符号三個點(...)作為剩餘運算符和展開運算符,不過這兩個運算符是有差別的。
最主要的差別就是,剩餘運算符将使用者提供的某些特定值的其餘部分放入JavaScript數組中,而展開運算符則将可疊代的對象展開為單個元素。
例如下面這段代碼,其中使用了剩餘運算符将部分值存放到數組中:
// Use rest to enclose the rest of specific user-supplied values into an array:
function myBio(firstName, lastName, ...otherInfo) {
return otherInfo;
}
// Invoke myBio function while passing five arguments to its parameters:
myBio("Oluwatobi", "Sofela", "CodeSweetly", "Web Developer", "Male");
// The invocation above will return:
["CodeSweetly", "Web Developer", "Male"]
檢視運作結果
上面的代碼中,我們使用...otherInfo将傳入函數myBio()參數中的剩餘部分"CodeSweetly","We Developer"和"Male"存放到數組中。
然後我們再來看下面這個例子,其中使用了展開運算符:
// Define a function with three parameters:
function myBio(firstName, lastName, company) {
return `${firstName} ${lastName} runs ${company}`;
}
// Use spread to expand an array’s items into individual arguments:
myBio(...["Oluwatobi", "Sofela", "CodeSweetly"]);
// The invocation above will return:
“Oluwatobi Sofela runs CodeSweetly”
上面的代碼中,我們使用展開運算符(...)将數組["Oluwatobi", "Sofela", "CodeSweetly"]的内容逐一展開并傳遞給函數myBio()的參數。
如果你對剩餘運算符和展開運算符不是很熟悉,不用太擔心,本文接下來會對它們進行介紹。
在接下來的章節中,我們将詳細讨論剩餘運算符和展開運算符在JavaScript中是如何工作的。
什麼是剩餘運算符?
正如上面所提到的,剩餘運算符将使用者提供的某些特定值的其餘部分放入JavaScript數組中。文法如下:
...yourValues
上面代碼中的三個點(...)用來表示剩餘運算符。
剩餘運算符之後的内容用來表示你希望填充到數組中的值。注意,隻能在函數定義的最後一個參數中使用剩餘運算符。
剩餘運算符在JavaScript函數中是如何工作的?
在JavaScript函數中,剩餘運算符被用在函數最後一個參數的前面。如下面的代碼:
// Define a function with two regular parameters and one rest parameter:
function myBio(firstName, lastName, ...otherInfo) {
return otherInfo;
}
這裡,剩餘運算符告訴JavaScript程式将使用者提供給參數otherInfo的任何值都添加到一個數組中。然後,将該數組指派給otherInfo參數。
是以,我們将...otherInfo稱之為剩餘參數。
需要注意的是,調用時傳遞給函數的實參中剩餘參數是可選的。
另一個例子:
// Define a function with two regular parameters and one rest parameter:
function myBio(firstName, lastName, ...otherInfo) {
return otherInfo;
}
// Invoke myBio function while passing five arguments to its parameters:
myBio("Oluwatobi", "Sofela", "CodeSweetly", "Web Developer", "Male");
// The invocation above will return:
["CodeSweetly", "Web Developer", "Male"]
上面的代碼中,調用函數myBio()時傳入了5個參數,而函數myBio()的實參隻有3個。也就是說,參數"Oluwatobi"和"Sofela"分别傳遞給了firstName和lastName,其它的參數("CodeSweetly","Web Developer"和"Male")都傳遞給了剩餘參數otherInfo。是以,函數myBio()傳回的結果是["CodeSweetly", "Web Developer", "Male"],它們都是剩餘參數otherInfo的内容。
注意!不能在包含剩餘參數的函數體中使用"use strict"
記住,不能在任何包含剩餘參數,預設參數,或參數解構的函數體中使用"use strict"指令。否則,JavaScript将報文法錯誤。
考慮下面的代碼:
// Define a function with one rest parameter:
function printMyName(...value) {
"use strict";
return value;
}
// The definition above will return:
"Uncaught SyntaxError: Illegal 'use strict' directive in function with non-simple parameter list"
注意:隻有當整個腳本或封閉作用域處于strict模式時,才可以将"use strict"放到函數體外。
現在我們知道了剩餘運算符在函數中的作用,接下來我們來看看它在參數解構中是如何工作的。
剩餘運算符在參數解構中是如何工作的?
剩餘運算符在參數解構指派時通常被用在最後一個變量的前面。下面是一個例子:
// Define a destructuring array with two regular variables and one rest variable:
const [firstName, lastName, ...otherInfo] = [
"Oluwatobi", "Sofela", "CodeSweetly", "Web Developer", "Male"
];
// Invoke the otherInfo variable:
console.log(otherInfo);
// The invocation above will return:
["CodeSweetly", "Web Developer", "Male"]
這裡的剩餘運算符(...)告訴JavaScript程式将使用者提供的其它值都添加到一個數組中。然後,将該數組指派給otherInfo變量。
是以,我們将這裡的...otherInfo稱之為剩餘變量。
// Define a destructuring object with two regular variables and one rest variable:
const { firstName, lastName, ...otherInfo } = {
firstName: "Oluwatobi",
lastName: "Sofela",
companyName: "CodeSweetly",
profession: "Web Developer",
gender: "Male"
}
// Invoke the otherInfo variable:
console.log(otherInfo);
// The invocation above will return:
{companyName: "CodeSweetly", profession: "Web Developer", gender: "Male"}
注意上面的代碼中,剩餘運算符将一個屬性對象(而不是數組)指派給otherInfo變量。也就是說,當你在解構一個對象時使用剩餘運算符,它将生成一個屬性對象而非數組。
但是,如果你在解構數組或函數時使用剩餘運算符,它将生成數組字面量。
你應該已經注意到了,在JavaScript arguments和剩餘參數之間存在一些差別,下面我們來看看這些差別都有哪些。
JavaScript arguments和剩餘參數之間有哪些差別?
下面我列出了JavaScript arguments和剩餘參數之間的一些差別:
差別1:arguments對象是一個類似于數組的對象,但它并非真正的數組!
請記住這一點,JavaScript arguments對象不是真正的數組。它是一個類似于數組的對象,不具備數組所擁有的任何特性。
而剩餘參數是一個真正的數組,你可以在它上面使用數組所擁有的任何方法。例如,你可以對一個剩餘參數使用sort()、map()、forEach()或pop()方法。但你不能對arguments對象使用這些方法。
差別2:不能在箭頭函數中使用arguments對象
arguments對象在箭頭函數中不可用,而剩餘參數在所有的函數中都是可用的,也包括箭頭函數。
差別3:優先使用剩餘參數
優先使用剩餘參數而不是arguments對象,特别是在編寫ES6相容代碼時。
我們已經了解了剩餘運算符是如何工作的,下面我們來讨論下展開運算符,并看看它和剩餘運算符之間的差別。
什麼是展開運算符以及它在JavaScript中是如何工作的?
展開運算符(...)将一個可疊代的對象展開為單個元素。
展開運算符可以應用在數組字面量,函數調用,以及被初始化的屬性對象中,它将可疊代對象的值逐一展開到單獨的元素中。實際上,它和剩餘操作符正好相反。
注意,隻有在數組字面量,函數調用,或被初始化的屬性對象中使用展開運算符時才有效。
下面我們來看一些實際的例子。
示例1:展開運算符在數組字面量中如何工作
const myName = ["Sofela", "is", "my"];
const aboutMe = ["Oluwatobi", ...myName, "name."];
console.log(aboutMe);
// The invocation above will return:
[ "Oluwatobi", "Sofela", "is", "my", "name." ]
上面的代碼中,展開運算符(...)将數組myName拷貝到aboutMe中。
注意:
- 對myName的任何修改不會反映到aboutMe中。因為myName數組中的所有值都是原語。擴充操作符隻是簡單地将myName數組的内容複制并粘貼到aboutMe中,而不建立任何對原始數組元素的引用。
- 展開運算符隻做淺拷貝。是以,當myName數組中包含任何非原語值時,JavaScript将在myName和aboutMe之間建立一個引用。有關展開運算符如何處理原語值和非原語值的更多資訊,可以檢視這裡。
- 假設我們沒有使用展開運算符來複制數組myName的内容,例如我們編寫這行代碼const aboutMe = [ "Oluwatobi", myName, "name." ] 這種情況下JavaScript将給myName配置設定一個引用,這樣,所有對myName數組所做的修改就都會反映到aboutMe數組中。
示例2:如何使用展開運算符将字元串轉換為數組
const myName = "Oluwatobi Sofela";
console.log([...myName]);
// The invocation above will return:
[ "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:展開運算符如何在函數調用中工作
const numbers = [1, 3, 5, 7];
function addNumbers(a, b, c, d) {
return a + b + c + d;
}
console.log(addNumbers(...numbers));
// The invocation above will return:
16
上面的代碼中,我們使用展開運算符将數組numbers的内容展開并傳遞給函數addNumbers()的參數。如果數組numbers的元素多于4個,JavaScript隻會将前4個元素作為參數傳遞給函數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));
// The invocation above will return:
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:展開運算符在對象字面量中如何工作
const myNames = ["Oluwatobi", "Sofela"];
const bio = { ...myNames, runs: "codesweetly.com" };
console.log(bio);
// The invocation above will return:
{ 0: "Oluwatobi", 1: "Sofela", runs: "codesweetly.com" }
上面的代碼中,我們在bio對象内部使用展開運算符将數組myNames的值展開為各個屬性。
有關展開運算符我們需要知道的
當使用展開運算符時,請記住以下三個基本資訊。
1. 展開運算符不能展開對象字面量的值
由于屬性對象是非可疊代對象,是以不能使用展開運算符将它的值進行展開。但是,你可以使用展開運算符将一個對象的屬性克隆到另一個對象中。
看下面這個例子:
const myName = { firstName: "Oluwatobi", lastName: "Sofela" };
const bio = { ...myName, website: "codesweetly.com" };
console.log(bio);
// The invocation above will return:
{ firstName: "Oluwatobi", lastName: "Sofela", website: "codesweetly.com" };
上面的代碼中,我們使用展開運算符将myName對象的内容克隆到了bio對象中。
- 展開運算符隻能展開可疊代對象的值。
- 隻有當一個對象包含一個帶有@@iterator key的屬性時,才是一個可疊代的對象。
- Array,TypedArray,String,Map,Set都是内置的可疊代類型,因為它們預設都帶有@@iterator屬性。
- 屬性對象是非可疊代數組類型,因為預設情況下它沒有@@iterator屬性。
- 可以在屬性對象上添加@@iterator使其成為可疊代對象。
2. 展開運算符不克隆相同的屬性
假設我們使用展開運算符将對象A的屬性克隆到對象B中,如果對象B包含與對象A中相同的屬性,那麼對象B的屬性将覆寫對象A的屬性。換句話說,在這個過程中,對象A中那些與對象B相同的屬性不會被克隆。
const myName = { firstName: "Tobi", lastName: "Sofela" };
const bio = { ...myName, firstName: "Oluwatobi", website: "codesweetly.com" };
console.log(bio);
// The invocation above will return:
{ firstName: "Oluwatobi", lastName: "Sofela", website: "codesweetly.com" };
注意,展開運算符沒有将myName對象的firstName屬性的值複制到bio對象中,因為對象bio中已經包含firstName屬性了。
3. 注意展開運算符在包含非原語的對象中是何如工作的
如果在隻包含原語值的對象(或數組)上使用展開運算符,JavaScript不會在原對象和複制對象之間建立任何引用。
看下面這段代碼:
const myName = ["Sofela", "is", "my"];
const aboutMe = ["Oluwatobi", ...myName, "name."];
console.log(aboutMe);
// The invocation above will return:
["Oluwatobi", "Sofela", "is", "my", "name."]
注意,myName中的每一個元素都是一個原語值,是以,當我們使用展開運算符将myName克隆到aboutMe時,JavaScript不會在兩個數組之間建立任何引用。是以,對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數組中,因為展開運算符并沒有在原始數組和複制數組之間建立任何引用。
如果myName數組包含非原語項呢?
假設myName包含非原語項,這種情況下,展開運算符将在原數組的非原語項和克隆項之間建立一個引用。
看下面的例子:
const myName = [["Sofela", "is", "my"]];
const aboutMe = ["Oluwatobi", ...myName, "name."];
console.log(aboutMe);
// The invocation above will return:
[ "Oluwatobi", ["Sofela", "is", "my"], "name." ]
注意,這裡的myName數組包含一個非原語項。
是以,當使用展開運算符将myName的内容克隆到aboutMe時,JavaScript将在兩個數組之間建立一個引用。這樣,任何對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數組中,因為展開運算符在原始數組和複制數組之間建立了一個引用。
另外一個例子:
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對象中,因為我們在隻包含原語值的對象上使用展開運算符。
注意,開發人員通常将這裡的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對象中,因為我們在包含非原語值的對象上使用了展開運算符。
- 我們稱這裡的myName為深對象,因為它包含非原語項。
- 将一個對象克隆到另一個對象時,如果建立了引用,則進行的是淺拷貝。例如,...myName産生了myName對象的一個淺拷貝,因為你對其中一個對象所做的任何修改都會反映到另一個對象中。
- 将一個對象克隆到另一個對象時,如果沒有建立引用,則進行的時深拷貝。例如,我可以通過const bio = JSON.parse(JSON.stringify(myName))将myName對象深拷貝到bio對象中。如此一來,JavaScript将把myName克隆到bio中而不建立任何引用。
- 我們可以通過用一個新對象來替換myName或bio中的fullName子對象,進而切斷myName和bio之間的引用。例如,使用myName.fullName = { firstName: "Tobi", lastName: "Sofela" }來斷開myName和bio之間的指針。
結語
本文讨論了剩餘操作符和展開操作符之間的差別,并通過示例說明了它們在JavaScript中是如何工作的。
原文位址:https://www.freecodecamp.org/news/javascript-rest-vs-spread-operators/