函式進階內容 Rest 引數與 Spread 語法
Rest 引數與 Spread 語法
在 JavaScript 中,很多內建函式都支援傳入任意數量的引數。
例如:
Math.max(arg1, arg2, ..., argN)
—— 返回入參中的最大值。Object.assign(dest, src1, ..., srcN)
—— 依次將屬性從src1..N
複製到dest
。- ……等。
在本章中,我們將學習如何程式設計實現支援函式可傳入任意數量的引數。以及,如何將陣列作為引數傳遞給這類函式。
Rest 引數...
在 JavaScript 中,無論函式是如何定義的,你都可以使用任意數量的引數呼叫函式。
例如:
function sum(a, b) {
return a + b;
}
alert( sum(1, 2, 3, 4, 5) );
雖然這裡不會因為傳入“過多”的引數而報錯。但是當然,在結果中只有前兩個引數被計算進去了。
Rest 引數可以通過使用三個點...
並在後面跟著包含剩餘引數的陣列名稱,來將它們包含在函式定義中。這些點的字面意思是“將剩餘引數收集到一個數組中”。
例如,我們需要把所有的引數都放到陣列args
中:
function sumAll(...args) { // 陣列名為 args
let sum = 0;
for (let arg of args) sum += arg;
return sum;
}
alert( sumAll(1) ); // 1
alert( sumAll(1, 2) ); // 3
alert( sumAll(1, 2, 3) ); // 6
我們也可以選擇獲取第一個引數作為變數,並將剩餘的引數收集起來。
下面的例子把前兩個引數定義為變數,並把剩餘的引數收集到titles
陣列中:
function showName(firstName, lastName, ...titles) {
alert( firstName + ' ' + lastName ); // Julius Caesar
// 剩餘的引數被放入 titles 陣列中
// i.e. titles = ["Consul", "Imperator"]
alert( titles[0] ); // Consul
alert( titles[1] ); // Imperator
alert( titles.length ); // 2
}
showName("Julius", "Caesar", "Consul", "Imperator");
Rest 引數必須放到引數列表的末尾
Rest 引數會收集剩餘的所有引數,因此下面這種用法沒有意義,並且會導致錯誤:
function f(arg1, ...rest, arg2) { // arg2 在 ...rest 後面?!
// error
}
...rest
必須處在最後。
“arguments” 變數
有一個名為arguments
的特殊的類陣列物件,該物件按引數索引包含所有引數。
例如:
function showName() {
alert( arguments.length );
alert( arguments[0] );
alert( arguments[1] );
// 它是可遍歷的
// for(let arg of arguments) alert(arg);
}
// 依次顯示:2,Julius,Caesar
showName("Julius", "Caesar");
// 依次顯示:1,Ilya,undefined(沒有第二個引數)
showName("Ilya");
在過去,JavaScript 中沒有 rest 引數,而使用arguments
是獲取函式所有引數的唯一方法。現在它仍然有效,我們可以在一些老程式碼裡找到它。
但缺點是,儘管arguments
是一個類陣列,也是可迭代物件,但它終究不是陣列。它不支援陣列方法,因此我們不能呼叫arguments.map(...)
等方法。
此外,它始終包含所有引數,我們不能像使用 rest 引數那樣只擷取入參的一部分。
因此,當我們需要這些功能時,最好使用 rest 引數。
箭頭函式是沒有"arguments"
如果我們在箭頭函式中訪問arguments
,訪問到的arguments
並不屬於箭頭函式,而是屬於箭頭函式外部的“普通”函式。
舉個例子:
function f() {
let showArg = () => alert(arguments[0]);
showArg();
}
f(1); // 1
我們已經知道,箭頭函式沒有自身的this
。現在我們知道了它們也沒有特殊的arguments
物件。
Spread 語法
我們剛剛看到了如何從引數列表中獲取陣列。
不過有時候我們也需要做與之相反的事兒。
例如,內建函式Math.max會返回引數中最大的值:
alert( Math.max(3, 5, 1) ); // 5
假如我們有一個數組[3, 5, 1]
,我們該如何用它呼叫Math.max
呢?
直接把陣列“原樣”傳入是不會奏效的,因為Math.max
希望你傳入一個列表形式的數值型引數,而不是一個數組:
let arr = [3, 5, 1];
alert( Math.max(arr) ); // NaN
毫無疑問,我們不可能手動地去一一設定引數Math.max(arg[0], arg[1], arg[2])
,因為我們不確定這兒有多少個。在指令碼執行時,可能引數陣列中有很多個元素,也可能一個都沒有。並且這樣設定的程式碼也很醜。
Spread 語法來幫助你了!它看起來和 rest 引數很像,也使用...
,但是二者的用途完全相反。
當在函式呼叫中使用...arr
時,它會把可迭代物件arr
“展開”到引數列表中。
以Math.max
為例:
let arr = [3, 5, 1];
alert( Math.max(...arr) ); // 5(spread 語法把陣列轉換為引數列表)
我們還可以通過這種方式傳遞多個可迭代物件:
let arr1 = [1, -2, 3, 4];
let arr2 = [8, 3, -8, 1];
alert( Math.max(...arr1, ...arr2) ); // 8
我們甚至還可以將 spread 語法與常規值結合使用:
let arr1 = [1, -2, 3, 4];
let arr2 = [8, 3, -8, 1];
alert( Math.max(1, ...arr1, 2, ...arr2, 25) ); // 25
並且,我們還可以使用 spread 語法來合併陣列:
let arr = [3, 5, 1];
let arr2 = [8, 9, 15];
let merged = [0, ...arr, 2, ...arr2];
alert(merged); // 0,3,5,1,2,8,9,15(0,然後是 arr,然後是 2,然後是 arr2)
在上面的示例中,我們使用陣列展示了 spread 語法,其實任何可迭代物件都可以。
例如,在這兒我們使用 spread 語法將字串轉換為字元陣列:
let str = "Hello";
alert( [...str] ); // H,e,l,l,o
Spread 語法內部使用了迭代器來收集元素,與for..of
的方式相同。
因此,對於一個字串,for..of
會逐個返回該字串中的字元,...str
也同理會得到"H","e","l","l","o"
這樣的結果。隨後,字元列表被傳遞給陣列初始化器[...str]
。
對於這個特定任務,我們還可以使用Array.from
來實現,因為該方法會將一個可迭代物件(如字串)轉換為陣列:
let str = "Hello";
// Array.from 將可迭代物件轉換為陣列
alert( Array.from(str) ); // H,e,l,l,o
執行結果與[...str]
相同。
不過Array.from(obj)
和[...obj]
存在一個細微的差別:
Array.from
適用於類陣列物件也適用於可迭代物件。- Spread 語法只適用於可迭代物件。
因此,對於將一些“東西”轉換為陣列的任務,Array.from
往往更通用。
獲取一個 array/object 的副本
還記得我們之前講過的Object.assign()
嗎?
使用 spread 語法也可以做同樣的事情(譯註:也就是進行淺拷貝)。
let arr = [1, 2, 3];
let arrCopy = [...arr]; // 將陣列 spread 到引數列表中
// 然後將結果放到一個新陣列
// 兩個陣列中的內容相同嗎?
alert(JSON.stringify(arr) === JSON.stringify(arrCopy)); // true
// 兩個陣列相等嗎?
alert(arr === arrCopy); // false(它們的引用是不同的)
// 修改我們初始的陣列不會修改副本:
arr.push(4);
alert(arr); // 1, 2, 3, 4
alert(arrCopy); // 1, 2, 3
並且,也可以通過相同的方式來複制一個物件:
let obj = { a: 1, b: 2, c: 3 };
let objCopy = { ...obj }; // 將物件 spread 到引數列表中
// 然後將結果返回到一個新物件
// 兩個物件中的內容相同嗎?
alert(JSON.stringify(obj) === JSON.stringify(objCopy)); // true
// 兩個物件相等嗎?
alert(obj === objCopy); // false (not same reference)
// 修改我們初始的物件不會修改副本:
obj.d = 4;
alert(JSON.stringify(obj)); // {"a":1,"b":2,"c":3,"d":4}
alert(JSON.stringify(objCopy)); // {"a":1,"b":2,"c":3}
這種方式比使用let arrCopy = Object.assign([], arr);
來複制陣列,或使用let objCopy = Object.assign({}, obj);
來複制物件寫起來要短得多。因此,只要情況允許,我們更喜歡使用它。
總結
當我們在程式碼中看到"..."
時,它要麼是 rest 引數,要麼就是 spread 語法。
有一個簡單的方法可以區分它們:
- 若
...
出現在函式引數列表的最後,那麼它就是 rest 引數,它會把引數列表中剩餘的引數收集到一個數組中。 - 若
...
出現在函式呼叫或類似的表示式中,那它就是 spread 語法,它會把一個數組展開為列表。
使用場景:
- Rest 引數用於建立可接受任意數量引數的函式。
- Spread 語法用於將陣列傳遞給通常需要含有許多引數的列表的函式。
它們倆的出現幫助我們輕鬆地在列表和引數陣列之間來回轉換。
“舊式”的arguments
(類陣列且可迭代的物件)也依然能夠幫助我們獲取函式呼叫中的所有引數。