JavaScript
es6
ECMAScript6
ECMAScript2015

ECMAScript 2015 の Proxy(Proxies) / Reflect をなんとなく理解する

More than 1 year has passed since last update.

今回は過去に Blog で書いた記事のまとめです
Microsoft Edge の機能を調べてみた - Proxy 初級編
Microsoft Edge の機能を調べてみた - Reflect 編

注意点 - Chrome (Opera)、Safari では使えない

Proxy / Reflect の機能をすべて使用可能なブラウザは現状 Microsoft Edge のみです。
Firefox はほぼ網羅しておりますが、Chrome と Safari では実装されていません。
V8 や JavaScriptCore で実装されているため、そのうち Chrome や Safari でも導入されるかもしれません。

参考サイト
http://kangax.github.io/compat-table/es6/

ざっくり説明

Proxy はオブジェクトが持つ機能を横取りして、自前でつくった命令を実行させることができます。
Reflect はオブジェクトが本来持つ機能を返します。

Proxy

空の Proxy をつくってみよう

sample1.js
var o = {};
var p = new Proxy(o,{});
p.a = 10;
// p =  {a: 10}、o =  {a: 10}

空のオブジェクト(target)に、空の命令(handler)を入れています。
ハンドラが空であるため、ここではただオブジェクトをコピーしたことになり、
Proxy の変更は元のオブジェクトにも適応されます。

ひとまず用語説明

用語 説明
target Proxy が振る舞いを変更するオブジェクト。Proxy を含め大体設定できます。
traps オブジェクトが持つプロパティへアクセスする為のメソッド
handler traps と自前で何かする命令を含んだオジェクト

と書いても、わかりずらいのでサンプル。

Proxy を適応したオブジェクトのプロパティに数字を入れると倍になって格納されるサンプル。

sample2.js
var target = {};
var handler = {
    set(target,name,value){
        if(typeof value === "number"){
            value *= 2;
        }
        target[name] = value;
    }
};
var p = new Proxy(target,handler);
p.a = 100;
p.b = 200;
p.c = "200";
// p = {a: 200, b:400, c:"200"}

空の target となるオブジェクト作成。
handler にオブジェクトに値を設定する trap である set で値を代入する機能を奪う。
Proxy に target、handler にセット。
その Proxy に値を入れると倍に。文字列はスルーされます。

set() の引数

サンプル通りですが、
第1引数は var p = new Proxy(target,handler) で設定した target となるオブジェクト、
第2引数は オブジェクトのプロパティ名、
第3引数は オブジェクトの値です。

set() でオブジェクト代入時の振る舞いを改変できるということは、
例えば、Proxy となったオブジェクトへ代入するだけで HTML タグの値を変更させるとかができたりします。
要するに Object.observe に近しいことが可能になるわけです。

以下、さらっとつくったものなのでつくりは雑ですが、最後の input 要素に入力し change のイベントが走るとプロパティ名と同じ ID の要素に入力した値が入ります。
p という Proxy のプロパティが変更されたら値を変えるようにしているため、console で p.v2 = 100 のように変更すると2番目の input の値が変わります。

さんぷる (http://toshihirogoto.github.io/ac_js_sample/)

sample.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Proxy Test</title>
</head>
<body>
    <input type="text" id="v1" value="0"><br>
    <input type="text" id="v2" value="0"><br>
    <input type="text" id="v3" value="0"><br><br>
    <label for="v">input: </label><input type="text" name="v" id="v"><br><br>

    <script>
        var t = {}
        var h = {
            set(target,name,value){
                f(name,value) // 関数にプロパティ名と値を渡す
                target[name] = value
            }
        }
        var p = new Proxy(t, h)

        // プロパティ名と同じ ID に値を入れる
        var f = function(n, v){
            document.getElementById(n).value = v
        }

        var v =  document.getElementById("v")
        v.addEventListener("change", function(){
            p.v1 = this.value
            p.v2 = this.value
            p.v3 = this.value
        })
    </script>
</body>
</html>

今回は set の trap のみですが、他の trap を使うことで JavaScript の様々な振る舞いを変更することができます。

Proxy の traps 一覧

以下の表は現状動作するものです。
また、メソッドの中身が空の場合は本来の機能を返します。

機能 ハンドラメソッド
関数呼び出しに対するトラップ handler.apply()
new からのコンストラクタ呼び出し操作に対するトラップ handler.construct()
Object.defineProperty()に対するトラップ handler.defineProperty()
delete 操作に対するトラップ handler.deleteProperty()
for...in 文に対するトラップ handler.enumerate()
プロパティ値を取得することに対するトラップ handler.get()
Object.getOwnPropertyDescriptor()に対するトラップ handler.getOwnPropertyDescriptor()
GetPrototypeOf 内部メソッドに対するトラップ handler.getPrototypeOf()
in 操作に対するトラップ handler.has()
Object.isExtensible()に対するトラップ handler.isExtensible()
Object.getOwnPropertyNames()に対するトラップ handler.ownKeys()
Object.preventExtensions()に対するトラップ handler.preventExtensions()
プロパティ値を設定することに対するトラップ handler.set()
Object.setPrototypeOf()に対するトラップ handler.setPrototypeOf()

詳しい機能説明は下にある MDN のリンクを参照。

Reflect

Proxy と同じメソッドを持ち、静的な関数を返します。

Reflect から値を出力する機能にアクセスする

sample3.js
var obj = { x: 1, y: 2 }; 
Reflect.get(obj, "x"); // 1 

Proxy と共に使う

大概の場合 Proxy の handler の中で使われると思われます。

sample4.js
var obj = { val0 : 0, val1: 1 }
var handler = {
    has(target,name){
        console.log(`こちらに ${name} プロパティはありますか?`);
        return Reflect.has(target,name);
    }
}
var proxy = new Proxy(obj,handler);
console.log('val1' in proxy);
// こちらに val1 プロパティはありますか?
// true

has() は「"プロパティ名" in オブジェクト」を使った際に、
指定したプロパティの有無を調べる際に動作する trap で boolean を返します。

in オペレータが動いた時点で has() の中身を実行しますが、自前で has() を変更した場合は、プロパティを調べる処理から値を返す必要があり面倒です。

そこで Reflect.has() 使い元のオブジェクトの機能をそのまま返すことでプロパティを調べる処理をつくらずに済みます。

まとめ

ざっくりとした説明ですが、この2つの機能を使うことでオブジェクトに対する振る舞いをほぼ変更できます。オブジェクトに対して何かを行うとそれに連携させる機能を付加することができるため幅広く応用できそうですね。
また、利点はあまりありませんがオブジェクト本来の動作を止めることもできます。

個人的には ECMAScript 2015 で最も面白く、重要な機能だと思っています。

参考サイト

MDN の方で Proxy と Reflect が日本語化されていたのでこちらもどぞ。
Proxy - JavaScript | MDN
Reflect - JavaScript | MDN

JS.next のブログでさらに詳しく書かれていますので、もっと詳しく知りたい方をこちらを読んで理解を深めるとよいでしょう。
http://js-next.hatenablog.com/entry/2015/12/03/045720