1. 程式人生 > >深度揭祕ES6代理Proxy

深度揭祕ES6代理Proxy

最近在部落格上看到關於ES6代理的文章都是一些關於如何使用Proxy的例子,很少有說明Proxy原理的文章,要知道只有真正掌握了一項技術的原理,才能夠寫出精妙絕倫的程式碼,所以我覺得有必要寫一篇關於深刻揭露ES6 Proxy的文章。

看完這篇文章你不會學到一些大型的使用Proxy的例子,但是你可以瞭解以下幾方面的內容:

  • 你將知道代理是個什麼東西
  • 你將知道所有代理物件可覆蓋的方法
  • 一些代理物件使用的小場景
什麼是代理?剛開始入行的同學可能就會問了。我覺得回答這個問題之前我們應該先弄清楚另一個問題:什麼是物件?有人會回答:物件是屬性的集合。好吧,這種回答的確找不出什麼bug,但是卻太抽象了,ES委員會為物件定義了一個由14種方法組成的集合,它是適用於所有物件的通用介面,你可以呼叫、刪除或覆寫普通方法,但是無法操作內部方法。
 下面讓我們來看一下這14個方法,[[]]代表內部方法,在一般程式碼中不可見。
  • [[GetPrototypeOf]] ( )
    獲取物件的原型時呼叫,在執行obj[__proto__]或Object.getPrototypeOf(obj)時呼叫。
  • [[SetPrototypeOf]] (V)
    設定一個物件的原型時呼叫,在執行obj.prototype=otherObj或則Object.SetPrototypeOf(v)的時候呼叫
  • [[IsExtensible]] ( )
    獲取物件的可擴充套件性時呼叫,執行Object.isExtensible(object)時被呼叫。
  • [[GetOwnProperty]] (P)
    獲取自有屬性時呼叫
  • [[PreventExtensions]]()

擴充套件一個不可擴充套件的物件時呼叫

  • [[DefineOwnProperty]] (P, Desc)
    定義自有屬性時呼叫
  • [[HasProperty]](P)
    檢測物件是否存在某個屬性時呼叫,如key in obj
  • [[Get]] (P, Receiver)
    獲取屬性時呼叫,如obj.key,obj[key]。
  • [[Set]] ( P, V, Receiver)
    為物件的屬性賦值時呼叫,如obj.key=value或obj[key]=value。
  • [[Delete]] (P)
    刪除某個屬性時呼叫
  • [[Enumerate]] ()
    列舉物件的可列舉屬性時呼叫,如for (var key in obj)。
  • [[OwnPropertyKeys]] ( )
    列舉物件的自有屬性時呼叫
  • functionObj.[[Call]](thisValue, arguments)
呼叫一個函式時被呼叫,functionObj()或者x.method()。
  • constructorObj.[[Construct]](arguments, newTarget)
使用new操作的時候呼叫,如new Date()。
在整個 ES6 標準中,只要有可能,任何語法或物件相關的內建函式都是基於這14種內部方法構建的 。但是我們不必記住這些物件的內建屬性,我們更應關注是handler與之相對應的方法。 現在我們已經清楚了物件的14個內建方法,那麼我們再回到第一個問題:什麼是代理?我們可以這樣說,代理Proxy是一個建構函式,它可以接受兩個引數:目標物件(target) 與控制代碼物件(handler ,返回一個代理物件Proxy,主要用於從外部控制對物件內部的訪問。
var target = {}, handler = {};
var proxy = new Proxy(target, handler);
那麼Proxy、target、handler這三者之間有什麼關係呢?Proxy的行為很簡單:將Proxy的所有內部方法轉發至target 。即呼叫Proxy的方法就會呼叫target上對應的方法。那麼handler又是用來幹嘛的?handler的方法可以覆寫任意代理的內部方法。 外界每次通過Proxy訪問target 物件的屬性時,就會經過 handler 物件,因此,我們可以通過重寫handler物件中的一些方法來做一些攔截的操作。以下是一個簡單的代理使用例子。
var person = {
	name: "Jhon",
	age: 23
};
var p = new Proxy(person,{
	get: function(target, prop, receiver){
		console.log("你訪問了person的屬性");
		return target["name"];
	}
});
console.log(p.age);
// 你訪問了person的屬性 
// Jhon
你看,我們雖然訪問了age屬性,它卻輸出了額外的字串和屬性name的值,這就是攔截器的作用。 對應物件內建的14個方法,handler也有14個方法可以覆蓋,下面我們將會一一講解。 1、handler.get() 方法用於攔截物件的讀取屬性操作。
var p = new Proxy(target, {
  get: function(target, property, receiver) {
  }
});
  • target,目標物件
  • property,被獲取的屬性名
  • receiver,Proxy或者繼承Proxy的物件
  • 可以返回任何值
  • this繫結到handler物件上
約束條件,proxy會丟擲 
  • 如果要訪問的目標屬性是不可寫以及不可配置的,則返回的值必須與該目標屬性的值相同。
  • 如果要訪問的目標屬性沒有配置訪問方法,即get方法是undefined的,則返回值必須為undefined。
2、handler.set() 方法用於攔截設定屬性值的操作
var p = new Proxy(target, {
  set: function(target, property, value, receiver) {
  }
});
  • target,目標物件
  • property,被設定的屬性名
  • value,被設定的新值
  • receiver,最初被呼叫的物件。通常是proxy本身,但handler的set方法也有可能在原型鏈上或以其他方式被間接地呼叫(因此不一定是proxy本身)
  • this繫結到handler物件上
  • 返回一個布林值,返回true代表此次設定屬性成功了,如果返回false且設定屬性操作發生在嚴格模式下,那麼會丟擲一個TypeError
約束,違背proxy會丟擲一個TypeError:
  • 若目標屬性是不可寫及不可配置的,則不能改變它的值
  • 如果目標屬性沒有配置儲存方法,即set方法是undefined的,則不能設定它的值
  • 在嚴格模式下,若set方法返回false,則會丟擲一個 TypeError 異常
3、handler.has() 方法可以看作是針對 in 操作的鉤子
var p = new Proxy(target, {
  has: function(target, prop) {
  }
});
  • targe,t目標物件
  • prop,需要檢查是否存在的屬性
  • this繫結到handler物件上
  • 返回一個boolean值
約束:
  • 如果目標物件的某一屬性本身不可被配置,則該屬性不能夠被代理隱藏
  • 如果目標物件為不可擴充套件物件,則該物件的屬性不能夠被代理隱藏
4、handler.getPrototypeOf() 是一個代理方法,當讀取代理物件的原型時,該方法就會被呼叫
var p = new Proxy(obj, {
  getPrototypeOf(target) {
  ...
  }
});
  • target,被代理的目標物件
  • this 指向的是它所屬的處理器物件
  • 必須返回一個物件值或者返回 null
約束:
  • getPrototypeOf() 方法返回的不是物件也不是 null
  • 目標物件是不可擴充套件的,且 getPrototypeOf() 方法返回的原型不是目標物件本身的原型
5、handler.defineProperty() 用於攔截對物件的 Object.defineProperty() 操作
var p = new Proxy(target, {
  defineProperty: function(target, property, descriptor) {
  }
});
  • target,目標物件
  • property,待檢索其描述的屬性名
  • descriptor,待定義或修改的屬性的描述符
  • this 繫結在 handler 物件上
  • 返回一個boolean值
約束:
  • 如果目標物件不可擴充套件, 將不能新增屬性、
  • 不能新增或者修改一個屬性為不可配置的,如果它不作為一個目標物件的不可配置的屬性存在的話
  • 如果目標物件存在一個對應的可配置屬性,這個屬性可能不會是不可配置的
  • 如果一個屬性在目標物件中存在對應的屬性,那麼 Object.defineProperty(target, prop, descriptor) 將不會丟擲異常
  • 在嚴格模式下, false 作為 handler.defineProperty 方法的返回值的話將會丟擲 TypeError 異
6、handler.deleteProperty() 方法用於攔截對物件屬性的 delete 操作
var p = new Proxy(target, {
  deleteProperty: function(target, property) {
  }
});
  • target,目標物件
  • property,待刪除的屬性名
  • this 被繫結在 handler上
  • 返回一個boolean值
約束:
  • 如果目標物件的屬性是不可配置的,那麼該屬性不能被刪除
7、handler.setPrototypeOf() 用於攔截對物件的 Object.setPrototypeOf() 操作
var p = new Proxy(target, {
  setPrototypeOf: function(target, prototype) {
  }
});
  • target,目標物件
  • prototype,待設定的屬性名或者null
  • this 被繫結在 handler上
  • 返回一個boolean值
約束:
  • 如果目標物件是不可擴充套件的,則原型的引數必須和Object.getPrototypeOf(target)相同
var p = new Proxy(target, {
  getOwnPropertyDescriptor: function(target, prop) {
  }
});
  • target,目標物件
  • prop,返回屬性名的描述符
  • this繫結到處理函式
  • 必須返回一個 物件 或 undefined
約束:
  • getOwnPropertyDescriptor 必須返回一個 object 或 undefined
  • 如果屬性作為目標物件的不可配置的屬性存在,則該屬性無法報告為不存在
  • 如果屬性作為目標物件的屬性存在,並且目標物件不可擴充套件,則該屬性無法報告為不存在
  • 如果屬性不存在作為目標物件的屬性,並且目標物件不可擴充套件,則不能將其報告為存在
  • 屬性不能被報告為不可配置,如果它不作為目標物件的自身屬性存在,或者作為目標物件的可配置的屬性存在
  • Object.getOwnPropertyDescriptor(target)的結果可以使用 Object.defineProperty 應用於目標物件,也不會丟擲異常
var p = new Proxy(target, {
  ownKeys: function(target) {
  }
});
  • target,目標物件
  • this繫結到處理函式
  • 返回一個數組
約束:
  • 返回值必須是一個數組
  • 陣列的每一個元素必須是字串或者Symbol
  • 陣列必須包含目標物件的所有非可配置屬性的鍵
  • 如果目標物件不可擴充套件,則陣列必須包含目標物件自身屬性的所有鍵,並且沒有其他值
var p = new Proxy(target, {
  isExtensible: function(target) {
  }
});
  • target,目標物件
  • this繫結到處理函式
  • 返回一個boolean值
約束:
  • Object.isExtensible(proxy) 必須返回和Object.isExtensible(target)相同的值
11、handler.apply() 方法用於攔截函式的呼叫
var p = new Proxy(target, {
  apply: function(target, thisArg, argumentsList) {
  }
});
  • target,目標物件(函式)
  • thisArg,被呼叫時的上下文物件
  • argumentsList,被呼叫時的引數列表
  • this繫結到handler物件
  • 可以返回任何值
約束
12、handler.construct()用於來接new操作
var p = new Proxy(target, {
  construct: function(target, argumentsList, newTarget) {
  }
});
  • target,目標物件
  • argumensList,構造器引數列表
  • newTarget,最初呼叫的建構函式
  • this繫結到handler
  • 返回一個物件
約束:
  • 返回值必須是一個物件
var p = new Proxy(target, {
  preventExtensions: function(target) {
  }
});
  • target,目標物件
  • this繫結到handler物件
  • 返回一個boolean值
約束:
  • Object.isExtensible(proxy)為假,則Object.preventExtensions(proxy)只能返回true
14、headler.enumerate() 以上我們列舉了所有可選擇覆蓋的14個headler方法,所有代理的操作都是基於這14個方法進行的,如果你打算進階的話,那麼現在可以去看一些大神的庫了。