在 JavaScript 中,很多內建函數都支持傳入任意數量的參數。
例如:
Math.max(arg1, arg2, ..., argN)
—— 返回參數中的最大值。
Object.assign(dest, src1, ..., srcN)
—— 依次將屬性從 src1..N
複制到 dest
。
……等。
在本章中,我們將學習如何編寫支持傳入任意數量參數的函數,以及如何將數組作爲參數傳遞給這類函數。
...
在 JavaScript 中,無論函數是如何定義的,妳都可以在調用它時傳入任意數量的參數。
例如:
function sum(a, b) { return a + b; } alert( sum(1, 2, 3, 4, 5) );
雖然這裏這個函數不會因爲傳入過多的參數而報錯。但是,當然,只有前兩個參數被求和了。
我們可以在函數定義中聲明壹個數組來收集參數。語法是這樣的:...變量名
,這將會聲明壹個數組並指定其名稱,其中存有剩余的參數。這三個點的語義就是“收集剩余的參數並存進指定數組中”。
例如,我們需要把所有的參數都放到數組 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
的特殊類數組對象可以在函數中被訪問,該對象以參數在參數列表中的索引作爲鍵,存儲所有參數。
例如:
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
對象。
我們剛剛看到了如何從參數列表中獲取數組。
有時候我們也需要做與之相反的事。
例如,內建函數 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 語法這樣操作任何可叠代對象。
例如,在這兒我們使用 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
往往更通用。
還記得我們 之前講過的 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
(類數組且可叠代的對象)也依然能夠幫助我們獲取函數調用中的所有參數。