Object實在是JavaScript中很基礎的東西了,在工作中,它只有那么貧瘠的幾個用法,讓人感覺不過爾爾,但是我們真的了解它嗎?
1. 當我們習慣用
var a = { name: 'tarol', age: 18 }; console.log(a.age); //18 a.age = 19; console.log(a.age); //19
初始化和訪問對象的時候,誰會在意這種方式也是合法的:
var a = { name: 'tarol', _age: 18, set age(value) { this._age = value; }, get age() { return this._age; } }; console.log(a.age); //18 a.age = 19; console.log(a.age); //19
2. 當我們習慣用
function A() { this.name = 'tarol'; } var a = new A(); function B() { this.age = 18; } B.prototype = a; var b = new B(); console.log(b.name); //tarol
實現繼承的時候,誰會在意其實也可以這樣:
var a = { name: 'tarol' }; var b = Object.create(a); b.age = 18; console.log(b.name); //tarol
3. 當我們知道原型鏈以后,想惡作劇修改內置函數的原型,卻發現沒有辦法
var a = {}; Object.prototype = a; console.log(Object.prototype === a); //false
如果你感興趣,那么我從頭說起:
首先,JavaScript中的對象是什么?ES5中只給出一句話,對象是屬性的集合。它只是一個盒子,它能做什么,取決於盒子里有什么。
那么,屬性是什么,一般看來,屬性是一個key, value對,這個說法是對的嗎?我們來剖析下屬性。
從一個程序員的角度來說,屬性分為可通過JS調用的的和不可通過JS調用的。不可調用的叫做內部屬性,那么可調用的我們對應着叫外部屬性吧。內部屬性是JS解釋器實現各種接口的時候使用的算法中需要調用的屬性,舉個栗子,有個內部屬性叫[[Put]],這是一個內部方法,傳入屬性名和值,它的作用就是為屬性賦值。所以當我們使用a.age = 18的時候,實際就調用到了這個內部屬性。而外部屬性又分為兩種,一種是數據屬性,一種是訪問器屬性。上面的例一中,第二種方式給對象a添加了三個屬性,其中name、_age是數據屬性,age是訪問器屬性。當屬性是數據屬性的時候,屬性是key、value對的說法好像是對的,但當屬性是訪問器屬性的時候,這個說法好像有問題了,因為一個key對應的是一個setter和一個getter。所以,這個說法是錯的?
其實,屬性不是我們看到的那樣,單單就一個key對應一個數據或者一個setter加一個getter。屬性還存在其他一些狀態,我們稱之為特性,無論是數據屬性還是訪問器屬性,都存在四個特性。數據屬性的特性為:[[Value]]、[[Writable]]、[[Enumerable]]、[[Configuration]],訪問器屬性的特性為:[[Get]]、[[Set]]、[[Enumerable]]、[[Configuration]]。其中[[Value]]、[[Get]]、[[Set]]相信已經很好理解了,[[Writable]]描述數據屬性是否可被重新賦值,[[Enumerable]]描述屬性是否可被for-in遍歷,[[Configuration]]描述屬性特性是否可被修改(一旦設置為false則不可以再修改此特性)。
JS開放了三個接口用於設置和獲取屬性的特性,分別是Object.defineProperty、Object.defineProperties和Object.getOwnPropertyDescriptor。
var a = { name : 'tarol', age : 18, job : 'coder' }; Object.defineProperty(a, 'name', { value: 'ctarol', writable: true, enumerable: true, configuration: true }); Object.defineProperties(a, { age: { value: 19, writable: true, enumerable: true, configuration: true }, job: { value: 'mental', writable: true, enumerable: true, configuration: true } }); console.log(a.name); //tarol console.log(a.age); //19 console.log(Object.getOwnPropertyDescriptor(a, 'job')); //Object {value: "mental", writable: true, enumerable: true, configurable: true}
總的看來,屬性還是可以作為一個key, value對的,但這個value不是我們賦的值,而是整個屬性特性的集合,我們稱之為屬性描述。
外部屬性的問題解決了,內部屬性我們還只是蜻蜓點水般淺嘗輒止,所以接下來我們開始從內部屬性入手,對JS中的對象做一個更深刻的認識。以下是內部屬性的表格:
屬性名 | 用途 | 屬性類型 | 方法返回值 (僅適用方法) |
他處引用 (僅適用數據) |
他處賦值 (僅適用數據) |
他處調用 (僅適用方法) |
調用其他 (僅適用方法) |
[[Prototype]] | 對象原型 | Object | __proto__ etc. |
||||
[[Class]] | 對象類型 | String | Object.prototype.toString() | ||||
[[Extensible]] | 可否添加屬性 | Boolean | Object.seal(obj) --> false Object.freeze(obj) --> false Object.preventExtensions(obj) --> false |
||||
[[GetOwnProperty]] | 返回自身指定的屬性描述 | func('prop') | 屬性描述 | Object.getOwnPropertyDescriptor(obj, 'prop') [[GetProperty]] |
|||
[[GetProperty]] | 返回原型鏈上指定的屬性描述 | func('prop') | 屬性描述 | [[GetOwnProperty]] | |||
[[HasProperty]] | 返回原型鏈上是否有指定屬性 | func('prop') | Boolean | [[GetProperty]] | |||
[[DefineOwnProperty]] | 創建或修改自身的屬性描述 | func('prop', desc, Boolean) | Boolean | Object.defineProperty(obj, 'prop', desc) Object.defineProperties(obj, descs) |
|||
[[DefaultValue]] | 將對象轉換為對應的基礎類型 | func(String/Number) | String / Number | toString() valueOf() |
|||
[[Delete]] | 刪除對象的屬性 | func('prop', Boolean) | Boolean | [[GetOwnProperty]] | |||
[[CanPut]] | 可否設置屬性的值 | func('prop') | Boolean | [[GetOwnProperty]] [[GetProperty]] [[Extensible]] |
|||
[[Get]] | 獲取屬性的值 | func('prop') | mixin | [[GetProperty]] | |||
[[Put]] | 設置屬性的值 | func('prop', mixin, Boolean) | Boolean | [[CanPut]] [[GetOwnProperty]] [[GetProperty]] [[DefineOwnProperty]] |
上面的表格稍顯晦澀,看不懂不要緊,我們來分下類。內部屬性中除了[[Class]]、[[DefaultValue]]用於展示信息以外,其他都是用來操作外部屬性的,可見對象的核心就是屬性。其中我列出[[CanPut]]和[[Put]]的算法實現,因為這兩個方法的實現涵蓋了基本所有的屬性操作和思想。
[[CanPut]]:
[[Put]]:
前面提到過,我們使用a.age=18進行賦值的時候,調用的就是[[Put]]這個內部方法。由上圖算法可知,當對屬性賦值時,只要這個屬性不是原型鏈上的訪問器屬性,那么就會修改或產生自身的數據屬性,即不存在一種情況,就是修改原型鏈上的數據屬性。我們測試下:
var a = { name: 'tarol', _age: 18, set age(value) { this._age = value; }, get age() { return this._age; } }; var b = Object.create(a); console.log(b.hasOwnProperty('name')); //false console.log(b.hasOwnProperty('_age')); //false console.log(b.hasOwnProperty('age')); //false b.name = 'okal'; b.age = 19; console.log(b.hasOwnProperty('name')); //true console.log(b.hasOwnProperty('_age')); //true console.log(b.hasOwnProperty('age')); //false console.log(a.name); //tarol console.log(a.age); //18
由結果可知,我們在對name這個原型鏈上的數據屬性進行賦值時,實際上是重新創建了一個自身屬性,對原型上的數據屬性是沒有影響的。而調用訪問器屬性age的[[Set]]方法的時候,傳入的this也是當前的對象而不是訪問器屬性的擁有者,所以在當前對象上創建了一個自身屬性_age。
好了,上面說的是通用的內部屬性,即Object類型的內部屬性,而像Boolean、Date、Number、String、Function等擁有更多的內部屬性,就留到下一篇再說。