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 レストパラメータ

パラメータ名の前にレスト演算子 (...) を付けると、そのパラメータは配列を介して残りのすべてのパラメータを受け取ります。

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];
}

2 番目のパラメータを省略すると、デフォルト値がトリガーされます。

> 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` がパラメータの欠落またはオブジェクトや配列の一部の欠落として解釈されるべき理由は、すぐには明らかではありません。そうする根拠は、デフォルト値の定義を委任できるようになるためです。2 つの例を見てみましょう。

最初の例(出典: Rick Waldron の 2012-07-24 の TC39 会議のメモ)では、`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});

2 番目の例では、`square()` は `x` のデフォルト値を定義する必要はありません。そのタスクを `multiply()` に委任できます。

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

デフォルト値は、`null` が空虚さを示すのに対し、`undefined` が何かが存在しないことを示すという役割をさらに強固にします。

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` の興味深い機能の 1 つは、通常のパラメータとすべてのパラメータの配列を同時に持つことができることです。

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 名前付きパラメータのシミュレート

プログラミング言語で関数(またはメソッド)を呼び出す場合、実パラメータ(呼び出し元によって指定される)を仮パラメータ(関数定義の)にマップする必要があります。これを行うには、2 つの一般的な方法があります。

名前付きパラメータには、2 つの主な利点があります。関数呼び出しで引数の説明を提供することと、オプションパラメータでうまく機能することです。最初に利点について説明し、次にオブジェクトリテラルを使用して JavaScript で名前付きパラメータをシミュレートする方法を示します。

11.5.1 説明としての名前付きパラメータ

関数が複数のパラメータを持つようになると、各パラメータが何に使用されるかについて混乱する可能性があります。たとえば、データベースからエントリを返す関数 `selectEntries()` があるとします。関数呼び出しを考えると

selectEntries(3, 20, 2);

これら 3 つの数字は何を意味するのでしょうか? 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 での名前付きパラメータのシミュレート

JavaScript は、Python や他の多くの言語とは異なり、名前付きパラメータをネイティブにサポートしていません。しかし、 reasonably elegant なシミュレーションがあります。各実パラメータは、結果が呼び出し先に単一の仮パラメータとして渡されるオブジェクトリテラルのプロパティです。この手法を使用すると、`selectEntries()` の呼び出しは次のようになります。

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

関数は、`start`、`end`、および `step` プロパティを持つオブジェクトを受け取ります。それらのいずれかを省略できます。

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`ループを使用しますが、Arrayメソッド`forEach()`も分割代入の恩恵を受けます。というか、そのコールバックが恩恵を受けます。

最初の例:配列内の配列を分割代入する。

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

2番目の例:配列内のオブジェクトを分割代入する。

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では、デフォルトパラメータ値を(悪用)して、より簡潔なコードを実現できます(クレジット:Allen Wirfs-Brockによるアイデア)。

/**
 * 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 最大アリティの強制

このセクションでは、最大アリティを強制するための3つのアプローチを紹介します。実行例は、最大アリティが2の関数`f`です。呼び出し側が3つ以上のパラメータを提供した場合、エラーがスローされます。

最初のアプローチは、すべての実パラメータを仮引数レストパラメータ`args`に収集し、その長さをチェックすることです。

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

2番目のアプローチは、不要な実パラメータが仮引数レストパラメータ`empty`に現れることに依存しています。

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

3番目のアプローチは、3番目のパラメータがある場合に消えるセンチネル値を使用します。1つの注意点は、値が`undefined`の3番目のパラメータがある場合にも、デフォルト値`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 スプレッド演算子(`...`)

スプレッド演算子(`...`)はレスト演算子とまったく同じように見えますが、その反対です。

11.8.1 関数とメソッドの呼び出しへの展開

`Math.max()`は、スプレッド演算子がメソッド呼び出しでどのように機能するかを示す良い例です。`Math.max(x1, x2, ···)`は、値が最大の引数を返します。任意の数の引数を受け入れますが、配列には適用できません。スプレッド演算子はこの問題を解決します。

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

レスト演算子とは対照的に、スプレッド演算子はパーツのシーケンスのどこにでも使用できます。

> 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']

スプレッド演算子の利点の1つは、そのオペランドが任意の反復可能値になることです(反復をサポートしていない配列メソッド`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`ループと同様に、スプレッド演算子は反復可能値に対してのみ機能することに注意してください。すべての組み込みデータ構造は反復可能です:配列、Map、およびSet。すべての配列のような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 モジュール性