1. 程式人生 > >ES6躬行記(24)——代理和反射

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

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

一、代理

  ES6引入代理(Proxy)地目的是攔截物件的內建操作,注入自定義的邏輯,改變物件的預設行為。也就是說,將某些JavaScript內部的操作暴露了出來,給予開發人員更多的許可權。這其實是一種超程式設計(metaprogramming)的能力,即把程式碼看成資料,對程式碼進行程式設計,改變程式碼的行為。

  在ES6中,代理是一種特殊的物件,如果要使用,需要像下面這樣先生成一個Proxy例項。

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種可用的陷阱,在後面的章節中會對它們做簡單的介紹。

1)陷阱

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

表12  十三種陷阱

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

 Object.getPrototypeOf()              __proto__

 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。

二、反射

  反射(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])

&n