關於JavaScript物件,你所不知道的事(二)- 再說屬性

libinfs發表於2019-02-17

說完了物件那些不常用的冷知識,是時候來看看JavaScript中物件屬性有哪些有意思的東西了。

不出你所料,物件屬性自然也有其相應的特徵屬性,但是這個話題有點複雜,讓我們先從簡單的說起,物件屬性的分類。

面對一個複雜的事物,尋找其內在共性,妥善分類往往是快速認知該事物的捷徑,這與程式設計師“將難以解決的大問題拆解為可以解決的小問題”的思維有異曲同工之妙。

那麼,物件的屬性根據不同的維度,可以如何分類呢?你或許想不到,竟然有如此多的分類方法,而不同的類別,有牽扯出特定的方法解決這一類別的某些問題。讓我們看看吧:

按來源分類

  • 私有屬性
  • 原型屬性

JavaScript是一門基於原型鏈的語言,物件繼承是節省記憶體空間,避免程式碼重複,邏輯混亂的好方法。而物件繼承對於屬性而言則帶來一個問題,即我們需要區分某物件內的屬性,究竟是物件自有的(私有屬性),還是繼承於其他物件(繼承屬性),無論是進行屬性遍歷還是對屬性進行操作,我們都需要謹慎的思考這個問題。

讓我們舉例兩個典型場景看看JavaScript是如何幫助我們解決這個問題的:

情景一: 屬性查詢

有時候,我們需要查詢某個物件是否有某個屬性,再進一步決定是否要執行下一步操作,JavaScript提供給我們的查詢工具是in操作符,in操作符用以在給定物件中查詢一個給定名稱的屬性,如果找到則返回true值。實際上,in操作符就是在雜湊表中查詢一個鍵是否存在(還記的我們的藍色章魚嗎,in操作符只是檢查章魚是否有那隻觸角,並不關心觸角上拿著的卡片上寫了什麼,更不會隨著卡片的地址去讀取值,這對於提升效能尤其重要),讓我們看看程式碼示例:

let obj = {
    x: 1,
}

console.log(`x` in obj)   // true
console.log(`y` in obj)   // false複製程式碼

但是遺憾的是,in操作符會檢查所有的私有屬性和原型屬性,因此你並不能通過in操作符知道該屬性的真正來源。

但好在JavaScript還給我們提供了一個.hasOwnProperty()方法,每一個物件都有這樣一個方法,專門用來判斷某個屬性是否是該物件的私有屬性。

我們終於得到了我們想要的。太棒了。

小結:

  1. 查詢屬性(不區分屬性來源):in操作符
  2. 查詢私有屬性:物件的.hasOwnProperty()方法

情景二: 屬性列舉

有些時候,我們想要獲得一個物件內所有屬性的鍵或值(或者全部都要),這時我們就要列舉一個物件內的所有屬性,通常,我們會使用for-in迴圈去實現這一點。

然而,很不巧的是,for-in迴圈會遍歷所有可列舉的原型屬性,注意這裡有兩點需要進一步說明:

  1. 可列舉:這牽扯到我們很快要談到的屬性特徵屬性(有點拗口是吧:))
  2. 會遍歷原型屬性:這樣當一個物件的繼承鏈很長而我們又只關心物件的私有屬性時就會變得非常麻煩

當然,你可以在for-in迴圈中,再使用我們剛提到的.hasOwnProperty()方法,但是JavaScript給予了我們更好的選擇:使用Object.keys()方法:

Object.keys()方法是ECMAScript5引入的方法,它可以獲取可列舉屬性的名字的陣列,並且它只返回物件的自有屬性。

因此,你可以基於是否需要一個陣列,是否只需要物件自有屬性來判斷使用哪一種方法。

小結:

  1. 列舉屬性(不區分屬性來源):for-in迴圈
  2. 只列舉私有屬性,且返回陣列:Object.keys()方法

按作用分類

  • 資料屬性
  • 訪問器屬性

你也許很少聽說過這樣的分類方式,因為我們幾乎都在使用資料屬性,讓我來簡要說明這兩種型別的屬性的區別:

資料屬性包含了一個值,我們之前提到的物件的內部方法[[Put]]的預設行為就是建立資料屬性:

let obj = {
    x: 1,   // x 是資料屬性
}複製程式碼

訪問器屬性不包含值,而是定義了一個當屬性被讀取時呼叫的函式(稱為“getter”)和一個當屬性被寫入時呼叫的函式(稱為“setter”):

let obj = {
    x: 1,

    get y() {
        return 2
    }

    set y() {
        return 3
    }
}

console.log(obj.y)   // 2複製程式碼

之所以訪問器屬性很少見到是因為我們很少需要在進行屬性賦值或讀取操作時觸發一些行為,不過反過來說,如果這恰恰是你面臨的場景,就大膽的使用吧。


物件屬性的特徵屬性

繞了一大圈,終於可以回到正題,談談屬性的特徵屬性了,相較於物件只有一個孤零零的[[Extensiable]]特徵屬性,物件屬性要複雜的多:

因為所有物件屬性都具有:

  1. [[Enumerable]]特徵屬性:決定一個屬性是否可以被遍歷;
  2. [[Configurable]]特徵屬性:決定一個屬性是否可以被配置

而只有資料屬性有以下兩個屬性:

  1. [[Value]]特徵屬性:即屬性的值;
  2. [[Writable]]特徵屬性:值為布朗型別,決定該屬性值是否可以寫入;

而只有訪問器屬性有以下兩個屬性:

  1. [[Get]]特徵屬性:即為getter函式內容;
  2. [[Set]]特徵屬性:即為setter函式內容;

讓我們先來看看這些特徵屬性的意義,再來談談如何配置這些特徵屬性:

[[Enumerable]]

並不是所有的屬性都是可列舉的,實際上,物件的大部分原生方法的[[Enumerable]]特徵屬性的值都被設定為false(所以使用for-in迴圈時,不會遍歷出一大堆你不需要的內容),那我們該如何判斷一個屬性是否是可列舉的呢?

JavaScript為我們提供了.propertyIsEnumerable()方法去檢查一個屬性是否可列舉,像.hasOwnProperty()方法一樣,每個物件都擁有這個方法:

let obj = {
    x: 1,
}

console.log(obj.propertyIsEnumerable(`x`))   // true複製程式碼

[[Configurable]]

可配置是指:

  1. 刪除操作;
  2. 屬性型別變更操作(從資料屬性變為訪問器屬性,或者相反):
  3. 使用Object.defineProperty()方法配置屬性(彆著急,我們之後會著重講解這個方法);

因此,當你設定某個屬性的[[Configurable]]特徵屬性為false時,以上三種操作就都不能正確執行。


配置特徵屬性

是時候講解JavaScript為我們提供的配置屬性特徵屬性的方法了:Object.defineProperty()

該方法接收三個引數:

  1. 擁有該屬性的物件
  2. 屬性名(字串)
  3. 包含需要設定的特徵的屬性描述物件(屬性描述物件具有和特徵屬性額同名的屬性,但是名字中不包含中括號)

讓我們看看該方法的實際用法:

let obj = {
    x: 1,
}

Object.defineProperty(obj, `x`, {
    enumerable: false,
})

console.log(`x` in obj)   // true
console.log(obj.propertyIsEnumerable(`x`))   // false複製程式碼

我們通過Object.defineProperty()方法使obj物件的x屬性為不可遍歷的,在之後的檢測中,我們看到控制檯輸入屬性存在,但不可遍歷。

讓我們再看看資料屬性配置特徵屬性的示例:

let obj = {
    x: 1,
}

Object.defineProperty(obj, `x`, {
    value: 2,
    enumerable: true,
    configurable: true,
    writable: true,
})

console.log(obj.x)   // 2

// 注意我們所做的實際上完全等同於以下這段程式碼

let obj = {
    x: 2,
}複製程式碼

下面是訪問器屬性配置特徵屬性的示例:

let obj = {
    x: 1,
}

Object.defineProperty(obj, `x`, {
    get: function() {
        console.log(`reading...`)
        return this.x
    },
    set: function(value) {
        console.log(`setting...`)
        this.x = value
    },
    enumerable: true,
    configurable: true,
})複製程式碼

使用訪問器屬性特徵屬性比使用物件字面形式定義訪問器屬性的優勢在於,你可以為已有的物件定義這些屬性。如果你想要用物件字面形式,你只能在建立物件時定義訪問器屬性。

需要注意的是,一旦你決定使用Object.defineProperty()方法配置屬性的特徵屬性,你需要完整在配置物件中列出enumerable屬性與configurable屬性,因為在預設情況下,這些屬性的值皆為false,這可能不是你想要的。


定義多重屬性

當你需要配置一個物件的多個屬性時,你需要使用Object.defineProperties()方法,其用法如下:

let obj = {
    x: 1,
}

Object.defineProperties(obj, {
    x: {
        value: 2,
        enumerable: true,
        configurable: true,
        writable: true,
    },
    y: {
        get: function() {
            console.log(`reading...`)
            return this.x
        },
        set: function(value) {
            console.log(`setting...`)
            this.x = value
        },
    },
})複製程式碼

獲取屬性特徵屬性

目前為止,我們提到了屬性的所有特徵屬性,以及如何設定,最後,讓我們看看JavaScript為我們提供的檢視屬性特徵屬性的方法:Object.getOwnPropertyDescriptor()。其用法如下:

let obj = {
    x: 1,
}

const descriptor = Object.getOwnPropertyDescriptor(obj, `x`)

console.log(descriptor.enumerable)   // true
console.log(descriptor.configurable)   //true
console.log(descriptor.writable)   // true
console.log(descriptor.value)   // 1複製程式碼

可以看到,該方法接收兩個引數,一個目標物件以及想要獲取特徵屬性的屬性名,該函式會返回一個特徵屬性描述物件,包含屬性特徵屬性的所有資訊。


難以置信,我們終於講完了屬性的所有特徵屬性。看到這裡的你也值得為自己鼓掌?

先休息一會吧,然後我們看看最後的一個主題(還記的我們上一章提到的封閉物件嗎?),定義禁止修改的物件


物件封印與物件凍結

物件封印

物件封印是指,通過使用Object.seal()方法使一個物件不僅不可擴充套件,其所有的屬性都不可配置,也就是說,對於一個被封印的物件,你不能:

  1. 新增新屬性;
  2. 刪除屬性或改變屬性型別;

當一個物件被封印時,你只能讀寫它已有的屬性。另外,我們可以通過Object.isSealed()方法檢驗一個物件是否為被封印物件。

程式碼如下:

let obj = {
    x: 1,
}

console.log(Object.isExtensible(obj))   // true
console.log(Object.isSealed(obj))    // false

// 封印物件
Object.seal(obj)

console.log(Object.isExtensible(obj))   // false
console.log(Object.isSealed(obj))    // true

obj.y = 2

console.log(`y` in obj)   // false

obj.x = 3

console.log(obj.x)   // 3

delete obj.x
console.log(obj.x)   // 3複製程式碼

物件凍結

讓我們好好想想物件封印都做了些什麼,它使我們不能新增屬性,只能對已有的屬性進行讀寫操作,但卻無法改變已有屬性的特徵屬性,也無法刪除已有屬性,我們的物件的封閉性已經非常強了。

物件凍結則更近一步,將物件屬性的操作限制為只讀,它更像是一個物件某一時刻的快照,除了看之外我們不能對它有任何操作。

在JavaScript中,我們使用Object.freeze()凍結一個物件,並且使用Object.isFrozen()來判斷一個物件是否被凍結。


終於結束了,讓我們簡短回顧一下我們在本章中都講了些什麼:

  • 首先,我們講到了屬性的分類:

    • 按來源分:私有屬性原型屬性
    • 按作用分:資料屬性訪問器屬性
  • 其次,我們談到了屬性的特徵屬性:

    • 共有特徵屬性:[[Enumerable]]`[[Configurable]]
    • 資料屬性特徵屬性:[[Value]][[Writable]]
    • 訪問器屬性特徵屬性:[[Set]][[Get]]
  • 以及:

    • 配置屬性特徵屬性的方法:Object.defineProperty()
    • 定義多重屬性特徵屬性的方法:Object.defineProperties()
    • 獲取屬性特徵屬性的方法:Object.getOwnPropertyDescriptor()
  • 最後,我們介紹了定義更加封閉物件的兩種方式:

    • 物件封印:Object.seal()Object.isSealed()方法用於檢驗一個物件是否被封印
    • 物件凍結:Object.freeze()Object.isFrozen()方法用來判斷一個物件是否被凍結

大功告成!你現在已經和我一樣完全瞭解JavaScript物件了,Good Job?

? Hey!喜歡這篇文章嗎?別忘了在下方? 點贊讓我知道。

相關文章