ProxyとReflectを使ってオブジェクトを拡張する

Proxyオブジェクトがサポートされたことで、
JavaScriptにでもオブジェクト操作が柔軟に拡張できるようになった。

これにより処理を動的に拡張するメタプログラミングできる。
ということで、参考になりそうな事例があったので試してみた。

caniuseでProxyのサポートを見るとモダンブラウザでは実装済である。
まず、Proxyオブジェクトをおさらいする。

Proxyオブジェクト

基本構文は次の通り

1
2
3
4
5
6
7
8
9
10
11
12
// target
const target = {};

// handler
const handler = {
// trap
get(target, property) {
return Reflect.get(target, property);
}
}

const proxy = new Proxy(target, handler);

targetはProxyで拡張するオブジェクト。
handlerはtrapを含んだオブジェクトとなる。
trapとはオブジェクトのプロパティへのアクセスを提供するメソッド。
上記の構文例だとgetとなる。
getの中ででてくるReflectオブジェクトはトラップされるビルトインメソッド(処理)を提供する。

ちなみにtrapが設定されてない場合は、デフォルトのメソッドが実行される。

handlerとtrap一覧

ブラウザへの実装が未対応のものもあるが、handlerとtrapとの関係は以下のようになっている。

handler / trap proxyされるビルトインメソッド
handler.getPrototypeOf() Object.getPrototypeOf()
handler.setPrototypeOf() Object.setPrototypeOf()
handler.isExtensible() Object.isExtensible()
handler.preventExtensions() Object.preventExtensions()
handler.getOwnPropertyDescriptor() Object.getOwnPropertyDescriptor()
handler.defineProperty() Object.defineProperty()
handler.has() in 演算子 (ロパティが指定されたオブジェクトにある)
prop in Object
handler.get() プロパティ値を取得
handler.set() プロパティ値を設定
handler.deleteProperty() delete 演算子
delete Object.prop
handler.ownKeys() Object.getOwnPropertyNames()
Object.getOwnPropertySymbols()
handler.apply() 関数呼び出し
handler.construct() new 演算子
new Object()

いくつか試す

objectのプロパティ変更イベント

プロパティ変更するメソッドのtrapすることで変更イベントを検知して特定の処理を実施できる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
const onChangeObject = (target, func) => {
const handler = {
set(target, property, value) {
const currentValue = target[property];
func(target, property, value, currentValue);
return Reflect.set(target, property, value);
},
defineProperty(target, property, descriptor) {
func(target, property, descriptor, undefined);
return Reflect.defineProperty(target, property, descriptor);
},
deleteProperty(target, property) {
const currentValue = target[property];
func(target, property, undefined, currentValue);
return Reflect.deleteProperty(target, property);
}
}

return new Proxy(target, handler);
}

let Animal = {
name: 'Pochi',
kind: 'dog',
}

const handle = (target, property, value, currentValue) => {
console.log(target);
console.log(`property - ${property}, new value - ${value}, current value - ${currentValue}`);
}

const watchedObject = onChangeObject.apply(null, [Animal, handle]);
watchedObject.name = 'Taro'; // property - name, new value - Taro, current value - Pochi
delete watchedObject.name; // property - name, new value - undefined, current value - Taro

enum

enumもできる

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
const createEnum = target =>{
const handler = {
set() {
throw new TypeError('Enum is read only');
},
setPrototypeOf() {
throw new TypeError('Enum is read only');
},
defineProperty() {
throw new TypeError('Enum is read only');
},
deleteProperty() {
throw new TypeError('Enum is read only');
},
get(target, property) {
if (!target.hasOwnProperty(property)) {
throw new ReferenceError(`Unknown enum key "${property}"`);
}
return Reflect.get(target, property);
}
}

return new Proxy(target, handler);
}

const object = {
ja: 'Japan',
us: 'United States of America'
}

const customEnum = createEnum.apply(null, [object]);
console.log(customEnum.ja); // Japan
console.log(customEnum.us); // United States of America

try {
customEnum.uk
} catch(error) {
console.log(error.message); // Unknown enum key "uk"
}

try {
customEnum.uk = 'United Kingdom';
} catch(error) {
console.log(error.message); // Enum is read only
}

// Object.freeze を使う

const freesedObject = Object.freeze(object);

console.log(freesedObject.ja); // Japan
console.log(freesedObject.us); // United States of America

const fail = (function () {
'use strict';
try {
freesedObject.uk = 'United Kingdom';
} catch(error) {
console.log(error.message); // Cannot add property uk, object is not extensible
}
})();

cached object

一定時間キャッシュ可能なオブジェクト

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
const objectCache = (target, ttl) => {
const data = {};
const handler = {
set(target, property, value) {
data[property] = new Date().getTime() + ttl;
return Reflect.set(target, property, value);
},
get(target, property) {
return Reflect.get(target, property);
}
}

function deleteCache() {
for(key in data) {
if (data[key] < new Date().getTime()) {
delete target[key];
}
}
}

setInterval(deleteCache, ttl);

return new Proxy(target, handler);
}

const cachedPbject = objectCache.apply(null, [{}, 1000]);

cachedPbject.test = 1;

console.log(cachedPbject); // {test: 1}

setTimeout(function() {
console.log(cachedPbject.test);
}, 1001); // undefined

object property validation

objectのプロパティ設定値をvalidateする。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const withValidate = target => {
const regexp = RegExp('[0-9]');
const handler = {
set(target, property, value) {
if (regexp.test(value)) {
return Reflect.set(target, property, value);
} else {
throw new TypeError(`${value} is not correct type`);
}
}
}
return new Proxy(target, handler);
}

const withValidationObject = withValidate.apply(null, [{}]);

try {
withValidationObject.int = 'a';
console.log(withValidationObject.int);
} catch(error) {
console.log(error.message); // a is not correct type
}

try {
withValidationObject.int = 1;
console.log(withValidationObject.int);
} catch(error) {
console.log(error.message);
}

singleton

シングルトンパターンを実現する

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
const createSingleton = target => {
let instance;
const handler = {
construct(target, argumentsList) {
if (!instance) {
instance = Reflect.construct(target, argumentsList);
}

return instance;
}
}

return new Proxy(target, handler);
}

const Person = function() {
this.name = '';
}

const person1 = new Person();
const person2 = new Person();

person1.name = 'Taro';
person2.name = 'Jiro';

console.log(person1.name); // Taro
console.log(person2.name); // Jiro

console.log(`Not singleton ${person1.name == person2.name}`);

const SingletonPerson = createSingleton.apply(null, [Person])
const person3 = new SingletonPerson();
person3.name = 'Taro';
const person4 = new SingletonPerson();
person3.name = 'Jiro';

console.log(person3.name); // Jiro
console.log(person4.name); // Jiro

console.log(`Is singleton ${person3.name == person4.name}`);

参考にしたページ

jsProxy
Proxy
Reflect

Comments