最近在整理js基礎方面相關的文檔,整理到對象的時候發現這一塊的內容看似簡單但實際上卻有不少容易忽視的點,本文針對於(《你不知道的Javascript》上卷的對象
)進行整理和總結,咱們先拋出幾個問題:javascript
咱們先來聊下一些基本概念。java
咱們知道對象是建立無序鍵 / 值對數據結構(映射)的主要機制。對象的建立能夠經過經常使用的兩種形式:字面量和構造形式。數組
對象的字面量:數據結構
var obj = { key: 'value1', arr: [1, 2, 3], foo: function() { // todo } }
構造器形式:閉包
var obj = new Object(); obj.key = "value1" obj.arr = [1, 2, 3], obj.foo = function() { // todo }
其實 var obj = {}
是 new Object()
的語法糖,它們生成的對象是同樣的。惟一的區別是便體如今了添加屬性的時候,字面量能夠添加多個鍵 / 值對,而構造形式必須逐個添加屬性。函數
在 JavaScript 中最新的語言類型包括8種:this
簡單基本類型(原始數據類型)除 object 之外,它們自己不是對象,但有些是存在它們的包裝類型,如:number 的 Number(),string 的 String() 等。prototype
注意: 雖然typeof null === 'object'
爲true,但這是js
自己的一個 bug ,修復的話能夠會形成更多意料以外的 bug 。《你不知道的JavaScript》 上卷中對象一節說起到實際上不一樣的對象在底層都表示爲二進制,在
js
中二進制前三位都爲 0 的話是被斷定爲object
類型的,而null
的二進制表示全是 0 ,因此執行typeof
的時候會返回object
。指針
與簡單基本類型相對的是複雜類型,如 Function
、Array
,它們都屬於對象的子類型。code
除了剛剛說起的function
、 array
,還有一類子類型對象即內置對象:
它們實際上都是內置函數,或者稱它們爲類,能夠經過 new
運算符調用構造器產生。
咱們能夠發現一些內置對象是跟基本類型相照應的,先看一下例子:
var str = "hello, 你好" str.indexOf(',') // 5 str.charAt(5) // , var number = 12.122; number.toFixed(2) // 12.12
這些列子說明了 js
引擎在編譯運行的時候會先把基礎類型的數據自動裝箱,而後就能夠調用其自身能夠訪問或者原型鏈上再或者頂級 Object
原型上定義的屬性或方法了。
null
和 undefined
沒有對應的包裝類型,相反 Date
沒有對應的字面量形式。
對象能夠經過點(.
)語法訪問屬性值,也能夠經過 []
相似數組訪問值的形式訪問屬性值。ES6新增的可計算屬性名即可以經過 []
這種語法來操做對象的屬性:
var suffix = '.png' var imgConfig = { ['img1' + suffix]: 'http://abc.com', ['img2' + suffix]: 'http://abc.com', ['img3' + suffix]: 'http://abc.com' }
稍後咱們還會說起一個使用Symbol.iterator
做爲計算屬性名的自定義迭代器,能夠配合for..of
使用。
針對於淺複製可使用 ES6 定義的 Object.assign
實現淺拷貝。可是針對於深拷貝的問題要複雜不少,必需要考慮不少狀況,諸如各類類型的對象的拷貝,循環引用等問題,具體分析能夠參見js之繼承
在ES5以前,JavaScript 未提供能夠表述對象屬性及自身屬性檢查的方法。從ES5以後,咱們建立的對象都具有了屬性描述符。
var obj = { name: "kkxiaoa" } Object.getOwnPropertyDescriptor(obj, 'name'); // { // configurable: true // enumerable: true // value: "kkxiaoa" // writable: true // }
在建立普通屬性時屬性描述符會使用默認值,能夠Object.defineProperty
進行修改已有屬性的描述,前提是它的 configurable
必須爲 true
。
writable決定了屬性是否能夠被修改:
var obj = {}; Object.defineProperty(obj, 'id', { value: 1, writable: false, configurable: true, enumerable: true }) obj.id = 2; obj.id; // 1
在非嚴格模式下,修改一個只讀屬性的值默認會忽略。可是在嚴格模式下,則會拋出異常:
"use strict"; var obj = {}; Object.defineProperty(obj, 'id', { value: 1, writable: false, configurable: true, enumerable: true }) obj.id = 2; // TypeError
像這樣便會報類型錯誤異常,表示沒法修改只讀屬性的值。
默認建立對象的時候,屬性 configurable
爲 true
表示可使用 defineProperty
進行配置,可是當手動更改它的可配置屬性爲 false
的時候,再次使用 defineProperty
則會拋出異常:
var obj = {}; Object.defineProperty(obj, 'id', { value: 1, writable: true, configurable: false, enumerable: true }) obj.id; // 1 obj.id = 2; obj.id; // 2 Object.defineProperty(obj, 'id', { value: 1, writable: true, configurable: true, enumerable: true }) // TypeError Cannot redefine property: id
注意:
- 把 configurable 修改成 false 是單項操做,不能撤銷
- 在
configurable
爲false
的前提下仍然能夠將writable
由true
置爲false
可是沒法由false
置爲true
- 在
configurable
爲false
的前提下該屬性沒法被刪除
該屬性表明對象的屬性是否可被枚舉,如 for..in
或 Object.keys
等遍歷中就是經過該屬性來遍歷可枚舉的屬性。
注意: 這裏的for..im
和in
操做符是兩回事,in
操做符是檢查該熟悉是否存在於制定對象中,不論它是否可枚舉。
有時候咱們但願定義一些常量,這時候可使用 const
關鍵字。可是若是咱們想定義一個常量類,這裏面存放的是一些基礎性的配置屬性,咱們並不但願它被擴展,屬性被改寫或刪除,這個時候咱們可使用下列方法讓這個對象密封或凍結。
咱們能夠結合 writable: false
和 configurable: false 建立一個不可變的常量
,這樣的話該屬性不能被刪除、重定義、修改:
var Constants = {}; Object.defineProperty(Constants, 'NUMBER_KEY', { value: 'AAFJJ1231', writable: false, configurable: false })
若是想要禁止一個對象添加新屬性而且保留已有屬性,可使用 Object.preventExtensions
:
var obj = { id: '111' } Object.preventExtensions(obj); obj.key = 'abc'; obj; // {id: '111'}
在嚴格模式下會報異常,在普通模式下會忽略擴展。
若是在禁止擴展
的前提下不想讓屬性進行配置刪除操做(可修改)可使用 Object.seal()
,該方法會在現有的對象上使用Object.preventExtensions
,並把現有屬性 configurable
更改成 false
。
若是想要在 密封
的前提下禁止修改對象屬性,可使用 Object.freeze()
,該方法會在現有對象上調用Object.seal()
,把全部數據訪問屬性的 writable
置爲 false
。
注意:使用freeze
方法只會凍結對象自己及任意直接屬性,對於那些保存着對象引用的屬性則不受該方法的影響。若是想要深度凍結,能夠經過遞歸的方式遍歷該對象,檢測到引用對象的存在時使用freeze
方法,但這樣可能會凍結掉全局
共享的對象,請當心使用。
屬性的訪問(不論是經過 .
或者 []
)訪問屬性的時候其實是實現了 [[Get]]
操做(相似於方法調用),當咱們訪問某一屬性的時候,如:obj.id
,語言內部首先在對象上查找是否具有相同名稱的屬性,存在則返回其值。不然會遍歷該對象的原型鏈,存在的話返回其值。若是都不存在[[Get]]
操做會返回 undefined
:
var obj = { id: undefined }; obj.id; // undefined obj.key; // undefined
這兩種都是返回 undefined
可是 obj.key
則會進行更復雜的處理,其不只僅是查找自身,還會遍歷原型鏈。
咱們再看一個常見的例子:
function Foo(id) { this.id = id } Foo.prototype.getId = function() { return this.id } var a = new Foo(1); var b = new Foo(2); a.getId() // 1 b.getId() // 2
咱們都知道這樣是能夠訪問的,可是仔細的品一下經過 Object.keys
(a) 它裏面只有 id
一個屬性,會什麼能夠訪問到 getId()
方法呢? 答案就是經過 [[Get]]
的默認行爲屬性在自身不存在時檢查原型鏈。
與 [[Get]]
操做相對應的即是 [[Put]]
操做,起初我認爲給對象賦值便會觸發[[Put]]
操做來實現編輯或者建立行爲,可是真正當[[Put]]
被觸發的時候,這裏面有多種因素可能致使賦值不會使用默認行爲,其中一個最終要的因素即是該屬性是否存在於其自身:
若是該屬性存在於其自身(非原型鏈)上,[[Put]]
操做將會進行以下檢查:
Setter
則直接調用 Setter
。writable
是否爲 false
?若是是,在普通模式下值會被忽略,而在嚴格模式下會拋 出 TypeError
異常。若是該屬性存在於其原型鏈上,[[Put]]
操做會出現的三種狀況:
writable
不爲 false
,那麼就會在該對象上添加一個同名的新屬性並賦值。writable
爲 false
,在嚴格模式下會拋出TypeError
異常,在普通模式下則會忽略本次賦值。Setter
那就會調用這個 Setter
,並不會添加新的屬性到這個對象上。前面咱們講到了數據描述符(用來描述數據的行爲),與之相對應即是訪問描述符(getter、setter
),若是屬性定義了訪問描述符(二者都存在的時候)JavaScript
會忽略它們的 value
和 writable
特性,取而代之的是get
、set
、configurable
、enumerable
的特性。
對象屬性值的設置和獲取默認使用 [[Set]]
和 [[Put]]
,ES5中可使用 Getter
和 Setter
改寫對象的默認操做,它們都是隱藏函數。若是對某一屬性設置了 getter
那麼獲取屬性值的時候會被調用。同理,setter
則會在對屬性設置值的時候被調用。
注意:getter
和setter
只能綁定到單個屬性上面。若是想要爲整個對象上的屬性進行定義,則能夠遍歷對象使用defineProperty
對屬性進行改造。如 Vue 中的變化偵測機制就是將整個對象使用getter
和setter
進行改造的。
咱們先來看下如何定義它們:
var person = { get name() { return 'kkxiaoa' } } Object.defineProperty(person, 'say', { get: function() { return 'hellow, my name is ' + this.name }, enumerable: true }) person; // {} 由於是隱藏函數,咱們並未定義屬性 person.name; // 'kkxiaoa' person.say // 'hellow, my name is kkxiaoa' Object.keys(person) // ['name', 'say']
這是兩種定義 getter
的方式,無論使用那一種,咱們看到都會建立一個不包含值的屬性。咱們獲取對應的屬性的時候會自動調用 getter
隱藏函數。因爲咱們只定義了 getter
,咱們設置屬性值試試:
person.name = 'abc' person.name; // 'kkxiaoa'
能夠看到對 name
賦值會被忽略,因爲咱們並未定義 setter
結果是符合預期的。咱們接着定義一下 setter
,它會覆蓋該屬性的默認 [[Put]]
操做。
var person = { get name() { return 'hello, ' + this._name }, set name(name) { this._name = name // 定義一個新屬性存儲,不要使用name,不然會形成循環引用 } } person.name = 'world' person.name; // 'hello, world'
講到遍歷會想到剛剛咱們說起的屬性描述符 enumerable
,它們是緊密關聯的。對於對象的遍歷咱們最多見的有for..in
、Object.keys
、for..of
,咱們先來談談關於存在性的問題。
咱們知道 in
和 hasOwnProperty
均可以判斷屬性是否存在於對象上,可是它倆的區別就是 in
除了自身外還會查找整個原型鏈是否存在該屬性(不論該屬性是否可枚舉),hasOwnProperty
一樣是檢查的屬性不管是否可被枚舉但它只會在對象自己查找,不會檢查原型鏈。
for..in
循環會遍歷對象自身及原型鏈上可枚舉屬性、Object.keys
會遍歷對象自身可枚舉的屬性。此外須要注意的是,這個循環只能獲取到 key
,並不能直接獲取對象中屬性的值。
若是想要直接獲取到屬性的值可使用 ES6 新增的 for..of
進行遍歷,前提是該對象已經定義了迭代器屬性。
var arrList = [1, 2, 3]; for (let v of arrList) { console.log(v) } // 1 // 2 // 3
因爲數組有內置的@@iterator,能夠直接使用它:
var arrList = [1, 2, 3]; var it = arrList[Symbol.iterator](); it.next(); // {value: 1, done: false} it.next(); // {value: 2, done: false} it.next(); // {value: 3, done: false} it.next(); // {value: undefined, done: true}
注意: 引用像iterator這樣的特殊對象的時候要使用符號名,經過
Symbol.iterator
來獲取@@iterator內部的屬性。
這裏的迭代器執行了四遍才執行完,和java
的迭代器機制相似,每次迭代 next 的時候內部指針(雖然js中沒有指針的概念)都會向前移動並返回對象屬性列表的下一個值(須要注意遍歷屬性/值的順序)。
因爲普通對象建立的時候未實現迭代器(@@iterator),致使對象沒法使用 for..of
。可是咱們能夠手動在對象上實現自定義的迭代器。
var obj = { a: 1, b: 2 } Object.defineProperty(obj, Symbol.iterator, { enumerable: false, configurable: true, writable: false, value: function() { var that = this, idx = 0, keys = Object.keys(that); return { next: function() { return { value: that[keys[idx++]], done: (idx > keys.length) } } } } }) // 使用迭代器遍歷 var it = obj[Symbol.iterator](); it.next() // {value: 1, done: false} it.next() // {value: 2, done: false} it.next() // {value: undefined, done: true} // 使用for..of遍歷 for(let v of obj) { console.log(v) } // 1 // 2
這裏的思路就是經過閉包產生迭代計數器,每次遍歷返回列表的下一個值。咱們甚至能夠自定義咱們想要的任何迭代器。須要注意的是Symblol
是不可枚舉的。
對象是建立無序鍵值對的一種數據結構,能夠經過字面量建立,也能夠經過 new
關鍵字調用構造器函數建立,但通常使用字面量更常見,能夠經過 .
語法和 []
來訪問屬性獲取屬性值。
屬性能夠經過數據描述符(默認建立方式)及訪問描述符來進行操做,使用它們能夠實現咱們想要的結構,如:禁止擴展、密封、凍結等操做。
屬性不必定包含值,它們能夠是經過setter
和 getter
來操做對象。
經過遍厲咱們瞭解到 enumerable
屬性描述符的做用,介紹了 for..of
、Object.keys
及for..of
對遍歷對象時各自的用途,其中經過for..of
咱們瞭解到能夠爲對象自定義迭代器來使用 for..of
遍歷。