ES6(十五):代理(Proxy)和反射(Reflection)

前面的話


??ES5ES6致力于為開發者提供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引擎內部目標的底層對象操作,這些底層操作被攔截后會觸發響應特定操作的陷阱函數

反射APIReflect對象的形式出現,對象中方法的默認特性與相同的底層操作一致,而代理可以覆寫這些操作,每個代理陷阱對應一個命名和參數都相同的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.nametarget.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,而對于nametostring則正確返回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, {});
  • 這段代碼強調了targetproxy的行為差異。Object.getPrototypeOf()target返回的是值,而給proxy返回值時,由于getPrototypeOf陷阱被調用,返回的是null;同樣,Object.setPrototypeOf()成功為target設置原型,而給proxy設置原型時,由于setPrototypeOf陷阱被調用,最終拋出一個錯誤

  • 如果使用這兩個陷阱的默認行為,則可以使用Reflect上的相應方法。例如,下面的代碼實現了getPrototypeOfsetPrototypeOf陷阱的默認行為

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陷阱僅使用了默認行為,因此可以交換使用targetparo×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。這種微妙的差異非常重要,在objectReflect上還有更多看似重復的方法,但是在所有代理陷阱中一定要使用Reflect上的方法

【對象可擴展性陷阱】

ES5已經通過Object.preventExtensions()方法和Object.isExtensible()方法修正了對象的可擴展性,ES6可以通過代理中的preventExtensionsisExtensible陷阱攔截這兩個方法并調用底層對象。兩個陷阱都接受唯一參數trapTarget對象,并調用它上面的方法。isExtensible陷阱返回的一定是一個布爾值,表示對象是否可擴展;preventExtensions陷阱返回的也一定是布爾值,表示操作是否成功

Reflect.preventExtensions()方法和 Reflect.IsExtensible()方法實現相應陷阱中默認行為,二者都返回布爾值

兩個基礎示例

以下這段代碼是對象可擴展性陷阱的實際應用,實現了isExtensiblepreventExtensions陷阱的默認行為

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,否則返回falsegetOwnPropertyDescriptor陷阱只接受trapTargetkey兩個參數,最終返回描述符。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

keySymbol類型時defineProperty代理陷阱返回false,否則執行默認行為。調用Object.defineProperty()并傳入"name",因此鍵的類型是字符串所以方法成功執行;調用Object.defineProperty()方法并傳入nameSymbol,defineProperty陷阱返回false所以拋出錯誤

[注意]如果讓陷阱返回true并且不調用Reflect.defineProperty()方法,則可以讓Object.defineProperty()方法靜默失效,這既消除了錯誤又不會真正定義屬性

描述符對象限制

為確保Object.defineProperty()方法和Object.getOwnPropertyDescriptor()方法的行為一致,傳入defineProperty陷阱的描述符對象已規范化。從getOwnPropertyDescriptor陷阱返回的對象由于相同原因被驗證

無論將什么對象作為第三個參數傳遞給Object.defineProperty()方法,都只有屬性enumerableconfigurablevalue、writable、getset將出現在傳遞給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、configurablevalue、writable、getset,在返回的對象中使用不被允許的屬性會拋出一個錯誤

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、_namenameSymbol。調用Object.getOwnPropertyNames()Object.Keys()時傳入proxy, 只返回name屬性;同樣,調用Object.getOwnPropertySymbols()時傳入proxy,只返回nameSymbol。由于_name屬性被過濾掉了,因此它不出現在這兩次結果中

  • 盡管ownKeys代理陷阱可以修改一小部分操作返回的鍵,但不影響更常用的操作,例如for-of循環和Object.keys()方法,這些不能使用代理來更改。ownKeys陷阱也會影響for-in 循環,當確定循環內部使用的鍵時會調用陷阱

【函數代理中的apply和construct陷阱】

所有代理陷阱中,只有applyconstruct的代理目標是一個函數。函數有兩個內部方法[[Call]][[Construct]]apply陷阱和construct陷阱可以覆寫這些內部方法。若使用new操作符調用函數,則執行[[Construct]]方法;若不用,則執行[[Construct]]方法,此時會執行apply陷阱,它和Reflect.apply()都接受以下參數

trapTaqget 被執行的函數(代理的目標)
thisArg 函數被調用時內部this的值
argumentsList 傳遞給函數的參數數組
  • 當使用new調用函數時調用的construct陷阱接受以下參數
trapTarget 被執行的函數(代理的目標)
argumentsList 傳遞給函數的參數數組
  • Reflect.construct()方法也接受這兩個參數,其還有一個可選的第三個參數newTarget。若給定這個參數,則該參數用于指定函數內部new.target的值
  • 有了applyconstruct陷阱,可以完全控制任何代理目標函數的行為
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??梢允謩佑么斫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);
  • 此示例創建一個可撤銷代理,它使用解構功能將proxyrevoke變量賦值給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,這種拋出錯誤的特性(其他語言中的做法)非常有用

  • 要明白,在這個示例中,理解trapTargetreceiver是不同的對象很重要。當代理被用作原型時,trapTarget是原型對象,receiver是實例對象。在這種情況下,trapTargettarget相等,receiverthing相等,所以可以訪問代理的原始目標和要操作的目標

【在原型上使用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的新實例,它有兩個自有屬性lengthwidth。讀取這兩個屬性的值時不會調用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()是在原型中找到的,搜索結束,代理沒有被調用

其他章節

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。