深入理解JS中的對象(一)

目錄

  • 一切皆是對象嗎?
  • 對象
    • 原型與原型鏈
    • 構造函數
  • 參考

1.一切皆是對象嗎?

首先,「在 JavaScript 中,一切皆是對象」這種表述是不完全正確的。

JavaScript 的數據類型分為兩類:原始值類型和對象(Object類型)。

原始值類型(ES5):

  • undefined
  • null – typeof null 的值為”object”,是因為 ES5 規範規定:對於 null 值的 typeof 字元串值返回”object”
  • true/false – 布爾值
  • number
  • string
var a = undefined
var b = null
var c = true
var d = 1
var e = "abc"

這些值是在底層上直接實現的,他們不是object,所以沒有原型(__proto__),沒有構造函數(constructor)。

但我們再實踐過程中,會發現雖然字元串,布爾值和數字是原始值類型,但卻表現得有點像對象。

以字元串為例:

字元串原始值類型

在上圖中,可以看到定義了一個值為”abc”的字元串變數 e,訪問其 _proto_ 和 constructor 屬性,發現其居然有值?不是說原始值類型沒有原型和構造函數,這是怎麼回事呢?

原來原始值類型(布爾值、數字、字元串)有其對應的包裝器對象:Boolean(布爾對象)、Number(數字對象)、String(字元串對象),在這些原始值類型上嘗試調用屬性或方法(比如 constructor 等)時,JS會自動進行 Auto-Boxing(臨時包裝)的過程,首先將其轉換為臨時包裝器對象,再訪問其上的屬性或方法,而不會影響原始值類型的屬性。

這也能解釋為什麼我們直接對原始值類型變數(布爾值、數字、字元串)添加了一些屬性,再次訪問依舊為 undefined,因為我們訪問屬性操作的是臨時包裝器對象,不會影響基本原始值類型本身。如下圖:

臨時包裝器對象

而原始值類型(null 與 undefined)沒有對應的包裝器對象,所以在其上嘗試訪問任何屬性或方法都會報錯。如下圖:

null 與 undefined 沒有包裝器對象

2.對象

在JS中,Object 是一個屬性的集合,並且擁有一個單獨的原型對象 [prototype object] (其可以是一個 object 或 null 值)。

在瀏覽器或 Node.js 中,可以通過 _proto_ 屬性訪問這個原型對象, _proto_ 被稱為該對象的原型,但為了和函數的原型屬性(prototype)區分,一般稱其為隱式原型。

var position = {
  x: 10,
  y: 20,
  z: 30,
}

上面的程式碼中,對象與隱式原型的關係如下圖:

對象與隱式原型的關係

(1)原型與原型鏈

在JS中,對象的繼承關係是通過隱式原型(__proto__)來實現的。對象的隱式原型在對象創建時由對象的構造函數自動關聯,也可以通過修改隱式原型,更改對象的繼承關係。

由 Object 構造函數創建的對象,其隱式原型指向 Object.prototype。而 Object.prototype 對象的隱式原型的值默認為 nulll。

程式碼示例:

// x, y, z 的隱式原型 __proto__ 默認都指向 Object.prototype
var x = {
    a: 10,
}
var y = {
    a: 20,
    b: 30,
}
var z = {
    a: 40,
    b: 50,
    c: 60,
}
  
// 設置 x 的隱式原型為 y
// 設置 y 的隱式原型為 z
x.__proto__ = y
y.__proto__ = z
  
console.log(x.a)  // 10 - 來自 x 自身的屬性
console.log(x.b)  // 30 - 來自 y 的屬性
console.log(x.c)  // 60 - 來自 z 的屬性

// 修改 y 的屬性 b 的值
y.b = 70

console.log(x.b)  // 70 - 來自 y 的屬性

// 移除 z 的屬性 c
delete z.c

console.log(x.c)  // undefined - 沿著隱式原型一級一級往上找,沒有找到該屬性

從上述程式碼,我們可以看到,當訪問一個對象的屬性時,會優先在這個對象的屬性中查找是否存在所要訪問的屬性,若存在,則獲取成功,停止查找;若沒有找到該屬性,則會繼續去查找該對象的隱式原型中是否存在,若存在,則獲取成功,停止查找;若還是沒有查找到,將繼續再往上一級的隱式原型中查找,直到找到則返回找到的屬性值 或 直到遇到隱式原型值為 null 則返回 undefined。

這種由原型相互關聯(指向)的關係就形成了所謂的原型鏈,而對象的屬性或方法的查找就是沿著原型鏈順序進行查找的。

上述程式碼示例中的原型鏈關係如下圖:

程式碼示例中的原型鏈關係

(2)構造函數

首先要明白,函數也是一個特殊的對象,除了和其他對象一樣有 _proto_ 屬性外,還有自己特有的屬性——顯示原型(prototype),這個屬性指向一個對象,其用途就是包含所有實例共享的屬性和方法。顯示原型對象也有一個 constructor 屬性,這個屬性指向原構造函數。

而所謂構造函數,就是提供一個生成對象的模板,並描述對象的基本結構的函數。一個構造函數,可以生成多個對象,每個對象都有相同的結構。而JS中所有函數(除了箭頭函數)都可以當做構造函數。

一個對象由構造函數創建時,其隱式原型(__proto__)指向構造該對象的構造函數(constructor)的顯示原型(prototype),這保證了實例能夠訪問在構造函數原型中定義的屬性和方法。

程式碼示例:

// 構造函數 C
function C(x) {
    this.x = x
}

// 繼承屬性 y
C.prototype.y = 30

// new 兩個對象實例a、b
var a = new C(10)
var b = new C(20)

console.log(a.x) // 10
console.log(a.y) // 30
console.log(b.x) // 20
console.log(b.y) // 30

// 對象實例 a、b 的隱式原型(__proto__)指向構造該對象的構造函數 C 的顯示原型(prototype)
console.log(a.__proto__ === C.prototype) // true
console.log(b.__proto__ === C.prototype) // true

// 構造函數的顯示原型(prototype)的 constructor 屬性指向原構造函數
console.log(C === C.prototype.constructor) // true

// 構造函數 C、Function 與 Object 的隱式原型(__proto__)指向構造該對象的構造函數 Function 的顯示原型(prototype)
console.log(C.__proto__ === Function.prototype) // true
console.log(Function.__proto__ === Function.prototype) // true
console.log(Object.__proto__ === Function.prototype) // true

// C.prototype 與 Function.prototype 的隱式原型(__proto__)指向構造該對象的構造函數 Object 的顯示原型(prototype)
console.log(C.prototype.__proto__ === Object.prototype) // true
console.log(Function.prototype.__proto__ === Object.prototype) // true

// Object.prototype 的隱式原型(__proto__)等於 null
console.log(Object.prototype.__proto__ === null) // true

上述程式碼示例中的完整原型鏈關係如下圖:

程式碼示例中的完整原型鏈關係

從上圖我們可以總結:

  1. 所有的(隱式)原型鏈的最末端最終都會指向 null(JS不允許有循環原型鏈,避免死循環)

  2. 所有函數默認都是有 Function 構造函數創建,即所有函數的隱式原型(__proto__)都指向 Function.prototype。

  3. 所有對象默認都繼承自Object對象,即默認情況下,所有對象的(隱式)原型鏈的末端都指向 Object.prototype。

註:所謂默認情況,即沒有手動修改原型鏈關係。

3.參考

JS中一切都是對象嗎?看這一篇就知道了

js中__proto__和prototype的區別和關係?

深入理解JavaScript系列(10):JavaScript核心(晉級高手必讀篇)

深入理解JavaScript系列(18):面向對象編程之ECMAScript實現(推薦)