ES6學習筆記10 Proxy
Proxy簡介
Proxy用於修改某些操作的預設行為,等同於在語言層面做出修改,所以屬於一種“超程式設計”(meta programming),即對程式語言進行程式設計。
Proxy 可以理解成,在目標物件之前架設一層“攔截”,外界對該物件的訪問,都必須先通過這層攔截,因此提供了一種機制,可以對外界的訪問進行過濾和改寫。
ES6 原生提供 Proxy
建構函式,用來生成 例項
var proxy = new Proxy(target, handler);
//其中 target表示所要攔截的目標物件,handler引數也是一個物件,用來定製攔截行為
例項
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
上述程式碼對一個空物件假設了一層攔截,重新定義了屬性的get
和set
操作,返回一個Proxy
,其中引數target
為攔截物件,key
為接受到的鍵名,value
為接受到的鍵值,receiver
為一個可選的物件。因此,當對返回的Proxy
讀取和修改屬性值時,就會呼叫相應的自定義方法,在控制檯輸出字串。
同一個hander
物件內可以設定多個攔截操作,對於沒有設定的操作,遵從原先的方式產生結果。注意,要使得Proxy起作用,必須針對Proxy例項進行操作,而不是針對目標物件進行操作,否則定義的攔截操作不會起作用。
Proxy支援的攔截操作包括
(1)get(target, propKey, receiver)
:攔截物件屬性的讀取,比如proxy.foo
和proxy['foo']
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 // 丟擲一個錯誤
上面程式碼表示,如果訪問目標物件不存在的屬性,會丟擲一個錯誤。如果沒有這個攔截函式,訪問不存在的屬性,只會返回undefined
。
get
方法可以繼承
let proto = new Proxy({}, {
get(target, propertyKey, receiver) {
console.log('GET ' + propertyKey);
return target[propertyKey];
}
});
let obj = Object.create(proto);
obj.foo // "GET foo"
第三個引數receiver
表示原始操作所在的物件
const proxy = new Proxy({}, {
get: function(target, property, receiver) {
return receiver;
}
});
proxy.getReceiver === proxy // true 原始讀操作就在proxy物件
const d = Object.create(proxy);
d.a === d // true 原始讀操作在d物件
如果一個屬性不可配置(configurable)且不可寫(writable),則 Proxy 不能修改該屬性,否則通過 Proxy 物件訪問該屬性會報錯
(2) set(target, propKey, value, receiver)
:攔截物件屬性的設定,比如proxy.foo = v
或proxy['foo'] = v
,返回一個布林值。
結合get
和set
方法,就可以做到防止某些以下劃線(_
)開頭,表示內部的屬性被外部讀寫。
const handler = {
get (target, key) {
invariant(key, 'get');
return target[key];
},
set (target, key, value) {
invariant(key, 'set');
target[key] = value;
return true;
}
};
function invariant (key, action) {
if (key[0] === '_') {
throw new Error(`Invalid attempt to ${action} private "${key}" property`);
}
}
const target = {};
const proxy = new Proxy(target, handler);
proxy._prop
// Error: Invalid attempt to get private "_prop" property
proxy._prop = 'c'
// Error: Invalid attempt to set private "_prop" property
上述程式碼,只要讀寫屬性的第一個字元是下劃線,則一律報錯,達到禁止讀寫內部屬性的目的
同樣與get
操作類似,set
操作的receiver
引數代表原始操作行為所在的那個物件
const handler = {
set: function(obj, prop, value, receiver) {
obj[prop] = receiver;
}
};
const proxy = new Proxy({}, handler);
proxy.foo = 'bar';
proxy.foo === proxy // true
const myObj = {};
Object.setPrototypeOf(myObj, proxy);
myObj.foo = 'bar';
myObj.foo === myObj // true
注意,如果目標物件自身的某個屬性,不可寫且不可配置,那麼set
方法將不起作用。
(3)apply(target, object, args)
:攔截 Proxy 例項作為函式呼叫的操作,比如proxy(...args)
、proxy.call(object, ...args)
、proxy.apply(...)
。apply方法可以接受三個引數,分別是目標物件、目標物件的上下文物件(this)和目標物件的引數陣列。
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"
上面程式碼中,變數p
是 Proxy 的例項,當它作為函式呼叫時(p()
),就會被apply
方法攔截,返回一個字串。
另外,直接呼叫Reflect.apply
方法,也會被攔截。
Reflect.apply(p, null, []) // 'I am the proxy'
(4)has(target, propKey)
:攔截propKey in proxy
的操作,返回一個布林值。判斷物件是否具有某個屬性時,這個方法會生效。典型的操作就是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);
'_prop' in proxy // false
上面程式碼中,如果原物件的屬性名的第一個字元是下劃線,proxy.has
就會返回false
,從而不會被in
運算子發現。
如果原物件不可配置或者禁止擴充套件,這時has
攔截會報錯。has方法攔截的是HasProperty
操作,而不是HasOwnProperty
操作,即has
方法不判斷一個屬性是物件自身的屬性,還是繼承的屬性。雖然for…in迴圈也用到了in運算子,但是has攔截對for…in迴圈不生效。
(5)construct(target,args)
:攔截Proxy
例項作為建構函式呼叫的操作,比如new proxy(...args)
。返回的必須是一個物件,否則會報錯
construct方法可以接受兩個引數:
- target:目標物件
- args:建構函式的引數物件
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
(6)deleteProperty(target, propKey)
:攔截delete proxy[propKey]
的操作,返回一個布林值。如果這個方法丟擲錯誤或者返回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
// Error: Invalid attempt to delete private "_prop" property
目標物件自身的不可配置(configurable)的屬性,不能被deleteProperty方法刪除,否則報錯。
(7)defineProperty(target,propKey,propDesc)
:攔截Object.defineProperty()
、Object.defineProperties()
,返回一個布林值。如果返回false
,則導致新新增的屬性總是無效
var handler = {
defineProperty (target, key, descriptor) {
return false;
}
};
var target = {};
var proxy = new Proxy(target, handler);
proxy.foo = 'bar' // 不會生效
注意,如果目標物件不可擴充套件(extensible),則defineProperty
不能增加目標物件上不存在的屬性,否則會報錯。另外,如果目標物件的某個屬性不可寫(writable)或不可配置(configurable),則defineProperty
方法不得改變這兩個設定。
(8)getOwnPropertyDescriptor(target, propKey)
:攔截Object.getOwnPropertyDescriptor(proxy, propKey)
,返回屬性的描述物件或undefined
。
(9)getPrototypeOf(target)
:攔截Object.getPrototypeOf(proxy)
,返回一個物件。返回值必須是物件或null,另外如果物件不可擴充套件,則必須返回目標物件的原型物件
(10)isExtensible(target)
:攔截Object.isExtensible(proxy)
,返回一個布林值。該方法有個限制,其返回值必須跟物件的isExtensible
屬性保持一致,也就是如果物件可以擴充套件,你必須返回true
(11)preventExtensions(target)
:攔截Object.preventExtensions(proxy)
,返回一個布林值。該方法有個限制,只有當目標物件不可擴充套件時,才能返回true,否則會報錯。
var p = new Proxy({}, {
preventExtensions: function(target) {
return true;
}
});
Object.preventExtensions(p) // 報錯
為了防止出現這個問題,通常要在proxy.preventExtensions
方法裡面,呼叫一次Object.preventExtensions
。
(12)ownKeys(target)
:攔截Object.getOwnPropertyNames(proxy)
、Object.getOwnPropertySymbols(proxy)
、Object.keys(proxy)
、for...in
迴圈,返回一個數組。該方法返回目標物件所有自身的屬性的屬性名,而Object.keys()
的返回結果僅包括目標物件自身的可遍歷屬性。
下面的例子是攔截第一個字元為下劃線的屬性名不被遍歷。
let target = {
_bar: 'foo',
_prop: 'bar',
prop: 'baz'
};
let handler = {
ownKeys (target) {
return Reflect.ownKeys(target).filter(key => key[0] !== '_');
}
};
let proxy = new Proxy(target, handler);
for (let key of Object.keys(proxy)) {
console.log(target[key]);
}
// "baz"
ownKeys
方法返回的陣列成員,只能是字串或 Symbol 值。如果有其他型別的值,或者返回的根本不是陣列,就會報錯。
如果目標物件自身包含不可配置的屬性,則該屬性必須被ownKeys方法返回,否則報錯。如果目標物件是不可擴充套件的(non-extensition),這時ownKeys方法返回的陣列之中,必須包含原物件的所有屬性,且不能包含多餘的屬性,否則報錯。
(13)setPrototypeOf(target, proto)
:攔截Object.setPrototypeOf(proxy, proto)
,返回一個布林值。
如果目標物件不可擴充套件(extensible),setPrototypeOf
方法不得改變目標物件的原型。
Proxy.revocable()
該方法返回一個可取消的Proxy例項和一個取消函式,呼叫該函式,可以取消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
的一個使用場景是,目標物件不允許直接訪問,必須通過代理訪問,一旦訪問結束,就收回代理權,不允許再次訪問。
this問題
在 Proxy 代理的情況下,目標物件內部的this
關鍵字會指向 Proxy 代理。將this
繫結原始物件可以解決這個問題
const target = new Date();
const handler = {};
const proxy = new Proxy(target, handler);
proxy.getDate(); //有些原生物件的內部屬性,只有通過正確的this才能拿到
// TypeError: this is not a Date object.
const target = new Date('2015-01-01');
const handler = {
get(target, prop) {
if (prop === 'getDate') {
return target.getDate.bind(target); //繫結this至目標物件
}
return Reflect.get(target, prop);
}
};
const proxy = new Proxy(target, handler);
proxy.getDate() // 1