深入理解 Object.defineProperty
Object.defineProperty() 和 Proxy 物件,都可以用來對資料的劫持操作。何為資料劫持呢?就是在我們訪問或者修改某個物件的某個屬性的時候,通過一段程式碼進行攔截行為,然後進行額外的操作,然後返回結果。那麼vue中雙向資料繫結就是一個典型的應用。
Vue2.x 是使用 Object.defindProperty(),來進行對物件的監聽的。
Vue3.x 版本之後就改用Proxy進行實現的。
下面我們先來理解下Object.defineProperty作用。
一: 理解Object.defineProperty的語法和基本作用。
在理解之前,我們先來看看一個普通的物件,物件它是由多個名/值對組成的無序集合。物件中每個屬性對於任意型別的值。
比如現在我們想建立一個簡單的物件,可以簡單的如下程式碼:
const obj = new Object; // 或 const obj = {}; obj.name = 'kongzhi'; console.log(obj.name); // 在控制檯中會列印 kongzhi obj.xxx = function() { console.log(111); } // 呼叫 xxx 方法 obj.xxx(); // 在控制檯中會列印 111
但是除了上面新增物件屬性之外,我們還可以使用 Object.defineProperty 來定義新的屬性或修改原有的屬性。最終會返回該物件。
接下來我們慢慢來理解下該用法。
基本語法:
Object.defineProperty(obj, prop, descriptor);
基本的引數解析如下:
obj: 可以理解為目標物件。
prop: 目標物件的屬性名。
descriptor: 對屬性的描述。
那麼對於第一個引數obj 和 prop引數,我們很容易理解,比如上面的實列demo,我們定義的 obj物件就是第一個引數的含義,我們在obj中定義的name屬性和xxx屬性是prop的含義,那麼第三個引數描述符是什麼含義呢?
descriptor: 屬性描述符,它是由兩部分組成,分別是:資料描述符和訪問器描述符,資料描述符的含義是:它是一個包含屬性的值,並說明這個屬性值是可讀或不可讀的物件。訪問器描述符的含義是:包含該屬性的一對 getter/setter方法的物件。
下面我們繼續來理解下 資料描述符 和 訪問器描述符具體包含哪些配置項含義及用法。
1.1 資料描述符
const obj = { name: 'kongzhi' }; // 對obj物件已有的name屬性新增資料描述 Object.defineProperty(obj, 'name', { configurable: true | false, enumerable: true | false, value: '任意型別的值', writable: true | false }); // 對obj物件新增新屬性的描述 Object.defineProperty(obj, 'newAttr', { configurable: true | false, enumerable: true | false, value: '任意型別的值', writable: true | false });
如上程式碼配置,資料描述符有如上configurable,enumerable,value 及 writable 配置項。
下面我們來看下 每個描述符中每個屬性的含義:
1)value
屬性對應的值,值的型別可以是任意型別的。比如我先定義一個obj物件,裡面有一個屬性 name 值為 'kongzhi', 現在我們通過如下程式碼改變 obj.name 的值,如下程式碼:
const obj = { name: 'kongzhi' }; // 對obj物件已有的name屬性新增資料描述 Object.defineProperty(obj, 'name', { value: '1122' }); console.log(obj.name); // 輸出 1122
如果上面我不設定 value描述符值的話,那麼它返回的值還是 kongzhi 的。比如如下程式碼:
const obj = { name: 'kongzhi' }; // 對obj物件已有的name屬性新增資料描述 Object.defineProperty(obj, 'name', { }); console.log(obj.name); // 輸出 kongzhi
2)writable
writable的英文的含義是:'可寫的',在該配置中它的含義是:屬性的值是否可以被重寫,設定為true可以被重寫,設定為false,是不能被重寫的,預設為false。
如下程式碼:
const obj = {}; Object.defineProperty(obj, 'name', { 'value': 'kongzhi' }); console.log(obj.name); // 輸出 kongzhi // 改寫obj.name 的值 obj.name = 111; console.log(obj.name); // 還是打印出 kongzhi
上面程式碼中 使用 Object.defineProperty 定義 obj.name 的值 value = 'kongzhi', 然後我們使用 obj.name 進行重新改寫值,再打印出 obj.name 可以看到 值 還是為 kongzhi , 這是 Object.defineProperty 中 writable 預設為false,不能被重寫,但是下面我們將它設定為true,就可以進行重寫值了,如下程式碼:
const obj = {}; Object.defineProperty(obj, 'name', { 'value': 'kongzhi', 'writable': true }); console.log(obj.name); // 輸出 kongzhi // 改寫obj.name 的值 obj.name = 111; console.log(obj.name); // 設定 writable為true的時候 打印出改寫後的值 111
3)enumerable
此屬性的含義是:是否可以被列舉,比如使用 for..in 或 Object.keys() 這樣的。設定為true可以被列舉,設定為false,不能被列舉,預設為false.
如下程式碼:
const obj = { 'name1': 'xxx' }; Object.defineProperty(obj, 'name', { 'value': 'kongzhi', 'writable': true }); // 列舉obj的屬性 for (const i in obj) { console.log(i); // 打印出 name1 }
如上程式碼,物件obj本身有一個屬性 name1, 然後我們使用 Object.defineProperty 給 obj物件新增 name屬性,但是通過for in迴圈出來後可以看到 只打印出 name1 屬性了,那是因為 enumerable 預設為false,它裡面的值預設是不可被列舉的。但是如果我們將它設定為true的話,那麼 Object.defineProperty 新增的屬性也是可以被列舉的,如下程式碼:
const obj = { 'name1': 'xxx' }; Object.defineProperty(obj, 'name', { 'value': 'kongzhi', 'writable': true, 'enumerable': true }); // 列舉obj的屬性 for (const i in obj) { console.log(i); // 打印出 name1 和 name }
4) configurable
該屬性英文的含義是:可配置的意思,那麼該屬性的含義是:是否可以刪除目標屬性。如果我們設定它為true的話,是可以被刪除。如果設定為false的話,是不能被刪除的。它預設值為false。
比如如下程式碼:
const obj = { 'name1': 'xxx' }; Object.defineProperty(obj, 'name', { 'value': 'kongzhi', 'writable': true, 'enumerable': true }); // 使用delete 刪除屬性 delete obj.name; console.log(obj.name); // 打印出kongzhi
如上程式碼 使用 delete命令刪除 obj.name的話,該屬性值是刪除不了的,因為 configurable 預設為false,不能被刪除的。
但是如果我們把它設定為true,那麼就可以進行刪除了。
如下程式碼:
const obj = { 'name1': 'xxx' }; Object.defineProperty(obj, 'name', { 'value': 'kongzhi', 'writable': true, 'enumerable': true, 'configurable': true }); // 使用delete 刪除屬性 delete obj.name; console.log(obj.name); // 打印出undefined
如上就是 資料描述符 中的四個配置項的基本含義。那麼下面我們來看看 訪問器描述符 的具體用法和含義。
1.2 訪問器描述符
訪問器描述符的含義是:包含該屬性的一對 getter/setter方法的物件。如下基本語法:
const obj = {}; Object.defineProperty(obj, 'name', { get: function() {}, set: function(value) {}, configurable: true | false, enumerable: true | false });
注意:使用訪問器描述符中 getter或 setter方法的話,不允許使用 writable 和 value 這兩個配置項。
getter/setter
當我們需要設定或獲取物件的某個屬性的值的時候,我們可以使用 setter/getter方法。
如下程式碼的使用demo.
const obj = {}; let initValue = 'kongzhi'; Object.defineProperty(obj, 'name', { // 當我們使用 obj.name 獲取該值的時候,會自動呼叫 get 函式 get: function() { return initValue; }, set: function(value) { initValue = value; } }); // 我們來獲取值,會自動呼叫 Object.defineProperty 中的 get函式方法。 console.log(obj.name); // 打印出kongzhi // 設定值的話,會自動呼叫 Object.defineProperty 中的 set方法。 obj.name = 'xxxxx'; console.log(obj.name); // 打印出 xxx
注意:configurable 和 enumerable 配置項和資料描述符中的含義是一樣的。
1.3:使用 Object.defineProperty 來實現一個簡單雙向繫結的demo
如下程式碼:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>標題</title> </head> <body> <input type="text" id="demo" /> <div id="xxx">{{name}}</div> <script type="text/javascript"> const obj = {}; Object.defineProperty(obj, 'name', { set: function(value) { document.getElementById('xxx').innerHTML = value; document.getElementById('demo').value = value; } }); document.querySelector('#demo').oninput = function(e) { obj.name = e.target.value; } obj.name = ''; </script> </body> </html>
1.4 Object.defineProperty 對陣列的監聽
看如下demo程式碼來理解下對陣列的監聽的情況。
const obj = {}; let initValue = 1; Object.defineProperty(obj, 'name', { set: function(value) { console.log('set方法被執行了'); initValue = value; }, get: function() { return initValue; } }); console.log(obj.name); // 1 obj.name = []; // 會執行set方法,會列印資訊 // 給 obj 中的name屬性 設定為 陣列 [1, 2, 3], 會執行set方法,會列印資訊 obj.name = [1, 2, 3]; // 然後對 obj.name 中的某一項進行改變值,不會執行set方法,不會列印資訊 obj.name[0] = 11; // 然後我們列印下 obj.name 的值 console.log(obj.name); // 然後我們使用陣列中push方法對 obj.name陣列新增屬性 不會執行set方法,不會列印資訊 obj.name.push(4); obj.name.length = 5; // 也不會執行set方法
如上執行結果我們可以看到,當我們使用 Object.defineProperty 對陣列賦值有一個新物件的時候,會執行set方法,但是當我們改變陣列中的某一項值的時候,或者使用陣列中的push等其他的方法,或者改變陣列的長度,都不會執行set方法。也就是如果我們對陣列中的內部屬性值更改的話,都不會觸發set方法。因此如果我們想實現資料雙向繫結的話,我們就不能簡單地使用 obj.name[1] = newValue; 這樣的來進行賦值了。那麼對於vue這樣的框架,那麼一般會重寫 Array.property.push方法,並且生成一個新的陣列賦值給資料,這樣資料雙向繫結就觸發了。
因此我們需要重新編寫陣列的push方法來實現陣列的雙向繫結,我們可以參照如下方法來理解下。
1) 重寫編寫陣列的方法:
const arrPush = {}; // 如下是 陣列的常用方法 const arrayMethods = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ]; // 對陣列的方法進行重寫 arrayMethods.forEach((method) => { const original = Array.prototype[method]; arrPush[method] = function() { console.log(this); return original.apply(this, arguments); } }); const testPush = []; // 對 testPush 的原型 指向 arrPush,因此testPush也有重寫後的方法 testPush.__proto__ = arrPush; testPush.push(1); // 列印 [], this指向了 testPush testPush.push(2); // 列印 [1], this指向了 testPush
2)使用 Object.defineProperty 對陣列方法進行監聽操作。
因此我們需要把上面的程式碼繼續修改下進行使用 Object.defineProperty 進行監聽即可:
Vue中的做法如下, 程式碼如下:
function Observer(data) { this.data = data; this.walk(data); } var p = Observer.prototype; var arrayProto = Array.prototype; var arrayMethods = Object.create(arrayProto); [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ].forEach(function(method) { // 使用 Object.defineProperty 進行監聽 Object.defineProperty(arrayMethods, method, { value: function testValue() { console.log('陣列被訪問到了'); const original = arrayProto[method]; // 使類陣列變成一個真正的陣列 const args = Array.from(arguments); original.apply(this, args); } }); }); p.walk = function(obj) { let value; for (let key in obj) { // 使用 hasOwnProperty 判斷物件本身是否有該屬性 if (obj.hasOwnProperty(key)) { value = obj[key]; // 遞迴呼叫,迴圈所有的物件 if (typeof value === 'object') { // 並且該值是一個數組的話 if (Array.isArray(value)) { const augment = value.__proto__ ? protoAugment : copyAugment; augment(value, arrayMethods, key); observeArray(value); } /* 如果是物件的話,遞迴呼叫該物件,遞迴完成後,會有屬性名和值,然後對 該屬性名和值使用 Object.defindProperty 進行監聽即可 */ new Observer(value); } this.convert(key, value); } } } p.convert = function(key, value) { Object.defineProperty(this.data, key, { enumerable: true, configurable: true, get: function() { console.log(key + '被訪問到了'); return value; }, set: function(newVal) { console.log(key + '被重新設定值了' + '=' + newVal); // 如果新值和舊值相同的話,直接返回 if (newVal === value) return; value = newVal; } }); } function observeArray(items) { for (let i = 0, l = items.length; i < l; i++) { observer(items[i]); } } function observer(value) { if (typeof value !== 'object') return; let ob = new Observer(value); return ob; } function def (obj, key, val) { Object.defineProperty(obj, key, { value: val, enumerable: true, writable: true, configurable: true }) } // 相容不支援 __proto__的方法 function protoAugment(target, src) { target.__proto__ = src; } // 不支援 __proto__的直接修改先關的屬性方法 function copyAugment(target, src, keys) { for (let i = 0, l = keys.length; i < l; i++) { const key = keys[i]; def(target, key, src[key]); } } // 下面是測試資料 var data = { testA: { say: function() { console.log('kongzhi'); } }, xxx: [{'a': 'b'}, 11, 22] }; var test = new Observer(data); console.log(test); data.xxx.push(33);