Proxy 意爲"代理器",是 ES6 新增,用於修改目標對象的成員以前進行"攔截",外界對該對象的成員的訪問,都必須先經過這層攔截。其中,成員包括未定義的,即訪問或設置不存在的屬性,也會觸發相應的代理函數。javascript
經過構造函數能夠建立 Proxy 對象(new Proxy()
),須要依次傳入兩個參數:java
let obj = { a: 1 }
let proxy = new Proxy(obj, {
get(obj, prop) {
return 'proxyReturn'
}
});
console.log(proxy.a); // 'proxyReturn'
console.log(proxy.b); // 'proxyReturn'複製代碼
例子中,proxy
對obj
對象攔截,訪問其屬性時,永遠返回的是'proxyReturn'
,即便屬性是不存在。而get
函數是衆多代理函數中的一種,下面會分別介紹其餘的代理函數。es6
有一種狀況,Proxy 對象做爲某個對象的原型。看下面例子:數組
let proxy = new Proxy({}, {
get(obj, prop) {
return 'proxyReturn'
}
});
let obj = { a: 1 }
Object.setPrototypeOf(obj, proxy);
console.log(obj.a); // 1
console.log(obj.b); // 'proxyReturn'複製代碼
當訪問obj.b
時,因爲b
在obj
對象內不存在,所以會到訪問原型是否存在該屬性,即訪問了proxy
對象,從而觸發了代理函數。瀏覽器
代理對象必須與被代理的對象具備相同特性(這裏說的特性指的是,對象是否凍結,是否密封,是否可擴展),即被代理的對象不能修改,使用代理對象的set
方法也會報錯。再例如,被代理的對象被凍結,get
方法不能返回與原值不一樣的值;getOwnPropertyDescriptor
必須返回描述符對象。總之一句,被代理對象不能改的,代理對象一樣不能改;代理對象的方法的返回值類型必須與被代理對象一致。bash
控制器對象內方法的this
指向控制器對象自己,看以下例子:
app
let handler = {
get() {
return this
}
}
let proxy = new Proxy({}, handler);
console.log(proxy.a === handler); // true複製代碼
上面提到,控制器對象能夠包含零個或多個的代理函數。像get
這樣的代理函數,一共有 13 個。簡單介紹下面出現的參數名意義:dom
get
方法用於攔截某個成員的讀取操做(包括方法),上面已經簡單給出了例子。下面給出代理數組的例子,使其能夠用負數索引。函數
let arr = [1, 2, 3, 4, 5];
let proxy = new Proxy(arr, {
get(target, prop) {
let length = target.length,
index = Number.parseInt(prop);
if (index < 0) {
return target[length + index]
}
if (index >= length) {
return target[length - 1]
}
return target[index]
}
});
console.log(proxy[-1]); // 5
console.log(proxy[10]); // 5
console.log(proxy[2]); // 3複製代碼
set
方法用於攔截對象屬性的設置。其中,value
表示屬性的目標值。比較實用的用法是,與 DOM 的數據綁定。ui
須要注意的是!當存在二級屬性時,如[{}, {}]
或{ a: {}, b: {} }
,對二級或以上的屬性修改並不會觸發set
方法。
以下例子:
let listData = ['one', 'two', 'three'];
let proxy = new Proxy(listData, {
set(target, prop, value) {
let dom = document.getElementsByClassName('list-item')[prop]
dom ? dom.innerText = value : null;
target[prop] = value;
}
})
proxy[0] = 'changed-one';複製代碼
has
方法用來攔截HasProperty
操做,即判斷對象是否具備某個屬性時,這個方法會生效。典型的操做就是in
運算符。
注意,hasOwnProperty()
和for...in
都不能觸發has
方法。
注意,has
方法只能返回布爾值,return
非布爾值會自動轉成布爾值。
let arr = [1, 2, 3, 4];
let proxyArr = new Proxy(arr, {
has(target, prop) {
return 'a'
}
})
console.log('x' in proxyArr); // true複製代碼
deleteProperty
方法用於攔截delete
操做,只能返回布爾值,非布爾值會被轉成布爾值,表明屬性是否被刪除成功,省略默認返回false
。
let obj = { a: 1, b: 2 }
let proxyDel = new Proxy(obj, {
deleteProperty(target, prop) {
delete target[prop]
}
});
console.log(delete proxyDel.a); // false
console.log(obj); // { b: 2 }複製代碼
getOwnPropertyDescriptor
方法攔截Object.getOwnPropertyDescriptor()
或Object.getOwnPropertyDescriptors()
,返回一個屬性描述對象或者undefined
。
注意,若返回值不是undefined
,則返回值必須是包含[[configurable]]
爲true
的對象,該值爲false
或省略會致使報錯;若其餘描述符未定義,以默認值填充;若返回對象包含除描述符之外的屬性,則該屬性被忽略。
// undefined
let proxy1 = new Proxy({}, {
getOwnPropertyDescriptor(target, prop) {
return undefined
}
});
console.log(Object.getOwnPropertyDescriptor(proxy1, 'a'));
// { value: undefined, writable: false, enumerable: false, configurable: true }
let proxy2 = new Proxy({}, {
getOwnPropertyDescriptor(target, prop) {
return { configurable: true }
}
});
console.log(Object.getOwnPropertyDescriptor(proxy2, 'a'));
// 報錯
let proxy3 = new Proxy({}, {
getOwnPropertyDescriptor(target, prop) {
return { configurable: false }
}
});
console.log(Object.getOwnPropertyDescriptor(proxy3, 'a'));複製代碼
defineProperty
方法攔截Object.defineProperty()
、Object.defineProperties()
、obj.prop = 'value'
等修改/添加對象的屬性的操做。該方法只能返回布爾值,非布爾值會轉成布爾值,表示是否成功定義屬性,返回fasle
會致使報錯。其中,propDesc
表示屬性的描述符。
注意,若是對象被凍結(Object.freeze()
)或對象被密封(Object.seal()
)或對象不可擴展( Object.preventExtensions()
),甚至是某個屬性被密封([[configurable]]
特性爲false
),對代理對象使用Object.defineProperty()
均可能會報錯。總之,和原對象的特性一致,不能被修改的被代理後一樣不能被修改。
let obj = { d: 4 }
let proxyObj = new Proxy(obj, {
defineProperty() {
console.log('proxy-defineProperty');
return true
}
})
// 如下4種方法都會觸發 defineProperty 方法
Object.defineProperty(proxyObj, 'a', {});
Object.defineProperties(proxyObj, { b: {} });
proxyObj.c = 3;
proxyObj.d = 5;複製代碼
apply
方法攔截函數的調用、call
、apply
、bind
操做。其中,object
表示函數上下文對象,args
表示函數的參數。
let obj = { a: 10 }
function fn() { }
let proxyFn = new Proxy(fn, {
apply(target, object, args) {
console.log('proxyFn-apply');
}
})
// 直接執行函數 會觸發
proxyFn();
// apply 和 call 會觸發
proxyFn.apply(obj);
proxyFn.call(obj);
// bind 定義時不會觸發 執行時觸發
let p = proxyFn.bind(obj);
p();複製代碼
construct
方法用於攔截new
命令。
注意,被代理的函數不能是箭頭函數,不然也會報錯。
注意,construct
方法必需要return
對象(包括函數、數組等),不然會報錯。
// 錯誤例子1
let P1 = new Proxy(()=>{},{
construct(){
return {}
}
});
let p1 = new P1(); // 報錯,被代理函數不能是箭頭函數
// 錯誤例子2
let P2 = new Proxy(function(){},{
construct(){}
});
let p2 = new P2(); // 報錯,construct必須返回對象複製代碼
getPrototypeOf
方法主要用來攔截獲取對象原型,返回值必須是null
或對象。具體來講,攔截下面這些操做。
Object.prototype.__proto__
// 只能在瀏覽器環境下執行Object.prototype.isPrototypeOf()
// 代理對象做爲參數Object.getPrototypeOf()
// 代理對象做爲參數Reflect.getPrototypeOf()
// 代理對象做爲參數instanceof
// 代理對象在運算符的左邊下面給出簡單例子:
let proxy = new Proxy({},{
getPrototypeOf(){
console.log('proxy-getPrototypeOf');
return null
}
});
// 都會打印 'proxy-getPrototypeOf'
let x1 = proxy.__proto__; // 只能在瀏覽器下執行
let x2 = {}.isPrototypeOf(proxy);
let x3 = Object.getPrototypeOf(proxy);
let x4 = Reflect.getPrototypeOf(proxy);
let x5 = (proxy instanceof function(){});複製代碼
setPrototypeOf
方法主要用來攔截Object.setPrototypeOf()
方法。和Object.setPrototypeOf()
方法同樣,必須返回被設置的對象,不然報錯。其中,proto
表示被設爲原型的對象。
注意,瀏覽器環境下能夠經過__proto__
設置原型對象,一樣能觸發setPrototypeOf
方法。
let proxy = new Proxy({}, {
setPrototypeOf(target, proto) {
console.log('proxy-setPrototypeOf')
return target;
}
});
// 都打印 'proxy-setPrototypeOf'
Object.setPrototypeOf(proxy, {});
proxy.__proto__ = {} // 非瀏覽器環境下使用會報錯複製代碼
isExtensible
方法攔截Object.isExtensible()
方法,必須返回布爾值,非布爾值會被轉成布爾值,且返回值必須與源對象所對應的值相同,不然報錯。
// 錯誤示範
let proxy = new Proxy({}, {
isExtensible() {
return false
}
});
console.log(Object.isExtensible(proxy));複製代碼
preventExtensions
方法攔截Object.preventExtensions()
方法,只能返回被設爲不可擴展的被代理對象,不然報錯。以下:
let proxy = new Proxy({}, {
preventExtensions(target) {
console.log('proxy-preventExtensions')
return Object.preventExtensions(target);
}
});
Object.preventExtensions(proxy); // proxy-preventExtensions
// 錯誤示範
let proxyErr = new Proxy({}, {
preventExtensions(target) {
return {}
}
});
Object.preventExtensions(proxyErr); // 報錯複製代碼
ownKeys
方法用來攔截對象自身全部屬性的讀取操做,必須返回數組或對象,不然報錯。具體來講,攔截如下操做。
Object.getOwnPropertyNames()
。Object.getOwnPropertySymbols()
。getOwnPropertyDescriptors()
。Object.entries()
、Object.keys()
、Object.values()
。for...in
循環。注意,雖然能夠返回非數組的對象,對於如Object.keys
這種默認返回數組的方法,獲得的結果會是一個空數組。
注意,諸如Object.entries
方法返回的數組不會含有Symbol
屬性名、不存在的屬性名、不可遍歷(enumerable
)的屬性名,與被代理對象的行爲一致。Object.getOwnPropertySymbols
及其餘方法同理,與被代理對象一致。
看下面簡單例子:
let proxy = new Proxy({ a: 1, b: 2 }, {
ownKeys() {
console.log('proxy-ownKeys');
return ['a', 'c']
}
});
// 都會打印 'proxy-ownKeys'
let x1 = Object.entries(proxy);
let x2 = Object.keys(proxy);
let x3 = Object.values(proxy);
for (let x4 in proxy) { }
let x5 = Object.getOwnPropertyNames(proxy);
let x6 = Object.getOwnPropertyDescriptors(proxy);
let x7 = Object.getOwnPropertySymbols(proxy);
// entries、keys、values 都會過濾不可能顯示的屬性,跟被代理對象一致
console.log(x1); // [ [ 'a', 1 ] ]
複製代碼
Proxy.revocable()
方法與new Proxy()
用法相似,都是依次傳入被代理對象和控制器對象,返回一個包含proxy
對象和revoke
方法的對象。其中,proxy
對象和上面描述的一致,revoke
方法用於解除proxy
對象的代理,一旦取消將沒法繼續使用。
let proxyR = Proxy.revocable({ a: 1 }, {});
console.dir(proxyR); // { proxy: { a: 1 }, revoke: [Function] }
console.log(proxyR.proxy); // { a: 1 }
proxyR.revoke();
// console.log(proxyR); // 報錯複製代碼
Reflect
對象也是 ES6 新增的,意爲"映射",表示把其餘內置對象中經常使用的方法映射到Reflect
對象上。因此只有靜態方法。Reflect
對象的設計目的有這樣幾個:
Object
對象的一些明顯屬於語言內部的方法放到Reflect
對象上。現階段,某些方法同時在Object
和Reflect
對象上部署,將來的新方法將只部署在Reflect
對象上。也就是說,從Reflect
對象上能夠拿到語言內部的方法。Object
方法的返回結果,讓其變得更合理。好比,Object.defineProperty(obj, name, desc)
在沒法定義屬性時,會拋出一個錯誤,而Reflect.defineProperty(obj, name, desc)
則會返回false
。Object
操做都變成函數行爲。某些Object
操做是命令式,好比name in obj
和delete obj[name]
,而Reflect.has(obj, name)
和Reflect.deleteProperty(obj, name)
讓它們變成了函數行爲。Reflect
對象的方法與Proxy
對象的方法一一對應,只要是Proxy
對象的方法,就能在Reflect
對象上找到對應的方法。這就讓Proxy
對象能夠方便地調用對應的Reflect
方法,完成默認行爲,做爲修改行爲的基礎。也就是說,無論Proxy
怎麼修改默認行爲,你總能夠在Reflect
上獲取默認行爲。以上摘自阮一峯的《ES6入門》。
Reflect
的靜態方法與Proxy
的控制器方法一一對應,甚至連傳參都是一致的,目前一樣是 13 個,下面簡單列出方法及其對應的"舊"方法。
obj.a
,不存在則返回undefined
。這裏須要注意第三個參數receiver
,若獲取的屬性是getter
,那麼getter
的this
會指向receiver
對象。obj.a = 'value'
。receiver
對象一樣影響setter
的this
指向。in
運算符,如'a' in obj
。delete
運算符,如delete obj.a
。方法返回布爾值,若刪除成功或被刪除的屬性不存在,返回true
;刪除失敗,即被刪除的屬性依然存在([[configurable]] = false
),返回false
。new
運算符,如new Fn(...args)
。Object.getPrototypeOf()
。二者差異在於當參數爲非對象時,Object.getPrototypeOf()
會被"包裝"成對象後傳入;而Reflect.getPrototypeOf()
會報錯。Object.setPrototypeOf()
。方法返回布爾值,表示是否設置成功。Function.prototype.apply.call()
。Object.defineProperty()
。方法返回布爾值,表示是否認義成功。Object.getOwnPropertyDescriptor()
。Object.isExtensible()
。Object.preventExtensions()
。Object.getOwnPropertyNames()
與Object.getOwnPropertySymbols()
。