Nullish 合体演算子

公開日 · タグ: ECMAScript ES2020

Nullish 合体演算子の提案 (提案はこちら)(??) は、デフォルト値を扱うための新しい短絡演算子を追加します。

既に、他の短絡演算子である&&||に馴染みがあるかもしれません。これら2つの演算子は、「真偽値」を扱います。lhs && rhsというコード例を考えてみましょう。lhs(左辺)が偽の場合、式はlhsと評価されます。そうでない場合、rhs(右辺)と評価されます。lhs || rhsというコード例では、逆になります。lhsが真の場合、式はlhsと評価されます。そうでない場合、rhsと評価されます。

しかし、「真偽値」とは正確には何でしょうか?仕様の用語では、ToBoolean抽象操作に相当します。私たち通常のJavaScript開発者にとって、undefinednullfalse0NaN、空文字列''を除く**すべて**が真偽値です。(技術的には、document.allに関連付けられた値も偽ですが、それは後で説明します。)

では、&&||の問題点は何でしょうか?そして、なぜ新しいNullish 合体演算子が必要なのでしょうか?それは、真偽値のこの定義がすべてのシナリオに適合せず、バグにつながるためです。以下の例を考えてみましょう。

function Component(props) {
const enable = props.enabled || true;
// …
}

この例では、enabledプロパティを、コンポーネントの機能の有効/無効を制御するオプションのブールプロパティとして扱います。つまり、enabledtrueまたはfalseに明示的に設定できます。しかし、オプションのプロパティであるため、何も設定しなければ暗黙的にundefinedに設定されます。undefinedの場合、コンポーネントがenabled = true(デフォルト値)であるかのように扱いたいと考えています。

この時点で、コード例のバグを見つけることができるでしょう。enabled = trueを明示的に設定すると、enable変数はtrueになります。enabled = undefinedを暗黙的に設定した場合も、enable変数はtrueになります。そして、enabled = falseを明示的に設定した場合でも、enable変数は依然としてtrueです!私たちの意図は値をtrueにデフォルト設定することでしたが、実際には値を強制していました。この場合の修正は、期待する値を非常に明確に記述することです。

function Component(props) {
const enable = props.enabled !== false;
// …
}

このようなバグは、すべての偽値で発生する可能性があります。これは、空文字列''が有効な入力と見なされるオプションの文字列、または0が有効な入力と見なされるオプションの数値である可能性があります。これは非常に一般的な問題であるため、この種のデフォルト値の割り当てを処理するために、Nullish 合体演算子を導入します。

function Component(props) {
const enable = props.enabled ?? true;
// …
}

Nullish 合体演算子(??)は||演算子と非常に似ていますが、「真偽値」を使用せずに演算子を評価します。代わりに「Nullish」の定義、「値がnullまたはundefinedと厳密に等しいか」を使用します。したがって、lhs ?? rhsという式を考えてみましょう。lhsがNullishでない場合、lhsと評価されます。そうでない場合、rhsと評価されます。

具体的には、false0NaN、空文字列''はすべて、Nullishではない偽値です。このような偽だがNullishではない値がlhs ?? rhsの左辺にある場合、式は右辺ではなくそれ自身と評価されます。バグはなくなります!

false ?? true;   // => false
0 ?? 1; // => 0
'' ?? 'default'; // => ''

null ?? []; // => []
undefined ?? []; // => []

デストラクチャリング時のデフォルトの割り当てについて #

最後のコード例は、オブジェクトデストラクチャリング内でデフォルトの割り当てを使用することで修正できることに気付いたかもしれません。

function Component(props) {
const {
enabled: enable = true,
} = props;
// …
}

少し回りくどいですが、完全に有効なJavaScriptです。ただし、わずかに異なるセマンティクスを使用します。オブジェクトデストラクチャリング内のデフォルトの割り当ては、プロパティがundefinedと厳密に等しいかどうかをチェックし、等しい場合は割り当てをデフォルト値にします。

しかし、undefinedのみに対するこのような厳密な等価性テストは常に望ましいとは限らず、デストラクチャリングを実行するオブジェクトが常に存在するとは限りません。例えば、関数の戻り値をデフォルト値にしたい場合(デストラクチャリングするオブジェクトがない)、または関数がnullを返す場合(DOM APIでは一般的です)があります。これらはNullish 合体演算子を使用したい場合です。

// Concise nullish coalescing
const link = document.querySelector('link') ?? document.createElement('link');

// Default assignment destructure with boilerplate
const {
link = document.createElement('link'),
} = {
link: document.querySelector('link') || undefined
};

さらに、オプションチェーンなどの特定の新しい機能は、デストラクチャリングと完全に連携しません。デストラクチャリングにはオブジェクトが必要であるため、オプションチェーンがオブジェクトではなくundefinedを返した場合に備えて、デストラクチャリングを保護する必要があります。Nullish 合体演算子を使用すれば、そのような問題は発生しません。

// Optional chaining and nullish coalescing in tandem
const link = obj.deep?.container.link ?? document.createElement('link');

// Default assignment destructure with optional chaining
const {
link = document.createElement('link'),
} = (obj.deep?.container || {});

演算子の混合 #

言語設計は難しく、開発者の意図に一定量の曖昧さが伴わずに新しい演算子を作成できるわけではありません。&&||演算子を混在させたことがある場合、おそらくこの曖昧さに遭遇したことがあるでしょう。lhs && middle || rhsという式を考えてみましょう。JavaScriptでは、これは実際には(lhs && middle) || rhsという式と同じように解析されます。次に、lhs || middle && rhsという式を考えてみましょう。これは実際にはlhs || (middle && rhs)と同じように解析されます。

&&演算子の左辺と右辺の優先順位は||演算子よりも高いことがわかるでしょう。つまり、暗黙的な括弧は||ではなく&&を囲みます。??演算子を設計する際に、優先順位を決定する必要がありました。次のいずれかの優先順位になります。

  1. &&||の両方よりも低い優先順位
  2. &&よりも低いが||よりも高い優先順位
  3. &&||の両方よりも高い優先順位

これらの優先順位の定義ごとに、4つの可能なテストケースを実行する必要がありました。

  1. lhs && middle ?? rhs
  2. lhs ?? middle && rhs
  3. lhs || middle ?? rhs
  4. lhs ?? middle || rhs

各テスト式において、暗黙的な括弧はどこに属するのかを決定する必要がありました。そして、開発者の意図どおりに式を正確に囲んでいなければ、コードは悪くなります。残念ながら、どの優先順位レベルを選択しても、テスト式の1つは開発者の意図に反する可能性があります。

最終的に、??と(&&または||)を混在させる場合は、明示的な括弧を付けることにしました(括弧のグループ化を明確にしたことに注目してください!メタジョーク!)。混在させる場合は、いずれかの演算子グループを括弧で囲む必要があります。そうでないと、構文エラーが発生します。

// Explicit parentheses groups are required to mix
(lhs && middle) ?? rhs;
lhs && (middle ?? rhs);

(lhs ?? middle) && rhs;
lhs ?? (middle && rhs);

(lhs || middle) ?? rhs;
lhs || (middle ?? rhs);

(lhs ?? middle) || rhs;
lhs ?? (middle || rhs);

このようにして、言語パーサーは常に開発者の意図に合致します。そして、後でコードを読む人もすぐに理解できます。素晴らしい!

document.allについて #

document.all は、絶対に使用すべきではない特別な値です。しかし、使用する場合、それが「真偽値」および「Nullish」とどのように相互作用するかを知っておくことが最善です。

document.allは配列のようなオブジェクトであり、配列のようなインデックス付きのプロパティと長さを持っています。オブジェクトは通常真偽値ですが、驚くべきことに、document.allは偽値であるふりをします!実際、nullundefinedの両方と緩く等価です(通常はプロパティをまったく持てないことを意味します)。

&&または||document.allを使用する場合、偽値であるふりをします。しかし、nullまたはundefinedと厳密に等しくないため、Nullishではありません。したがって、??document.allを使用する場合、他のオブジェクトと同じように動作します。

document.all || true; // => true
document.all ?? true; // => HTMLAllCollection[]

Nullish 合体演算子のサポート #