ES2015 プロキシの概要

Addy Osmani
Addy Osmani

ES2015 プロキシChrome 49 以降)では、JavaScript に介入 API が提供されます。これにより、ターゲット オブジェクトに対するすべてのオペレーションをトラップまたはインターセプトし、このターゲットの動作を変更できます。

プロキシには次のような多くの用途があります。

  • インターセプト
  • オブジェクトの仮想化
  • リソース管理
  • デバッグ用のプロファイリングまたはロギング
  • セキュリティとアクセス制御
  • オブジェクトの使用に関する契約

Proxy API には、指定されたターゲット オブジェクトとハンドラ オブジェクトを受け取る Proxy コンストラクタが含まれています。

var target = { /* some properties */ };
var handler = { /* trap functions */ };
var proxy = new Proxy(target, handler);

プロキシの動作はハンドラによって制御されます。ハンドラは、ターゲット オブジェクトの元の動作をさまざまな方法で変更できます。ハンドラには、プロキシで対応するオペレーションが実行されたときに呼び出されるオプションのトラップ メソッド.get().set().apply() など)が含まれています。

インターセプト

まず、単純なオブジェクトを取得し、Proxy API を使用してインターセプト ミドルウェアを追加します。コンストラクタに渡される最初のパラメータはターゲット(プロキシされるオブジェクト)で、2 つ目のパラメータはハンドラ(プロキシ自体)です。ここで、ゲッター、セッター、その他の動作のフックを追加できます。

var target = {};

var superhero = new Proxy(target, {
    get: function(target, name, receiver) {
        console.log('get was called for:', name);
        return target[name];
    }
});

superhero.power = 'Flight';
console.log(superhero.power);

Chrome 49 で上記のコードを実行すると、次のようになります。

get was called for: power  
"Flight"

実際に確認できるように、プロキシ オブジェクトでプロパティの取得またはプロパティの設定を正しく実行すると、ハンドラの対応するトラップへのメタレベル呼び出しが行われます。ハンドラ オペレーションには、プロパティの読み取り、プロパティの割り当て、関数の適用などがあり、これらはすべて対応するトラップに転送されます。

トラップ関数は、必要に応じて任意のオペレーションを実装できます(オペレーションをターゲット オブジェクトに転送するなど)。トラップが指定されていない場合、デフォルトではこの処理が行われます。たとえば、次のノーオペレーション転送プロキシは、まさにこのことを行います。

var target = {};

var proxy = new Proxy(target, {});
    // operation forwarded to the target
proxy.paul = 'irish';
// 'irish'. The operation has been  forwarded
console.log(target.paul);

ここまでは単純なオブジェクトのプロキシ化について説明しましたが、関数オブジェクトのプロキシ化も同様に簡単に行えます。この場合、関数がターゲットになります。今回は handler.apply() トラップを使用します。

// Proxying a function object
function sum(a, b) {
    return a + b;
}

var handler = {
    apply: function(target, thisArg, argumentsList) {
        console.log(`Calculate sum: ${argumentsList}`);
        return target.apply(thisArg, argumentsList);
    }
};

var proxy = new Proxy(sum, handler);
proxy(1, 2);
// Calculate sum: 1, 2
// 3

プロキシの特定

プロキシの ID は、JavaScript の等価演算子(=====)を使用して確認できます。ご存じのとおり、これらの演算子は 2 つのオブジェクトに適用するとオブジェクト ID を比較します。次の例は、この動作を示しています。2 つの異なるプロキシを比較すると、基盤となるターゲットは同じであるにもかかわらず false が返されます。同様に、ターゲット オブジェクトはプロキシとは異なります。

// Continuing previous example

var proxy2 = new Proxy (sum, handler);
(proxy==proxy2); // false
(proxy==sum); // false

理想的には、プロキシとプロキシ以外のオブジェクトを区別できないようにし、プロキシを配置してもアプリの結果に影響しないようにする必要があります。これが、Proxy API にオブジェクトがプロキシかどうかを確認する方法が含まれていない理由の一つです。また、オブジェクトに対するすべてのオペレーションにトラップも用意されていません。

ユースケース

前述のとおり、プロキシにはさまざまなユースケースがあります。アクセス制御やプロファイリングなど、上記の多くは汎用ラッパーに該当します。これは、同じアドレス「空間」に他のオブジェクトをラップするプロキシです。仮想化も言及されました。仮想オブジェクトは、他のオブジェクトをエミュレートするプロキシであり、それらのオブジェクトが同じアドレス空間に存在する必要はありません。たとえば、リモート オブジェクト(他のスペース内のオブジェクトをエミュレートする)や、透過的なフューチャー(まだ計算されていない結果をエミュレートする)などがあります。

ハンドラとしてのプロキシ

プロキシ ハンドラの一般的なユースケースは、ラップされたオブジェクトに対してオペレーションを実行する前に、検証またはアクセス制御チェックを実行することです。チェックが成功した場合にのみ、オペレーションが転送されます。次の検証例は、このことを示しています。

var validator = {
    set: function(obj, prop, value) {
    if (prop === 'yearOfBirth') {
        if (!Number.isInteger(value)) {
        throw new TypeError('The yearOfBirth is not an integer');
        }

        if (value > 3000) {
        throw new RangeError('The yearOfBirth seems invalid');
        }
    }

    // The default behavior to store the value
    obj[prop] = value;
    }
};

var person = new Proxy({}, validator);

person.yearOfBirth = 1986;
console.log(person.yearOfBirth); // 1986
person.yearOfBirth = 'eighties'; // Throws an exception
person.yearOfBirth = 3030; // Throws an exception

このパターンのより複雑な例では、プロキシ ハンドラがインターセプトできるさまざまなオペレーションをすべて考慮に入れることができます。各トラップでアクセス チェックとオペレーションの転送のパターンを複製する必要がある実装を想像できます。

各オペレーションを異なる方法で転送する必要がある場合、抽象化が難しくなる可能性があります。理想的なシナリオでは、すべてのオペレーションを 1 つのトラップに均一に転送できる場合、ハンドラは 1 つのトラップで検証チェックを 1 回だけ実行する必要があります。これを行うには、プロキシ ハンドラ自体をプロキシとして実装します。残念ながら、これはこの記事の対象外です。

オブジェクト拡張機能

プロキシのもう 1 つの一般的なユースケースは、オブジェクトに対するオペレーションのセマンティクスを拡張または再定義することです。たとえば、ハンドラでオペレーションをロギングしたり、オブザーバーに通知したり、未定義を返す代わりに例外をスローしたり、オペレーションを別のターゲットにリダイレクトして保存したりできます。このような場合、プロキシを使用すると、ターゲット オブジェクトを使用する場合とは大きく異なる結果になる可能性があります。

function extend(sup,base) {

    var descriptor = Object.getOwnPropertyDescriptor(base.prototype,"constructor");

    base.prototype = Object.create(sup.prototype);

    var handler = {
    construct: function(target, args) {
        var obj = Object.create(base.prototype);
        this.apply(target,obj, args);
        return obj;
    },

    apply: function(target, that, args) {
        sup.apply(that,args);
        base.apply(that,args);
    }
    };

    var proxy = new Proxy(base, handler);
    descriptor.value = proxy;
    Object.defineProperty(base.prototype, "constructor", descriptor);
    return proxy;
}

var Vehicle = function(name){
    this.name = name;
};

var Car = extend(Vehicle, function(name, year) {
    this.year = year;
});

Car.prototype.style = "Saloon";

var Tesla = new Car("Model S", 2016);

console.log(Tesla.style); // "Saloon"
console.log(Tesla.name); // "Model S"
console.log(Tesla.year);  // 2016

アクセス制御

アクセス制御もプロキシのユースケースの一つです。信頼できないコードにターゲット オブジェクトを渡すのではなく、一種の保護膜に包まれたプロキシを渡すことができます。信頼できないコードが特定のタスクを完了したと判断されると、アプリは参照を取り消し、プロキシをターゲットから切断できます。メブランは、定義された元のターゲットから到達可能なすべてのオブジェクトに、この切断を再帰的に拡張します。

プロキシでリフレクションを使用する

Reflect は、インターセプト可能な JavaScript オペレーションのメソッドを提供する新しい組み込みオブジェクトです。プロキシの操作に非常に便利です。実際、Reflect メソッドはプロキシ ハンドラのメソッドと同じです。

Python や C# などの静的型付け言語では、長い間リフレクション API が提供されてきましたが、JavaScript は動的言語であるため、実際には必要ありませんでした。ES5 には、他の言語でリフレクションと見なされる Array.isArray()Object.getOwnPropertyDescriptor() など、すでに多くのリフレクション機能が含まれていると主張できます。ES2015 では、このカテゴリの今後のメソッドを格納する Reflection API が導入され、推論が容易になります。これは、Object がリフレクション メソッドのバッケットではなく、ベース プロトタイプであることを意味します。

Reflect を使用すると、前述の Superhero の例を改善して、get トラップと set トラップで適切なフィールド インターセプションを実現できます。

// Field interception with Proxy and the Reflect API

var pioneer = new Proxy({}, {
    get: function(target, name, receiver) {
        console.log(`get called for field: ${name}`);
        return Reflect.get(target, name, receiver);
    },

    set: function(target, name, value, receiver) {
        console.log(`set called for field: ${name} and value: ${value}`);
        return Reflect.set(target, name, value, receiver);
    }
});

pioneer.firstName = 'Grace';
pioneer.secondName = 'Hopper';
// Grace
pioneer.firstName

出力は次のようになります。

set called for field: firstName and value: Grace
set called for field: secondName and value: Hopper
get called for field: firstName

別の例として、次のような場合が挙げられます。

  • プロキシ定義をカスタム コンストラクタ内にラップすると、特定のロジックを操作するたびに新しいプロキシを手動で作成する必要がなくなります。

  • 変更を「保存」する機能を追加します(ただし、データが実際に変更された場合にのみ保存します。保存オペレーションが非常にコストが高いため、仮定しています)。

function Customer() {

    var proxy = new Proxy({
    save: function(){
        if (!this.dirty){
        return console.log('Not saving, object still clean');
        }
        console.log('Trying an expensive saving operation: ', this.changedProperties);
    },

    }, {

    set: function(target, name, value, receiver) {
        target.dirty = true;
        target.changedProperties = target.changedProperties || [];

        if(target.changedProperties.indexOf(name) == -1){
        target.changedProperties.push(name);
        }
        return Reflect.set(target, name, value, receiver);
    }

    });

    return proxy;
}


var customer = new Customer();

customer.name = 'seth';
customer.surname = 'thompson';
// Trying an expensive saving operation:  ["name", "surname"]
customer.save();

Reflect API のその他の例については、Tagtree の ES6 Proxies をご覧ください。

Object.observe() のポリフィル

Object.observe()廃止されますが、ES2015 プロキシを使用して polyfill できるようになりました。Simon Blackwell が最近、Proxy ベースの Object.observe() シムを作成しました。これは確認する価値があります。Erik Arvidsson は、2012 年にかなり仕様が完成したバージョンも作成しています。

ブラウザ サポート

ES2015 プロキシは、Chrome 49、Opera、Microsoft Edge、Firefox でサポートされています。Safari では、この機能に対する公開シグナルが賛否両論ですが、Google は引き続き楽観視しています。Reflect は Chrome、Opera、Firefox で利用可能で、Microsoft Edge では現在開発中です。

Google は、Proxy の限定的なポリフィルをリリースしました。これは、プロキシの作成時に既知のプロパティのみをプロキシできるため、汎用ラッパーにのみ使用できます。

関連情報