はじめに

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

用語

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

構文

var p = new Proxy(target, handler);

引数

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

メソッド

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

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

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

非常に簡単な例

このプロキシは、与えられたプロパティ名がオブジェクトに存在しない場合、既定値である 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 2017 Draft (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) を返します。これは、ES2015 の getPrototypeOf トラップが未実装であるためです (バグ 888969バグ 888969)。
  • Array.isArray(proxy) は無条件に Array.isArray(target) を返します (バグ 1111785バグ 1111785)。
  • Object.prototype.toString.call(proxy) は無条件に Object.prototype.toString.call(target) を返します。これは ES62015 の Symbol.toStringTag が未実装であるためです (バグ 1114580)。

参考資料

ライセンスに関する注記

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

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

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