實現(xiàn)雙向綁定Object.defineProperty與proxy的VS
關注 入坑互聯(lián)網(wǎng) ,回復“加群”
加入我們一起學習,天天進步
實現(xiàn)雙向綁定的方法有很多,KnockoutJS基于觀察者模式的雙向綁定,Ember基于數(shù)據(jù)模型的雙向綁定,Angular基于臟檢查的雙向綁定,本篇文章我們重點講面試中常見的基于數(shù)據(jù)劫持的雙向綁定。
常見的基于數(shù)據(jù)劫持的雙向綁定有兩種實現(xiàn),一個是目前Vue在用的Object.defineProperty,另一個是ES2015中新增的Proxy,而Vue的作者宣稱將在Vue3.0版本后加入Proxy從而代替Object.defineProperty,通過本文你也可以知道為什么Vue未來會選擇Proxy。
(嚴格來講Proxy應該被稱為『代理』而非『劫持』,不過由于作用有很多相似之處,我們在下文中就不再做區(qū)分,統(tǒng)一叫『劫持』。)
什么是數(shù)據(jù)劫持
指的是在訪問或者修改對象的某個屬性時,通過一段代碼攔截這個行為,進行額外的操作或者修改返回結果。
比較典型的是 Object.defineProperty() 和 ES2015 中新增的 Proxy 對象。數(shù)據(jù)劫持最著名的應用當屬雙向綁定,這也是一個已經(jīng)被討論爛了的面試必考題。例如 Vue 2.x 使用的是 Object.defineProperty()(Vue 在 3.x 版本之后改用 Proxy 進行實現(xiàn))。
Object.defineProperty
ES5 提供了 Object.defineProperty 方法,該方法可以在一個對象上定義一個新屬性,或者修改一個對象的現(xiàn)有屬性,并返回這個對象。
語法:
Object.defineProperty(obj, prop, descriptor)
參數(shù):
obj: 要在其上定義屬性的對象。
prop: 要定義或修改的屬性的名稱。
descriptor: 將被定義或修改的屬性的描述符。
來個例子:
// 這是將要被劫持的對象const data = {name: '',};function say(name) {if (name === '孫紅雷') {console.log('給大家推薦一款超好玩的游戲');} else if (name === '綠茶婊') {console.log('綠茶婊,撩妹屬性爆表');} else {console.log('來做我的兄弟');}}// 遍歷對象,對其屬性值進行劫持Object.keys(data).forEach(function(key) {Object.defineProperty(data, key, {//當且僅當該屬性的 enumerable 為 true 時,該屬性才能夠出現(xiàn)在對象的枚舉屬性中。默認為 false。enumerable: true,//當且僅當該屬性的 configurable 為 true 時,該屬性描述符才能夠被改變,也能夠被刪除。默認為 false。configurable: true,get: function() {console.log('get');},set: function(newVal) {// 當屬性值發(fā)生變化時我們可以進行額外操作console.log(`大家好,我系${newVal}`);say(newVal);},});});data.name = '綠茶婊';//大家好,我系綠茶婊//綠茶婊,撩妹屬性爆表
Proxy
Proxy 用于修改某些操作的默認行為,等同于在語言層面做出修改,所以屬于一種“元編程”(meta programming),即對編程語言進行編程。
Proxy 可以理解成,在目標對象之前架設一層“攔截”,外界對該對象的訪問,都必須先通過這層攔截,因此提供了一種機制,可以對外界的訪問進行過濾和改寫。Proxy 這個詞的原意是代理,用在這里表示由它來“代理”某些操作,可以譯為“代理器”。
我們來看看它的語法:
var proxy = new Proxy(target, handler);proxy 對象的所有用法,都是上面這種形式,不同的只是handler參數(shù)的寫法。其中,new Proxy()表示生成一個Proxy實例,target參數(shù)表示所要攔截的目標對象,handler參數(shù)也是一個對象,用來定制攔截行為。
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 運算符發(fā)現(xiàn)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 方法攔截函數(shù)的調(diào)用、call 和 apply 操作。
apply 方法可以接受三個參數(shù),分別是目標對象、目標對象的上下文對象(this)和目標對象的參數(shù)數(shù)組,不過這里我們簡單演示一下:
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"
我們使用 proxy 再來寫一下 watch 函數(shù)。使用效果如下:
(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});
我們也可以發(fā)現(xiàn),使用 defineProperty 和 proxy 的區(qū)別,當使用 defineProperty,我們修改原來的 obj 對象就可以觸發(fā)攔截,而使用 proxy,就必須修改代理對象,即 Proxy 的實例才可以觸發(fā)攔截。
vue3為什么用proxy?舍棄Object.defineProperty
Object.defineProperty缺點:
①不能監(jiān)聽數(shù)組的變化
數(shù)組的以下幾個方法不會觸發(fā) set:
push、pop、shift、unshift、splice、sort、reverse;
Vue 把這些方法定義為變異方法 (mutation method),指的是會修改原來數(shù)組的方法。與之對應則是非變異方法 (non-mutating method),例如 filter, concat, slice 等,它們都不會修改原始數(shù)組,而會返回一個新的數(shù)組。
let arr = [1,2,3]let obj = {}Object.defineProperty(obj, 'arr', {get () {console.log('get arr')return arr},set (newVal) {console.log('set', newVal)arr = newVal}})obj.arr.push(4) // 只會打印 get arr, 不會打印 setobj.arr = [1,2,3,4] // 這個能正常 set
②必須遍歷對象的每個屬性
使用 Object.defineProperty() 多數(shù)要配合 Object.keys() 和遍歷,于是多了一層嵌套。
Object.keys(obj).forEach(key => {Object.defineProperty(obj, key, {// ...})})
③必須深層遍歷嵌套的對象
如果是這一類嵌套對象,那就必須逐層遍歷,直到把每個對象的每個屬性都調(diào)用 Object.defineProperty() 為止。Vue 的源碼中就能找到這樣的邏輯 (叫做 walk 方法)。
let obj = {info: {name: 'eason'}}
針對Object.defineProperty有這些缺點,為什么用proxy?
1.proxy可以直接監(jiān)聽數(shù)組的變化;
2.proxy可以監(jiān)聽對象而非屬性.它在目標對象之前架設一層“攔截”,外界對該對象的訪問,都必須先通過這層攔截,因此提供了一種機制,可以對外界的訪問進行過濾和改寫。
3.Proxy返回的是一個新對象,我們可以只操作新的對象達到目的,而Object.defineProperty只能遍歷對象屬性直接修改。
4.Proxy有多達13種攔截方法,不限于apply、ownKeys、deleteProperty、has等等是Object.defineProperty不具備的。
當然,Proxy的劣勢就是兼容性問題,而且無法用polyfill磨平,因此Vue的作者才聲明需要等到下個大版本(3.0)才能用Proxy重寫。
?? 看完三件事
如果你覺得這篇內(nèi)容對你挺有啟發(fā),我想邀請你幫我三個小忙:
點贊,讓更多的人也能看到這篇內(nèi)容(收藏不點贊,都是耍流氓)。 關注公眾號「入坑互聯(lián)網(wǎng)」,不定期分享原創(chuàng)知識。 也看看其它文章
- END -
結伴同行前端路

