manlili blog

ES6之Proxy

Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。

1
var proxy = new Proxy(target, handler);

new Proxy()表示生成一个Proxy实例,target参数表示所要拦截的目标对象,handler参数也是一个对象,用来定制拦截行为.可省略。

1
2
3
4
5
var target = {};
var handler = {};
var proxy = new Proxy(target, handler);
proxy.a = 'b';
target.a // "b"

上面代码中,handler是一个空对象,没有任何拦截效果,访问proxy就等同于访问target。

Proxy 实例的方法

get方法用于拦截某个属性的读取操作。

get(target, propKey, receiver)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var person = {
name: "张三"
};
var proxy = new Proxy(person, {
get: function(target, property) {
if (property in target) {
return target[property];
} else {
throw new ReferenceError("Property \"" + property + "\" does not exist.");
}
}
});
proxy.name // "张三"
proxy.age // 抛出一个错误

get方法可以继承:

1
2
3
4
5
6
7
8
9
let proto = new Proxy({}, {
get(target, propertyKey, receiver) {
console.log('GET '+propertyKey);
return target[propertyKey];
}
});
let obj = Object.create(proto);
obj.xxx // "GET xxx"

set(target, propKey, value, receiver)

set方法用来拦截某个属性的赋值操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let validator = {
set: function(obj, prop, value) {
if (prop === 'age') {
if (!Number.isInteger(value)) {
throw new TypeError('The age is not an integer');
}
if (value > 200) {
throw new RangeError('The age seems invalid');
}
}
// 对于age以外的属性,直接保存
obj[prop] = value;
}
};
let person = new Proxy({}, validator);
person.age = 100;
person.age // 100
person.age = 'young' // The age is not an integer
person.age = 300 // The age seems invalid

has(target, propKey)

has方法用来拦截HasProperty操作

1
2
3
4
5
6
7
8
9
10
11
var handler = {
has (target, key) {
if (key[0] === '_') {
return false;
}
return '存在';
}
};
var target = { _prop: 'foo', prop: 'foo' };
var proxy = new Proxy(target, handler);
'_prop' in proxy // false

deleteProperty(target, propKey)

deleteProperty方法用于拦截delete操作,如果这个方法抛出错误或者返回false,当前属性就无法被delete命令删除。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var handler = {
deleteProperty (target, key) {
invariant(key, 'delete');
return true;
}
};
function invariant (key, action) {
if (key[0] === '_') {
throw new Error(`Invalid attempt to ${action} private "${key}" property`);
}
}
var target = { _prop: 'foo' };
var proxy = new Proxy(target, handler);
delete proxy._prop // Error: Invalid attempt to delete private "_prop" property

ownKeys(target)

ownKeys方法用来拦截对象自身属性的读取操作。具体来说,拦截以下操作:
(1)Object.getOwnPropertyNames()
(2)Object.getOwnPropertySymbols()
(3)Object.keys()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let target = {
a: 1,
b: 2,
c: 3
};
let handler = {
ownKeys(target) {
return ['a'];
}
};
let proxy = new Proxy(target, handler);
Object.keys(proxy) // [ 'a' ]
var p = new Proxy({}, {
ownKeys: function(target) {
return ['a', 'b', 'c'];
}
});
Object.getOwnPropertyNames(p)
// [ 'a', 'b', 'c' ]

getOwnPropertyDescriptor(target, propKey)

getOwnPropertyDescriptor方法拦截Object.getOwnPropertyDescriptor(),返回一个属性描述对象或者undefined。

1
2
3
4
5
6
7
8
9
10
11
12
13
var handler = {
getOwnPropertyDescriptor (target, key) {
if (key[0] === '_') {
return;
}
return Object.getOwnPropertyDescriptor(target, key);
}
};
var target = { _foo: 'bar', baz: 'tar' };
var proxy = new Proxy(target, handler);
Object.getOwnPropertyDescriptor(proxy, 'wat'); // undefined
Object.getOwnPropertyDescriptor(proxy, '_foo'); // undefined
Object.getOwnPropertyDescriptor(proxy, 'baz'); // { value: 'tar', writable: true, enumerable: true, configurable: true }

defineProperty(target, propKey, propDesc)

defineProperty方法拦截了Object.defineProperty操作。

1
2
3
4
5
6
7
8
var handler = {
defineProperty (target, key, descriptor) {
return false;
}
};
var target = {};
var proxy = new Proxy(target, handler);
proxy.foo = 'bar'; // TypeError: proxy defineProperty handler returned false for property '"foo"'

preventExtensions(target)

preventExtensions方法拦截Object.preventExtensions()。该方法必须返回一个布尔值,否则会被自动转为布尔值。
这个方法有一个限制,只有目标对象不可扩展时(即Object.isExtensible(proxy)为false),proxy.preventExtensions才能返回true,否则会报错。

1
2
3
4
5
6
7
var p = new Proxy({}, {
preventExtensions: function(target) {
return true;
}
});
Object.preventExtensions(p) // 报错

上面代码中,proxy.preventExtensions方法返回true,但这时Object.isExtensible(proxy)会返回true,因此报错。
为了防止出现这个问题,通常要在proxy.preventExtensions方法里面,调用一次Object.preventExtensions:

1
2
3
4
5
6
7
8
9
var p = new Proxy({}, {
preventExtensions: function(target) {
console.log('called');
Object.preventExtensions(target);
return true;
}
});
Object.preventExtensions(p) // "called" // true

getPrototypeOf(target)

getPrototypeOf方法主要用来拦截获取对象原型。具体来说,拦截下面这些操作。
(1)Object.prototype.proto
(2)Object.prototype.isPrototypeOf()
(3)Object.getPrototypeOf()
(4)Reflect.getPrototypeOf()
(5)instanceof

1
2
3
4
5
6
7
var proto = {};
var p = new Proxy({}, {
getPrototypeOf(target) {
return proto;
}
});
Object.getPrototypeOf(p) === proto // true

isExtensible(target)

isExtensible方法拦截Object.isExtensible操作。

1
2
3
4
5
6
7
8
var p = new Proxy({}, {
isExtensible: function(target) {
console.log("called");
return true;
}
});
Object.isExtensible(p) // "called" true

setPrototypeOf(target, proto)

setPrototypeOf方法主要用来拦截Object.setPrototypeOf方法。

1
2
3
4
5
6
7
8
9
var handler = {
setPrototypeOf (target, proto) {
throw new Error('Changing the prototype is forbidden');
}
};
var proto = {};
var target = function () {};
var proxy = new Proxy(target, handler);
Object.setPrototypeOf(proxy, proto); // Error: Changing the prototype is forbidden

apply(target, object, args)

apply方法拦截函数的调用、call和apply操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var target = function () { return 'I am the target'; };
var handler = {
apply () {
return 'I am the proxy';
}
};
var p = new Proxy(target, handler);
p(); // "I am the proxy"
var twice = {
apply (target, ctx, args) {
return Reflect.apply(...arguments) * 2;
}
};
function sum (left, right) {
return left + right;
};
var proxy = new Proxy(sum, twice);
proxy(1, 2) // 6
proxy.call(null, 5, 6) // 22
proxy.apply(null, [7, 8]) // 30
Reflect.apply(proxy, null, [9, 10]) // 38

construct(target, args)

construct方法用于拦截new命令

1
2
3
4
5
6
7
8
var p = new Proxy(function () {}, {
construct: function(target, args) {
console.log('called: ' + args.join(', '));
return { value: args[0] * 10 };
}
});
(new p(1)).value; // "called: 1" // 10

Proxy.revocable()

Proxy.revocable方法返回一个可取消的 Proxy 实例。

1
2
3
4
5
6
7
8
9
10
let target = {};
let handler = {};
let {proxy, revoke} = Proxy.revocable(target, handler);
proxy.foo = 123;
proxy.foo // 123
revoke();
proxy.foo // TypeError: Revoked

Proxy.revocable方法返回一个对象,该对象的proxy属性是Proxy实例,revoke属性是一个函数,可以取消Proxy实例。上面代码中,当执行revoke函数之后,再访问Proxy实例,就会抛出一个错误。

this 问题

虽然 Proxy 可以代理针对目标对象的访问,但它不是目标对象的透明代理,即不做任何拦截的情况下,也无法保证与目标对象的行为一致。主要原因就是在 Proxy 代理的情况下,目标对象内部的this关键字会指向 Proxy 代理。

1
2
3
4
5
6
7
8
9
10
11
const target = {
m: function () {
console.log(this === proxy);
}
};
const handler = {};
const proxy = new Proxy(target, handler);
target.m() // false
proxy.m() // true

请我喝杯果汁吧!