Proxy リダイレクト 1

非標準

警告: SpiderMonkey の Proxy 実装は試作段階であり、Proxy API と動作仕様はまだ安定したものではありません。SpiderMonkey の実装が最新の仕様書ドラフトを反映していないこともありえます。これは実験的な機能として提供されており、いつでも変更の可能性があります。現在の実装に依存して本番環境用のコードを書くことはお避けください。

警告: このページは Firefox 18 で実装された (「ダイレクトプロキシ」と呼ばれる) 新しい API について解説しています。(Firefox 17 以下の) 従来の API については、旧 Proxy API ページを参照してください。

注: 翻訳記事のパスが /ja/docs/Core_JavaScript_1.5_Reference/Global_Objects/Proxy となっていますが、この Proxy API は JavaScript 1.5 の機能ではありません。これは MDN のバグ によるもので、本来のパスは /ja/docs/JavaScript/Reference/Global_Objects/Proxy です。

はじめに

プロキシとは、開発者自身が JavaScript で動作を定義するオブジェクトです。オブジェクトの既定動作は JavaScript エンジンに実装されており、たいていは C++ などの低レベル言語で記述されています。プロキシは、オブジェクトの挙動のほとんどを JavaScript で定義できるようにします。これは メタプログラミング API を提供するものと言われています。

用語

キャッチオール機構 (あるいは「仲裁 API」)
この機能の専門用語です。
プロキシ
アクセスを傍受するオブジェクト。
ハンドラ
トラップを含むプレースホルダオブジェクト。
トラップ
プロパティへのアクセスを提供するメソッド。これは OS におけるトラップのコンセプトに似たものです。
ターゲット
プロキシが仮想化するオブジェクト。たいていはプロキシのストレージバックエンドとして使用されます。オブジェクトの拡張や設定を禁止するプロパティに関する不変条件は、このターゲットについて検証されます。

プロキシ API

プロキシは新しいオブジェクトであり、既存のオブジェクトを「プロキシ化」することはできません。プロキシは以下のようにして作成できます。

var p = new Proxy(target, handler);

ここでは、

  • target はオブジェクトです (ネイティブの配列、関数、あるいは他のプロキシも含め、どのような種類のオブジェクトでも構いません)。
  • handler は、関数をプロパティとして持つオブジェクトで、その関数で、プロキシに対して操作が行われた場合の挙動を定義します。

ハンドラ API

トラップはすべてオプションです。トラップが定義されてない場合、ターゲットへ操作は既定の挙動で行われます。

JavaScript コード ハンドラメソッド 意味
Object.getOwnPropertyDescriptor(proxy, name) getOwnPropertyDescriptor
function(target, name) -> PropertyDescriptor | undefined
妥当なプロパティディスクリプタオブジェクトを返します。そのエミュレートされたオブジェクトに name という名前のプロパティが存在しない場合は undefined を返します。
Object.getOwnPropertyNames(proxy) getOwnPropertyNames function(target) -> [String] そのエミュレートされたオブジェクトの、(継承していない) 独自プロパティ名をすべて含んだ配列を返します。
Object.defineProperty(proxy,name,pd) defineProperty function(target, name, propertyDescriptor) -> any 与えられた propertyDescriptor によって属性が決定される新しいプロパティを定義します。このメソッドの戻り値は無視されます。
delete proxy.name deleteProperty function(target, name) -> boolean 指名されたプロパティをプロキシから削除します。このメソッドの戻り値は真偽値を取り、name プロパティが正しく削除されたかどうかを示します。
Object.freeze(proxy) freeze function(target) -> boolean オブジェクトを凍結します。真偽値は、その操作が成功したかどうかを示します。
Object.seal(proxy) seal function(target) -> boolean オブジェクトを封印します。真偽値は、その操作が成功したかどうかを示します。
Object.preventExtensions(proxy) preventExtensions function(target) -> boolean オブジェクトの拡張を禁止します。真偽値は、その操作が成功したかどうかを示します。
name in proxy has function(target, name) -> boolean  
Object.prototype.hasOwnProperty.call(proxy, name) hasOwn function(target, name) -> boolean  

proxy.name (「値を取得する」コンテキストにおいて)

receiver.name (receiver がプロキシから継承したもので、name を上書きしていない場合)

get function(target, name, receiver) -> any receiver はプロキシもしくはプロキシから継承したオブジェクトです。

proxy.name = val (「値を設定する」コンテキストにおいて)

receiver.name = val (receiver がプロキシから継承したもので、name を上書きしていない場合)

set function(target, name, val, receiver) -> boolean receiver はプロキシもしくはプロキシから継承したオブジェクトです。
for(prop in proxy){...} enumerate function(target) -> [String] プロキシを使う開発者の視点で見た場合、for..in ループ内で現れるプロパティの順番は、戻り値の配列と同じになります。for..in ループには、列挙トラップであるべきところ反復トラップが呼び出されるという 既知のバグ があります。
for(prop of proxy){...} iterate function(target) -> iterator  
Object.keys(proxy) keys function(target) -> [String]  
proxy.apply(thisValue, args) apply function(target, thisValue, args) -> any  
new proxy(...args) construct function(target, args) -> any  

不変条件

プロキシは多くの機能を開発者に与えてくれるものの、言語の一貫性を保つため、一部の操作はトラップに掛からないようになっています。

  • 二重あるいは三重イコール演算子 (=====) はトラップに掛かりません。p1 === p2 は、p1p2 が同じプロキシを参照している場合のみ真となります。
  • Object.getPrototypeOf(proxy) は無条件に Object.getPrototypeOf(target) を返します。
  • typeof proxy は無条件に typeof target を返します。
  • Object.prototype.toString.call(proxy) は無条件に Object.prototype.toString.call(target) を返します。

非常に簡単な例

このプロキシは、与えられたプロパティ名がオブジェクトに存在しない場合、既定値である 37 を返します。

var handler = {
    get: function(target, name){
        return name in target?
            target[name] :
            37;
    }
};

var p = new Proxy({}, handler);
p.a = 1;
p.b = undefined;

console.log(p.a, p.b); // 1, undefined
console.log('c' in p, p.c); // false, 37

何もしない転送プロキシ

この例では、プロキシが、それに対して適用されるすべての操作を転送する先に、ネイティブの JavaScript オブジェクトを使っています。

var target = {};
var p = new Proxy(target, {});

p.a = 37; // 操作はプロキシへ転送されます

console.log(target.a); // 37 が出力されます。操作は正しく転送されました

バリデーション

Proxy を使うと、オブジェクトに渡された値を簡単に検証できます。

let validator = {
  set: function(obj, prop, value) {
    if (prop === 'age') {
      if (!Number.isInteger(value)) {
        throw new TypeError('年齢が整数ではありません');
      }
      if (value > 200) {
        throw new RangeError('年齢が不正なようです');
      }
    }

    // 値を保存する既定の挙動
    obj[prop] = value;
  }
};

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

person.age = 100;
console.log(person.age); // 100
person.age = 'young'; // 例外が投げられる
person.age = 300; // 例外が投げられる

DOM ノードの操作

2 つの異なる要素の属性やクラス名を切り替えたい場合があります。それを実現する方法を紹介しましょう。

let view = new Proxy({
  selected: null
},
{
  set: function(obj, prop, newval) {
    let oldval = obj[prop];

    if (prop === 'selected') {
      if (oldval) {
        oldval.setAttribute('aria-selected', 'false');
      }
      if (newval) {
        newval.setAttribute('aria-selected', 'true');
      }
    }

    // 値を保存する既定の挙動
    obj[prop] = newval;
  }
});

let i1 = view.selected = document.getElementById('item-1');
console.log(i1.getAttribute('aria-selected')); // 'true'

let i2 = view.selected = document.getElementById('item-2');
console.log(i1.getAttribute('aria-selected')); // 'false'
console.log(i2.getAttribute('aria-selected')); // 'true'

値補正と追加プロパティ

この products プロキシオブジェクトは、渡された値を評価し、必要であれば配列に変換します。また、latestBrowser という追加プロパティをゲッターとセッターの両方でサポートしています。

let products = new Proxy({
  browsers: ['Internet Explorer', 'Netscape']
},
{
  get: function(obj, prop) {
    // 追加プロパティ
    if (prop === 'latestBrowser') {
      return obj.browsers[obj.browsers.length - 1];
    }

    // 値を返す既定の挙動
    return obj[prop];
  },
  set: function(obj, prop, value) {
    // 追加プロパティ
    if (prop === 'latestBrowser') {
      obj.browsers.push(value);
      return;
    }

    // 値が配列でなければ変換
    if (typeof value === 'string') {
      value = [value];
    }

    // 値を保存する既定の挙動
    obj[prop] = value;
  }
});

console.log(products.browsers); // ['Internet Explorer', 'Netscape']
products.browsers = 'Firefox'; // (間違えて) 文字列を渡す
console.log(products.browsers); // ['Firefox'] <- 問題ありません、値は配列になっています

products.latestBrowser = 'Chrome';
console.log(products.browsers); // ['Firefox', 'Chrome']
console.log(products.latestBrowser); // 'Chrome'

配列項目のオブジェクトをそのプロパティから検索

このプロキシは配列をいくつかの実用機能で拡張しています。見ての通り、Object.defineProperties を使わなくても柔軟にプロパティを「定義」できます。この例は、テーブルの列をそのセルから検索するようなコードに応用できます。その場合、ターゲットは table.rows となります。

let products = new Proxy([
  { name: 'Firefox', type: 'browser' },
  { name: 'SeaMonkey', type: 'browser' },
  { name: 'Thunderbird', type: 'mailer' }
],
{
  get: function(obj, prop) {
    // 値を返す既定の挙動、prop は通常整数値
    if (prop in obj) {
      return obj[prop];
    }

    // 製品の数を取得、products.length のエイリアス
    if (prop === 'number') {
      return obj.length;
    }

    let result, types = {};

    for (let product of obj) {
      if (product.name === prop) {
        result = product;
      }
      if (types[product.type]) {
        types[product.type].push(product);
      } else {
        types[product.type] = [product];
      }
    }

    // 製品を名前で取得
    if (result) {
      return result;
    }

    // 製品を種類で取得
    if (prop in types) {
      return types[prop];
    }

    // 製品の種類を取得
    if (prop === 'types') {
      return Object.keys(types);
    }

    return undefined;
  }
});

console.log(products[0]); // { name: 'Firefox', type: 'browser' }
console.log(products['Firefox']); // { name: 'Firefox', type: 'browser' }
console.log(products['Chrome']); // undefined
console.log(products.browser); // [{ name: 'Firefox', type: 'browser' }, { name: 'SeaMonkey', type: 'browser' }]
console.log(products.types); // ['browser', 'mailer']
console.log(products.number); // 3

参考資料

ライセンスに関する注記

このページ内の一部のコンテンツ (テキストと例) は、CC 2.0 BY-NC-SA でコンテンツがライセンスされている ECMAScript wiki から引用あるいは参考としています。

Document Tags and Contributors

Contributors to this page: ethertank
最終更新者: ethertank,