2022 年 10 月 18 日

休息參數和擴散語法

許多 JavaScript 內建函式支援任意數量的參數。

例如

  • Math.max(arg1, arg2, ..., argN) – 傳回參數中最大的值。
  • Object.assign(dest, src1, ..., srcN) – 將屬性從 src1..N 複製到 dest 中。
  • …以此類推。

在本章節中,我們將學習如何執行相同的操作。此外,我們還將學習如何將陣列傳遞給函式作為參數。

Rest 參數 ...

函式可以呼叫任何數量的引數,無論其定義方式為何。

如下所示

function sum(a, b) {
  return a + b;
}

alert( sum(1, 2, 3, 4, 5) );

不會因為「過多」的引數而發生錯誤。但當然在結果中,只會計算前兩個引數,因此上述程式碼中的結果為 3

可以使用三個點 ... 後接將包含這些引數的陣列名稱,將其餘的參數包含在函式定義中。這些點字面上的意思為「將其餘的參數收集到陣列中」。

例如,要將所有引數收集到陣列 args

function sumAll(...args) { // args is the name for the array
  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

  // the rest go into titles array
  // 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 after ...rest ?!
  // error
}

...rest 必須永遠是最後一個。

「arguments」變數

還有一個特殊的類陣列物件稱為 arguments,其中包含所有引數及其索引。

例如

function showName() {
  alert( arguments.length );
  alert( arguments[0] );
  alert( arguments[1] );

  // it's iterable
  // for(let arg of arguments) alert(arg);
}

// shows: 2, Julius, Caesar
showName("Julius", "Caesar");

// shows: 1, Ilya, undefined (no second argument)
showName("Ilya");

在以前,語言中沒有 Rest 參數,而使用 arguments 是取得函式所有引數的唯一方法。而且它仍然有效,我們可以在舊程式碼中找到它。

但缺點是,儘管 arguments 類似陣列且可迭代,但它並非陣列。它不支援陣列方法,因此我們無法呼叫 arguments.map(...)

此外,它總是包含所有引數。我們無法像使用 Rest 參數那樣部分擷取它們。

因此,當我們需要這些功能時,建議使用 Rest 參數。

Arrow 函式沒有 "arguments"

如果我們從 Arrow 函式存取 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(arr[0], arr[1], arr[2]) 中手動列出項目,因為我們可能不確定有多少個項目。當我們的指令碼執行時,可能會有很多,也可能沒有。那樣會很醜陋。

展開語法 來救援!它看起來類似於 rest 參數,也使用 ...,但做的是完全相反的事。

當在函式呼叫中使用 ...arr 時,它會將可迭代物件 arr「展開」成引數清單。

對於 Math.max

let arr = [3, 5, 1];

alert( Math.max(...arr) ); // 5 (spread turns array into a list of arguments)

我們也可以用這種方式傳遞多個可迭代物件

let arr1 = [1, -2, 3, 4];
let arr2 = [8, 3, -8, 1];

alert( Math.max(...arr1, ...arr2) ); // 8

我們甚至可以將展開語法與一般值結合使用

let arr1 = [1, -2, 3, 4];
let arr2 = [8, 3, -8, 1];

alert( Math.max(1, ...arr1, 2, ...arr2, 25) ); // 25

此外,展開語法可以用於合併陣列

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, then arr, then 2, then arr2)

在上面的範例中,我們使用陣列來示範展開語法,但任何可迭代物件都可以。

例如,這裡我們使用展開語法將字串轉換成字元陣列

let str = "Hello";

alert( [...str] ); // H,e,l,l,o

展開語法在內部使用迭代器來收集元素,就像 for..of 所做的一樣。

因此,對於字串,for..of 會傳回字元,而 ...str 會變成 "H","e","l","l","o"。字元清單會傳遞給陣列初始化器 [...str]

對於這個特定任務,我們也可以使用 Array.from,因為它會將可迭代物件(例如字串)轉換成陣列

let str = "Hello";

// Array.from converts an iterable into an array
alert( Array.from(str) ); // H,e,l,l,o

結果與 [...str] 相同。

Array.from(obj)[...obj] 之間有一個細微的差別

  • Array.from 對類陣列和可迭代物件都有效。
  • 展開語法只對可迭代物件有效。

因此,對於將某個東西轉換成陣列的任務,Array.from 往往更通用。

複製陣列/物件

還記得我們以前討論過 Object.assign()

使用展開語法也可以做到同樣的事情。

let arr = [1, 2, 3];

let arrCopy = [...arr]; // spread the array into a list of parameters
                        // then put the result into a new array

// do the arrays have the same contents?
alert(JSON.stringify(arr) === JSON.stringify(arrCopy)); // true

// are the arrays equal?
alert(arr === arrCopy); // false (not same reference)

// modifying our initial array does not modify the copy:
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 the object into a list of parameters
                          // then return the result in a new object

// do the objects have the same contents?
alert(JSON.stringify(obj) === JSON.stringify(objCopy)); // true

// are the objects equal?
alert(obj === objCopy); // false (not same reference)

// modifying our initial object does not modify the copy:
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 objCopy = Object.assign({}, obj) 或陣列的 let arrCopy = Object.assign([], arr) 短得多,所以我們盡量在可以的時候使用它。

摘要

當我們在程式碼中看到 "..." 時,它可能是 rest 參數或展開語法。

區分它們的方法很簡單

  • ... 出現在函式參數的結尾時,它就是「rest 參數」,它會將參數清單的其餘部分收集到陣列中。
  • ... 出現在函式呼叫或類似的地方時,它稱為「展開語法」,它會將陣列展開成清單。

使用模式

  • Rest 參數用於建立可接受任意數目參數的函式。
  • 展開語法用於將陣列傳遞給通常需要許多參數清單的函式。

它們一起協助輕鬆地在清單和參數陣列之間移動。

函式呼叫的所有參數也可用於「舊式」arguments:類似陣列的可迭代物件。

教學地圖

留言

留言前請先閱讀…
  • 如果您有改善建議,請 提交 GitHub 議題 或發起 pull request,而不是留言。
  • 如果您無法理解文章中的某個部分,請詳細說明。
  • 若要插入少數幾個字元的程式碼,請使用 <code> 標籤,若要插入多行程式碼,請將它們包覆在 <pre> 標籤中,若要插入超過 10 行的程式碼,請使用沙盒 (plnkrjsbincodepen…)