ES6躬行記(24)——代理和反射

  代理和反射是ES6新增的兩個特性,二者之間是協調合做的關係,它們的具體功能將在接下來的章節中分別講解。編程

1、代理

  ES6引入代理(Proxy)地目的是攔截對象的內置操做,注入自定義的邏輯,改變對象的默認行爲。也就是說,將某些JavaScript內部的操做暴露了出來,給予開發人員更多的權限。這實際上是一種元編程(metaprogramming)的能力,即把代碼當作數據,對代碼進行編程,改變代碼的行爲。數組

  在ES6中,代理是一種特殊的對象,若是要使用,須要像下面這樣先生成一個Proxy實例。app

new Proxy(target, handler);

  構造函數Proxy()有兩個參數,其中target是要用代理封裝的目標對象,handler也是一個對象,它的方法被稱爲陷阱(trap),用於指定攔截後的行爲。下面是一個代理的簡單示例。函數

var obj = {},
  handler = {
    set(target, property, value, receiver) {
      target[property] = "hello " + value;
    }
  },
  p = new Proxy(obj, handler);
p.name = "strick";
console.log(p.name);        //"hello strick"

  在上面的代碼中,p是一個Proxy實例,它的目標對象是obj,使用了屬性相關的陷阱:set()方法。當它寫入obj的name屬性時,會對其進行攔截,在屬性值以前加上「hello 」前綴。除了上例使用的set()方法,ES6還給出了另外12種可用的陷阱,在後面的章節中會對它們作簡單的介紹。this

1)陷阱spa

  表12羅列了目前全部可用的陷阱,第二列表示當前陷阱可攔截的行爲,注意,只挑選了其中的幾個用於展現。prototype

表12  十三種陷阱代理

陷阱 攔截 返回值
 get()  讀取屬性  任意值
 set()  寫入屬性  布爾值
 has()  in運算符  布爾值
 deleteProperty()  delete運算符  布爾值
 getOwnPropertyDescriptor()  Object.getOwnPropertyDescriptor()  屬性描述符對象
 defineProperty()  Object.defineProperty()  布爾值
 preventExtensions()  Object.preventExtensions()  布爾值
 isExtensible()  Object.isExtensible()  布爾值
 getPrototypeOf()

 Object.getPrototypeOf()              __proto__code

 Object.prototype.isPrototypeOf()  instanceof對象

 對象 
 setPrototypeOf()  Object.setPrototypeOf()   布爾值
 apply()

 Function.prototype.apply()           函數調用

 Function.prototype.call()

 任意值
 construct()  new運算符做用於構造函數  對象
 ownKeys()

 Object.getOwnPropertyNames()    Object.keys()

 Object.getOwnPropertySymbols()  for-in循環

 數組

  目前支持的攔截就上面幾種,像typeof運算符、全等比較等操做還不被ES6支持。接下來會挑選其中的兩次個陷阱,講解它們的簡單應用。

  在JavaScript中,當讀取對象上不存在的屬性時,不會報錯而是返回undefined,這其實在某些狀況下會發生歧義,如今利用陷阱中的get()方法就能改變默認行爲,以下所示。

var obj = {
    name: "strick"    
  },
  handler = {
    get(target, property, receiver) {
      if(property in target)
        return target[property];
      throw "未定義的錯誤";  
    }
  },
  p = new Proxy(obj, handler);
p.name;          //"strick"
p.age;            //未定義的錯誤

  在get()方法中有3個參數,target是目標對象(即obj),property是讀取的屬性的名稱(即「name」和「age」),receiver是當前的Proxy實例(即p)。在讀取屬性時,會用in運算符判斷當前屬性是否存在,若是存在就返回相應的屬性值,不然就會拋出錯誤,這樣就能避免歧義的出現。   

  在衆多陷阱中,只有apply()和construct()的目標對象得是函數。以apply()方法爲例,它有3個參數,target是目標函數,thisArg是this的指向,argumentsList是函數的參數序列,它的具體使用以下所示。

function getName(name) {
  return name;
}
var obj = {
    prefix: "hello "    
  },
  handler = {
    apply(target, thisArg, argumentsList) {
      if(thisArg && thisArg.prefix)
        return target(thisArg.prefix + argumentsList[0]);
      return target(...argumentsList);
    }
  },
  p = new Proxy(getName, handler);
p("strick");                //"strick"
p.call(obj, "strick");        //"hello strick"

  p是一個Proxy實例,p("strick")是一次普通的函數調用,此時雖然攔截了,可是仍然會把參數原樣傳過去;而p.call(obj, "strick")是間接的函數調用,此時會給第一個參數添加前綴,從而改變函數最終的返回值。

2)撤銷代理

  Proxy.revocable()方法可以建立一個可撤銷的代理,它能接收兩個參數,其含義與構造函數Proxy()中的相同,但返回值是一個對象,包含兩個屬性,以下所列。

(1)proxy:新生成的Proxy實例。

(2)revoke:撤銷函數,它沒有參數,能把與它一塊兒生成的Proxy實例撤銷掉。

  下面是一個簡單的示例,obj是目標對象,handler是陷阱對象,傳遞給Proxy.revocable()後,經過對象解構將返回值賦給了proxy和revoke兩個變量。

var obj = {},
  handler = {};
let {proxy, revoke} = Proxy.revocable(obj, handler);
revoke();
delete proxy.name;        //類型錯誤
typeof proxy;             //"object"

  在調用revoke()函數後,就不能再對proxy進行攔截了。像上例使用delete運算符,就會拋出類型錯誤,但像typeof之類的不可攔截的運算符仍是能夠成功執行的。

3)原型

  代理能夠成爲其它對象的原型,就像下面這樣。

var obj = {
    name: "strick"    
  },
  handler = {
    get(target, property, receiver) {
      if(property == "name")
        return "hello " + target[property];
      return true;
    }
  },
  p = new Proxy({}, handler);
Object.setPrototypeOf(obj, p);     //obj的原型指向Proxy實例
obj.name;                      //"strick"
obj.age;                       //true

  p是一個Proxy實例,它會攔截屬性的讀取操做,obj的原型指向了p,注意,p的目標對象不是obj。當obj讀取name屬性時,不會觸發攔截,由於name是自有屬性,因此不會去原型上查找,最終獲得的結果是沒有前綴的「strick」。以前的代理都是直接做用於相關對象(例如上面的obj),所以只要執行可攔截的動做就會被處理,但如今中間隔了個原型,有了更多的限制。而在讀取age屬性時,因爲自有屬性中沒有它,所以就會去原型上查找,從而觸發了攔截操做,返回了true。

2、反射

  反射(Reflect)向外界暴露了一些底層操做的默認行爲,它是一個沒有構造函數的內置對象,相似於Math對象,其全部方法都是靜態的。代理中的每一個陷阱都會對應一個同名的反射方法(例如Reflect.set()、Reflect.ownKeys()等),而每一個反射方法又都會關聯到對應代理所攔截的行爲(例如in運算符、Object.defineProperty()等),這樣就能保證某個操做的默認行爲可隨時被訪問到。反射讓對象的內置行爲變得更加嚴謹、合理與便捷,具體表現以下所列。

  (1)參數的檢驗更爲嚴格,Object的getPrototypeOf()、isExtensible()等方法會將非對象的參數自動轉換成相應的對象(例如字符串轉換成String對象,以下代碼所示),而關聯的反射方法卻不會這麼作,它會直接拋出類型錯誤。

Object.getPrototypeOf("strick") === String.prototype;     //true
Reflect.getPrototypeOf("strick");                         //類型錯誤

  (2)更合理的返回值,Object.setPrototypeOf()會返回它的第一個參數,而Reflect的同名方法會返回一個布爾值,後者能更直觀的反饋設置是否成功,兩個方法的對好比下所示。

var obj = {};
Object.setPrototypeOf(obj, String) === obj;                //true
Reflect.setPrototypeOf(obj, String);                       //true

  (3)用方法替代運算符,反射能以調用方法的形式完成new、in、delete等運算符的功能,在下面的示例中,先使用運算符,再給出對應的反射方法。

function func() { }
new func();
Reflect.construct(func, []);

var people = {
    name: "strick"
};
"name" in people;
Reflect.has(people, "name");

delete people["name"];
Reflect.deleteProperty(people, "name");

  (4)避免冗長的方法調用,以apply()方法爲例,以下所示。

Function.prototype.apply.call(Math.ceil, null, [2.5]);        //3
Reflect.apply(Math.ceil, null, [2.5]);                      //3

  上面代碼的第一條語句比較繞,須要將其分解成兩部分:Function.prototype.apply()和call()。ES5規定apply()和call()兩個方法在最後都要調用一個有特殊功能的內部函數,以下代碼所示,func參數表示調用這兩個方法的函數。

[[Call]](func, thisArg, argList)

  內部函數的功能就是在調用func()函數時,傳遞給它的參數序列是argList,其內部的this指向了thisArg。當執行第一條語句時,傳遞給[[Call]]函數的三個參數以下所示。

[[Call]](Function.prototype.apply, Math.ceil, [null, [2.5]])

  接下來會調用原型上的apply()方法,因爲其this指向了Math.ceil(即當前調用apply()方法的是Math.ceil),所以[[Call]]函數的第一個參數就是Math.ceil,以下所示。

[[Call]](Math.ceil, null, [2.5])
//至關於
Math.ceil.apply(null, [2.5])
相關文章
相關標籤/搜索