前面的話
??
ES5
和ES6
致力于為開發者提供JS
已有卻不可調用的功能。例如在ES5
出現以前,JS
環境中的對象包含許多不可枚舉和不可寫的屬性,但開發者不能定義自己的不可枚舉或不可寫屬性,于是ES5
引入了Object.defineProperty()
方法來支持開發者去做JS
引擎早就可以實現的事情。ES6
添加了一些內建對象,賦予開發者更多訪問JS
引擎的能力。代理(Proxy)
是一種可以攔截并改變底層JS
引擎操作的包裝器,在新語言中通過它暴露內部運作的對象,從而讓開發者可以創建內建的對象。本文將詳細介紹代理(Proxy)
和反射(Reflection)
引入
【數組問題】
在
ES6
之前,開發者不能通過自己定義的對象模仿JS
數組對象的行為方式。當給數組的特定元素賦值時,影響到該數組的length
屬性,也可以通過length
屬性修改數組元素
let colors = ["red", "green", "blue"];
console.log(colors.length); // 3
colors[3] = "black";
console.log(colors.length); // 4
console.log(colors[3]); // "black"
colors.length = 2;
console.log(colors.length); // 2
console.log(colors[3]); // undefined
console.log(colors[2]); // undefined
console.log(colors[1]); // "green"
-
colors
數組一開始有3個元素,將colors[3]
賦值為"black"
時,length
屬性會自動增加到4,將length
屬性設置為2時,會移除數組的后兩個元素而只保留前兩個。在ES5
之前開發者無法自己實現這些行為,現在通過代理可以實現
代理和反射
調用
new Proxy()
可創建代替其他目標(target)
對象的代理,它虛擬化了目標,所以二者看起來功能一致
代理可以攔截
JS
引擎內部目標的底層對象操作,這些底層操作被攔截后會觸發響應特定操作的陷阱函數
反射
API
以Reflect
對象的形式出現,對象中方法的默認特性與相同的底層操作一致,而代理可以覆寫這些操作,每個代理陷阱對應一個命名和參數都相同的Reflect
方法。下表總結了代理陷阱的特性
每個陷阱覆寫
JS
對象的一些內建特性,可以用它們攔截并修改這些特性。如果仍需使用內建特性,則可以使用相應的反射API
方法
【創建簡單代理】
用
Proxy
構造函數創建代理需要傳入兩個參數:目標(target)
和處理程序(handler)
。處理程序用于定義一個或多個陷阱的對象,在代理中,除了專門為操作定義的陷阱外,其余操作均使用默認特性。不使用任何陷阱的處理程序等價于簡單的轉發代理
let target = {};
let proxy = new Proxy(target, {});
proxy.name = "proxy";
console.log(proxy.name); // "proxy"
console.log(target.name); // "proxy"
target.name = "target";
console.log(proxy.name); // "target"
console.log(target.name); // "target"
- 這個示例中的代理將所有操作直接轉發到目標,將
"proxy"
賦值給proxy.name
屬性時會在目標上創建name
,代理只是簡單地將操作轉發給目標,它不會儲存這個屬性。由于proxy.name
和target.name
引用的都是target.name
,因此二者的值相同,從而為target.name
設置新值后,proxy.name
也一同變化
陷阱代理
【使用set陷阱驗證屬性】
假設創建一個屬性值是數字的對象,對象中每新增一個屬性都要加以驗證,如果不是數字必須拋出錯誤。為了實現這個任務,可以定義一個
set
陷阱來覆寫設置值的默認特性
set
陷阱接受4個參數
trapTaqget 用于接收屬性(代理的目標)的對象
key 要寫入的屬性鍵(字符串或Symbol類型)
value 被寫入屬性的值
receiver 操作發生的對象(通常是代理)
Reflect.set()
是set
陷阱對應的反射方法和默認特性,它和set
代理陷阱一樣也接受相同的4個參數,以方便在陷阱中使用。如果屬性已設置陷阱應該返回true
,如果未設置則返回false
。(Reflect.set()
方法基于操作是否成功來返回恰當的值)可以使用
set
陷阱并檢查傳入的值來驗證屬性值
let target = {
name: "target"
};
let proxy = new Proxy(target, {
set(trapTarget, key, value, receiver) {
// 忽略已有屬性,避免影響它們
if (!trapTarget.hasOwnProperty(key)) {
if (isNaN(value)) {
throw new TypeError("Property must be a number.");
}
}
// 添加屬性
return Reflect.set(trapTarget, key, value, receiver);
}
});
// 添加一個新屬性
proxy.count = 1;
console.log(proxy.count); // 1
console.log(target.count); // 1
// 你可以為 name 賦一個非數值類型的值,因為該屬性已經存在
proxy.name = "proxy";
console.log(proxy.name); // "proxy"
console.log(target.name); // "proxy"
// 拋出錯誤
proxy.anotherName = "proxy";
這段代碼定義了一個代理來驗證添加到
target
的新屬性,當執行proxy.count=1
時,set
陷阱被調用,此時trapTarget
的值等于target
,key
等于"count"
,value
等于1,receiver
等于proxy
由于
target
上沒有count
屬性,因此代理繼續將value
值傳入isNaN()
,如果結果是NaN
,則證明傳入的屬性值不是數字,同時也拋出一個錯誤。在這段代碼中,count
被設置為1,所以代理調用Reflect.set()
方法并傳入陷阱接受的4個參數來添加新屬性proxy.name
可以成功被賦值為一個字符串,這是因為target
已經擁有一個name
屬性,但通過調用trapTarget.hasownproperty()
方法驗證檢查后被排除了,所以目標已有的非數字屬性仍然可以被操作。然而,將
proxy.anotherName
賦值為一個字符串時會拋出錯誤。目標上沒有anotherName
屬性,所以它的值需要被驗證,而由于"Proxy"
不是一個數字值,因此拋出錯誤set
代理陷阱可以攔截寫入屬性的操作,get
代理陷阱可以攔截讀取屬性的操作
【用get陷阱驗證對象結構(Object Shape)】
JS
有一個時常令人感到困惑的特殊行為,即讀取不存在的屬性時不會拋出錯誤,而是用undefined
代替被讀取屬性的值
let target = {};
console.log(target.name); // undefined
在大多數其他語言中,如果
target
沒有name
屬性,嘗試讀取target.name
會拋出一個錯誤。但JS
卻用undefined
來代替target.name
屬性的值。這個特性會導致重大問題,特別是當錯誤輸入屬性名稱的時候,而代理可以通過檢查對象結構來回避這個問題對象結構是指對象中所有可用屬性和方法的集合,
JS
引擎通過對象結構來優化代碼,通常會創建類來表示對象,如果可以安全地假定一個對象將始終具有相同的屬性和方法,那么當程序試圖訪問不存在的屬性時會拋出錯誤。代理讓對象結構檢驗變得簡單因為只有當讀取屬性時才會檢驗屬性,所以無論對象中是否存在某個屬性,都可以通過
get
陷阱來檢測,它接受3個參數
trapTarget 被讀取屬性的源對象(代理的目標)
key 要讀取的屬性鍵(字符串或Symbol)
receiver 操作發生的對象(通常是代理)
由于
get
陷阱不寫入值,所以它復刻了set
陷阱中除value
外的其他3個參數,Reflect.get()
也接受同樣3個參數并返回屬性的默認值如果屬性在目標上不存在,則使用
get
陷阱和Reflect.get()
時會拋出錯誤
let proxy = new Proxy({}, {
get(trapTarget, key, receiver) {
if (!(key in receiver)) {
throw new TypeError("Property " + key + " doesn't exist.");
}
return Reflect.get(trapTarget, key, receiver);
}
});
// 添加屬性的功能正常
proxy.name = "proxy";
console.log(proxy.name); // "proxy"http:// 讀取不存在屬性會拋出錯誤
console.log(proxy.nme); // 拋出錯誤
此示例中的
get
陷阱可以攔截屬性讀取操作,并通過in
操作符來判斷receiver
上是否具有被讀取的屬性,這里之所以用in操作符檢查receiver
而不檢查trapTarget
,是為了防止receiver
代理含有has
陷阱。在這種情況下檢查trapTarget
可能會忽略掉has
陷阱,從而得到錯誤結果。屬性如果不存在會拋出一個錯誤,否則就使用默認行為這段代碼展示了如何在沒有錯誤的情況下給
proxy
添加新屬性name
,并寫入值和讀取值。最后一行包含一個輸入錯誤:proxy.nme
有可能是proxy.namer
,由于nme
是一個不存在的屬性,因而拋出錯誤
【使用has陷阱隱藏已有屬性】
可用
in
操作符來檢測給定對象是否含有某個屬性,如果自有屬性或原型屬性匹配這個名稱或Symbol
返回true
let target = {
value: 42;
}
console.log("value" in target); // true
console.log("toString" in target); // true
value
是一個自有屬性,tostring
是一個繼承自Object
的原型屬性,二者在對象上都存在,所以用in
操作符檢測二者都返回true
。在代理中使用has
陷阱可以攔截這些in操作并返回一個不同的值每當使用
in
操作符時都會調用has
陷阱,并傳入兩個參數
trapTaqget讀取屬性的對象(代理的目標)
key要檢查的屬性鍵(字符串或Symbol)
-
Reflect.has()
方法也接受這些參數并返回in操作符的默認響應,同時使用has
陷阱和Reflect.has()
可以改變一部分屬性被in檢測時的行為,并恢復另外一些屬性的默認行為。例如,可以像這樣隱藏之前示例中的value
屬性
let target = {
name: "target",
value: 42
};
let proxy = new Proxy(target, {
has(trapTarget, key) {
if (key === "value") {
return false;
} else {
return Reflect.has(trapTarget, key);
}
}
});
console.log("value" in proxy); // false
console.log("name" in proxy); // true
console.log("toString" in proxy); // true
- 代理中的
has
陷阱會檢查key
是否為"value"
,如果是的話返回false
,若不是則調用Reflect.has()
方法返回默認行為。結果是即使target
上實際存在value
屬性,但用in
操作符檢查還是會返回false
,而對于name
和tostring
則正確返回true
【用deleteProperty陷阱防止刪除屬性】
delete
操作符可以從對象中移除屬性,如果成功則返回true
,不成功則返回false
。在嚴格模式下,如果嘗試刪除一個不可配置(nonconfigurable)
屬性則會導致程序拋出錯誤,而在非嚴格模式下只是返回false
let target = {
name: "target",
value: 42
};
Object.defineProperty(target, "name", {
configurable: false
});
console.log("value" in target); // true
let result1 = delete target.value;
console.log(result1); // true
console.log("value" in target); // false
// 注:下一行代碼在嚴格模式下會拋出錯誤
let result2 = delete target.name;
console.log(result2); // false
console.log("name" in target); // true
用
delete
操作符刪除value
屬性后,第三個console.log()
調用中的in
操作最終返回false
。不可配置屬性name
無法被刪除,所以delete
操作返回false
(如果這段代碼運行在嚴格模式下會拋出錯誤)。在代理中,可以通過deleteProperty
陷阱來改變這個行為每當通過
delete
操作符刪除對象屬性時,deleteProperty
陷阱都會被調用,它接受兩個參數
trapTarget 要刪除屬性的對象(代理的目標)
key 要刪除的屬性鍵(字符串或Symbol)
-
Reflect.deleteProperty()
方法為deleteProperty
陷阱提供默認實現,并且接受同樣的兩個參數。結合二者可以改變delete的具體表現行為,例如,可以像這樣來確保value
屬性不會被刪除
let target = {
name: "target",
value: 42
};
let proxy = new Proxy(target, {
deleteProperty(trapTarget, key) {
if (key === "value") {
return false;
} else {
return Reflect.deleteProperty(trapTarget, key);
}
}
});
// 嘗試刪除 proxy.value
console.log("value" in proxy); // true
let result1 = delete proxy.value;
console.log(result1); // false
console.log("value" in proxy); // true
// 嘗試刪除 proxy.name
console.log("name" in proxy); // true
let result2 = delete proxy.name;
console.log(result2); // true
console.log("name" in proxy); // false
- 這段代碼與
has
陷阱的示例非常相似,deleteProperty
陷阱檢查key
是否為"value"
,如果是的話返回false,否則調用Reflect.deleteProperty()
方法來使用默認行為。由于通過代理的操作被捕獲,因此value
屬性無法被刪除,但name
屬性就如期被刪除了。如果希望保護屬性不被刪除,而且在嚴格模式下不拋出錯誤,那么這個方法非常使用
【原型代理陷阱】
Object.setPrototypeOf()
方法被用于作為ES5
中的Object.getPrototypeOf()
方法的補充。通過代理中的setPrototypeOf
陷阱和getPrototypeOf
陷阱可以攔截這兩個方法的執行過程,在這兩種情況下,Object
上的方法會調用代理中的同名陷阱來改變方法的行為
兩個陷阱均與代理有關,但具體到方法只與每個陷阱的類型有關,
setPrototypeOf
陷阱接受以下這些參數
trapTarget 接受原型設置的對象(代理的目標)
proto 作為原型使用的對象
- 傳入
Object.setPrototypeOf()
方法和Reflect.setPrototypeOf()
方法的均是以上兩個參數,另一方面,getPrototypeOf
陷阱中的Object.getPrototypeOf()
方法和Reflect.getPrototypeOf()
方法只接受參數trapTarget
原型代理陷阱的運行機制
原型代理陷阱有一些限制。首先,
getPrototypeOf
陷阱必須返回對象或null,否則將導致運行時錯誤,返回值檢查可以確保Object.getPrototypeOf()
返回的總是預期的值;其次,在setPrototypeOf
陷阱中,如果操作失敗則返回的一定是false
,此時Object.setPrototypeOf()
會拋出錯誤,如果setPrototypeOf
返回了任何不是false
的值,那么Object.setPrototypeOf()
便假設操作成功
- 以下示例通過總是返回
null
,且不允許改變原型的方式隱藏了代理的原型
let target = {};
let proxy = new Proxy(target, {
getPrototypeOf(trapTarget) {
return null;
},
setPrototypeOf(trapTarget, proto) {
return false;
}
});
let targetProto = Object.getPrototypeOf(target);
let proxyProto = Object.getPrototypeOf(proxy);
console.log(targetProto === Object.prototype); // true
console.log(proxyProto === Object.prototype); // false
console.log(proxyProto); // null// 成功
Object.setPrototypeOf(target, {});// 拋出錯誤
Object.setPrototypeOf(proxy, {});
這段代碼強調了
target
和proxy
的行為差異。Object.getPrototypeOf()
給target
返回的是值,而給proxy
返回值時,由于getPrototypeOf
陷阱被調用,返回的是null
;同樣,Object.setPrototypeOf()
成功為target
設置原型,而給proxy
設置原型時,由于setPrototypeOf
陷阱被調用,最終拋出一個錯誤如果使用這兩個陷阱的默認行為,則可以使用
Reflect
上的相應方法。例如,下面的代碼實現了getPrototypeOf
和setPrototypeOf
陷阱的默認行為
let target = {};
let proxy = new Proxy(target, {
getPrototypeOf(trapTarget) {
return Reflect.getPrototypeOf(trapTarget);
},
setPrototypeOf(trapTarget, proto) {
return Reflect.setPrototypeOf(trapTarget, proto);
}
});
let targetProto = Object.getPrototypeOf(target);
let proxyProto = Object.getPrototypeOf(proxy);
console.log(targetProto === Object.prototype); // true
console.log(proxyProto === Object.prototype); // true// 成功
Object.setPrototypeOf(target, {});// 同樣成功
Object.setPrototypeOf(proxy, {});
- 由于本示例中的
getPrototypeOf
陷阱和setPrototypeOf
陷阱僅使用了默認行為,因此可以交換使用target
和paro×y
并得到相同結果。由于Reflect.getPrototypeOf()
方法和Reflect.setPrototypeOf()
方法與Object
上的同名方法存在一些重要差異,因此使用它們是很重要的
為什么有兩組方法
令人困惑的是,
Reflect.getPrototypeOf()
方法和Reflect.setPrototypeOf()
方法疑似Object.getPrototypeOf()
方法和Object.setPrototypeOf()
方法,盡管兩組方法執行相似的操作,但兩者間仍有一些不同之處
Object.getPrototypeOf()
和Object.setPrototypeOf()
是給開發者使用的高級操作;而Reflect.getPrototypeOf()
方法和Reflect.setprototypeOf()
方法則是底層操作,其賦予開發者可以訪問之前只在內部操作的[[GetPrototypeOf]]
和[[setPrototypeOf]]
的權限
Reflect.getPrototypeOf()
方法是內部[[GetprototypeOf]]
操作的包裹器,Reflect.setPrototypeOf()
方法與[[setPrototypeOf]]
的關系與之相同。Object
上相應的方法雖然也調用了[[GetPrototypeOf]]
和[[Setprototypeof]]
,但在此之前會執行一些額外步驟,并通過檢查返回值來決定下一步的操作
如果傳入的參數不是對象,則
Reflect.getPrototypeOf()
方法會拋出錯誤,而Object.getPrototypeOf()
方法則會在操作執行前先將參數強制轉換為一個對象。給這兩個方法傳入一個數字,會得到不同的結果
let result1 = Object.getPrototypeOf(1);
console.log(result1 === Number.prototype); // true// 拋出錯誤
Reflect.getPrototypeOf(1);
Object.getPrototypeOf()
方法會強制讓數字1變為Number
對象,所以可以檢索它的原型并得到返回值Number.prototype
;而由于Reflect.getPrototypeOf()
方法不強制轉化值的類型,而且1又不是一個對象,故會拋出一個錯誤Reflect.setPrototypeOf()
方法與Object.setPrototypeOf()
方法也不盡相同。具體而言,Reflect.setPrototypeOf()
方法返回一個布爾值來表示操作是否成功,成功時返回true
,失敗則返回false
;而Object.setPrototypeOf()
方法一旦失敗則會拋出一個錯誤當
setPrototypeOf
代理陷阱返回false
時會導致Object.setPrototypeOf()
拋出一個錯誤。Object.setPrototypeOf()
方法返回第一個參數作為它的值,因此其不適合用于實現setPrototypeOf
代理陷阱的默認行為
let target1 = {};
let result1 = Object.setPrototypeOf(target1, {});
console.log(result1 === target1); // true
let target2 = {};
let result2 = Reflect.setPrototypeOf(target2, {});
console.log(result2 === target2); // false
console.log(result2); // true
- 在這個示例中,
Object.setPrototypeOf()
返回target1
,但Reflect.setPrototypeOf()
返回的是true
。這種微妙的差異非常重要,在object
和Reflect
上還有更多看似重復的方法,但是在所有代理陷阱中一定要使用Reflect
上的方法
【對象可擴展性陷阱】
ES5
已經通過Object.preventExtensions()
方法和Object.isExtensible()
方法修正了對象的可擴展性,ES6
可以通過代理中的preventExtensions
和isExtensible
陷阱攔截這兩個方法并調用底層對象。兩個陷阱都接受唯一參數trapTarget
對象,并調用它上面的方法。isExtensible
陷阱返回的一定是一個布爾值,表示對象是否可擴展;preventExtensions
陷阱返回的也一定是布爾值,表示操作是否成功
Reflect.preventExtensions()
方法和Reflect.IsExtensible()
方法實現相應陷阱中默認行為,二者都返回布爾值
兩個基礎示例
以下這段代碼是對象可擴展性陷阱的實際應用,實現了
isExtensible
和preventExtensions
陷阱的默認行為
let target = {};
let proxy = new Proxy(target, {
isExtensible(trapTarget) {
return Reflect.isExtensible(trapTarget);
},
preventExtensions(trapTarget) {
return Reflect.preventExtensions(trapTarget);
}
});
console.log(Object.isExtensible(target)); // true
console.log(Object.isExtensible(proxy)); // true
Object.preventExtensions(proxy);
console.log(Object.isExtensible(target)); // false
console.log(Object.isExtensible(proxy)); // false
- 此示例展示了
Object.preventExtensions()
方法和Object.isExtensible()
方法直接從proxy
傳遞到target
的過程,當然,可以改變這種默認行為,例如,如果想讓Object.preventExtensions()
對于proxy
失效,那么可以在preventExtensions
陷阱中返回false
let target = {};
let proxy = new Proxy(target, {
isExtensible(trapTarget) {
return Reflect.isExtensible(trapTarget);
},
preventExtensions(trapTarget) {
return false
}
});
console.log(Object.isExtensible(target)); // true
console.log(Object.isExtensible(proxy)); // true
Object.preventExtensions(proxy);
console.log(Object.isExtensible(target)); // true
console.log(Object.isExtensible(proxy)); // true
- 這里的
Object.preventExtensions(proxy)
調用實際上被忽略了,這是因為preventExtensions
陷阱返回了false
,所以操作不會轉發到底層目標,Object.isExtensible()
最終返回true
【重復的可擴展性方法】
Object.isExtensible()
方法和Reflect.isExtensible()
方法非常相似,只有當傳入非對象值時,Object.isExtensible()
返回false
,而Reflect.isExtensible()
則拋出一個錯誤
let result1 = Object.isExtensible(2);
console.log(result1); // false// 拋出錯誤
let result2 = Reflect.isExtensible(2);
這條限制類似于
Object.getPrototypeOf()
方法與Reflect.getPrototypeOf()
方法之間的差異,因為相比高級功能方法而言,底層的具有更嚴格的錯誤檢査Object.preventExtensions()
方法和Reflect.preventExtensions()
方法同樣非常相似。無論傳入Object.preventExtensions()
方法的參數是否為一個對象,它總是返回該參數;而如果Reflect.preventExtensions()
方法的參數不是對象就會拋出錯誤;如果參數是一個對象,操作成功時Reflect.preventExtensions()
會返回true
,否則返回false
let result1 = Object.preventExtensions(2);
console.log(result1); // 2
let target = {};
let result2 = Reflect.preventExtensions(target);
console.log(result2); // true// 拋出錯誤
let result3 = Reflect.preventExtensions(2);
- 在這里,即使值2不是一個對象,
Object.preventExtensions()
方法也將其透傳作為返回值,而Reflect.preventExtensions()
方法則會拋出錯誤,只有當傳入對象時它才返回true
【屬性描述符陷阱】
ES5
最重要的特性之一是可以使用Object.defineProperty()
方法定義屬性特性(property attribute)
。在早期版本的JS
中無法定義訪問器屬性,無法將屬性設置為只讀或不可配置。直到Object.defineProperty()
方法出現之后才支持這些功能,并且可以通過Object.getOwnPropertyDescriptor()
方法來獲取這些屬性
在代理中可以分別用
defineProperty
陷阱和getOwnPropertyDescriptor
陷阱攔截Object.defineProperty()
方法和Object.getOwnPropertyDescriptor()
方法的調用。definePropepty
陷阱接受以下參數
trapTarget 要定義屬性的對象(代理的目標)
key 屬性的鍵(字符串或Symbol)
descriptor 屬性的描述符對象
-
defineProperty
陷阱需要在操作成功后返回true
,否則返回false
。getOwnPropertyDescriptor
陷阱只接受trapTarget
和key
兩個參數,最終返回描述符。Reflect.defineProperty()
方法和Reflect.getOwnPropertyDescriptor()
方法與對應的陷阱接受相同參數。這個示例實現的是每個陷阱的默認行為
let proxy = new Proxy({}, {
defineProperty(trapTarget, key, descriptor) {
return Reflect.defineProperty(trapTarget, key, descriptor);
},
getOwnPropertyDescriptor(trapTarget, key) {
return Reflect.getOwnPropertyDescriptor(trapTarget, key);
}
});
Object.defineProperty(proxy, "name", {
value: "proxy"
});
console.log(proxy.name); // "proxy"
let descriptor = Object.getOwnPropertyDescriptor(proxy, "name");
console.log(descriptor.value); // "proxy"
- 這段代碼通過
Object.defineProperty()
方法在代理上定義了屬性"name"
,該屬性的描述符可通過Object.getOwnPropertyDescriptor()
方法來獲取
給Object.defineProperty()添加限制
defineProperty
陷阱返回布爾值來表示操作是否成功。返回true
時,Object.defineProperty()
方法成功執行;返回false
時,Object.defineProperty()
方法拋出錯誤。這個功能可以用來限制Object.defineProperty()
方法可定義的屬性類型,例如,如果希望阻止Symbol
類型的屬性,則可以當屬性鍵為symbol
時返回false
當
key
是Symbol
類型時defineProperty
代理陷阱返回false
,否則執行默認行為。調用Object.defineProperty()
并傳入"name"
,因此鍵的類型是字符串所以方法成功執行;調用Object.defineProperty()
方法并傳入nameSymbol
,defineProperty
陷阱返回false
所以拋出錯誤
[注意]如果讓陷阱返回true
并且不調用Reflect.defineProperty()
方法,則可以讓Object.defineProperty()
方法靜默失效,這既消除了錯誤又不會真正定義屬性
描述符對象限制
為確保
Object.defineProperty()
方法和Object.getOwnPropertyDescriptor()
方法的行為一致,傳入defineProperty
陷阱的描述符對象已規范化。從getOwnPropertyDescriptor
陷阱返回的對象由于相同原因被驗證
無論將什么對象作為第三個參數傳遞給
Object.defineProperty()
方法,都只有屬性enumerable
、configurable
、value
、writable
、get
和set
將出現在傳遞給defineProperty
陷阱的描述符對象中
let proxy = new Proxy({}, {
defineProperty(trapTarget, key, descriptor) {
console.log(descriptor.value); // "proxy"
console.log(descriptor.name); // undefined
return Reflect.defineProperty(trapTarget, key, descriptor);
}
});
Object.defineProperty(proxy, "name", {
value: "proxy",
name: "custom"
});
在這段代碼中,調用
Object.defineProperty()
時傳入包含非標準name
屬性的對象作為第三個參數。當defineProperty
陷阱被調用時,descriptor
對象有value
屬性卻沒有name
屬性,這是因為descriptor
不是實際傳入Object.defineProperty()
方法的第三個參數的引用,而是一個只包含那些被允許使用的屬性的新對象。Reflect.defineProperty()
方法同樣也忽略了描述符上的所有非標準屬性getOwnPropertyDescriptor
陷阱的限制條件稍有不同,它的返回值必須是null
、undefined
或一個對象。如果返回對象,則對象自己的屬性只能是enumepable
、configurable
、value
、writable
、get
和set
,在返回的對象中使用不被允許的屬性會拋出一個錯誤
let proxy = new Proxy({}, {
getOwnPropertyDescriptor(trapTarget, key) {
return {
name: "proxy"
};
}
});
// 拋出錯誤
let descriptor = Object.getOwnPropertyDescriptor(proxy, "name");
屬性描述符中不允許有
name
屬性,當調用Object.getOwnPropertyDescriptor()
時,getOwnPropertyDescriptor
的返回值會觸發一個錯誤。這條限制可以確保無論代理中使用了什么方法,Object.getOwnPropertyDescriptor()
返回值的結構總是可靠的
重復的描述符方法
再一次在
ES6
中看到這些令人困惑的相似方法:看起來Object.defineProperty()
方法和Object.getOwnPropertyDescriptor()
方法分別與Reflect.defineProperty()
方法和Reflect.getOwnPropertyDescriptor()
方法做了同樣的事情。這4個方法也有一些微妙但卻很重要的差異
Object.defineProperty()
方法和Reflect.defineProperty()
方法只有返回值不同:Object.defineProperty()
方法返回第一個參數,而Reflect.defineProperty()
的返回值與操作有關,成功則返回true
,失敗則返回false
let target = {};
let result1 = Object.defineProperty(target, "name", {
value: "target "
});
console.log(target === result1); // true
let result2 = Reflect.defineProperty(target, "name", {
value: "reflect"
});
console.log(result2); // true
調用
Object.defineProperty()
時傳入target
,返回值是target
;調用Reflect.defineProperty()
時傳入target
,返回值是true
,表示操作成功。由于defineProperty
代理陷阱需要返回一個布爾值,因此必要時最好用Reflect.defineProperty()
來實現默認行為調用
Object.getOwnPropertyDescriptor()
方法時傳入原始值作為第一個參數,內部將這個值強制轉換為一個對象;另一方面,若調用Reflect.getOwnPropertyDescriptor()
方法時傳入原始值作為第一個參數,則拋出一個錯誤
let descriptor1 = Object.getOwnPropertyDescriptor(2, "name");
console.log(descriptor1); // undefined// 拋出錯誤
let descriptor2 = Reflect.getOwnPropertyDescriptor(2, "name");
- 由于
Object.getOwnPropertyDescriptor()
方法將數值2強制轉換為一個不含name
屬性的對象,因此它返回undefined
,這是當對象中沒有指定的name
屬性時的標準行為。然而當調用Reflect.getOwnPropertyDescriptor()
時立即拋出一個錯誤,因為該方法不接受原始值作為第一個參數
【ownKeys陷阱】
ownKeys
代理陷阱可以攔截內部方法[[OwnPropertyKeys]]
,我們通過返回個數組的值可以覆寫其行為。這個數組被用于Object.keys()
、Object.getOwnPropertyNames()
、Object.getOwnPropertySymbols()
和Object.assign()
4個方法,Object.assign()
方法用數組來確定需要復制的屬性
ownKeys
陷阱通過Reflect.ownKeys()
方法實現默認的行為,返回的數組中包含所有自有屬性的鍵名,字符串類型和Symbol
類型的都包含在內。Object.getOwnPropertyNames()
方法和Object.keys()
方法返回的結果將Symbol
類型的屬性名排除在外,Object.getOwnPropertySymbols()
方法返回的結果將字符串類型的屬性名排除在外。Object.assign()
方法支持字符串和Symbol
兩種類型
ownKeys
陷阱唯一接受的參數是操作的目標,返回值必須是一個數組或類數組對象,否則就拋出錯誤。當調用Object.keys()
、Object.getOwnPropertyNames()
、Object.getOwnPropertySymbols()
或Object.assign()
方法時,可以用ownKeys
陷阱來過濾掉不想使用的屬性鍵。假設不想引入任何以下劃線字符(在JS
中下劃線符號表示字段是私有的)開頭的屬性名稱,則可以用ownKeys
陷阱過濾掉那些鍵
let proxy = new Proxy({}, {
ownKeys(trapTarget) {
return Reflect.ownKeys(trapTarget).filter(key => {
return typeof key !== "string" || key[0] !== "_";
});
}
});
let nameSymbol = Symbol("name");
proxy.name = "proxy";
proxy._name = "private";
proxy[nameSymbol] = "symbol";
let names = Object.getOwnPropertyNames(proxy),
keys = Object.keys(proxy);
symbols = Object.getOwnPropertySymbols(proxy);
console.log(names.length); // 1
console.log(names[0]); // "name"
console.log(keys.length); // 1
console.log(keys[0]); // "name"
console.log(symbols.length); // 1
console.log(symbols[0]); // "Symbol(name)"
這個示例使用了一個
ownKeys
陷阱,它首先調用Reflect.ownKeys()
獲取目標的默認鍵列表;接下來,用filter()
過濾掉以下劃線字符開始的字符串。然后,將3個屬性添加到proxy
對象:name
、_name
和nameSymbol
。調用Object.getOwnPropertyNames()
和Object.Keys()
時傳入proxy
, 只返回name
屬性;同樣,調用Object.getOwnPropertySymbols()
時傳入proxy
,只返回nameSymbol
。由于_name
屬性被過濾掉了,因此它不出現在這兩次結果中盡管
ownKeys
代理陷阱可以修改一小部分操作返回的鍵,但不影響更常用的操作,例如for-of
循環和Object.keys()
方法,這些不能使用代理來更改。ownKeys
陷阱也會影響for-in
循環,當確定循環內部使用的鍵時會調用陷阱
【函數代理中的apply和construct陷阱】
所有代理陷阱中,只有
apply
和construct
的代理目標是一個函數。函數有兩個內部方法[[Call]]
和[[Construct]]
,apply
陷阱和construct
陷阱可以覆寫這些內部方法。若使用new
操作符調用函數,則執行[[Construct]]
方法;若不用,則執行[[Construct]]
方法,此時會執行apply
陷阱,它和Reflect.apply()
都接受以下參數
trapTaqget 被執行的函數(代理的目標)
thisArg 函數被調用時內部this的值
argumentsList 傳遞給函數的參數數組
- 當使用
new
調用函數時調用的construct
陷阱接受以下參數
trapTarget 被執行的函數(代理的目標)
argumentsList 傳遞給函數的參數數組
-
Reflect.construct()
方法也接受這兩個參數,其還有一個可選的第三個參數newTarget
。若給定這個參數,則該參數用于指定函數內部new.target
的值 - 有了
apply
和construct
陷阱,可以完全控制任何代理目標函數的行為
let target = function() {
return 42
},
proxy = new Proxy(target, {
apply: function(trapTarget, thisArg, argumentList) {
return Reflect.apply(trapTarget, thisArg, argumentList);
},
construct: function(trapTarget, argumentList) {
return Reflect.construct(trapTarget, argumentList);
}
});
// 使用了函數的代理,其目標對象會被視為函數
console.log(typeof proxy); // "function"
console.log(proxy()); // 42
var instance = new proxy();
console.log(instance instanceof proxy); // true
console.log(instance instanceof target); // true
- 在這里,有一個返回數字42的函數,該函數的代理分別使用
apply
陷阱和construct
陷阱來將那些行為委托給Reflect.apply()
方法和Reflect.construct()
方法。最終結果是代理函數與目標函數完全相同,包括在使用typeof
時將自己標識為函數。不用new
調用代理時返回42,用new
調用時創建一個instance對象,它同時是代理和目標的實例,因為instanceo
f通過原型鏈來確定此信息,而原型鏈查找不受代理影響,這也就是代理和目標好像有相同原型的原因
驗證函數參數
apply
陷阱和construct
陷阱增加了一些可能改變函數執行方式的可能性,例如,假設驗證所有參數都屬于特定類型,則可以在apply
陷阱中檢查參數
// 將所有參數相加
function sum(...values) {
return values.reduce((previous, current) => previous + current, 0);
}
let sumProxy = new Proxy(sum, {
apply: function(trapTarget, thisArg, argumentList) {
argumentList.forEach((arg) => {
if (typeof arg !== "number") {
throw new TypeError("All arguments must be numbers.");
}
});
return Reflect.apply(trapTarget, thisArg, argumentList);
},
construct: function(trapTarget, argumentList) {
throw new TypeError("This function can't be called with new.");
}
});
console.log(sumProxy(1, 2, 3, 4)); // 10// 拋出錯誤
console.log(sumProxy(1, "2", 3, 4));// 同樣拋出錯誤
let result = new sumProxy();
此示例使用
apply
陷阱來確保所有參數都是數字,sum()
函數將所有傳入的參數相加。如果傳入非數字值,函數仍將嘗試操作,可能導致意外結果發生。通過在sumProxy()
代理中封裝sum()
,這段代碼攔截了函數調用,并確保每個參數在被調用前一定是數字。為了安全起見,代碼還使用construct
陷阱來確保函數不會被new
調用還可以執行相反的操作,確保必須用
new
來調用函數并驗證其參數為數字
function Numbers(...values) {
this.values = values;
}
let NumbersProxy = new Proxy(Numbers, {
apply: function(trapTarget, thisArg, argumentList) {
throw new TypeError("This function must be called with new.");
},
construct: function(trapTarget, argumentList) {
argumentList.forEach((arg) => {
if (typeof arg !== "number") {
throw new TypeError("All arguments must be numbers.");
}
});
return Reflect.construct(trapTarget, argumentList);
}
});
let instance = new NumbersProxy(1, 2, 3, 4);
console.log(instance.values); // [1,2,3,4]
// 拋出錯誤
NumbersProxy(1, 2, 3, 4);
- 在這個示例中,
apply
陷阱拋出一個錯誤,而construct
陷阱使用Reflect.construct()
方法來驗證輸入并返回一個新實例。當然,也可以不借助代理而用new.target
來完成相同的事情
不用new調用構造函數
-
new.target
元屬性是用new
調用函數時對該函數的引用,所以可以通過檢查new.target
的值來確定函數是否是通過new
來調用的
function Numbers(...values) {
if (typeof new.target === "undefined") {
throw new TypeError("This function must be called with new.");
}
this.values = values;
}
let instance = new Numbers(1, 2, 3, 4);
console.log(instance.values); // [1,2,3,4]
// 拋出錯誤
Numbers(1, 2, 3, 4);
在這段代碼中,不用
new
調用Numbers()
會拋出一個錯誤。如果目標是防止用new
調用函數,則這樣編寫代碼比使用代理簡單得多。但有時不能控制要修改行為的函數,在這種情況下,使用代理才有意義假設
Numbers()
函數定義在無法修改的代碼中,知道代碼依賴new.target
,希望函數避免檢查卻仍想調用函數。在這種情況下,用new
調用時的行為已被設定,所以只能使用apply
陷阱
function Numbers(...values) {
if (typeof new.target === "undefined") {
throw new TypeError("This function must be called with new.");
}
this.values = values;
}
let NumbersProxy = new Proxy(Numbers, {
apply: function(trapTarget, thisArg, argumentsList) {
return Reflect.construct(trapTarget, argumentsList);
}
});
let instance = NumbersProxy(1, 2, 3, 4);
console.log(instance.values); // [1,2,3,4]
-
apply
陷阱用傳入的參數調用Reflect.construct()
,就可以讓Numbersproxy()
函數無須使用new
就能實現用new
調用Numbers()
的行為。Numbers()
內部的new.target
等于Numbers()
,所以不會有錯誤拋出。盡管這個修改new.target
的示例非常簡單,但這樣做顯得更加直接
覆寫抽象基類構造函數
進一步修改
new.target
,可以將第三個參數指定為Reflect.construct()
作為賦值給new.target
的特定值。這項技術在函數根據已知值檢查new.target
時很有用,例如創建抽象基類構造函數。在一個抽象基類構造函數中,new.target
理應不同于類的構造函數,就像在這個示例中
class AbstractNumbers {
constructor(...values) {
if (new.target === AbstractNumbers) {
throw new TypeError("This function must be inherited from.");
}
this.values = values;
}
}
class Numbers extends AbstractNumbers {}
let instance = new Numbers(1, 2, 3, 4);
console.log(instance.values); // [1,2,3,4]
// 拋出錯誤
new AbstractNumbers(1, 2, 3, 4);
- 當調用
new AbstractNumbers()
時,new.Target
等于AbstractNumbers
并拋出一個錯誤。調用new Numbers()
仍然有效,因為new.target
等于Numbers
??梢允謩佑么斫onew.target
賦值來繞過構造函數限制
class AbstractNumbers {
constructor(...values) {
if (new.target === AbstractNumbers) {
throw new TypeError("This function must be inherited from.");
}
this.values = values;
}
}
let AbstractNumbersProxy = new Proxy(AbstractNumbers, {
construct: function(trapTarget, argumentList) {
return Reflect.construct(trapTarget, argumentList, function() {});
}
});
let instance = new AbstractNumbersProxy(1, 2, 3, 4);
console.log(instance.values); // [1,2,3,4]
-
AbstractNumbersProxy
使用construct
陷阱來攔截對new AbstractNumbersProxy()
方法的調用。然后傳入陷阱的參數來調用Reflect.construct()
方法,并添加一個空函數作為第三個參數。這個空函數被用作構造函數內部new.target
的值。由于new.target
不等于AbstractNumbers
,因此不會拋出錯誤,構造函數可以完全執行
可調用的類構造函數
必須用
new
來調用類構造函數,因為類構造函數的內部方法[[Call]]
被指定來拋出一個錯誤。但是代理可以攔截對[[Call]]
方法的調用,這意味著可以通過使用代理來有效地創建可調用類構造函數。例如,如果希望類構造函數不用new
就可以運行,那么可以使用apply
陷阱來創建一個新實例
class Person {
constructor(name) {
this.name = name;
}
}
let PersonProxy = new Proxy(Person, {
apply: function(trapTarget, thisArg, argumentList) {
return new trapTarget(...argumentList);
}
});
let me = PersonProxy("huochai");
console.log(me.name); // "huochai"
console.log(me instanceof Person); // true
console.log(me instanceof PersonProxy); // true
-
PersonProxy
對象是Person
類構造函數的代理,類構造函數是函數,所以當它們被用于代理時就像函數一樣。apply
陷阱覆寫默認行為并返回trapTarget
的新實例,該實例與pepson
相等。用展開運算符將argumentList
傳遞給trapTarget
來分別傳遞每個參數。不使用new
調用PersonProxy()
可以返回一個person
的實例,如果嘗試不使用new
調用person()
,則構造函數將拋出一個錯誤。創建可調用類構造函數只能通過代理來進行
可撤銷代理
通常,在創建代理后,代理不能脫離其目標。但是可能存在希望撤銷代理的情況,然后代理便失去效力。無論是出于安全目的通過
API
提供一個對象,還是在任意時間點切斷訪問,撤銷代理都非常有用
可以使用
proxy.revocable()
方法創建可撤銷的代理,該方法采用與Proxy
構造函數相同的參數:目標對象和代理處理程序,返回值是具有以下屬性的對象
proxy 可被撤銷的代理對象
revoke 撤銷代理要調用的函
- 當調用
revoke()
函數時,不能通過proxy
執行進一步的操作。任何與代理對象交互的嘗試都會觸發代理陷阱拋出錯誤
let target = {
name: "target"
};
let { proxy, revoke } = Proxy.revocable(target, {});
console.log(proxy.name); // "target"revoke();
// 拋出錯誤
console.log(proxy.name);
- 此示例創建一個可撤銷代理,它使用解構功能將
proxy
和revoke
變量賦值給Proxy.revocable()
方法返回的對象上的同名屬性。之后,proxy
對象可以像不可撤銷代理對象一樣使用。因此proxy.name
返回"target"
,因為它直接透傳了target.name
的值。然而,一旦revoke()
函數被調用,代理不再是函數,嘗試訪問proxy.name
會拋出一個錯誤,正如任何會觸發代理上陷阱的其他操作一樣
模仿數組
在
ES6
出現以前,開發者不能在JS
中完全模仿數組的行為。而ES6
中的代理和反射API
可以用來創建一個對象,該對象的行為與添加和刪除屬性時內建數組類型的行為相同
let colors = ["red", "green", "blue"];
console.log(colors.length); // 3
colors[3] = "black";
console.log(colors.length); // 4
console.log(colors[3]); // "black"
colors.length = 2;
console.log(colors.length); // 2
console.log(colors[3]); // undefined
console.log(colors[2]); // undefined
console.log(colors[1]); // "green"
- 此示例中有兩個特別重要的行為
1、當給colors[3]
賦值時,length
屬性的值增加到4
2、當length
屬性被設置為2時,數組中最后兩個元素被刪除
- 要完全重造內建數組,只需模擬上述兩種行為。下面=將講解如何創建一個能正確模仿這些行為的對象
【檢測數組索引】
為整數屬性鍵賦值是數組才有的特例,因為它們與非整數鍵的處理方式不同。要判斷一個屬性是否是一個數組索引,可以參考
ES6
規范提供的以下說明
當且僅當
ToString(ToUint32(P))
等于P
,并且ToUint32(P)
不等于2<sup>32</sup>-1
時,字符串屬性名稱P
才是一個數組索引
- 此操作可以在JS中實現,如下所示
function toUint32(value) {
return Math.floor(Math.abs(Number(value))) % Math.pow(2, 32);
}
function isArrayIndex(key) {
let numericKey = toUint32(key);
return String(numericKey) == key && numericKey < (Math.pow(2, 32) - 1);
}
-
toUint32()
函數通過規范中描述的算法將給定的值轉換為無符號32位整數;isArrayIndex()
函數先將鍵轉換為uint32
結構,然后進行一次比較以確定這個鍵是否是數組索引。有了這兩個實用函數,就可以開始實現一個模擬內建數組的對象
【添加新元素時增加length的值】
之前描述的數組行為都依賴屬性賦值,只需用
set
代理陷阱即可實現之前提到的兩個行為。請看以下這個示例,當操作的數組索引大于length-1
時,length
屬性也一同增加,這實現了兩個特性中的前一個
function toUint32(value) {
return Math.floor(Math.abs(Number(value))) % Math.pow(2, 32);
}
function isArrayIndex(key) {
let numericKey = toUint32(key);
return String(numericKey) == key && numericKey < (Math.pow(2, 32) - 1);
}
function createMyArray(length=0) {
return new Proxy({ length }, {
set(trapTarget, key, value) {
let currentLength = Reflect.get(trapTarget, "length");
// 特殊情況
if (isArrayIndex(key)) {
let numericKey = Number(key);
if (numericKey >= currentLength) {
Reflect.set(trapTarget, "length", numericKey + 1);
}
}
// 無論鍵的類型是什么,都要執行這行代碼
return Reflect.set(trapTarget, key, value);
}
});
}
let colors = createMyArray(3);
console.log(colors.length); // 3
colors[0] = "red";
colors[1] = "green";
colors[2] = "blue";
console.log(colors.length); // 3
colors[3] = "black";
console.log(colors.length); // 4
console.log(colors[3]); // "black"
這段代碼用
set
代理陷阱來攔截數組索引的設置過程。如果鍵是數組索引,則將其轉換為數字,因為鍵始終作為字符串傳遞。接下來,如果該數值大于或等于當前長度屬性,則將length
屬性更新為比數字鍵多1(設置位置3意味著length
必須是4)。然后,由于希望被設置的屬性能夠接收到指定的值,因此調用Reflect.set()
通過默認行為來設置該屬性調用
createMyArray()
并傳入3作為length
的值來創建最初的自定義數組,然后立即添加這3個元素的值,在此之前length
屬性一直是3,直到把位置3賦值為值"black"
時,ength
才被設置為4
【減少length的值來刪除元素】
僅當數組索引大于等于
length
屬性時才需要模擬第一個數組特性,第二個特性與之相反,即當length
屬性被設置為比之前還小的值時會移除數組元素。這不僅涉及長度屬性的改變,還要刪除原本可能存在的元素。例如有一個長度為4的數組,如果將length
屬性設置為2,則會刪除位置2和3中的元素。同樣可以在set
代理陷阱中完成這個操作,這不會影響到第一個特性。以下示例在之前的基礎上更新了createMyArray
方法
function toUint32(value) {
return Math.floor(Math.abs(Number(value))) % Math.pow(2, 32);
}
function isArrayIndex(key) {
let numericKey = toUint32(key);
return String(numericKey) == key && numericKey < (Math.pow(2, 32) - 1);
}
function createMyArray(length=0) {
return new Proxy({ length }, {
set(trapTarget, key, value) {
let currentLength = Reflect.get(trapTarget, "length");
// 特殊情況
if (isArrayIndex(key)) {
let numericKey = Number(key);
if (numericKey >= currentLength) {
Reflect.set(trapTarget, "length", numericKey + 1);
}
}
else if (key === "length") {
if (value < currentLength) {
for (let index = currentLength - 1; index >= value; index--) {
Reflect.deleteProperty(trapTarget, index);
}
}
}
// 無論鍵的類型是什么,都要執行這行代碼
return Reflect.set(trapTarget, key, value);
}
});
}
let colors = createMyArray(3);
console.log(colors.length); // 3
colors[0] = "red";
colors[1] = "green";
colors[2] = "blue";
colors[3] = "black";
console.log(colors.length); // 4
colors.length = 2;
console.log(colors.length); // 2
console.log(colors[3]); // undefined
console.log(colors[2]); // undefined
console.log(colors[1]); // "green"
console.log(colors[0]); // "red"
該代碼中的
set
代理陷阱檢查key
是否為"length"
,以便正確調整對象的其余部分。當開始檢查時,首先用Reflect.get()
獲取當前長度值,然后與新的值進行比較,如果新值比當前長度小,則通過一個for
循環刪除目標上所有不再可用的屬性,fop
循環從后往前從當前數組長度(current Length)
處開始刪除每個屬性,直到到達新的數組長度(value)
為止此示例為
colors
添加了4種顏色,然后將它的length
屬性設置為2,位于位置2和3的元素被移除,因此嘗試訪問它們時返回的是undefined
。length
屬性被正確設置為2,位置0和1中的元素仍可訪問實現了這兩個特性,就可以很輕松地創建一個模仿內建數組特性的對象了。但創建一個類來封裝這些特性是更好的選擇,所以下一步用一個類來實現這個功能
【實現MyArray類】
想要創建使用代理的類,最簡單的方法是像往常一樣定義類,然后在構造函數中返回一個代理,那樣的話,當類實例化時返回的對象是代理而不是實例(構造函數中
this
的值是該實例)。實例成為代理的目標,代理則像原本的實例那樣被返回。實例完全私有化,除了通過代理間接訪問外,無法直接訪問它
下面是從一個類構造函數返回一個代理的簡單示例
class Thing {
constructor() {
return new Proxy(this, {});
}
}
let myThing = new Thing();
console.log(myThing instanceof Thing); // true
在這個示例中,類
Thing
從它的構造函數中返回一個代理,代理的目標是this
,所以即使myThing
是通過調用Thing
構造函數創建的,但它實際上是一個代理。由于代理會將它們的特性透傳給目標,因此myThing
仍然被認為是Thing
的一個實例,故對任何使用Thing類的人來說代理是完全透明的從構造函數中可以返回一個代理,理解這個概念后,用代理創建一個自定義數組類就相對簡單了。其代碼與之前"減少
length
的值來刪除元素"的代碼大部分是一樣的,可以使用相同的代理代碼,但這次需要把它放在一個類構造函數中。下面是完整的示例
function toUint32(value) {
return Math.floor(Math.abs(Number(value))) % Math.pow(2, 32);
}
function isArrayIndex(key) {
let numericKey = toUint32(key);
return String(numericKey) == key && numericKey < (Math.pow(2, 32) - 1);
}
class MyArray {
constructor(length=0) {
this.length = length;return new Proxy(this, {
set(trapTarget, key, value) {
let currentLength = Reflect.get(trapTarget, "length");
// 特殊情況
if (isArrayIndex(key)) {
let numericKey = Number(key);
if (numericKey >= currentLength) {
Reflect.set(trapTarget, "length", numericKey + 1);
}
}
else if (key === "length") {
if (value < currentLength) {
for (let index = currentLength - 1; index >= value; index--) {
Reflect.deleteProperty(trapTarget, index);
}
}
}
// 無論鍵的類型是什么,都要執行這行代碼
return Reflect.set(trapTarget, key, value);
}
});
}
}
let colors = new MyArray(3);
console.log(colors instanceof MyArray); // true
console.log(colors.length); // 3
colors[0] = "red";
colors[1] = "green";
colors[2] = "blue";
colors[3] = "black";
console.log(colors.length); // 4
colors.length = 2;
console.log(colors.length); // 2
console.log(colors[3]); // undefined
console.log(colors[2]); // undefined
console.log(colors[1]); // "green"
console.log(colors[0]); // "red"
這段代碼創建了一個
MyArray
類,從它的構造函數返回一個代理。length
屬性被添加到構造函數中,初始化為傳入的值或默認值0,然后創建代理并返回。colors
變量看起來好像只是MyArray
的一個實例,并實現了數組的兩個關鍵特性雖然從類構造函數返回代理很容易,但這也意味著每創建一個實例都要創建一個新代理。然而,有一種方法可以讓所有實例共享一個代理:將代理用作原型
將代理用作原型
如果代理是原型,僅當默認操作繼續執行到原型上時才會調用代理陷阱,這會限制代理作為原型的能力
let target = {};
let newTarget = Object.create(new Proxy(target, {
// 永遠不會被調用
defineProperty(trapTarget, name, descriptor) {
// 如果被調用就會引發錯誤
return false;
}
}));
Object.defineProperty(newTarget, "name", {
value: "newTarget"
});
console.log(newTarget.name); // "newTarget"
console.log(newTarget.hasOwnProperty("name")); // true
創建
newTarget
對象,它的原型是一個代理。由于代理是透明的,用target
作為代理的目標實際上讓target
成為newTarget
的原型?,F在,僅當newTarget
上的操作被透傳給目標時才會調用代理陷阱調用
Object.defineProperty()
方法并傳入newTarget
來創建一個名為name
的自有屬性。在對象上定義屬性的操作不需要操作對象原型,所以代理中的defineProperty
陷阱永遠不會被調用,name
作為自有屬性被添加到newTarget
上盡管代理作為原型使用時極其受限,但有幾個陷阱卻仍然有用
【在原型上使用get陷阱】
調用內部方法
[[Get]]
讀取屬性的操作先查找自有屬性,如果未找到指定名稱的自有屬性,則繼續到原型中查找,直到沒有更多可以查找的原型過程結束
如果設置一個
get
代理陷阱,則每當指定名稱的自有屬性不存在時,又由于存在以上過程,往往會調用原型上的陷阱。當訪問我們不能保證存在的屬性時,則可以用get
陷阱來預防意外的行為。只需創建一個對象,在嘗試訪問不存在的屬性時拋出錯誤即可
let target = {};
let thing = Object.create(new Proxy(target, {
get(trapTarget, key, receiver) {
throw new ReferenceError(`${key} doesn't exist`);
}
}));
thing.name = "thing";
console.log(thing.name); // "thing"
// 拋出錯誤
let unknown = thing.unknown;
在這段代碼中,用一個代理作為原型創建了
thing
對象,當調用它時,如果其上不存在給定的鍵,那么get
陷阱會拋出錯誤。由于thing.name
屬性存在,故讀取它的操作不會調用原型上的get
陷阱,只有當訪問不存在的thing.unknown
屬性時才會調用當執行最后一行時,由于
unknown
不是thing
的自有屬性,因此該操作繼續在原型上查找,之后get
陷阱會拋出一個錯誤。在JS
中,訪問未知屬性通常會靜默返回undefined
,這種拋出錯誤的特性(其他語言中的做法)非常有用要明白,在這個示例中,理解
trapTarget
和receiver
是不同的對象很重要。當代理被用作原型時,trapTarget
是原型對象,receiver
是實例對象。在這種情況下,trapTarget
與target
相等,receiver
與thing
相等,所以可以訪問代理的原始目標和要操作的目標
【在原型上使用set陷阱】
內部方法
[[Set]]
同樣會檢查目標對象中是否含有某個自有屬性,如果不存在則繼續查找原型。當給對象屬性賦值時,如果存在同名自有屬性則賦值給它;如果不存在給定名稱,則繼續在原型上查找。最棘手的是,無論原型上是否存在同名屬性,給該屬性賦值時都將默認在實例(不是原型)中創建該屬性
let target = {};
let thing = Object.create(new Proxy(target, {
set(trapTarget, key, value, receiver) {
return Reflect.set(trapTarget, key, value, receiver);
}
}));
console.log(thing.hasOwnProperty("name")); // false
// 觸發了 `set` 代理陷阱
thing.name = "thing";
console.log(thing.name); // "thing"
console.log(thing.hasOwnProperty("name")); // true
// 沒有觸發 `set` 代理陷阱
thing.name = "boo";
console.log(thing.name); // "boo"
在這個示例中,
target
一開始沒有自有屬性,對象thing
的原型是一個代理,其定義了一個set
陷阱來捕獲任何新屬性的創建。當thing.name
被賦值為"thing"
時,由于name
不是thing
的自有屬性,故set代理陷阱會被調用。在陷阱中,trapTarget
等于target
,receiver
等于thing
。最終該操作會在thing
上創建一個新屬性,很幸運,如果傳入receiver
作為第4個參數,Reflect.set()
就可以實現這個默認行為一旦在
thing
上創建了name
屬性,那么在thing.name
被設置為其他值時不再調用set
代理陷阱,此時name
是一個自有屬性,所以[[Set]]
操作不會繼續在原型上查找
【在原型上使用has陷阱】
回想一下
has
陷阱,它可以攔截對象中的in
操作符。in
操作符先根據給定名稱搜索對象的自有屬性,如果不存在,則沿著原型鏈依次搜索后續對象的自有屬性,直到找到給定的名稱或無更多原型為止
因此,只有在搜索原型鏈上的代理對象時才會調用
has
陷阱,而用代理作為原型時,只有當指定名稱沒有對應的自有屬性時才會調用has
陷阱
let target = {};
let thing = Object.create(new Proxy(target, {
has(trapTarget, key) {
return Reflect.has(trapTarget, key);
}
}));
// 觸發了 `has` 代理陷阱
console.log("name" in thing); // false
thing.name = "thing";// 沒有觸發 `has` 代理陷阱
console.log("name" in thing); // true
- 這段代碼在
thing
的原型上創建了一個has
代理陷阱,由于使用in
操作符時會自動搜索原型,因此這個has
陷阱不像get
陷阱和set
陷阱一樣再傳遞一個receiver
對象,它只操作與target
相等的trapTarget
。在此示例中,第一次使用in
操作符時會調用has
陷阱,因為屬性name
不是thing
的自有屬性;而給thing.name
賦值時會再次使用in
操作符,這一次不會調用has
陷阱,因為name
已經是thing
的自有屬性了,故不會繼續在原型中查找
【將代理用作類的原型】
由于類的
prototype
屬性是不可寫的,因此不能直接修改類來使用代理作為類的原型。然而,可以通過繼承的方法來讓類誤以為自己可以將代理用作自己的原型。首先,需要用構造函數創建一個ES5
風格的類型定義
function NoSuchProperty() {
// empty
}
NoSuchProperty.prototype = new Proxy({}, {
get(trapTarget, key, receiver) {
throw new ReferenceError(`${key} doesn't exist`);
}
});
let thing = new NoSuchProperty();
// 由于 `get` 代理陷阱而拋出了錯誤
let result = thing.name;
NoSuchProperty
表示類將繼承的基類,函數的prototype
屬性沒有限制,于是可以用代理將它重寫。當屬性不存在時會通過get
陷阱來拋出錯誤,thing
對象作為NoSuchProperty
的實例被創建,被訪問的屬性name
不存在于是拋出錯誤下一步是創建一個從
NoSuchProperty
繼承的類
function NoSuchProperty() {
// empty
}
NoSuchProperty.prototype = new Proxy({}, {
get(trapTarget, key, receiver) {
throw new ReferenceError(`${key} doesn't exist`);
}
});
class Square extends NoSuchProperty {
constructor(length, width) {
super();
this.length = length;
this.width = width;
}
}
let shape = new Square(2, 6);
let area1 = shape.length * shape.width;
console.log(area1); // 12
// 由于 "wdth" 不存在而拋出了錯誤
let area2 = shape.length * shape.wdth;
-
Square
類繼承自NoSuchProperty
,所以它的原型鏈中包含代理。之后創建的shape
對象是Square
的新實例,它有兩個自有屬性length
和width
。讀取這兩個屬性的值時不會調用get
代理陷阱,只有當訪問shape
對象上不存在的屬性時(例如shape.wdth
,很明顯這是一個錯誤拼寫)才會觸發get
代理陷阱并拋出一個錯誤。另一方面這也說明代理確實在shape
對象的原型鏈中。但是有一點不太明顯的是,代理不是shape
對象的直接原型,實際上它位于shape
對象的原型鏈中,需要幾個步驟才能到達
function NoSuchProperty() {
// empty
}
// 對于將要用作原型的代理,存儲對其的一個引用
let proxy = new Proxy({}, {
get(trapTarget, key, receiver) {
throw new ReferenceError(`${key} doesn't exist`);
}
});
NoSuchProperty.prototype = proxy;
class Square extends NoSuchProperty {
constructor(length, width) {
super();
this.length = length;
this.width = width;
}
}
let shape = new Square(2, 6);
let shapeProto = Object.getPrototypeOf(shape);
console.log(shapeProto === proxy); // false
let secondLevelProto = Object.getPrototypeOf(shapeProto);
console.log(secondLevelProto === proxy); // true
在這一版代碼中,為了便于后續識別,代理被存儲在變量
proxy
中。shape
的原型Shape.prototype
不是一個代理,但是shape.prototype
的原型是繼承自NoSuchProperty
的代理通過繼承在原型鏈中額外增加另一個步驟非常重要,因為需要經過額外的一步才能觸發代理中的
get
陷阱。如果Shape.prototype
有一個屬性,將會阻止get
代理陷阱被調用
function NoSuchProperty() {
// empty
}
NoSuchProperty.prototype = new Proxy({}, {
get(trapTarget, key, receiver) {
throw new ReferenceError(`${key} doesn't exist`);
}
});
class Square extends NoSuchProperty {
constructor(length, width) {
super();
this.length = length;
this.width = width;
}
getArea() {
return this.length * this.width;
}
}
let shape = new Square(2, 6);
let area1 = shape.length * shape.width;
console.log(area1); // 12
let area2 = shape.getArea();
console.log(area2); // 12
// 由于 "wdth" 不存在而拋出了錯誤
let area3 = shape.length * shape.wdth;
- 在這里,
Square
類有一個getArea()
方法,這個方法被自動地添加到Square.prototype
,所以當調用shape.getArea()
時,會先在shape
實例搜索getArea()
方法然后再繼續在它的原型中搜索。由于getArea()
是在原型中找到的,搜索結束,代理沒有被調用