我們常聽說vue是用getter與setter實現數據監控的,那么getter與setter到底是什么東西,它與defineProperty是什么關系,平時有哪些用處呢?本文將為大家一一道來。
對象的屬性
按照一貫的“由淺到深”行文原則,我們先溫習一下對象的屬性。我們知道對象有自身的屬性以及原型上的屬性,它們都可以通過obj.key這樣的方式訪問到。
要設置/修改對象的屬性也是很簡單的,只需obj.key='value'即可。要注意的是,如果key位于原型上,那么此時會在對象自身設置該值,而不是修改原型上的。
另外需要注意的是,原型上的屬性有時候會被for in給“不小心”遍歷出來,例如下面的代碼:
var arr = [1,2,3];
arr.__proto__.test = 4;
for(i in arr){
console.log(arr[i]);
}
//輸出:1234
所以我們一般在用for in的時候都要加上hasOwnProperty判斷,或者是拋棄for in,用forEach.
認識defineProperty
defineProperty是掛載在Object上的一個方法,作用是:為對象定義一個屬性,或是修改已有屬性的值,并設置該屬性的描述符。該方法返回修改后的對象。
如果沒有后半句作用的話,那它與obj.key = 'value'這種賦值語句沒什么兩樣。他的完整語法是這樣:Object.defineProperty(obj, prop, descriptor)
obj: 目標對象
prop: 屬性名稱
descriptor: 屬性描述符
前兩個就不必講了,需要重點理解的是第三參數。屬性描述符用于定義該屬性的一些特性。具體來講分了兩類:數據描述符(data descriptor)、訪問描述符(accessor descriptor).
這兩類描述符有兩個必選項:
configurable
從字面意思看它表示“可配置”,含義是:當它為true時,該屬性的描述符可被修改,并且該屬性可被delete刪除。同理,當它為false時,我們無法再次調用defineProperty去修改描述符,也不可通過delete刪除。enumerable
從字面意思看它表示“可枚舉”,含義是:當它為true時,該屬性可被迭代器枚舉出來。比如使用for in或者是Object.keys。
接下來就是數據描述符(data descriptor)了,有兩個:
value
這個就是該屬性的值啦,即通過obj.key訪問時返回。任何js數據類型都可以使用(number,string,object,function等)。writable
這個也很好理解,表示該屬性是否可寫。當它為false時,屬性不可被任何賦值語句重寫。然而,此時還可以調用defineProperty來修改value,當然前提是configurable為true啦。
剩下的就是訪問描述符啦,先賣個關子講兩個注意事項。
描述符的原型與默認值
一般情況,我們會創建一個descriptor對象,然后傳給defineProperty方法。如下:
var descriptor = {
writable: false
}
Object.defineProperty(obj, 'key', descriptor);
這種情況是有風險的,如果descriptor的原型上面有相關特性,也會通過原型鏈被訪問到,算入在對key的定義中。比如:
descriptor.__proto__.enumerable = true;
Object.defineProperty(obj, 'key', descriptor);
Object.getOwnPropertyDescriptor(obj,'key'); //返回的enumerable為true
為了避免發生這樣的意外情況,官方建議使用Object.freeze凍結對象,或者是使用Object.create(null)創建一個純凈的對象(不含原型)來使用。
接下來的注意點是默認值,首先我們會想普通的賦值語句會生成怎樣的描述符,如obj.key="value"
。
可以使用Object.getOwnPropertyDescriptor來返回一個屬性的描述符:
obj = {};
obj.key = "value";
Object.getOwnPropertyDescriptor(obj, 'key');
/*輸出
{
configurable:true,
enumerable:true,
value:"value",
writable:true,
}
*/
這也是復合我們預期的,通過賦值語句添加的屬性,相關描述符都為true,可寫可配置可枚舉。但是使用defineProperty定義的屬性,默認值就不是這樣了,其規則是這樣的:
configurable: false
enumerable: false
writable: false
value: undefined
所以這里還是要注意下的,使用的時候把描述符寫全,免得默認都成false了。
getter與setter
所謂getter與setter其實是兩個概念,并沒有這樣的屬性。與之對應的是兩個訪問描述符(access descriptor):
- get
它是一個函數,訪問該屬性時會自動調用,函數的返回值即為該屬性的value。默認為undefined。
你可能會想,既有value又有get函數,那么屬性的值是什么呢?那你就想多了,這種情況在定義的時候就直接報錯了,本身邏輯就矛盾嘛。
- set
它是一個函數,為該屬性賦值時會自動調用,并且新值會被當做參數傳入。
看到這里你可能就眼前一亮了,為屬性賦值的時候會自動執行一個函數,那豈不是就能監控到數據的變化,從而實現mvvm的雙向綁定?其實vue的數據監控用到的核心原理也就是這個啦。如果你用過knockout可能感受會更深,knockout能做到在IE6都支持雙向綁定,就是強制讓屬性值為函數類型,必須手動執行函數才能拿到值。
還好現在有了瀏覽器的默認支持,ES5開始就支持gettter、setter了,現在移動端基本完全可用,pc端需要IE9+。
實際應用
這么好用的方法,我們平時好像也不怎么用呀?寫業務代碼可能用到的確實少,但是當你要寫一個公共模塊乃至寫一個框架時,就可能用到啦。
比如你寫一個公共模塊,會往window上掛一些全局屬性,并且你不希望別人在其他地方不小心覆蓋這個屬性,那就可以用defineProperty讓該屬性不可寫、不可配置。貼一個我們項目中的代碼:
//向全局掛載通用方法
for(let key in methods){
if(methods.hasOwnProperty(key)){
Object.defineProperty(WIN, key, {
value : methods[key]
});
}
}
另外一個用途呢,就是你自己想干壞事。覆蓋別人寫的代碼,比如寫chrome插件刷頁面。或者說是想篡改瀏覽器的一些信息。
比如你想把瀏覽器的userAgent給改了,直接寫navigator.userAgent = 'iPhoneX'
.你再輸出一下userAgent,發現并沒有修改。這是為什么呢?我們用這行代碼看一下:
Object.getOwnPropertyDescriptor(window, 'navigator');
//輸出
{
configurable:true,
enumerable:true,
get:ƒ (),
set:undefined
}
原因就找到了,navigator是有setter的,每次取值總會執行這個set函數來做返回。但是好消息是什么呢?configurable為true,那就意味這我們可以通過defineProperty來修改這個屬性,代碼就相當簡單了:
Object.defineProperty(navigator, 'userAgent', {get: function(){return 'iphoneX'}})
console.log(navigator.userAgent); //輸出iphoneX
喏,篡改瀏覽器userAgent的方法我教給你了。
文章列表