Skip to content

Instantly share code, notes, and snippets.

@bellbind
Last active January 20, 2022 04:26
Show Gist options
  • Save bellbind/8f33d81458dd454b430d4cd949076b30 to your computer and use it in GitHub Desktop.
Save bellbind/8f33d81458dd454b430d4cd949076b30 to your computer and use it in GitHub Desktop.
[es6]research on ES6 `Proxy`
# Add plugin for your editors
# see: http://editorconfig.org
root = true
[*]
end_of_line = lf
charset = utf-8
tab_width = 8
[*.md]
indent_style = space
indent_size = 4
trim_trailing_whitespace = true
insert_final_newline = true
[*.{js,json}]
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true

ES6 Proxyについて学ぼう: 基礎編

ES6で新に追加されたProxyについて、その概念や仕組みについての基礎編と、 様々な状況でのProxyの使い方についての活用編に分けてあります。

この基礎編は、Proxyの仕組み全体についてまとめたものです。 細部はECMAScriptの仕様文書でのランタイム仕様に基づいていて、 まったく手軽なものではありません。 むしろ、コード例で応用の仕方をまとめた活用編のほうが、 Proxyのもつ意味について認識しやすいかもしれません。

この文書を書くにあたって、Proxyについて自ら調査し、 Proxyの視点で単一の文書としてまとめていきました。 Proxyを調べていく時点で判明したのは、 ProxyというのはECMAScript内部のランタイム仕様に強く関わるものであり、 きちんと扱うには横断した仕様の解釈が必要となることです。 このために、(どのランタイム実装でも実装されてもいないmodulesを含めて) その他のES6機能にくらべて、文書が極端に少ない状況にあるのだと思われます。

ES6 Proxyとは

Proxyは、ES6(ECMAScript2015)より追加されたビルトインオブジェクトです。 ブラウザ固有拡張にあった__proto____noSuchMethod__等による 動的な オブジェクト実装の機能 を、標準仕様 として可能にするものです。

Promiseと違い、その実現には実行環境の支援が必要なしくみであり、 babel等のtranspilerやJavaScript実装の Polyfillでは、 完全なサポートができない ものです。

また、標準に組み込まれることでPromiseがJavaScriptによる 非同期プログラミングの標準モデルとなりつつあるように、 Proxy.revocableが標準に組み込まれている点から、 Proxyを用いた Object Capability によるアクセス制御モデルが JavaScriptでの標準となっていくのかもしれません。

ES6 Proxyの基本構造

まずproxy = new Proxy(target, handler)の式で、Proxyオブジェクトが作られます。 ここで、以下の要素とその関係を軽くまとめておきます:

  • インスタンス: ここでは値ではないオブジェクト

    • null以外でtypeof"object", "function"になるもの全般
      • "boolean""number""string""undefined""symbol"は値
  • Proxy : new Proxyで生成されるインスタンス

    • 実行環境によるサポートが必然である例として、 new Proxy({}, {}) instanceof Proxyは必ずfalseになります
  • Target: Proxyのデフォルト実行の対象になるインスタンス

    • typeof演算子等でtargetの属性が用いられます
    • proxy === targetは必ずfalseになります
  • Handler: Proxyインスタンスに対する各種インスタンスアクセスへのTrap群を 1つにまとめたインスタンス

    • Trapの名前は、Handler上でのプロパティ名になります
  • Trap: 各インスタンスアクセスに対応した関数(function インスタンス)

    • handler.has(target, name) {...}など

例として、console.log(proxy.member)という式が実行されると、 Trapのhandler.get(target, "member", proxy)として呼び出され、 このProxyを作るときに与えたHandler上のTrap handler.get()のコードが実行され、 その結果(戻り値、例外)が上記のproxy.member式の評価値へ反映される、 という関係になっています。

Reflectビルトイン

ProxyとともにES6で追加されたビルトインオブジェクトReflectは、 "Esential Internal Method"と呼ばれるエンジンの内部機能に 対応した13の関数を持つオブジェクトです。

  • Reflect.isExtensible(obj) => [[IsExtensible]]()
  • Reflect.preventExtensions(obj) => [[PreventExtensions]]()
  • Reflect.getPrototypeOf(obj) => [[GetPrototypeOf]]()
  • Reflect.setPrototypeOf(obj, proto) => [[SetPrototypeOf]](V)
  • Reflect.getOwnPropertyDescriptor(obj, name) => [[GetOwnProperty]](P)
  • Reflect.defineProperty(obj, name, desc) => [[DefineOwnProperty]](P, Desc)
  • Reflect.get(obj, name, receiver) => [[Get]](P, Rceiver)
  • Reflect.set(obj, name, value, receiver) => [[Set]](P, V, Receiver)
  • Reflect.has(obj, name) => [[HasProperty]](P)
  • Reflect.onwKeys(obj) => [[OwnPropertyKeys]]()
  • Reflect.apply(func, thisArg, args) => [[Call]](thisArgument, argumentsList)
  • Reflect.construct(func, args, newTarget) => [[Construct]](argumentList, newTarget)

Essential Internal Methodはstringなどの値にも存在しますが、 Reflectの関数はインスタンスに対してのみ利用が可能です。

ProxyでTrapが存在しないHandlerのデフォルトは、 Targetに対してそのTrapに対応するEssential Internal Methodを実行するものです。 よってReflectは、 ProxyのデフォルトのTrap実装 を集めたオブジェクトとも言えます。

  • すなわちnew Proxy({}, {})new Proxy({}, Reflect)は、 すべて同じProxy処理を行います

メタプログラミングとしてES5追加のObject付属関数が有用であるように、 Proxy抜きであってもReflect単独で有用です。

  • Reflectの関数は、 ES5でのObjectに付随したメタプログラミング用関数群と似ているが、 細かい点に違いがあります
    • 違いとしては、失敗時に、エラーを投げるか、戻り値でfalseを返すか、 ES6のSymbolも扱う、など
    • Object.getOwnPropertyNames(obj)Reflect.ownKeys(obj)Object.defineProperty(obj, name, desc)Reflect.defineProperty(obj, name, desc)など

ES6新規の構造にも対応している点から、 ES6環境であればObject付属の関数から、Reflect付属の同種の関数に 置き換えるのもよいでしょう。

また、func.apply()は、内部で[[Call]]を呼び出す実装のメソッドであり インスタンスで上書きすることも可能です。Reflect.apply(func)を使えば、 上書きされていても直接的に[[Call]]を起動できます。

TargetからProxyへの制約の継承

ProxyのTrapでは、ただtargetをReflect経由で各デフォルト処理へ 転送する以外にも、差し替えや仮想的な属性があるかのように見せかけるなどの 様々なことが可能です。

ただしProxyのTrapでは、どんなことでも可能になるわけではなく、 Target側で制約が設定される ことによって、 Trapで可能な結果が制限される ようになっています。

以下は、その制約の元になる情報です:

  • Object.isExtensibile(target): これがfalseのとき、Trap実行では、targetに対するデフォルトの結果と くらべて違いが出るような結果を返すことができない

    • ES5で追加されたオブジェクトの属性変更を制限するビルトイン 関数Object.freeze(o)Object.seal(o)Object.preventExtensions(o) のいずれかが適用されると、Object.isExtensible(o)falseになる
  • desc.configurable: Object.defineProperty等で各プロパティに設定する プロパティデスクリプタでのメンバーのconfigurablefalseのとき、 プロパティに関するTrapでは、その名前のプロパティに対してはtargetに対する 結果とくらべて違いが出る結果を返すことができない

    • ES5の時点で、configurablefalseの時、同一オブジェクトだけでなく、 そのオブジェクトをプロトタイプとして持つオブジェクトに対しても Object.defineProperty()等ができなくなる
    • この考えが、TargetとProxyのあいだの関係にも拡大された感じ
  • desc.writable: プロパティデスクリプタのwritablefalseのとき、 get Trapとset Trapでは、その名前のプロパティに対しては targetに対する結果とくらべて違いが出るような結果を返すことができない

    • falseのとき、get Trapでtargetでの値と違う値は返せない
    • falseのとき、set Trapでfalseを返さなくてはいけない

Trapでこれらの制約に違反した結果を返す実装をすると、 Trapが発動する操作がProxyに対して行われたとき、 (strict modeいかんにかかわらず)ProxyからTypeErrorが発生します。

ECMAScriptにおいてTypeErrorとは、実行時に、 各構文やビルトイン関数と、そこで扱う値とのあいだで、 なんらかの不整合がある場合全般で発生する例外です。

各Essential Internal Methodにおける具体的な制約の条件は、 ECMAScript仕様では、 "6.1.7.3 Invariants of the Essential Internal Methods" および "9.5 Proxy Object Internal Methods and Internal Slots" に記載されています。

注意したいのは、デフォルトTrap実装がそうであるというだけであり、 Trap実装の中でアクセス対象にするインスタンスを new Proxyでのtargetとして設定する必然性はあまりありません。

制約からの制限を外したければ、targetには{}function () {}など、 同種で無制約のままのインスタンスを渡し、各Trapでtargetは無視して、 制約されたオブジェクトに対してアクセスさせればよいだけです。

このため、このTargetの制約の継承は、 制約設定をProxyで活用するために活用することになるのではないしょうか。

ProxyについてのECMAScript仕様の読み方

たとえば、Object.keys(obj)がどうProxyに影響するかを調べましょう。

Object.keysは、仕様では、"19.1.2.14 Object.keys ( O )"にあります。 その中で"7.3.21 EnumerableOwnNames (O)"が参照され、 そこの2でEssential Internal MethodのO.[[OwnPropertyKeys]]()が参照されており、 ProxyからownKeys Trapが呼び出されます。

そして、この下4にO.[[GetOwnProperty]](key)があることを忘れてはいけません。 ここでProxyからgetOwnPropertyDescriptor Trapが呼び出され、 その結果がundefinedでなく、enumerabletrueである場合に keyが結果であるnamesリストに追加される、と書いてあります。

5では、namesの順序を、"13.7.5.15 EnumerateObjectProperties (O)"での順序 と同じにすることが記載されています。

ここから、Object.keysに作用させるProxyを作るには、 ownKeys Trapでリストを返すだけでなく、getOwnPropertyDescriptor Trapで リスト中の名前に対してenumerabletrueになるプロパティデスクリプタを 返す必要もあります。

そして、さらに呼び出されるProxyの各Trapの条件もチェックします。

"9.5.11 OwnPropertyKeys ( )" のNOTEに条件がまとめられています。

  • Targetの各keyでのプロパティデスクリプタのconfigurablefalseなとき、Trapの結果にそのkeyが含まれていなければTypeError
  • IsExtensible(target)falseの場合、Targetのプロパティリストと Trapの結果との間に過不足があったらTypeError

また、"9.5.5 GetOwnProperty (P)"のNOTEを見ると、

  • Targetのデスクリプタがconfigurablefalseの場合、undefinedを返すと TypeError
  • IsExtensible(target)falseの場合、Targetのデスクリプタが存在する場合 undefinedを返すとTypeError
  • IsExtensible(target)falseの場合、Targetのデスクリプタが存在しない場合、 デスクリプタを返すとTypeError
  • Targetのデスクリプタが存在しないか、 デスクリプタのconfigurabletrueの場合、 configurablefalseのデスクリプタを返すとTypeError

つまり、enumerabletrueなデスクリプタを返すには、 この最後の条件も満たす必要があるのです。

最終的にこれらをまとめると、Object.keys(proxy)Object.keys(target) に存在しないプロパティを返すためには、 以下の3条件を満たすようTrapを実装する必要があることが判明します:

  • Reflect.isExtensible(target)trueであること
  • ownKeys Trapで返す各プロパティについて、Targetのデスクリプタが 存在してる場合に、そのconfigurablefalseでないこと
  • ownKeys Trapで返す各プロパティについて、 getOwnPropertyDescriptor Trapでは以下の内容のデスクリプタを返すこと
    • enumerabletrueであること
    • configurabletrueであること

このように、Proxyに対する仕様の読み方としては、 ProxyのTrapが発動するビルトインや構文の"Runtime Semantics"を起点に、 内部仕様をたぐってくことになります。

また、上記でenumerabletrueのデスクリプタを返すためには 別の条件からconfigurabletrueにしなくてはいけないように、 複数の仕様から導かれる条件が生まれることにも注意する必要があります。

ES6 ProxyのTrap群

以下のコードは、Handlerインスタンスの全13 Trapを、 5カテゴリに分けて列挙したものです:

const handler = {
  //[define proxy extensibility]
  isExtensible(target) {//=> bool (extensible or not)
    return Reflect.isExtensible(target);
  },
  preventExtensions(target) {//=> bool (block extending or not)
    return Reflect.preventExtensions(target);
  },

  //[access target.constructor.prototype]
  getPrototypeOf(target) {//=> prototype object
    return Reflect.getPrototypeOf(target);
  },
  setPrototypeOf(target, proto) {//=> bool (success or not)
    return Reflect.setPrototypeOf(target, proto);
  },

  //[configure properties]
  getOwnPropertyDescriptor(target, name) {//=> descriptor object (or undefined)
    return Reflect.getOwnPropertyDescriptor(target, name);
  },
  defineProperty(target, name, desc) {//=> bool (success or not)
    return Reflect.defineProperty(target, name, desc);
  },

  //[basic property access]
  get(target, name, receiver) {//=> value
    return Reflect.get(target, name, receiver);
  },
  set(target, name, value, receiver) {//=> bool (success or not)
    return Reflect.set(target, name, value, receiver);
  },
  deleteProperty(target, name) {//=> bool (success or not)
    return Reflect.deleteProperty(target, name);
  },
  has(target, name) {//=> bool
    return Reflect.has(target, name);
  },
  ownKeys(target) {//=> [name]
    return Reflect.ownKeys(target);
  },

  //[proxy as functions] (required `typeof target === "function"`)
  apply(target, thisArg, args) {//=> value (result as function)
    return Reflect.apply(target, thisArg, args);
  },
  construct(target, args, newTarget) {//=> object instance
    return Reflect.construct(target, args, protoCtor);
  },
};

Proxyインスタンスのアクセス式とTrap発動の関係

以下は、各Trapと、それが発動する代表的な式や構文との関係です:

  • handler.isExtensible(target)

    • Object.isExtensible(proxy)
    • Object.isSealed(proxy)
    • Object.isFrozen(proxy)
  • handler.preventExtensions(target)

    • Object.preventExtensions(proxy)
    • Object.seal(proxy)
    • Object.freeze(proxy)
  • handler.getPrototypeOf(target)

    • proxy instanceof Constructor
    • Object.getPrototypeOf(proxy)
    • proxy.__proto__, proxy.valueOf(), (handler.getで差し替えてない場合)
    • Object.create(proxy)Object.assign(proxy)
  • handler.setPrototypeOf(target, proto)

    • Object.setPrototypeOf(proxy, proto)
    • proxy.__proto__ = proto (handler.setで差し替えてない場合)
  • handler.getOwnPropertyDescriptor(target, name)

    • Object.getOwnPropertyDescriptor(proxy, name)
    • proxy.valueOf(), (handler.getで差し替えてない場合)
    • Object.create(proxy)Object.assign(proxy)
  • handler.defineProperty(target, name, desc)

    • Object.defineProperty(proxy, name, desc)
    • Object.defineProperties(proxy, descs)
  • handler.get(target, name, receiver)

    • proxy.name, proxy[name], proxy.name(), proxy[name]() (および、属性参照をするビルトイン: Object.assign({}, proxy), ...)
    • (destructuring) let {name} = proxy;
    • proxy.valueOf(), proxy[Symbol.toPrimitive] を内部で駆動する各種の演算子: +proxy, proxy == true, proxy * 10, ...
  • handler.set(target, name, value, receiver)

    • proxy.name = value, proxy[name] = value (および、属性代入をするビルトイン: Object.assign(proxy, {foo:1}), ...)
  • handler.deleteProperty(target, name)

    • delete proxy.name, delete proxy[name]
    • Object.deleteProperty(proxy, name)
  • handler.has(target, name)

    • name in proxy
  • handler.ownKeys(target)

    • Object.getOwnPropertyNames(proxy)
    • Object.getOwnPropertySymbols(proxy)
    • for (name in proxy) {...} (注: 要デスクリプタでenumerable)
    • Object.keys(proxy) (注: 要デスクリプタでenumerable)
  • handler.apply(target, thisArg, args)

    • proxy(...args)
    • ({m: proxy}).m(...args)
    • proxy.apply(thisArg, args)
    • proxy.call(thisArg, ...args)
  • handler.construct(target, args, newTarget)

    • new proxy(...args)

また、Reflectの同名関数をProxyに対して呼び出すと、 第二引数以降が同じ値となって、Trapが発動します。 ただし、デフォルト値は、Reflectの関数呼び出し時でのデフォルト値が 渡ります。

  • Reflect.get(proxy, name, value, obj) => handler.get(target, name, value, obj)
  • Reflect.get(proxy, name, value) => handler.get(target, name, value, proxy)
  • Reflect.construct(proxy, [], func) => handler.construct(target, [], func)
  • Reflect.construct(proxy, []) => handler.construct(target, [], proxy)

そして以下の式に対しては、ProxyからTrapは発動しません:

  • typeof proxy
    • typeof targetの結果がそのまま使われる
  • proxy === value, proxy !== value
    • 通常のオブジェクトとしての同一性で、Proxyインスタンスの同一性を評価する
    • p = new Proxy(t, h); p === p
    • new Proxy(t, h) !== new Proxy(t, h)
    • new Proxy(t, h) !== t

Trapの引数や戻り値についての補足

ここは、各Trapの引数や戻り値について、誤解しやすい点に注目したものです。

成功失敗を表すbool戻り値の意味

Trapの戻り値がbool (success or not)である、いうのは、 falseを返すと、(strict modeのとき)Proxyアクセスした側で TypeErrorを発生させることを意味します。

const proxy = new Proxy({}, {
  set(target, name, value, receiver) {
    return false;
  }
});

(function () {
  "use strict";
  proxy.foo = "foo"; //throw> TypeError
})();

Targetに対して実際に同種の処理を適用して成功したかどうかのことではありません。

const proxy = new Proxy({}, {
  set(target, name, value, receiver) {
    return true;
  }
});

(function () {
  "use strict";
  proxy.foo = "foo";      //(pass silently)
  console.log(proxy.foo); //=> undefined
})();

プロパティ名の型

引数のnamestringsymbolです。 symbol以外はすべてstring化されます。 targetArrayのインデックスアクセスであってもnumberにはなりません。

const proxy = new Proxy([], {
  get(target, name, receiver) {
    return typeof name;
  }
});

console.log(proxy[0]);                 //=> string
console.log(proxy[null]);              //=> string
console.log(proxy[undefined]);         //=> string
console.log(proxy[{}]);                //=> string
console.log(proxy[new String("key")]); //=> string

console.log(proxy[Symbol("key")]); //=> symbol

getter/setter用レシーバー

getsetの最後の引数のreceiverは、プロパティアクセスreceiver.property の右のオブジェクトです。別の表現では、メソッド実行receiver.property()で、 thisとなるものです。普通はProxyになっているでしょう。

そうでない場合はたとえば、Proxyをプロトタイプにしたオブジェクトの プロパティアクセスで、receiverはそのオブジェクトになります。

const target = {};
const proxy = new Proxy(target, {
  get(target, name, receiver) {
    return receiver;
  }
});

console.log(proxy.foo === proxy);  //=> true
console.log(proxy.foo !== target); //=> true

// proxy as a prototype
const obj = Object.create(proxy);
console.log(Reflect.getPrototypeOf(obj) === proxy); //=> true

console.log(obj.foo === obj);   //=> true
console.log(obj.foo !== proxy); //=> true

receiverが重要なのは、プロパティがgetter/setterである時です。 receiverは、getter/setter関数の中で使うthisとなるものです。

const target = {value: 5};
Object.defineProperty(target, "prop", {
  get() {
    return this.value * 10;
  }
});

// use proxy value
const proxy = new Proxy(target, {
  get(target, name, receiver) {
    if (name === "value") return 2;
    return Reflect.get(target, name, receiver);
  }
});
console.log(proxy.prop); //=> 20;

// use target value
const buggy = new Proxy(target, {
  get(target, name) {
    if (name === "value") return 2;
    return Reflect.get(target, name);
  }
});
console.log(buggy.prop); //=> 50;

new.targetとは

construct TrapのnewTargetは、 生成されたインスタンスのconstructorプロパティになるコンストラクタ関数です。 通常のnew呼び出しでは(getter/setterのreceiver同様に)proxyとなります。

const target = function Ctor() {};
const proxy = new Proxy(target, {
  construct(target, args, newTarget) {
    return {newTarget};
  }
});

console.log(new proxy().newTarget === proxy);  //=> true
console.log(new proxy().newTarget !== target); //=> true

また、Reflect.construct(ctor, args, newTarget)を直接使うことで、自由に、 第三引数で(newが可能な)functionオブジェクトをセットすることもできます。

この場合ではctorが実行されたとき、その中でのthis.constructornewTargetに なります(さらにnew.target式の値もnewTargetになります)。 単にconstructorプロパティにセットされるだけであって、 newTarget関数としては一切実行されません。

const ctor = function Ctor() {
  this.newTarget = new.target;
};

console.log(Reflect.construct(ctor, []).newTarget === ctor); //=> true

const other = function Other() {
  this.extra = true;
};
other.prototype.method = function () {
  return "OK";
};

const obj = Reflect.construct(ctor, [], other);
console.log(obj.newTarget === other); //=> true
console.log(!(obj instanceof ctor));  //=> true
console.log(obj instanceof other);    //=> true
console.log(obj.extra);               //=> undefined
console.log(obj.method());            //=> "OK"

拡張可能設定のTrap

  • handler.isExtensible(target): boolean (extensible or not)
  • handler.preventExtensions(target): boolean (prevent or not)

isExtensible Trapは必ずReflect.isExtensible(target)と同じ値を 返さなくてはいけません。結果の差し替えが不可能です。

const proxy = new Proxy({}, {
  isExtensible(target) {return false;}
});

Object.isExtensible(proxy); //throw> TypeError

preventExtensions Trapの戻り値は、falseだと「制約させられない」、 という意味になります。具体的にはObject.preventEextensions(proxy)等 を呼ぶとTypeErrorが出るようになります。

const proxy = new Proxy({}, {
  preventExtensions(target) {
    return false;
  }
});

const o = Object.freeze(proxy); //throw> TypeError

preventExtensions Trapの戻り値を別の値にすることは、 isExtensible(target)trueのときに、targetにタッチせずfalseを返して Object.preventExtensions等をできないようにする場合などです。

プロトタイプ設定のTrap

  • handler.getPrototypeOf(target): prototype object (or undefined)
  • handler.setPrototypeOf(target, proto): boolean (success or not)

ブラウザ拡張実装由来で、ES6 Annex Bで追認された インスタンスごとのプロトタイプ設定obj.__proto__の読み書きに相当する機能です。 obj.__proto__の読み書きの代わりに、Reflect.getPrototypeOf(obj)Reflect.setPrototypeOf(obj, proto)が使えます。

ただしobject.__proto__の実装はgetter/setterであり、その中で getPrototypeOf(this)/setPrototypeOf(this, proto)が呼び出される仕様です。

Proxyオブジェクトへのproxy.__proto__アクセスは、 まず__proto__への get(target, name, receiver)/set(target, name, value, receiver) Trapが 呼ばれ、デフォルトではそこでtargetのgetter/setterが取り出され、 receiverであるProxyをthisにして実行されます。 そこからgetPrototyoeOf(proxy)/setPrototyoeOf(proxy, proto)が呼ばれ、 それによって handler.getPrototypeOf(target)/setPrototypeOf(target, proto) Trapが 呼び出される、という仕組みになります。

const proxy = new Proxy({}, {
  getPrototypeOf(target) {
    return new Date();
  },
  get(target, name, receiver) {
    return Reflect.get(target, name, receiver);
  }
});

console.log(proxy.__proto__ instanceof Date); //=> true

get/set Trapで最後の引数receiverを省略してしまうと、 receivertargetになり、 getPrototypeOf(target)/setPrototypeOf(target, proto)に直接 つながってしまい、 getPrototypeOf/setPrototypeOf Trapに届かなくなるので注意です。

const proxy = new Proxy({}, {
  getPrototypeOf(target) {
    return new Date();
  },
  get(target, name) {
    return Reflect.get(target, name);
  }
});

console.log(proxy.__proto__ instanceof Date); //=> false

属性設定のTrap

  • handler.getOwnPropertyDescriptor(target, name): descriptor object (or undefined)
  • handler.defineProperty(target, name, desc): boolean (success or not)

ES5でのObject.getOwnPropertyDescriptor(obj, name)Object.defineProperty(obj, name, desc)に対応して呼び出されるTrapです。

Object.definePropertyの戻り値は設定したオブジェクト またはTypeError発生ですが、 Reflect.definePropertyの戻り値はbooleanです。

あと注意点としては、configurablefalseのときにReflect.defineProperty 実行が必ずfalseになるわけではなく、valueも含めて同じ値になる デスクリプタであれば結果はtrueになります (Object.definePropertyの成否と同じ)。

const obj = {};
Reflect.defineProperty(obj, "prop", {configurable: false, value: 100});

console.log(
  Reflect.defineProperty(obj, "prop", {configurable: false, value: 100})
); //=> true

console.log(
  Reflect.defineProperty(obj, "prop", {configurable: false, value: 10})
); //=> false

Object.getOwnPropertyDescriptorは数値や文字列に対しても正常として実行 しundefinedを返しますが、 Reflect.getOwnPropertyDescriptorはインスタンス以外に 対して行うとTypeErrorが発生します。

属性アクセスのTrap

  • handler.has(target, name): boolean
  • handler.get(target, name, receiver): value
  • handler.set(target, name, value, receiver): boolean (success or not)
  • handler.deleteProperty(target, name): boolean (success or not)
  • handler.ownKeys(target): Array of names

has Trapは、in演算子や、withにProxyをセットした時に ブロック内での(let/const以外の)変数へのアクセス等で発動します。

const proxy = new Proxy({}, {
  has(target, name) {
    if (name === "console") return false;
    console.log("[has]", name);
    return true;
  }
});

"foo" in proxy; //=> "[has] foo"

with (proxy) {
  var bar = 100; //=> "[has] bar"
}

get/set Trapでreceiverが存在するのは、プロパティがgetter/setter関数 な場合、その関数で使うthisとなるオブジェクトを受け取るためです。 Reflect.getReflect.settargetにフォワードする場合では、 receiverproxyになることで、targetのgetter/setter関数でのthisへの プロパティアクセスから、さらにget/set Trapが発動されることになります。

ownKeys Trapのデフォルト実装であるReflect.ownKeys(obj)は、 以下の順でソートされたリストを返します:

  • 非負整数値の文字列: 数値として昇順
  • その他文字列: 追加順(chronological order)
  • シンボル: 追加順

ただし、ownKeys Trapの戻り値としては、 この順で整列されていなくてもTypeErrorにならず、 Object.getOwnPropertyNames()等ではその順のままで処理されます。

またこの内容をObject.keys(proxy)for-inループで使用させる場合には、 返す名前に対応するプロパティデスクリプタでenumetabletrueにして 返す実装も行う必要があります(さらにTargetに存在しないプロパティの場合は configurabletrueにする必要があります)。

const proxy = new Proxy({}, {
  ownKeys(target) {return ["a", "bb", "ccc"];},
  getOwnPropertyDescriptor(target, name) {
    if (name.length % 2 === 1) return {
      enumerable: true, configurable: true,
    };
    return undefined;
  },
});

console.log(Object.keys(proxy)); //=> ["a", "ccc"]

isExtensibledesc.configurablefalseの場合、 追加や変更だけでなく、 Targetにあるプロパティの存在を隠蔽することも 許されない点にも注意が必要でしょう。 たとえば、enumerableなプロパティをownKeys Trapの結果から省くこと が許されません。

関数呼び出しのTrap

  • handler.apply(target, thisArg, args): value
  • handler.construct(target, args, newTarget): object

まず、これらのTrapを使う場合、Targetがfunctionオブジェクトである 必要があります(typeof target === "function"であること)。

const fproxy = new Proxy(() => {}, {
  apply(target, thisArg, args) {
    return "OK";
  }
});

console.log(proxy()); //=> OK

const proxy = new Proxy({}, {
  apply(target, thisArg, args) {
    return "OK";
  }
});

console.log(proxy()); //throw> TypeError

さらに、construct Trapでは、Targetが、newでの使用が禁止される Arrow Function(() => {}等)および メソッド({method() {}}.method等)の場合は使えません(TypeError)。

const fproxy = new Proxy(function () {}, {
  construct(target, args, newTarget) {
    return {OK: true};
  }
});
console.log(new fproxy());

const aproxy = new Proxy(() => {}, {
  construct(target, args, newTarget) {
    return {OK: true};
  }
});
try {
  console.log(new aproxy());
} catch (er) {
  console.log(er.message); //=> TypeError
}

const o = {
  method() {}
};
const mproxy = new Proxy(o.method, {
  construct(target, args, newTarget) {
    return {OK: true};
  }
});
try {
  console.log(new mproxy());
} catch (er) {
  console.log(er.message); //=> TypeError
}

construct Trapでは、値でないオブジェクトを返す必要があります。 つまり、nullnumberstringは返せません。 しかし、返すオブジェクトは、コンストラクタが同じである必要も、 新しいインスタンスである必要もありません。 newしても同じオブジェクトを返し続けるSingletonの実装も可能です。

Proxy.revocable(target, handler)について

const {proxy, revoke} = Proxy.revocable(target, handler);

でrevoke(無効化)可能なProxyを作ることができます。

このrevoke()は関数で、呼び出した瞬間にproxyhandlerとの関係性を 断ち切り、(Trapが作用しないtypeof===/!==を除いて) proxyへのアクセスに対してTypeErrorが発するようになります。

Revocableは、Object Capabilityでの代表的なパターンの一つです。 proxyを渡した相手に何か問題があったらrevokeしてそれ以降アクセス拒否できます。

逆のCapabilityとして、実績がたまったら制限がアンロックされていくようなProxy というのもできるでしょう。

Resources

  • ECMA262 ECMAScript Language Specification: Trap実装の失敗から TypeErrorが出るのは、"Essential Internal Method"の"Invariant"や "Abstract Operation"の各操作のチェックに 違反するためです。操作の中で入れ子で操作が使われることもあるので、 直接Proxyに言及してある以外の部分も解釈する必要があります。

  • MDN Proxy handler: Trap単位で上記仕様から導いた不変条件をまとめてあります。 古い仕様も混ざっている点、及び、日本語訳は単語が過剰に訳されている ("非設定" => configurableがfalse、など)点には注意。

  • Object Capability Model: Capabilityは独立した複数の主体のあいだでプログラムと オブジェクトを相互に動かしあうことが前提にあります。 オブジェクトの受け手が静的に(interfaceの制限によって)関係ないものは扱わない ようにするのとは、逆に、 オブジェクトの送り手が動的に必要な分だけアクセスできるようにしたハンドル (facet, forwarder, proxy)を作って渡す、という考え方をするようです

  • You Don't Know JS (book series): ECMAScript自体の言語構造について 理解するための本です。 言語自体を扱う話では、このレベルの知識は前提として必要でしょう。 Proxyについては"ES6 & Beyond"の7章に少しあります。

ES6 Proxyについて学ぼう: 活用編

ここからはES6 Proxyの活用法をいくつかの例に基づいて考えていきます。 実用例というよりも、実用例を作るためのポイントにフォーカスを置いて、 対象を単純化したコードにしてあります。

活用: デフォルト辞書

const dd = Object.create(new Proxy({}, {get() {return -1;}}));

console.log(dd.foo); //=> -1
dd.foo++;
dd.foo++;
console.log(dd.foo); //=> 1
console.log(JSON.stringify(dd)); //=> {"foo": 1}

デフォルト値を持った辞書のワンライナーです。 プロトタイプのほうでProxyを使う例です。

関数にすると、以下のようになるでしょう:

function defaultDict(value) {
  return Object.create(new Proxy(Object.prototype, {
    get(target, name, receiver) {
      return value;
    }
  }));
}

ただしこの実装では、(Object.create(null)のように) toString等のメソッドも効かなくなります。

メソッド実装も返すようにする場合は、 たとえばArrayに対しては、普通のProxyを使うほうが良いでしょう:

function defaultArray(value = 0) {
  return new Proxy([], {
    get(target, name, receiver) {
      return Reflect.get(target, name, receiver) || value;
    }
  });
}

活用: コンテキストのキャプチャ

//"use strict";// `with` block is required no strict

const env = {};
const proxy = new Proxy(env, {
  has(target) {return true;} // everything captured (include global's console)
});


// usage
with (proxy) {
  (_ => {foo = 10;})(); // capture non-decraed variables in any place
  var bar = 20;         // capture var variables in the `with` block
  let buzz = 30;        // not captured by block scope `let` variables
  const quux = 40;      // not captured by block scope `const` variables
}

console.log(env);//=> {foo:10, bar:20}

with文のコンテキストとしてhas Trapを実装したProxyを使うことで、 ブロック内で生成された変数をキャプチャできます。 (プロトタイプチェーンではなく、)コンテキストのチェーンへProxyを挟む例です。

ただし、この例だとconsoleなどもキャプチャしてundefinedになってしまいます。 has Trapで"console"のときはfalseを返すようにすると、 上のコンテキストに回ってconsoleオブジェクトを参照できるようになるでしょう。

仕様としては"8.1.1.2 Object Environment Records" の範疇です。

余談ですが、このProxyを使ったwith文のことを考えると、 var変数を単に関数スコープと呼ぶのは正確ではないようにも思えます。

活用: classnewなしでインスタンス化可能にするdecorator

// decorator
function newless(ctor) {
  return new Proxy(ctor, {
    apply(target, thisArgs, args) {
      return Reflect.construct(ctor, args);
    }
  });
}

apply TrapにてReflect.constructするだけです。 classへのデコレータとして、以下のように使います。

// example
const Foo = newless(class Foo {
  constructor(name) {
    this.name = name;
  }
});

const foo = Foo("Taro");
console.log(foo.name); //=> "Taro"
console.log(foo instanceof Foo); //=> True

classnew必須が嫌な人向けです。

活用: newしてもSingletonを返すように差し替える

function singleton(ctor, ...args) {
  const instance = Reflect.construct(ctor, args);
  return new Proxy(ctor, {
    construct(target, args, protoCtor) {
      return instance;
    }
  });
}

// example
const Bar = singleton(class Bar {
});
console.log(new Bar() === new Bar()); //=> true

construct Trapではインスタンスであればなんでも返せる ことを示すための例の一つです。 (Singletonパターン自体をnewでやることを推奨するものではありません。)

より実用的な応用はFlyweightパターンの実装で、 タプルのようなimmutableな"Value Object"を Mapのキーに使えるよう生成時にidレベルで同一化する実装は、 役立つかもしれません。 (これも利用側であえてnewさせる必要はないですが。)

活用: プロパティでアクセスできるMap

実装

function mapAsObj(map) {
  function toPrimitive(hint) {
    if (hint === "number") return NaN;
    return JSON.stringify(this);
  }

  return new Proxy({}, {
    get(target, name, receiver) {
      if ([Symbol.toPrimitive, "toString"].includes(name)) return toPrimitive;
      return map.get(name);
    },
    set(target, name, value, receiver) {
      return map.set(name, value);
    },
    has(target, name) {
      return map.has(name);
    },
    deleteProperty(target, name) {
      return map.delete(name);
    },
    ownKeys(target) {
      return [...map.keys()];
    },
    getOwnPropertyDescriptor(target, name) {
      return map.has(name) ? {
        enumerable: true, configurable: true, value: map.get(name)
      } : undefined;
    },
  });
}

利用例

// example
const map = new Map();
const mapObj = mapAsObj(map);
mapObj.foo = 10;
console.log(map); //=> Map { 'foo' => 10 }
console.log(mapObj.foo); //=> 10
console.log("foo" in mapObj); //=> true
console.log(Reflect.ownKeys(mapObj)); //=> ['foo']
console.log(Object.keys(mapObj)); //=> ["foo"]

console.log(`${mapObj}`); //=> '{"foo":10}}'
console.log(JSON.stringify(mapObj)); //=> '{"foo":10}}'
console.log(mapObj); //=> {foo: 10} (on nodejs)

注釈

Reflectではなく、MapのTrap相当メソッドを呼ぶだけです。 実用的には、mapの代わりにデータベース等に対して 同パターンを使うことが可能でしょう。

JSON.stringify等にも対応させるには、getOwnPropertyDescriptor Trap でenumerable: trueになるデスクリプタを返すことが必須です。

活用: オブジェクトツリーを変更不能にするProxy

実装

const cache = new WeakMap(), proxies = new WeakSet();
function immutable(obj) {
  if (!(obj instanceof Object)) return obj; // obj is a values
  if (proxies.has(obj)) return obj; // obj is an immutable proxy
  if (cache.has(obj)) return cache.get(obj); // immutable proxy existed
  const proxy = new Proxy(obj, {
    getPrototypeOf(target) {
      return immutable(Reflect.getPrototypeOf(target));
    },
    setPrototypeOf(target, proto) {
      return true;
    },
    isExtensible(target) {
      return false;
    },
    preventExtensions(target) {
      return true;
    },
    defineProperty(target, name, desc) {
      return true;
    },
    get(target, name, receiver) {
      return immutable(Reflect.get(target, name, immutable(receiver)));
    },
    set(target, name, value, receiver) {
      return true;
    },
    deleteProperty(target, name) {
      return true;
    },
    apply(target, thisArg, args) {
      return immutable(Reflect.apply(target, thisArg, args.map(immutable)));
    },
    construct(target, args, newTarget) {
      return immutable(Reflect.construct(
        target, args.map(immutable), immutable(newTarget)));
    },
  });
  cache.set(obj, proxy);
  proxies.add(proxy);
  return proxy;
}

利用例

// example
const obj = {
  foo: [100, 200],
  bar: "abc"
};
const v = immutable(obj);

// access nested properties
console.log(v.foo[0]); //=> 100

// (you can change actions `v` to `obj` to change output via `v`)
// block side effect method
v.foo.push(100);
console.log(v.foo); //=> [100, 200]

// block direct assign
v.bar = "def";
console.log(v.bar); //=> "abc"

// block adding property
v.buzz = new Date();
console.log(v.buzz); //=> undefined

// block deleting
Reflect.deleteProperty(v, "bar");
console.log(v.bar); //=> "abc"

// keep proxy identity
console.log(v.foo === v.foo); // true

// block adding as prototype
Reflect.setPrototypeOf(v, {buzz: 100});
console.log(v.buzz); //=> undefined

// block adding as descriptor
Reflect.defineProperty(v, "buzz", {value: 100});
console.log(v.buzz); //=> undefined

注釈

プロトタイプへの操作を含め、 オブジェクトツリーに対する一切の変更操作をブロックするラッパーProxy。 再帰Proxyの例でもあり、隠蔽プロパティとしてのWeakMap利用および プロパティを使わないマーカーとしてのWeakSet利用の例でもあります (外部からアクセス可能なオブジェクトのプロパティを使う代わりに、 隠蔽可能なWeakMapWeakSet(booleanフラグの場合)を使う)。

receiverargsのほうもimmutableにしているのは、 メソッドの引数にわたされたオブジェクト経由で書き込み可能オブジェクトが リークしないようにするためです。

更新系の戻り値でtrueを返すことで、変更操作はただスルーされます。 ここでfalseを返すようにすれば、(strict modeなら)TypeErrorが発生して 中断されるようにもできます。

(Capabilityの観点では、この機能の名前としてimmutableはふさわしくなく、 更新系を除去したread only facetということになるでしょうか)。

活用: classで定義したインタフェースで作るFacet

実装

function protect(obj, iface) {
  const proxy = new Proxy(iface.prototype, {
    get(target, name, receiver) {
      const desc = Reflect.getOwnPropertyDescriptor(target, name);
      if (!desc) return undefined;
      if (desc.value) return wrapMethod(Reflect.get(obj, name));
      if (desc.get) return this2proxy(Reflect.get(obj, name));
      return undefined;
    },
    set(target, name, value, receiver) {
      const desc = Reflect.getOwnPropertyDescriptor(target, name);
      if (desc && desc.set) return Reflect.set(obj, name, proxy2this(value));
      return true; // not spawn typeerror to assign
    },
  });
  function this2proxy(result) {
    return result === obj ? proxy : result;
  }
  function proxy2this(value) {
    return value === proxy ? obj : value;
  }
  function wrapMethod(method) {
    return new Proxy(method, {
      apply(target, thisArg, args) {
        return this2proxy(Reflect.apply(method, obj, args.map(proxy2this)));
      }
    });
  }
  return proxy;
}

利用例

// example
// public interface
class Interface {
  get getonly() {}
  get prop() {}
  set prop(v) {}
  method(v) {}
}

// implementation class
class Impl {
  constructor(go, pr) {
    this.go = go;
    this.pr = pr;
  }
  get getonly() {return this.go;}
  get prop() {return this.pr;}
  set prop(v) {this.pr = v;}
  method(v) {return v + this.go + this.pr;}
}

// make facet to pass others
const p = protect(new Impl(10, 20), Interface);

// access via proxy
console.log(p); //=> Interface {}
console.log(p.getonly); //=> 10
console.log(p.prop); //=> 20
p.prop = 30;
console.log(p.prop); //=> 30
console.log(p.method(40)); //=> 80

// protect private props access
p.go = 100;
console.log(p.go); // undefined
console.log(p.getonly); // 10

注釈

指定したインタフェースclassに存在するアクセッサーとメソッドのみ フォワードするラッパーProxy。

ただし、プロパティやメソッドの戻り値が対象オブジェクト自体の場合は、 Proxyのほうを返すようにはしてありますが、 先のimmutable Proxyのように引数のオブジェクト経由で 対象オブジェクトを渡らせないように徹底するまではしていません。 (コールバック等を考えると、引数やreceiverのほうも再帰的にapply Trapで かならずProxyがようなラッパーProxyをかぶせる必要があるでしょう)。

もともとは、public/privateの概念がないES6 classに対して 相応のことをするため、C++でのPimplパターンのつもりで書いたものです。 しかし、Capabilityの観点からはインタフェースを切り取ったfacet(もしくはスタブ) と呼ぶのがよいかもしれません。

活用: Promise非同期処理もプロパティアクセスのように使えるよう変換するProxy

実装

function thenable(promise, parent) {
  const prom = Promise.resolve(promise);
  return new Proxy(function () {}, {
    getPrototypeOf(target) {
      return Promise;
    },
    get(target, name, receiver) {
      if (name === "then" || name === "catch") {
        return Reflect.get(prom, name).bind(prom);
      }
      return thenable(prom.then(t => Reflect.get(t, name)), prom);
    },
    set(target, name, value, receiver) {
      //NOTICE: set cannot return promises
      // it cannot ensure next actions after the assignment finished
      thenable(Promise.all([
        prom, value
      ]).then(([t, v])=> Reflect.set(t, name, v)), prom);
      return true;
    },
    apply(target, thisArg, args) {
      return thenable(Promise.all([
        prom, parent, Promise.all(args)
      ]).then(
        ([func, fthis, fargs]) => Reflect.apply(func, fthis, fargs)));
    },
    construct(target, args, protoCtor) {
      return thenable(Promise.all([
        prom, Promise.all(args), protoCtor
      ]).then(
        ([func, args, ctor]) => Reflect.construct(func, args, ctor)));
    }
  });
}

利用例

// example
const obj = {
    foo: {bar: 100},
    buzz(v) {return v + this.foo.bar;},
};

// property and method access
thenable(Promise.resolve(obj)).foo.bar.then(
    v => console.log("props to promise:", v));
thenable(Promise.resolve(obj)).buzz(Promise.resolve(200)).then(
    v => console.log("method to promise:", v));

// thenable console without then
thenable(console).log(
  "thenable console:",
  thenable(obj).buzz(Promise.resolve(300)));

// new as thenable (Promise)
const ctor = function bar(v) {this.value = v;};
new (thenable(Promise.resolve(ctor)))(50).value.then(
    v => console.log("constructor as thenable promise", v));

// setter example
thenable(obj).buzz = Promise.resolve(50);
thenable(console).log(thenable(obj));

注釈

Promiseの結果に対するプロパティアクセスや関数呼び出しはthenの中で行う 必要があります。

このProxyではPromiseをラップするもので、 その中でproxyに対するプロパティアクセスや関数呼び出しを、 同相のPromiseのthenコールバックに変換してその結果を返すPromiseにし、 そのPromiseを同様にラップしたものを渡す再帰Proxyです。

このProxyのTargetには、newでもProxyにして返すTrapができるように、 空のfunctionを渡しています。

applyTrapやconstruct Trapでは引数やレシーバーも Promiseであるとみなしてすべて解決したときに実行するようになっています。 例であるようにconsoleオブジェクトをラップすれば、 Promiseでも普通のオブジェクトのようにlogの引数に直接渡せる記述ができます。

ただし、set Trapも実装したけど、Proxyを返せないので、終了を持って実行させる 継続処理を書くことはできません。

実用化のためには、エラー時にわかりやすいメッセージを生成できるように することでしょう。このままではundefinedのプロパティがアクセスできません、 とか、nullは関数ではありません、のようなメッセージでcatchされますが、 parent引数のように積み重ねることでプロパティチェーンを生成すれば、 エラーメッセージで使うことができるでしょう。

案: async/await構文との組み合わせ

このthenableラッパーを、 次期のasync/await構文相当のジェネレータ処理の部分でラップできると await/yieldの記述箇所が減らせるので、面白いかもしれません。

const flow = (gfunc) => (...args) => thenable(new Promise((f, r) => {
  const g = gfunc(...args.map(thenable)),
        n = r => g.next(r), t = r => g.throw(r);
  const step = item => item.done ? f(item.value) :
    thenable(item.value).then(n, t).then(step, r);
  step(n());
}));

flow(function* ({console, fetch}) {
  yield console.log(fetch("data.json").json());
})({console, fetch});

//FYI: async/await and raw promise based API
(async function () {
  console.log(await (await fetch("data.json")).json());
})();

活用: seal/unsealパターン

実装

function makeSealPair() {
  const unsealer = {};
  function seal(obj) {
    return new Proxy(obj, {
      get(target, name, receiver) {
        if (receiver !== unsealer) return undefined;
        return Reflect.get(target, name, target);
      },
      // hide everything
      set() {return false;},
      setPrototypeOf() {return false;},
      has() {return false;},
      getOwnPropertyDescriptor() {return undefined;},
      defineProperty() {return false;},
      ownKeys() {return [];}
    });
  }
  function unseal(sealed) {
    Reflect.setPrototypeOf(unsealer, sealed);
    return unsealer;
  }
  return {seal, unseal};
}

利用例

// esxample flow
const pair1 = makeSealPair(); // pass pair.seal to the sender

// sender make sealed proxy
const sealed1 = pair1.seal({foo: 100, bar() {return this.foo * 2;}});

// sealed proxy itself cannot acquired props for passing men in the middle
console.log("sealed1.foo", sealed1.foo); //=> undefined
console.log(Object.keys(sealed1));       //=> []

// Only valid unsealer can access props and methods
const unsealed1 = pair1.unseal(sealed1);
console.log("unsealed1.foo", unsealed1.foo);     //=> 100
console.log("unsealed1.bar()", unsealed1.bar()); //=> 200

// invalid unsealers cannot access props and methods
const pair2 = makeSealPair();
const unsealed2 = pair2.unseal(sealed1);
console.log("unsealed2.foo", unsealed2.foo); //=> undefined
console.log("unsealed2.bar", unsealed2.bar); //=> undefined

注釈

Object Capabilityのパターンの一つのseal/unseal。 これは、公開鍵暗号のような封印錠前の受け渡しのようなことを オブジェクトレベルで行う仕組みです。

まず受け手はseal/unsealのペアを作り、sealだけ送り手に渡します。 送り手はsealによってオブジェクトを封印し、 中継者を経由しつつ受け手に封印オブジェクトを渡します。 受け手はunsealを封印オブジェクトにかけることで、 オブジェクトにアクセスできるようになります。 unsealを持つもの以外では封印は解けず、 オブジェクトにアクセスできないようにします。

この実装では、まず非公開鍵に当たるunsealerオブジェクトを ローカルに用意します。 Proxyではreceiverunsealerのときのみプロパティアクセスを 認めるという処理にし、それを公開鍵にあたるsealed Proxyとし、 そのファクトリsealを相手に渡します。 unsealではunsealerのプロトタイプにsealed Proxyをセットすることで、 unsealerからのみのプロパティアクセスが可能になります。

ちなみにsealで何個もsealed Proxyを作れ、 unsealからプロトタイプを差し替えることで、 それぞれのsealdを切り替えてアクセスすることもできます。

Object Capabilityの世界について

(Revocableでもそうですが、)プログラムの実行主体が複数存在し、 その間で制約をつけ合いながらオブジェクトを渡していく システムで有用なモデルです。

一方、全権限を持つ神様1つがいて、 各状況で場合分けをしてその1役割になって システムとして不正動作をしないように注意しながら振る舞わせる 統合されたプログラムを実行するのが、 普通のアプリケーションサービスのシステムです。

そういう超越者が行うシステムでは、 中間で封印するといった行為に対しての意味は見出しにくいかもしれません。

"use strict";
// the code is for trying various extensibility setting and its effects
{
// block prevent extensions
const o = {};
const p = new Proxy(o, {
isExtensible(target) {
return Reflect.isExtensible(target); // must be same result to target
},
preventExtensions(target) {
return false;
},
});
Reflect.preventExtensions(p);
p.foo = 100;
console.log(p); //=> {foo: 100}
const p2 = new Proxy(p, {
isExtensible(target) {
return Reflect.isExtensible(target);
},
preventExtensions(target) {
//invariant: preventExtensions(p) === true => isExtensible(p) === false
return true;
},
});
try {
Reflect.preventExtensions(p2);
} catch (err) {
console.log("[invariant of preventExtensions]", err.message);
}
}
{
// cannot use different list of props when not extensible
const o = Object.freeze({});
const p = new Proxy(o, {
ownKeys(target) {
return ["foo", "bar"]; //invalid
},
});
try {
console.log(Reflect.ownKeys(p));
} catch (err) {
console.log("[not extensible, but set extra props]", err.message);
}
}
{
// invalid result for not configurable property
const o = {};
Object.defineProperty(o, "foo", {
value: 100, configurable: false, enumerable: true, writable: false});
const p = new Proxy(o, {
getOwnPropertyDescriptor(target, name) {
//invalid: must return same desc with target's one
return {value: null,
configurable: false, enumerable: true, writable: false};
},
defineProperty(target, name, desc) {
//invalid: must return falsee when different desc from target's one
return true;
},
get(target, name) {
//invalid: must return same value with target's prop
return name;
},
set(target, name, value) {
//invalid: must return false when not writable
return true;
},
ownKeys(target) {
//invalid: must include not configurable with enumerable keys
return [];
},
});
try {
console.log(Reflect.ownKeys(p));
} catch (err) {
console.log("[not configurable, cannot hide the key]", err.message);
}
try {
console.log(p.foo);
} catch (err) {
console.log("[not configurable, prop value must be same]", err.message);
}
try {
p.foo = 200;
} catch (err) {
console.log("[not writablle, trap must say failed]", err.message);
}
try {
console.log(Object.getOwnPropertyDescriptor(p, "foo"));
} catch (err) {
console.log("[not configurable, descriptor must be same]", err.message);
}
try {
Object.defineProperty(p, "foo", {value: 200});
} catch (err) {
console.log("[not configurable, defineProperty must failed]", err.message);
}
}
"use strict";
// handler is collection of traps
// default trap impls are the 13 same name methods in `Reflect` to the target
const handler = {
// params
// - target: target of `new Reflect(target, handler)`
// - name: string or Symbol (note: string `"0"` when a[0]`)
// - args: [value]
// - receiver: object accessing the property (usually the proxy itself)
// retruns
// - bool(success or not): `false` spawn `TypeError`, `true` goes silently
//[access target.constructor.prototype]
getPrototypeOf(target) {//=> prototype object (or null)
return Reflect.getPrototypeOf(target);
},
setPrototypeOf(target, proto) {//=> bool (success or not)
return Reflect.setPrototypeOf(target, proto);
},
//[define proxy extensibility]
isExtensible(target) {//=> bool (extensible or not)
return Reflect.isExtensible(target);
},
preventExtensions(target) {//=> bool (block extending or not)
return Reflect.preventExtensions(target);
},
//[configure properties]
getOwnPropertyDescriptor(target, name) {//=> descriptor object (or undefined)
return Reflect.getOwnPropertyDescriptor(target, name);
},
defineProperty(target, name, desc) {//=> bool (success or not)
return Reflect.defineProperty(target, name, desc);
},
//[basic property access]
has(target, name) {//=> bool
return Reflect.has(target, name);
},
get(target, name, receiver) {//=> value
return Reflect.get(target, name, receiver);
},
set(target, name, value, receiver) {//=> bool (success or not)
return Reflect.set(target, name, value, receiver);
},
deleteProperty(target, name) {//=> bool (success or not)
return Reflect.deleteProperty(target, name);
},
ownKeys(target) {//=> [name]
//NOTE: Reflect.ownKeys(obj) returns a list sorted by index, name, symbol
// But ownKeys trap can return unorderd list.
// then Reflect.ownKey(proxy) returns the trap order (not sorted)
return Reflect.ownKeys(target);
},
//[proxy as functions] (required `typeof target === "function"`)
apply(target, thisArg, args) {//=> value (result as function)
return Reflect.apply(target, thisArg, args);
},
construct(target, args, protoCtor) {//=> object instance
//NOTE: protoCtor is for setting another `obj.constructor.prottotype`
// e.g.: for adding methods, for adding `instanceof` relations
return Reflect.construct(target, args, protoCtor);
},
};
// create proxy object with target
const target = {};
const proxy = new Proxy(target, handler);
// proxy for function
const p = new Proxy(function foo() {this.foo = 10;}, handler);
console.log("new:", new p());
const ctor = function bar() {};
Object.assign(ctor.prototype, {twice() {return this.foo * this.foo;}});
const ins = Reflect.construct(p, [], ctor);
console.log("construct object with other ctor/proto:",
ins.constructor.name, ins.twice()); //=> bar 100
// `this` in proxy method(aka receiver) === proxy (not target bound function)
const mp = new Proxy({method() {return this;},}, handler);
console.log(mp.method() === mp); //=> true
// Proxy usages are:
// - wrapping with extra processing: e.g. logging
// - intercept and modify behaviors: e.g. network stub
// - add new functionalities as a plain object: e.g. manage `Map` as object
//
// The latter two usages are required extensibility for its proxy.
// IMPORTANT: `proxy` extensibility is limited with the `target` extensibility.
// When the `target` is not extensible, proxy cannot modify properties, e.g.:
// - `isExtensible` and `preventExtension` cannot return `false`
// - `ownKey` cannot returns different key list
// - `has` cannot return false for existing names in target
// - `getOwnPropertyDescriptor` and `get` cannot return for extra names
// - `defineProperty` cannot accept extra names
//
// `TypeError` spawned when traps returns these invalid results.
//
// `Reflect.isExtensible(target) === false` becomes by:
// - Object.freeze(target), Object.seal(target)
// - Object.preventExtensions(target), Reflect.preventExtensions(target)
//
// NOTE: target prop desc of `configurable: false` has also same restrictions:
// - cannot hide names at `has`, `ownKey`
// - cannot return different value at `get`
// - cannot set value at `set` when also `writable: false`
"use strict";
// Proxy caching each result of iter (<=> prefetching Array.from(iter))
function cachingIter(iterable) {
const iter = iterable[Symbol.iterator]();
const cache = [];
let done = null;
const caching = () => {
return function* () {
let index = 0;
while (true) {
if (index < cache.length) yield cache[index++];
else if (done) return done.value;
else {
const r = iter.next();
if (r.done) {
done = r;
return done.value;
} else {
cache.push(r.value);
index++;
yield r.value;
}
}
}
}();
};
return new Proxy(iterable, {
get(target, name, receiver) {
if (name === Symbol.iterator) return caching;
return Reflect.get(target, name, receiver);
}
});
}
const gtor = function* () {
console.log("init");
yield 1;
console.log("yield 1");
yield 2;
console.log("yield 2");
yield 3;
console.log("yield 3");
return 0;
};
// example:
const iter = cachingIter(gtor());
const i1 = iter[Symbol.iterator]();
const i2 = iter[Symbol.iterator]();
console.log("i1", i1.next().value); //=> init
console.log("i1", i1.next().value); //=> yield 1
console.log("i2", i2.next().value);
console.log("i2", i2.next().value);
console.log("i2", i2.next().value); //=> yield 2
console.log("i1", i1.next().value);
console.log("i2", i2.next().value); //=> yield 3
console.log("i1", i1.next().value);
"use strict";
// remove required `new` of `class`
function newless(ctor) {
return new Proxy(ctor, {
apply(target, thisArgs, args) {
return Reflect.construct(ctor, args);
}
});
}
const Foo = newless(class Foo {
constructor(name) {
this.name = name;
}
});
const foo = Foo("Taro");
console.log(foo.name); //=> "Taro"
console.log(foo instanceof Foo); //=> True
// make as singleton
function singleton(ctor, ...args) {
const instance = Reflect.construct(ctor, args);
return new Proxy(ctor, {
construct(target, args, protoCtor) {
return instance;
}
});
}
const Bar = singleton(class Bar {
});
console.log(new Bar() === new Bar()); //=> true
"use strict";
const cache = new WeakMap(), proxies = new WeakSet();
function immutable(obj) {
if (!(obj instanceof Object)) return obj; // obj is a values
if (proxies.has(obj)) return obj; // obj is an immutable proxy
if (cache.has(obj)) return cache.get(obj); // immutable proxy existed
const proxy = new Proxy(obj, {
getPrototypeOf(target) {
return immutable(Reflect.getPrototypeOf(target));
},
setPrototypeOf(target, proto) {
return true;
},
isExtensible(target) {
return false;
},
preventExtensions(target) {
return true;
},
defineProperty(target, name, desc) {
return true;
},
get(target, name, receiver) {
return immutable(Reflect.get(target, name, immutable(receiver)));
},
set(target, name, value, receiver) {
return true;
},
deleteProperty(target, name) {
return true;
},
apply(target, thisArg, args) {
return immutable(Reflect.apply(target, thisArg, args.map(immutable)));
},
construct(target, args, newTarget) {
return immutable(Reflect.construct(
target, args.map(immutable), immutable(newTarget)));
},
});
cache.set(obj, proxy);
proxies.add(proxy);
return proxy;
}
// example
const obj = {
foo: [100, 200],
bar: "abc"
};
const v = immutable(obj);
// access same value
console.log(v.foo[0]);
// (you can change actions `v` to `obj` to change output via `v`)
// block side effect method
v.foo.push(100);
console.log(v.foo);
// block direct assign
v.bar = "def";
console.log(v.bar);
// block adding property
v.buzz = new Date();
console.log(v.buzz);
// block deleting
Reflect.deleteProperty(v, "bar");
console.log(v.bar);
// keep proxy identity
console.log(v.foo === v.foo);
// block adding as prototype
Reflect.setPrototypeOf(v, {buzz: 100});
console.log(v.buzz);
// block adding as descriptor
Reflect.defineProperty(v, "buzz", {value: 100});
console.log(v.buzz);
"use strict";
// interface protection with proxy
function protect(obj, iface) {
const proxy = new Proxy(iface.prototype, {
get(target, name, receiver) {
const desc = Reflect.getOwnPropertyDescriptor(target, name);
if (!desc) return undefined;
if (desc.value) return wrapMethod(Reflect.get(obj, name));
if (desc.get) return this2proxy(Reflect.get(obj, name));
return undefined;
},
set(target, name, value, receiver) {
const desc = Reflect.getOwnPropertyDescriptor(target, name);
if (desc && desc.set) return Reflect.set(obj, name, proxy2this(value));
return true; // not spawn typeerror to assign
},
});
function this2proxy(result) {
return result === obj ? proxy : result;
}
function proxy2this(value) {
return value === proxy ? obj : value;
}
function wrapMethod(method) {
return new Proxy(method, {
apply(target, thisArg, args) {
return this2proxy(Reflect.apply(method, obj, args.map(proxy2this)));
}
});
}
return proxy;
}
class Interface {
get getonly() {}
get prop() {}
set prop(v) {}
method(v) {}
}
class Impl {
constructor(go, pr) {
this.go = go;
this.pr = pr;
}
get getonly() {return this.go;}
get prop() {return this.pr;}
set prop(v) {this.pr = v;}
method(v) {return v + this.go + this.pr;}
}
const p = protect(new Impl(10, 20), Interface);
// access via proxy
console.log(p);
console.log(p.getonly);
console.log(p.prop);
p.prop = 30;
console.log(p.prop);
console.log(p.method(40));
// protect private props access
p.go = 100;
console.log(p.go);
console.log(p.getonly);
"use strict";
// access Map as Plain Object
function mapAsObj(map) {
function toPrimitive(hint) {
if (hint === "number") return NaN;
return JSON.stringify(this);
}
return new Proxy({}, {
get(target, name, receiver) {
if ([Symbol.toPrimitive, "toString"].includes(name)) return toPrimitive;
return map.get(name);
},
set(target, name, value, receiver) {
return map.set(name, value);
},
has(target, name) {
return map.has(name);
},
deleteProperty(target, name) {
return map.delete(name);
},
ownKeys(target) {
return [...map.keys()];
},
getOwnPropertyDescriptor(target, name) {
return map.has(name) ? {
enumerable: true, configurable: true, value: map.get(name)
} : undefined;
},
});
}
const map = new Map();
const mapObj = mapAsObj(map);
mapObj.foo = 10;
console.log(map);
console.log(mapObj.foo);
console.log("foo" in mapObj);
console.log(Reflect.ownKeys(mapObj));
console.log(Object.getOwnPropertyNames(mapObj));
console.log(Object.keys(mapObj));
console.log(`${mapObj}`); // toPrimitive
console.log(JSON.stringify(mapObj)); //toJSON
console.log(mapObj); // inspect (nodejs only)
console.log(mapObj.toString());
"use strict";
// Any object turns Promise and also access members promise like Plain Object
function thenable(promise, parent) {
const prom = Promise.resolve(promise);
return new Proxy(function () {}, {
getPrototypeOf(target) {
return Promise;
},
get(target, name, receiver) {
if (name === "then" || name === "catch") {
return Reflect.get(prom, name).bind(prom);
}
return thenable(prom.then(t => Reflect.get(t, name)), prom);
},
set(target, name, value, receiver) {
//NOTICE: set cannot return promises
// it cannot ensure next actions after the assignment finished
thenable(Promise.all([
prom, value
]).then(([t, v])=> Reflect.set(t, name, v)), prom);
return true;
},
apply(target, thisArg, args) {
return thenable(Promise.all([
prom, parent, Promise.all(args)
]).then(
([func, fthis, fargs]) => Reflect.apply(func, fthis, fargs)));
},
construct(target, args, protoCtor) {
return thenable(Promise.all([
prom, Promise.all(args), protoCtor
]).then(
([func, args, ctor]) => Reflect.construct(func, args, ctor)));
}
});
}
// example
const obj = {
foo: {bar: 100},
buzz(v) {return v + this.foo.bar;},
};
// property and method access
thenable(Promise.resolve(obj)).foo.bar.then(
v => console.log("props to promise:", v));
thenable(Promise.resolve(obj)).buzz(Promise.resolve(200)).then(
v => console.log("method to promise:", v));
// thenable console without then
thenable(console).log(
"thenable console:",
thenable(obj).buzz(Promise.resolve(300)));
// new as thenable (Promise)
const ctor = function bar(v) {this.value = v;};
new (thenable(Promise.resolve(ctor)))(50).value.then(
v => console.log("constructor as thenable promise", v));
// setter example
thenable(obj).buzz = Promise.resolve(50);
thenable(console).log(thenable(obj));
"use strict";
// default dict with proxy as prototype
function defaultDict(obj, value) {
const proto = new Proxy({}, {
get(target, name) {return value;}
});
Reflect.setPrototypeOf(obj, proto);
return obj;
}
const dict = defaultDict({foo: 10}, -1);
console.log(dict.foo); //=> 10
console.log(dict.bar); //=> -1
dict.bar += 20;
console.log(dict.bar); //=> 19
console.log(JSON.stringify(dict)); //=> {"foo":10,"bar":19}
// or one-liner: empty default dict with value -1
const dd = Object.create(new Proxy({}, {get() {return -1;}}));
console.log(dd.foo); //=> -1
dd.foo++;
dd.foo++;
console.log(dd.foo); //=> 1
console.log(JSON.stringify(dd)); //=> {"foo":1}
"use strict";
// array accepts over the range [0, length)
function ring(array = []) {
return new Proxy(array, {
get(target, name, receiver) {
if (typeof name === "symbol") return Reflect.get(target, name, receiver);
if (/^-?\d+$/.test(name)) {
const n = name % target.length;
return target[n < 0 ? n + target.length : n];
}
return Reflect.get(target, name, receiver);
}
});
}
const r = ring(["a", "b", "c"]);
for (let i = -3; i < 6; i++) console.log(r[i]);
"Use strict";
// capability pattern for seal/unseal
function makeSealPair() {
const unsealer = {};
function seal(obj) {
return new Proxy(obj, {
get(target, name, receiver) {
if (receiver !== unsealer) return undefined;
return Reflect.get(target, name, target);
},
// hide everything
set() {return false;},
setPrototypeOf() {return false;},
has() {return false;},
getOwnPropertyDescriptor() {return undefined;},
defineProperty() {return false;},
ownKeys() {return [];}
});
}
function unseal(sealed) {
Reflect.setPrototypeOf(unsealer, sealed);
return unsealer;
}
return {seal, unseal};
}
// esxample flow
const pair1 = makeSealPair(); // pass pair.seal to the sender
// sender make sealed proxy
const sealed1 = pair1.seal({foo: 100, bar() {return this.foo * 2;}});
// sealed proxy itself cannot acquired props for passing men in the middle
console.log("sealed1.foo", sealed1.foo); //=> undefined
console.log(Object.keys(sealed1)); //=> []
// Only valid unsealer can access props and methods
const unsealed1 = pair1.unseal(sealed1);
console.log("unsealed1.foo", unsealed1.foo); //=> 100
console.log("unsealed1.bar()", unsealed1.bar()); //=> 200
// invalid unsealers cannot access props and methods
const pair2 = makeSealPair();
const unsealed2 = pair2.unseal(sealed1);
console.log("unsealed2.foo", unsealed2.foo); //=> undefined
console.log("unsealed2.bar", unsealed2.bar); //=> undefined
//"use strict";// `with` block is required no strict
// capture all var variables with proxy and `with` block
const env = {};
const proxy = new Proxy(env, {
has(target) {return true;} // everything captured (include global's console)
});
with (proxy) {
(_ => {foo = 10;})(); // capture non-decraed variables in any place
var bar = 20; // capture var variables in the `with` block
let buzz = 30; // not captured by block scope `let` variables
const quux = 40; // not captured by block scope `const` variables
}
console.log(env);//=> {foo:10, bar:20}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment