深入瞭解Object.defineProperty
原來寫文章都是一次寫兩三個小時寫完,偶爾看到一個人的部落格瞭解到還有草稿箱這個功能,所以以後寫文章的時候就舒服多了哈哈,可以存起來再發,不需要一口氣寫完了
最近一直在看JavaScript高階程式設計,看到defineProperty的時候感受挺深的,因為大名鼎鼎的Vue的雙向資料繫結的原理就是根據這個東西來的,所以看到這裡的時候長了見識
要說起Object.defineProperty的話,需要先來介紹一下JavaScript中的物件。
JavaScript中的物件
面向物件的語言有一個標誌,那就是他們都有類的概念,而通過類可以建立任意多個具有相同屬性和方法的物件。ECMAScript中沒有類的概念,因此它的物件也與基於類的語言中的物件有所不同。
JavaScript中的物件定義: 無序屬性的集合,其屬性可以包含基本值、物件或者函式。嚴格來講,這就相當於說物件是一組沒有特定順序的值。物件的每個屬性或方法都有一個名字,而每個名字都對映到一個值。正因為這樣,可以把物件想象成散列表,就是一個鍵值對,其中的值可以是任意的(資料,函式,物件,陣列~~)
前面balabala一堆概念,真心感覺如果可以理解挺重要的
建立物件
可以使用建構函式,建立一個Object的例項,然後為其新增屬性和方法
var person = new Object(); person.name = 'zhang'; person.age = 123; person.job = "SoftWare Engineer"; person.sayName = function () { alert(this.name); }; person.sayName();
這個例子建立了一個名為person的物件,併為其添加了三個屬性(name、age、和job)和一個方法(sayName),其中sayName()方法用於顯示this.name 這個會被解析為person.name和值。
不過現在建立這種物件大都是通過字面量的方式來建立了
var person = { name: 'zhang', age: 18, job: 'SoftWare Engineer', sayName: function () { alert(this.name); } }; person.sayName();
這種寫法的結果和上面的那一種是一樣的,這種寫法如果你要拓展功能的話就不能再賦值了,也是用到了上面的寫法
person.home = 'hebei'; person.showHome = function () { alert(this.home); };
這個樣子去拓展它。
屬性型別
ES5定義只有內部採用的特性時,描述了屬性的各種特徵。定義這些特性時為了實現JavaScript引擎用的,因此在JavaScript中不能直接訪問它們。為了表示特徵是內部值,該規範把它們放在了兩對兒方括號中,例如[[Enumerable]]
前面例子中的name,age,job,home,showHome都是屬性,引號後面的東西是值,說一下屬性的一些東西。
ECMAScript中有兩種屬性: 資料屬性和訪問器屬性
資料屬性
資料屬性包含一個數據值的位置。在這個位置可以讀取和寫入值。資料屬性有4個描述其行為的特性。
[[Configurable]] 表示能否通過delete刪除屬性從而重新定義屬性,能夠通過屬性的特性,或者能否把屬性修改為訪問器屬性,像前面的例子中那樣直接在物件上定義的屬性,它們的這個特性預設值為true。
[[Enumerable]] 表示能否通過for-in迴圈返回屬性。像前面例子中那樣直接在物件上定義的屬性,他們的這個特性預設值為true
[[Writable]] 表示能夠修改屬性的值。像前面例子中那樣直接在物件上定義的屬性,他們的這個特性預設值為true
[[Value]] 包含這個屬性的資料值。讀取屬性值的時候,從這個位置讀;寫入屬性值的時候,把新值儲存在這個位置。這個特性的預設值為undefined
例子
var person = { name: 'zhang' }; for(var key in person){ console.log(key); } // 他這個屬性的value值就是他在上面所定義的那一些,對這個值的任何修改都反映在這個位置 console.log(person.name); person.name = 'goudan'; // 在這裡也可以修改,因為[[Writable]]屬性預設為true console.log(person.name); // [[Configurable]] 這個東西為true 表示也可以刪除它 delete person.name; console.log(person.name); // undefined 因為刪除之後這個屬性就不存在了 [[Value]]特性的預設值為undefined
重點來了
Object.defineProperty()
如果要修改 屬性預設的特性 就必要要使用ES5的Object。defineProperty()方法。這個方法接收三個引數。
屬性所在的物件、 屬性的名字、 一個描述符物件 (其中描述符物件的屬性必須是: configurable、enumerable、writeable、和value)設定其中一個或多個值,可以修改對應的特性值
下面去演示這些例子了,希望你也可以試著去嘗試一遍,我舉得例子寫法可能比較囉嗦,希望沒有嘗試過的可以試著練習一下
提示:我後面 // 註釋後面的東西 是輸出列印的東西,如有錯誤可以自己具體情況分析一下
writable的情況演示
var person = { name: 'zhang', age: 12 }; Object.defineProperty(person,"name",{ writable: false }); // 讀取person.name console.log(person.name); // zhang console.log(person.age); //12 // 修改person.name person.name = 'wang'; person.age = 14; console.log(person.name); // zhang ?????? 咋沒變 console.log(person.age); // 14 // 原因: 那個配置裡面也說了的[[Writable]]預設是true,但是改為false之後 就是這個屬性不可寫,沒有寫進去的也可以看到是可以正常修改的
enumerable的情況演示
var person = { name: 'zhang', baba: '1', job: 'Software Engineer' }; for(var key in person){ console.log(key); // name, baba, job } // 三個屬性都可以正常的打印出來// 配置一下 enumerable Object.defineProperty(person,'baba',{ enumerable: false }); for(var key1 in person){ console.log(key1); // name job } // ??? baba去哪了? 我告訴你去哪了 因為配置了 enumerable: false console.log(person.baba); // 1 不會影響輸出
Configurable 的演示
var person = { name: 'zhang', age: 12 }; Object.defineProperty(person,'name',{ configurable: false }); console.log(person.name); // zhang console.log(person.age); // 12 delete person.name; console.log(person.name); // zhang // 剛才刪除不是undefined嗎 configurable: false就不允許刪除了 // 如果宣告是嚴格模式的話 那一句 delete person.name 還會報錯 person.name = 'wang' console.log(person.name); // wang
注意:這個配置有一點需要注意一下
var person = { name: 'zhang' }; Object.defineProperty(person,'name',{ configurable: false }); // 如果以後你有需求你還想刪除他的話 你會想到 Object.defineProperty(person,'name',{ configurable: true }); // 控制檯列印錯誤訊息: Uncaught TypeError: Cannot redefine property: name at Function.defineProperty (<anonymous>) // 這個東西一旦被配置為 false,以後在也不可以把他變為true了 會報錯
Value的演示
var person = { name: 'zhang' }; console.log(person.name); // zhang Object.defineProperty(person,'name',{ value: 'wang' }); console.log(person.name); // wang // 你小子怎麼改姓了 person.name = 'zhang'; // 給我改回來 console.log(person.name); //zhang 又是我老張家的孩子了,這個是可以修改回來的
當然了上面的屬性只是單獨演示的, 這四個屬性可以結合起來使用,比如我要定義一個永遠都無法改變它的值
var person = { name: 'zhang' }; console.log(person.name); // zhang Object.defineProperty(person,'name',{ writable: false, value: 'wang' }); console.log(person.name); // wang // 臥槽你個小兔崽子又改姓了 給我改回來 person.name = 'zhang'; console.log(person.name); // wang // 你會發現 有些事情發生了就是發生了,永遠也回不來了 person.name = 'zhang1'; console.log(person.name); // wang
注意 在呼叫 Object.defineProperty()方法時,如果不指定,configurable、enumerable和writable特性的預設值都是true。
你給我演示這一堆東西到底有啥意思,能幹啥。我想告訴你多數情況下可能都沒有必要利用Object.defineProperty()方法提供的這些高階功能。不過,理解這些概念對理解JavaScript物件卻非常有用
訪問器屬性
訪問器屬性不包含資料值;他們包含一對兒getter和setter函式(不過這兩個函式都不是必須的)在讀取訪問器屬性時,會呼叫getter函式,這個函式負責返回有效的值;在寫入訪問器屬性時,會呼叫setter函式並傳入新值,這個函式負責決定如何處理資料。訪問器屬性有如下4個特性
[[Configurable]] : 能否修改屬性的特性,或者能否把屬性修改為資料屬性。像前面的例子中那樣直接在物件上定義的屬性,它們的這個特性預設值為true
[[Enumerable]] : 能否通過for-in迴圈 *********和上面的一樣[[Get]]: 在讀取屬性的時候呼叫。 預設值 undefined[[Set]]: 在寫入屬性的時候呼叫 預設值 undefined
前面兩個屬性大同小異,幾乎是一樣的,同樣的訪問器屬性不能直接定義,必須使用Object.defineProperty()來定義.
get和set的演示,
這個重要
var book = { _year: 2018, edition: 1 }; Object.defineProperty(book,'year',{ get: function () { console.log(`獲取的時候觸發函式`); // 每次獲取的時候都會觸發這個函式 console.log(this === book); // true 這裡面的this就是book的引用 也就是book物件 return this._year; }, set: function (newValue) { console.log('修改值的時候觸發函式'); // newValue ----- 這裡的newValue就是你設定的修改後的值 if(newValue> 2018){ this._year = newValue; this.edition = newValue - 2018; } } }); console.log(book.year); // 2018 他的值也就是我們返回的值 book._year // 如果get函式不寫返回值就是undefined book.year = 2020; // 修改值的時候觸發函式 console.log(book); // {_year: 2019, edition: 2};
vue雙向資料繫結就是這個,當我們在修改資料的時候,會觸發set那個函式,就在set函式中執行我們的邏輯,去修改資料,以後我會寫一個大概的雙向資料繫結的那個程式碼,會發出來,網上也有好多類似的程式碼。
上面的程式碼建立了一個book物件,並給他定義兩個預設的屬性: _year和edition。
而訪問器屬性year則包含一個getter函式和一個setter函式。 getter函式返回_year的值,setter函式修改了_year和edition。這是使用訪問器屬性的常見方式,即設定一個屬性的值會導致其他屬性發生變化。
不一定非要同時制定getter和setter。只指定getter意味著屬性是不能寫,嘗試寫入屬性會被忽略。在嚴格模式下,嘗試寫入至指定了getter函式的屬性會丟擲錯誤。類似的,只指定setter函式的屬性也不能讀,否則在非嚴格模式下回返回undefined,而在嚴格模式下會丟擲錯誤。
同時定義多個屬性的寫法由於未物件定義多個屬性的可能性很大,ES5又定義了一個Object.defineProperties()方法利用這個方法可以通過描述符一次定義多個屬性。
這個方法接收兩個物件引數:第一個物件是喲啊新增和修改其屬性的物件,第二個物件的屬性與第一個物件中要新增或修改的屬性一一對應。
同時定義多個屬性的寫法
var book = {}; Object.defineProperties(book,{ _year: { value: 2018 }, edition: { value: 1 }, year: { get: function () { return this._year; }, set: function (newValue) { console.log('修改函式觸發觸發了嗎'); if(newValue>2018){ this._year = newValue; this.edition += newValue - 2018; } } } }); console.log(book.year); book.year = 2020; console.log(book); //{_year: 2018, edition: 1} // ???? 我前面不是觸發了修改的函數了嗎為什麼沒有改呢 上一個分開寫的都修改成功了
彆著急,下面來看一個新的API
Object.getOwnPropertyDescriptor()
讀取屬性的特性
使用Es5的Object.getOwnPropertyDescription() 方法,可以取得給定屬性描述符。這個方法接收兩個引數: 屬性所在的物件和要讀取其描述符的屬性名稱。返回值是一個物件,如果是訪問器屬性,這個物件的屬性有 configurable enumerable writable 和value
var descriptor1 = Object.getOwnPropertyDescriptor(book,"_year"); var descriptor2 = Object.getOwnPropertyDescriptor(book,"edition"); console.log(descriptor1); /* {configurable: false, enumerable: false, value: 2018, writable: false} */ console.log(descriptor2); /* { configurable: false, enumerable: false, value: 1, writable: false} */ */ // 前面分開寫可以成功的原因可以用這個東西去獲取一下,可以看到他們是true的,這個是false,所以 // 雖然你修改了,但是他是不生效的
在JavaScript中,可以針對任何物件------包括DOM和BOM物件,使用Object.getOwnPropertyDescriptor()方法。
如果你閱讀了本文章有了一些收穫,我會感到非常開心,由於能力有限,文章有的部分解釋的不到位,希望在以後的日子裡能慢慢提高自己能力,如果不足之處,還望指正。