詳談Object.defineProperty 及實(shí)現(xiàn)數(shù)據(jù)雙向綁定
Object.defineProperty() 和 Proxy 對(duì)象,都可以用來(lái)對(duì)數(shù)據(jù)的劫持操作。何為數(shù)據(jù)劫持呢?就是在我們?cè)L問(wèn)或者修改某個(gè)對(duì)象的某個(gè)屬性的時(shí)候,通過(guò)一段代碼進(jìn)行攔截行為,然后進(jìn)行額外的操作,然后返回結(jié)果。那么vue中雙向數(shù)據(jù)綁定就是一個(gè)典型的應(yīng)用。
Vue2.x 是使用 Object.defindProperty(),來(lái)進(jìn)行對(duì)對(duì)象的監(jiān)聽(tīng)的。
Vue3.x 版本之后就改用Proxy進(jìn)行實(shí)現(xiàn)的。
下面我們先來(lái)理解下Object.defineProperty作用。
一: 理解Object.defineProperty的語(yǔ)法和基本作用。
在理解之前,我們先來(lái)看看一個(gè)普通的對(duì)象,對(duì)象它是由多個(gè)名/值對(duì)組成的無(wú)序集合。對(duì)象中每個(gè)屬性對(duì)于任意類型的值。
比如現(xiàn)在我們想創(chuàng)建一個(gè)簡(jiǎn)單的對(duì)象,可以簡(jiǎn)單的如下代碼:
const obj = new Object; // 或 const obj = {};
obj.name = 'kongzhi';
console.log(obj.name); // 在控制臺(tái)中會(huì)打印 kongzhi
obj.xxx = function() {
console.log(111);
}
// 調(diào)用 xxx 方法
obj.xxx(); // 在控制臺(tái)中會(huì)打印 111
但是除了上面添加對(duì)象屬性之外,我們還可以使用 Object.defineProperty 來(lái)定義新的屬性或修改原有的屬性。最終會(huì)返回該對(duì)象。
接下來(lái)我們慢慢來(lái)理解下該用法。
基本語(yǔ)法:
Object.defineProperty(obj, prop, descriptor);
基本的參數(shù)解析如下:
obj: 可以理解為目標(biāo)對(duì)象。
prop: 目標(biāo)對(duì)象的屬性名。
descriptor: 對(duì)屬性的描述。
那么對(duì)于第一個(gè)參數(shù)obj 和 prop參數(shù),我們很容易理解,比如上面的實(shí)列demo,我們定義的 obj對(duì)象就是第一個(gè)參數(shù)的含義,我們?cè)趏bj中定義的name屬性和xxx屬性是prop的含義,那么第三個(gè)參數(shù)描述符是什么含義呢?
descriptor: 屬性描述符,它是由兩部分組成,分別是:數(shù)據(jù)描述符和訪問(wèn)器描述符,數(shù)據(jù)描述符的含義是:它是一個(gè)包含屬性的值,并說(shuō)明這個(gè)屬性值是可讀或不可讀的對(duì)象。訪問(wèn)器描述符的含義是:包含該屬性的一對(duì) getter/setter方法的對(duì)象。
下面我們繼續(xù)來(lái)理解下 數(shù)據(jù)描述符 和 訪問(wèn)器描述符具體包含哪些配置項(xiàng)含義及用法。
1.1 數(shù)據(jù)描述符
const obj = {
name: 'kongzhi'
};
// 對(duì)obj對(duì)象已有的name屬性添加數(shù)據(jù)描述
Object.defineProperty(obj, 'name', {
configurable: true | false,
enumerable: true | false,
value: '任意類型的值',
writable: true | false
});
// 對(duì)obj對(duì)象添加新屬性的描述
Object.defineProperty(obj, 'newAttr', {
configurable: true | false,
enumerable: true | false,
value: '任意類型的值',
writable: true | false
});
如上代碼配置,數(shù)據(jù)描述符有如上configurable,enumerable,value 及 writable 配置項(xiàng)。
下面我們來(lái)看下 每個(gè)描述符中每個(gè)屬性的含義:
1)value
屬性對(duì)應(yīng)的值,值的類型可以是任意類型的。比如我先定義一個(gè)obj對(duì)象,里面有一個(gè)屬性 name 值為 'kongzhi', 現(xiàn)在我們通過(guò)如下代碼改變 obj.name 的值,如下代碼:
const obj = {
name: 'kongzhi'
};
// 對(duì)obj對(duì)象已有的name屬性添加數(shù)據(jù)描述
Object.defineProperty(obj, 'name', {
value: '1122'
});
console.log(obj.name); // 輸出 1122
如果上面我不設(shè)置 value描述符值的話,那么它返回的值還是 kongzhi 的。比如如下代碼:
const obj = {
name: 'kongzhi'
};
// 對(duì)obj對(duì)象已有的name屬性添加數(shù)據(jù)描述
Object.defineProperty(obj, 'name', {
});
console.log(obj.name); // 輸出 kongzhi
2)writable
writable的英文的含義是:'可寫(xiě)的',在該配置中它的含義是:屬性的值是否可以被重寫(xiě),設(shè)置為true可以被重寫(xiě),設(shè)置為false,是不能被重寫(xiě)的,默認(rèn)為false。
如下代碼:
const obj = {};
Object.defineProperty(obj, 'name', {
'value': 'kongzhi'
});
console.log(obj.name); // 輸出 kongzhi
// 改寫(xiě)obj.name 的值
obj.name = 111;
console.log(obj.name); // 還是打印出 kongzhi
上面代碼中 使用 Object.defineProperty 定義 obj.name 的值 value = 'kongzhi', 然后我們使用 obj.name 進(jìn)行重新改寫(xiě)值,再打印出 obj.name 可以看到 值 還是為 kongzhi , 這是 Object.defineProperty 中 writable 默認(rèn)為false,不能被重寫(xiě),但是下面我們將它設(shè)置為true,就可以進(jìn)行重寫(xiě)值了,如下代碼:
const obj = {};
Object.defineProperty(obj, 'name', {
'value': 'kongzhi',
'writable': true
});
console.log(obj.name); // 輸出 kongzhi
// 改寫(xiě)obj.name 的值
obj.name = 111;
console.log(obj.name); // 設(shè)置 writable為true的時(shí)候 打印出改寫(xiě)后的值 111
3)enumerable
此屬性的含義是:是否可以被枚舉,比如使用 for..in 或 Object.keys() 這樣的。設(shè)置為true可以被枚舉,設(shè)置為false,不能被枚舉,默認(rèn)為false.
如下代碼:
const obj = {
'name1': 'xxx'
};
Object.defineProperty(obj, 'name', {
'value': 'kongzhi',
'writable': true
});
// 枚舉obj的屬性
for (const i in obj) {
console.log(i); // 打印出 name1
}
如上代碼,對(duì)象obj本身有一個(gè)屬性 name1, 然后我們使用 Object.defineProperty 給 obj對(duì)象新增 name屬性,但是通過(guò)for in循環(huán)出來(lái)后可以看到 只打印出 name1 屬性了,那是因?yàn)?enumerable 默認(rèn)為false,它里面的值默認(rèn)是不可被枚舉的。但是如果我們將它設(shè)置為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
該屬性英文的含義是:可配置的意思,那么該屬性的含義是:是否可以刪除目標(biāo)屬性。如果我們?cè)O(shè)置它為true的話,是可以被刪除。如果設(shè)置為false的話,是不能被刪除的。它默認(rèn)值為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的話,該屬性值是刪除不了的,因?yàn)?configurable 默認(rèn)為false,不能被刪除的。但是如果我們把它設(shè)置為true,那么就可以進(jìn)行刪除了。
如下代碼:
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
如上就是 數(shù)據(jù)描述符 中的四個(gè)配置項(xiàng)的基本含義。那么下面我們來(lái)看看 訪問(wèn)器描述符 的具體用法和含義。
1.2 訪問(wèn)器描述符
訪問(wèn)器描述符的含義是:包含該屬性的一對(duì) getter/setter方法的對(duì)象。如下基本語(yǔ)法:
const obj = {};
Object.defineProperty(obj, 'name', {
get: function() {},
set: function(value) {},
configurable: true | false,
enumerable: true | false
});
注意:使用訪問(wèn)器描述符中 getter或 setter方法的話,不允許使用 writable 和 value 這兩個(gè)配置項(xiàng)。
getter/setter
當(dāng)我們需要設(shè)置或獲取對(duì)象的某個(gè)屬性的值的時(shí)候,我們可以使用 setter/getter方法。
如下代碼的使用demo.
const obj = {};
let initValue = 'kongzhi';
Object.defineProperty(obj, 'name', {
// 當(dāng)我們使用 obj.name 獲取該值的時(shí)候,會(huì)自動(dòng)調(diào)用 get 函數(shù)
get: function() {
return initValue;
},
set: function(value) {
initValue = value;
}
});
// 我們來(lái)獲取值,會(huì)自動(dòng)調(diào)用 Object.defineProperty 中的 get函數(shù)方法。
console.log(obj.name); // 打印出kongzhi
// 設(shè)置值的話,會(huì)自動(dòng)調(diào)用 Object.defineProperty 中的 set方法。
obj.name = 'xxxxx';
console.log(obj.name); // 打印出 xxx
注意:configurable 和 enumerable 配置項(xiàng)和數(shù)據(jù)描述符中的含義是一樣的。
1.3:使用 Object.defineProperty 來(lái)實(shí)現(xiàn)一個(gè)簡(jiǎn)單雙向綁定的demo
如下代碼:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>標(biāo)題</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 對(duì)數(shù)組的監(jiān)聽(tīng)
看如下demo代碼來(lái)理解下對(duì)數(shù)組的監(jiān)聽(tīng)的情況。
const obj = {};
let initValue = 1;
Object.defineProperty(obj, 'name', {
set: function(value) {
console.log('set方法被執(zhí)行了');
initValue = value;
},
get: function() {
return initValue;
}
});
console.log(obj.name); // 1
obj.name = []; // 會(huì)執(zhí)行set方法,會(huì)打印信息
// 給 obj 中的name屬性 設(shè)置為 數(shù)組 [1, 2, 3], 會(huì)執(zhí)行set方法,會(huì)打印信息
obj.name = [1, 2, 3];
// 然后對(duì) obj.name 中的某一項(xiàng)進(jìn)行改變值,不會(huì)執(zhí)行set方法,不會(huì)打印信息
obj.name[0] = 11;
// 然后我們打印下 obj.name 的值
console.log(obj.name);
// 然后我們使用數(shù)組中push方法對(duì) obj.name數(shù)組添加屬性 不會(huì)執(zhí)行set方法,不會(huì)打印信息
obj.name.push(4);
obj.name.length = 5; // 也不會(huì)執(zhí)行set方法
如上執(zhí)行結(jié)果我們可以看到,當(dāng)我們使用 Object.defineProperty 對(duì)數(shù)組賦值有一個(gè)新對(duì)象的時(shí)候,會(huì)執(zhí)行set方法,但是當(dāng)我們改變數(shù)組中的某一項(xiàng)值的時(shí)候,或者使用數(shù)組中的push等其他的方法,或者改變數(shù)組的長(zhǎng)度,都不會(huì)執(zhí)行set方法。
也就是如果我們對(duì)數(shù)組中的內(nèi)部屬性值更改的話,都不會(huì)觸發(fā)set方法。
因此如果我們想實(shí)現(xiàn)數(shù)據(jù)雙向綁定的話,我們就不能簡(jiǎn)單地使用 obj.name[1] = newValue; 這樣的來(lái)進(jìn)行賦值了。
那么對(duì)于vue這樣的框架,那么一般會(huì)重寫(xiě) Array.property.push方法,并且生成一個(gè)新的數(shù)組賦值給數(shù)據(jù),這樣數(shù)據(jù)雙向綁定就觸發(fā)了。
因此我們需要重新編寫(xiě)數(shù)組的push方法來(lái)實(shí)現(xiàn)數(shù)組的雙向綁定,我們可以參照如下方法來(lái)理解下。
1) 重寫(xiě)編寫(xiě)數(shù)組的方法:
const arrPush = {};
// 如下是 數(shù)組的常用方法
const arrayMethods = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
];
// 對(duì)數(shù)組的方法進(jìn)行重寫(xiě)
arrayMethods.forEach((method) => {
const original = Array.prototype[method];
arrPush[method] = function() {
console.log(this);
return original.apply(this, arguments);
}
});
const testPush = [];
// 對(duì) testPush 的原型 指向 arrPush,因此testPush也有重寫(xiě)后的方法
testPush.__proto__ = arrPush;
testPush.push(1); // 打印 [], this指向了 testPush
testPush.push(2); // 打印 [1], this指向了 testPush
2)使用 Object.defineProperty 對(duì)數(shù)組方法進(jìn)行監(jiān)聽(tīng)操作。
因此我們需要把上面的代碼繼續(xù)修改下進(jìn)行使用 Object.defineProperty 進(jìn)行監(jiān)聽(tīng)即可:
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 進(jìn)行監(jiān)聽(tīng)
Object.defineProperty(arrayMethods, method, {
value: function testValue() {
console.log('數(shù)組被訪問(wèn)到了');
const original = arrayProto[method];
// 使類數(shù)組變成一個(gè)真正的數(shù)組
const args = Array.from(arguments);
original.apply(this, args);
}
});
});
p.walk = function(obj) {
let value;
for (let key in obj) {
// 使用 hasOwnProperty 判斷對(duì)象本身是否有該屬性
if (obj.hasOwnProperty(key)) {
value = obj[key];
// 遞歸調(diào)用,循環(huán)所有的對(duì)象
if (typeof value === 'object') {
// 并且該值是一個(gè)數(shù)組的話
if (Array.isArray(value)) {
const augment = value.__proto__ ? protoAugment : copyAugment;
augment(value, arrayMethods, key);
observeArray(value);
}
/*
如果是對(duì)象的話,遞歸調(diào)用該對(duì)象,遞歸完成后,會(huì)有屬性名和值,然后對(duì)
該屬性名和值使用 Object.defindProperty 進(jìn)行監(jiān)聽(tīng)即可
*/
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 + '被訪問(wèn)到了');
return value;
},
set: function(newVal) {
console.log(key + '被重新設(shè)置值了' + '=' + 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__的直接修改先關(guān)的屬性方法
function copyAugment(target, src, keys) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i];
def(target, key, src[key]);
}
}
// 下面是測(cè)試數(shù)據(jù)
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);
以上這篇詳談Object.defineProperty 及實(shí)現(xiàn)數(shù)據(jù)雙向綁定就是小編分享給大家的全部?jī)?nèi)容了,希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
- vue數(shù)據(jù)監(jiān)聽(tīng)解析Object.defineProperty與Proxy區(qū)別
- Vue中的Object.defineProperty全面理解
- vue用Object.defineProperty手寫(xiě)一個(gè)簡(jiǎn)單的雙向綁定的示例
- js中Object.defineProperty()方法的不詳解
- 淺談vue實(shí)現(xiàn)數(shù)據(jù)監(jiān)聽(tīng)的函數(shù) Object.defineProperty
- 詳解如何使用Object.defineProperty實(shí)現(xiàn)簡(jiǎn)易的vue功能
相關(guān)文章
vue配置electron使用electron-builder進(jìn)行打包的操作方法
這篇文章主要介紹了vue配置electron使用electron-builder進(jìn)行打包的操作方法,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),感興趣的朋友跟隨小編一起看看吧2024-08-08
詳解Jest結(jié)合Vue-test-utils使用的初步實(shí)踐
這篇文章主要介紹了詳解Jest結(jié)合Vue-test-utils使用的初步實(shí)踐,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-06-06
antd-DatePicker組件獲取時(shí)間值,及相關(guān)設(shè)置方式
這篇文章主要介紹了antd-DatePicker組件獲取時(shí)間值,及相關(guān)設(shè)置方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-10-10
element用腳本自動(dòng)化構(gòu)建新組件的實(shí)現(xiàn)示例
本文主要介紹了element-ui的用腳本自動(dòng)化構(gòu)建新組件的實(shí)現(xiàn)示例,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-12-12

