Reflect#
ディズニー版のムーランには「Reflection」という曲があり、ムーランは水面に映る自分を見つめ、未来の選択について考えています。歌詞と映像は、Reflection という言葉の二つの意味、すなわち:
- 反射、映し出す
- 沈思、内省
を同時に含んでいます。
プログラミングにおける Reflect を「反射」と訳すと確かに混乱を招くかもしれませんが、実際には「自省」の方向に寄せるべきです。Reflect は JavaScript のメタプログラミングにおいて重要なメソッドの一つであり、メタプログラミングはより高次元で望む機能を実現することができます。例えば、プログラムが本来の挙動を調整したり、内部メソッドを呼び出したり、プログラミングの方法でコードを書くことができます。もちろん、Reflect は JavaScript の独占用語ではなく、go、java などの言語にも Reflect メソッドがあります。
JavaScript の Reflect には以下のメソッドがあります:
Reflect.apply()Reflect.construct()Reflect.defineProperty()Reflect.deleteProperty()Reflect.get()Reflect.getOwnPropertyDescriptor()Reflect.getPrototypeOf()Reflect.has()Reflect.isExtensible()Reflect.ownKeys()Reflect.preventExtensions()Reflect.set()Reflect.setPrototypeOf()
これだけのメソッドを見ると、ああ、見覚えがあるなと感じます。ここには多くのメソッドが以前どこかで使われたようです。実際、Reflect は Object や Function に掛かっているいくつかのメソッドをここに集約しています。
// 例えばお馴染みの apply
Function.prototype.apply.call(Math.floor, undefined, [1.75])
// こう書くこともできます
Reflect.apply(Math.floor, undefined, [1.75])
これにより、Reflect を使用する最初の明らかな利点、コードの簡素化がもたらされます。さらに、apply は実際には JavaScript エンジンの内部メソッド [[Call]] を呼び出しているため、Reflect はメタプログラミングの範疇に入るのです。
例えば、Reflect.ownKeys は Object.getOwnPropertyNames の機能を担い、オブジェクトの属性を検視します:
const object1 = {
a: 1,
b: 2,
c: 3,
}
console.log(Reflect.ownKeys(object1))
console.log(Object.getOwnPropertyNames(object1))
// 期待される出力: Array ["a", "b", "c"]
Reflect.deleteProperty() や Reflect.has() のようなメソッドは、いくつかの演算子の機能を担っています:
const obj = { x: 1, y: 2 }
Reflect.deleteProperty(obj, 'x') // true
console.log(obj) // { y: 2 }
// deleteProperty と delete の機能は同じですが、deleteProperty は操作結果を返し、delete は静かに操作します
// それでも、delete 操作は本当に失敗するのでしょうか...
const obj = { x: 1, y: 2 }
delete obj.x
console.log(obj) // { y: 2 }
get、set などのメソッドも、前述のように内部メソッド [[Get]]、[[Set]] を呼び出します:
const object1 = {
x: 1,
y: 2,
}
console.log(Reflect.get(object1, 'x'))
しかし、ここで混乱が生じます。明らかに object1.x も内部メソッド [[Get]] を呼び出しており、しかもより簡単です。なぜわざわざ Reflect.get のような面倒な方法を使うのでしょうか?答えは **Proxy と組み合わせるため ** です。
Proxy#
Proxy は理解しやすい概念で、皆さんも特定の分野(犬の頭)でこの言葉をよく目にすることでしょう。ここでも同じ意味で、代理を指します。Proxy インスタンスはオブジェクトであり、このオブジェクトに対する操作は他の関数によって代理されます。
基本的な Proxy は次のようになります:
const target = {
message1: 'hello',
message2: 'everyone',
}
const handler = {}
const proxy1 = new Proxy(target, handler)
実際、このような Proxy では何も起こりません。すべては通常通りです。何かを起こしたい場合は、handler を工夫する必要があります。
function proxyFactory(traps) {
let target = {}
let handler = {}
for (let i = 0; i < traps.length; i++) {
let trap = traps[i]
handler[trap] = function (...args) {
console.log('trap: ' + trap, args)
return Reflect[trap](...args)
}
}
let test = new Proxy(target, handler)
return test
}
proxyFactory には traps リストを渡す必要があり、関数が実行されると trap が handler に配置され、次に new Proxy(target, handler) で trap が代理オブジェクトの操作に使用されます。
では、trap とは何でしょうか?「罠」と呼ぶのも良いですが、実際にはいくつかの操作をこの「罠」に「引き込む」ことを意味し、直接罠関数を実行します。
対応するオブジェクトの内部メソッドの動作を定義する関数です。(これはオペレーティングシステムにおけるトラップの概念に類似しています。)
MDN の定義によれば、trap はオブジェクトの内部メソッドの動作を定義する関数であり、これは Reflect の機能と完全に一致します。実際、Reflect のメソッド名は trap の名前と完全に一致するため、次のように設定できます:
let test = proxyFactory(Reflect.ownKeys(Reflect))
こうして得られた test オブジェクトは、すべての内部メソッド呼び出し時にログを出力します。例えば:
test.a = 1
// trap: set (4) [{…}, 'a', 1, Proxy]
// trap: getOwnPropertyDescriptor (2) [{…}, 'a']
// trap: defineProperty (3) [{…}, 'a', {…}]
test.a
// trap: get (3) [{…}, 'a', Proxy]
'a' in test
// trap: has (2) [{…}, 'a']
例では操作と引数を表示するだけですが、実際には trap 内でオブジェクトに対して任意の処理を行うことができ、これによりプログラミングの思考が広がります。例えば、Vue3 のリアクティブ原理は Reflect と Proxy に密接に関連しています。
https://ssshooter.com/2023-01-30-javascript-reflect/
Takeaway#
Reflectは JavaScript のメタプログラミングにおいて重要なオブジェクトであり、オブジェクトの内部メソッドを呼び出し、内部操作を行うことができます。ReflectはFunction.prototype.apply.call、Object.getOwnPropertyNamesなどの関数をより直感的で簡潔にします。Reflectはinなどの演算子の機能を担い、操作結果を返します。ReflectはProxyと完璧に組み合わせることができ、オブジェクト操作の動作を自由に定義できます。