JavaScript之Proxy
Proxy代理是一个共通的概念,可以起到拦截的作用。ES6里将Proxy标准化了,提供了Proxy构造函数,用来生成Proxy实例。例如var p = new Proxy(target, handler);
。参照MDN
构造函数有两个参数,第一个参数target是要拦截的对象,第二个参数是拦截函数对象。先看一个最基本的例子,感受一下:
var handler = {
get: function(target, name){
return name in target ? target[name] : 'No prop!';
}
};
var p = new Proxy({}, handler);
p.a = 1;
p.b = 2;
console.log(p.a); //1
console.log(p.b); //2
console.log(p.c); //No prop!
上面例子中为Object对象定义了get的拦截行为。如果对象内有该属性,就返回属性值。如果对象内没有该属性,就返回错误信息。结果一目了然,当你要get对象属性值时,会被Proxy拦截到,最终得到的是经由handler拦截函数处理后的值。小细节注意一下,如示例那样,拦截操作是在Proxy实例对象p上进行的,而非在{}对象上进行的。
Proxy的handler回调函数提供了13种拦截行为:
- getPrototypeOf / setPrototypeOf
- isExtensible / preventExtensions
- ownKeys / getOwnPropertyDescriptor
- defineProperty / deleteProperty
- get / set / has
- apply / construct
getPrototypeOf / setPrototypeOf
handler.getPrototypeOf(target)可以拦截取对象的原型对象的行为:
Object.getPrototypeOf()
Reflect.getPrototypeOf()
.proto
Object.prototype.isPrototypeOf()
Instanceof
参数target即想获取它原型对象的对象。返回值是返回该原型对象或null。参照MDN。例如:
var proto = {};
var p = new Proxy({}, {
getPrototypeOf(target) {
return proto;
}
});
console.log(Object.getPrototypeOf(p) === proto); // true
handler.setPrototypeOf(target, prototype)可以拦截变更对象的原型对象的行为:
Object.setPrototypeOf()
Reflect.setPrototypeOf()
参数target是目标对象,参数prototype是给目标对象设置的原型对象或null。返回值如果目标对象的原型对象被成功改变,返回true,否则返回false。参照MDN。例如:
var handler = {
setPrototypeOf (target, prototype) {
return false;
}
};
var newProto = {};
var p = new Proxy({}, handler);
console.log(Object.setPrototypeOf(p, newProto));
//TypeError: proxy setPrototypeOf handler returned false
console.log(Reflect.setPrototypeOf(p, newProto)); //false
isExtensible / preventExtensions
handler.isExtensible(target)可以拦截判断对象是否可扩展(即是否能追加新属性)的行为:
Object.isExtensible()
Reflect.isExtensible()
参数target是目标对象。返回值如果目标对象可扩展,返回true,否则返回false。参照MDN。例如:
var p = new Proxy({}, {
isExtensible: function(target) {
console.log("called");
return true;
}
});
console.log(Object.isExtensible(p));
//called
//true
handler.preventExtensions(target)可以拦截阻止对象被扩展(即不能为对象增加新属性,但是既有属性的值仍然可以更改,也可以把属性删除)的行为:
Object.preventExtensions()
Reflect.preventExtensions()
参数target是目标对象。返回值如果想阻止对象被扩展返回true,否则返回false。但要注意只有在Object.isExtensible(proxy)为false时,才能返回true,否则会报错。参照MDN。例如:
var obj = {};
var p = new Proxy(obj, {
preventExtensions: function(target) {
console.log(Object.isExtensible(target));
return true;
}
});
console.log(Object.preventExtensions(p));
//true
//TypeError: proxy can't report an extensible object as non-extensible
因为Object.isExtensible(target);返回ture,表示对象可扩展,此时你拦截preventExtensions并返回true的话会报错,无法阻止一个可扩展对象进行扩展。所以通常应该在handler.preventExtensions里调用Object.preventExtensions来阻止对象的可扩展性,让Object.isExtensible(target);返回false:
var obj = {};
obj.newProp = 1;
console.log(obj.newProp); //1
var p = new Proxy(obj, {
preventExtensions: function(target) {
Object.preventExtensions(target);
console.log(Object.isExtensible(target));
return true;
}
});
console.log(Object.preventExtensions(p));
//false
//Object {}
obj.newProp2 = 2;
console.log(obj.newProp2); //undefined
ownKeys / getOwnPropertyDescriptor
handler.ownKeys(target)可以拦截获取属性名的行为:
Object.getOwnPropertyNames()
Object.getOwnPropertySymbols()
Object.keys()
Reflect.ownKeys()
参数target是目标对象。返回一个数组包含对象所有自身的属性,而Object.keys()仅返回对象可遍历的属性。参照MDN。例如拦截前缀为下划线的属性名:
let person = {
_age: 33,
_location: 'shanghai',
name: 'Jack'
};
let handler = {
ownKeys (target) {
return Reflect.ownKeys(target).filter(key => key[0] !== '_');
}
};
let p = new Proxy(person, handler);
for (let key of Object.keys(p)) {
console.log(person[key]);
}
//Jack
handler.getOwnPropertyDescriptor(target, prop)可以拦截获取自身属性描述的行为:
Object.getOwnPropertyDescriptor()
Reflect.getOwnPropertyDescriptor()
参数target是目标对象,参数prop是自身的属性名。返回该属性的描述或undefined。参照MDN。例如拦截获取前缀为下划线的属性并返回undefined:
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);
console.log(Object.getOwnPropertyDescriptor(proxy, 'wat')); //undefined
console.log(Object.getOwnPropertyDescriptor(proxy, '_foo')); //undefined
console.log(Object.getOwnPropertyDescriptor(proxy, 'baz'));
//{ value: 'tar', writable: true, enumerable: true, configurable: true }
defineProperty / deleteProperty
handler.defineProperty(target, property, descriptor)可以拦截定义属性的行为:
Object.defineProperty()
Reflect.defineProperty()
参数target是目标对象,参数property是属性名,参数descriptor是属性描述符。返回值如果该属性被定义成功,返回true,否则返回false。参照MDN。例如:
var obj = {};
var p = new Proxy(obj, {
defineProperty: function(target, prop, descriptor) {
console.log("called: " + prop);
Object.defineProperty(target, "a", desc)
return true;
}
});
var desc = { configurable: true, enumerable: true, value: 10 };
console.log(Object.defineProperty(p, "a", desc));
//called: a
//Object { a=10 }
console.log(obj.a); //10
handler.deleteProperty(target, property)可以拦截delete行为:
Property deletion: delete proxy[foo] and delete proxy.foo
Reflect.deleteProperty()
参数target是目标对象,参数property是要删除的属性名。返回值如果该属性被删除成功,返回true,否则返回false。参照MDN。例如不允许删除前缀为下划线的属性:
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
get / set / has
handler.get(target, property, receiver)可以拦截读取对象属性值的行为:
Property access: proxy[foo]and proxy.bar
Inherited property access: Object.create(proxy)[foo]
Reflect.get()
参数target是目标对象,参数property是属性名,参数receiver是一个可选对象,有时我们必须要搜索几个对象,可能是一个在receiver原型链上的对象。返回值就是属性值。参照MDN。例如:
var person = {
name: "Jack"
};
var p = new Proxy(person, {
get: function(target, prop, receiver) {
if (prop in target) {
return target[prop];
} else {
throw new ReferenceError("Property \"" + prop + "\" does not exist.");
}
}
});
console.log(p.name); //Jack
console.log(p.age); //ReferenceError: Property "age" does not exist.
handler.set(target, property, value, receiver)可以拦截设置对象属性值的行为:
Property assignment: proxy[foo] = bar and proxy.foo = bar
Inherited property assignment: Object.create(proxy)[foo] = bar
Reflect.set()
参数target是目标对象,参数property是属性名,参数value是属性值,参数receiver是一个可选对象,有时我们必须要搜索几个对象,可能是一个在receiver原型链上的对象。返回值如果设值成功,返回true,否则返回false。参照MDN。例如:
var handler = {
set: function(obj, prop, value) {
if (prop === 'age') {
if (!Number.isInteger(value)) {
throw new TypeError('The age is not an integer');
}
}
obj[prop] = value;
return true;
}
};
var p = new Proxy({}, handler);
p.age = 100;
console.log(p.age); //100
p.age = 'Jack'; //TypeError: The age is not an integer
示例中,如果值非数字则直接抛出异常。利用set方法,可以实现数据绑定,当值发生变化时,自动更新DOM。
因为get和set方法比较常用,再举个例子。例如私有属性可以在属性名请加上_下划线,但这只是潜规则,外部仍旧能畅通无阻地读写这些属性。现在用get和set方法来真正阻止外部读写带下划线的属性:
var handler = {
get (target, property) {
invariant(property, 'get');
return target[property];
},
set (target, property, value) {
invariant(property, 'set');
return true;
}
};
function invariant (property, action) {
if (property[0] === '_') {
throw new Error(`Invalid attempt to ${action} private "${property}" property`);
}
}
var target = {};
var p = new Proxy(target, handler);
p._prop; //Error: Invalid attempt to get private "_prop" property
p._prop = 'c'; //Error: Invalid attempt to set private "_prop" property
handler.has(target, prop)可以拦截检查是否含有该参数的in行为:
Property query: foo in proxy
Inherited property query: foo in Object.create(proxy)
with check: with(proxy) { (foo); }
Reflect.has()
参数target是目标对象,参数prop是属性名。返回值如果含有该属性,返回true,否则返回false。参照MDN。例如用has方法隐藏带下划线前缀的属性,不让其被in运算符发现:
var handler = {
has (target, key) {
if (key[0] === '_') {
return false;
}
return key in target;
}
};
var target = { _prop: 'foo', prop: 'foo' };
var proxy = new Proxy(target, handler);
console.log('_prop' in proxy); //false
如果原对象不可扩展,用has拦截会报错。
var obj = { a: 10 };
Object.preventExtensions(obj);
var p = new Proxy(obj, {
has: function(target, prop) {
return false;
}
});
"a" in p;
//TypeError: proxy can't report an existing own property as non-existent on a non-extensible object
注意,has方法拦截的是hasProperty操作,而不是hasOwnProperty操作,即has方法不care该属性是对象自身的属性,还是继承来的属性。另外,虽然for…in循环也用到了in运算符,但是Chrome55,Firefox49,Opera39上试下来,for…in里并不触发has拦截。
apply / construct
Proxy不止可以拦截对象的操作还能用这两个方法拦截函数。
handler.apply(target, thisArg, argumentsList)可以拦截函数调用的行为,包括apply调用,call调用:
proxy(…args)
Function.prototype.apply() and Function.prototype.call()
Reflect.apply()
参数target是函数对象,参数thisArg是函数对象的this,参数argumentsList是函数参数。返回值可返回任意东西。参照MDN。例如:
var target = function () { return 'I am the target'; };
var handler = {
apply: function () {
return 'I am proxy';
}
};
var p = new Proxy(target, handler);
console.log(p()); //I am the proxy
再看看apply和call的拦截:
var twice = {
apply (target, ctx, args) {
return Reflect.apply(...arguments) * 2;
}
};
function sum (left, right) {
return left + right;
};
var proxy = new Proxy(sum, twice);
console.log(proxy(1, 2)); //6
console.log(proxy.call(null, 3, 4)); //14
console.log(proxy.apply(null, [5, 6])); //22
handler.construct(target, argumentsList, newTarget)可以拦截new命令:
new proxy(…args)
Reflect.construct()
参数target是目标对象,参数argumentsList是构造函数参数,参数newTarget。返回new后的对象,注意必须是对象,否则例如返回数字会报错。参照MDN。例如:
var p = new Proxy(function() {}, {
construct: function(target, args) {
console.log('called: ' + args.join(', '));
return { value: args[0] * 10 };
}
});
console.log(new p(1).value);
//called: 1
//10
同一个拦截器函数,可以同时设置多个上面介绍的13种拦截方法:
var handler = {
get: function(target, name) {
if (name === 'prototype') {
return Object.prototype;
}
return 'Hello, ' + name;
},
apply: function(target, thisBinding, args) {
return args[0];
},
construct: function(target, args) {
return {value: args[1]};
}
};
var fproxy = new Proxy(function(x, y) {
return x + y;
}, handler);
console.log(fproxy(1, 2)); //1
console.log(new fproxy(1,2)); //Object { value=2}
console.log(fproxy.prototype === Object.prototype); //true
console.log(fproxy.foo); //Hello, foo
Proxy.revocable()
上面介绍的都是handler对象的方法。Proxy自身还有个静态方法Proxy.revocable(target, handler),用于创建并返回一个可取消的Proxy对象。返回的这个可取消的Proxy对象有两个属性:proxy和revoke
属性proxy会调用new Proxy(target, handler)创建一个新的Proxy对象。属性revoke是一个无参函数,用于取消,即让该Proxy对象无效。例如:
var revocable = Proxy.revocable({}, {
get: function(target, name) {
return "[[" + name + "]]";
}
});
var p = revocable.proxy;
console.log(p.foo); // "[[foo]]"
revocable.revoke();
console.log(p.foo); //TypeError: illegal operation attempted on a revoked proxy
p.foo = 1; //TypeError: illegal operation attempted on a revoked proxy
delete p.foo; //TypeError: illegal operation attempted on a revoked proxy
console.log(typeof p); //object
示例中Proxy.revocable方法返回一个可取消的Proxy对象。调用该对象的proxy属性得到真实的Proxy对象。如果不想用了,可以调用revoke()方法将该Proxy对象无效化。之后对Proxy对象的任何操作都将抛出异常。