1. 程式人生 > >深入理解JS中的物件(一)

深入理解JS中的物件(一)

**目錄** - 一切皆是物件嗎? - 物件 - 原型與原型鏈 - 建構函式 - 參考
**1.一切皆是物件嗎?** 首先,“在 JavaScript 中,一切皆是物件”這種表述是不完全正確的。 JavaScript 的資料型別分為兩類:原始值型別和物件(Object型別)。 原始值型別(ES5): - undefined - null - typeof null 的值為"object",是因為 ES5 規範規定:對於 null 值的 typeof 字串值返回"object" - true/false - 布林值 - number - string ```js var a = undefined var b = null var c = true var d = 1 var e = "abc" ``` 這些值是在底層上直接實現的,它們不是object,所以沒有原型(_\_proto__),沒有建構函式(constructor)。 但我們再實踐過程中,會發現雖然字串,布林值和數字是原始值型別,但卻表現得有點像物件。 以字串為例: ![字串原始值型別](https://img2020.cnblogs.com/blog/898684/202005/898684-20200511073026309-115340623.png) 在上圖中,可以看到定義了一個值為"abc"的字串變數 e,訪問其 _\_proto__ 和 constructor 屬性,發現其居然有值?不是說原始值型別沒有原型和建構函式,這是怎麼回事呢? 原來原始值型別(布林值、數字、字串)有其對應的包裝器物件:Boolean(布林物件)、Number(數字物件)、String(字串物件),在這些原始值型別上嘗試呼叫屬性或方法(比如 constructor 等)時,JS會自動進行 Auto-Boxing(臨時包裝)的過程,首先將其轉換為臨時包裝器物件,再訪問其上的屬性或方法,而不會影響原始值型別的屬性。 這也能解釋為什麼我們直接對原始值型別變數(布林值、數字、字串)添加了一些屬性,再次訪問依舊為 undefined,因為我們訪問屬性操作的是臨時包裝器物件,不會影響基本原始值型別本身。如下圖: ![臨時包裝器物件](https://img2020.cnblogs.com/blog/898684/202005/898684-20200511073112797-2132777052.png) 而原始值型別(null 與 undefined)沒有對應的包裝器物件,所以在其上嘗試訪問任何屬性或方法都會報錯。如下圖: ![null 與 undefined 沒有包裝器物件](https://img2020.cnblogs.com/blog/898684/202005/898684-20200511073151945-920483988.png)
**2.物件** 在JS中,Object 是一個屬性的集合,並且擁有一個單獨的原型物件 [prototype object] (其可以是一個 object 或 null 值)。 在瀏覽器或 Node.js 中,可以通過 _\_proto__ 屬性訪問這個原型物件, _\_proto__ 被稱為該物件的原型,但為了和函式的原型屬性(prototype)區分,一般稱其為隱式原型。 ```js var position = { x: 10, y: 20, z: 30, } ``` 上面的程式碼中,物件與隱式原型的關係如下圖: ![物件與隱式原型的關係](https://img2020.cnblogs.com/blog/898684/202005/898684-20200511073236583-166584668.png)
(1)原型與原型鏈 在JS中,物件的繼承關係是通過隱式原型(_\_proto__)來實現的。物件的隱式原型在物件建立時由物件的建構函式自動關聯,也可以通過修改隱式原型,更改物件的繼承關係。 > 由 Object 建構函式建立的物件,其隱式原型指向 Object.prototype。而 Object.prototype 物件的隱式原型的值預設為 nulll。 程式碼示例: ```js // 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。 這種由原型相互關聯(指向)的關係就形成了所謂的原型鏈,而物件的屬性或方法的查詢就是沿著原型鏈順序進行查詢的。 上述程式碼示例中的原型鏈關係如下圖: ![程式碼示例中的原型鏈關係](https://img2020.cnblogs.com/blog/898684/202005/898684-20200511073303555-1849276931.png)
(2)建構函式 首先要明白,函式也是一個特殊的物件,除了和其他物件一樣有 _\_proto__ 屬性外,還有自己特有的屬性——顯示原型(prototype),這個屬性指向一個物件,其用途就是包含所有例項共享的屬性和方法。顯示原型物件也有一個 constructor 屬性,這個屬性指向原建構函式。 而所謂建構函式,就是提供一個生成物件的模板,並描述物件的基本結構的函式。一個建構函式,可以生成多個物件,每個物件都有相同的結構。而JS中所有函式(除了箭頭函式)都可以當做建構函式。 一個物件由建構函式建立時,其隱式原型(_\_proto__)指向構造該物件的建構函式(constructor)的顯示原型(prototype),這保證了例項能夠訪問在建構函式原型中定義的屬性和方法。 程式碼示例: ```js // 建構函式 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 ``` 上述程式碼示例中的完整原型鏈關係如下圖: ![程式碼示例中的完整原型鏈關係](https://img2020.cnblogs.com/blog/898684/202005/898684-20200511073332815-104712634.png) 從上圖我們可以總結: 1. 所有的(隱式)原型鏈的最末端最終都會指向 null(JS不允許有迴圈原型鏈,避免死迴圈) 2. 所有函式預設都是有 Function 建構函式建立,即所有函式的隱式原型(_\_proto__)都指向 Function.prototype。 3. 所有物件預設都繼承自Object物件,即預設情況下,所有物件的(隱式)原型鏈的末端都指向 Object.prototype。 注:所謂預設情況,即沒有手動修改原型鏈關係。
**3.參考** [JS中一切都是物件嗎?看這一篇就知道了](https://zhuanlan.zhihu.com/p/65059409) [js中__proto__和prototype的區別和關係?](https://www.zhihu.com/question/34183746) [深入理解JavaScript系列(10):JavaScript核心(晉級高手必讀篇)](https://www.cnblogs.com/TomXu/archive/2012/01/12/2308594.html) [深入理解JavaScript系列(18):面向物件程式設計之ECMAScript實現(推薦)](https://www.cnblogs.com/TomXu/archive/2012/02/06/233060