理解defineProperty以及getter、setter

呂大豹發表於2017-12-05

我們常聽說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).

這兩類描述符有兩個必選項:

  1. configurable
    從字面意思看它表示“可配置”,含義是:當它為true時,該屬性的描述符可被修改,並且該屬性可被delete刪除。同理,當它為false時,我們無法再次呼叫defineProperty去修改描述符,也不可通過delete刪除。

  2. enumerable
    從字面意思看它表示“可列舉”,含義是:當它為true時,該屬性可被迭代器列舉出來。比如使用for in或者是Object.keys。

接下來就是資料描述符(data descriptor)了,有兩個:

  1. value
    這個就是該屬性的值啦,即通過obj.key訪問時返回。任何js資料型別都可以使用(number,string,object,function等)。

  2. 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):

  1. get
    它是一個函式,訪問該屬性時會自動呼叫,函式的返回值即為該屬性的value。預設為undefined。

你可能會想,既有value又有get函式,那麼屬性的值是什麼呢?那你就想多了,這種情況在定義的時候就直接報錯了,本身邏輯就矛盾嘛。

  1. 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的方法我教給你了。

相關文章