JavaScript for impatient programmers (ES2022 版)
本書のサポートをお願いします: 購入 または 寄付
(広告、ブロックしないでください。)

37 分割代入



37.1 分割代入の最初の味見

通常の代入では、一度に1つのデータを抽出します。たとえば、

const arr = ['a', 'b', 'c'];
const x = arr[0]; // extract
const y = arr[1]; // extract

分割代入を使用すると、データを受け取る場所のパターンを介して、複数のデータを同時に抽出できます。上記のコードの = の左側がそのような場所の1つです。次のコードでは、A行の角かっこは分割代入パターンです。

const arr = ['a', 'b', 'c'];
const [x, y] = arr; // (A)
assert.equal(x, 'a');
assert.equal(y, 'b');

このコードは、前のコードと同じことを行います。

パターンはデータよりも「小さい」ことに注意してください。必要なものだけを抽出しています。

37.2 構築と抽出

分割代入が何であるかを理解するために、JavaScriptには反対の2種類の操作があることを考えてください。

データの構築は次のようになります。

// Constructing: one property at a time
const jane1 = {};
jane1.first = 'Jane';
jane1.last = 'Doe';

// Constructing: multiple properties
const jane2 = {
  first: 'Jane',
  last: 'Doe',
};

assert.deepEqual(jane1, jane2);

データの抽出は次のようになります。

const jane = {
  first: 'Jane',
  last: 'Doe',
};

// Extracting: one property at a time
const f1 = jane.first;
const l1 = jane.last;
assert.equal(f1, 'Jane');
assert.equal(l1, 'Doe');

// Extracting: multiple properties (NEW!)
const {first: f2, last: l2} = jane; // (A)
assert.equal(f2, 'Jane');
assert.equal(l2, 'Doe');

A行の操作は新しいものです。2つの変数 f2l2 を宣言し、それらを分割代入(複数値の抽出)によって初期化します。

A行の次の部分は、分割代入パターンです。

{first: f2, last: l2}

分割代入パターンは、複数値の構築に使用されるリテラルと構文的に似ています。ただし、データが作成される場所(たとえば、代入の右側)ではなく、データが受信される場所(たとえば、代入の左側)に現れます。

37.3 どこで分割代入できるか?

分割代入パターンは、次のような「データシンクの場所」で使用できます。

変数宣言には、for-of ループの const および let 宣言が含まれることに注意してください。

const arr = ['a', 'b'];
for (const [index, element] of arr.entries()) {
    console.log(index, element);
}
// Output:
// 0, 'a'
// 1, 'b'

次の2つのセクションでは、2種類の分割代入(オブジェクト分割代入と配列分割代入)について詳しく見ていきます。

37.4 オブジェクト分割代入

オブジェクト分割代入を使用すると、オブジェクトリテラルのように見えるパターンを介して、プロパティの値を一括抽出できます。

const address = {
  street: 'Evergreen Terrace',
  number: '742',
  city: 'Springfield',
  state: 'NT',
  zip: '49007',
};

const { street: s, city: c } = address;
assert.equal(s, 'Evergreen Terrace');
assert.equal(c, 'Springfield');

パターンは、データの上に配置する透明なシートと考えることができます。パターンキーの 'street' はデータ内に一致するものを持っています。したがって、データ値 'Evergreen Terrace' はパターン変数 s に代入されます。

プリミティブ値をオブジェクト分割代入することもできます。

const {length: len} = 'abc';
assert.equal(len, 3);

また、配列をオブジェクト分割代入することもできます。

const {0:x, 2:y} = ['a', 'b', 'c'];
assert.equal(x, 'a');
assert.equal(y, 'c');

なぜそれが機能するのですか? 配列インデックスもプロパティです。

37.4.1 プロパティ値の省略記法

オブジェクトリテラルはプロパティ値の省略記法をサポートしており、オブジェクトパターンも同様です。

const { street, city } = address;
assert.equal(street, 'Evergreen Terrace');
assert.equal(city, 'Springfield');

  演習:オブジェクト分割代入

exercises/destructuring/object_destructuring_exrc.mjs

37.4.2 残余プロパティ

オブジェクトリテラルでは、スプレッドプロパティを持つことができます。オブジェクトパターンでは、残余プロパティを持つことができます(これは最後に来る必要があります)。

const obj = { a: 1, b: 2, c: 3 };
const { a: propValue, ...remaining } = obj; // (A)

assert.equal(propValue, 1);
assert.deepEqual(remaining, {b:2, c:3});

remaining(A行)などの残余プロパティ変数には、パターンで言及されていないすべてのデータプロパティを持つオブジェクトが代入されます。

remaining は、プロパティ aobj から非破壊的に削除した結果としても見ることができます。

37.4.3 構文上の落とし穴:オブジェクト分割代入による代入

代入でオブジェクト分割代入を行う場合、構文のあいまいさによって引き起こされる落とし穴に直面します。JavaScriptは、ステートメントを中かっこで開始することはできません。なぜなら、JavaScriptはそれがブロックを開始すると考えるからです。

let prop;
assert.throws(
  () => eval("{prop} = { prop: 'hello' };"),
  {
    name: 'SyntaxError',
    message: "Unexpected token '='",
  });

  なぜ eval() なのか?

eval() は、assert.throws() のコールバックが実行されるまで、解析(したがって SyntaxError)を遅延させます。使用しない場合、このコードが解析されるときにすでにエラーが発生し、assert.throws() は実行されません。

回避策は、代入全体を括弧で囲むことです。

let prop;
({prop} = { prop: 'hello' });
assert.equal(prop, 'hello');

37.5 配列分割代入

配列分割代入を使用すると、配列リテラルのように見えるパターンを介して、配列要素の値を一括抽出できます。

const [x, y] = ['a', 'b'];
assert.equal(x, 'a');
assert.equal(y, 'b');

配列パターンの中に穴を記述することで、要素をスキップできます。

const [, x, y] = ['a', 'b', 'c']; // (A)
assert.equal(x, 'b');
assert.equal(y, 'c');

A行の配列パターンの最初の要素は穴であるため、インデックス0の配列要素は無視されます。

37.5.1 配列分割代入は任意のイテラブルで動作する

配列分割代入は、配列だけでなく、イテラブルである任意の値に適用できます。

// Sets are iterable
const mySet = new Set().add('a').add('b').add('c');
const [first, second] = mySet;
assert.equal(first, 'a');
assert.equal(second, 'b');

// Strings are iterable
const [a, b] = 'xyz';
assert.equal(a, 'x');
assert.equal(b, 'y');

37.5.2 残余要素

配列リテラルでは、スプレッド要素を持つことができます。配列パターンでは、残余要素を持つことができます(これは最後に来る必要があります)。

const [x, y, ...remaining] = ['a', 'b', 'c', 'd']; // (A)

assert.equal(x, 'a');
assert.equal(y, 'b');
assert.deepEqual(remaining, ['c', 'd']);

remaining(A行)などの残余要素変数には、分割代入された値の中でまだ言及されていないすべての要素を含む配列が代入されます。

37.6 分割代入の例

37.6.1 配列分割代入:変数値を交換する

配列分割代入を使用して、一時変数を使用せずに2つの変数の値を交換できます。

let x = 'a';
let y = 'b';

[x,y] = [y,x]; // swap

assert.equal(x, 'b');
assert.equal(y, 'a');

37.6.2 配列分割代入:配列を返す操作

配列分割代入は、正規表現メソッド .exec() のように、操作が配列を返す場合に便利です。

// Skip the element at index 0 (the whole match):
const [, year, month, day] =
  /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/
  .exec('2999-12-31');

assert.equal(year, '2999');
assert.equal(month, '12');
assert.equal(day, '31');

37.6.3 オブジェクト分割代入:複数の戻り値

分割代入は、関数が複数の値を返す場合(配列としてパッケージ化されているか、オブジェクトとしてパッケージ化されているか)非常に便利です。

配列内の要素を見つける関数 findElement() を考えてみましょう。

findElement(array, (value, index) => «boolean expression»)

その2番目のパラメータは、要素の値とインデックスを受け取り、これが呼び出し元が探している要素であるかどうかを示すブール値を返す関数です。

ここで、ジレンマに直面しています。findElement() は、見つかった要素の値またはインデックスのどちらを返す必要がありますか?1つの解決策は、2つの別々の関数を作成することですが、両方の関数は非常に似ているため、コードが重複します。

次の実装は、見つかった要素のインデックスと値の両方を含むオブジェクトを返すことで重複を回避します。

function findElement(arr, predicate) {
  for (let index=0; index < arr.length; index++) {
    const value = arr[index];
    if (predicate(value)) {
      // We found something:
      return { value, index };
    }
  }
  // We didn’t find anything:
  return { value: undefined, index: -1 };
}

分割代入は、findElement() の結果を処理するのに役立ちます。

const arr = [7, 8, 6];

const {value, index} = findElement(arr, x => x % 2 === 0);
assert.equal(value, 8);
assert.equal(index, 1);

プロパティキーを処理しているため、valueindex のどちらを先に記述してもかまいません。

const {index, value} = findElement(arr, x => x % 2 === 0);

重要なのは、2つの結果の1つだけに興味がある場合でも、分割代入が役に立つことです。

const arr = [7, 8, 6];

const {value} = findElement(arr, x => x % 2 === 0);
assert.equal(value, 8);

const {index} = findElement(arr, x => x % 2 === 0);
assert.equal(index, 1);

これらの便利な機能が組み合わさることで、複数の戻り値を処理するこの方法は非常に用途が広くなります。

37.7 パターンの一部が何にも一致しない場合はどうなるか?

パターンの部分に一致するものがない場合はどうなりますか?一括演算子を使用しない場合と同じことが起こります。undefined が返されます。

37.7.1 オブジェクト分割代入と欠落したプロパティ

オブジェクトパターンのプロパティが右側に一致するものを持たない場合、undefined が返されます。

const {prop: p} = {};
assert.equal(p, undefined);

37.7.2 配列分割代入と欠落した要素

配列パターンの要素が右側に一致するものを持たない場合、undefined が返されます。

const [x] = [];
assert.equal(x, undefined);

37.8 分割代入できない値は何ですか?

37.8.1 undefined および null はオブジェクト分割代入できない

オブジェクト分割代入が失敗するのは、分割代入する値が undefined または null の場合のみです。つまり、ドット演算子を介してプロパティにアクセスする場合も失敗する場合です。

> const {prop} = undefined
TypeError: Cannot destructure property 'prop' of 'undefined'
as it is undefined.

> const {prop} = null
TypeError: Cannot destructure property 'prop' of 'null'
as it is null.

37.8.2 非イテラブルな値は配列分割代入できない

配列分割代入では、分割代入された値がイテラブルである必要があります。したがって、undefinednull は配列分割代入できません。また、非イテラブルなオブジェクトも配列分割代入できません。

> const [x] = {}
TypeError: {} is not iterable

  クイズ:基本

クイズアプリを参照してください。

37.9 (高度な内容)

残りのセクションはすべて高度な内容です。

37.10 デフォルト値

通常、パターンに一致するものがない場合、対応する変数は undefined に設定されます。

const {prop: p} = {};
assert.equal(p, undefined);

別の値を使用する場合は、デフォルト値を指定する必要があります(= を介して)。

const {prop: p = 123} = {}; // (A)
assert.equal(p, 123);

A行では、p のデフォルト値を 123 に指定します。分割代入するデータに prop という名前のプロパティがないため、そのデフォルト値が使用されます。

37.10.1 配列分割代入でのデフォルト値

ここでは、分割代入される配列に対応する要素が存在しないため、変数 xy に割り当てられる2つのデフォルト値があります。

const [x=1, y=2] = [];

assert.equal(x, 1);
assert.equal(y, 2);

配列パターンの最初の要素のデフォルト値は 1 です。2番目の要素のデフォルト値は 2 です。

37.10.2 オブジェクト分割代入でのデフォルト値

オブジェクト分割代入にデフォルト値を指定することもできます。

const {first: f='', last: l=''} = {};
assert.equal(f, '');
assert.equal(l, '');

プロパティキー first もプロパティキー last も、分割代入されるオブジェクトには存在しません。したがって、デフォルト値が使用されます。

プロパティ値の省略記法を使用すると、このコードはよりシンプルになります。

const {first='', last=''} = {};
assert.equal(first, '');
assert.equal(last, '');

37.11 パラメータ定義は分割代入に似ている

この章で学習したことを考慮すると、パラメータ定義は配列パターン(残余要素、デフォルト値など)と多くの共通点があります。実際、次の2つの関数宣言は同等です。

function f1(«pattern1», «pattern2») {
  // ···
}

function f2(...args) {
  const [«pattern1», «pattern2»] = args;
  // ···
}

37.12 ネストされた分割代入

これまで、変数はデストラクチャリングパターンの中で代入ターゲット(データの受け皿)としてのみ使用してきました。しかし、パターンを代入ターゲットとして使用することもでき、これによりパターンを任意の深さにネストできます。

const arr = [
  { first: 'Jane', last: 'Bond' },
  { first: 'Lars', last: 'Croft' },
];
const [, {first}] = arr; // (A)
assert.equal(first, 'Lars');

A行の配列パターン内には、インデックス1にネストされたオブジェクトパターンがあります。

ネストされたパターンは理解が難しくなる可能性があるため、控えめに使用するのが最適です。

  クイズ:上級

クイズアプリを参照してください。