はじめに

Proxy オブジェクトは、基本的な操作 (例えばプロパティの検索、代入、列挙、関数の起動など) について独自の動作を定義するために使用します。

用語

ハンドラ
トラップを含むプレースホルダオブジェクト。
トラップ
プロパティへのアクセスを提供するメソッド。これは OS におけるトラップのコンセプトに似たものです。
ターゲット
Proxy が仮想化するオブジェクト。たいていは Proxy のストレージバックエンドとして使用されます。オブジェクトの拡張や設定を禁止するプロパティに関する (変化していないという意味での) 不変条件は、このターゲットについて検証されます。

構文

var p = new Proxy(target, handler);

引数

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

メソッド

Proxy.revocable()
取り消し可能な Proxy オブジェクトを生成します。

handler オブジェクトのメソッド

handler オブジェクトは、Proxy のトラップを含むプレースホルダオブジェクトです。

すべてのトラップはオプションです。トラップが定義されていないなら、デフォルトの振る舞いはターゲットに操作を転送することです。

handler.getPrototypeOf()
Object.getPrototypeOfに対するトラップ
handler.setPrototypeOf()
Object.setPrototypeOfに対するトラップ
handler.isExtensible()
Object.isExtensibleに対するトラップ
handler.preventExtensions()
Object.preventExtensionsに対するトラップ
handler.getOwnPropertyDescriptor()
Object.getOwnPropertyDescriptorに対するトラップ
handler.defineProperty()
Object.definePropertyに対するトラップ
handler.has()
in 操作に対するトラップ
handler.get()
プロパティ値を取得するためのトラップ
handler.set()
プロパティ値を設定するためのトラップ
handler.deleteProperty()
delete 操作に対するトラップ
handler.enumerate()
for...in 構文に対するトラップ
handler.ownKeys()
Object.getOwnPropertyNamesに対するトラップ
handler.apply()
関数呼び出しに対するトラップ
handler.construct()
new 操作に対するトラップ

非標準のトラップは 非推奨で取り除かれました.

非常に簡単な例

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

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 を使うと、オブジェクトに渡された値を簡単に検証できます。この例では set ハンドラを使用しています。

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; // 例外が投げられる

コンストラクタを拡張する

関数の Proxy で、コンストラクタを新たなコンストラクタへ簡単に拡張できます。この例では construct および apply ハンドラを使用しています。

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 Person = function(name){
  this.name = name;
};

var Boy = extend(Person, function(name, age) {
  this.age = age;
});

Boy.prototype.sex = "M";

var Peter = new Boy("Peter", 13);
console.log(Peter.sex);  // "M"
console.log(Peter.name); // "Peter"
console.log(Peter.age);  // 13

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

完全な traps リストの例

traps リストの完全なサンプルを作成するため教育用に、そのような操作が特に適している非ネイティブオブジェクトを Proxy 化しましょう。document.cookie のページにある "リトルフレームワーク" で生成される docCookies グローバルオブジェクトです。

/*
  var docCookies = ... get the "docCookies" object here:  
  https://developer.mozilla.org/ja/docs/DOM/document.cookie#A_little_framework.3A_a_complete_cookies_reader.2Fwriter_with_full_unicode_support
*/

var docCookies = new Proxy(docCookies, {
  "get": function (oTarget, sKey) {
    return oTarget[sKey] || oTarget.getItem(sKey) || undefined;
  },
  "set": function (oTarget, sKey, vValue) {
    if (sKey in oTarget) { return false; }
    return oTarget.setItem(sKey, vValue);
  },
  "deleteProperty": function (oTarget, sKey) {
    if (sKey in oTarget) { return false; }
    return oTarget.removeItem(sKey);
  },
  "enumerate": function (oTarget, sKey) {
    return oTarget.keys();
  },
  "ownKeys": function (oTarget, sKey) {
    return oTarget.keys();
  },
  "has": function (oTarget, sKey) {
    return sKey in oTarget || oTarget.hasItem(sKey);
  },
  "defineProperty": function (oTarget, sKey, oDesc) {
    if (oDesc && "value" in oDesc) { oTarget.setItem(sKey, oDesc.value); }
    return oTarget;
  },
  "getOwnPropertyDescriptor": function (oTarget, sKey) {
    var vValue = oTarget.getItem(sKey);
    return vValue ? {
      "value": vValue,
      "writable": true,
      "enumerable": true,
      "configurable": false
    } : undefined;
  },
});

/* Cookies test */

console.log(docCookies.my_cookie1 = "First value");
console.log(docCookies.getItem("my_cookie1"));

docCookies.setItem("my_cookie1", "Changed value");
console.log(docCookies.my_cookie1);

仕様

仕様書 策定状況 コメント
ECMAScript 2015 (6th Edition, ECMA-262)
Proxy の定義
標準 最初期の定義
ECMAScript 2016 Draft (7th Edition, ECMA-262)
Proxy の定義
ドラフト  

ブラウザ実装状況

機能 Chrome Edge Firefox (Gecko) Internet Explorer Opera Safari
基本サポート 49.0 13 (10586) 18 (18) 未サポート ? ?
機能 Android Chrome for Android Firefox Mobile (Gecko) IE Mobile Opera Mobile Safari Mobile
基本サポート ? 49.0 18 (18) 13 (10586) ? ?

Gecko に関する注記

  • 現在、Object.getPrototypeOf(proxy) は無条件に Object.getPrototypeOf(target) を返します。これは、ES6 の getPrototypeOf トラップが未実装であるためです (バグ 888969バグ 888969)。
  • Array.isArray(proxy) は無条件に Array.isArray(target) を返します (バグ 1111785バグ 1111785)。
  • Object.prototype.toString.call(proxy) は無条件に Object.prototype.toString.call(target) を返します。これは ES6 の Symbol.toStringTag が未実装であるためです (バグ 1114580)。

参考資料

ライセンスに関する注記

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

ドキュメントのタグと貢献者

 このページの貢献者: yyss, teoli, ethertank, kohei.yoshino
 最終更新者: yyss,