defineProperty和Proxy基礎(chǔ)功能及性能對比
前言
最近公司項目從vue2遷移到vue3,感覺自己對Object.defineProperty和Proxy的了解還是在淺嘗輒止的地步,所以今天抽空整體對二者進行了深入(基礎(chǔ))的了解,主要是二者的基礎(chǔ)用法,性能對比,在vue中的應(yīng)用進行了探索,希望能夠幫助到想了解的小伙伴。
Object.defineProperty簡介
首先,來看看MDN上的定義:
Object.defineProperty() 方法會直接在一個對象上定義一個新屬性,或者修改一個對象的現(xiàn)有屬性,并返回此對象。
備注: 應(yīng)當直接在 Object 構(gòu)造器對象上調(diào)用此方法,而不是在任意一個 Object 類型的實例上調(diào)用。
語法
Object.defineProperty(obj, prop, descriptor)
參數(shù)說明:
obj:要定義屬性的對象。
prop:要定義或修改的屬性的名稱或Symbol。
descriptor:要定義或修改的屬性描述符。
簡單示例
let person = {};
let name = 'yuanwill';
Object.defineProperty(person, 'name', {
get() {
return name === 'yuanwill' ? 'zhangsan' : 'lisi'
},
set(newVal) {
name = newVal
}
});
console.log(person.name); // zhangsan
person.name = 'haha';
console.log(person.name); // lisi
- 讀取
person的name屬性時,訪問了get方法,第一次name是yuanwill,所以返回了zhangsan - 修改
person的name屬性時,訪問了set方法,修改了name變量的值 - 第二次讀取
person的name屬性,同理,返回了lisi。
仿vue使用
在vue2中,使用了Object.defineProperty來實現(xiàn)數(shù)據(jù)雙向綁定的基礎(chǔ)(具體的observe,watcher,dep等等balabala就不細說了),我們主要仿造vue來看看怎么通過Object.defineProperty來實現(xiàn)一個對象或數(shù)組(不扯對數(shù)組方法的攔截AOP)的屬性攔截和監(jiān)聽。
對象的攔截
準備一個對象如下:
let person = {
name: 'yuanwill',
age: 26,
address: {
home: 'guangzhou',
now: 'shenzhen'
}
};
很容易想到,我們需要遍歷person中的key,然后對每一個key進行轉(zhuǎn)換即可,于是很自然的寫出了下面的錯誤示例:
Object.keys(person).forEach(key => {
Object.defineProperty(person, key, {
get() {
console.log('攔截到正在獲取屬性:' + key);
return person[key]; // ①
},
set(val) {
console.log('攔截到正在修改屬性:' + key);
person[key] = val; // ②
}
})
})
console.log(person.name)
運行代碼發(fā)現(xiàn)棧溢出了,錯誤有兩處,代碼已經(jīng)標明:
- 在
get中,直接使用person[key] 會繼續(xù)調(diào)用get,導(dǎo)致死循環(huán) - 在
set中同理。
所以,需要使用一個方法,來傳遞person[key] 的值。
const defineReactive = (obj, key, val) => {
Object.defineProperty(obj, key, {
get() {
console.log('攔截到正在獲取屬性:' + key);
return val;
},
set(newVal) {
console.log('攔截到正在修改屬性:' + key);
val = newVal;
}
})
}
const observer = obj => {
// 如果obj不是一個對象,就沒必要包裝了
if(typeof obj !== 'object' || !obj) {
return;
}
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}
實驗一下:
observer(person); person.name = 'haha'; // 攔截到正在修改屬性:name console.log(person.name); // 攔截到正在獲取屬性:name, haha
可是,還有瑕疵,比如:
person.name = {
firstName: 'yuan',
lastName: 'will'
}; // 攔截到正在修改屬性:name
person.name.firstName = 'haha'; // 攔截到正在獲取屬性:name
console.log(person.name); // 攔截到正在獲取屬性:name
可以看到,person.name.firstName并沒有攔截到正在修改firstName屬性。原因是我們在set的時候,newVal可能也是一個object,所以也需要進行observer。
修改set如下:
set(newVal) {
if(typeof newVal === 'object') {
observer(newVal);
}
console.log('攔截到正在修改屬性:' + key);
val = newVal;
}
當然,還有瑕疵,比如訪問深層對象:
console.log(person.address.home) // 攔截到正在獲取屬性:address
并沒有攔截到訪問屬性home,所以我們還需要判斷val如果是對象也應(yīng)該再一次observer。優(yōu)化后的完整代碼如下:
const defineReactive = (obj, key, val) => {
if(typeof val === 'object') {
observer(val);
}
Object.defineProperty(obj, key, {
get() {
console.log('攔截到正在獲取屬性:' + key);
return val;
},
set(newVal) {
if(typeof newVal === 'object') {
observer(newVal);
}
console.log('攔截到正在修改屬性:' + key);
val = newVal;
}
})
}
const observer = obj => {
// 如果obj不是一個對象,就沒必要包裝了
if(typeof obj !== 'object' || !obj) {
return;
}
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}
數(shù)組的攔截
我們總說,Object.defineProperty不能攔截數(shù)組,這種說法不太準確,看示例:
let list = [1,2,3,4]; observer(list); console.log(list[0]) // 攔截到正在獲取屬性:0 list[0] = 2; // 攔截到正在修改屬性:0 list[6] = 6; // 無法攔截... list.push(3); // 無法攔截...
可以看到,通過索引去訪問或修改已經(jīng)存在的元素,是可以攔截到的。如果是不存在的元素,或者是通過push等方法去修改數(shù)組,則無法攔截。
正因為如此,vue2在實現(xiàn)的時候,通過重寫了數(shù)組原型上的七個方法(push、pop、shift、unshift、splice、sort、reverse)來解決(具體可以看vue/src/core/observer/array.js),就不展開了。
Proxy簡介
同樣,來看看MDN上的定義:
Proxy 對象用于創(chuàng)建一個對象的代理,從而實現(xiàn)基本操作的攔截和自定義(如屬性查找、賦值、枚舉、函數(shù)調(diào)用等)。
語法
const p = new Proxy(target, handler)
參數(shù)說明:
target:要使用Proxy包裝的目標對象(可以是任何類型的對象,包括原生數(shù)組,函數(shù),甚至另一個代理)。handler:一個通常以函數(shù)作為屬性的對象,各屬性中的函數(shù)分別定義了在執(zhí)行各種操作時代理p的行為。
handler對象總共有13個屬性方法,具體的可以參考MDN,就不一一列舉了。
簡單示例
let person = {
name: 'yuanwill'
}
let personProxy = new Proxy(person, {
get(target, key) {
return target[key] === 'yuanwill' ? 'zhangsan': 'lisi'
},
set(target, key, val) {
target[key] = val;
return true;
}
});
console.log(personProxy.name); // zhangsan
personProxy.name = 'haha';
console.log(personProxy.name); // lisi
案例及其簡單,就不介紹了。
攔截的本質(zhì)
proxy的攔截,并不是“萬事萬物”都攔截,看MDN上面的定義,是對基本操作的攔截和自定義,那么何為基本操作呢,看下面的例子:
const person = {
name: 'yuanwill',
say() {
console.log('你好呀')
}
}
let personProxy = new Proxy(person, {
get(target, key) {
console.log('攔截到正在獲取屬性:' + key);
return target[key]
},
set(target, key, val) {
console.log('攔截到正在修改屬性:' + key);
target[key] = val;
},
apply(target, thisArg, arguments) {
console.log('攔截到了正在執(zhí)行的方法:' + target);
return target.call(thisArg, ...arguments)
}
})
console.log(personProxy.name); // 攔截到正在獲取屬性:name
personProxy.name = 'haha'; // 攔截到正在修改屬性:name
personProxy.say(); // 攔截到正在獲取屬性:say
重點在最后一句代碼,發(fā)現(xiàn)personProxy.say()并沒有走入apply方法中,原因就在于只攔截基本操作。
那么到底什么是基本操作呢?像上面的personProxy.name這種屬性的讀取,personProxy.name = 'haha'這種屬性的賦值就是基本操作,而personProxy.say()是由兩個基本操作(personProxy.say的讀取以及函數(shù)的調(diào)用)組成的復(fù)合操作,我們代理的對象是person,而不是person.say,所以,我們只攔截到了person.say的讀取操作。
孿生兄弟Reflect
來看看MDN上的定義:
Reflect 是一個內(nèi)置的對象,它提供攔截 JavaScript 操作的方法。這些方法與proxy handlers (en-US)的方法相同。Reflect不是一個函數(shù)對象,因此它是不可構(gòu)造的。
換句話說,Reflet對象的方法和proxy的攔截器(第二個入?yún)andler)的方法完全一致,因此也有13個方法,就不一一列舉了。
Reflect的作用也及其簡單,可以參考MDN上。
那么,為什么我們需要Reflect呢,來看下面的例子:
const person = {
name: 'yuanwill',
get firstName() {
return this.name;
}
};
const personProxy = new Proxy(person, {
get(target, key) {
console.log('攔截到正在獲取屬性:' + key);
return target[key]
},
set(target, key, val) {
console.log('攔截到正在修改屬性:' + key);
target[key] = val;
}
});
console.log(personProxy.firstName); // 攔截到正在獲取屬性:firstName
按照我們的理解,應(yīng)該還需要攔截到name屬性,因為我們在firstName中返回的是name屬性,那么為什么沒有攔截到呢?關(guān)鍵在于this指向問題,personProxy.firstName會被get攔截,然后返回target[key],這里的target就是person,key就是firstName,所以這個時候的this.name就是person.name,而我們的代理對象是personProxy,所以訪問name屬性就不會被攔截了。
那這個時候,Reflect就派上用場了:
const personProxy = new Proxy(person, {
get(target, key, receiver) {
console.log('攔截到正在獲取屬性:' + key);
return Reflect.get(target, key, receiver);
},
set(target, key, val, receiver) {
console.log('攔截到正在修改屬性:' + key);
return Reflect.set(target, key, val, receiver);
}
});
這個時候,就能攔截到了。原因在于,Reflect.get中的第三個參數(shù)receiver作用就是改變this的指向,MDN描述如下:
如果target對象中指定了getter,receiver則為getter調(diào)用時的this值。
仿vue使用
對象的攔截
還是使用上面的對象:
let person = {
name: 'yuanwill',
age: 26,
address: {
home: 'guangzhou',
now: 'shenzhen'
}
};
我們很自然就能寫出如下代碼:
const observer = obj => {
// 如果obj不是一個對象,就沒必要包裝了
if(typeof obj !== 'object' || !obj) {
return obj;
}
const proxyConfig = {
get(target, key, receiver) {
console.log('攔截到正在獲取屬性:' + key);
return Reflect.get(target, key, receiver)
},
set(target, key, val, receiver) {
console.log('攔截到正在修改屬性:' + key);
return Reflect.set(target, key, val, receiver);;
}
};
const observed = new Proxy(obj, proxyConfig);
return observed;
}
測試一下:
const personProxy = observer(person); personProxy.name = 'haha'; // 攔截到正在修改屬性:name console.log(personProxy.name); // 攔截到正在獲取屬性:name
當然,也有瑕疵:
personProxy.name = {
firstName: 'yuan',
lastName: 'will'
}; // 攔截到正在修改屬性:name
personProxy.name.firstName = 'haha'; // 攔截到正在獲取屬性:name
console.log(personProxy.name); // 攔截到正在獲取屬性:name
可以看到,person.name.firstName依然沒有攔截到正在修改firstName屬性。原因在于,get返回的可能是個對象,我們需要對這個對象再次代理,所以修改如下:
const observer = obj => {
// 如果obj不是一個對象,就沒必要包裝了
if(typeof obj !== 'object' || !obj) {
return obj;
}
const proxyConfig = {
get(target, key, receiver) {
console.log('攔截到正在獲取屬性:' + key);
const result = Reflect.get(target, key, receiver);
return observer(result);
},
set(target, key, val, receiver) {
console.log('攔截到正在修改屬性:' + key);
return Reflect.set(target, key, val, receiver);;
}
};
const observed = new Proxy(obj, proxyConfig);
return observed;
}
仔細分析上面的代碼,我們在get的時候,才去判斷了獲取的值是不是一個對象,而Object.defineProperty是最開始就循環(huán)遍歷,對每個屬性進行代理,所以,這樣性能就提升了。同時,我們獲取personProxy.address.home也能攔截到home屬性了(想想就知道為啥了)。
數(shù)組的攔截
let list = [1,2,3,4]; let listProxy = observer(list); console.log(listProxy[0]) // 攔截到正在獲取屬性:0 listProxy[0] = 2; // 攔截到正在修改屬性:0 listProxy[6] = 6; // 攔截到正在修改屬性:6 /** * 攔截到正在獲取屬性:push * 攔截到正在獲取屬性:length * 攔截到正在修改屬性:7 * 攔截到正在修改屬性:length */ listProxy.push(3);
可以看到,proxy天然的解決了數(shù)組的相關(guān)問題。
最后
Object.defineProperty和Proxy的相關(guān)基礎(chǔ)就介紹完了,文章只是講解了比較基礎(chǔ)的功能,學(xué)無止境,沒辦法了,慢慢來把~~~
更多關(guān)于defineProperty Proxy基礎(chǔ)功能的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
vue-seamless-scroll 實現(xiàn)簡單自動無縫滾動且添加對應(yīng)點擊事件的簡單整理
vue-seamless-scroll是一個基于Vue.js的簡單無縫滾動組件, 基于requestAnimationFrame實現(xiàn),配置多滿足多樣需求,目前支持上下左右無縫滾動,單步滾動,及支持水平方向的手動切換功能,本節(jié)介紹,vue添加 vue-seamless-scroll實現(xiàn)自動無縫滾動的效果,并對應(yīng)添加點擊事件2023-01-01
Vue 3集成海康Web插件實現(xiàn)視頻監(jiān)控功能
本文詳細介紹了如何使用 Vue 3 框架集成??礧eb插件實現(xiàn)視頻監(jiān)控功能,通過定義屬性、事件、變量,以及編寫初始化、播放視頻、處理節(jié)點點擊事件等方法,我們成功實現(xiàn)了視頻監(jiān)控系統(tǒng)的前端部分,感興趣的朋友一起看看吧2024-11-11
Vue?Axios請求取消和重發(fā)封裝的實現(xiàn)代碼
這篇文章主要介紹了Vue?Axios請求取消和重發(fā)的封裝的實現(xiàn),文章通過代碼示例講解的非常詳細,對大家的學(xué)習(xí)或工作有一定的幫助,需要的朋友可以參考下2024-09-09
詳解Vue.js組件可復(fù)用性的混合(mixin)方式和自定義指令
本篇文章主要介紹了詳解Vue.js組件可復(fù)用性的混合(mixin)方式和自定義指令,具有一定的參考價值,有興趣的可以了解一下2017-09-09

