理解defineProperty以及getter、setter


我們常聽說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的方法我教給你了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM