一.寫在前面
原型和原型鏈是 JavaScript 中的重難點之一,雖然 ES6 我們已經可以使用class
進行定義類,可以使用extends
來繼承父類,但究其本質在 JavaScript 的內部還是使用的原型和原型鏈來實現的,所以學習和理解原型和原型鏈對於理解和深入 JavaScript 是必不可少的,好了 🦀 廢話不多說,讓我們開始今天的學習吧! 這篇文章我們會按照下述的內容模組進行學習和介紹。
二.普通物件的原型
當我們在瀏覽器上執行如下的程式碼的時候,我們會看到輸出的內容中有一個比較特殊的物件[[Prototype]]
,這個物件就是我們編寫的物件的原型物件.
let obj = { name: 'CodeUp', age: 12, } console.log(obj)
我們想要獲取這個物件不能直接使用obj.[[Prototype]]
的方式來獲取,JavaScript 給我們提供了 API 來獲取普通物件的原型物件。
console.log(obj.__proto__) // 不推薦,僅僅是瀏覽器的實現 console.log(Object.getPrototypeOf(obj)) // 推薦,標準的實現
我們 來試一下,發現都是可以正常獲取到的,但是在開發中推薦使用標準的寫法__proto__
可能在某些瀏覽器上存在不相容的情況,但是鑑於大多數瀏覽器都支援,這個 API 使用起來也方便,我們在使用的時候增加判斷,防止某些情況下的不相容即可。
三.函式物件的原型
在 JavaScript 函式也是物件的一種,它是Object
類的子類,關於Object
我們在下面會介紹,函式物件也具有(__proto__
),因為不經常使用我們稱之為隱式原型,同時也具有作為函式自身的原型(prototype
)由於經常使用我們稱之為顯式原型,簡單理解就是普通物件有的函式也有,但是是有差別的,作為函式自身它具有prototype
這個纔是函式貨真價實的原型。
function foo() {} console.log(foo.prototype) console.log(foo.__proto__)
函式物件和普通的物件是有關係的,為什麼這麼說哪? 因為所有的函式都是可以new
的,也就是透過函式可以建立物件,他們之間的關係我們可以透過以下的程式碼來看出來
function Foo(name, age) { this.name = name this.age = age } let obj1 = new Foo('芒果', 12) let obj2 = new Foo('招財', 13) console.log(Foo.prototype === obj1.__proto__) console.log(Foo.prototype === obj2.__proto__)
透過程式碼我們可以看出,函式的prototype
和所建立物件的__proto__
指向的是同一個物件,也就是本質上說這兩個東西是相等的。
四.函式原型的 constructor
事實上原型物件上還有一個屬性叫做constructor
屬性,預設情況下原型上都會新增一個屬性叫做 constructor,這個 constructor 指向當前的函式物件,我們可以使用程式碼來驗證下
function Person() {} var personPrototype = Person.prototype console.log(personPrototype) console.log(personPrototype.constructor) console.log(personPrototype.constructor === Person) // true
講到這裏我們已經基本理清楚了,物件,函式,隱式原型,顯式原型以及constructor
之前的關係,一圖勝千言,他們之間的關係用圖來表示其實就是這樣的。
五.重寫原型物件
如果我們需要在原型上新增非常多的東西的時候我們可能需要重寫原型物件,我們可以直接對原型物件進行賦值,舉個簡單的例子,當我們在編寫一個函式的時候需要往原型上新增很多的屬性,就像下列的程式碼一樣。
function Person() {} Person.prototype.message = 'Hello Person' Person.prototype.info = { name: '哈哈哈', age: 30 } Person.prototype.running = function () {} Person.prototype.eating = function () {}
但是需要掛載在原型上的東西非常多的時候我們也可以直接對原型物件進行重寫。
Person.prototype = { message: 'Hello Person', info: { name: '哈哈哈', age: 30 }, running: function () {}, eating: function () {}, }
但是雖然重寫比較方便但是這樣其實也會造成一個問題,那就是constructor
從上面的關係圖我們可以看出來,建構函式會指向這個函式,所以我們就需要增加一行程式碼,並且原來的constructor
是不可列舉的,所以我們還需要透過屬性描述符來進行不可列舉的控制。
Person.prototype = { message: 'Hello Person', info: { name: '哈哈哈', age: 30 }, running: function () {}, eating: function () {}, } Object.defineProperty(Person.prototype, 'constructor', { value: Person, enumerable: false, })
六.物件的原型鏈
當我們瞭解了物件的原型以及物件的原型與函式的原型,以及和constructor
之間的關係後,我們就可以透過物件之間一層一層的關係來研究和學習一下一個重要的概念原型鏈
var obj = { name: 'mongo', age: 1, } console.log(obj.message)
當我們在瀏覽器中執行上述的程式碼的時候,會經歷如下幾個階段
obj
上查詢message
屬性obj.__proto__
上面查詢對應的屬性obj.__proto__.__proto__
上查詢結果為 null 返回 undefined
上述的這個過程就是標準的原型鏈中查詢對應屬性的過程,接下來我們來對上述的程式碼進行改造一下
obj.__proto__ = { message: 'Hello AAA', } obj.__proto__.__proto__ = { message: 'Hello BBB', } obj.__proto__.__proto__.__proto__ = { message: 'Hello CCC', }
我們可以對原型物件進行重新賦值,然後obj
查詢就會沿著原型鏈進行查詢,當最後沒有繼續賦值的話預設會指向Object.prorotype
然後會指向null
七.Object 詳解
在 JavaScript 中Object
是一個非常特別的存在,如果你熟悉 JavaScript 那麼你肯定見過如下的程式碼
let obj = new Object() let obj = {}
其實這兩種操作本質上是一樣的,物件字面量本質上就是使用Object
建構函式來建立的,在這裏Object
是一個函式,從瀏覽器的輸出我們可以看出來指向的是一個相同的物件,並且constructor
屬性指向的是一個函式。
其實我們在上面對物件進行重寫的時候我們直接使用物件覆蓋了物件的原型,同時也覆蓋了對應的constructor
如果我們不重寫constructor
函式將它賦值為原函式,其實賦值的這個物件的原型就是指向Object.prototype
這個物件的原型是物件的預設指向。
let obj = { name: 'CodeUp', age: 12, } obj.__proto__ = { message: '招財是隻鳥', } console.log(obj.__proto__.__proto__)
八.總結
這篇文章我們學習了原型和原型鏈,普通物件和函式都有自己的原型,但是比較常用的是函式的prototype
我們稱之為顯式原型,物件的原型預設指向的是Object.prototype
當我們對某個物件的原型進行重寫後預設指向的都是這個,原型之間這樣一層一層組成的鏈,我們稱之為原型鏈,在 JavaScript 中繼承是基於原型的,這寫內容也是我們在後續學習 ES5 相關繼承方式的前提,雖然原型設計的非常巧妙,但是它並非是 JavaScript 的原創,但是確是這門語言的設計哲學的精妙所在。