Proxy代理是一個共通的概念,可以起到攔截的作用。ES6里將Proxy標(biāo)準(zhǔn)化了,提供了Proxy構(gòu)造函數(shù),用來生成Proxy實(shí)例。例如var p = new Proxy(target, handler);。參照MDN
構(gòu)造函數(shù)有兩個參數(shù),第一個參數(shù)target是要攔截的對象,第二個參數(shù)是攔截函數(shù)對象。先看一個最基本的例子,感受一下:
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的攔截行為。如果對象內(nèi)有該屬性,就返回屬性值。如果對象內(nèi)沒有該屬性,就返回錯誤信息。結(jié)果一目了然,當(dāng)你要get對象屬性值時,會被Proxy攔截到,最終得到的是經(jīng)由handler攔截函數(shù)處理后的值。小細(xì)節(jié)注意一下,如示例那樣,攔截操作是在Proxy實(shí)例對象p上進(jìn)行的,而非在{}對象上進(jìn)行的。
Proxy的handler回調(diào)函數(shù)提供了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
參數(shù)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()
參數(shù)target是目標(biāo)對象,參數(shù)prototype是給目標(biāo)對象設(shè)置的原型對象或null。返回值如果目標(biāo)對象的原型對象被成功改變,返回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)可以攔截判斷對象是否可擴(kuò)展(即是否能追加新屬性)的行為:
Object.isExtensible()
Reflect.isExtensible()
參數(shù)target是目標(biāo)對象。返回值如果目標(biāo)對象可擴(kuò)展,返回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)可以攔截阻止對象被擴(kuò)展(即不能為對象增加新屬性,但是既有屬性的值仍然可以更改,也可以把屬性刪除)的行為:
Object.preventExtensions()
Reflect.preventExtensions()
參數(shù)target是目標(biāo)對象。返回值如果想阻止對象被擴(kuò)展返回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
因?yàn)镺bject.isExtensible(target);返回ture,表示對象可擴(kuò)展,此時你攔截preventExtensions并返回true的話會報錯,無法阻止一個可擴(kuò)展對象進(jìn)行擴(kuò)展。所以通常應(yīng)該在handler.preventExtensions里調(diào)用Object.preventExtensions來阻止對象的可擴(kuò)展性,讓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()
參數(shù)target是目標(biāo)對象。返回一個數(shù)組包含對象所有自身的屬性,而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()
參數(shù)target是目標(biāo)對象,參數(shù)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()
參數(shù)target是目標(biāo)對象,參數(shù)property是屬性名,參數(shù)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()
參數(shù)target是目標(biāo)對象,參數(shù)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()
參數(shù)target是目標(biāo)對象,參數(shù)property是屬性名,參數(shù)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)可以攔截設(shè)置對象屬性值的行為:
Property assignment: proxy[foo] = bar and proxy.foo = bar
Inherited property assignment: Object.create(proxy)[foo] = bar
Reflect.set()
參數(shù)target是目標(biāo)對象,參數(shù)property是屬性名,參數(shù)value是屬性值,參數(shù)receiver是一個可選對象,有時我們必須要搜索幾個對象,可能是一個在receiver原型鏈上的對象。返回值如果設(shè)值成功,返回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
示例中,如果值非數(shù)字則直接拋出異常。利用set方法,可以實(shí)現(xiàn)數(shù)據(jù)綁定,當(dāng)值發(fā)生變化時,自動更新DOM。
因?yàn)間et和set方法比較常用,再舉個例子。例如私有屬性可以在屬性名請加上_下劃線,但這只是潛規(guī)則,外部仍舊能暢通無阻地讀寫這些屬性?,F(xiàn)在用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)可以攔截檢查是否含有該參數(shù)的in行為:
Property query: foo in proxy
Inherited property query: foo in Object.create(proxy)
with check: with(proxy) { (foo); }
Reflect.has()
參數(shù)target是目標(biāo)對象,參數(shù)prop是屬性名。返回值如果含有該屬性,返回true,否則返回false。參照MDN。例如用has方法隱藏帶下劃線前綴的屬性,不讓其被in運(yùn)算符發(fā)現(xiàn):
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
如果原對象不可擴(kuò)展,用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循環(huán)也用到了in運(yùn)算符,但是Chrome55,F(xiàn)irefox49,Opera39上試下來,for…in里并不觸發(fā)has攔截。
apply / construct
Proxy不止可以攔截對象的操作還能用這兩個方法攔截函數(shù)。
handler.apply(target, thisArg, argumentsList)可以攔截函數(shù)調(diào)用的行為,包括apply調(diào)用,call調(diào)用:
proxy(…args)
Function.prototype.apply() and Function.prototype.call()
Reflect.apply()
參數(shù)target是函數(shù)對象,參數(shù)thisArg是函數(shù)對象的this,參數(shù)argumentsList是函數(shù)參數(shù)。返回值可返回任意東西。參照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()
參數(shù)target是目標(biāo)對象,參數(shù)argumentsList是構(gòu)造函數(shù)參數(shù),參數(shù)newTarget。返回new后的對象,注意必須是對象,否則例如返回數(shù)字會報錯。參照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
同一個攔截器函數(shù),可以同時設(shè)置多個上面介紹的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自身還有個靜態(tài)方法Proxy.revocable(target, handler),用于創(chuàng)建并返回一個可取消的Proxy對象。返回的這個可取消的Proxy對象有兩個屬性:proxy和revoke
屬性proxy會調(diào)用new Proxy(target, handler)創(chuàng)建一個新的Proxy對象。屬性revoke是一個無參函數(shù),用于取消,即讓該P(yáng)roxy對象無效。例如:
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對象。調(diào)用該對象的proxy屬性得到真實(shí)的Proxy對象。如果不想用了,可以調(diào)用revoke()方法將該P(yáng)roxy對象無效化。之后對Proxy對象的任何操作都將拋出異常。