11. 參數處理
目錄
請支持這本書:購買(PDF、EPUB、MOBI)捐款
(廣告,請不要阻擋。)

11. 參數處理



11.1 概觀

ECMAScript 6 中的參數處理已大幅升級。它現在支援參數預設值、剩餘參數(變數引數)和解構。

此外,展開運算子有助於函式/方法/建構函式呼叫和陣列文字。

11.1.1 預設參數值

預設參數值是透過等號(=)為參數指定的。如果呼叫者未提供參數值,則使用預設值。在以下範例中,y 的預設參數值為 0

function func(x, y=0) {
    return [x, y];
}
func(1, 2); // [1, 2]
func(1); // [1, 0]
func(); // [undefined, 0]

11.1.2 剩餘參數

如果您在參數名稱之前加上 rest 運算子 (...),該參數會透過陣列接收所有剩餘參數

function format(pattern, ...params) {
    return {pattern, params};
}
format(1, 2, 3);
    // { pattern: 1, params: [ 2, 3 ] }
format();
    // { pattern: undefined, params: [] }

11.1.3 透過解構進行命名參數

如果您在參數清單中使用物件樣式進行解構,您可以模擬命名參數

function selectEntries({ start=0, end=-1, step=1 } = {}) { // (A)
    // The object pattern is an abbreviation of:
    // { start: start=0, end: end=-1, step: step=1 }

    // Use the variables `start`, `end` and `step` here
    ···
}

selectEntries({ start: 10, end: 30, step: 2 });
selectEntries({ step: 3 });
selectEntries({});
selectEntries();

A 行中的 = {} 讓您可以不帶參數呼叫 selectEntries()

11.1.4 散佈運算子 (...)

在函式和建構式呼叫中,散佈運算子會將可迭代值轉換為參數

> Math.max(-1, 5, 11, 3)
11
> Math.max(...[-1, 5, 11, 3])
11
> Math.max(-1, ...[-5, 11], 3)
11

在陣列文字中,散佈運算子會將可迭代值轉換為陣列元素

> [1, ...[2,3], 4]
[1, 2, 3, 4]

11.2 將參數處理視為解構

ES6 處理參數的方式等同於透過正式參數解構實際參數。也就是說,下列函式呼叫

function func(«FORMAL_PARAMETERS») {
    «CODE»
}
func(«ACTUAL_PARAMETERS»);

大致等同於

{
    let [«FORMAL_PARAMETERS»] = [«ACTUAL_PARAMETERS»];
    {
        «CODE»
    }
}

範例 – 下列函式呼叫

function logSum(x=0, y=0) {
    console.log(x + y);
}
logSum(7, 8);

會變成

{
    let [x=0, y=0] = [7, 8];
    {
        console.log(x + y);
    }
}

接著我們來看具體功能。

11.3 參數預設值

ECMAScript 6 讓您可以指定參數的預設值

function f(x, y=0) {
  return [x, y];
}

略過第二個參數會觸發預設值

> f(1)
[1, 0]
> f()
[undefined, 0]

注意 – undefined 也會觸發預設值

> f(undefined, undefined)
[undefined, 0]

預設值會依需求計算,只在實際需要時才計算

> const log = console.log.bind(console);
> function g(x=log('x'), y=log('y')) {return 'DONE'}
> g()
x
y
'DONE'
> g(1)
y
'DONE'
> g(1, 2)
'DONE'

11.3.1 為什麼 undefined 會觸發預設值?

undefined 應該被解譯為遺失參數或遺失物件或陣列的一部分,這一點並不顯而易見。這樣做的理由是,它讓您可以委派預設值的定義。我們來看兩個範例。

在第一個範例中(來源:Rick Waldron 的 TC39 會議記錄,2012-07-24),我們不必在 setOptions() 中定義預設值,我們可以將這項任務委派給 setLevel()

function setLevel(newLevel = 0) {
    light.intensity = newLevel;
}
function setOptions(options) {
    // Missing prop returns undefined => use default
    setLevel(options.dimmerLevel);
    setMotorSpeed(options.speed);
    ···
}
setOptions({speed:5});

在第二個範例中,square() 不必為 x 定義預設值,它可以將這項任務委派給 multiply()

function multiply(x=1, y=1) {
    return x * y;
}
function square(x) {
    return multiply(x, x);
}

預設值進一步強化了 undefined 的角色,表示某個東西不存在,而 null 表示空值。

11.3.2 在預設值中參照其他參數

在參數預設值中,你可以參照任何變數,包括其他參數

function foo(x=3, y=x) {}
foo();     // x=3; y=3
foo(7);    // x=7; y=7
foo(7, 2); // x=7; y=2

但是,順序很重要。參數從左到右宣告。在預設值「內部」,如果你存取尚未宣告的參數,你會得到一個 ReferenceError

function bar(x=y, y=4) {}
bar(3); // OK
bar(); // ReferenceError: y is not defined

11.3.3 在預設值中參照「內部」變數

預設值存在於它們自己的範圍中,這個範圍介於函式周圍的「外部」範圍與函式主體的「內部」範圍之間。因此,你無法從預設值存取「內部」變數

const x = 'outer';
function foo(a = x) {
    const x = 'inner';
    console.log(a); // outer
}

如果在先前的範例中沒有外部 x,預設值 x 會產生一個 ReferenceError(如果觸發的話)。

如果預設值是閉包,這個限制可能會令人最為驚訝

const QUX = 2;
function bar(callback = () => QUX) { // returns 2
    const QUX = 3;
    callback();
}
bar(); // ReferenceError

11.4 剩餘參數

將剩餘運算子 (...) 放在最後一個形式參數前面,表示它會在陣列中接收所有剩餘實際參數。

function f(x, ...y) {
    ···
}
f('a', 'b', 'c'); // x = 'a'; y = ['b', 'c']

如果沒有剩餘參數,剩餘參數會設定為空陣列

f(); // x = undefined; y = []

11.4.1 不再有 arguments

剩餘參數可以完全取代 JavaScript 臭名昭著的特殊變數 arguments。它們的優點是永遠都是陣列

// ECMAScript 5: arguments
function logAllArguments() {
    for (var i=0; i < arguments.length; i++) {
        console.log(arguments[i]);
    }
}

// ECMAScript 6: rest parameter
function logAllArguments(...args) {
    for (const arg of args) {
        console.log(arg);
    }
}
11.4.1.1 結合解構和存取解構值

arguments 的一個有趣功能是,你可以同時擁有正常參數和所有參數的陣列

function foo(x=0, y=0) {
    console.log('Arity: '+arguments.length);
    ···
}

在這種情況下,如果你將剩餘參數與陣列解構結合,就可以避免使用 arguments。產生的程式碼較長,但更明確

function foo(...args) {
    let [x=0, y=0] = args;
    console.log('Arity: '+args.length);
    ···
}

相同的技術適用於命名參數(選項物件)

function bar(options = {}) {
    let { namedParam1, namedParam2 } = options;
    ···
    if ('extra' in options) {
        ···
    }
}
11.4.1.2 arguments 可迭代

arguments 在 ECMAScript 6 中是可迭代5,這表示您可以使用 for-of 和展開運算子

> (function () { return typeof arguments[Symbol.iterator] }())
'function'
> (function () { return Array.isArray([...arguments]) }())
true

11.5 模擬命名參數

在程式語言中呼叫函式(或方法)時,您必須將實際參數(由呼叫者指定)對應到形式參數(函式定義)。有兩種常見的方法可以這樣做

命名參數有兩個主要好處:它們提供函式呼叫中參數的描述,而且它們適用於選用參數。我將先說明這些好處,然後向您展示如何透過物件文字在 JavaScript 中模擬命名參數。

11.5.1 命名參數作為描述

函式一旦有多個參數,您可能會對每個參數的用途感到困惑。例如,假設您有一個函式 selectEntries(),它會從資料庫傳回資料庫項目。給定函式呼叫

selectEntries(3, 20, 2);

這三個數字是什麼意思?Python 支援命名參數,它們可以輕鬆找出發生了什麼事

# Python syntax
selectEntries(start=3, end=20, step=2)

11.5.2 選用命名參數

選用位置參數只有在它們在最後被省略時才有效。在其他任何地方,您都必須插入 null 等佔位符,以便其餘參數具有正確的位置。

使用可選命名參數,這不是問題。您可以輕鬆省略任何一個。以下是幾個範例

# Python syntax
selectEntries(step=2)
selectEntries(end=20, start=3)
selectEntries()

11.5.3 在 JavaScript 中模擬命名參數

與 Python 和許多其他語言不同,JavaScript 不支援原生命名參數。但有一個相當優雅的模擬:每個實際參數都是物件文字中的屬性,其結果傳遞為單一形式參數給受呼叫者。當您使用此技術時,selectEntries() 的呼叫如下所示。

selectEntries({ start: 3, end: 20, step: 2 });

函數接收具有屬性 startendstep 的物件。您可以省略任何一個

selectEntries({ step: 2 });
selectEntries({ end: 20, start: 3 });
selectEntries();

在 ECMAScript 5 中,您會實作 selectEntries() 如下所示

function selectEntries(options) {
    options = options || {};
    var start = options.start || 0;
    var end = options.end || -1;
    var step = options.step || 1;
    ···
}

在 ECMAScript 6 中,您可以使用解構,如下所示

function selectEntries({ start=0, end=-1, step=1 }) {
    ···
}

如果您使用零個參數呼叫 selectEntries(),解構會失敗,因為您無法將物件模式與 undefined 相符。這可透過預設值來修正。在以下程式碼中,如果缺少第一個參數,物件模式會與 {} 相符。

function selectEntries({ start=0, end=-1, step=1 } = {}) {
    ···
}

您也可以將位置參數與命名參數結合。後者通常放在最後

someFunc(posArg1, { namedArg1: 7, namedArg2: true });

原則上,JavaScript 引擎可以最佳化這個模式,以便不會建立中間物件,因為呼叫網站中的物件文字和函數定義中的物件模式都是靜態的。

11.6 參數處理中解構的範例

11.6.1 forEach() 和解構

您可能大多會在 ECMAScript 6 中使用 for-of 迴圈,但陣列方法 forEach() 也受益於解構。或者更確切地說,其回呼會受益。

第一個範例:解構陣列中的陣列。

const items = [ ['foo', 3], ['bar', 9] ];
items.forEach(([word, count]) => {
    console.log(word+' '+count);
});

第二個範例:解構陣列中的物件。

const items = [
    { word:'foo', count:3 },
    { word:'bar', count:9 },
];
items.forEach(({word, count}) => {
    console.log(word+' '+count);
});

11.6.2 轉換 Map

ECMAScript 6 Map 沒有 map() 方法(像陣列一樣)。因此,必須

如下所示。

const map0 = new Map([
    [1, 'a'],
    [2, 'b'],
    [3, 'c'],
]);

const map1 = new Map( // step 3
    [...map0] // step 1
    .map(([k, v]) => [k*2, '_'+v]) // step 2
);
// Resulting Map: {2 -> '_a', 4 -> '_b', 6 -> '_c'}

11.6.3 處理透過 Promise 傳回的陣列

工具方法 Promise.all() 的運作方式如下

解構有助於處理 Promise.all() 完成結果的陣列

const urls = [
    'http://example.com/foo.html',
    'http://example.com/bar.html',
    'http://example.com/baz.html',
];

Promise.all(urls.map(downloadUrl))
.then(([fooStr, barStr, bazStr]) => {
    ···
});

// This function returns a Promise that is fulfilled
// with a string (the text)
function downloadUrl(url) {
    return fetch(url).then(request => request.text());
}

fetch()XMLHttpRequest 的 Promise 版本。它是 Fetch 標準 的一部分。

11.7 編碼風格提示

本節提到一些描述性參數定義的技巧。它們很聰明,但也有缺點:它們會增加視覺上的混亂,並可能讓您的程式碼更難理解。

11.7.1 選用參數

有些參數沒有預設值,但可以省略。在這種情況下,我偶爾會使用預設值 undefined,以清楚地表示該參數是選用的。這是多餘的,但具有描述性。

function foo(requiredParam, optionalParam = undefined) {
    ···
}

11.7.2 必要參數

在 ECMAScript 5 中,您有幾個選項可以確保提供必要的參數,這些選項都相當笨拙

function foo(mustBeProvided) {
    if (arguments.length < 1) {
        throw new Error();
    }
    if (! (0 in arguments)) {
        throw new Error();
    }
    if (mustBeProvided === undefined) {
        throw new Error();
    }
    ···
}

在 ECMAScript 6 中,您可以(濫用)預設參數值來達成更簡潔的程式碼(感謝:艾倫·維爾夫斯-布洛克的點子)

/**
 * Called if a parameter is missing and
 * the default value is evaluated.
 */
function mandatory() {
    throw new Error('Missing parameter');
}
function foo(mustBeProvided = mandatory()) {
    return mustBeProvided;
}

互動

> foo()
Error: Missing parameter
> foo(123)
123

11.7.3 強制執行最大元數

本節介紹三種強制執行最大元數的方法。執行的範例是一個最大元數為 2 的函數 f – 如果呼叫者提供超過 2 個參數,應擲回錯誤。

第一種方法是收集正式剩餘參數 args 中的所有實際參數,並檢查其長度。

function f(...args) {
    if (args.length > 2) {
        throw new Error();
    }
    // Extract the real parameters
    let [x, y] = args;
}

第二種方法依賴於正式剩餘參數 empty 中出現不需要的實際參數。

function f(x, y, ...empty) {
    if (empty.length > 0) {
        throw new Error();
    }
}

第三種方法使用一個哨兵值,如果存在第三個參數,則該值會消失。一個需要注意的地方是,如果存在值為 undefined 的第三個參數,預設值 OK 也會觸發。

const OK = Symbol();
function f(x, y, arity=OK) {
    if (arity !== OK) {
        throw new Error();
    }
}

遺憾的是,這些方法每一個都會引入顯著的視覺和概念混亂。我傾向於建議檢查 arguments.length,但我還希望 arguments 消失。

function f(x, y) {
    if (arguments.length > 2) {
        throw new Error();
    }
}

11.8 展開運算子 (...)

展開運算子 (...) 看起來與 rest 運算子完全相同,但它恰好相反

11.8.1 展開到函式和方法呼叫中

Math.max() 是展示展開運算子如何在方法呼叫中運作的一個好範例。Math.max(x1, x2, ···) 傳回值最大的參數。它接受任意數量的參數,但無法套用於陣列。展開運算子修正了這個問題

> Math.max(-1, 5, 11, 3)
11
> Math.max(...[-1, 5, 11, 3])
11

與 rest 運算子相反,你可以在部分序列的任何地方使用展開運算子

> Math.max(-1, ...[5, 11], 3)
11

另一個範例是 JavaScript 沒有辦法將一個陣列的元素破壞性地附加到另一個陣列。然而,陣列確實有方法 push(x1, x2, ···),它會將所有參數附加到其接收器。以下程式碼顯示你可以如何使用 push()arr2 的元素附加到 arr1

const arr1 = ['a', 'b'];
const arr2 = ['c', 'd'];

arr1.push(...arr2);
// arr1 is now ['a', 'b', 'c', 'd']

11.8.2 展開到建構函式中

除了函式和方法呼叫之外,展開運算子也適用於建構函式呼叫

new Date(...[1912, 11, 24]) // Christmas Eve 1912

這是 ECMAScript 5 中難以達成的

11.8.3 展開到陣列中

展開運算子也可以用在陣列文字中

> [1, ...[2,3], 4]
[1, 2, 3, 4]

這為你提供了一個合併陣列的便捷方法

const x = ['a', 'b'];
const y = ['c'];
const z = ['d', 'e'];

const arr = [...x, ...y, ...z]; // ['a', 'b', 'c', 'd', 'e']

展開運算子的一個優點是它的運算元可以是任何可迭代值(與陣列方法 concat() 相反,後者不支援迭代)。

11.8.3.1 將可迭代或類陣列物件轉換成陣列

展開運算子讓你能夠將任何可迭代值轉換成陣列

const arr = [...someIterableObject];

我們將一個 Set 轉換成陣列

const set = new Set([11, -1, 6]);
const arr = [...set]; // [11, -1, 6]

您自己的可迭代物件可以用相同的方式轉換為陣列

const obj = {
    * [Symbol.iterator]() {
        yield 'a';
        yield 'b';
        yield 'c';
    }
};
const arr = [...obj]; // ['a', 'b', 'c']

請注意,就像 for-of 迴圈一樣,展開運算子僅對可迭代值有效。所有內建資料結構都是可迭代的:陣列、映射和集合。所有類陣列 DOM 資料結構也是可迭代的。

如果您遇到不可迭代但類陣列(索引元素加上屬性 length)的項目,您可以使用 Array.from()6 將其轉換為陣列

const arrayLike = {
    '0': 'a',
    '1': 'b',
    '2': 'c',
    length: 3
};

// ECMAScript 5:
var arr1 = [].slice.call(arrayLike); // ['a', 'b', 'c']

// ECMAScript 6:
const arr2 = Array.from(arrayLike); // ['a', 'b', 'c']

// TypeError: Cannot spread non-iterable value
const arr3 = [...arrayLike];
下一步:III 模組化