get
, set
)get
, set
)get
)set
)enumerate
トラップはどこにありますか?プロキシを使用すると、オブジェクトに対して実行される操作 (プロパティの取得など) をインターセプトしてカスタマイズできます。これらはメタプログラミング機能です。
次の例では、proxy
は操作をインターセプトするオブジェクトであり、handler
はインターセプトを処理するオブジェクトです。この場合、インターセプトしているのは単一の操作であるget
(プロパティの取得)のみです。
const
target
=
{};
const
handler
=
{
get
(
target
,
propKey
,
receiver
)
{
console
.
log
(
'get '
+
propKey
);
return
123
;
}
};
const
proxy
=
new
Proxy
(
target
,
handler
);
proxy.foo
プロパティを取得すると、ハンドラーがその操作をインターセプトします。
> proxy.foo
get foo
123
インターセプトできる操作の一覧については、完全なAPIのリファレンスを参照してください。
プロキシとは何か、そしてなぜ便利なのかを理解する前に、まずメタプログラミングとは何かを理解する必要があります。
プログラミングにはレベルがあります。
ベースレベルとメタレベルは異なる言語である可能性があります。次のメタプログラムでは、メタプログラミング言語はJavaScriptであり、ベースプログラミング言語はJavaです。
const
str
=
'Hello'
+
'!'
.
repeat
(
3
);
console
.
log
(
'System.out.println("'
+
str
+
'")'
);
メタプログラミングはさまざまな形式をとることができます。前の例では、Javaコードをコンソールに出力しました。メタプログラミング言語とベースプログラミング言語の両方にJavaScriptを使用してみましょう。この典型的な例は、eval()
関数です。これは、JavaScriptコードをその場で評価/コンパイルできます。eval()
にはそれほど多くの実際のユースケースはありません。以下の対話では、5 + 2
式を評価するために使用します。
> eval('5 + 2')
7
他の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
=
{
prop
:
'abc'
};
>
const
obj2
=
{};
>
moveProperty
(
obj1
,
'prop'
,
obj2
);
>
obj1
{}
>
obj2
{
prop
:
'abc'
}
ECMAScript 5はインターセッションをサポートしていません。プロキシはそのギャップを埋めるために作成されました。
ECMAScript 6プロキシは、JavaScriptにインターセッションをもたらします。それらは次のように機能します。オブジェクトobj
に対して実行できる操作はたくさんあります。例えば
obj
のプロパティprop
を取得する (obj.prop
)obj
にプロパティprop
があるかどうかを確認する ('prop' in obj
)プロキシは、これらの操作の一部をカスタマイズできる特別なオブジェクトです。プロキシは2つのパラメーターで作成されます。
handler
: 各操作に対して、対応するハンドラーメソッドが存在する場合、その操作を実行します。このようなメソッドは、(ターゲットへの途中で)操作をインターセプトし、トラップ(オペレーティングシステムのドメインから借用した用語)と呼ばれます。target
: ハンドラーが操作をインターセプトしない場合、その操作はターゲットで実行されます。つまり、ハンドラーのフォールバックとして機能します。ある意味で、プロキシはターゲットをラップします。次の例では、ハンドラーは操作get
とhas
をインターセプトします。
const
target
=
{};
const
handler
=
{
/** Intercepts: getting properties */
get
(
target
,
propKey
,
receiver
)
{
console
.
log
(
`GET
${
propKey
}
`
);
return
123
;
},
/** Intercepts: checking whether properties exist */
has
(
target
,
propKey
)
{
console
.
log
(
`HAS
${
propKey
}
`
);
return
true
;
}
};
const
proxy
=
new
Proxy
(
target
,
handler
);
プロパティfoo
を取得すると、ハンドラーがその操作をインターセプトします。
> proxy.foo
GET foo
123
同様に、in
演算子はhas
をトリガーします。
> 'hello' in proxy
HAS hello
true
ハンドラーはトラップset
(プロパティの設定)を実装していません。したがって、proxy.bar
の設定はtarget
に転送され、target.bar
が設定されます。
> proxy.bar = 'abc';
> target.bar
'abc'
ターゲットが関数の場合、さらに2つの操作をインターセプトできます。
apply
: 関数呼び出し。次のようにトリガーされます。proxy(···)
proxy.call(···)
proxy.apply(···)
construct
: コンストラクター呼び出し。次のようにトリガーされます。new proxy(···)
これらのトラップを関数ターゲットに対してのみ有効にする理由は簡単です。そうしないと、操作apply
とconstruct
を転送できなくなるからです。
プロキシを介してメソッド呼び出しをインターセプトする場合、1つの課題があります。それは、操作get
(プロパティ値の取得)をインターセプトでき、操作apply
(関数の呼び出し)をインターセプトできますが、インターセプトできるメソッド呼び出しの単一の操作はありません。これは、メソッド呼び出しが2つの異なる操作として見なされるためです。最初に、関数を取得するためのget
があり、次に、その関数を呼び出すためのapply
があります。
したがって、get
をインターセプトし、関数呼び出しをインターセプトする関数を返す必要があります。次のコードは、その方法を示しています。
function
traceMethodCalls
(
obj
)
{
const
handler
=
{
get
(
target
,
propKey
,
receiver
)
{
const
origMethod
=
target
[
propKey
];
return
function
(...
args
)
{
const
result
=
origMethod
.
apply
(
this
,
args
);
console
.
log
(
propKey
+
JSON
.
stringify
(
args
)
+
' -> '
+
JSON
.
stringify
(
result
));
return
result
;
};
}
};
return
new
Proxy
(
obj
,
handler
);
}
後者のタスクにはプロキシを使用していません。元のメソッドを関数でラップしているだけです。
traceMethodCalls()
を試すために、次のオブジェクトを使用してみましょう。
const
obj
=
{
multiply
(
x
,
y
)
{
return
x
*
y
;
},
squared
(
x
)
{
return
this
.
multiply
(
x
,
x
);
},
};
tracedObj
は、obj
のトレースされたバージョンです。各メソッド呼び出し後の最初の行はconsole.log()
の出力で、2行目はメソッド呼び出しの結果です。
> const tracedObj = traceMethodCalls(obj);
> tracedObj.multiply(2,7)
multiply[2,7] -> 14
14
> tracedObj.squared(9)
multiply[9,9] -> 81
squared[9] -> 81
81
素晴らしい点は、obj.squared()
内で実行されるthis.multiply()
呼び出しでさえトレースされることです。これは、this
がプロキシを参照し続けるためです。
これは最も効率的な解決策ではありません。たとえば、メソッドをキャッシュすることができます。さらに、プロキシ自体がパフォーマンスに影響を与えます。
ECMAScript 6を使用すると、取り消し(オフ)できるプロキシを作成できます。
const
{
proxy
,
revoke
}
=
Proxy
.
revocable
(
target
,
handler
);
代入演算子(=
)の左側では、Proxy.revocable()
によって返されたオブジェクトのプロパティproxy
とrevoke
にアクセスするために、分割代入を使用しています。
関数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
.
foo
=
123
;
console
.
log
(
proxy
.
foo
);
// 123
revoke
();
console
.
log
(
proxy
.
foo
);
// TypeError: 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
.
bla
;
// Output:
// GET bla
プロパティbla
は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
}
各トラップについて、最初に操作の名前をログに記録し、次に手動で実行して転送します。ECMAScript 6には、転送に役立つモジュールのようなオブジェクトReflect
があります。各トラップに対して
handler
.
trap
(
target
,
arg_1
,
···
,
arg_n
)
Reflect
にはメソッドがあります。
Reflect
.
trap
(
target
,
arg_1
,
···
,
arg_n
)
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
}
各トラップが行うことは非常に似ているため、プロキシを介してハンドラーを実装できます。
const
handler
=
new
Proxy
({},
{
get
(
target
,
trapName
,
receiver
)
{
// Return the handler method named trapName
return
function
(...
args
)
{
// Don’t log args[0]
console
.
log
(
trapName
.
toUpperCase
()
+
' '
+
args
.
slice
(
1
));
// Forward the operation
return
Reflect
[
trapName
](...
args
);
}
}
});
各トラップについて、プロキシはget
操作を介してハンドラーメソッドを要求し、それを提供します。つまり、すべてのハンドラーメソッドは、単一のメタメソッドget
を介して実装できます。このような仮想化を簡単に行うことは、プロキシAPIの目標の1つでした。
このプロキシベースのハンドラーを使用してみましょう。
>
const
target
=
{};
>
const
proxy
=
new
Proxy
(
target
,
handler
);
>
proxy
.
foo
=
123
;
SET
foo
,
123
,[
object
Object
]
>
proxy
.
foo
GET
foo
,[
object
Object
]
123
次の対話では、set
操作がターゲットに正しく転送されたことを確認します。
> target.foo
123
プロキシオブジェクトは、ターゲットオブジェクトに対して実行される操作をインターセプトするものと見なすことができます。プロキシはターゲットをラップします。プロキシのハンドラーオブジェクトは、プロキシのオブザーバーまたはリスナーのようなものです。対応するメソッド(プロパティを読み取る場合はget
など)を実装することにより、インターセプトする操作を指定します。操作のハンドラーメソッドが見つからない場合、その操作はインターセプトされません。単にターゲットに転送されます。
したがって、ハンドラーが空のオブジェクトの場合、プロキシはターゲットを透過的にラップする必要があります。残念ながら、それは必ずしも機能するとは限りません。
this
に影響する さらに深く掘り下げる前に、ターゲットをラップするとthis
にどのような影響があるかを簡単に確認しましょう。
const
target
=
{
foo
()
{
return
{
thisIsTarget
:
this
===
target
,
thisIsProxy
:
this
===
proxy
,
};
}
};
const
handler
=
{};
const
proxy
=
new
Proxy
(
target
,
handler
);
target.foo()
を直接呼び出すと、this
はtarget
を指します。
> target.foo()
{ thisIsTarget: true, thisIsProxy: false }
プロキシ経由でそのメソッドを呼び出すと、this
はproxy
を指します。
> proxy.foo()
{ thisIsTarget: false, thisIsProxy: true }
これは、たとえばターゲットがthis
でメソッドを呼び出す場合に、プロキシがループ内に留まるようにするためです。
通常、空のハンドラーを持つプロキシはターゲットを透過的にラップします。プロキシが存在することに気づかず、ターゲットの動作を変更することはありません。
ただし、ターゲットがプロキシによって制御されないメカニズムを介してthis
と情報を関連付ける場合、問題が発生します。ターゲットがラップされているかどうかによって異なる情報が関連付けられるため、機能しなくなります。
たとえば、次のPerson
クラスは、WeakMap _name
にプライベート情報を保存します(この手法の詳細については、クラスの章を参照してください)。
const
_name
=
new
WeakMap
();
class
Person
{
constructor
(
name
)
{
_name
.
set
(
this
,
name
);
}
get
name
()
{
return
_name
.
get
(
this
);
}
}
Person
のインスタンスは透過的にラップできません。
> const jane = new Person('Jane');
> jane.name
'Jane'
> const proxy = new Proxy(jane, {});
> proxy.name
undefined
jane.name
は、ラップされたproxy.name
とは異なります。次の実装ではこの問題は発生しません。
class
Person2
{
constructor
(
name
)
{
this
.
_name
=
name
;
}
get
name
()
{
return
this
.
_name
;
}
}
const
jane
=
new
Person2
(
'Jane'
);
console
.
log
(
jane
.
name
);
// Jane
const
proxy
=
new
Proxy
(
jane
,
{});
console
.
log
(
proxy
.
name
);
// Jane
ほとんどの組み込みコンストラクターのインスタンスにも、プロキシによってインターセプトされないメカニズムがあります。したがって、それらも透過的にラップすることはできません。Date
のインスタンスで問題を説明します。
const
target
=
new
Date
();
const
handler
=
{};
const
proxy
=
new
Proxy
(
target
,
handler
);
proxy
.
getDate
();
// TypeError: this is not a Date object.
プロキシの影響を受けないメカニズムは、内部スロットと呼ばれます。これらのスロットは、インスタンスに関連付けられたプロパティのようなストレージです。仕様では、これらのスロットを角かっこで囲まれた名前を持つプロパティのように扱います。たとえば、次のメソッドは内部メソッドであり、すべてのオブジェクトO
で呼び出すことができます。
O
.[[
GetPrototypeOf
]]()
ただし、内部スロットへのアクセスは、通常の「get」および「set」操作では発生しません。getDate()
がプロキシ経由で呼び出されると、this
に必要な内部スロットを見つけることができず、TypeError
でエラーが発生します。
Date
メソッドについては、言語仕様では次のように規定されています。
特に明記されていない限り、以下に定義されている Number プロトタイプオブジェクトのメソッドはジェネリックではなく、それらに渡される
this
値は、Number 値であるか、Number 値に初期化された[[NumberData]]
内部スロットを持つオブジェクトである必要があります。
他の組み込みとは対照的に、配列は透過的にラップできます。
> const p = new Proxy(new Array(), {});
> p.push('a');
> p.length
1
> p.length = 0;
> p.length
0
配列がラップ可能な理由は、プロパティアクセスがlength
が機能するようにカスタマイズされているにもかかわらず、Array メソッドが内部スロットに依存していないためです。それらはジェネリックです。
回避策として、ハンドラーがメソッド呼び出しを転送する方法を変更し、this
をプロキシではなくターゲットに選択的に設定できます。
const
handler
=
{
get
(
target
,
propKey
,
receiver
)
{
if
(
propKey
===
'getDate'
)
{
return
target
.
getDate
.
bind
(
target
);
}
return
Reflect
.
get
(
target
,
propKey
,
receiver
);
},
};
const
proxy
=
new
Proxy
(
new
Date
(
'2020-12-24'
),
handler
);
proxy
.
getDate
();
// 24
このアプローチの欠点は、メソッドがthis
で実行する操作のいずれもプロキシを通過しないことです。
謝辞:このセクションで説明した落とし穴を指摘してくれた Allen Wirfs-Brock に感謝します。
このセクションでは、プロキシが何に使用できるかを説明します。これにより、API が実際に動作するのを見る機会が得られます。
get
、set
)の追跡 obj
のプロパティのうち、キーが配列propKeys
にあるプロパティが設定または取得されるたびにログに記録する関数tracePropAccess(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
p
=
new
Point
(
5
,
7
);
p
=
tracePropAccess
(
p
,
[
'x'
,
'y'
]);
追跡対象のオブジェクトp
のプロパティを取得および設定すると、次の影響があります。
> p.x
GET x
5
> p.x = 21
SET x=21
21
興味深いことに、Point
がプロパティにアクセスするときにも追跡が機能します。これは、this
がPoint
のインスタンスではなく、追跡対象のオブジェクトを参照するようになったためです。
> p.toString()
GET x
GET y
'Point(21, 7)'
ECMAScript 5 では、tracePropAccess()
を次のように実装します。各プロパティを、アクセスを追跡するゲッターとセッターに置き換えます。セッターとゲッターは、プロパティのデータを保存するために、追加のオブジェクトpropData
を使用します。元の実装を破壊的に変更していることに注意してください。これは、メタプログラミングを行っていることを意味します。
function
tracePropAccess
(
obj
,
propKeys
)
{
// 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
()
{
console
.
log
(
'GET '
+
propKey
);
return
propData
[
propKey
];
},
set
:
function
(
value
)
{
console
.
log
(
'SET '
+
propKey
+
'='
+
value
);
propData
[
propKey
]
=
value
;
},
});
});
return
obj
;
}
ECMAScript 6 では、よりシンプルなプロキシベースのソリューションを使用できます。プロパティの取得と設定をインターセプトし、実装を変更する必要はありません。
function
tracePropAccess
(
obj
,
propKeys
)
{
const
propKeySet
=
new
Set
(
propKeys
);
return
new
Proxy
(
obj
,
{
get
(
target
,
propKey
,
receiver
)
{
if
(
propKeySet
.
has
(
propKey
))
{
console
.
log
(
'GET '
+
propKey
);
}
return
Reflect
.
get
(
target
,
propKey
,
receiver
);
},
set
(
target
,
propKey
,
value
,
receiver
)
{
if
(
propKeySet
.
has
(
propKey
))
{
console
.
log
(
'SET '
+
propKey
+
'='
+
value
);
}
return
Reflect
.
set
(
target
,
propKey
,
value
,
receiver
);
},
});
}
get
、set
) プロパティへのアクセスに関して、JavaScript は非常に寛容です。たとえば、プロパティを読み取ろうとして名前をスペルミスした場合、例外は発生せず、結果undefined
が得られます。このような場合に例外を取得するためにプロキシを使用できます。これは次のように機能します。プロキシをオブジェクトのプロトタイプにします。
プロパティがオブジェクトで見つからない場合、プロキシのget
トラップがトリガーされます。プロキシ後のプロトタイプチェーンにもプロパティが存在しない場合、それは本当に欠落しており、例外をスローします。それ以外の場合は、継承されたプロパティの値を返します。そのためには、get
操作をターゲットに転送します(ターゲットのプロトタイプもプロキシのプロトタイプです)。
const
PropertyChecker
=
new
Proxy
({},
{
get
(
target
,
propKey
,
receiver
)
{
if
(
!
(
propKey
in
target
))
{
throw
new
ReferenceError
(
'Unknown property: '
+
propKey
);
}
return
Reflect
.
get
(
target
,
propKey
,
receiver
);
}
});
作成するオブジェクトにPropertyChecker
を使用してみましょう。
>
const
obj
=
{
__proto__
:
PropertyChecker
,
foo
:
123
};
>
obj
.
foo
// own
123
>
obj
.
fo
ReferenceError
:
Unknown
property
:
fo
>
obj
.
toString
()
// inherited
'
[
object
Object
]
'
PropertyChecker
をコンストラクターにすると、extends
を介して ECMAScript 6 クラスに使用できます。
function
PropertyChecker
()
{
}
PropertyChecker
.
prototype
=
new
Proxy
(
···
);
class
Point
extends
PropertyChecker
{
constructor
(
x
,
y
)
{
super
();
this
.
x
=
x
;
this
.
y
=
y
;
}
}
const
p
=
new
Point
(
5
,
7
);
console
.
log
(
p
.
x
);
// 5
console
.
log
(
p
.
z
);
// ReferenceError
誤ってプロパティを作成することを心配している場合は、2つのオプションがあります。set
をトラップするオブジェクトをプロキシでラップするか、Object.preventExtensions(obj)
を介してオブジェクトobj
を拡張不可にすることができます。これは、JavaScript がobj
に新しい(独自の)プロパティを追加できないことを意味します。
get
) 一部の Array メソッドでは、-1
で最後の要素、-2
で最後から2番目の要素などを参照できます。たとえば
> ['a', 'b', 'c'].slice(-1)
[ 'c' ]
残念ながら、ブラケット演算子([]
)を介して要素にアクセスする場合は、それは機能しません。ただし、プロキシを使用してその機能を追加できます。次の関数createArray()
は、負のインデックスをサポートする配列を作成します。これを行うには、配列インスタンスをプロキシでラップします。プロキシは、ブラケット演算子によってトリガーされるget
操作をインターセプトします。
function
createArray
(...
elements
)
{
const
handler
=
{
get
(
target
,
propKey
,
receiver
)
{
// Sloppy way of checking for negative indices
const
index
=
Number
(
propKey
);
if
(
index
<
0
)
{
propKey
=
String
(
target
.
length
+
index
);
}
return
Reflect
.
get
(
target
,
propKey
,
receiver
);
}
};
// Wrap a proxy around an Array
const
target
=
[];
target
.
push
(...
elements
);
return
new
Proxy
(
target
,
handler
);
}
const
arr
=
createArray
(
'a'
,
'b'
,
'c'
);
console
.
log
(
arr
[
-
1
]);
// c
謝辞:この例のアイデアは、hemanth.hm のブログ記事から来ています。
set
) データバインディングとは、オブジェクト間でデータを同期することです。一般的なユースケースの1つは、MVC(モデルビューコントローラー)パターンに基づくウィジェットです。データバインディングを使用すると、モデル(ウィジェットによって視覚化されるデータ)を変更した場合でも、ビュー(ウィジェット)は最新の状態を維持します。
データバインディングを実装するには、オブジェクトに加えられた変更を監視し、それに対応する必要があります。次のコードスニペットでは、配列に対する変更の監視がどのように機能するかを概説します。
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
(
`
${
key
}
=
${
value
}
`
));
observedArray
.
push
(
'a'
);
出力
0=a
length=1
プロキシを使用して、任意のメソッドを呼び出すことができるオブジェクトを作成できます。次の例では、関数createWebService
が、そのようなオブジェクトservice
を作成します。service
でメソッドを呼び出すと、同じ名前の Web サービスリソースの内容が取得されます。取得は、ECMAScript 6 Promise を介して処理されます。
const
service
=
createWebService
(
'http://example.com/data'
);
// Read JSON data in http://example.com/data/employees
service
.
employees
().
then
(
json
=>
{
const
employees
=
JSON
.
parse
(
json
);
···
});
次のコードは、ECMAScript 5 でのcreateWebService
の簡単で乱暴な実装です。プロキシがないため、service
でどのメソッドが呼び出されるかを事前に知っておく必要があります。パラメーターpropKeys
は、その情報を提供し、メソッド名を含む配列を保持します。
function
createWebService
(
baseUrl
,
propKeys
)
{
const
service
=
{};
propKeys
.
forEach
(
function
(
propKey
)
{
service
[
propKey
]
=
function
()
{
return
httpGet
(
baseUrl
+
'/'
+
propKey
);
};
});
return
service
;
}
createWebService
の ECMAScript 6 実装では、プロキシを使用でき、よりシンプルです。
function
createWebService
(
baseUrl
)
{
return
new
Proxy
({},
{
get
(
target
,
propKey
,
receiver
)
{
// Return the method to be called
return
()
=>
httpGet
(
baseUrl
+
'/'
+
propKey
);
}
});
}
どちらの実装も、次の関数を使用して HTTP GET リクエストを行います(その仕組みについては、Promise の章で説明しています)。
function
httpGet
(
url
)
{
return
new
Promise
(
(
resolve
,
reject
)
=>
{
const
request
=
new
XMLHttpRequest
();
Object
.
assign
(
request
,
{
onload
()
{
if
(
this
.
status
===
200
)
{
// Success
resolve
(
this
.
response
);
}
else
{
// Something went wrong (404 etc.)
reject
(
new
Error
(
this
.
statusText
));
}
},
onerror
()
{
reject
(
new
Error
(
'XMLHttpRequest Error: '
+
this
.
statusText
));
}
});
request
.
open
(
'GET'
,
url
);
request
.
send
();
});
}
取り消し可能な参照は、次のように機能します。クライアントは、重要なリソース(オブジェクト)に直接アクセスすることは許可されておらず、参照(中間オブジェクト、リソースのラッパー)を介してのみアクセスできます。通常、参照に適用されたすべての操作はリソースに転送されます。クライアントが完了すると、リソースは参照を取り消すことによって保護されます。つまり、オフに切り替えます。今後、参照に操作を適用すると例外がスローされ、何も転送されなくなります。
次の例では、リソースの取り消し可能な参照を作成します。次に、参照を介してリソースのプロパティの1つを読み取ります。参照によりアクセスが許可されるため、機能します。次に、参照を取り消します。これで、参照はプロパティを読み取ることができなくなります。
const
resource
=
{
x
:
11
,
y
:
8
};
const
{
reference
,
revoke
}
=
createRevocableReference
(
resource
);
// Access granted
console
.
log
(
reference
.
x
);
// 11
revoke
();
// Access denied
console
.
log
(
reference
.
x
);
// TypeError: Revoked
プロキシは、操作をインターセプトして転送できるため、取り消し可能な参照の実装に最適です。これは、createRevocableReference
のシンプルなプロキシベースの実装です。
function
createRevocableReference
(
target
)
{
let
enabled
=
true
;
return
{
reference
:
new
Proxy
(
target
,
{
get
(
target
,
propKey
,
receiver
)
{
if
(
!
enabled
)
{
throw
new
TypeError
(
'Revoked'
);
}
return
Reflect
.
get
(
target
,
propKey
,
receiver
);
},
has
(
target
,
propKey
)
{
if
(
!
enabled
)
{
throw
new
TypeError
(
'Revoked'
);
}
return
Reflect
.
has
(
target
,
propKey
);
},
···
}),
revoke
()
{
enabled
=
false
;
},
};
}
前のセクションのプロキシをハンドラーとして使用する手法を使用して、コードを簡略化できます。今回は、ハンドラーは基本的にReflect
オブジェクトです。したがって、get
トラップは通常、適切なReflect
メソッドを返します。参照が取り消された場合、代わりにTypeError
がスローされます。
function
createRevocableReference
(
target
)
{
let
enabled
=
true
;
const
handler
=
new
Proxy
({},
{
get
(
dummyTarget
,
trapName
,
receiver
)
{
if
(
!
enabled
)
{
throw
new
TypeError
(
'Revoked'
);
}
return
Reflect
[
trapName
];
}
});
return
{
reference
:
new
Proxy
(
target
,
handler
),
revoke
()
{
enabled
=
false
;
},
};
}
ただし、取り消し可能な参照を自分で実装する必要はありません。ECMAScript 6 では、取り消すことができるプロキシを作成できるからです。今回は、取り消しはハンドラーではなくプロキシで行われます。ハンドラーが行う必要があるのは、すべての操作をターゲットに転送することだけです。ハンドラーがトラップを実装しない場合、それは自動的に行われることがわかりました。
function
createRevocableReference
(
target
)
{
const
handler
=
{};
// forward everything
const
{
proxy
,
revoke
}
=
Proxy
.
revocable
(
target
,
handler
);
return
{
reference
:
proxy
,
revoke
};
}
メンブレンは、取り消し可能な参照のアイデアに基づいています。信頼されていないコードを実行するように設計された環境は、そのコードを分離し、システムの残りの部分を安全に保つために、そのコードをメンブレンでラップします。オブジェクトは、2つの方向にメンブレンを通過します。
どちらの場合も、取り消し可能な参照はオブジェクトの周りにラップされます。ラップされた関数またはメソッドによって返されるオブジェクトもラップされます。さらに、ラップされたウェットオブジェクトがメンブレンに渡されると、ラップが解除されます。
信頼されていないコードが完了すると、取り消し可能な参照はすべて取り消されます。その結果、外部のコードは実行できなくなり、外部のオブジェクトも機能しなくなります。 Caja Compilerは、「サードパーティの HTML、CSS、および JavaScript を Web サイトに安全に埋め込むためのツール」です。メンブレンを使用してこのタスクを実現します。
ブラウザのDocument Object Model (DOM) は通常、JavaScriptとC++の混合で実装されています。純粋なJavaScriptで実装することは、以下の場合に役立ちます。
残念ながら、標準のDOMはJavaScriptで複製するのが容易ではないことを実行できます。例えば、ほとんどのDOMコレクションは、DOMの現在の状態に対するライブビューであり、DOMが変更されるたびに動的に変化します。その結果、DOMの純粋なJavaScript実装はあまり効率的ではありません。JavaScriptにプロキシを追加した理由の1つは、より効率的なDOM実装を記述するのに役立てるためでした。
プロキシには、さらに多くのユースケースがあります。例えば
このセクションでは、プロキシの仕組みと、そのように動作する理由について詳しく説明します。
Firefoxでは、以前からインターセプティブメタプログラミングが可能でした。__noSuchMethod__
という名前のメソッドを定義すると、存在しないメソッドが呼び出されたときに通知されます。以下は、__noSuchMethod__
を使用する例です。
const
obj
=
{
__noSuchMethod__
:
function
(
name
,
args
)
{
console
.
log
(
name
+
': '
+
args
);
}
};
// Neither of the following two methods exist,
// but we can make it look like they do
obj
.
foo
(
1
);
// Output: foo: 1
obj
.
bar
(
1
,
2
);
// Output: bar: 1,2
このように、__noSuchMethod__
はプロキシトラップと同様に機能します。プロキシとは対照的に、トラップは、操作をインターセプトしたいオブジェクトの独自のメソッドまたは継承されたメソッドです。このアプローチの問題点は、基本レベル(通常のメソッド)とメタレベル(__noSuchMethod__
)が混ざり合っていることです。基本レベルのコードが誤ってメタレベルのメソッドを呼び出したり、認識したりする可能性があり、誤ってメタレベルのメソッドを定義してしまう可能性があります。
標準のECMAScript 5でさえ、基本レベルとメタレベルが混ざり合っている場合があります。たとえば、以下のメタプログラミングメカニズムは、基本レベルに存在するため、失敗する可能性があります。
obj.hasOwnProperty(propKey)
:プロトタイプチェーン内のプロパティが組み込みの実装をオーバーライドする場合、この呼び出しは失敗する可能性があります。たとえば、obj
が次の場合は失敗します。
{
hasOwnProperty
:
null
}
このメソッドを安全に呼び出す方法は次のとおりです。
Object
.
prototype
.
hasOwnProperty
.
call
(
obj
,
propKey
)
// Abbreviated version:
{}.
hasOwnProperty
.
call
(
obj
,
propKey
)
func.call(···)
、func.apply(···)
:これらの2つのメソッドそれぞれについて、問題と解決策はhasOwnProperty
と同じです。obj.__proto__
:ほとんどのJavaScriptエンジンでは、__proto__
はobj
のプロトタイプを取得および設定できる特別なプロパティです。したがって、オブジェクトを辞書として使用する場合は、プロパティキーとして__proto__
を使用しないように注意する必要があります。これで、(基本レベルの)プロパティキーを特殊化することが問題であることが明らかになったはずです。したがって、プロキシは階層化されています。基本レベル(プロキシオブジェクト)とメタレベル(ハンドラーオブジェクト)は別々です。
プロキシは2つの役割で使用されます。
プロキシAPIの初期の設計では、プロキシは純粋な仮想オブジェクトとして考えられていました。しかし、その役割においても、ターゲットが不変性を強制するために(後で説明します)、また、ハンドラーが実装していないトラップのフォールバックとして役立つことがわかりました。
プロキシは2つの方法で保護されています。
両方の原則により、プロキシは他のオブジェクトを偽装するためのかなりの権限を得ることができます。(後で説明するように)不変性を強制する理由の1つは、その権限を抑制することです。
プロキシを非プロキシと区別する方法が必要な場合は、自分で実装する必要があります。次のコードは、2つの関数をエクスポートするモジュールlib.js
です。1つはプロキシを作成し、もう1つはオブジェクトがそれらのプロキシの1つであるかどうかを判断します。
// lib.js
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
);
}
このモジュールは、プロキシを追跡するためにECMAScript 6のデータ構造WeakSet
を使用しています。WeakSet
は、その要素がガベージコレクションされるのを妨げないため、この目的に最適です。
次の例は、lib.js
の使用方法を示しています。
// main.js
import
{
createProxy
,
isProxy
}
from
'./lib.js'
;
const
p
=
createProxy
({});
console
.
log
(
isProxy
(
p
));
// true
console
.
log
(
isProxy
({}));
// false
このセクションでは、JavaScriptが内部的にどのように構造化されているか、およびプロキシトラップのセットがどのように選択されたかを調べます。
プログラミング言語と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メソッドは次のとおりです。
[[GetOwnProperty]]
(トラップgetOwnPropertyDescriptor
)[[GetPrototypeOf]]
(トラップgetPrototypeOf
)[[Get]]
(トラップget
)[[Call]]
(トラップapply
)行Aでは、プロトタイプチェーン内のプロキシが、「より早い」オブジェクトにプロパティが見つからない場合にget
について見つける理由を確認できます。キーがpropKey
の独自のプロパティがない場合、検索はthis
のプロトタイプparent
で続行されます。
基本操作と派生操作。[[Get]]
が他のMOP操作を呼び出していることがわかります。それを行う操作は派生と呼ばれます。他の操作に依存しない操作は基本と呼ばれます。
プロキシのメタオブジェクトプロトコルは、通常のオブジェクトのメタオブジェクトプロトコルとは異なります。通常のオブジェクトの場合、派生操作は他の操作を呼び出します。プロキシの場合、各操作(基本か派生かに関係なく)は、ハンドラーメソッドによってインターセプトされるか、ターゲットに転送されます。
どの操作がプロキシを介してインターセプト可能である必要がありますか?1つの可能性は、基本操作に対してのみトラップを提供することです。もう1つの可能性は、いくつかの派生操作を含めることです。そうすることの利点は、パフォーマンスが向上し、より便利になることです。例えば、get
のトラップがない場合、getOwnPropertyDescriptor
を介してその機能を実装する必要があります。派生トラップの1つの問題は、プロキシの動作が矛盾する可能性があることです。例えば、get
は、getOwnPropertyDescriptor
によって返される記述子の値とは異なる値を返す場合があります。
プロキシによる仲介は選択的です。すべての言語操作をインターセプトできるわけではありません。なぜいくつかの操作が除外されたのでしょうか?2つの理由を見てみましょう。
第一に、安定した操作は仲介には適していません。操作は、同じ引数に対して常に同じ結果を生成する場合、安定しています。プロキシが安定した操作をトラップできる場合、不安定になり、信頼性が低下する可能性があります。厳密等価(===
)は、そのような安定した操作の1つです。トラップすることはできず、その結果は、プロキシ自体を別のオブジェクトとして扱うことによって計算されます。安定性を維持する別の方法は、プロキシではなくターゲットに操作を適用することです。後で説明するように、プロキシの不変性がどのように強制されるかを見ると、ターゲットが非拡張可能なプロキシにObject.getPrototypeOf()
が適用されるときにこれが起こります。
より多くの操作をインターセプト可能にしない2番目の理由は、仲介は通常は不可能な状況でカスタムコードを実行することを意味するからです。コードのこのインターリーブが多くなればなるほど、プログラムを理解してデバッグするのが難しくなります。また、パフォーマンスにも悪影響を及ぼします。
get
とinvoke
ECMAScript 6プロキシを介して仮想メソッドを作成する場合は、get
トラップから関数を返す必要があります。これにより、次の疑問が生じます。メソッド呼び出し用の追加のトラップ(たとえば、invoke
)を導入しないのはなぜですか?これにより、次のものを区別できるようになります。
obj.prop
を介してプロパティを取得する(トラップget
)obj.prop()
を介してメソッドを呼び出す(トラップinvoke
)そうしない理由は2つあります。
第一に、すべての実装がget
とinvoke
を区別しているわけではありません。たとえば、AppleのJavaScriptCoreは区別していません。
第二に、メソッドを抽出してcall()
またはapply()
を介して後で呼び出すと、ディスパッチを介してメソッドを呼び出すのと同じ効果があるはずです。言い換えれば、次の2つのバリアントは同等に機能する必要があります。追加のトラップinvoke
があった場合、その同等性を維持するのがより困難になります。
// Variant 1: call via dynamic dispatch
const
result
=
obj
.
m
();
// Variant 2: extract and call directly
const
m
=
obj
.
m
;
const
result
=
m
.
call
(
obj
);
invoke
のユースケース get
とinvoke
を区別できる場合にのみ実行できることがあります。したがって、現在のプロキシAPIではそれらの操作は不可能です。2つの例は、自動バインディングと欠落したメソッドのインターセプトです。プロキシがinvoke
をサポートしている場合に、それらをどのように実装するかを見てみましょう。
自動バインディング。プロキシをオブジェクトobj
のプロトタイプにすることで、メソッドを自動的にバインドできます。
obj.m
を介してメソッドm
の値を取得すると、this
がobj
にバインドされた関数が返されます。obj.m()
はメソッド呼び出しを実行します。自動バインディングは、メソッドをコールバックとして使用するのに役立ちます。例えば、前の例のバリアント2はよりシンプルになります。
const
boundMethod
=
obj
.
m
;
const
result
=
boundMethod
();
存在しないメソッドのインターセプト。 invoke
を使うと、プロキシは以前に述べた Firefox がサポートする __noSuchMethod__
メカニズムをエミュレートできます。プロキシは再びオブジェクト obj
のプロトタイプになります。未知のプロパティ foo
がどのようにアクセスされるかに応じて、異なる反応をします。
obj.foo
を介してそのプロパティを読み取る場合、介入は発生せず、undefined
が返されます。obj.foo()
を行うと、プロキシがインターセプトし、例えば、コールバックに通知します。不変性とは何か、そしてプロキシでどのように強制されるかを見る前に、オブジェクトが拡張不可および設定不可によってどのように保護できるかを確認しましょう。
オブジェクトを保護する方法は 2 つあります。
拡張不可性。オブジェクトが拡張不可の場合、プロパティを追加できず、プロトタイプを変更することもできません。
'use strict'
;
// switch on strict mode to get TypeErrors
const
obj
=
Object
.
preventExtensions
({});
console
.
log
(
Object
.
isExtensible
(
obj
));
// false
obj
.
foo
=
123
;
// TypeError: object is not extensible
Object
.
setPrototypeOf
(
obj
,
null
);
// TypeError: object is not extensible
設定不可性。プロパティのすべてのデータは属性に格納されます。プロパティはレコードのようなもので、属性はそのレコードのフィールドのようなものです。属性の例:
value
はプロパティの値を保持します。writable
は、プロパティの値を変更できるかどうかを制御します。configurable
は、プロパティの属性を変更できるかどうかを制御します。したがって、プロパティが非書き込み可能かつ非設定可能の両方である場合、それは読み取り専用であり、その状態を維持します。
'use strict'
;
// switch on strict mode to get TypeErrors
const
obj
=
{};
Object
.
defineProperty
(
obj
,
'foo'
,
{
value
:
123
,
writable
:
false
,
configurable
:
false
});
console
.
log
(
obj
.
foo
);
// 123
obj
.
foo
=
'a'
;
// TypeError: Cannot assign to read only property
Object
.
defineProperty
(
obj
,
'foo'
,
{
configurable
:
true
});
// TypeError: Cannot redefine property
これらのトピック(Object.defineProperty()
がどのように機能するかを含む)の詳細については、「Speaking JavaScript」の以下のセクションを参照してください。
従来、拡張不可性と設定不可性は、
これらの、言語操作に直面しても変更されないその他の特性は、不変性と呼ばれます。プロキシを使用すると、不変性は本質的に拡張不可性などによって制約されないため、不変性を破るのが簡単です。
プロキシ API は、ハンドラーメソッドのパラメーターと結果をチェックすることにより、プロキシが不変性を破るのを防ぎます。以下は、4 つの不変性(任意のオブジェクト obj
の場合)の例と、それらがプロキシでどのように強制されるかを示しています(網羅的なリストはこの章の最後に示されています)。
最初の 2 つの不変性は、拡張不可性と設定不可性に関係します。これらは、ターゲットオブジェクトを簿記に使用することにより強制されます。ハンドラーメソッドによって返される結果は、ほとんどターゲットオブジェクトと同期している必要があります。
Object.preventExtensions(obj)
が true
を返す場合、今後のすべての呼び出しは false
を返す必要があり、obj
は拡張不可である必要があります。true
を返すが、ターゲットオブジェクトが拡張可能でない場合に TypeError
をスローすることにより、プロキシに対して強制されます。Object.isExtensible(obj)
は常に false
を返す必要があります。Object.isExtensible(target)
と同じでない(強制型変換後)場合に TypeError
をスローすることにより、プロキシに対して強制されます。残りの 2 つの不変性は、戻り値をチェックすることにより強制されます。
Object.isExtensible(obj)
はブール値を返す必要があります。Object.getOwnPropertyDescriptor(obj, ···)
はオブジェクトまたは undefined
を返す必要があります。TypeError
をスローすることにより、プロキシに対して強制されます。不変性を強制することには、次の利点があります。
次の 2 つのセクションでは、不変性が強制される例を示します。
getPrototypeOf
トラップに応答して、ターゲットが拡張不可である場合、プロキシはターゲットのプロトタイプを返す必要があります。
この不変性を示すために、ターゲットのプロトタイプとは異なるプロトタイプを返すハンドラーを作成しましょう。
const
fakeProto
=
{};
const
handler
=
{
getPrototypeOf
(
t
)
{
return
fakeProto
;
}
};
ターゲットが拡張可能であれば、プロトタイプの偽装は機能します。
const
extensibleTarget
=
{};
const
ext
=
new
Proxy
(
extensibleTarget
,
handler
);
console
.
log
(
Object
.
getPrototypeOf
(
ext
)
===
fakeProto
);
// true
ただし、拡張不可のオブジェクトに対してプロトタイプを偽装すると、エラーが発生します。
const
nonExtensibleTarget
=
{};
Object
.
preventExtensions
(
nonExtensibleTarget
);
const
nonExt
=
new
Proxy
(
nonExtensibleTarget
,
handler
);
Object
.
getPrototypeOf
(
nonExt
);
// TypeError
ターゲットに書き込み不可で設定不可のプロパティがある場合、ハンドラーは get
トラップに応答してそのプロパティの値を返す必要があります。この不変性を示すために、常にプロパティに同じ値を返すハンドラーを作成しましょう。
const
handler
=
{
get
(
target
,
propKey
)
{
return
'abc'
;
}
};
const
target
=
Object
.
defineProperties
(
{},
{
foo
:
{
value
:
123
,
writable
:
true
,
configurable
:
true
},
bar
:
{
value
:
456
,
writable
:
false
,
configurable
:
false
},
});
const
proxy
=
new
Proxy
(
target
,
handler
);
プロパティ target.foo
は書き込み不可でも設定不可でもないため、ハンドラーは異なる値を持っていると見せかけることができます。
> proxy.foo
'abc'
ただし、プロパティ target.bar
は書き込み不可で設定不可の両方です。したがって、その値を偽装することはできません。
> proxy.bar
TypeError: Invariant check failed
enumerate
トラップはどこにありますか? ES6 にはもともと for-in
ループによってトリガーされるトラップ enumerate
がありました。しかし、プロキシを簡素化するために最近削除されました。Reflect.enumerate()
も削除されました。(出典:TC39 ノート)
このセクションは、プロキシ API のクイックリファレンスとして機能します。グローバルオブジェクト Proxy
と Reflect
です。
プロキシを作成する方法は 2 つあります。
const proxy = new Proxy(target, handler)
const {proxy, revoke} = Proxy.revocable(target, handler)
revoke
を介して取り消すことができるプロキシを作成します。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.foo // propKey = 'foo'
get(target, propKey, receiver) : any
receiver[propKey]
receiver.foo // propKey = 'foo'
getOwnPropertyDescriptor(target, propKey) : PropDesc|Undefined
Object.getOwnPropertyDescriptor(proxy, propKey)
getPrototypeOf(target) : Object|Null
Object.getPrototypeOf(proxy)
has(target, propKey) : boolean
propKey in proxy
isExtensible(target) : boolean
Object.isExtensible(proxy)
ownKeys(target) : Array<PropertyKey>
Object.getOwnPropertyPropertyNames(proxy)
(文字列キーのみを使用)Object.getOwnPropertyPropertySymbols(proxy)
(シンボルキーのみを使用)Object.keys(proxy)
(列挙可能な文字列キーのみを使用。列挙可能性は Object.getOwnPropertyDescriptor
を介してチェックされます)preventExtensions(target) : boolean
Object.preventExtensions(proxy)
set(target, propKey, value, receiver) : boolean
receiver[propKey] = value
receiver.foo = value // propKey = 'foo'
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
を呼び出すことによって実装できます。
不変性は、ハンドラーの安全性の制約です。このサブセクションでは、プロキシ API によって強制される不変性と、その方法について説明します。「ハンドラーは X を行う必要がある」と書かれている場合は、そうしないと TypeError
がスローされることを意味します。一部の不変性は戻り値を制限し、他の不変性はパラメーターを制限します。トラップの戻り値の正確さは、2 つの方法で保証されます。通常、不正な値は TypeError
がスローされることを意味します。ただし、ブール値が期待される場合は常に、強制型変換を使用して非ブール値を有効な値に変換します。
これは、強制される不変条件の完全なリストです。
apply(target, thisArgument, argumentsList)
construct(target, argumentsList, newTarget)
null
またはプリミティブ値ではない)でなければなりません。defineProperty(target, propKey, propDesc)
propKey
はターゲットの自身のキーの1つである必要があります。propDesc
が属性configurable
をfalse
に設定する場合、ターゲットはキーがpropKey
である設定不可能な自身のプロパティを持つ必要があります。propDesc
がターゲットの自身のプロパティを(再)定義するために使用される場合、例外が発生してはなりません。writable
およびconfigurable
属性(非拡張性は最初のルールで処理されます)によって変更が禁止されている場合、例外がスローされます。deleteProperty(target, propKey)
get(target, propKey, receiver)
propKey
である自身の、書き込み不可、設定不可のデータプロパティがある場合、ハンドラーはそのプロパティの値を返す必要があります。undefined
を返す必要があります。getOwnPropertyDescriptor(target, propKey)
undefined
のいずれかを返す必要があります。writable
およびconfigurable
属性によって許可されていない場合(非拡張性は3番目のルールで処理されます)、例外がスローされます。したがって、ハンドラーは設定不可のプロパティを設定可能として報告することはできず、設定不可の書き込み不可プロパティに対して異なる値を報告することもできません。getPrototypeOf(target)
null
のいずれかである必要があります。has(target, propKey)
isExtensible(target)
target.isExtensible()
と同じである必要があります。ownKeys(target)
preventExtensions(target)
target.isExtensible()
はその後false
である必要があります。set(target, propKey, value, receiver)
propKey
である自身の、書き込み不可、設定不可のデータプロパティがある場合、value
はそのプロパティの値と同じである必要があります(つまり、プロパティは変更できません)。TypeError
がスローされます(つまり、そのようなプロパティは設定できません)。setPrototypeOf(target, proto)
proto
はターゲットのプロトタイプと同じである必要があります。それ以外の場合は、TypeError
がスローされます。通常のオブジェクトの次の操作は、プロトタイプチェーン内のオブジェクトに対して操作を実行します。したがって、そのチェーン内のオブジェクトの1つがプロキシである場合、そのトラップがトリガーされます。仕様では、操作は(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
のプロトタイプで呼び出されます。他のすべての操作は自身のプロパティにのみ影響し、プロトタイプチェーンには影響しません。
グローバルオブジェクトReflect
は、JavaScriptメタオブジェクトプロトコルのすべてのインターセプト可能な操作をメソッドとして実装します。これらのメソッドの名前は、ハンドラーメソッドの名前と同じであり、既に見たように、ハンドラーからターゲットへの操作の転送に役立ちます。
Reflect.apply(target, thisArgument, argumentsList) : any
Function.prototype.apply()
と同じです。Reflect.construct(target, argumentsList, newTarget=target) : Object
new
演算子。target
は呼び出すコンストラクターであり、オプションのパラメータnewTarget
は、現在のコンストラクター呼び出しのチェーンを開始したコンストラクターを指します。ES6でのコンストラクター呼び出しのチェーン方法に関する詳細については、クラスの章を参照してください。Reflect.defineProperty(target, propertyKey, propDesc) : boolean
Object.defineProperty()
に似ています。Reflect.deleteProperty(target, propertyKey) : boolean
delete
演算子。ただし、少し異なる動作をします。プロパティの削除に成功した場合、またはプロパティが一度も存在しなかった場合は、true
を返します。プロパティを削除できず、まだ存在する場合は、false
を返します。プロパティを削除から保護する唯一の方法は、プロパティを設定不可にすることです。緩いモードでは、delete
演算子は同じ結果を返します。ただし、厳格モードでは、false
を返す代わりに、TypeError
をスローします。Reflect.get(target, propertyKey, receiver=target) : any
receiver
は、get
がプロトタイプチェーンの後半でgetterに到達したときに必要です。次に、this
の値を提供します。Reflect.getOwnPropertyDescriptor(target, propertyKey) : PropDesc|Undefined
Object.getOwnPropertyDescriptor()
と同じです。Reflect.getPrototypeOf(target) : Object|Null
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()
を使用する方が短いです。
Reflect
.
apply
(
func
,
thisArg
,
argArray
)
delete
演算子がスローされます。Reflect.deleteProperty()
はその場合、false
を返します。Object.*
対Reflect.*
今後、Object
は通常のアプリケーションに関心のある操作をホストし、Reflect
はより低レベルの操作をホストします。
これで、プロキシAPIの詳細な解説は終わりです。各アプリケーションでは、パフォーマンスを考慮し、必要であれば測定する必要があります。プロキシは常に十分な速度が出るとは限りません。一方、パフォーマンスがそれほど重要でない場合は、プロキシがもたらすメタプログラミングの力を活用できるのは素晴らしいことです。これまで見てきたように、プロキシは多くのユースケースで役立ちます。
[1] Tom Van CutsemとMark Millerによる「On the design of the ECMAScript Reflection API」。テクニカルレポート、2012年。[この章の重要な情報源です。]
[2] Gregor Kiczales、Jim des Rivieres、Daniel G. Bobrowによる「The Art of the Metaobject Protocol」。書籍、1991年。
[3] Ira R. FormanとScott H. Danforthによる「Putting Metaclasses to Work: A New Dimension in Object-Oriented Programming」。書籍、1999年。
[4] Tom Van Cutsemによる「Harmony-reflect: Why should I use this library?」。[Reflect
がなぜ有用なのかを説明しています。]