Deep JavaScript
本書をサポートしてください:購入する または 寄付する
(広告、ブロックしないでください。)

3 デストラクチャリングアルゴリズム



この章では、デストラクチャリングを別の角度から見ていきます。再帰的なパターンマッチングアルゴリズムとしてです。

このアルゴリズムにより、デフォルト値をより深く理解することができます。これは、最後に、以下の2つの関数の違いを解明しようとする際に役立ちます。

function move({x=0, y=0} = {})         { ··· }
function move({x, y} = { x: 0, y: 0 }) { ··· }

3.1 パターンマッチングアルゴリズムの準備

デストラクチャリング代入は、このようになります。

«pattern» = «value»

`pattern` を使用して `value` からデータを取り出したいと考えています。

この種の代入を実行するためのアルゴリズムを見ていきます。このアルゴリズムは、関数型プログラミングでは *パターンマッチング* (略して *マッチング*)として知られています。これは、`pattern` と `value` をマッチングさせ、その際に変数に代入する `←` (「に対してマッチング」)演算子を指定します。

«pattern» ← «value»

デストラクチャリング代入のみを検討しますが、デストラクチャリング変数宣言とデストラクチャリングパラメータ定義も同様に機能します。高度な機能についても説明しません。計算されたプロパティキー、プロパティ値の省略記法、オブジェクトのプロパティと配列要素を代入ターゲットとすることは、この章の範囲外です。

マッチ演算子の仕様は、両方のオペランドの構造を下りていく宣言的ルールで構成されています。宣言的な表記には慣れる必要があるかもしれませんが、仕様をより簡潔にします。

3.1.1 マッチングアルゴリズムの指定のための宣言的ルール

この章で使用される宣言的ルールは、入力に対して動作し、副作用を介してアルゴリズムの結果を生成します。これはそのようなルールの1つです(後で再び見ます)。

このルールには次の部分があります。

ルール (2c) では、先頭は、少なくとも1つのプロパティ(キーは `key`)と0個以上の残りのプロパティを持つオブジェクトパターンがある場合にこのルールを適用できることを意味します。このルールの効果は、プロパティ値パターンが `obj.key` とマッチングされ、残りのプロパティが `obj` とマッチングされることを継続することです。

この章からもう1つのルールを考えてみましょう。

ルール (2e) では、先頭は、空のオブジェクトパターン `{}` が値 `obj` とマッチングされた場合にこのルールが実行されることを意味します。本体は、この場合、完了したことを意味します。

ルール (2c) とルール (2e) を組み合わせると、左辺の矢印のパターンのプロパティを反復処理する宣言的ループが形成されます。

3.1.2 宣言的ルールに基づいた式の評価

完全なアルゴリズムは、宣言的ルールのシーケンスによって指定されます。次のマッチング式を評価したいと仮定しましょう。

{first: f, last: l} ← obj

ルールのシーケンスを適用するには、上から下へ順番に見ていき、最初に適用可能なルールを実行します。そのルールの本体にマッチングする式がある場合、ルールは再び適用されます。そして、以下同様に続きます。

場合によっては、先頭にルールが適用可能かどうかを決定する条件も含まれます。たとえば

3.2 パターンマッチングアルゴリズム

3.2.1 パターン

パターンは次のいずれかです。

次の3つのセクションでは、マッチング式におけるこれらの3つのケースを処理するためのルールを指定します。

3.2.2 変数のルール

3.2.3 オブジェクトパターンのルール

ルール 2a と 2b は無効な値を扱います。ルール 2c~2e はパターンのプロパティをループします。ルール 2d では、`obj` に一致するプロパティがない場合にマッチングする代替手段としてデフォルト値を提供できることがわかります。

3.2.4 配列パターンのルール

**配列パターンと反復可能オブジェクト。** 配列デストラクチャリングのアルゴリズムは、配列パターンと反復可能オブジェクトから始まります。

ヘルパー関数

function isIterable(value) {
  return (value !== null
    && typeof value === 'object'
    && typeof value[Symbol.iterator] === 'function');
}

**配列要素とイテレータ。** アルゴリズムは次のように続きます。

これらがルールです。

ヘルパー関数

function getNext(iterator) {
  const {done,value} = iterator.next();
  return (done ? undefined : value);
}

イテレータが終了することは、オブジェクトのプロパティが不足していることと似ています。

3.3 空のオブジェクトパターンと配列パターン

アルゴリズムのルールの興味深い結果:空のオブジェクトパターンと空の配列パターンでデストラクチャリングできます。

空のオブジェクトパターン `{}` が与えられた場合:デストラクチャリングされる値が `undefined` でも `null` でもない場合、何も起こりません。そうでない場合、`TypeError` がスローされます。

const {} = 123; // OK, neither undefined nor null
assert.throws(
  () => {
    const {} = null;
  },
  /^TypeError: Cannot destructure 'null' as it is null.$/)

空の配列パターン `[]` が与えられた場合:デストラクチャリングされる値が反復可能オブジェクトの場合、何も起こりません。そうでない場合、`TypeError` がスローされます。

const [] = 'abc'; // OK, iterable
assert.throws(
  () => {
    const [] = 123; // not iterable
  },
  /^TypeError: 123 is not iterable$/)

つまり、空のデストラクチャリングパターンは値に特定の特性を持たせることを強制しますが、それ以外の効果はありません。

3.4 アルゴリズムの適用

JavaScriptでは、名前付きパラメータはオブジェクトを介してシミュレートされます。呼び出し側はオブジェクトリテラルを使用し、呼び出し側はデストラクチャリングを使用します。このシミュレーションについては、「JavaScript for impatient programmers」で詳しく説明されています。次のコードは例を示しています。関数 `move1()` には、`x` と `y` の2つの名前付きパラメータがあります。

function move1({x=0, y=0} = {}) { // (A)
  return [x, y];
}
assert.deepEqual(
  move1({x: 3, y: 8}), [3, 8]);
assert.deepEqual(
  move1({x: 3}), [3, 0]);
assert.deepEqual(
  move1({}), [0, 0]);
assert.deepEqual(
  move1(), [0, 0]);

行Aには3つのデフォルト値があります。

しかし、なぜパラメータを前のコードスニペットのように定義するのでしょうか?なぜ次のようにしないのでしょうか?

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

`move1()` が正しい理由を確認するために、2つの例で両方の関数を使用します。その前に、パラメータの受け渡しをマッチングでどのように説明できるかを見てみましょう。

3.4.1 背景:マッチングによるパラメータの受け渡し

関数呼び出しでは、 *仮パラメータ* (関数定義内)は *実パラメータ* (関数呼び出し内)とマッチングされます。例として、次の関数定義と関数呼び出しを考えてみましょう。

function func(a=0, b=0) { ··· }
func(1, 2);

パラメータ `a` と `b` は、次のデストラクチャリングと同様に設定されています。

[a=0, b=0] ← [1, 2]

3.4.2 `move2()` の使用

`move2()` のデストラクチャリングの動作を調べましょう。

**例1。** 関数呼び出し `move2()` は、このデストラクチャリングにつながります。

[{x, y} = { x: 0, y: 0 }] ← []

左辺の単一の配列要素は、右辺にマッチングがないため、`{x,y}` はデフォルト値とマッチングされ、右辺のデータとはマッチングされません(ルール 3b、3d)。

{x, y} ← { x: 0, y: 0 }

左辺には *プロパティ値の省略記法* が含まれています。これは次の略記です。

{x: x, y: y} ← { x: 0, y: 0 }

このデストラクチャリングは、次の2つの代入につながります(ルール 2c、1)。

x = 0;
y = 0;

これが私たちが望んでいたことです。しかし、次の例では、それほど幸運ではありません。

**例2。** 関数呼び出し `move2({z: 3})` を調べましょう。これは次のデストラクチャリングにつながります。

[{x, y} = { x: 0, y: 0 }] ← [{z: 3}]

インデックス 0 に配列要素があります。したがって、デフォルト値は無視され、次のステップになります(ルール 3d)。

{x, y} ← { z: 3 }

これにより、`x` と `y` の両方が `undefined` に設定されます。これは私たちが望んでいることではありません。問題は、`{x,y}` がデフォルト値とはもうマッチングされず、`{z:3}` とマッチングされることです。

3.4.3 `move1()` の使用

`move1()` を試してみましょう。

**例1:** `move1()`

[{x=0, y=0} = {}] ← []

インデックス 0 に配列要素がないため、デフォルト値が使用されます(ルール 3d)。

{x=0, y=0} ← {}

左辺にはプロパティ値の省略記法が含まれているため、このデストラクチャリングは次のものと同等です。

{x: x=0, y: y=0} ← {}

プロパティ `x` とプロパティ `y` のいずれにも、右辺にマッチングがありません。したがって、デフォルト値が使用され、次に次のデストラクチャリングが実行されます(ルール 2d)。

x ← 0
y ← 0

これにより、次の代入が行われます(ルール 1)。

x = 0
y = 0

ここでは、私たちが望むものを得ます。次の例でもうまくいくかどうか見てみましょう。

**例2:** `move1({z: 3})`

[{x=0, y=0} = {}] ← [{z: 3}]

配列パターンの最初の要素は、右辺にマッチングがあり、そのマッチングを使用してデストラクチャリングを続けます(ルール 3d)。

{x=0, y=0} ← {z: 3}

例1と同様に、右辺にプロパティ `x` と `y` がないため、デフォルト値が使用されます。

x = 0
y = 0

期待通りに動作します!今回は、`{z:3}` とマッチングされる `x` と `y` を含むパターンは問題になりません。それらには独自のローカルデフォルト値があるためです。

3.4.4 結論:デフォルト値はパターンパーツの特徴

これらの例は、デフォルト値がパターンパーツ(オブジェクトプロパティまたは配列要素)の特徴であることを示しています。パーツにマッチングがない場合、または `undefined` とマッチングされた場合、デフォルト値が使用されます。つまり、パターンは代わりにデフォルト値とマッチングされます。