咱們或多或少都聽過「數據綁定」這個詞,「數據綁定」的關鍵在於監聽數據的變化,但是對於這樣一個對象:var obj = {value: 1}
,咱們該怎麼知道 obj 發生了改變呢?git
ES5 提供了 Object.defineProperty 方法,該方法能夠在一個對象上定義一個新屬性,或者修改一個對象的現有屬性,並返回這個對象。es6
語法github
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
雖然咱們能夠直接添加屬性和值,可是使用這種方式,咱們能進行更多的配置。app
函數的第三個參數 descriptor 所表示的屬性描述符有兩種形式:數據描述符和存取描述符。異步
二者均具備如下兩種鍵值:函數
configurable優化
當且僅當該屬性的 configurable 爲 true 時,該屬性描述符纔可以被改變,也可以被刪除。默認爲 false。
enumerablethis
當且僅當該屬性的 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
之因此講到 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 }]
既然能夠監控數據的改變,那我能夠這樣設想,即當數據改變的時候,自動進行渲染工做。舉個例子:
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; })()
如今咱們已經能夠監控對象屬性值的改變,而且能夠根據屬性值的改變,添加回調函數,棒棒噠~
使用 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 方法能夠攔截對象自身屬性的讀取操做。具體來講,攔截如下操做:
下面的例子是攔截第一個字符爲下劃線的屬性名,不讓它被 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 來彌補。
咱們使用 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 的實例才能夠觸發攔截。
ES6 系列目錄地址:https://github.com/mqyqingfeng/Blog
ES6 系列預計寫二十篇左右,旨在加深 ES6 部分知識點的理解,重點講解塊級做用域、標籤模板、箭頭函數、Symbol、Set、Map 以及 Promise 的模擬實現、模塊加載方案、異步處理等內容。
若是有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝。若是喜歡或者有所啓發,歡迎 star,對做者也是一種鼓勵。
本文爲雲棲社區原創內容,未經容許不得轉載。