We're looking for a user researcher to understand the needs of developers and designers. Is this you or someone you know? Check out the post: https://mzl.la/2IGzdXS

この翻訳は不完全です。英語から この記事を翻訳 してください。

はじめに

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 2016 (ECMA-262)
Proxy の定義
標準
ECMAScript 2017 (ECMA-262)
Proxy の定義
標準
ECMAScript Latest Draft (ECMA-262)
Proxy の定義
ドラフト

ブラウザ実装状況

機能ChromeEdgeFirefoxInternet ExplorerOperaSafari
基本対応491218 なし3610
revocable あり あり34 なし あり10
handler.apply491218 なし3610
handler.construct491218 なし3610
handler.defineProperty491218 なし3610
handler.deleteProperty491218 なし3610
handler.enumerate なし なし37 — 47 なし なし なし
handler.get491218 なし3610
handler.getOwnPropertyDescriptor491218 なし3610
handler.getPrototypeOf なし なし49 なし なし なし
handler.has491218 なし3610
handler.isExtensible ? ?31 なし ? ?
handler.ownKeys4912181 なし3610
handler.preventExtensions491222 なし3610
handler.set491218 なし3610
handler.setPrototypeOf ? ?49 なし ? ?
機能Android webviewChrome for AndroidEdge mobileFirefox for AndroidOpera AndroidiOS SafariSamsung Internet
基本対応4949 あり1836105.0
revocable あり あり あり34 あり10 あり
handler.apply4949 あり1836105.0
handler.construct4949 あり1836105.0
handler.defineProperty4949 あり1836105.0
handler.deleteProperty4949 あり1836105.0
handler.enumerate なし なし なし37 — 47 なし なし なし
handler.get4949 あり1836105.0
handler.getOwnPropertyDescriptor4949 あり1836105.0
handler.getPrototypeOf なし なし なし49 なし なし なし
handler.has4949 あり1836105.0
handler.isExtensible ? ? ?31 ? ? ?
handler.ownKeys4949 あり18136105.0
handler.preventExtensions4949 あり2236105.0
handler.set4949 あり1836105.0
handler.setPrototypeOf ? ? ?49 ? ? ?

1. In Firefox 42, the implementation got updated to reflect the final ES2015 specification: The result is now checked if it is an array and if the array elements are either of type string or of type symbol. Enumerating duplicate own property names is not a failure anymore.

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 から引用あるいは参考としています。

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

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