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以前開發者沒法本身實現這些行爲,如今經過代理能夠實現app
調用new Proxy()可建立代替其餘目標(target)對象的代理,它虛擬化了目標,因此兩者看起來功能一致函數
代理能夠攔截JS引擎內部目標的底層對象操做,這些底層操做被攔截後會觸發響應特定操做的陷阱函數性能
反射API以Reflect對象的形式出現,對象中方法的默認特性與相同的底層操做一致,而代理能夠覆寫這些操做,每一個代理陷阱對應一個命名和參數都相同的Reflect方法。下表總結了代理陷阱的特性優化
每一個陷阱覆寫JS對象的一些內建特性,能夠用它們攔截並修改這些特性。若是仍需使用內建特性,則可使用相應的反射API方法ui
【建立簡單代理】this
用Proxy構造函數建立代理須要傳入兩個參數:目標(target)和處理程序(handler)。處理程序用於定義一個或多個陷阱的對象,在代理中,除了專門爲操做定義的陷阱外,其他操做均使用默認特性。不使用任何陷阱的處理程序等價於簡單的轉發代理spa
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" // 讀取不存在屬性會拋出錯誤 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對象,它同時是代理和目標的實例,由於instanceof經過原型鏈來肯定此信息,而原型鏈查找不受代理影響,這也就是代理和目標好像有相同原型的緣由
驗證函數參數
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。能夠手動用代理給new.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"
此示例中有兩個特別重要的行爲
一、當給colors[3]賦值時,length屬性的值增長到4
二、當length屬性被設置爲2時,數組中最後兩個元素被刪除
要徹底重造內建數組,只需模擬上述兩種行爲。下面=將講解如何建立一個能正確模仿這些行爲的對象
【檢測數組索引】
爲整數屬性鍵賦值是數組纔有的特例,由於它們與非整數鍵的處理方式不一樣。要判斷一個屬性是不是一個數組索引,能夠參考ES6規範提供的如下說明
當且僅當ToString(ToUint32(P))等於P,而且ToUint32(P)不等於232-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"時,length才被設置爲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的原型。如今,僅當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()是在原型中找到的,搜索結束,代理沒有被調用