1. 程式人生 > >ES6-proxy

ES6-proxy

前言

我們或多或少都聽過“資料繫結”這個詞,“資料繫結”的關鍵在於監聽資料的變化,可是對於這樣一個物件:var obj = {value: 1},我們該怎麼知道 obj 發生了改變呢?

definePropety

ES5 提供了 Object.defineProperty 方法,該方法可以在一個物件上定義一個新屬性,或者修改一個物件的現有屬性,並返回這個物件。

語法

Object.defineProperty(obj, prop, descriptor)

引數

obj: 要在其上定義屬性的物件。

prop:  要定義或修改的屬性的名稱。

descriptor: 將被定義或修改的屬性的描述符。

舉個例子:

var obj = {};
Object.defineProperty(obj, "num", {
    value : 1, writable : true, enumerable : true, configurable : true }); // 物件 obj 擁有屬性 num,值為 1

雖然我們可以直接新增屬性和值,但是使用這種方式,我們能進行更多的配置。

函式的第三個引數 descriptor 所表示的屬性描述符有兩種形式:資料描述符和存取描述符

兩者均具有以下兩種鍵值

configurable

當且僅當該屬性的 configurable true 時,該屬性描述符才能夠被改變,也能夠被刪除。預設為 false

enumerable

當且僅當該屬性的 enumerable true 時,該屬性才能夠出現在物件的列舉屬性中。預設為 false

資料描述符同時具有以下可選鍵值

value

該屬性對應的值。可以是任何有效的 JavaScript 值(數值,物件,函式等)。預設為 undefined。

writable

當且僅當該屬性的 writable true 時,該屬性才能被賦值運算子改變。預設為 false

存取描述符同時具有以下可選鍵值

get

一個給屬性提供 getter 的方法,如果沒有 getter 則為 undefined。該方法返回值被用作屬性值。預設為 undefined。

set

一個給屬性提供 setter 的方法,如果沒有 setter 則為 undefined。該方法將接受唯一引數,並將該引數的新值分配給該屬性。預設為 undefined。

值得注意的是:

屬性描述符必須是資料描述符或者存取描述符兩種形式之一,不能同時是兩者。這就意味著你可以:

Object.defineProperty({}, "num", {
    value: 1,
    writable: true, enumerable: true, configurable: true });

也可以:

var value = 1;
Object.defineProperty({}, "num", {
    get : function(){ return value; }, set : function(newValue){ value = newValue; }, enumerable : true, configurable : true });

但是不可以:

// 報錯
Object.defineProperty({}, "num", {
    value: 1, get: function() { return 1; } });

此外,所有的屬性描述符都是非必須的,但是 descriptor 這個欄位是必須的,如果不進行任何配置,你可以這樣:

var obj = Object.defineProperty({}, "num", {});
console.log(obj.num); // undefined

Setters 和 Getters

之所以講到 defineProperty,是因為我們要使用存取描述符中的 get 和 set,這兩個方法又被稱為 getter 和 setter。由 getter 和 setter 定義的屬性稱做”存取器屬性“。

當程式查詢存取器屬性的值時,JavaScript 呼叫 getter方法。這個方法的返回值就是屬性存取表示式的值。當程式設定一個存取器屬性的值時,JavaScript 呼叫 setter 方法,將賦值表示式右側的值當做引數傳入 setter。從某種意義上講,這個方法負責“設定”屬性值。可以忽略 setter 方法的返回值。

舉個例子:

var obj = {}, value = null;
Object.defineProperty(obj, "num", {
    get: function(){ console.log('執行了 get 操作') return value; }, set: function(newValue) { console.log('執行了 set 操作') value = newValue; } }) obj.value = 1 // 執行了 set 操作 console.log(obj.value); // 執行了 get 操作 // 1

這不就是我們要的監控資料改變的方法嗎?我們再來封裝一下:

function Archiver() {
    var value = null; // archive n. 檔案 var archive = []; Object.defineProperty(this, 'num', { get: function() { console.log('執行了 get 操作') return value; }, set: function(value) { console.log('執行了 set 操作') value = value; archive.push({ val: value }); } }); this.getArchive = function() { return archive; }; } var arc = new Archiver(); arc.num; // 執行了 get 操作 arc.num = 11; // 執行了 set 操作 arc.num = 13; // 執行了 set 操作 console.log(arc.getArchive()); // [{ val: 11 }, { val: 13 }]

watch API

既然可以監控資料的改變,那我可以這樣設想,即當資料改變的時候,自動進行渲染工作。舉個例子:

HTML 中有個 span 標籤和 button 標籤

<span id="container">1</span> <button id="button">點選加 1</button>

當點選按鈕的時候,span 標籤裡的值加 1。

傳統的做法是:

document.getElementById('button').addEventListener("click", function(){ var container = document.getElementById("container"); container.innerHTML = Number(container.innerHTML) + 1; });

如果使用了 defineProperty:

var obj = {
    value: 1
}

// 儲存 obj.value 的值
var value = 1; Object.defineProperty(obj, "value", { get: function() { return value; }, set: function(newValue) { value = newValue; document.getElementById('container').innerHTML = newValue; } }); document.getElementById('button').addEventListener("click", function() { obj.value += 1; });

程式碼看似增多了,但是當我們需要改變 span 標籤裡的值的時候,直接修改 obj.value 的值就可以了。

然而,現在的寫法,我們還需要單獨宣告一個變數儲存 obj.value 的值,因為如果你在 set 中直接 obj.value = newValue就會陷入無限的迴圈中。此外,我們可能需要監控很多屬性值的改變,要是一個一個寫,也很累吶,所以我們簡單寫個 watch 函式。使用效果如下:

var obj = {
    value: 1
}

watch(obj, "num", function(newvalue){ document.getElementById('container').innerHTML = newvalue; }) document.getElementById('button').addEventListener("click", function(){ obj.value += 1 });

我們來寫下這個 watch 函式:

(function(){
    var root = this; function watch(obj, name, func){ var value = obj[name]; Object.defineProperty(obj, name, { get: function() { return value; }, set: function(newValue) { value = newValue; func(value) } }); if (value) obj[name] = value } this.watch = watch; })()

現在我們已經可以監控物件屬性值的改變,並且可以根據屬性值的改變,添加回調函式,棒棒噠~

proxy

使用 defineProperty 只能重定義屬性的讀取(get)和設定(set)行為,到了 ES6,提供了 Proxy,可以重定義更多的行為,比如 in、delete、函式呼叫等更多行為。

Proxy 這個詞的原意是代理,用在這裡表示由它來“代理”某些操作,ES6 原生提供 Proxy 建構函式,用來生成 Proxy 例項。我們來看看它的語法:

var proxy = new Proxy(target, handler);

proxy 物件的所有用法,都是上面這種形式,不同的只是handler引數的寫法。其中,new Proxy()表示生成一個Proxy例項,target引數表示所要攔截的目標物件,handler引數也是一個物件,用來定製攔截行為。

var proxy = new Proxy({}, {
    get: function(obj, prop) { console.log('設定 get 操作') return obj[prop]; }, set: function(obj, prop, value) { console.log('設定 set 操作') obj[prop] = value; } }); proxy.time = 35; // 設定 set 操作 console.log(proxy.time); // 設定 get 操作 // 35

除了 get 和 set 之外,proxy 可以攔截多達 13 種操作,比如 has(target, propKey),可以攔截 propKey in proxy 的操作,返回一個布林值。

// 使用 has 方法隱藏某些屬性,不被 in 運算子發現
var handler = {
  has (target, key) {
    if (key[0] === '_') { return false; } return key in target; } }; var target = { _prop: 'foo', prop: 'foo' }; var proxy = new Proxy(target, handler); console.log('_prop' in proxy); // false

又比如說 apply 方法攔截函式的呼叫、call 和 apply 操作。

apply 方法可以接受三個引數,分別是目標物件、目標物件的上下文物件(this)和目標物件的引數陣列,不過這裡我們簡單演示一下:

var target = function () { return 'I am the target'; }; var handler = { apply: function () { return 'I am the proxy'; } }; var p = new Proxy(target, handler); p(); // "I am the proxy"

又比如說 ownKeys 方法可以攔截物件自身屬性的讀取操作。具體來說,攔截以下操作:

  • Object.getOwnPropertyNames()
  • Object.getOwnPropertySymbols()
  • Object.keys()

下面的例子是攔截第一個字元為下劃線的屬性名,不讓它被 for of 遍歷到。

let target = {
  _bar: 'foo',
  _prop: 'bar', prop: 'baz' }; let handler = { ownKeys (target) { return Reflect.ownKeys(target).filter(key => key[0] !== '_'); } }; let proxy = new Proxy(target, handler); for (let key of Object.keys(proxy)) { console.log(target[key]); } // "baz"

更多的攔截行為可以檢視阮一峰老師的 《ECMAScript 6 入門》

值得注意的是,proxy 的最大問題在於瀏覽器支援度不夠,而且很多效果無法使用 poilyfill 來彌補。

watch API 優化

我們使用 proxy 再來寫一下 watch 函式。使用效果如下:

(function() {
    var root = this; function watch(target, func) { var proxy = new Proxy(target, { get: function(target, prop) { return target[prop]; }, set: function(target, prop, value) { target[prop] = value; func(prop, value); } }); if(target[name]) proxy[name] = value; return proxy; } this.watch = watch; })() var obj = { value: 1 } var newObj = watch(obj, function(key, newvalue) { if (key == 'value') document.getElementById('container').innerHTML = newvalue; }) document.getElementById('button').addEventListener("click", function() { newObj.value += 1 });

我們也可以發現,使用 defineProperty 和 proxy 的區別,當使用 defineProperty,我們修改原來的 obj 物件就可以觸發攔截,而使用 proxy,就必須修改代理物件,即 Proxy 的例項才可以觸發攔截。

 


https://segmentfault.com/a/1190000016959805