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