JavaScript
esnext
戸田奈津子訳
proposal

これから来そうなJavaScript新機能3選

時々PHPのRFCを紹介していますが、JavaScriptにもproposalという似たような仕組みが存在します。
JavaScript最大の進歩だったasync/awaitproposal出身です。

以下はそのproposalの中から、有用そうな提案3点を取り挙げた記事、Here are three upcoming changes to JavaScript that you'll loveの戸田奈津子訳です。

Here are three upcoming changes to JavaScript that you'll love

今後来るであろうJavaScriptの便利な新機能を見てみましょう。
ここでは新しい構文の紹介、最新状況へのリンク、そして今すぐその構文を使ってみる小さなテストスイートを作成します。

How JavaScript Changes

0__ttLRUUBsYYoQS5u_.png

Ecma TC39がどのようにJavaScriptの変更を管理しているかを既に知っている場合は、このセクションは飛ばしてもかまいません。

JavaScriptの言語仕様がどのように決定しているか知らない人のために、ここでプロセスの概要を簡単に説明します。

JavaScriptは、ECMAScriptという言語標準の実装です。
この標準は、Webブラウザの進化の初期段階において、JavaScriptの独自進化を標準化するために形成されました。
ECMAScriptには8のエディションが存在しており、7回のリリースが存在します(4番目のエディションは放棄された)。

JavaScriptエンジンは、ECMAScriptがリリースされた後で、変更点の実装を開始します。
このチャートを見るとわかるように、全てのJavaScriptエンジンが全ての機能を実装しているわけではなく、一部エンジンは機能を実装するのに非常に時間がかかることもあります。
これは最善ではないと思うかもしれませんが、それでも標準が存在しない世界よりは遙かに良いと私は考えています。

Proposals

ECMAScriptの各エディションは、proposalによる審査のプロセスを経て公開されます。
proposalが有用で、かつ後方互換性を壊さないと見做された場合、それは次のエディションに追加されることになります。

proposalは5段階のステージに分かれています。
全てのproposalは、最初は"strawman"と呼ばれるステージ0から始まります。
この段階のproposalは、まだ十分に検討されていないか、もしくは、次に進む基準を満たしていないしリジェクトもされてはいないという状態です。

ステージ0のproposalは今後どうなるか不安定で、破棄されたり大幅に変更されることもあります。
従って、それらをプロダクトアプリに使ったりしないことを強く勧めます。

ここで取り上げるproposalは、いずれもステージ0ではありません。

Test Suite

プログラミングの紹介では、コードはしばしば抜粋した形で表示されます。
私はTDDが大好きなので、新機能を学ぶためにはテストを書いてみるのが一番だと思っています。

ここではKent Beckによる学習テストを使用します。
このテストで書くアサーションは、自分で書いたコードをテストするというより、言語仕様自体を確認するという意味合いが強いです。
このコンセプトは、サードパーティのAPIや他の言語を学ぶときにも役立つ方法だと思います。

Transpilers

既にトランスパイラを使っているのであれば、この章は飛ばしてもかまいません。

まだ実装されていない機能なのに、どうやって使用するのか不思議に思う人もいるかもしれません。

JavaScriptには、JavaScriptをJavaScriptに変換するトランスパイラというものがいくつも存在します。
聞いただけではとても役に立ちそうには思えない気もしますが、しかしこれが実際にはとても役立つものだと私が保証します。
ステージ0のproposalsを含んだ最新バージョンのJavaScriptを書いたとしても、トランスパイラが、今のWebブラウザやNode.jsのような実行環境で動作するようにしてくれるのです。

これは、トランスパイラが未実装のコードを古いバージョンでも動くJavaScriptに変換することによって実現されます。

現在最も普及しているJavaScriptのトランスパイラはBabelであり、以下の記事ではBabelを使用します。

Setup

以下のコードを試してみるには、Node.jsNPMがインストールされている必要があります。
トランスパイラの導入は以下のコマンドで行います。

npm init -f && npm i ava@1.0.0-beta.3 @babel/preset-env@7.0.0-beta.42 @babel/preset-stage-0@7.0.0-beta.42 @babel/register@7.0.0-beta.42 @babel/polyfill@7.0.0-beta.42 @babel/plugin-transform-runtime@7.0.0-beta.42 @babel/runtime@7.0.0-beta.42 --save-dev

次にpackage.jsonに以下を追加します。

    "scripts": {
      "test": "ava"
    },
    "ava": {
      "require": [
        "@babel/register",
        "@babel/polyfill"
      ]  
    }

最後に.babelrcファイルを作成します。

    {
      "presets": [
        ["@babel/preset-env", {
          "targets": {
            "node": "current"
          }
        }],
        "@babel/preset-stage-0"
      ],
      "plugins": [
        "@babel/plugin-transform-runtime"
      ]
    }

これで、テストを動かす準備が完了しました。

1. Optional Chaining

JavaScriptはObjectでできています。
Objectはしばしば、期待したような形になっていないことがあります。
以下は、データベースやAPIなどから取得したデータオブジェクトの例です。

    const data = {
      user: {
        address: {
          street: 'Pennsylvania Avenue',
        }, 
      },
    };

一方、データの登録を完了していないユーザがいたとしたらこうなります。

    const data = {
      user: {},
    };

何も考えずにstreetにアクセスしていた場合、以下のエラーが発生します。

    console.log(data.user.address.street); // Uncaught TypeError: Cannot read property 'street' of undefined

現状では、エラーを防ぎつつstreetプロパティを確認するには、以下のようにアクセスしなければなりません。

    const street = data && data.user && data.user.address && data.user.address.street;
    console.log(street); // undefined

私の感想としては、この方法は、
・醜悪
・厄介
・冗長
の三重苦です。

そこでOptional Chainingの登場です。

    console.log(data.user?.address?.street); // undefined

素晴らしく簡単ですね。
この機能の有用性がわかったので、もっと詳しく見ていきましょう。
さっそくテストを書いてみます。

    import test from 'ava';

    const valid = {
      user: {
        address: {
          street: 'main street',
        },
      },
    };

    function getAddress(data) {
      return data?.user?.address?.street;
    }

    test('存在すれば値を返す', (t) => {
      const result = getAddress(valid);
      t.is(result, 'main street');
    });

Optional Chainingは、?のつかないドット記法と互換を保っていることがわかります。
次に、存在しないプロパティを参照するテストを追加しましょう。

    test('プロパティがなければundefinedが返ってくる', (t) => {
      t.is(getAddress(), undefined);
      t.is(getAddress(null), undefined);
      t.is(getAddress({}), undefined);
    });

配列プロパティに対してのOptional Chainingの動作は以下のとおりです。

    const valid = {
      user: {
        address: {
          street: 'main street',
          neighbors: [
            'john doe',
            'jane doe',
          ],
        },
      },
    };

    function getNeighbor(data, number) {
      return data?.user?.address?.neighbors?.[number];
    }

    test('Optional Chainingは配列プロパティも対象', (t) => {
      t.is(getNeighbor(valid, 0), 'john doe');
    });

    test('配列プロパティがなければundefinedが返ってくる', (t) => {
      t.is(getNeighbor({}, 0), undefined);
    });

Object内に関数が実装されているかどうかわからないことがあります。
よくある例はWebブラウザです。
古いブラウザには特定の機能が存在しない場合があります。
幸いなことに、関数の実装有無についてもOptional Chainingで検出可能です。

    const data = {
      user: {
        address: {
          street: 'main street',
          neighbors: [
            'john doe',
            'jane doe',
          ],
        },
        getNeighbors() {
          return data.user.address.neighbors;
        }
      },
    };

    function getNeighbors(data) {
      return data?.user?.getNeighbors?.();
    }

    test('Optional Chainingはfunctionも対象', (t) => {
      const neighbors = getNeighbors(data);
      t.is(neighbors.length, 2);
      t.is(neighbors[0], 'john doe');
    });

    test('関数がなければundefinedが返ってくる', (t) => {
      const neighbors = getNeighbors({});
      t.is(neighbors, undefined);
    });

Optional Chainingで値を取得できなかった場合、そこで処理が止まります。
動作のイメージとしては概ね以下のようなかんじになります。

    value == null ? value[some expression here]: undefined;

従って、オペレータ?がundefinedかnullを返した場合、それ以降の処理は実行されません。
以下のテストでその動作を確認できます。

    let neighborCount = 0;

    function getNextNeighbor(neighbors) {
      return neighbors?.[++neighborCount];
    }

    test('++neighborCountは一回しか呼ばれてない', (t) => {
      const neighbors = getNeighbors(data);
      t.is(getNextNeighbor(neighbors), 'jane doe');
      t.is(getNextNeighbor(undefined), undefined);
      t.is(neighborCount, 1);
    });

貴方は既にこの機能を使うことができます。
Optional Chainingを使うことで、余計なif文、lodashのようなライブラリ、&&を使わずに済むようになります。

A word of warning

もしかしたら、Optional Chainingに多少のオーバーヘッドがあることに気付いたかもしれません。
?を過度に使用しすぎると、パフォーマンスが低下します。

Objectを作成したときか、受け取ったときにだけOptional Chainingを使って検証を行うことを勧めます。
それによって以後のチェックの必要がなくなり、パフォーマンス低下を抑えられます。

Link

proposalはここにあります。
この記事の一番下にもリンクの一覧を置いておきます。

2. Nullish coalescing

JavaScriptでよく見られる処理に以下のようなものがあります。
・値がundefinedかnullでないかをチェックする
・チェックに引っかかればデフォルト値を入れる
・ただし0false、''などfalsyな値はそのまま通す

以下のようなコードになるでしょう。

    value != null ? value : 'default value';

もしくは以下のように不適切な処理を見たことがあるかもしれません。

    value || 'default value'

こう書いてしまうと、0false、''なども全てfalseと見做されてしまい、デフォルト値が入ってしまいます。
そのため、値は明示的にnullと比較しなければなりません。

    value != null

これは以下と同じです。

    value !== null && value !== undefined

ここで新たなオペレータ、Nullish coalescingの登場です。

    value ?? 'default value';

これは、falsyな値をうっかりデフォルト値で上書きすることを防ぎつつ、三項演算子と!= nullを使わずにnullとundefinedをチェックすることができます。
構文がわかったので、実際にどう動くかテストを書いてみます。

    import test from 'ava';

    test('nullは該当', (t) => {
      t.is(null ?? 'default', 'default');
    });

    test('undefinedも該当', (t) => {
      t.is(undefined ?? 'default', 'default');
    });

    test('void 0も該当', (t) => {
      t.is(void 0 ?? 'default', 'default');
    });

    test('0はNullish coalescingではない', (t) => {
      t.is(0 ?? 'default', 0);
    });

    test('空文字もNullish coalescingではない', (t) => {
      t.is('' ?? 'default', '');
    });

    test('falseもNullish coalescingではない', (t) => {
      t.is(false ?? 'default', false);
    });

null、undefined、およびvoid 0がNullish coalescingにひっかかることがわかります。
逆に0や''、falseなどfalsyな値では引っかかりません。
なおvoid 0はundefinedと評価されます。

proposalはこちらです。

3. Pipeline operator

関数型プログラミングには複数の関数を合成するCompositionという機能があります。
各関数は手前の関数の出力を入力として受け取ります。
プレーンなJavaScriptでの例を以下に示します。

    function doubleSay (str) {
      return str + ", " + str;
    }
    function capitalize (str) {
      return str[0].toUpperCase() + str.substring(1);
    }
    function exclaim (str) {
      return str + '!';
    }
    let result = exclaim(capitalize(doubleSay("hello")));
    result //=> "Hello, hello!"

これらの書き方はとても一般的なため、lodashramdaをはじめ多くのライブラリでサポートされています。

Pipeline operatorを使うと、サードパーティライブラリを使わず以下のように書けるようになります。

    let result = "hello"
      |> doubleSay
      |> capitalize
      |> exclaim;

    result //=> "Hello, hello!"

これの目的は、関数のチェーンをより読みやすくするためです。

将来は部分適用もできるようになる予定ですが、現在は以下のように確認できます。

    let result = 1
      |> (_ => Math.max(0, _));

    result //=> 1
    let result = -5
      |> (_ => Math.max(0, _));

    result //=> 0

シンタックスがわかったのでテストを書いてみます。

    import test from 'ava';

    function doubleSay (str) {
      return str + ", " + str;
    }

    function capitalize (str) {
      return str[0].toUpperCase() + str.substring(1);
    }

    function exclaim (str) {
      return str + '!';
    }

    test('Simple pipeline usage', (t) => {
      let result = "hello"
        |> doubleSay
        |> capitalize
        |> exclaim;

      t.is(result, 'Hello, hello!');
    });

    test('Partial application pipeline', (t) => {
      let result = -5
        |> (_ => Math.max(0, _));

      t.is(result, 0);
    });

    test('Async pipeline', async (t) => {
      const asyncAdd = (number) => Promise.resolve(number + 5);
      const subtractOne = (num1) => num1 - 1;
      const result = 10
        |> asyncAdd
        |> (async (num) => subtractOne(await num));

      t.is(await result, 14);
    });

ひとつ注意点としては、Pipeline operatorで非同期関数を使う場合はawaitしなければならないことです。
そうしないと、値がPromiseになってしまうからです。
非同期関数をうまく扱う|> awaitのような文法がいくつか提案されていますが、いずれも未実装か未決定です。

さて、ここまで読んできたことで、これらの機能を使いこなすことができるようになったことでしょう。
これらの機能を使って快適なプログラミングができることを信じています。

The full code

全てのテストコードはこちらで見付けることができます。

TC39/proposal-optional-chaining
TC39/proposal-nullish-coalescing
TC39/proposal-partial-application
TC39/proposal-pipeline-operator

感想

PHPの@叩かなかった人だけがOptional Chainingを賞賛するがよい。

今回は英文がかなりわからなかった。
「If you want to follow along with the code then feel free.」わからん。
「Each level that you check with ? must be wrapped in some sort of conditional logic under the hood. 」わからん。