📝

Destructuring assignmentのご利用は計画的に

2021/07/04に公開

@armorik83 です。ES2015 に勢いがあるので本日 2 本目の記事です(前の記事はESLint 1.0.0 新ルールまとめ)。

ES2015 の活用

今、業務では Babel の許可が出ているので、フルに ES2015(ES6 とも呼ばれていました)を活用しています。Class 構文やImportは TypeScript 経験も手伝ってもはや当たり前になっていますが、最近使い始めた Destructuring assignment は迂闊に使うと(特にチーム内での)混乱に繋がるぞ…と危険視し始めたので、挙動を確認しておきます。

Destructuring assignment

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment

ブラウザのサポートはまだまだで、現状 Babel の変換結果に頼っている状態です。なお個人的な感想ですが、Babel 開発者は PR などで送られた仕様外の挙動は容赦なく reject するため、準拠度合いには一定の信頼を持っています。

以下の挙動確認はすべて Babel 5.8.x によるものです。Babel Try it out

構文例

es6.js
const [a, b] = [1, 2];
const [c, d, ...rest] = [1, 2, 3, 4, 5];
const {e, f} = {e: 1, f: 2};
es6.js
function f({a, b}) {
  return a + b;
}

f({a: 1, b: 2}); // 3
es6.js
class Person {
  constructor({first, last}) {
    this.first = first;
    this.last = last;
  }
}

const p = new Person({first: 'John', last: 'Appleseed'});

様々なケースを試す

今回は挙動を検証したいだけの記事なので、この構文の何が便利かという疑問については別の機会とします。

1

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

f({ a: 1, b: 2 }); // 3

1-1. a に初期値

function f({ a = 3, b }) {
  return a + b;
}

f({ a: 1, b: 2 }); // 3
f({ b: 2 }); // 5
f({}); // null
f(); // Cannot read property 'a' of undefined

1-2. a, b に初期値

function f({ a = 3, b = 5 }) {
  return a + b;
}

f({ a: 1, b: 2 }); // 3
f({ a: 1 }); // 6
f({ b: 2 }); // 5
f({}); // 8
f(); // Cannot read property 'a' of undefined

1-3. {a, b}に初期値、argument[0]自体に初期値(空オブジェクト)

function f({ a = 3, b = 5 } = {}) {
  return a + b;
}

f({ a: 1, b: 2 }); // 3
f({ a: 1 }); // 6
f({ b: 2 }); // 5
f({}); // 8
f(); // 8

1-4. {a, b}に初期値、argument[0]自体に初期値(プロパティ値指定)

function f({ a = 3, b = 5 } = { a: 7, b: 11 }) {
  return a + b;
}

f({ a: 1, b: 2 }); // 3
f({ a: 1 }); // 6
f({ b: 2 }); // 5
f({}); // 8
f(); // 18

この辺りから、Destructuring assignment を学習していない者には厳しい構文になってきます。

2

function f({ a, b, c, d }) {
  return a + b + c + d;
}

f({ a: 1, b: 2, c: 3, d: 5 }); // 11

2-1. 2 つに分割

function f({ a, b }, { c, d }) {
  return a + b + c + d;
}

f({ a: 1, b: 2 }, { c: 3, d: 5 }); // 11
f({ c: 3, d: 5 }, { a: 1, b: 2 }); // null

2-2. 初期値を指定

function f({ a = 7, b = 11 }, { c = 13, d = 17 }) {
  return a + b + c + d;
}

f({ a: 1, b: 2 }, { c: 3, d: 5 }); //  1 +  2 +  3 +  5 = 11
f({ a: 1 }, { c: 3 }); //  1 + 11 +  3 + 17 = 32
f({}, {}); //  7 + 11 + 13 + 17 = 48
f({}); // Cannot read property 'c' of undefined
f(); // Cannot read property 'a' of undefined

2-3. argument[0]自体に初期値を指定

function f(
  { a = 7, b = 11 } = { a: 19, b: 23, c: 29, d: 31 },
  { c = 13, d = 17 }
) {
  return a + b + c + d;
}

f({ a: 1, b: 2 }, { c: 3, d: 5 }); //  1 +  2 +  3 +  5 = 11
f({ a: 1 }, { c: 3 }); //  1 + 11 +  3 + 17 = 32
f({}, {}); //  7 + 11 + 13 + 17 = 48
f(void 0, {}); // 19 + 23 + 13 + 17 = 72
f({}); // Cannot read property 'c' of undefined
f(); // Cannot read property 'c' of undefined

2-4. argument[0], argument[1]自体に初期値を指定

function f(
  { a = 7, b = 11 } = { a: 19, b: 23, c: 29, d: 31 },
  { c = 13, d = 17 } = { a: 37, b: 41, c: 43, d: 47 }
) {
  return a + b + c + d;
}

f({ a: 1, b: 2 }, { c: 3, d: 5 }); //  1 +  2 +  3 +  5 =  11
f({ a: 1 }, { c: 3 }); //  1 + 11 +  3 + 17 =  32
f({}, {}); //  7 + 11 + 13 + 17 =  48
f(void 0, {}); // 19 + 23 + 13 + 17 =  72
f({}); //  7 + 11 + 43 + 47 = 108
f(); // 19 + 23 + 43 + 47 = 132

3

3-1. Object 初期値と引数初期値を揃える

function f({ a = 1, b = 2 } = { a: 1, b: 2 }) {
  return a + b;
}

f({ a: 3, b: 5 }); // 8
f({ a: 3 }); // 5
f({ b: 5 }); // 6
f({ a: null }); // 2
f({ b: null }); // 1
f({ a: void 0 }); // 3
f({ b: void 0 }); // 3
f({}); // 3
f(); // 3

function g(a = 1, b = 2) {
  return a + b;
}

g(3, 5); // 8
g(3); // 5
g(5); // 7
g(void 0, 5); // 6
g(null, 5); // 5
g(); // 3

3-2. TypeScript の場合

typescript.ts
interface I {
  a?: number;
  b?: number;
}

function f({a = 1, b = 2}: I = {a: 1, b: 2}) {
  return a + b;
}

f({a: 3, b: 5}); // 8
f({a: 3});       // 5
f({b: 5});       // 6
f({a: null});    // 2
f({b: null});    // 1
f({a: void 0});  // 3
f({b: void 0});  // 3
f({});           // 3
f();             // 3

TypeScript でもinterfaceを指定すれば書ける。

何がしたかったか

  • 過剰な例を試して初期値の渡り方を確認したかった
  • Object 内の初期値と引数自体の初期値を 2 種類宣言できてしまうところが毒にも薬にも成りうる
  • 引数自体の初期値に入れたプロパティは、Object 内のプロパティ以外採用されない
  • DI コンテナ風の機構を作れないかと考えている
    • この用途だと、Decorators 構文の方が便利かもと思い始めている

ご利用は計画的に。

Discussion