ECMAScript 6 入門筆記(八)Proxy,Reflect
Proxy
proxy用於修改某些操作的預設行為,等同於在語言層面作出修改,屬於”超程式設計”。可以理解成架設一層“攔截”,外界對該物件訪問都必須通過這層攔截。
var obj = new Proxy({},{
get: function(){target, key, receiver}{
console.log(`getting ${key}`);
return Reflect.get(target, key, receiver);
},
set: function(target, key, value, receiver){
console.log(`setting ${key}!`);
return Reflect.set(target, key, value, receiver);
}
});
obj.count = 1
// setting count
++obj.count;
//getting count
//setting count
//2
var proxt = new Proxy({},{
get: function(target,property){
return 35;
}
});
proxy.time //35
proxy.name //35
Proxy物件
Proxy例項也可以作為其他物件的原型物件
var proxy = new Proxy({},{
get: function(target,property){
return 35;
}
});
let obj = Object.create(proxy);
obj.time //35
同一個攔截器函式,可以設定攔截多個操作
var handler = {
get: function(target,name){
if(name === ‘prototype’){
return Object.prototype;
}
return ‘Hello, ‘+name;
},
apply: function(target, thisBinding, args){
reutrn args[0];
},
construct: function(target,args){
return {value: args[1]};
}
};
var fproxy = new Proxy(function(x,y){
return x + y;
},handler);
fproxy(1,2); //1
new fproxy(1,2); // {value:2}
fproxy.prototype === Object.prototype //true
fproxy.foo
Proxy支援的操作
get()
var person = {
name : ‘張三’
};
var proxy = new Proxy(person,{
get: function(target, property){
if(prototype in target){
return target[property];
}else{
throw new ReferenceError(“Property \”” + property + “\” does not exist.”);
}
}
});
proxy.name //張三
proxy.age //丟擲錯誤
如果一個屬性不可配置和不可寫,則不能通過Proxy物件訪問該屬性會報錯
const target = Object.defineProperties({},{
foo:{
value : 123,
writable: false,
configurable:false
}
});
const handler = {
get(target, propKey){
return ‘abc’;
}
};
const proxy = new Proxy(target, handler);
proxy.foo //typeError
set()
set方法用來攔截某個屬性的賦值操作
假定Person物件有一個age屬性,該屬性應該是一個不大於200的整數,那麼可以使用Proxy保證age的屬性值符合要求
假定Person物件有一個age屬性,不大於200的整數
let validator = {
set: function(obj, prop, value){
if(prop === ‘age’){
if(!Number.isInteger(value)){
throw new TypeError(‘The age is not an interger’);
}
if(value>200){
throw new TypeError(‘The age seems invalid’);
}
}
obj[prop] = value;
}
};
let person = new Proxy({},validator);
person.age = 1000;
person.age // 1000
person.age = ‘123’ //報錯
apply()
apply方法攔截函式的呼叫,call和apply操作,可以接收三個引數,分別是目標物件,目標物件的上下文物件this和目標物件的引數陣列
var handler = {
apply(target, ctx, args){
return Reflect.apply(…arguments);
}
};
var target = function(){ return ‘I am the target’;};
var handler = {
apply: function(){
return ‘I am the proxy’;
}
};
var p = new Proxy(target,handler);
p() //I am the proxy
var twice = {
apply (target, ctx, args){
return Reflext.apply(…arguments) *2;
}
};
function sum(left, right){
return left + right;
}
var proxy = new Proxy(sum, twice);
proxy(1,2);
proxy.call(null,5,6) //22
proxy.apply(null,[7,8]) //30
has()
has方法用來攔截HasProperty操作
下面的例子使用has方法隱藏某些屬性,不被iin元素運算子發現
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);
‘_prop’ in proxy; //false
construct()
用於攔截new()命令,下面是攔截物件的寫法
var p = new Proxy(function(){},{
construct: function(target, args){
console.log(‘called:’ + args.join(‘, ‘));
return {value: args[0] * 10};
}
});
(new p(1)).value;
deleteProperty()
deleteProperty方法用於攔截delete操作,如果這個方法丟擲錯誤或者返回false,當前屬性就無法被delete命令刪除
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;
getOwnPropertyDescriptor()
攔截Object.getOwnPropertyDescriptor()
getPrototypeOf()
用來攔截獲取物件原型
var proto = {};
var p = new Proxy({},{
getPrototypeOf(target){
return proto;
}
});
Object.getPrototypeOf(p) === proto; //true
isExtensible()
攔截Object.isExtensible操作
var p = new Proxy({},{
isExtensible: function(target){
console.log(“called”);
return true;
}
});
Object.isExtensible(p);
//”called”
//true
ownKeys()
攔截物件自身屬性的讀取操作
let target = {
a:1,
b:2,
c:3
};
let handler = {
ownKeys(target){
return [‘a’];
}
};
let proxy = new Proxy(target,handler);
Object.keys(proxy); //[‘a’]
preventExtensions()
攔截Object.preventExtensions().這個方法有一個限制,只有目標物件不可擴充套件時(即Object.isExtensible(proxy)為false),proxy.preventExtensions才能返回true,否則會報錯。
setPrototypeOf()
setPrototypeOf方法主要用來攔截Object.setPrototypeOf方法。
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
Proxy.revocable()
Proxy.revocable方法返回一個可取消的 Proxy 例項。
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例項,就會丟擲一個錯誤。
Proxy.revocable的一個使用場景是,目標物件不允許直接訪問,必須通過代理訪問,一旦訪問結束,就收回代理權,不允許再次訪問。
this 問題
雖然 Proxy 可以代理針對目標物件的訪問,但它不是目標物件的透明代理,即不做任何攔截的情況下,也無法保證與目標物件的行為一致。主要原因就是在 Proxy 代理的情況下,目標物件內部的this關鍵字會指向 Proxy 代理。
const target = {
m: function () {
console.log(this === proxy);
}
};
const handler = {};
const proxy = new Proxy(target, handler);
target.m() // false
proxy.m() // true
上面程式碼中,一旦proxy代理target.m,後者內部的this就是指向proxy,而不是target。
Reflect
Reflect物件與Proxy物件一樣,也是 ES6 為了操作物件而提供的新 API。Reflect物件的設計目的有這樣幾個。
(1) 將Object物件的一些明顯屬於語言內部的方法(比如Object.defineProperty),放到Reflect物件上。現階段,某些方法同時在Object和Reflect物件上部署,未來的新方法將只部署在Reflect物件上。也就是說,從Reflect物件上可以拿到語言內部的方法。
(2) 修改某些Object方法的返回結果,讓其變得更合理。比如,Object.defineProperty(obj, name, desc)在無法定義屬性時,會丟擲一個錯誤,而Reflect.defineProperty(obj, name, desc)則會返回false。
// 老寫法
try {
Object.defineProperty(target, property, attributes);
// success
} catch (e) {
// failure
}
// 新寫法
if (Reflect.defineProperty(target, property, attributes)) {
// success
} else {
// failure
}
(3) 讓Object操作都變成函式行為。某些Object操作是命令式,比如name in obj和delete obj[name],而Reflect.has(obj, name)和Reflect.deleteProperty(obj, name)讓它們變成了函式行為。
// 老寫法
‘assign’ in Object // true
// 新寫法
Reflect.has(Object, ‘assign’) // true
(4)Reflect物件的方法與Proxy物件的方法一一對應,只要是Proxy物件的方法,就能在Reflect物件上找到對應的方法。這就讓Proxy物件可以方便地呼叫對應的Reflect方法,完成預設行為,作為修改行為的基礎。也就是說,不管Proxy怎麼修改預設行為,你總可以在Reflect上獲取預設行為。
Proxy(target, {
set: function(target, name, value, receiver) {
var success = Reflect.set(target,name, value, receiver);
if (success) {
log(‘property ’ + name + ’ on ’ + target + ’ set to ’ + value);
}
return success;
}
});
上面程式碼中,Proxy方法攔截target物件的屬性賦值行為。它採用Reflect.set方法將值賦值給物件的屬性,確保完成原有的行為,然後再部署額外的功能。
下面是另一個例子。
var loggedObj = new Proxy(obj, {
get(target, name) {
console.log(‘get’, target, name);
return Reflect.get(target, name);
},
deleteProperty(target, name) {
console.log(‘delete’ + name);
return Reflect.deleteProperty(target, name);
},
has(target, name) {
console.log(‘has’ + name);
return Reflect.has(target, name);
}
});
上面程式碼中,每一個Proxy物件的攔截操作(get、delete、has),內部都呼叫對應的Reflect方法,保證原生行為能夠正常執行。新增的工作,就是將每一個操作輸出一行日誌。
有了Reflect物件以後,很多操作會更易讀。
// 老寫法
Function.prototype.apply.call(Math.floor, undefined, [1.75]) // 1
// 新寫法
Reflect.apply(Math.floor, undefined, [1.75]) // 1
Reflect物件一共有13個靜態方法。
Reflect.apply(target,thisArg,args)
Reflect.construct(target,args)
Reflect.get(target,name,receiver)
Reflect.set(target,name,value,receiver)
Reflect.defineProperty(target,name,desc)
Reflect.deleteProperty(target,name)
Reflect.has(target,name)
Reflect.ownKeys(target)
Reflect.isExtensible(target)
Reflect.preventExtensions(target)
Reflect.getOwnPropertyDescriptor(target, name)
Reflect.getPrototypeOf(target)
Reflect.setPrototypeOf(target, prototype)