《你不知道的JavaSript》之對象

前言

最近在整理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

  • string
  • number
  • boolean
  • null
  • undefined
  • symbol
  • bigint (新增)
  • object

簡單基本類型(原始數據類型)除 object 之外,它們自己不是對象,但有些是存在它們的包裝類型,如:number 的 Number(),string 的 String() 等。prototype

注意: 雖然 typeof null === 'object' 爲true,但這是 js 自己的一個 bug ,修復的話能夠會形成更多意料以外的 bug 。

《你不知道的JavaScript》 上卷中對象一節說起到實際上不一樣的對象在底層都表示爲二進制,在 js 中二進制前三位都爲 0 的話是被斷定爲object 類型的,而 null 的二進制表示全是 0 ,因此執行 typeof 的時候會返回 object指針

與簡單基本類型相對的是複雜類型,如 FunctionArray,它們都屬於對象的子類型。code

內置對象

除了剛剛說起的functionarray,還有一類子類型對象即內置對象:

  • Boolean
  • Number
  • String
  • Function
  • Array
  • Date
  • RegExp
  • Error
  • Object

它們實際上都是內置函數,或者稱它們爲類,能夠經過 new 運算符調用構造器產生。

咱們能夠發現一些內置對象是跟基本類型相照應的,先看一下例子:

var str = "hello, 你好"
str.indexOf(',')   // 5
str.charAt(5)      // ,

var number = 12.122;
number.toFixed(2)  // 12.12

這些列子說明了 js 引擎在編譯運行的時候會先把基礎類型的數據自動裝箱,而後就能夠調用其自身能夠訪問或者原型鏈上再或者頂級 Object 原型上定義的屬性或方法了。

nullundefined 沒有對應的包裝類型,相反 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

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

默認建立對象的時候,屬性 configurabletrue 表示可使用 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

注意:

  1. 把 configurable 修改成 false 是單項操做,不能撤銷
  2. configurablefalse 的前提下仍然能夠將 writabletrue 置爲 false 可是沒法由 false 置爲 true
  3. configurablefalse 的前提下該屬性沒法被刪除

Enumerable

該屬性表明對象的屬性是否可被枚舉,如 for..inObject.keys 等遍歷中就是經過該屬性來遍歷可枚舉的屬性。

注意: 這裏的 for..imin 操做符是兩回事, in 操做符是檢查該熟悉是否存在於制定對象中,不論它是否可枚舉。

不變性

有時候咱們但願定義一些常量,這時候可使用 const 關鍵字。可是若是咱們想定義一個常量類,這裏面存放的是一些基礎性的配置屬性,咱們並不但願它被擴展,屬性被改寫或刪除,這個時候咱們可使用下列方法讓這個對象密封或凍結。

對象常理

咱們能夠結合 writable: falseconfigurable: 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]]

屬性的訪問(不論是經過 . 或者 [])訪問屬性的時候其實是實現了 [[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]] 的默認行爲屬性在自身不存在時檢查原型鏈。

[[Put]]

[[Get]] 操做相對應的即是 [[Put]]操做,起初我認爲給對象賦值便會觸發[[Put]]操做來實現編輯或者建立行爲,可是真正當[[Put]] 被觸發的時候,這裏面有多種因素可能致使賦值不會使用默認行爲,其中一個最終要的因素即是該屬性是否存在於其自身:

  • 若是該屬性存在於其自身(非原型鏈)上,[[Put]]操做將會進行以下檢查:

    1. 屬性是不是訪問描述符?是而且存在Setter 則直接調用 Setter
    2. 屬性的數據描述符中的 writable 是否爲 false ?若是是,在普通模式下值會被忽略,而在嚴格模式下會拋 出 TypeError 異常。
    3. 若是以上兩種狀況都不存在,則會對該屬性賦值。
  • 若是該屬性存在於其原型鏈上,[[Put]]操做會出現的三種狀況:

    1. 若是原型鏈上存在同名的屬性,而且它的數據描述符中的 writable 不爲 false ,那麼就會在該對象上添加一個同名的新屬性並賦值。
    2. 若是原型鏈上存在同名的屬性,而且它的數據描述符中的 writablefalse ,在嚴格模式下會拋出TypeError 異常,在普通模式下則會忽略本次賦值。
    3. 若是原型鏈上存在同名的屬性且它是訪問描述符 Setter 那就會調用這個 Setter ,並不會添加新的屬性到這個對象上。

Getter 和 Setter

前面咱們講到了數據描述符(用來描述數據的行爲),與之相對應即是訪問描述符(getter、setter),若是屬性定義了訪問描述符(二者都存在的時候)JavaScript 會忽略它們的 valuewritable 特性,取而代之的是getsetconfigurableenumerable 的特性。

對象屬性值的設置和獲取默認使用 [[Set]][[Put]],ES5中可使用 GetterSetter改寫對象的默認操做,它們都是隱藏函數。若是對某一屬性設置了 getter 那麼獲取屬性值的時候會被調用。同理,setter 則會在對屬性設置值的時候被調用。

注意: gettersetter 只能綁定到單個屬性上面。若是想要爲整個對象上的屬性進行定義,則能夠遍歷對象使用 defineProperty 對屬性進行改造。如 Vue 中的變化偵測機制就是將整個對象使用 gettersetter 進行改造的。

咱們先來看下如何定義它們:

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..inObject.keysfor..of,咱們先來談談關於存在性的問題。

咱們知道 inhasOwnProperty 均可以判斷屬性是否存在於對象上,可是它倆的區別就是 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 關鍵字調用構造器函數建立,但通常使用字面量更常見,能夠經過 . 語法和 [] 來訪問屬性獲取屬性值。

屬性能夠經過數據描述符(默認建立方式)及訪問描述符來進行操做,使用它們能夠實現咱們想要的結構,如:禁止擴展、密封、凍結等操做。

屬性不必定包含值,它們能夠是經過settergetter 來操做對象。

經過遍厲咱們瞭解到 enumerable 屬性描述符的做用,介紹了 for..ofObject.keysfor..of 對遍歷對象時各自的用途,其中經過for..of 咱們瞭解到能夠爲對象自定義迭代器來使用 for..of 遍歷。

相關文章
相關標籤/搜索