get
, set
)get
, set
)get
)set
)enumerate
トラップはどこにありますか?プロキシを使用すると、オブジェクトに対して実行される操作 (プロパティの取得など) をインターセプトしてカスタマイズできます。プロキシは*メタプログラミング*機能です。
次の例では
proxy
は空のオブジェクトです。
handler
は、特定のメソッドを実装することにより、proxy
に対して実行される操作をインターセプトできます。
ハンドラが操作をインターセプトしない場合、操作は target
に転送されます。
ここでは、1 つの操作 (get
(プロパティの取得)) のみをインターセプトしています。
const logged = [];
const target = {size: 0};
const handler = {
get(target, propKey, receiver) {
logged.push('GET ' + propKey);
return 123;
}
};
const proxy = new Proxy(target, handler);
プロパティ proxy.size
を取得すると、ハンドラはその操作をインターセプトします。
インターセプト可能な操作のリストについては、完全な API のリファレンスを参照してください。
プロキシとは何か、なぜ便利なのかを理解するには、まず*メタプログラミング*とは何かを理解する必要があります。
プログラミングにはレベルがあります。
基底レベルとメタレベルは異なる言語にすることができます。次のメタプログラムでは、メタプログラミング言語は JavaScript で、基底プログラミング言語は Java です。
メタプログラミングはさまざまな形式をとることができます。前の例では、Java コードをコンソールに出力しました。JavaScript をメタプログラミング言語と基底プログラミング言語の両方として使用してみましょう。この古典的な例は、JavaScript コードをオンザフライで評価/コンパイルできる eval()
関数 です。以下のインタラクションでは、式 5 + 2
を評価するために使用しています。
他の JavaScript 操作はメタプログラミングのように見えないかもしれませんが、よく見ると実際にはメタプログラミングです。
// Base level
const obj = {
hello() {
console.log('Hello!');
},
};
// Meta level
for (const key of Object.keys(obj)) {
console.log(key);
}
プログラムは実行中に独自の構造を調べています。これはメタプログラミングのように見えません。なぜなら、JavaScript ではプログラミング構成要素とデータ構造の区別があいまいだからです。すべての Object.*
メソッドはメタプログラミング機能と見なすことができます。
反射的メタプログラミングとは、プログラムが自身を処理することを意味します。Kiczales 他 [2] は、3 種類の反射的メタプログラミングを区別しています。
例を見てみましょう。
**例: イントロスペクション。** Object.keys()
はイントロスペクションを実行します (前の例を参照)。
**例: 自己変更。** 次の関数 moveProperty
は、プロパティをソースからターゲットに移動します。プロパティアクセス用のブラケット演算子、代入演算子、および delete
演算子を介して自己変更を実行します。(本番コードでは、おそらくこのタスクに プロパティ記述子 を使用します。)
function moveProperty(source, propertyName, target) {
target[propertyName] = source[propertyName];
delete source[propertyName];
}
moveProperty()
は次のように使用されます。
const obj1 = { color: 'blue' };
const obj2 = {};
moveProperty(obj1, 'color', obj2);
assert.deepEqual(
obj1, {});
assert.deepEqual(
obj2, { color: 'blue' });
ECMAScript 5 はインターセッションをサポートしていません。プロキシはそのギャップを埋めるために作成されました。
プロキシは JavaScript にインターセッションをもたらします。プロキシは次のように機能します。オブジェクト obj
に対して実行できる操作はたくさんあります。たとえば、
obj
のプロパティ prop
を取得する (obj.prop
)obj
にプロパティ prop
があるかどうかを確認する ('prop' in obj
)プロキシは、これらの操作の一部をカスタマイズできる特別なオブジェクトです。プロキシは 2 つのパラメータで作成されます。
handler
: 各操作に対応するハンドラメソッドがあり、存在する場合、その操作を実行します。このようなメソッドは操作を (ターゲットへの途中で) *インターセプト* し、*トラップ* と呼ばれます。これはオペレーティングシステムの分野から借用した用語です。target
: ハンドラが操作をインターセプトしない場合、操作はターゲットに対して実行されます。つまり、ハンドラのフォールバックとして機能します。ある意味で、プロキシはターゲットをラップします。注: 「インターセッション」の動詞形は「仲裁する」です。仲裁は本質的に双方向です。インターセプトは本質的に一方向です。
次の例では、ハンドラは操作 get
と has
をインターセプトします。
const logged = [];
const target = {};
const handler = {
/** Intercepts: getting properties */
get(target, propKey, receiver) {
logged.push(`GET ${propKey}`);
return 123;
},
/** Intercepts: checking whether properties exist */
has(target, propKey) {
logged.push(`HAS ${propKey}`);
return true;
}
};
const proxy = new Proxy(target, handler);
プロパティを取得する (行 A) か、in
演算子を使用する (行 B) 場合、ハンドラはこれらの操作をインターセプトします。
assert.equal(proxy.age, 123); // (A)
assert.equal('hello' in proxy, true); // (B)
assert.deepEqual(
logged, [
'GET age',
'HAS hello',
]);
ハンドラはトラップ set
(プロパティの設定) を実装していません。したがって、proxy.age
の設定は target
に転送され、target.age
が設定されます。
ターゲットが関数の場合、さらに 2 つの操作をインターセプトできます。
apply
: 関数呼び出しを行う。以下によってトリガーされます。proxy(···)
proxy.call(···)
proxy.apply(···)
construct
: コンストラクタ呼び出しを行う。以下によってトリガーされます。new proxy(···)
これらのトラップを関数ターゲットに対してのみ有効にする理由は簡単です。そうでなければ、操作 apply
と construct
を転送できません。
プロキシを介してメソッド呼び出しをインターセプトする場合、課題に直面します。メソッド呼び出し用のトラップはありません。代わりに、メソッド呼び出しは 2 つの操作のシーケンスとして表示されます。
get
apply
したがって、メソッド呼び出しをインターセプトする場合、2 つの操作をインターセプトする必要があります。
get
をインターセプトして関数を返します。次のコードは、その方法を示しています。
const traced = [];
function traceMethodCalls(obj) {
const handler = {
get(target, propKey, receiver) {
const origMethod = target[propKey];
return function (...args) { // implicit parameter `this`!
const result = origMethod.apply(this, args);
traced.push(propKey + JSON.stringify(args)
+ ' -> ' + JSON.stringify(result));
return result;
};
}
};
return new Proxy(obj, handler);
}
2 番目のインターセプトにはプロキシを使用していません。元のメソッドを関数でラップしているだけです。
次のオブジェクトを使用して traceMethodCalls()
を試してみましょう。
const obj = {
multiply(x, y) {
return x * y;
},
squared(x) {
return this.multiply(x, x);
},
};
const tracedObj = traceMethodCalls(obj);
assert.equal(
tracedObj.squared(9), 81);
assert.deepEqual(
traced, [
'multiply[9,9] -> 81',
'squared[9] -> 81',
]);
obj.squared()
内の this.multiply()
の呼び出しでさえトレースされます! これは、this
がプロキシを参照し続けているためです。
これは最も効率的な解決策ではありません。たとえば、メソッドをキャッシュすることができます。さらに、プロキシ自体がパフォーマンスに影響を与えます。
プロキシは*取消* (オフにする) ことができます。
関数 revoke
を初めて呼び出した後、proxy
に適用する操作はすべて TypeError
を発生させます。revoke
の後続の呼び出しは、それ以上の影響を与えません。
const target = {}; // Start with an empty object
const handler = {}; // Don’t intercept anything
const {proxy, revoke} = Proxy.revocable(target, handler);
// `proxy` works as if it were the object `target`:
proxy.city = 'Paris';
assert.equal(proxy.city, 'Paris');
revoke();
assert.throws(
() => proxy.prop,
/^TypeError: Cannot perform 'get' on a proxy that has been revoked$/
);
プロキシ proto
はオブジェクト obj
のプロトタイプになることができます。obj
で始まる一部の操作は proto
で続行される場合があります。そのような操作の 1 つは get
です。
const proto = new Proxy({}, {
get(target, propertyKey, receiver) {
console.log('GET '+propertyKey);
return target[propertyKey];
}
});
const obj = Object.create(proto);
obj.weight;
// Output:
// 'GET weight'
プロパティ weight
は obj
には見つからないため、検索は proto
で続行され、そこでトラップ get
がトリガーされます。プロトタイプに影響を与える操作は他にもあります。それらは本章の最後にリストされています。
ハンドラが実装していないトラップの操作は、自動的にターゲットに転送されます。操作を転送することに加えて、実行したいタスクがある場合があります。たとえば、操作をターゲットに到達させずに、すべての操作をインターセプトしてログに記録します。
const handler = {
deleteProperty(target, propKey) {
console.log('DELETE ' + propKey);
return delete target[propKey];
},
has(target, propKey) {
console.log('HAS ' + propKey);
return propKey in target;
},
// Other traps: similar
}
Reflect.*
の使用各トラップについて、まず操作の名前をログに記録し、手動で実行することで転送します。JavaScriptには、転送に役立つモジュールのようなオブジェクトReflect
があります。
各トラップについて
Reflect
にはメソッドがあります
Reflect
を使用すると、前の例は次のようになります。
const handler = {
deleteProperty(target, propKey) {
console.log('DELETE ' + propKey);
return Reflect.deleteProperty(target, propKey);
},
has(target, propKey) {
console.log('HAS ' + propKey);
return Reflect.has(target, propKey);
},
// Other traps: similar
}
各トラップの動作が非常に似ているため、Proxyを介してハンドラを実装できます。
const handler = new Proxy({}, {
get(target, trapName, receiver) {
// Return the handler method named trapName
return (...args) => {
console.log(trapName.toUpperCase() + ' ' + args[1]);
// Forward the operation
return Reflect[trapName](...args);
};
},
});
各トラップについて、Proxyはget
操作を介してハンドラメソッドを要求し、それを提供します。つまり、すべてのハンドラメソッドは、単一のメタメソッドget
を介して実装できます。この種の仮想化をシンプルにすることは、Proxy APIの目標の1つでした。
このProxyベースのハンドラを使用してみましょう
const target = {};
const proxy = new Proxy(target, handler);
proxy.distance = 450; // set
assert.equal(proxy.distance, 450); // get
// Was `set` operation correctly forwarded to `target`?
assert.equal(
target.distance, 450);
// Output:
// 'SET distance'
// 'GETOWNPROPERTYDESCRIPTOR distance'
// 'DEFINEPROPERTY distance'
// 'GET distance'
Proxyオブジェクトは、ターゲットオブジェクトで実行される操作をインターセプトするものと見なすことができます。Proxyはターゲットをラップします。Proxyのハンドラオブジェクトは、Proxyのオブザーバーまたはリスナーのようなものです。対応するメソッド(プロパティの読み取りの場合はget
など)を実装することにより、どの操作をインターセプトするかを指定します。操作のハンドラメソッドがない場合、その操作はインターセプトされません。単にターゲットに転送されます。
したがって、ハンドラが空のオブジェクトの場合、Proxyはターゲットを透過的にラップする必要があります。しかし、それは必ずしも機能するとは限りません。
this
に影響するさらに詳しく調べる前に、ターゲットをラップするとthis
がどのように影響を受けるかを簡単に確認しましょう
const target = {
myMethod() {
return {
thisIsTarget: this === target,
thisIsProxy: this === proxy,
};
}
};
const handler = {};
const proxy = new Proxy(target, handler);
target.myMethod()
を直接呼び出すと、this
はtarget
を指します
Proxyを介してそのメソッドを呼び出すと、this
はproxy
を指します
つまり、Proxyがメソッド呼び出しをターゲットに転送する場合、this
は変更されません。結果として、ターゲットがメソッド呼び出しを行うためにthis
を使用する場合など、Proxyはループ内に留まります。
通常、空のハンドラを持つProxyは、ターゲットを透過的にラップします。つまり、Proxyが存在することは気付かず、ターゲットの動作も変更されません。
ただし、ターゲットがProxyによって制御されないメカニズムを介してthis
に情報を関連付けている場合、問題が発生します。ターゲットがラップされているかどうかによって異なる情報が関連付けられるため、処理が失敗します。
たとえば、次のクラスPerson
は、プライベート情報をWeakMap _name
に格納します(この手法の詳細については、『JavaScript for impatient programmers』を参照してください)
const _name = new WeakMap();
class Person {
constructor(name) {
_name.set(this, name);
}
get name() {
return _name.get(this);
}
}
Person
のインスタンスは透過的にラップできません
const jane = new Person('Jane');
assert.equal(jane.name, 'Jane');
const proxy = new Proxy(jane, {});
assert.equal(proxy.name, undefined);
jane.name
は、ラップされたproxy.name
とは異なります。次の実装には、この問題はありません
class Person2 {
constructor(name) {
this._name = name;
}
get name() {
return this._name;
}
}
const jane = new Person2('Jane');
assert.equal(jane.name, 'Jane');
const proxy = new Proxy(jane, {});
assert.equal(proxy.name, 'Jane');
ほとんどの組み込みコンストラクターのインスタンスも、Proxyによってインターセプトされないメカニズムを使用します。したがって、それらも透過的にラップすることはできません。 Date
のインスタンスを使用すると、それがわかります
const target = new Date();
const handler = {};
const proxy = new Proxy(target, handler);
assert.throws(
() => proxy.getFullYear(),
/^TypeError: this is not a Date object\.$/
);
Proxyの影響を受けないメカニズムは、*内部スロット*と呼ばれます。これらのスロットは、インスタンスに関連付けられたプロパティのようなストレージです。仕様では、これらのスロットを角括弧で囲まれた名前を持つプロパティのように扱います。たとえば、次のメソッドは内部メソッドであり、すべてのオブジェクトO
で呼び出すことができます
プロパティとは対照的に、内部スロットへのアクセスは通常の「get」および「set」操作を介して行われません。 .getFullYear()
がProxyを介して呼び出された場合、this
に必要な内部スロットが見つからず、TypeError
を介してエラーを報告します。
Date
メソッドについては、言語仕様で次のように規定されています
明示的に定義されていない限り、以下に定義されているDateプロトタイプオブジェクトのメソッドは汎用ではなく、それらに渡される
this
値は、時刻値に初期化された[[DateValue]]
内部スロットを持つオブジェクトである必要があります。
回避策として、ハンドラがメソッド呼び出しを転送する方法を変更し、this
をProxyではなくターゲットに選択的に設定できます
const handler = {
get(target, propKey, receiver) {
if (propKey === 'getFullYear') {
return target.getFullYear.bind(target);
}
return Reflect.get(target, propKey, receiver);
},
};
const proxy = new Proxy(new Date('2030-12-24'), handler);
assert.equal(proxy.getFullYear(), 2030);
このアプローチの欠点は、メソッドがthis
で実行する操作のいずれもProxyを通過しないことです。
他の組み込みとは対照的に、配列は透過的にラップできます
const p = new Proxy(new Array(), {});
p.push('a');
assert.equal(p.length, 1);
p.length = 0;
assert.equal(p.length, 0);
配列がラップ可能である理由は、プロパティアクセスが.length
を機能させるようにカスタマイズされていても、配列メソッドは内部スロットに依存しないため、汎用であるためです。
このセクションでは、Proxyの用途を示します。これにより、APIの動作を確認する機会が得られます。
get
、set
)配列propKeys
にあるキーを持つobj
のプロパティが設定または取得されるたびにログを記録する関数tracePropertyAccesses(obj, propKeys)
があるとします。次のコードでは、その関数をクラスPoint
のインスタンスに適用します
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return `Point(${this.x}, ${this.y})`;
}
}
// Trace accesses to properties `x` and `y`
const point = new Point(5, 7);
const tracedPoint = tracePropertyAccesses(point, ['x', 'y']);
トレースされたオブジェクトp
のプロパティの取得と設定には、次の効果があります
興味深いことに、this
がPoint
のインスタンスではなく、トレースされたオブジェクトを参照するため、Point
がプロパティにアクセスするたびにトレースも機能します
tracePropertyAccesses()
を実装するProxyがなければ、tracePropertyAccesses()
は次のように実装します。各プロパティを、アクセスをトレースするゲッターとセッターに置き換えます。セッターとゲッターは、追加のオブジェクトpropData
を使用してプロパティのデータを格納します。元の implementation を破壊的に変更していることに注意してください。これは、メタプログラミングを行っていることを意味します。
function tracePropertyAccesses(obj, propKeys, log=console.log) {
// Store the property data here
const propData = Object.create(null);
// Replace each property with a getter and a setter
propKeys.forEach(function (propKey) {
propData[propKey] = obj[propKey];
Object.defineProperty(obj, propKey, {
get: function () {
log('GET '+propKey);
return propData[propKey];
},
set: function (value) {
log('SET '+propKey+'='+value);
propData[propKey] = value;
},
});
});
return obj;
}
パラメータlog
を使用すると、この関数を簡単にユニットテストできます
const obj = {};
const logged = [];
tracePropertyAccesses(obj, ['a', 'b'], x => logged.push(x));
obj.a = 1;
assert.equal(obj.a, 1);
obj.c = 3;
assert.equal(obj.c, 3);
assert.deepEqual(
logged, [
'SET a=1',
'GET a',
]);
tracePropertyAccesses()
を実装するProxyを使用すると、より簡単なソリューションが得られます。プロパティの取得と設定をインターセプトし、実装を変更する必要はありません。
function tracePropertyAccesses(obj, propKeys, log=console.log) {
const propKeySet = new Set(propKeys);
return new Proxy(obj, {
get(target, propKey, receiver) {
if (propKeySet.has(propKey)) {
log('GET '+propKey);
}
return Reflect.get(target, propKey, receiver);
},
set(target, propKey, value, receiver) {
if (propKeySet.has(propKey)) {
log('SET '+propKey+'='+value);
}
return Reflect.set(target, propKey, value, receiver);
},
});
}
get
、set
)プロパティへのアクセスに関しては、JavaScriptは非常に寛容です。たとえば、プロパティを読み取ろうとして名前のスペルを間違えた場合、例外は発生せず、結果undefined
が返されます。
Proxyを使用して、そのような場合に例外を発生させることができます。これは次のように機能します。Proxyをオブジェクトのプロトタイプにします。オブジェクトにプロパティが見つからない場合、Proxyのget
トラップがトリガーされます
get
操作をターゲットに転送することで行います(Proxyはターゲットからプロトタイプを取得します)。これは、このアプローチの実装です
const propertyCheckerHandler = {
get(target, propKey, receiver) {
// Only check string property keys
if (typeof propKey === 'string' && !(propKey in target)) {
throw new ReferenceError('Unknown property: ' + propKey);
}
return Reflect.get(target, propKey, receiver);
}
};
const PropertyChecker = new Proxy({}, propertyCheckerHandler);
オブジェクトにPropertyChecker
を使用してみましょう
const jane = {
__proto__: PropertyChecker,
name: 'Jane',
};
// Own property:
assert.equal(
jane.name,
'Jane');
// Typo:
assert.throws(
() => jane.nmae,
/^ReferenceError: Unknown property: nmae$/);
// Inherited property:
assert.equal(
jane.toString(),
'[object Object]');
PropertyChecker
PropertyChecker
をコンストラクターに変換すると、extends
を介してクラスに使用できます
// We can’t change .prototype of classes, so we are using a function
function PropertyChecker2() {}
PropertyChecker2.prototype = new Proxy({}, propertyCheckerHandler);
class Point extends PropertyChecker2 {
constructor(x, y) {
super();
this.x = x;
this.y = y;
}
}
const point = new Point(5, 7);
assert.equal(point.x, 5);
assert.throws(
() => point.z,
/^ReferenceError: Unknown property: z/);
これはpoint
のプロトタイプチェーンです
const p = Object.getPrototypeOf.bind(Object);
assert.equal(p(point), Point.prototype);
assert.equal(p(p(point)), PropertyChecker2.prototype);
assert.equal(p(p(p(point))), Object.prototype);
プロパティを誤って*作成*してしまうことを心配している場合は、2つの選択肢があります
set
をトラップするオブジェクトの周りにProxyをラップすることができます。Object.preventExtensions(obj)
を介してオブジェクトobj
を拡張不可にすることができます。これは、JavaScriptで新しい(独自の)プロパティをobj
に追加できないことを意味します。get
)一部の配列メソッドでは、-1
を介して最後の要素、-2
を介して最後から2番目の要素などを参照できます。例えば
残念ながら、ブラケット演算子([]
)を介して要素にアクセスする場合、これは機能しません。ただし、Proxyを使用してその機能を追加できます。次の関数createArray()
は、負のインデックスをサポートする配列を作成します。これは、配列インスタンスの周りにProxyをラップすることによって行います。Proxyは、ブラケット演算子によってトリガーされるget
操作をインターセプトします。
function createArray(...elements) {
const handler = {
get(target, propKey, receiver) {
if (typeof propKey === 'string') {
const index = Number(propKey);
if (index < 0) {
propKey = String(target.length + index);
}
}
return Reflect.get(target, propKey, receiver);
}
};
// Wrap a proxy around the Array
return new Proxy(elements, handler);
}
const arr = createArray('a', 'b', 'c');
assert.equal(
arr[-1], 'c');
assert.equal(
arr[0], 'a');
assert.equal(
arr.length, 3);
set
)データバインディングとは、オブジェクト間のデータを同期することです。一般的なユースケースの1つは、MVC(Model View Controller)パターンに基づくウィジェットです。データバインディングを使用すると、*モデル*(ウィジェットによって視覚化されるデータ)を変更すると、*ビュー*(ウィジェット)が最新の状態に保たれます。
データバインディングを実装するには、オブジェクトに加えられた変更を観察して対応する必要があります。次のコードスニペットは、配列の変更の観察がどのように機能するかを示すスケッチです。
function createObservedArray(callback) {
const array = [];
return new Proxy(array, {
set(target, propertyKey, value, receiver) {
callback(propertyKey, value);
return Reflect.set(target, propertyKey, value, receiver);
}
});
}
const observedArray = createObservedArray(
(key, value) => console.log(
`${JSON.stringify(key)} = ${JSON.stringify(value)}`));
observedArray.push('a');
// Output:
// '"0" = "a"'
// '"length" = 1'
Proxyを使用して、任意のメソッドを呼び出すことができるオブジェクトを作成できます。次の例では、関数createWebService()
は、そのようなオブジェクトの1つであるservice
を作成します。 service
でメソッドを呼び出すと、同じ名前のWebサー
const service = createWebService('http://example.com/data');
// Read JSON data in http://example.com/data/employees
service.employees().then((jsonStr) => {
const employees = JSON.parse(jsonStr);
// ···
});
次のコードは、ProxyなしでcreateWebService
を迅速かつ大雑把に実装したものです。 service
でどのメソッドが呼び出されるかを事前に知る必要があります。パラメータpropKeys
は、その情報を提供します。メソッド名を含む配列を保持します。
function createWebService(baseUrl, propKeys) {
const service = {};
for (const propKey of propKeys) {
service[propKey] = () => {
return httpGet(baseUrl + '/' + propKey);
};
}
return service;
}
Proxyを使用すると、createWebService()
はよりシンプルになります
function createWebService(baseUrl) {
return new Proxy({}, {
get(target, propKey, receiver) {
// Return the method to be called
return () => httpGet(baseUrl + '/' + propKey);
}
});
}
どちらの実装も、HTTP GETリクエストを行うために次の関数を使用します(その仕組みについては、『JavaScript for impatient programmers』で説明されています)。
function httpGet(url) {
return new Promise(
(resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.onload = () => {
if (xhr.status === 200) {
resolve(xhr.responseText); // (A)
} else {
// Something went wrong (404, etc.)
reject(new Error(xhr.statusText)); // (B)
}
}
xhr.onerror = () => {
reject(new Error('Network error')); // (C)
};
xhr.open('GET', url);
xhr.send();
});
}
*失効可能な参照*は次のように機能します。クライアントは、重要なリソース(オブジェクト)に直接アクセスすることは許可されておらず、参照(中間オブジェクト、リソースのラッパー)を介してのみアクセスできます。通常、参照に適用されるすべての操作はリソースに転送されます。クライアントが完了した後、リソースは参照を*失効*させること、つまりスイッチをオフにすることによって保護されます。以降、参照に操作を適用すると例外がスローされ、何も転送されなくなります。
次の例では、リソースの失効可能な参照を作成します。次に、参照を介してリソースのプロパティの1つを読み取ります。参照がアクセスを許可するため、これは機能します。次に、参照を失効させます。これで、参照はプロパティを読み取ることができなくなります。
const resource = { x: 11, y: 8 };
const {reference, revoke} = createRevocableReference(resource);
// Access granted
assert.equal(reference.x, 11);
revoke();
// Access denied
assert.throws(
() => reference.x,
/^TypeError: Cannot perform 'get' on a proxy that has been revoked/
);
Proxyは、操作をインターセプトして転送できるため、失効可能な参照の実装に最適です。これは、ProxyベースのcreateRevocableReference
の簡単な実装です
function createRevocableReference(target) {
let enabled = true;
return {
reference: new Proxy(target, {
get(target, propKey, receiver) {
if (!enabled) {
throw new TypeError(
`Cannot perform 'get' on a proxy that has been revoked`);
}
return Reflect.get(target, propKey, receiver);
},
has(target, propKey) {
if (!enabled) {
throw new TypeError(
`Cannot perform 'has' on a proxy that has been revoked`);
}
return Reflect.has(target, propKey);
},
// (Remaining methods omitted)
}),
revoke: () => {
enabled = false;
},
};
}
前のセクションのProxy-as-handler手法を使用すると、コードを簡略化できます。今回は、ハンドラは基本的にReflect
オブジェクトです。したがって、get
トラップは通常、適切なReflect
メソッドを返します。参照が失効している場合は、代わりにTypeError
がスローされます。
function createRevocableReference(target) {
let enabled = true;
const handler = new Proxy({}, {
get(_handlerTarget, trapName, receiver) {
if (!enabled) {
throw new TypeError(
`Cannot perform '${trapName}' on a proxy`
+ ` that has been revoked`);
}
return Reflect[trapName];
}
});
return {
reference: new Proxy(target, handler),
revoke: () => {
enabled = false;
},
};
}
ただし、Proxyは失効可能であるため、失効可能な参照を自分で実装する必要はありません。今回は、失効はハンドラではなくProxyで行われます。ハンドラが行う必要があるのは、すべての操作をターゲットに転送することだけです。ハンドラがトラップを実装していない場合、それが自動的に行われることはすでに見てきました。
function createRevocableReference(target) {
const handler = {}; // forward everything
const { proxy, revoke } = Proxy.revocable(target, handler);
return { reference: proxy, revoke };
}
*メンブレン*は、失効可能な参照のアイデアに基づいて構築されています。信頼できないコードを安全に実行するためのライブラリは、そのコードの周りにメンブレンをラップして分離し、システムの残りの部分を安全に保ちます。オブジェクトはメンブレンを双方向に通過します
どちらの場合も、取り消し可能な参照がオブジェクトの周りにラップされます。ラップされた関数またはメソッドによって返されるオブジェクトもラップされます。さらに、ラップされたウェットオブジェクトがメンブレンに渡されると、ラップが解除されます。
信頼できないコードの実行が完了すると、すべての取り消し可能な参照が取り消されます。その結果、外部にあるコードは実行できなくなり、参照している外部オブジェクトも機能しなくなります。Caja Compiler は、「Webサイトに埋め込むサードパーティのHTML、CSS、JavaScriptを安全にするためのツール」です。この目標を達成するためにメンブレンを使用します。
ブラウザのドキュメントオブジェクトモデル(DOM)は、通常、JavaScriptとC ++の組み合わせとして実装されます。純粋なJavaScriptで実装すると、以下のことが可能です。
残念ながら、標準のDOMはJavaScriptでは簡単に複製できないことができます。たとえば、ほとんどのDOMコレクションは、DOMが変更されるたびに動的に変化するDOMの現在の状態のライブビューです。そのため、DOMの純粋なJavaScript実装はあまり効率的ではありません。JavaScriptにProxyを追加した理由の1つは、より効率的なDOM実装を可能にするためです。
Proxyには、他にもユースケースがあります。例えば
リモート処理:ローカルのプレースホルダーオブジェクトは、メソッド呼び出しをリモートオブジェクトに転送します。このユースケースは、Webサービスの例に似ています。
データベースのデータアクセスオブジェクト:オブジェクトの読み取りと書き込みは、データベースの読み取りと書き込みを行います。このユースケースは、Webサービスの例に似ています。
プロファイリング:メソッドの呼び出しをインターセプトして、各メソッドで費やされた時間を追跡します。このユースケースは、トレースの例に似ています。
Immer(Michel Weststrate作) は、データを非破壊的に更新するのに役立ちます。適用する変更は、メソッドの呼び出し、プロパティの設定、配列要素の設定など、(潜在的にネストされた)*ドラフト状態*で行います。ドラフト状態はProxyを介して実装されます。
MobX を使用すると、オブジェクト、配列、クラスインスタンスなどのデータ構造に対する変更を観察できます。これはProxyを介して実装されます。
Alpine.js(Caleb Porzio作) は、Proxyを介してデータバインディングを実装するフロントエンドライブラリです。
on-change(Sindre Sorhus作) は、オブジェクトの変更を(Proxyを介して)監視し、報告します。
Envユーティリティ(Nicholas C. Zakas作) を使用すると、プロパティを介して環境変数にアクセスでき、存在しない場合は例外がスローされます。これはProxyを介して実装されます。
LDflex(Ruben VerborghとRuben Taelman作) は、リンクトデータ(セマンティックWebと考えてください)のクエリ言語を提供します。流動的なクエリAPIは、Proxyを介して実装されます。
このセクションでは、Proxyの仕組みとその理由について詳しく説明します。
Firefoxは、しばらくの間、限定的なメタプログラミングの介入をサポートしていました。オブジェクト `O` に `__noSuchMethod__` という名前のメソッドがある場合、存在しないメソッドが `O` で呼び出されるたびに通知されました。次のコードは、その仕組みを示しています
const calc = {
__noSuchMethod__: function (methodName, args) {
switch (methodName) {
case 'plus':
return args.reduce((a, b) => a + b);
case 'times':
return args.reduce((a, b) => a * b);
default:
throw new TypeError('Unsupported: ' + methodName);
}
}
};
// All of the following method calls are implemented via
// .__noSuchMethod__().
assert.equal(
calc.plus(3, 5, 2), 10);
assert.equal(
calc.times(2, 3, 4), 24);
assert.equal(
calc.plus('Parts', ' of ', 'a', ' string'),
'Parts of a string');
したがって、`__noSuchMethod__` はProxyトラップと同様に機能します。Proxyとは対照的に、トラップは、操作をインターセプトするオブジェクトの所有メソッドまたは継承メソッドです。このアプローチの問題点は、基本レベル(通常のメソッド)とメタレベル(`__noSuchMethod__`)が混在していることです。基本レベルのコードが誤ってメタレベルのメソッドを呼び出したり、見たりすることがあり、誤ってメタレベルのメソッドを定義する可能性があります。
標準のECMAScriptでも、基本レベルとメタレベルが混在している場合があります。たとえば、次のメタプログラミングメカニズムは、基本レベルに存在するため、失敗する可能性があります
`obj.hasOwnProperty(propKey)`:プロトタイプチェーンのプロパティが組み込みの実装をオーバーライドする場合、この呼び出しは失敗する可能性があります。たとえば、次のコードでは、`obj` が原因でエラーが発生します
const obj = { hasOwnProperty: null };
assert.throws(
() => obj.hasOwnProperty('width'),
/^TypeError: obj.hasOwnProperty is not a function/
);
これらは、`.hasOwnProperty()` を呼び出す安全な方法です
`func.call(···)`、`func.apply(···)`:どちらのメソッドも、問題と解決策は `.hasOwnProperty()` と同じです。
`obj.__proto__`:プレーンオブジェクトでは、`__proto__` は、レシーバーのプロトタイプを取得および設定できる特別なプロパティです。したがって、プレーンオブジェクトを辞書として使用する場合、プロパティキーとして `__proto__` を使用しないようにする必要があります。
これで、(基本レベルの)プロパティキーを特別なものにすることは問題があることが明らかになったはずです。したがって、Proxyは*階層化*されています。基本レベル(Proxyオブジェクト)とメタレベル(ハンドラーオブジェクト)は分離されています。
Proxyは、2つの役割で使用されます
*ラッパー*として、ターゲットを*ラップ*し、ターゲットへのアクセスを制御します。ラッパーの例としては、取り消し可能なリソースとProxyを介したトレースがあります。
*仮想オブジェクト*として、特別な動作を持つオブジェクトであり、ターゲットは重要ではありません。例として、メソッド呼び出しをリモートオブジェクトに転送するProxyがあります。
Proxy APIの初期の設計では、Proxyは純粋に仮想オブジェクトとして考えられていました。ただし、その役割でも、ターゲットは、不変条件(後で説明します)を適用するため、およびハンドラーが実装していないトラップのフォールバックとして役立つことがわかりました。
Proxyは、2つの方法で保護されています
どちらの原則も、Proxyに他のオブジェクトを偽装するためのかなりの力を与えます。*不変条件*を適用する理由の1つ(後で説明します)は、その力を抑制することです。
Proxyと非Proxyを区別する方法が必要な場合は、自分で実装する必要があります。次のコードは、2つの関数をエクスポートするモジュール `lib.mjs` です。1つはProxyを作成し、もう1つはオブジェクトがそれらのProxyの1つであるかどうかを判断します。
// lib.mjs
const proxies = new WeakSet();
export function createProxy(obj) {
const handler = {};
const proxy = new Proxy(obj, handler);
proxies.add(proxy);
return proxy;
}
export function isProxy(obj) {
return proxies.has(obj);
}
このモジュールは、Proxyを追跡するためにデータ構造 `WeakSet` を使用します。`WeakSet` は、要素がガベージコレクションされるのを妨げないため、この目的に最適です。
次の例は、`lib.mjs` の使用方法を示しています。
// main.mjs
import { createProxy, isProxy } from './lib.mjs';
const proxy = createProxy({});
assert.equal(isProxy(proxy), true);
assert.equal(isProxy({}), false);
このセクションでは、JavaScriptの内部構造とProxyトラップのセットの選択方法について詳しく説明します。
プログラミング言語とAPI設計のコンテキストでは、*プロトコル*は、インターフェイスのセットとそれらを使用するためのルールです。ECMAScript仕様では、JavaScriptコードの実行方法について説明しています。これには、オブジェクトを処理するためのプロトコルが含まれています。このプロトコルはメタレベルで動作し、*メタオブジェクトプロトコル*(MOP)と呼ばれることもあります。JavaScript MOPは、すべてのオブジェクトが持つ独自の内部メソッドで構成されます。「内部」とは、仕様にのみ存在し(JavaScriptエンジンがそれらを持っている場合と持っていない場合があります)、JavaScriptからアクセスできないことを意味します。内部メソッドの名前は、二重角かっこで囲んで記述されます。
プロパティを取得するための内部メソッドは、`.[[Get]]()`と呼ばれます。二重角かっこの代わりに二重下線を使用すると、このメソッドはJavaScriptで大体次のように実装されます。
// Method definition
__Get__(propKey, receiver) {
const desc = this.__GetOwnProperty__(propKey);
if (desc === undefined) {
const parent = this.__GetPrototypeOf__();
if (parent === null) return undefined;
return parent.__Get__(propKey, receiver); // (A)
}
if ('value' in desc) {
return desc.value;
}
const getter = desc.get;
if (getter === undefined) return undefined;
return getter.__Call__(receiver, []);
}
このコードで呼び出されるMOPメソッドは次のとおりです。
行Aでは、プロパティが「以前の」オブジェクトで見つからない場合、プロトタイプチェーンのProxyが `get` についてどのようにして पता लगाता है かを確認できます。キーが `propKey` である独自のプロパティがない場合、検索は `this` のプロトタイプ `parent` で続行されます。
**基本操作と派生操作。** `.[[Get]]()` が他のMOP操作を呼び出すことがわかります。これを行う操作は*派生*と呼ばれます。他の操作に依存しない操作は*基本*と呼ばれます。
Proxyのメタオブジェクトプロトコルは、通常のオブジェクトのプロトコルとは異なります。通常のオブジェクトの場合、派生操作は他の操作を呼び出します。Proxyの場合、各操作(基本操作か派生操作かに関係なく)は、ハンドラーメソッドによってインターセプトされるか、ターゲットに転送されます。
どの操作をProxyを介してインターセプトできるようにする必要がありますか?
後者を行うことの利点は、パフォーマンスが向上し、より便利になることです。たとえば、`get` のトラップがなければ、`getOwnPropertyDescriptor` を介してその機能を実装する必要があります。
派生トラップを含めることの欠点は、Proxyの動作に矛盾が生じる可能性があることです。たとえば、`get` は、`getOwnPropertyDescriptor` によって返される記述子の値とは異なる値を返す場合があります。
Proxyによるインターセプトは*選択的*です。すべての言語操作をインターセプトすることはできません。一部の操作が除外されたのはなぜですか?2つの理由を見てみましょう。
まず、*安定した*操作はインターセプトには適していません。操作は、同じ引数に対して常に同じ結果を生成する場合、*安定*しています。Proxyが安定した操作をトラップできる場合、不安定になり、信頼性が低下する可能性があります。厳密な等価性(`===`)は、そのような安定した操作の1つです。トラップすることはできず、結果はProxy自体を別のオブジェクトとして扱うことによって計算されます。安定性を維持する別の方法は、Proxyではなくターゲットに操作を適用することです。後で説明するように、Proxyの不変条件がどのように適用されるかを見ると、ターゲットが拡張不可能なProxyに `Object.getPrototypeOf()` が適用された場合にこれが発生します。
より多くの操作をインターセプト可能にしない2番目の理由は、インターセプトは通常不可能な状況でカスタムコードを実行することを意味するためです。このコードのインターリーブが多ければ多いほど、プログラムの理解とデバッグが難しくなります。また、パフォーマンスにも悪影響を及ぼします。
Proxyを介して仮想メソッドを作成する場合、`get` トラップから関数を返す必要があります。ここで疑問が生じます。メソッド呼び出しの追加トラップ(例:`invoke`)を導入しないのはなぜですか?これにより、次のことを区別できます。
そうしない理由は2つあります。
まず、すべての実装が `get` と `invoke` を区別しているわけではありません。たとえば、AppleのJavaScriptCoreは区別していません。
次に、メソッドを抽出して `.call()` または `.apply()` を介して後で呼び出すと、ディスパッチを介してメソッドを呼び出すのと同じ効果が得られるはずです。つまり、次の2つのバリアントは同等に機能するはずです。追加のトラップ `invoke` がある場合、その等価性を維持することはより困難になります。
// Variant 1: call via dynamic dispatch
const result1 = obj.m();
// Variant 2: extract and call directly
const m = obj.m;
const result2 = m.call(obj);
`get` と `invoke` を区別できる場合にのみ実行できることがいくつかあります。したがって、これらのことは、現在のProxy APIでは不可能です。2つの例は、自動バインディングと欠落しているメソッドのインターセプトです。Proxyが `invoke` をサポートしている場合、どのように実装するかを見てみましょう。
自動バインディング。 オブジェクト obj
のプロトタイプを Proxy にすることで、メソッドを自動的にバインドできます。
obj.m
を介してメソッド m
の値を取得すると、this
が obj
にバインドされた関数が返されます。obj.m()
はメソッド呼び出しを実行します。自動バインディングは、メソッドをコールバックとして使用する場合に役立ちます。たとえば、前の例の変形 2 はよりシンプルになります。
存在しないメソッドのインターセプト。 invoke
を使用すると、Proxy は前述の __noSuchMethod__
メカニズムをエミュレートできます。 Proxy は再びオブジェクト obj
のプロトタイプになります。不明なプロパティ prop
へのアクセス方法に応じて、異なる反応を示します。
obj.prop
を介してそのプロパティを読み取る場合、インターセプトは発生せず、undefined
が返されます。obj.prop()
を行うと、Proxy はインターセプトし、たとえばコールバックに通知します。不変条件とは何か、そして Proxy に対してどのように強制されるかを見ていく前に、オブジェクトを拡張不可と設定不可によってどのように保護できるかを復習しましょう。
オブジェクトを保護するには2つの方法があります。
拡張不可はオブジェクトを保護します。オブジェクトが拡張不可の場合、プロパティを追加したり、プロトタイプを変更したりすることはできません。
設定不可はプロパティ(またはむしろその属性)を保護します。
writable
は、プロパティの値を変更できるかどうかを制御します。configurable
は、プロパティの属性を変更できるかどうかを制御します。このトピックの詳細については、§10「オブジェクトの変更からの保護」を参照してください。
従来、拡張不可と設定不可は
言語操作に直面しても変化しないこれらの特性やその他の特性は、*不変条件*と呼ばれます。 Proxy は拡張不可などに本質的に束縛されていないため、Proxy を介して不変条件に違反するのは簡単です。 Proxy API は、ターゲットオブジェクトとハンドラーメソッドの結果をチェックすることにより、それが発生するのを防ぎます。
次の2つのサブセクションでは、4つの不変条件について説明します。不変条件の完全なリストは、この章の最後に示されています。
次の2つの不変条件は、拡張不可と設定不可に関係しています。これらは、ターゲットオブジェクトをブックキーピングに使用することで強制されます。ハンドラーメソッドによって返される結果は、ターゲットオブジェクトとほぼ同期している必要があります。
Object.preventExtensions(obj)
が true
を返す場合、将来のすべての呼び出しは false
を返し、obj
は拡張不可でなければなりません。true
を返すが、ターゲットオブジェクトが拡張可能でない場合、Proxy に対して TypeError
をスローすることで強制されます。Object.isExtensible(obj)
は常に false
を返さなければなりません。Object.isExtensible(target)
と(強制変換後)同じでない場合、Proxy に対して TypeError
をスローすることで強制されます。次の2つの不変条件は、戻り値をチェックすることで強制されます。
Object.isExtensible(obj)
はブール値を返さなければなりません。Object.getOwnPropertyDescriptor(obj, ···)
はオブジェクトまたは undefined
を返さなければなりません。TypeError
をスローすることで強制されます。不変条件を強制することには、次の利点があります。
次の2つのセクションでは、不変条件が強制される例を示します。
getPrototypeOf
トラップに応答して、Proxy はターゲットが拡張不可の場合、ターゲットのプロトタイプを返さなければなりません。
この不変条件を示すために、ターゲットのプロトタイプとは異なるプロトタイプを返すハンドラーを作成してみましょう。
ターゲットが拡張可能な場合、プロトタイプの偽装は機能します。
const extensibleTarget = {};
const extProxy = new Proxy(extensibleTarget, handler);
assert.equal(
Object.getPrototypeOf(extProxy), fakeProto);
ただし、拡張不可のオブジェクトのプロトタイプを偽装すると、エラーが発生します。
const nonExtensibleTarget = {};
Object.preventExtensions(nonExtensibleTarget);
const nonExtProxy = new Proxy(nonExtensibleTarget, handler);
assert.throws(
() => Object.getPrototypeOf(nonExtProxy),
{
name: 'TypeError',
message: "'getPrototypeOf' on proxy: proxy target is"
+ " non-extensible but the trap did not return its"
+ " actual prototype",
});
ターゲットに書き込み不可で設定不可のプロパティがある場合、ハンドラーは get
トラップに応答してそのプロパティの値を返さなければなりません。この不変条件を示すために、プロパティに対して常に同じ値を返すハンドラーを作成してみましょう。
const handler = {
get(target, propKey) {
return 'abc';
}
};
const target = Object.defineProperties(
{}, {
manufacturer: {
value: 'Iso Autoveicoli',
writable: true,
configurable: true
},
model: {
value: 'Isetta',
writable: false,
configurable: false
},
});
const proxy = new Proxy(target, handler);
プロパティ target.manufacturer
は書き込み不可で設定不可の両方ではないため、ハンドラーは異なる値を持っているかのように見せかけることができます。
ただし、プロパティ target.model
は書き込み不可で設定不可の両方です。したがって、その値を偽装することはできません。
assert.throws(
() => proxy.model,
{
name: 'TypeError',
message: "'get' on proxy: property 'model' is a read-only and"
+ " non-configurable data property on the proxy target but"
+ " the proxy did not return its actual value (expected"
+ " 'Isetta' but got 'abc')",
});
enumerate
トラップはどこにありますか?ECMAScript 6 には元々、for-in
ループによってトリガーされるトラップ enumerate
がありました。しかし、Proxy を簡素化するために、最近削除されました。 Reflect.enumerate()
も削除されました。(出典:TC39 ノート)
このセクションは、Proxy API のクイックリファレンスです。
Proxy
Reflect
リファレンスは、次のカスタムタイプを使用します。
Proxy を作成するには2つの方法があります。
const proxy = new Proxy(target, handler)
指定されたターゲットと指定されたハンドラーを使用して、新しい Proxy オブジェクトを作成します。
const {proxy, revoke} = Proxy.revocable(target, handler)
関数 revoke
を介して取り消すことができる Proxy を作成します。 revoke
は複数回呼び出すことができますが、最初の呼び出しのみが有効で、proxy
をオフに切り替えます。その後、proxy
で実行された操作はすべて、TypeError
がスローされます。
このサブセクションでは、ハンドラーによって実装できるトラップと、それらをトリガーする操作について説明します。いくつかのトラップはブール値を返します。トラップ has
と isExtensible
の場合、ブール値は操作の結果です。他のすべてのトラップの場合、ブール値は操作が成功したかどうかを示します。
すべてのオブジェクトのトラップ
defineProperty(target, propKey, propDesc): boolean
Object.defineProperty(proxy, propKey, propDesc)
deleteProperty(target, propKey): boolean
delete proxy[propKey]
delete proxy.someProp
get(target, propKey, receiver): any
receiver[propKey]
receiver.someProp
getOwnPropertyDescriptor(target, propKey): undefined|PropDesc
Object.getOwnPropertyDescriptor(proxy, propKey)
getPrototypeOf(target): null|object
Object.getPrototypeOf(proxy)
has(target, propKey): boolean
propKey in proxy
isExtensible(target): boolean
Object.isExtensible(proxy)
ownKeys(target): Array<PropertyKey>
Object.getOwnPropertyNames(proxy)
(文字列キーのみを使用)Object.getOwnPropertySymbols(proxy)
(シンボルキーのみを使用)Object.keys(proxy)
(列挙可能な文字列キーのみを使用; 列挙可能性は Object.getOwnPropertyDescriptor
を介してチェックされます)preventExtensions(target): boolean
Object.preventExtensions(proxy)
set(target, propKey, value, receiver): boolean
receiver[propKey] = value
receiver.someProp = value
setPrototypeOf(target, proto): boolean
Object.setPrototypeOf(proxy, proto)
関数のトラップ(ターゲットが関数の場合は使用可能)
apply(target, thisArgument, argumentsList): any
proxy.apply(thisArgument, argumentsList)
proxy.call(thisArgument, ...argumentsList)
proxy(...argumentsList)
construct(target, argumentsList, newTarget): object
new proxy(..argumentsList)
次の操作は*基本操作*であり、他の操作を使用して作業を行うことはありません:apply
、defineProperty
、deleteProperty
、getOwnPropertyDescriptor
、getPrototypeOf
、isExtensible
、ownKeys
、preventExtensions
、setPrototypeOf
他のすべての操作は*派生操作*であり、基本操作を介して実装できます。たとえば、get
は、getPrototypeOf
を介してプロトタイプチェーンを反復処理し、各チェーンメンバーに対して getOwnPropertyDescriptor
を呼び出すことによって、独自のプロパティが見つかるかチェーンが終了するまで実装できます。
不変条件は、ハンドラーの安全制約です。このサブセクションでは、Proxy API によってどのような不変条件がどのように強制されるかについて説明します。以下で「ハンドラーはXを実行する必要がある」と読むたびに、それが行われない場合は TypeError
がスローされることを意味します。一部の不変条件は戻り値を制限し、他の不変条件はパラメーターを制限します。トラップの戻り値の正確性は、2つの方法で保証されます。
TypeError
を引き起こします。これは、強制される不変条件の完全なリストです。
apply(target, thisArgument, argumentsList): any
construct(target, argumentsList, newTarget): object
null
またはその他のプリミティブ値ではありません)。defineProperty(target, propKey, propDesc): boolean
propDesc
が属性 configurable
を false
に設定した場合、ターゲットにはキーが propKey
である設定不可の独自プロパティが必要です。propDesc
が属性 configurable
と writable
の両方を false
に設定した場合、ターゲットにはキーが propKey
である設定不可で書き込み不可の独自プロパティが必要です。propKey
を持つ独自のプロパティがある場合、propDesc
はそのプロパティと互換性がある必要があります。記述子でターゲットプロパティを再定義する場合、例外をスローしないでください。deleteProperty(target, propKey): boolean
propKey
を持つ設定不可の独自プロパティがある場合。propKey
を持つ独自プロパティがある場合。get(target, propKey, receiver): any
propKey
である、独自の書き込み不可で設定不可のデータプロパティがある場合、ハンドラーはそのプロパティの値を返さなければなりません。undefined
を返さなければなりません。getOwnPropertyDescriptor(target, propKey): undefined|PropDesc
undefined
またはオブジェクトのいずれかを返さなければなりません。getPrototypeOf(target): null|object
null
またはオブジェクトのいずれかである必要があります。has(target, propKey): boolean
isExtensible(target): boolean
target.isExtensible()
と同じでなければなりません。ownKeys(target): Array<PropertyKey>
preventExtensions(target): boolean
target.isExtensible()
が false
の場合にのみ、truthy 値(変更の成功を示す)を返す必要があります。set(target, propKey, value, receiver): boolean
propKey
である書き込み不可で構成不可なデータ プロパティがある場合、プロパティを変更することはできません。その場合、value
はその プロパティの値でなければならず、そうでない場合は TypeError
がスローされます。setPrototypeOf(target, proto): boolean
proto
はターゲットのプロトタイプと同じでなければなりません。そうでない場合は、TypeError
がスローされます。 ECMAScript 仕様における不変条件
仕様書では、不変条件はセクション「プロキシオブジェクトの内部メソッドと内部スロット」に記載されています。
通常のオブジェクトの以下の操作は、プロトタイプチェーン内のオブジェクトに対して操作を実行します。そのため、そのチェーン内のオブジェクトの1つが Proxy である場合、そのトラップがトリガーされます。仕様では、操作を(JavaScript コードから見えない)内部独自のメソッドとして実装しています。ただし、このセクションでは、トラップと同じ名前を持つ通常のメソッドであるかのように扱います。パラメータ target
はメソッド呼び出しのレシーバになります。
target.get(propertyKey, receiver)
target
に指定されたキーを持つ独自の プロパティがない場合、get
は target
のプロトタイプで呼び出されます。target.has(propertyKey)
get
と同様に、target
に指定されたキーを持つ独自の プロパティがない場合、has
は target
のプロトタイプで呼び出されます。target.set(propertyKey, value, receiver)
get
と同様に、target
に指定されたキーを持つ独自の プロパティがない場合、set
は target
のプロトタイプで呼び出されます。他のすべての操作は独自の プロパティにのみ影響し、プロトタイプチェーンには影響しません。
ECMAScript 仕様における内部操作
仕様書では、これらの(およびその他の)操作は、「通常のオブジェクトの内部メソッドと内部スロット」セクションに記載されています。
グローバルオブジェクト Reflect
は、JavaScript メタオブジェクトプロトコルのすべてのインターセプト可能な操作をメソッドとして実装します。これらのメソッドの名前は、ハンドラメソッドの名前と同じです。これは、見てきたように、ハンドラからターゲットへの操作の転送に役立ちます。
Reflect.apply(target, thisArgument, argumentsList): any
Function.prototype.apply()
と同様です。
Reflect.construct(target, argumentsList, newTarget=target): object
関数としての new
演算子。target
は呼び出すコンストラクタであり、オプションのパラメータ newTarget
は現在のコンストラクタ呼び出しのチェーンを開始したコンストラクタを指します。
Reflect.defineProperty(target, propertyKey, propDesc): boolean
Object.defineProperty()
と同様です。
Reflect.deleteProperty(target, propertyKey): boolean
関数としての delete
演算子。ただし、動作が少し異なります。 プロパティを正常に削除した場合、または プロパティが存在しなかった場合に true
を返します。 プロパティを削除できず、まだ存在する場合に false
を返します。 プロパティを削除から保護する唯一の方法は、それらを構成不可にすることです。sloppy モードでは、delete
演算子は同じ結果を返します。ただし、strict モードでは、false
を返す代わりに TypeError
をスローします。
Reflect.get(target, propertyKey, receiver=target): any
プロパティを取得する関数。オプションのパラメータ receiver
は、取得が開始されたオブジェクトを指します。 get
がプロトタイプチェーンの後半でゲッターに到達したときに必要になります。それから this
の値を提供します。
Reflect.getOwnPropertyDescriptor(target, propertyKey): undefined|PropDesc
Object.getOwnPropertyDescriptor()
と同じです。
Reflect.getPrototypeOf(target): null|object
Object.getPrototypeOf()
と同じです。
Reflect.has(target, propertyKey): boolean
関数としての in
演算子。
Reflect.isExtensible(target): boolean
Object.isExtensible()
と同じです。
Reflect.ownKeys(target): Array<PropertyKey>
すべての独自の プロパティキーを配列で返します。すべての列挙可能および列挙不可な プロパティの文字列キーとシンボルキーです。
Reflect.preventExtensions(target): boolean
Object.preventExtensions()
と同様です。
Reflect.set(target, propertyKey, value, receiver=target): boolean
プロパティを設定する関数。
Reflect.setPrototypeOf(target, proto): boolean
オブジェクトのプロトタイプを設定する新しい標準的な方法。ほとんどのエンジンで機能する現在の非標準的な方法は、特別な プロパティ __proto__
を設定することです。
いくつかのメソッドはブール値の結果を持ちます。.has()
と .isExtensible()
の場合、それらは操作の結果です。残りのメソッドの場合、操作が成功したかどうかを示します。
Reflect
のユースケース操作の転送とは別に、Reflect
はなぜ便利なのでしょうか?[4]
異なる戻り値:Reflect
は Object
の以下のメソッドを複製しますが、そのメソッドは操作が成功したかどうかを示すブール値を返します(Object
メソッドは変更されたオブジェクトを返します)。
Object.defineProperty(obj, propKey, propDesc): object
Object.preventExtensions(obj): object
Object.setPrototypeOf(obj, proto): object
関数としての演算子:以下の Reflect
メソッドは、そうでなければ演算子によってのみ利用可能な機能を実装します
Reflect.construct(target, argumentsList, newTarget=target): object
Reflect.deleteProperty(target, propertyKey): boolean
Reflect.get(target, propertyKey, receiver=target): any
Reflect.has(target, propertyKey): boolean
Reflect.set(target, propertyKey, value, receiver=target): boolean
apply()
の短いバージョン:関数のメソッド apply()
を呼び出すことについて完全に安全性を確保したい場合、動的ディスパッチを介して行うことはできません。関数はキー 'apply'
を持つ独自の プロパティを持っている可能性があるためです
func.apply(thisArg, argArray) // not safe
Function.prototype.apply.call(func, thisArg, argArray) // safe
Reflect.apply()
を使用すると、安全なバージョンよりも短くなります
プロパティの削除時に例外が発生しない:構成不可な独自の プロパティを削除しようとすると、strict モードでは delete
演算子がスローされます。Reflect.deleteProperty()
はその場合に false
を返します。
Object.*
対 Reflect.*
今後、Object
は通常のアプリケーションにとって関心のある操作をホストし、Reflect
はより低レベルの操作をホストします。
これで、Proxy API の詳細な説明は終わりです。注意すべき点の1つは、Proxy がコードを遅くすることです。パフォーマンスが重要な場合は、それが問題になる可能性があります。
一方、パフォーマンスはしばしば重要ではなく、Proxy が私たちに与えるメタプログラミング能力を持っていることは素晴らしいことです。
謝辞
Allen Wirfs-Brock は、§18.3.7「落とし穴:すべてのオブジェクトが Proxy によって透過的にラップできるわけではない」で説明されている落とし穴を指摘しました。
§18.4.3「負の配列インデックス(get
)」のアイデアは、ブログ投稿 Hemanth.HM からのものです。
André Jaenisch は、Proxy を使用するライブラリのリストに貢献しました。
[1] “ECMAScript リフレクション API の設計について” Tom Van Cutsem and Mark Miller 著。技術報告書、2012年。[本章の重要な情報源。]
[2] “メタオブジェクトプロトコルの芸術” Gregor Kiczales、Jim des Rivieres、Daniel G. Bobrow 著。書籍、1991年。
[3] “メタクラスを仕事に活用する:オブジェクト指向プログラミングの新しい次元” Ira R. Forman and Scott H. Danforth 著。書籍、1999年。
[4] “Harmony-reflect: なぜこのライブラリを使うべきなのか?” Tom Van Cutsem 著。[Reflect
がなぜ便利なのかを説明しています。]