【JavaScript】Proxyについて学ぶ
なんとなく聞いたことはあるけど、あまりよくわからないJavaScriptの技術の1つとして、Proxyがあると思います。(もしかしたら私だけ)
そんなProxyについて改めて学び直したので、本記事はそのまとめです。
基本的な使い方
Proxyを使用するとオブジェクトの基本的な操作を拡張したオブジェクトを生成することが可能です。
まずは簡単に使い方をみてみましょう。
Proxyは第1引数にtarget
、第2引数にhandler
の2つの引数を取ります。
-
target
: もととなるオブジェクト -
handler
: 特定の操作をインターセプトする、またインターセプトした操作の処理を定義するオブジェクト
// もととなるオブジェクト
const target = {
hoge: "fuga",
};
// 動作を定義するhandlerオブジェクト
const handler = {};
// Proxyオブジェクトを生成
const proxy = new Proxy(target, handler);
// handlerが空なので、proxyはもとのtargetと同様の動作をする
console.log(proxy.hoge); // "fuga"
console.log(proxy.piyo); // undefined
proxy.hoge = "piyo";
console.log(proxy.hoge); // "piyo"
console.log(target); // { hoge: 'piyo' }
上記ではhandler
が空のため、proxy
がもとのオブジェクト(target
)と同様の動作をします。
このままではあまり意味がないので、handler
に適切なメソッドを定義してその挙動を確認してみましょう。またhandler
で定義するメソッドのことを、Proxyではしばしばトラップと呼びます。
const target = {
hoge: "fuga",
};
// handlerにgetトラップを定義
const handler = {
get: (target, prop, receiver) => {
return "hello";
},
};
const proxy = new Proxy(target, handler);
proxy.hoge = "piyo";
console.log(proxy.hoge); // "hello"
console.log(target.hoge); // "piyo"
今回はhandler
にgetトラップ
を定義しました。
注目してほしいのはconsole.log(proxy.hoge)
の部分です。
驚くべきことに「hello」が結果として表示されていますね。
想像がついている方も多いと思いますが、getトラップ
はプロパティの取得をインターセプトします。すなわち上記の実装では、実際のプロパティの値が何であろうと常に「hello」が返ってくるのです。
ここで気をつけてほしいのは、この例ではあくまでProxyオブジェクトのプロパティの取得をインターセプトしただけであって、target.hoge
の値が「hello」になったわけではありません。その証拠にtarget.hoge
の結果にはpiyo
が表示されます。
proxyという英単語には「代理」という意味があります。ここまでの挙動を省みるとProxyオブジェクトという名前の意味が、なんとなくわかるのではないでしょうか。
ハンドラー関数
当然ながらgetトラップ
以外にも、ハンドラー関数(トラップ)は存在します。以下にハンドラー関数を列挙します。
おそらくこの中でもっとも使用頻度が高く、馴染み深いのはプロパティの取得と設定でしょう。
まずは理解しやすいgetトラップ
から考えていきます。
getトラップ
冒頭でも出てきましたが、getトラップ
はプロパティが取得された時に呼び出されるトラップです。
引数
次の引数が順に渡ってきます。
-
target
: もとのオブジェクト -
property
: 取得するプロパティ名 -
receiver
: proxyオブジェクト(またはproxyから継承している場合は継承したオブジェクト)
以下は存在しないプロパティを取得しようとした時にエラーとなる実装です。
const target = {
hoge: "fuga",
};
const handler = {
get: (target, property, receiver) => {
if (property in target) {
return target[property];
}
throw new Error("プロパティが存在しません");
},
};
const proxy = new Proxy(target, handler);
console.log(proxy.hoge); // fuga
console.log(proxy.piyo); // Uncaught Error: プロパティが存在しません
setトラップ
取得があれば、当然設定もあります。
setトラップ
はプロパティを設定する時に呼び出されるトラップです。
引数
次の引数が順に渡ってきます。
-
target
: もとのオブジェクト -
property
: 値を設定するプロパティ名(もしくはSymbol) -
value
: 設定するプロパティの新しい値 -
receiver
: proxyオブジェクト
戻り値
setトラップ
は真偽値を返す必要があります。
以下はプロパティ設定時のバリデーションを実装したコードです。
const target = {
hoge: "fuga",
};
const handler = {
set: (target, property, value, receiver) => {
if (!(property in target)) {
throw new Error("存在しないプロパティです");
return false
}
if (typeof value !== "string") {
throw new Error("文字列を入力してください");
return false
}
target[property] = value;
return true
},
};
const proxy = new Proxy(target, handler);
proxy.hoge = "piyo";
proxy.hoge = 1; // Uncaught Error: 文字列を入力してください
deletePropertyトラップ
プロパティの取得・設定ときたら、次は削除についてみてみます。
deletePropertyトラップ
はdelete 演算子
に対するトラップです。
delete 演算子
を使うと、オブジェクトからプロパティを削除することが可能です。
引数
次の引数が順に渡ってきます。
-
target
: もとのオブジェクト -
property
: オブジェクトから削除するプロパティ名
戻り値
deletePropertyトラップ
は真偽値を返す必要があります。
以下はアンダーバーで始まるプロパティをdelete 演算子
で削除できないようにする実装です。
const target = {
hoge: "fuga",
_hoge: "_fuga",
};
const handler = {
deleteProperty(target, property) {
if (property in target) {
if (property.startsWith("_")) {
console.log(`${property}プロパティは削除できません`);
return false;
}
console.log(`${property}プロパティを削除しました`);
delete target[property];
return true;
}
consoel.log(`${property}プロパティは存在しません`);
return false;
},
};
const proxy = new Proxy(target, handler);
delete proxy.hoge; // hogeプロパティを削除しました
delete proxy._hoge; // _hogeプロパティは削除できません
console.log(target); // {_hoge: '_fuga'}
hasトラップ
hasトラップ
はin 演算子
に対するトラップです。
これまでのコードにもしれっと登場していますが、in 演算子
はあるオブジェクトに指定されたプロパティが存在するか否かを判断する演算子です。指定されたプロパティが存在するときはtrue
を、しないときはfalse
を返します。
引数
次の引数が順に渡ってきます。
-
target
: もとのオブジェクト -
property
: 対象となるプロパティ名
戻り値
hasトラップ
は真偽値を返す必要があります。
以下の実装はオブジェクトの中にプロパティが存在するか否かにかかわらず、常にtrue
を返します。
const target = {
hoge: "fuga",
};
const handler = {
has: (target, property) => {
// プロパティが存在しているか否かにかかわらず、trueを返す
return true;
},
};
const proxy = new Proxy(target, handler);
console.log("piyo" in proxy); // true => 実際にpiyoが存在しているか否かによらず、常に存在しているように振る舞う
console.log(proxy.piyo); // undefined
こちらもgetトラップ
の時と同様に、あくまでin 演算子
の操作をインターセプトしているだけであり、proxy.piyo
はundefined
となります。
ownKeysトラップ
ownKeysトラップ
はObject.getOwnPropertyNames
とObject.getOwnPropertySymbols
に対するトラップです。
Object.getOwnPropertyNames
は対象のオブジェクト上のシンボルを除くすべてのプロパティを、Object.getOwnPropertySymbols
は対象のオブジェクト上のすべてのシンボルプロパティを配列で返します。
引数
次の引数が渡ってきます。
-
object
: 列挙されるオブジェクト
戻り値
ownKeysトラップ
は列挙可能オブジェクトを返す必要があります。
以下はアンダーバーで始まるプロパティを除いたプロパティ一覧を返す実装になります。
const target = {
hoge: "fuga",
_hoge: "_fuga",
[Symbol("piyo")]: "piyo",
};
const handler = {
ownKeys: (object) => {
// アンダーバーで始まるプロパティを除く
return [
...Object.getOwnPropertyNames(object).filter(
key => !key.startsWith("_")
),
...Object.getOwnPropertySymbols(object),
];
},
};
const proxy = new Proxy(target, handler);
console.log(Object.getOwnPropertyNames(proxy)); // ['hoge'] => _hogeがフィルターされている
console.log(Object.getOwnPropertySymbols(proxy)); // [Symbol(piyo)]
簡単にownKeysトラップ
の実装方法を確認したところで、次のコードをみてみましょう。
もとのオブジェクトによらず、常に["hoge", "fuga"]
をリターンしています。
const handler = {
ownKeys: object => {
// 常に["hoge", "fuga"]を返す
return ["hoge", "fuga"];
},
};
const proxy = new Proxy({}, handler);
console.log(Object.getOwnPropertyNames(proxy)); // ['hoge', 'fuga']
console.log(Object.keys(proxy)); // [] => 空の配列
注目すべきなのはObject.keys(proxy)
の部分ですね。結果には空の配列が表示されました。
これはなぜかというとObject.keys
があくまで列挙可能なプロパティのみを返すからです。列挙可能なプロパティとはすなわちenumerable属性
がtrue
のプロパティのことです。
enumerable属性
については本記事では解説しませんが、参考として以下のリンクを貼っておきます。
では列挙可能でないプロパティを取得するにはどうするのかいうと、次で紹介するgetOwnPropertyDescriptorトラップ
を使用することで可能になります。
getOwnPropertyDescriptorトラップ
getOwnPropertyDescriptorトラップ
はObject.getOwnPropertyDescriptor
に対するトラップです。
Object.getOwnPropertyDescriptor
を使うと、プロパティ記述子(ディスクリプタ)を取得することが可能となります。
引数
次の引数が順に渡ってきます。
-
target
: もとのオブジェクト -
property
: 記述の対象となるプロパティ名、またはSymbol
戻り値
オブジェクト、またはundefined
を返す必要があります。
以下の実装は列挙可能か否かにかかわらず、プロパティを取得することが可能です。
const target = {};
const handler = {
ownKeys(target) {
return ["hoge", "fuga"];
},
getOwnPropertyDescriptor(target, property) {
return {
value: property,
writable: true,
enumerable: true,
configurable: true,
};
},
};
const proxy = new Proxy(target, handler);
console.log(Object.getOwnPropertyDescriptor(proxy, "piyo")); // {value: 'piyo', writable: true, enumerable: true, configurable: true}
console.log(Object.keys(proxy)); // ['hoge', 'fuga'] => enumerableがtrueなので取得できる
definePropertyトラップ
definePropertyトラップ
はObject.defineProperty
に対するトラップです。
Object.defineProperty
を使うと、プロパティ記述子(ディスクリプタ)を設定することが可能となります。
引数
次の引数が順に渡ってきます。
-
target
: もとのオブジェクト -
property
: 説明を受け取るプロパティ名、またはSymbol
-
descriptor
: 定義や変更されるプロパティに対するディスクリプタ
戻り値
プロパティが正しく定義されたか否かを表す真偽値を返す必要があります。
const target = { hoge: "fuga" };
const handler = {
defineProperty: (target, key, descriptor) => {
Object.defineProperty(target, key, descriptor);
return true;
},
};
const proxy = new Proxy(target, handler);
Object.defineProperty(proxy, "hoge", {
value: "piyo",
writable: true,
enumerable: true,
configurable: true,
});
console.log(Object.getOwnPropertyDescriptor(proxy, "hoge")); // {value: 'piyo', writable: true, enumerable: true, configurable: true}
getPrototypeOfトラップ
getPrototypeOfトラップ
はObject.getPrototypeOf
に対するトラップです。
Object.getPrototypeOf
は指定されたオブジェクトのプロトタイプを取得できます。
引数
次の引数が順に渡ってきます。
-
target
: もとのオブジェクト -
property
: 説明を受け取るプロパティ名、またはSymbol
-
descriptor
: 定義や変更されるプロパティに対するディスクリプター
以下はプロトタイプをNumber.prototype
に偽る実装です。
const target = { hoge: "fuga" };
const proxy = new Proxy(target, {
getPrototypeOf: (target) => {
// Numberのprototypeを返す
return Number.prototype;
},
});
console.log(Object.getPrototypeOf(proxy) === Number.prototype); // true
proxy.toFixed(0); // Uncaught TypeError: proxy.toFixed is not a function
結果をみてわかるように、これもgetトラップ
同様、あくまでプロトタイプの取得をインターセプトしているだけであり、実際にproxy
のプロトタイプが変更されているわけではありません。
その証拠にproxy.toFixed(0)
はエラーとなります。
setPrototypeOfトラップ
setPrototypeOfトラップ
はObject.setPrototypeOf
に対するトラップです。
Object.setPrototypeOf
は指定されたオブジェクトのプロトタイプを設定できます。
引数
次の引数が順に渡ってきます。
-
target
: もとのオブジェクト -
prototype
: 設定するオブジェクトのプロトタイプ、またはnull
戻り値
setPrototypeOfトラップ
は真偽値を返す必要があります。
以下は配列のプロトタイプをObject.prototype
に変更してます。
その証拠にproxy.push
がエラーとなります。
const target = [];
const proxy = new Proxy(target, {
setPrototypeOf: (target, prototype) => {
Object.setPrototypeOf(target, prototype);
return true;
},
});
Object.setPrototypeOf(proxy, Object.prototype);
proxy.push(1); // Uncaught TypeError: proxy.push is not a function
isExtensibleトラップ
isExtensible
はObject.isExtensible
に対するトラップです。
Object.isExtensible
はオブジェクトが拡張可能か否かを真偽値で返します。
拡張可能とはオブジェクトに新しいプロパティ追加できる状態を指します。
引数
次の引数が渡ってきます。
-
target
: もとのオブジェクト
戻り値
isExtensibleトラップ
は真偽値を返す必要があります。
今までのトラップでは操作をインターセプトすることにより、(実際に正しい挙動かどうかによらず)その挙動を偽ることができました。
一方でisExtensibleトラップ
は制限が厳しく、Object.isExtensible
がtrue
を返すべきところでfalse
を返したり、逆にfalse
を返すべきところでtrue
を返すことができません。
すなわち以下の実装は必ずエラーとなります。
const target = {};
const handler = {
isExtensible: (target) => {
// Object.isExtensibleとは逆の挙動
// 必ずエラーとなる!!
return !Object.isExtensible(target);
},
};
const proxy = new Proxy(target, handler);
console.log(Object.isExtensible(proxy)); // Uncaught TypeError: 'isExtensible' on proxy: trap result does not reflect extensibility of proxy target (which is 'true')
preventExtensionsトラップ
preventExtensions
トラップはObject.preventExtensions
に対するトラップです。
Object.preventExtensions
はオブジェクトを拡張不可能とするメソッドです。
拡張不可能となったオブジェクトは、新しいプロパティを追加することができません(ただし既存のプロパティの変更と削除はできる)。
引数
次の引数が渡ってきます。
-
target
: もとのオブジェクト
戻り値
preventExtensionsトラップ
は真偽値を返す必要があります。preventExtensions
の操作に成功した(オブジェクトが拡張不可能になった)ときはtrue
、失敗したときはfalse
を返す必要があります。
const target = {
hoge: "hoge",
};
const handler = {
preventExtensions: (target) => {
Object.preventExtensions(target);
return true;
},
};
const proxy = new Proxy(target, handler);
proxy.fuga = "fuga";
// ここから拡張不可能
Object.preventExtensions(proxy);
proxy.piyo = "piyo";
console.log(target); // {hoge: 'hoge', fuga: 'fuga'} => piyoは追加されていない
applyトラップ
applyトラップ
は関数呼び出しに対応するトラップです。new Proxy
の第1引数に渡すオブジェクトは関数である必要があります。
引数
次の引数が順に渡ってきます。
-
target
: もとのオブジェクト -
thisArg
: 関数呼び出し時のthis
-
argumentsList
: 関数呼び出し時の引数の配列
以下は関数実行時に渡した引数をconsole.log
で表示するコードです。
const target = (...args) => {
console.log([...args]);
};
const handler = {
apply: (target, thisValue, args) => {
return target.apply(thisValue, args);
},
};
const proxy = new Proxy(target, handler);
proxy("hoge", "fuga", "piyo"); // ['hoge', 'fuga', 'piyo']
constructトラップ
constructトラップ
はnew 演算子
に対するトラップです。applyトラップ
の時と同様に、このトラップではnew Proxy
の第1引数に渡すオブジェクトは、コンストラクタとして使用できる(new
できる)必要があります。
引数
次の引数が順に渡ってきます。
-
target
: もとのオブジェクト -
argumentsList
: コンストラクタに対する引数の配列 -
newTarget
: Proxyオブジェクト自身
戻り値
constructトラップ
はオブジェクトを返す必要があります。
const target = class Target {
constructor({ firstname, lastname, age }) {
this.firstname = firstname;
this.lastname = lastname;
this.age = age;
}
echo() {
console.log(
`${this.lastname}${this.firstname}さんは${this.age}歳です`
);
}
};
const handler = {
construct: (target, argumentsList, newTarget) => {
return new target(...argumentsList);
},
};
const proxy = new Proxy(target, handler);
const person = new proxy({
firstname: "太郎",
lastname: "佐藤",
age: 20,
});
person.echo(); // 佐藤太郎さんは20歳です
取り消し可能なProxyオブジェクト(Proxy.revocable)
Proxy.revocable
メソッドを使うことで取り消し可能なproxyオブジェクトを生成できます。
以下のように使います。
const target = {
// 略
};
const handler = {
// 略
};
const { proxy, revoke } = Proxy.revocable(target, handler);
基本的には通常のProxyオブジェクトの生成と同じ(new Proxy
と同じ)ですが、生成された取り消し可能なProxyオブジェクトはproxy
とrevoke
の2つのプロパティを持っています。
proxy
はnew Proxy(target, handler)
で生成できるProxyオブジェクトと同様なものです。
revoke
はproxy
を 無効にする(取り消す) メソッドです。
revoke
関数が実行されると、ハンドラー関数のトラップに関わる操作がすべてエラーとなります。
const target = { hoge: "fuga" };
const handler = {
get: (target, property, receiver) => {
return Reflect.get(target, property, receiver);
},
};
const { proxy, revoke } = Proxy.revocable(target, handler);
console.log(proxy.hoge); // "fuga"
revoke();
console.log(proxy.hoge); // Uncaught TypeError: Cannot perform 'get' on a proxy that has been revoked
proxy
が無効化されると、もととなるターゲットオブジェクトへの内部参照がなくなるので、メモリが節約できるという点でメリットがあります。
Reflect
ここでProxyと関わりのあるReflectについても軽くみておきましょう。
Reflectはオブジェクト操作を行うためのメソッドを提供する組み込みオブジェクトです。
Reflect自体はコンストラクタではないので、new
することはできません。(Mathオブジェクトのように)Reflectのメソッドとプロパティは静的です。
RefectはProxyのハンドラー関数と同じ名前で、同じ引数のメソッドをもちます。つまりProxyのトラップと1対1で対応するメソッドをもっているのです。
各々のメソッドの挙動を1つ1つ確認していくと、かなり冗長になってしまう(またReflectについてはこの記事の目的ではない)ので、本記事ではRelfect.get
だけ簡単に紹介します。
基本的にはProxyのgetトラップ
と同じ引数を持つので、Reflect.get
は次の3つの引数を順に取ります。
-
taregt
: 対象のオブジェクト -
property
: 取得するプロパティ名 -
receiver
: ゲッターがある場合、target
の呼び出しで使用するthis
(第3引数は省略可能)
const target = {
hoge: "fuga",
};
// target.hogeと同じ
console.log(Reflect.get(tagret, 'hoge')); // "hoge"
シンプルな挙動ですね。この場合target.hoge
と変わらないので、あまりメリットを感じませんが、ProxyとReflectを一緒に使うことで効果を発揮することがあります。
Proxyのgetトラップ
でReflect.get
を使ってみましょう。
const target = {
hoge: "fuga",
};
const handler = {
get: (target, property, receiver) => {
// target[property]と同じ?
return Reflect.get(target, property, receiver);
},
};
const proxy = new Proxy(target, handler);
console.log(proxy.hoge); // "fuga"
この場合でもtarget[property]
と置き換えられるように思えます。
では下記の場合はどうでしょうか。proxyで存在しないプロパティを取得しようとした時はdefaultValue
が返ってくること期待した実装です。
const target = {
hoge: "hogeValue",
get piyo() {
return this.fuga;
},
};
const handler = {
get: (target, property, receiver) => {
if (property in target) {
return target[property];
}
return "defaultValue";
},
};
const proxy = new Proxy(target, handler);
console.log(proxy.piyo); // undefined
なんと期待とは裏腹に、proxy.piyo
がundefined
となりました。
上記のコードをみるとgetトラップ
で返される値がtarget[property]
ですね。
この場合target.piyo
がゲッタなので、その中のthis
はtarget
となります。したがってtarget
上にはthis.fuga
が存在せず、undefined
が返ってきます。
ではdefaultValue
を返すためにはどうすれば良いでしょうか。このような場合の解決策としてReflect.get
を使います。
const target = {
hoge: "hogeValue",
get piyo() {
return this.fuga;
},
};
const handler = {
get: (target, property, receiver) => {
if (property in target) {
- return target[property];
+ return Reflect.get(target, property, receiver);
}
return "defaultValue";
},
};
const proxy = new Proxy(target, handler);
console.log(proxy.piyo); // defaultValue
今度は期待通りproxy.piyo
がdefaultValue
となりました。
肝となるのはReflect.get
の第3引数(receiver
)です。第3引数に渡されたオブジェクトが、ゲッタ呼び出しの際のthis
となるので、期待通りの挙動となります。
ざっくりですが、Reflectについて紹介しました。本記事ではこれ以上深掘りはしませんが、参考までにMDNのリンクだけ載せておきます。
ユースケース
さて、ここまでProxy(とReflect)の挙動についてみてきました。
しかし、挙動がわかってきたところで使う場面が思いつかない……なんてことはないでしょうか。実際、この記事を書いている私自身もユースケースをあまり思いつきませんでした。
ということで、Proxyのユースケースをいくつか調べてみましたので紹介します。
デフォルト値の取得
これは比較的思い浮かべやすい使い方ですね。
存在しないオブジェクトのプロパティを取得しようとした時にデフォルトの値を返すというものです。
const target = {
"1th": "金メダル",
"2th": "銀メダル",
"3th": "銅メダル",
};
const handler = {
get: (target, property, receiver) => {
if (property in target) {
return Reflect.get(target, property, receiver);
}
return "参加賞";
},
};
const proxy = new Proxy(target, handler);
console.log(proxy["1th"]); // 金メダル
console.log(proxy["4th"]); // 参加賞
プロパティの加工
プロパティを加工することで、大文字/小文字をを区別せずに同じ値を取得できます。
const handler = {
get: (target, property, receiver) => {
return Reflect.get(target, property.toLowerCase(), receiver);
},
set: (target, property, value, receiver) => {
Reflect.set(target, property.toLowerCase(), value, receiver);
return true;
},
};
const proxy = new Proxy({}, handler);
proxy.hoge = "fuga";
console.log(proxy.HOGE); // "fuga"
バリデーション
setトラップ
の箇所でも書いてしましたが、プロパティの値の設定時にバリデーションが可能となります。
const target = {
name: "田中",
age: 20,
};
const handler = {
set: (target, property, value, receiver) => {
if (!(property in target)) {
console.error("文字列を入力してください");
return false;
}
if (property === "name" && typeof value !== "string") {
console.error("nameには文字列を入力してください");
return false;
}
if (property === "age" && typeof value !== "number") {
console.error("ageには数字を入力してください");
return false;
}
Reflect.set(target, property, value, receiver);
return true;
},
};
const proxy = new Proxy(target, handler);
proxy.name = "佐藤";
proxy.age = "30"; // ageには数字を入力してください
console.log(target); // {name: '佐藤', age: 20}
配列に対しても同様です。
const target = [0, 1, 2];
const handler = {
set: (target, property, value, receiver) => {
if (typeof value === "number") {
Reflect.set(target, property, value, receiver);
return true;
}
return false;
},
};
const proxy = new Proxy(target, handler);
proxy.push(3);
proxy.push("4"); // Uncaught TypeError: 'set' on proxy: trap returned falsish for property '4'
プライベートプロパティ
外部からアクセスできないプライベートなプロパティをつくることができます。
const target = {
public: "Public",
_private: "Private",
getPrivate: function () {
return this._private;
},
};
const handler = {
get: (target, property, receiver) => {
const value = Reflect.get(target, property, receiver);
if (typeof value === "function") {
return value.bind(target);
}
if (!property.startsWith("_")) {
return value;
}
return undefined;
},
ownKeys: object => {
return Reflect.ownKeys(object).filter(key => !key.startsWith("_"));
},
has: (target, property) => {
if (property.startsWith("_")) {
return false;
}
return Reflect.has(target, property);
},
set: (target, property, value, receiver) => {
if (property.startsWith("_")) {
console.error(`${property}に値は設定できません`);
return false;
}
return Reflect.set(target, property, value, receiver);
},
};
const proxy = new Proxy(target, handler);
console.log(proxy.public); // "Public"
console.log(proxy._private); // undefined
console.log("_private" in proxy); // false
console.log(proxy.getPrivate()); // Private
console.log(Object.keys(proxy)); // ['public']
proxy._private = "hoge"; // _privateに値は設定できません
ReadOnlyなプロパティ
ReadOnlyなプロパティをつくることが可能です。
const ReadOnlyError = () => {
throw new Error("ReadOnlyError");
};
const target = {
hoge: "fuga",
};
const handler = {
set: ReadOnlyError,
defineProperty: ReadOnlyError,
deleteProperty: ReadOnlyError,
setPrototypeOf: ReadOnlyError,
preventExtensions: ReadOnlyError,
};
const proxy = new Proxy(target, handler);
proxy.hoge = "piyo"; // Uncaught Error: ReadOnlyError
配列のマイナスインデックス
配列のインデックスに負の値を対応させる(負の値の場合、末尾から数える)ことも可能です。
const target = ["hoge", "fuga", "piyo"];
const handler = {
get: (target, property, receiver) => {
const num = Number(property);
if (num < 0) {
const i = target.length + num;
return Reflect.get(target, i, receiver);
}
return Reflect.get(target, property, receiver);
},
};
const proxy = new Proxy(target, handler);
// マイナスインデックスで取得したら、末尾から数える
console.log(proxy[0]); // "hoge"
console.log(proxy[-1]); // "piyo"
console.log(proxy[-2]); // "fuga"
生存時間のあるキャッシュ
TTL(生存時間)のあるキャッシュを実装できます。
以下は一定時間経過後に、hoge
プロパティへアクセスするとundefined
が返ってくるコードです。
const SECONDS = 5;
const start = Date.now();
const isExpired = s => {
return Date.now() - start > s * 1000;
};
const target = {
hoge: "fuga",
};
const handler = {
get: (target, propetry) => {
return isExpired(SECONDS) ? undefined : Reflect.get(target, propetry);
},
};
const proxy = new Proxy(target, handler);
setTimeout(() => {
console.log(proxy.hoge); // "fuga"
}, (SECONDS - 1) * 1000);
setTimeout(() => {
console.log(proxy.hoge); // undefined
}, (SECONDS + 1) * 1000);
データバインディング
オブジェクト間で、値の同期をシンプルに実装できます。
オブジェクトの値が変わった時に、input
の値を変更してみましょう。
加えてinput
が変化した時に、オブジェクトの値を変更することで双方向のデータ同期を実装できます。
// <input type="text" id="inputText" /> が HTML上に存在すると仮定
const inputText = document.getElementById("inputText");
const target = {
value: "",
};
const handler = {
set: (target, property, value, receiver) => {
if (property === "value") {
inputText.value = value;
return Reflect.set(target, property, value, receiver);
}
return false;
},
};
const proxy = new Proxy(target, handler);
// proxyの値を変えると、inputの値が変わる
proxy.value = "hoge";
// inputに値を入力すると、proxyの値が変わる
inputText.addEventListener("input", e => {
proxy.value = e.currentTarget.value;
});
別の処理のトリガー
別の処理のトリガーとして使うこともできます。
たとえば何かしらの入力が成功した時に、メールを送信するような実装が考えられます。
const sendEmail = () => {
// ...処理
console.log("メールを送信しました");
};
const errorLog = () => {
// ...処理
console.log("処理に失敗しました");
};
const target = {
status: "",
};
const handler = {
set: (target, property, value, receiver) => {
if (property !== "status") {
console.error("存在しないプロパティです");
return false;
}
if (value === "success") {
sendEmail();
return Reflect.set(target, property, value, receiver);
}
if (value === "failure") {
errorLog();
return Reflect.set(target, property, value, receiver);
}
console.error("success か failure を入力してください");
return false;
},
};
const proxy = new Proxy(target, handler);
proxy.status = "success"; // "メールを送信しました"
まとめ
今回はJavaScriptのProxyについて、改めて学び直しました。
正直な話、ほとんど使ったことも遭遇したこともありませんでしたが、今回いろいろ調べてみて少し理解が進んだと思います。
今後は適切な場面で使えると嬉しいですね。
参考
Discussion