目次


多忙な JavaScript 開発者のための ECMAScript 6 ガイド、第 4 回

標準のライブラリーに追加された新しいオブジェクトと型

新しい JavaScript に追加されたモジュール、コレクション、プロキシーその他多数の機能を利用する

Comments

コンテンツシリーズ

このコンテンツは全4シリーズのパート#です: 多忙な JavaScript 開発者のための ECMAScript 6 ガイド、第 4 回

このシリーズの続きに乞うご期待。

このコンテンツはシリーズの一部分です:多忙な JavaScript 開発者のための ECMAScript 6 ガイド、第 4 回

このシリーズの続きに乞うご期待。

Interactive code: When you see Run at the top of a code sample, you can run the code, check the results, make changes, and run it again.

編集者による注記: このシリーズは、対話式コーディング機能を追加して更新されています。コード・リストに「実行」ボタンがある場合、そのコードを実行して結果を確認し、コードを変更してから再度実行して、その成果を確認できることを意味します。

シリーズのこれまでの記事では、ECMAScript 6 仕様で行われた JavaScript の変更内容のうち、とりわけ大幅な変更について説明してきました。シリーズを読み続けていれば、構文の変更のいくつかをサンプリングし、新しく導入されたアロー関数などの関数型の機能を発見し、JavaScript プログラムで従来型のクラス構文を試してみたはずです。皆さんが私の同類であるとしたら、ECMAScript Technical Committee が 20 年にわたって誰もが気に入ってきたこのスクリプト言語で、その使いやすさやプロトタイプ・ベースのオブジェクト・システムを犠牲にすることなく大幅な変更を成し遂げたことを知って、かなり安心していることでしょう。

シリーズ最終回となるこの記事では、標準のライブラリーに加わった新しいオブジェクトと型をいくつか取り上げて紹介します。これらのオブジェクトと型の中には、JavaScript や他の言語で間違いなく使用したことがあるものもあれば、少々、あるいはかなり新鮮に映るものもあるでしょう。いずれにしても、これらのオブジェクトと型が追加されたことには価値があり、ゆくゆくは開発者のツールキットに加わることになると断言します。

モジュール

ECMAScript 6 でのライブラリーに関する機能拡張のうち、日常的なプログラミング作業で最も目立つのはモジュールでしょう。これまでは Node.js の慣例に従って、ファイルを require として指定し、名前付きグローバル変数オブジェクト exports を使用して戻り値を記述していましたが、その必要はもうありません! ECMAScript 6 では、import および export ステートメントを使ってモジュールの概念を形式化するようになっています。お察しのとおり、export は ECMAScript ファイルからエクスポートする名前付き値 (通常はクラスまたは関数ですが、変数の場合もあります) を宣言するために使用し、import はこれらのエクスポートされた名前を別のファイルにプルするために使用します。

注: セキュリティー上の理由により、developerWorks サンドボックスではファイルのインポートは許可されません。以下のコードを試す場合は、独自の output.js ファイルを作成して、ご使用のコードにインポートする必要があります。

基本的に、モジュールとして扱いたいファイル (例えば、output という名前のファイル) がある場合、以下のように export を使用して、他の場所で使用できるようにするシンボルをエクスポートするだけで、ファイルをモジュールとして扱えるようになります。

    // output.js
    export function out() {
      console.log("OUT: ", arguments);
    }

関数の前にキーワード export を入力することで、ECMAScript に対し、このファイルをモジュールとして扱うように指示します。これにより、以降はこのファイルをインポートする他のすべてのファイルで、この関数を使用できるようになります。

    import { output } from 'output.js';
    out("I'm using output!");

お気付きかもしれませんが、この import 構文には 1 つの大きな欠点があります。それは、モジュールを使用するためには、インポートする名前をすべて知っていなければならないことです (ただし、これは利点として受け止められることもあります。開発者にインポートする意図がなければ、シンボルは決してインポートされないためです)。モジュールからすべての名前をエクスポートするには、ワイルドカード (*) インポート構文を使用できますが、その場合、それらの名前のスコープとするモジュールの名前を定義する必要があります。

    import * as Output from 'output.js';
    Output.out("I'm using output!");

モジュール名の定義が要件となっているのは、スコープ・メカニズムを適用するためです。モジュール名を定義せずに 2 つのモジュールのそれぞれが out をエクスポートしたとすると、新しいほうが暗黙的に前のものを置き換えることになります。入力する名前はモジュール名にラップされるため、さらに曖昧さがなくなります。

Symbol

ECMAScript 6 で導入された、それほど明らかではない機能のうちの 1 つは、新しい Symbol 型です。表面上、この新しい型は、それほど注目に値するようには見えません。基本的に、Symbol のインスタンスは別の場所では複製できない一意の名前であるというだけのことです。

ECMAScript オブジェクトは単なる名前と値のペアのコレクションであり、値はデータ (文字列、数値、オブジェクト参照など) または動作 (関数参照の形) のいずれかにできるということを思い出してください。一般に名前がわかれば、疑うことなく、その値を取得できます。

ただし場合によっては、オブジェクトの所有者が、選択される名前が他の名前と競合しないことを確認できる必要があることもあります。そのような場合は、従来の String ベースの名前を使用するのではなく、Symbol インスタンスを使用できます。Symbol を使用すれば、競合する名前が他にないことが確実になります。

一例として、典型的な Person 型に取り組みましょう。

Show result

上記の 3 つのオブジェクト・フィールドには、そのフィールドの名前を知っていれば誰でも簡単にアクセスできます。フィールド名には、オブジェクトの内容に対する単純な繰り返し処理によってアクセスできます。未だかつて ECMAScript がセキュリティーに優れた言語として認知されたことはないにせよ、このサンプル・コードは基本的なレベルのカプセル化でさえ失敗します。

Symbol を使用したアクセス制御

例えば、いくつかのフィールドを非表示にする必要があるとします。その場合にまず考えられるのは、標準の文字列ではなく、Symbol 型の名前を使用してアクセス可能にするという方法です。

Show result

上記に示されているように、Symbol 関数を使用して Symbol のインスタンスを作成できます。これらのインスタンスのそれぞれを、該当するオブジェクトで名前として使用できます。誰かが標準の String ベースの名前 (例えば、firstName) を使用してフィールドにアクセスしようとすると、データはその名前では存在しなくなっているため、結果が未定義になります。新しい仕様では、JavaScript は標準のオブジェクト繰り返し処理で Symbol ベースの名前を表示することさえしません。オブジェクトで従来型のリフレクションを使用しようとすると、基本的に失敗します。

もう 1 つ重要な点として、外部から新しいメンバーをオブジェクトに追加する場合 (メタオブジェクト・プログラミングの例)、文字列 firstName を使用しても、既存のメンバーと競合することも、既存のメンバーを置き換えることもないという点があります。既存のオブジェクトにさらに動作またはメンバーを追加する必要があるライブラリーやフレームワーク (つまり、現在使用されている、ほぼすべての最近のフレームワーク) には、この点が極めて重要です。

ただし、リスト 5 の最後の行に示されているように、呼び出し側に Symbol インスタンスがある場合は、ためらうことなく、そのインスタンスを使用してデータにアクセスできます。他の言語での private キーワードとは異なり、Symbol はアクセス制御を適用するにはそれほど効果的ではありません。

メンバー名

JavaScript では、環境固有のパターンに従ったオブジェクトを作成する際に役立つ、よく知られたメンバー名を多数サポートしています。その一例は、iterator です。反復動作をサポートするオブジェクトでは、iterator を使用して関数の名前を指定できます。フィボナッチ数列を生成するオブジェクトを作成して、そのオブジェクトを標準の ECMAScript イテレーターに見せ掛けるとしたら、iterator 関数を使用してそのオブジェクトを作成する必要があります。ただし、他の誰かが iterator という名前を使用している可能性が考えられるため、ECMAScript 6 では iterator ではなく、iteratorSymbol を使用するよう主張しています。

Show result

上記の例でも、Symbol の主な役割は、プログラマーがさまざまなライブラリーの間での名前の競合を回避できるようにすることです。最初は少々理解しにくいものの、Symbol は、入力された文字列名に基づく一意のハッシュだと考えるようにしてください。

コレクション型

ECMAScript を 10 分も使ってみれば、この言語は配列をサポートすることにすぐに気付くはずです。バージョン 1.0 以降、配列はこの仕様の中核的な部分となっています。いくつかの制約事項はあるとは言え (とりわけ、サイズが固定されること)、配列は非常に役立ちます。これから先も、そうであり続けることでしょう。

ただし、開発者たちが他言することがないとしても、ある事実を認めなければならない時期が来ています。それは、配列は、言い辛いのですが、万能ではないことです。

配列の不足を補うために、ECMAScript 6 では標準の JavaScript 環境に MapSet という 2 つのコレクション型を追加しています。

Map は、一連の名前/値のペアです。この点は ECMAScript オブジェクトとほとんど同じですが、Map には、ロー ECMAScript オブジェクトよりも簡単に処理できる以下のメソッドが含まれるという点が異なります。

  • get()set()。それぞれ、キー/値のペアを検索、設定します。
  • clear()。コレクションを完全に空にします。
  • keys()Map 内の繰り返し処理できるキーのコレクションを返します。
  • values()。Map 内の繰り返し処理できる値のコレクションを返します。

また、Array と同じように、Map には関数型言語から着想を得たメソッドも含まれています。その一例は、Map 自体に作用する forEach() です。

Show result

一方、Set は、オブジェクトをそのまま追加できるという点で、従来型のオブジェクトのコレクションと同じように見えます。けれども Set の場合、各オブジェクトが順に検査されて、コレクション内にすでに含まれる値と重複していないことが確認されます。

Show result

Map と同じく、Set にも関数型スタイルのやりとりを可能にする forEach などのメソッドがあります。根本的に Set は配列と同様ですが、Set では角括弧を使用しません。Set は動的に拡大しますが、順序付けメカニズムは一切ないため、Set を使用する場合、配列と異なりインデックスを使用してオブジェクトを参照することはできません。

弱参照

ECMAScript 6 では、WeakMapWeakSet も導入されています。この 2 つの MapSet は、従来型の強参照ではなく、弱参照によって値を保持します。WeakMap に保持されたオブジェクトがどうなるのかについては、以下のように ECMAScript 仕様で最も適切に説明しています。この説明は、WeakSet にも同じく当てはまります。

WeakMap のキー/値ペアのキーとして使用されているオブジェクトに到達するのに、WeakMap 内から始まる一連の参照を辿るしか方法がないとしたら、そのキー/値ペアはアクセス不可能であり、自動的に WeakMap から削除されます。

この記事では WeakMapWeakSet の有用性を詳しく説明することはしません。これらは主にライブラリー・コード用に (特に、キャッシングに関して) 使用されることになるため、アプリケーション・コード内で出現することはほとんどありません。

Promise

非同期処理は Node.js のシナリオの中核です (通常は、イベント駆動型プログラミングという謳い文句で使用されます)。けれどもこれまで、非同期処理を実装するのは決して容易なことではありませんでした。当初、Node.js コミュニティーはイベント・サブスクリプションを使用するという方法に落ち着くように見えましたが、そのうち、開発者たちはコールバック駆動型のスタイルに移行するようになりました。この傾向は私たちが現在陥っているコールバック地獄へと至っています。つまり、Node.js コードが画面上を延々と流れていくという事態です。

Show result

上記のコードは、1 冊の本しか置いていない書店での注文を処理するものです。注文を処理するために必要なロジック全体が、複数の関数呼び出しをネストしてインライン化されています。これが実際の処理を行うコードであったとしたら、それぞれのコールバックとその次のコールバックの間に多数の行が必要になるため、コードが難解になり、デバッグするにも、保守するにも極めて難しいものになってしまいます。

上記の例では、スペース 2 個分のインデントを使用していることにも注意してください。開発者がインデントをスペース 4 個分または 8 個分にすると主張して譲らなかった場合のスクロール操作を想像してみてください。

まだか、まだかと待たせた末に、ECMAScript コミュニティーは非同期計算でのコールバックに代わる手法を提案しました。それが、今回導入された標準の Promise 型です。

JavaScript 内での Promise の使用には、二重の意味があります。まず、(上記のリストでのように)「起動して何かを処理する」コードを作成した開発者は、従来型の同期処理や Node.js のコールバック表現を使うのではなく、Promise を返すようになります。これにより、呼び出し側は Promisethen() メソッドを使用して順次呼び出しを連鎖させ、catch() メソッドを使用してエラーが発生した場合の処理を定義できるようになります。

doSomething.then(function(result) {
  console.log(result); // It did something
}).catch(function(err) {
  console.log(err); // Error
});

doSomething() 側では、Promise インスタンスを生成するコードを作成できるようになります。その方法は一般に、非同期処理を行う関数を Promise でラップするというものです。

let promise = new Promise(function(resolve, reject) {
  let result = // do a thing, usually async in nature

  if (success) {
    resolve(result);
  }
  else {
    reject(Error("It didn't work"));
  }
});

Promise を使用して上記のコードをリファクタリングすると、以下のようになります。前のリストにインラインで組み込まれていたロジックは、複数の個別の関数に分解されて、それぞれの関数が Promise を使用するようになっています。このコードは長くなったとは言え、前のコードより遥かに理解しやすくなっています。

Show result

リストの先頭にある、大幅に単純化された processBookOrders() 関数が、このコードの内容を明らかにしています。つまり、このコードは次の注文を受けると、書店にその本が置かれているかどうかをチェックし、置かれていなければ、その本の在庫状況をチェックします。ここで呼び出される関数 (getNextOrder()doWeCarryThisBook()、または isItInStock()) はいずれも、reject 節を使用して JavaScript Error オブジェクトを返す場合、そこで関数は停止します。そうでなければ、関数は次の then ステートメントに進みます。このように複数の処理を連鎖できる機能は、非常に強力です。

bookOrder.isbn または bookOrder.qty の値を変更するか、getNextOrder() 内のコードを変更して、success = false になるようにしてみてください。注文が失敗すると、適切な Error オブジェクトが返されるはずです。

ここまでくると、明らかに、有用性が人それぞれに異なってくる領域になります。私の経験から想像するに、開発者はライブラリーから手渡される Promise を使用すると思うので、ほとんどの開発者は自分で作成するよりも、既製の Promise を使用することになるでしょう。けれどもゆくゆくは、別のモジュールで使用する独自の Promise を作成する開発者が増えてくるはずです。

Promise については他にも説明することがたくさんありますが、それについては別の記事に回さなければなりません。詳細を調べてからでなければ新しいものには手を出したくないという開発者にとって幸いなことに、ロー・コールバックおよびイベントは除去されていないので、この新機能を今すぐ導入する必要はありません。

動的プロキシー

動的プロキシーを使用した JavaScript プログラミングはすでに広まっていますが、ECMAScript 6 では新しい Proxy 型を標準化しています。標準化された手法があると、ライブラリー間で思いがけない競合が発生したり、混乱を招いたりするのを回避できます。基本的に、Proxy は「インターセプト」動作を実装して、あるオブジェクトを別のオブジェクトに優先できるようにします。つまり、インターセプトするオブジェクトが、元のターゲット・オブジェクトに意図されていた任意のメソッド呼び出しまたはプロパティー参照の機会を先に得るということです。

オブジェクトのメソッドを別の定義で置き換えられるという機能は、ECMAScript では新しいものではありませんが、Proxy 型ではそれ以上のことが可能です。メソッド呼び出し、プロパティー参照など、ターゲット・オブジェクトに存在しないリクエストをインターセプトすることさえできます。

百聞は一見に如かずということで、早速、サンプル・コードに取り掛かりましょう。プロキシーをデモするには、メソッド呼び出しのロギングを使用するのが慣例となっているので、私もそれに従います。ここでは、シリーズのすべての記事で一貫して使用している Person クラスを再び取り上げます。

Person オブジェクトを作成した後に、面白半分といった感じで、いくつかの興味深いメソッドを追加します。それらのメソッドを追加することで、ECMAScript オブジェクトがどのように作成または定義されているかに関わらず、動的プロキシーではあらゆる ECMAScript オブジェクトを処理できるという点も明らかになります。

Show result

上記のコードには、2 つのメソッドが追加されています。その 1 つはパラメーターを取らない sayHowdy()、もう 1 つは 1 つのパラメーターを取り、結果を返す waveGoodbye() です。メソッド自体はそれほど興味深いものとは言えませんが、捕捉したいメソッドを表すには十分です。この 2 つのメソッドのどちらかが呼び出されるたびに、理想的にはメソッド呼び出しに関する詳しく知りたい情報と併せて「メソッドが起動されました」というメッセージがコンソールに表示されるようにします。

ついでに、プロパティーへのアクセスが行われるタイミングを確認できるようにしたいと思いませんか?そのような機能があれば、値を取得または設定するために利用できます。これを可能にするのが、プロキシーです。プロキシーは、コンストラクターの呼び出しや、それほどよく使われない他の呼び出しをインターセプトできるだけではありません。基本的に、オブジェクトに対して可能なアクションであればすべて、プロキシーがインターセプトできるので、別のアクション (別の動作を追加するなど) を実行することが可能になります。

ECMAScript 6 でのプロキシーの使用

プロキシーをセットアップするには、まず、ターゲットを識別する必要があります。ターゲットとは、インターセプトしたいメソッドまたはプロパティーを持つオブジェクトのことです。この例では、Person オブジェクトをインターセプトします。次に、呼び出し側とターゲットの間に追加する動作を識別する必要があります。ECMAScript の用語では、これをハンドラーと呼びます。各ハンドラーが以下のメソッドから 1 つ以上を定義します。

  • get() および set()。それぞれプロパティーの値を検索する試行、設定する試行をインターセプトします (オブジェクトの関数もプロパティーであることに注意してください)。
  • apply()。関数の呼び出しをインターセプトします。
  • has()in 演算子をインターセプトします。
  • ownKeys()Object クラスの getOwnPropertyNames() メソッドをインターセプトします。
  • getPrototypeOf()setPrototypeOf()isExtensible()preventExtensions()defineProperty()getOwnPropertyDescriptor()。それぞれ、Object クラスの同じ名前のメソッドをインターセプトします。
  • deleteProperty()delete 演算子をインターセプトします。

まずは、Person (ターゲット) オブジェクトのプロパティーを取得および設定するリクエストのすべてを捕捉します。それには、それぞれ setget という名前のメソッドを指定するハンドラー・オブジェクトを作成します。

Show result

set ハンドラーはメッセージのログをコンソールに記録するだけでなく、プロパティーの代入処理も行うことに注意してください。代入処理が必要になるわけは、ハンドラーが完全に制御を掌握するためです。プロパティーをターゲットに割り当てなければ、プロパティーは設定されません。同じことは、get ハンドラーにも当てはまり、このハンドラーはターゲットのプロパティー値を返す必要があります。プロパティーを割り当てないと、プロパティーは返されません (undefined が返されるだけです)。

最後のステップは、Proxy オブジェクトをターゲットとハンドラーにつなぎ合わせることです。Proxy オブジェクトを元の変数に取り込むには、以下のコードを使用します。
ted = new Proxy(ted, handler);

シナリオによっては、元のターゲットを維持して、インターセプターを介さずにターゲットにアクセスできなければならない場合もあります。けれどもほとんどの場合は、ターゲット・オブジェクトを使用するクライアントがターゲットとの間に何かがあることを気付くことさえないように、Proxy をサイレント・プロセッサーとして使用することになるでしょう。

ハンドラーが設定された後に waveGoodbye()sayHowdy() を追加したとすると、ハンドラーはプロパティー設定処理に対して呼び出されます。これは、waveGoodbye()sayHowdy() は厳密には関数型のプロパティーであるためです。コードを実行して、プロパティーまたは関数へのアクセスがあるたびに、ハンドラーの get 関数と set 関数がどのように呼び出されるかを確認してください。その後、sayHowdy() および sayGoodbye() 関数をハンドラーの定義の後に移動し、コードを再度実行してみてください。

get() ハンドラーが呼び出されるはずです。メソッドにアクセスするということは、そのメソッドを (呼び出すために) 取得してから、(sayHowdy の場合は) そのメソッド内で参照されているすべてのプロパティーの値を取得することを意味します。ハンドラーが作成された時点では、favoriteColor プロパティーは存在していませんが、set() メソッドは、このプロパティーが設定される際に呼び出されます。新しいプロパティーにアクセスすると、ご想像のとおり、get() メソッドがトリガーされます。

関数上でのプロキシー・ハンドラー

誤解のないように言っておくと、プロパティーがどのように定義されているかに関わらず、get ハンドラーは常に呼び出されます。これを説明するために、Person クラス内に eat() という名前のメソッドを定義します。

Person で型付けされたオブジェクトの eat() メソッドが呼び出されると、ECMAScript は最初のタスクとして、プロパティー名「eat」を、関数になるプロパティーとして解決します。そしてこの関数を取得し、その直後に関数呼び出しを行います。呼び出される関数の詳細を確認するには、関数呼び出しプロセスの途中で、関数が特定されて返された後に、新しいハンドラーを追加する必要があります。そのための最も簡単な方法は、元の関数をラップする関数を返すことです。

Show result

このプロシージャーは複雑に見えるかもしれませんが、実はそうではありません。アクセスされるプロパティーが関数以外のものであれば、結果を取得して返すだけです。プロパティーが関数であれば、関数リテラルを作成して、関数ではなく、そのリテラルを返します。返された関数リテラルによって、元の関数が呼び出されます。関数オブジェクト上で ECMAScript の apply メソッドを使用すると、正しい this が所定の場所にバインドされることが確実になります (target ではなく、this を文字通り使用したとすると、this はターゲットではなく、ハンドラーになります)。

実に簡単だと思いませんか?

実際のところ、このようなタイプのコードを今までに見たことがない開発者にとって、このコードはかなり斬新なコードです。Proxy を使用すれば、例えば型セーフなプロパティー検証を行うことができます (設定される値の型が、特定のプロパティーに適切なものであることを確認するハンドラーを作成します)。また、リモート実行も可能になります (HTTP API を介してリモート呼び出しを行う方法を把握し、引数を JSON 配列に直列化して、結果を非直列化するプロキシーを返します)。さらに、アクセス許可の境界を設けることさえできます (ドメイン・オブジェクトを、メモリー内で特定のユーザーの資格情報をチェックするプロキシーでラップします)。公式には、これらの使用法のすべては、アスペクト指向プログラミングという見出しの下に分類されます。プロキシーとアスペクト指向プログラミングが結合することで、JavaScript での懸念をどのようにして攻略するのかをまったく新しい角度から考える世界がもたらされます。

まとめ

ECMAScript 6 は、これまでの JavaScript のリビジョンの中で群を抜いて大掛かりなものとなっています。そのため、調整期間が伴うのは必然のことであり、ECMAScript インタープリターはまだ完全にはこの新しい仕様に追いついていません。コードが折に触れて失敗するとしても驚かないでください。その場合は、インタープリターを調べてサポートされていない要素を確認し、必要に応じてコードを調整すればよいだけのことです。

また、コードがどうしても実行されないとしても、お手上げというわけはありません。よく使われている Node.js トランスパイラーのいずれかを利用して、開発最先端の ECMAScript コードを、やや保守的な色合いのコードに調整することもできます。

ECMAScript の素晴らしい点は、ECMAScript Technical Committee はこの言語を前進させながらも、かなりの後方互換性を実現したところにあります。このことから、少しずつ ECMAScript 6 を導入していくという段階的アプローチを取ることができます。つまり、最初は気に入った機能だけを選んでコードに統合し、その機能を使い慣れてきたら、次に試してみたい機能を選んでコードに統合するといった方法です。自分で対処できる以上に (または生産性を損ねるまでに) 深く関わる必要はありませんが、常に前進を続けてください。そうしていくうちに、標準の JavaScript に含まれる、数々の強力な新機能と慣例を徐々に利用できるようになるはずです。

これで、このシリーズは完了です。難しいことは言わずに、この言葉で締めくくります...

    return "Enjoy!";

またお会いしましょう。


ダウンロード可能なリソース


関連トピック


コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Web development
ArticleID=1047467
ArticleTitle=多忙な JavaScript 開発者のための ECMAScript 6 ガイド、第 4 回: 標準のライブラリーに追加された新しいオブジェクトと型
publish-date=07132017