【ES6】改變 JS 內置行爲的代理與反射

代理(Proxy)能夠攔截並改變 JS 引擎的底層操做,如數據讀取、屬性定義、函數構造等一系列操做。ES6 經過對這些底層內置對象的代理陷阱和反射函數,讓開發者能進一步接近 JS 引擎的能力。

1、代理與反射的基本概念

什麼是代理和反射呢?
代理是用來替代另外一個對象(target),JS 經過new Proxy()建立一個目標對象的代理,該代理與該目標對象表面上能夠被看成同一個對象來對待javascript

當目標對象上的進行一些特定的底層操做時,代理容許你攔截這些操做而且覆寫它,而這本來只是 JS 引擎的內部能力。java

若是你對些代理&反射的概念比較困惑的話, 能夠直接看後面的應用示例,最後再從新看這些定義就會更清晰!

攔截行爲使用了一個可以響應特定操做的函數( 被稱爲陷阱),每一個代理陷阱對應一個反射(Reflect)方法。segmentfault

ES6 的反射 API 以 Reflect 對象的形式出現,對象每一個方法都與對應的陷阱函數同名,而且接收的參數也與之一致。如下是 Reflect 對象的一些方法:數組

代理陷阱 覆寫的特性 方法
get 讀取一個屬性的值 Reflect.get()
set 寫入一個屬性 Reflect.set()
has in 運算符 Reflect.has()
deleteProperty delete 運算符 Reflect.deleteProperty()
getPrototypeOf Object.getPrototypeOf() Reflect.getPrototypeOf()
isExtensible Object.isExtensible() Reflect.isExtensible()
defineProperty Object.defineProperty() Reflect.defineProperty
apply 調用一個函數 Reflect.apply()
construct 使用 new 調用一個函數 Reflect.construct()

每一個陷阱函數均可以重寫 JS 對象的一個特定內置行爲,容許你攔截並修改它。安全

綜合來講,想要控制或改變JS的一些底層操做,能夠先建立一個代理對象,在這個代理對象上掛載一些陷阱函數,陷阱函數裏面有反射方法。經過接下來的應用示例能夠更清晰的明白代理的過程。app

2、開始一個簡單的代理

當你使用 Proxy 構造器來建立一個代理時,須要傳遞兩個參數:目標對象(target)以及一個處理器( handler),函數

建立一個僅進行傳遞的代理以下:測試

// 目標對象
let target = {}; 
// 代理對象
let proxy = new Proxy(target, {});

proxy.name = "hello";
console.log(proxy.name); // "hello"
console.log(target.name); // "hello"

target.name = "world";
console.log(proxy.name); // "world"
console.log(target.name); // "world

上例中的 proxy 代理對象將全部操做直接傳遞給 target 目標對象,代理對象 proxy 自身並無存儲該屬性,它只是簡單將值傳遞給 target 對象,proxy.name 與 target.name 的屬性值老是相等,由於它們都指向 target.name。this

此時代理陷阱的處理器爲空對象,固然處理器能夠定義了一個或多個陷阱函數。雙向綁定

2.1 set 驗證對象屬性的存儲

假設你想要建立一個對象,並要求其屬性值只能是數值,這就意味着該對象的每一個新增屬性
都要被驗證,而且在屬性值不爲數值類型時應當拋出錯誤。

這時須要使用 set 陷阱函數來攔截傳入的 value,該陷阱函數能接受四個參數:

  • trapTarget :將接收屬性的對象( 即代理的目標對象)
  • key :須要寫入的屬性的鍵( 字符串類型或符號類型)
  • value :將被寫入屬性的值;
  • receiver :操做發生的對象( 一般是代理對象)

set 陷阱對應的反射方法和默認特性是Reflect.set(),和陷阱函數同樣接受這四個參數,並會基於操做是否成功而返回相應的結果:

let targetObj = {};
let proxyObj = new Proxy(targetObj, {
  set: set
});

/* 定義 set 陷阱函數 */
function set (trapTarget, key, value, receiver) {
  if (isNaN(value)) {
     throw new TypeError("Property " + key + " must be a number.");
  }
  return Reflect.set(trapTarget, key, value, receiver);
}

/* 測試 */
proxyObj.count = 123;
console.log(proxyObj.count); // 123
console.log(targetObj.count); // 123

proxyObj.anotherName = "proxy" // TypeError: Property anotherName must be a number.

示例中set 陷阱函數成功攔截傳入的 value 值,你能夠嘗試一下,若是註釋或不return Reflect.set()會發生什麼?,答案是攔截陷阱就不會有反射響應。

須要注意的是,直接給 targetObj 目標對象賦值時是不會觸發 set 代理陷阱的,須要經過給代理對象賦值纔會觸發 set 代理陷阱與反射。

2.2 get 驗證對象屬性的讀取

JS 很是有趣的特性之一,是讀取不存在的屬性時並不會拋出錯誤,而會把undefined看成該屬性的值。

對於大型的代碼庫,當屬性名稱存在書寫錯誤時(不會拋錯)會致使嚴重的問題。這時使用 get 代理陷阱驗證對象結構(Object Shape),訪問不存在的屬性時就拋出錯誤,使對象結構驗證變得簡單。

get 陷阱函數會在讀取屬性時被調用,即便該屬性在對象中並不存在,它能接受三個參數:

  • trapTarget :將會被讀取屬性的對象( 即代理的目標對象)
  • key :須要讀取的屬性的鍵( 字符串類型或符號類型)
  • receiver :操做發生的對象( 一般是代理對象)

Reflect.get()方法接受與之相同的參數,並返回默認屬性的默認值。

let proxyObj = new Proxy(targetObj, {
  set: set,
  get: get
});

/* 定義 get 陷阱函數 */
function get(trapTarget, key, receiver) {
  if (!(key in receiver)) {
    throw new TypeError("Property " + key + " doesn't exist.");
  }
  return Reflect.get(trapTarget, key, receiver);
}

console.log(proxyObj.count); // 123
console.log(proxyObj.newcount) // TypeError: Property newcount doesn't exist.

這段代碼容許添加新的屬性,而且此後能夠正常讀取該屬性的值,但當讀取的屬性並
不存在時,程序拋出了一個錯誤,而不是將其默認爲undefined

還可使用 has 陷阱驗證in運算符,使用 deleteProperty 陷阱函數避免屬性被delete刪除。

注:in運算符用於判斷對象中是否存在某個屬性,若是自有屬性或原型屬性匹配這個名稱字符串或Symbol,那麼in運算符返回 true。

targetObj = {
  name: 'targetObject'
};
console.log("name" in targetObj); // true
console.log("toString" in targetObj); // true

其中 name 是對象自身的屬性,而 toString 則是原型屬性( 從 Object 對象上繼承而來),因此檢測結果都爲 true。

has 陷阱函數會在使用in運算符時被調用,而且會傳入兩個參數(同名反射Reflect.has()方法也同樣):

  • trapTarget :須要讀取屬性的對象( 代理的目標對象)
  • key :須要檢查的屬性的鍵( 字符串類型或 Symbol符號類型)

deleteProperty 陷阱函數會在使用delete運算符去刪除對象屬性時下被調用,而且也會被傳入兩個參數(Reflect.deleteProperty() 方法也接受這兩個參數):

  • trapTarget :須要刪除屬性的對象( 即代理的目標對象) ;
  • key :須要刪除的屬性的鍵( 字符串類型或符號類型) 。
一些思考:分析過 Vue 源碼的都瞭解過,給一個 Vue 實例中掛載的 data,是經過 Object.defineProperty代理 vm._data 中的對象屬性,實現雙向綁定...... 同理能夠考慮使用 ES6 的 Proxy 的 get 和 set 陷阱實現這個代理。

3、對象屬性陷阱

3.1 數據屬性與訪問器屬性

ES5 最重要的特徵之一就是引入了 Object.defineProperty() 方法定義屬性的特性。屬性的特性是爲了實現javascript引擎用的,屬於內部值,所以不能直接訪問他們。

屬性分爲數據屬性和訪問器屬性。使用Object.defineProperty()方法修改數據屬性的特性值的示例以下:

let obj1 = {
  name: 'myobj',
}
/* 數據屬性*/
Object.defineProperty(obj1,'name',{
  configurable: false, // default true
  writable: false,     // default true
  enumerable: true,    // default true
  value: 'jenny'       // default undefined
})
console.log(obj1.name) // 'jenny'

其中[[Configurable]] 表示可否經過 delete 刪除屬性從而從新定義爲訪問器屬性;[[Enumerable]] 表示可否經過for-in循環返回屬性;[[Writable]] 表示可否修改屬性的值; [[Value]] 包含這個屬性的數據值。

對於訪問器屬性,該屬性不包含數據值,包含一對getter和setter函數,定義訪問器屬性必須使用Object.defineProperty()方法:

let obj2 = {
  age: 18
}
/* 訪問器屬性 */
Object.defineProperty(obj2,'_age',{
  configurable: false, // default true
  enumerable: false,   // default true
  get () {             // default undefined
    return this.age
  },
  set (num) {          // default undefined
    this.age = num
  }
})
/* 修改訪問器屬性調用 getter */
obj2._age = 20  
console.log(obj2.age)  // 20

/* 輸出訪問器屬性 */
console.log(Object.getOwnPropertyDescriptor(obj2,'_age')) 
// { get: [Function: get],
//   set: [Function: set],
//   enumerable: false,
//   configurable: false }

[[Get]] 在讀取屬性時調用的函數, [[Set]] 再寫入屬性時調用的函數。使用訪問器屬性的經常使用方式,是設置一個屬性的值致使其餘屬性發生變化。

3.2 檢查屬性的修改

代理容許你使用 defineProperty 同名函數陷阱函數攔截Object.defineProperty()的調用,defineProperty 陷阱函數接受下列三個參數:

  • trapTarget :須要被定義屬性的對象( 即代理的目標對象);
  • key :屬性的鍵( 字符串類型或符號類型);
  • descriptor :爲該屬性準備的描述符對象。

defineProperty 陷阱函數要求在操做後返回一個布爾值用於判斷操做是否成功,若是返回了 false 則拋出錯誤,故可使用該功能來限制哪些屬性能夠被Object.defineProperty() 方法定義。

例如,若是想阻止定義Symbol符號類型的屬性,你能夠檢查傳入的屬性值,如果則返回 false:

/* 定義代理 */
let proxy = new Proxy({}, {
  defineProperty(trapTarget, key, descriptor) {
    if (typeof key === "symbol") {
      return false;
    }
    return Reflect.defineProperty(trapTarget, key, descriptor);
  }
});

Object.defineProperty(proxy, "name", {
  value: "proxy"
});
console.log(proxy.name); // "proxy"

let nameSymbol = Symbol("name");
// 拋出錯誤
Object.defineProperty(proxy, nameSymbol, {
  value: "proxy"
})

4、函數代理

4.1 構造函數 & 當即執行

函數的兩個內部方法:[[Call]][[Construct]]會在函數被調用時調用,經過代理函數來爲這兩個內部方法設置陷阱,從而控制函數的行爲。

[[Construct]]會在函數被使用new運算符調用時執行,代理觸發construct()陷阱函數,並和Reflect.construct()同樣接收到下列兩個參數:

  • trapTarget :被執行的函數( 即代理的目標對象) ;
  • argumentsList :被傳遞給函數的參數數組。

[[Call]]會在函數被直接調用時執行,代理觸發apply()陷阱函數,它和Reflect.apply()都接收三個參數:

  • trapTarget :被執行的函數( 代理的目標函數) ;
  • thisArg :調用過程當中函數內部的 this 值;
  • argumentsList :被傳遞給函數的參數數組。
每一個函數都包含 call()apply()方法,用於重置函數運行的做用域即 this 指向,區別只是接收參數的方式不一樣: call()的參數須要逐個列舉、 apply()是參數數組。

顯然,apply 與 construct 要求代理目標對象必須是一個函數,這兩個代理陷阱在函數的執行方式上開啓了不少的可能性,結合使用就能夠徹底控制任意的代理目標函數的行爲。

4.2 驗證函數的參數

看到apply()construct()陷阱的參數都有被傳遞給函數的參數數組argumentsList,因此能夠用來驗證函數的參數。

例如須要保證全部參數都是某個特定類型的,而且不能經過 new 構造使用,示例以下:

/* 定義 sum 目標函數 */
function sum(...values) {
  return values.reduce((previous, current) => previous + current, 0);
}
/* 定義 apply 陷阱函數 */
function applyRef (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 constructRef () {
  throw new TypeError("This function can't be called with new.");
}
/* 定義 sumProxy 代理函數 */
let sumProxy = new Proxy(sum, {
  apply: applyRef,
  construct: constructRef
});

console.log(sumProxy(1, 2, 3, 4)); // 10

// console.log(sumProxy(1, "2", 3, 4)); // TypeError: All arguments must be numbers.
// let result = new sumProxy() // TypeError: This function can't be called with new.

sum() 函數會將全部傳遞進來的參數值相加,此代碼經過將 sum() 函數封裝在 sumProxy() 代理中,若是傳入參數的值不是數值類型,該函數仍然會嘗試加法操做,但在函數運行以前攔截了函數調用,觸發apply陷阱函數以保證每一個參數都是數值。

出於安全的考慮,這段代碼使用 construct 陷阱拋出錯誤,以確保該函數不會被使用 new 運算符調用

實例對象 instance 對象會被同時斷定爲 proxy 與 target 對象的實例,是由於 instanceof 運算符使用了原型鏈來進行推斷,而原型鏈查找並無受到這個代理的影響,所以 proxy 對象與 target 對象對於 JS 引擎來講就有同一個原型。

4.3 調用類的構造函數

ES6 中新引入了class類的概念,類使用constructor構造函數封裝數據,並規定必須始終使用 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("Jenny");
console.log(me.name); // "Jenny"
console.log(me instanceof Person); // true
console.log(me instanceof PersonProxy); // true

類構造器即類的構造函數,使用代理時它的行爲就像函數同樣,apply陷阱函數重寫了默認的構造行爲。

關於類的更多有趣的用法,可參考 【ES6】更易於繼承的類語法

總結來講,代理的用途很是普遍,由於它提供了修改 JS 內置對象的全部行爲的入口。上述例子只是簡單的一些應用入門,還有更多複雜的示例,推薦閱讀《深刻理解ES6》。

繼續加油鴨少年!!!

相關文章
相關標籤/搜索