1. 程式人生 > 其它 >js 的 defineProperty 資料劫持

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();