js 的 defineProperty 資料劫持
技術標籤:JavaScriptvuevuejs
我們知道,在Vue中我們操作某個屬性時vue可以實時響應,這是因為當一個vue例項被創建出來之後,它會生成一個Observer觀察者例項,然後對vue中的物件屬性進行遍歷,通過defineProperty將它們轉換為了getter和setter,當我們操作某個資料時,就會觸發getter和setter,從而完成一個監聽操作。
下面我們詳細解釋一下defineProperty以及它的優劣所在:
Object.defineProperty(obj,"",{})
defineProperty總共有三個引數,第一個引數表示我們將在哪個物件上進行操作
第二個引數表示為要操作的是哪個欄位,注意,這個引數是一個字元格式的
第三個引數為一個物件,它用來設定我們操作的這個屬性需要哪些特性
一、defineProperty的第三個引數總共有六個屬性可以設定,第一個屬性為 value
let obj = {
age:19,
};
Object.defineProperty(obj,"name",{
value:"zhangsan"
})
obj.name = 666;
console.log(obj.name);
這個屬性代表我們設定的這個name欄位的值為 "zhangsan" , 注意,現在我們只設置了一個屬性【value】,在預設情況下這就是一個只讀欄位,我們對他進行修改不會有效,因此將他賦值為 666 不會有任何效果。
它的結果仍然是張三
二、 writable
這個屬性代表的是這個name欄位是否是可寫欄位,在預設我們不設定的情況下它為false,即不可寫,因此上面我們對這個欄位進行賦值就無效
let obj = {
age:19,
};
Object.defineProperty(obj,"name",{
value:"zhangsan",
writable:true,
})
obj.name = 666;
console.log(obj.name);
現在我們將它設定為true,因此此時它是可寫屬性,所有修改就會有效了
三、enumerable
這個屬性用於設定我們定義的這個name欄位是否可以被列舉,預設我們不設定的情況下它為false,即不可列舉,因此當我們去查詢這個鍵值時查詢不到
(列舉的意思就是是否可以被列舉,被查詢出來)
let obj = {
age:19,
};
Object.defineProperty(obj,"name",{
value:"zhangsan",
writable:true,
})
obj.name = 666;
console.log(Object.keys(obj));
//Object.keys方法用於查詢此物件中的所有欄位,並用一個數組返回
此時因為我們沒有設定,因此預設情況下它是查不到這個name欄位的
只能查到 age欄位,並沒有name欄位,但若我們將enumerable屬性設定為true
let obj = {
age:19,
};
Object.defineProperty(obj,"name",{
value:"zhangsan",
writable:true,
enumerable:true,
})
obj.name = 666;
console.log(Object.keys(obj));
此時它就可以查到這個name欄位了。
四、configurable
這個屬性用於設定該欄位是否可以被刪除,或者是否可以更改它的特性,為false表示不能刪除和更改,true則表示允許刪除和更改;它的預設值是false,即不允許刪除和更改特性。
let obj = {
age:19,
};
Object.defineProperty(obj,"name",{
value:"zhangsan",
writable:true,
enumerable:true,
configurable:false
})
//delete 關鍵字用於刪除某個物件中的某個屬性
delete obj.name;
console.log(obj);
此時我們執行delete是無效的,因為它現在是不可刪除的,此時這個name欄位仍然存在
並且為false時我們不能再次更改這個欄位的特性,否則會報錯;
let obj = {
age:19,
};
Object.defineProperty(obj,"name",{
value:"zhangsan",
writable:true,
enumerable:true,
configurable:false
})
//此處我再次對它的特性進行更改,將writable和enumerable設定為false
Object.defineProperty(obj,"name",{
value:"zhangsan",
writable:false,
enumerable:false,
configurable:false
})
但如果configurable為true時,則又是另一種效果:
let obj = {
age:19,
};
Object.defineProperty(obj,"name",{
value:"zhangsan",
writable:true,
enumerable:true,
configurable:true
})
delete obj.name
console.log(obj)
此時我們可以看到,name欄位已經被刪除了,並且這個時候我們再去更改它的特性它也不會報錯了。
五、get和set
這兩個屬性用於監聽欄位的讀寫操作,也是用的最多的兩個屬性,注意,這兩個屬性不能和writable以及value屬性同時出現,否則會報錯。
當我們去操作這個欄位時,它就會觸發get方法和set方法,從而實現監聽操作
let obj = {
age:19,
};
let tempName = ""
Object.defineProperty(obj,"name",{
get(){
console.log("讀取了這個屬性")
return tempName;
},
set(val){
console.log("設定了這個屬性")
tempName = val;
}
})
obj.name = 999;
console.log(obj.name)
使用get和set我麼可以做一些有趣的事,比如我們可以讓一個變數同時等於多個值
let obj = {
age:19,
};
let tempName = 0;
Object.defineProperty(obj,"name",{
get(){
tempName++;
return tempName;
},
set(val){
tempName = val;
}
})
console.log(obj.name===1 && obj.name===2 && obj.name===3)
缺點
defineProperty無法針對整個物件,它只能給物件的某一個屬性進行監聽,如果我們想監聽整個操作,必須使用遍歷方法對整個物件進行遍歷,一個一個的對他們執行監聽操作,這麼做起來比較麻煩。
let data = {
name:"張三",
age:"19",
gender:"男",
}
let temp = {}
Object.keys(data).forEach(key=>{
temp[key] = data[key]
Object.defineProperty(data,key,{
get(){
console.log(`獲取了${key}`)
return temp[key]
},
set(val){
console.log(`設定了${key}`)
temp[key] = val;
}
})
})
console.log(data.name)
console.log(data.age)
console.log(data.gender)
其次,第二個問題在於它無法監聽一些陣列api的操作
push,sort,pop,unshift,shift,splice,reverse
這七個api它監聽不到;
let arr = [1,2,3,4,5]
Object.defineProperty(window,"data",{
get(){
console.log("獲取了陣列")
return arr;
},
set(val){
console.log("設定了陣列")
}
})
data.push("12312312")
當我們執行push時,它只觸發了get方法,但是set方法卻並沒有被觸發,因為它無法監聽push方法的操作;
在Vue中能監聽到是因為它自己重寫了這些方法,它其實使用了函式劫持的方式,自己對這些方法進行了重寫,事實上它對我們在data中定義的陣列進行了原型鏈重寫,將其指向了vue自己定義的陣列方法上,從而讓這些重寫的方法來進行資料劫持,這樣當我們呼叫陣列api時就可以通知依賴更新。如果陣列中還包含了引用型別,它還會對引用型別再次遞迴遍歷進行監控,這樣就實現了檢測陣列的變化;
一個簡單實現:
let methods = ["push","pop","splice","sort","unshift","shift","reverse"];
let ArrayM = [];
methods.forEach(method=>{
let original = Array.prototype[method]
ArrayM[method] = function(){
console.log(`呼叫了${method}`);
return original.apply(this,arguments)
}
})
let list = [];
list.__proto__ = ArrayM;
list.push(123);
list.reverse();