1. 程式人生 > 其它 >函式進階內容 Rest 引數與 Spread 語法

函式進階內容 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(類陣列且可迭代的物件)也依然能夠幫助我們獲取函式呼叫中的所有引數。