Proxy オブジェクトは、基本的な操作 (例えばプロパティの検索、代入、列挙、関数の起動など) について独自の動作を定義するために使用します。
用語
- ハンドラ
- トラップを含むプレースホルダオブジェクト。
- トラップ
- プロパティへのアクセスを提供するメソッド。これは OS におけるトラップのコンセプトに似たものです。
- ターゲット
- Proxy が仮想化するオブジェクト。たいていは Proxy のストレージバックエンドとして使用されます。オブジェクトの拡張や設定を禁止するプロパティに関する (変化していないという意味での) 不変条件は、このターゲットについて検証されます。
構文
var p = new Proxy(target, handler);
引数
targetProxyでラップするターゲットの(ネイティブ配列や関数、別のプロキシなどを含む、あらゆる種類の)オブジェクトです。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.ownKeys()Object.getOwnPropertyNamesとObject.getOwnPropertySymbolsに対するトラップです。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 が出力されます。操作は正しく転送されました
上記のコードはJavaScriptオブジェクトでは動作しますが、DOM要素などのネイティブブラウザオブジェクトでは動作しないことに注意してください。See this for one solution.
バリデーション
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;
// 値の保存が成功したことを返します。
return true;
}
};
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.gender = "M";
var Peter = new Boy("Peter", 13);
console.log(Peter.gender); // "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;
// 値の保存が成功したことを返します。
return true;
}
});
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;
// 値の保存が成功したことを返します。
return true;
}
});
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 の定義 |
ドラフト |
ブラウザ実装状況
| デスクトップ | モバイル | サーバー | |||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Proxy | Chrome 完全対応 49 | Edge 完全対応 12 | Firefox 完全対応 18 | IE 未対応 なし | Opera 完全対応 36 | Safari 完全対応 10 | WebView Android 完全対応 49 | Chrome Android 完全対応 49 | Firefox Android 完全対応 18 | Opera Android 完全対応 36 | Safari iOS 完全対応 10 | Samsung Internet Android 完全対応 5.0 | nodejs 完全対応 6.0.0 |
handler.apply | Chrome 完全対応 49 | Edge 完全対応 12 | Firefox 完全対応 18 | IE 未対応 なし | Opera 完全対応 36 | Safari 完全対応 10 | WebView Android 完全対応 49 | Chrome Android 完全対応 49 | Firefox Android 完全対応 18 | Opera Android 完全対応 36 | Safari iOS 完全対応 10 | Samsung Internet Android 完全対応 5.0 | nodejs 完全対応 6.0.0 |
handler.construct | Chrome 完全対応 49 | Edge 完全対応 12 | Firefox 完全対応 18 | IE 未対応 なし | Opera 完全対応 36 | Safari 完全対応 10 | WebView Android 完全対応 49 | Chrome Android 完全対応 49 | Firefox Android 完全対応 18 | Opera Android 完全対応 36 | Safari iOS 完全対応 10 | Samsung Internet Android 完全対応 5.0 | nodejs 完全対応 6.0.0 |
handler.defineProperty | Chrome 完全対応 49 | Edge 完全対応 12 | Firefox 完全対応 18 | IE 未対応 なし | Opera 完全対応 36 | Safari 完全対応 10 | WebView Android 完全対応 49 | Chrome Android 完全対応 49 | Firefox Android 完全対応 18 | Opera Android 完全対応 36 | Safari iOS 完全対応 10 | Samsung Internet Android 完全対応 5.0 | nodejs 完全対応 6.0.0 |
handler.deleteProperty | Chrome 完全対応 49 | Edge 完全対応 12 | Firefox 完全対応 18 | IE 未対応 なし | Opera 完全対応 36 | Safari 完全対応 10 | WebView Android 完全対応 49 | Chrome Android 完全対応 49 | Firefox Android 完全対応 18 | Opera Android 完全対応 36 | Safari iOS 完全対応 10 | Samsung Internet Android 完全対応 5.0 | nodejs 完全対応 6.0.0 |
handler.enumerate | Chrome 未対応 なし | Edge 未対応 なし | Firefox 未対応 37 — 47 | IE 未対応 なし | Opera 未対応 なし | Safari 未対応 なし | WebView Android 未対応 なし | Chrome Android 未対応 なし | Firefox Android 未対応 37 — 47 | Opera Android 未対応 なし | Safari iOS 未対応 なし | Samsung Internet Android 未対応 なし | nodejs 未対応 なし |
handler.get | Chrome 完全対応 49 | Edge 完全対応 12 | Firefox 完全対応 18 | IE 未対応 なし | Opera 完全対応 36 | Safari 完全対応 10 | WebView Android 完全対応 49 | Chrome Android 完全対応 49 | Firefox Android 完全対応 18 | Opera Android 完全対応 36 | Safari iOS 完全対応 10 | Samsung Internet Android 完全対応 5.0 | nodejs 完全対応 6.0.0 |
handler.getOwnPropertyDescriptor | Chrome 完全対応 49 | Edge 完全対応 12 | Firefox 完全対応 18 | IE 未対応 なし | Opera 完全対応 36 | Safari 完全対応 10 | WebView Android 完全対応 49 | Chrome Android 完全対応 49 | Firefox Android 完全対応 18 | Opera Android 完全対応 36 | Safari iOS 完全対応 10 | Samsung Internet Android 完全対応 5.0 | nodejs 完全対応 6.0.0 |
handler.getPrototypeOf | Chrome 完全対応 49 | Edge 未対応 なし | Firefox 完全対応 49 | IE 未対応 なし | Opera 完全対応 36 | Safari 未対応 なし | WebView Android 完全対応 49 | Chrome Android 完全対応 49 | Firefox Android 完全対応 49 | Opera Android 完全対応 36 | Safari iOS 未対応 なし | Samsung Internet Android 完全対応 5.0 | nodejs 完全対応 6.0.0 |
handler.has | Chrome 完全対応 49 | Edge 完全対応 12 | Firefox 完全対応 18 | IE 未対応 なし | Opera 完全対応 36 | Safari 完全対応 10 | WebView Android 完全対応 49 | Chrome Android 完全対応 49 | Firefox Android 完全対応 18 | Opera Android 完全対応 36 | Safari iOS 完全対応 10 | Samsung Internet Android 完全対応 5.0 | nodejs 完全対応 6.0.0 |
handler.isExtensible | Chrome 完全対応 49 | Edge 完全対応 12 | Firefox 完全対応 31 | IE 未対応 なし | Opera 完全対応 36 | Safari 完全対応 10 | WebView Android 完全対応 49 | Chrome Android 完全対応 49 | Firefox Android 完全対応 31 | Opera Android 完全対応 36 | Safari iOS 完全対応 10 | Samsung Internet Android 完全対応 5.0 | nodejs 完全対応 6.0.0 |
handler.ownKeys | Chrome 完全対応 49 | Edge 完全対応 12 | Firefox
完全対応
18
| IE 未対応 なし | Opera 完全対応 36 | Safari 完全対応 10 | WebView Android 完全対応 49 | Chrome Android 完全対応 49 | Firefox Android
完全対応
18
| Opera Android 完全対応 36 | Safari iOS 完全対応 10 | Samsung Internet Android 完全対応 5.0 | nodejs 完全対応 6.0.0 |
handler.preventExtensions | Chrome 完全対応 49 | Edge 完全対応 12 | Firefox 完全対応 22 | IE 未対応 なし | Opera 完全対応 36 | Safari 完全対応 10 | WebView Android 完全対応 49 | Chrome Android 完全対応 49 | Firefox Android 完全対応 22 | Opera Android 完全対応 36 | Safari iOS 完全対応 10 | Samsung Internet Android 完全対応 5.0 | nodejs 完全対応 6.0.0 |
handler.set | Chrome 完全対応 49 | Edge 完全対応 12 | Firefox 完全対応 18 | IE 未対応 なし | Opera 完全対応 36 | Safari 完全対応 10 | WebView Android 完全対応 49 | Chrome Android 完全対応 49 | Firefox Android 完全対応 18 | Opera Android 完全対応 36 | Safari iOS 完全対応 10 | Samsung Internet Android 完全対応 5.0 | nodejs 完全対応 6.0.0 |
handler.setPrototypeOf | Chrome 完全対応 49 | Edge 完全対応 12 | Firefox 完全対応 49 | IE 未対応 なし | Opera 完全対応 36 | Safari 完全対応 10 | WebView Android 完全対応 49 | Chrome Android 完全対応 49 | Firefox Android 完全対応 49 | Opera Android 完全対応 36 | Safari iOS 完全対応 10 | Samsung Internet Android 完全対応 5.0 | nodejs 完全対応 6.0.0 |
revocable | Chrome 完全対応 63 | Edge 完全対応 12 | Firefox 完全対応 34 | IE 未対応 なし | Opera 完全対応 あり | Safari 完全対応 10 | WebView Android 完全対応 63 | Chrome Android 完全対応 63 | Firefox Android 完全対応 34 | Opera Android 完全対応 あり | Safari iOS 完全対応 10 | Samsung Internet Android 完全対応 8.0 | nodejs 完全対応 6.0.0 |
凡例
- 完全対応
- 完全対応
- 未対応
- 未対応
- 非標準。ブラウザー間の互換性が低い可能性があります。
- 非標準。ブラウザー間の互換性が低い可能性があります。
- 非推奨。新しいウェブサイトでは使用しないでください。
- 非推奨。新しいウェブサイトでは使用しないでください。
- 実装ノートを参照してください。
- 実装ノートを参照してください。
参考資料
- "Proxies are awesome" Brendan Eich の JSConf でのプレゼンテーション (スライド)
- ECMAScript Harmony のプロキシ提案ページ と ECMAScript Harmony のプロキシ動作ページ
- プロキシチュートリアル
- 旧 Proxy API ページ
Object.watch()は非標準の機能ですが、Gecko が長期間サポートしてきました。
ライセンスに関する注記
このページ内の一部のコンテンツ (テキストと例) は、CC 2.0 BY-NC-SA でコンテンツがライセンスされている ECMAScript wiki から引用あるいは参考としています。