はじめに
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)。
参考資料
- "Proxies are awesome" Brendan Eich の JSConf でのプレゼンテーション (スライド)
- ECMAScript Harmony のプロキシ提案ページ と ECMAScript Harmony のプロキシ動作ページ
- プロキシチュートリアル
- 旧 Proxy API ページ
Object.watch()
は非標準の機能ですが、Gecko が長期間サポートしてきました。
ライセンスに関する注記
このページ内の一部のコンテンツ (テキストと例) は、CC 2.0 BY-NC-SA でコンテンツがライセンスされている ECMAScript wiki から引用あるいは参考としています。