在深入學習JavaScript原型這一塊我們會碰到幾個概念,原型,構(gòu)造函數(shù),原型鏈,下面就通過圖解的方式來看看吧。
構(gòu)造函數(shù)
首先我們通過構(gòu)造函數(shù)創(chuàng)建一個對象,然后創(chuàng)建一個實例原型屬性
function Person(){}
// prototype
Person.prototype.name = 'xiaoming';
let person1 = new Person();
let person2 = new Person();
console.log(person1.name); // xiaoming
console.log(person2.name); // xiaoming
這里提出了一個實例原型的概念,它是由構(gòu)造函數(shù)的 prototype 屬性所指向的實例原型,這里我們就有疑問了,prototype 不就是原型了嗎?真的是這樣嗎,接著往下看,其實函數(shù)的 prototype 屬性指向了一個對象,這個對象正是調(diào)用該構(gòu)造函數(shù)而創(chuàng)建的實例原型,也就是這個例子中的 person1 和 person2 的原型。通過下面這種圖我們能了解上面構(gòu)造函數(shù)和實例原型的關(guān)系。
那到這里我們的實例對象和實例原型直接到底是什么關(guān)系呢?這個時候我們引出一個__proto__
屬性,該屬性是對象和實例原型直接的紐帶,MDN中是這么解釋的:
Object.prototype
的__proto__
屬性是一個訪問器屬性(一個getter函數(shù)和一個setter函數(shù)), 暴露了通過它訪問的對象的內(nèi)部[[Prototype]]
。絕大部分瀏覽器都支持這個非標準的方法訪問原型,然而它并不存在于 Person.prototype 中,實際上,它是來自于 Object.prototype ,與其說是一個屬性,不如說是一個 getter/setter,當使用 obj.proto 時,可以理解成返回了 Object.getPrototypeOf(obj)。
function Person() {}
let person = new Person();
console.log(person.__proto__ === Person.prototype); // true
這個時候添加上實例對象和實例原型之間的關(guān)系圖
這里多說一句這個非正式的屬性的一些注意的地方,參考MDN:
警告: 通過現(xiàn)代瀏覽器的操作屬性的便利性,可以改變一個對象的
[[Prototype]]
屬性, 這種行為在每一個JavaScript引擎和瀏覽器中都是一個非常慢且影響性能的操作,使用這種方式來改變和繼承屬性是對性能影響非常嚴重的,并且性能消耗的時間也不是簡單的花費在obj.__proto__ = ...
語句上, 它還會影響到所有繼承來自該 [[Prototype]] 的對象,如果你關(guān)心性能,你就不應該在一個對象中修改它的[[Prototype]].
。相反, 創(chuàng)建一個新的且可以繼承[[Prototype]]
的對象,推薦使用Object.create()
;
接下來我們看一下構(gòu)造函數(shù)和實例原型的關(guān)系,這里實例原型并沒有指向?qū)嵗怯幸粋€constructor指向構(gòu)造函數(shù),如圖所示:
function Person() {}
let person = new Person();
console.log(person.__proto__ == Person.prototype) // true
console.log(Person.prototype.constructor == Person) // true
到這里我們就清楚了實例,構(gòu)造函數(shù)以及實例原型之間的關(guān)系,接著往下走啊。我們知道JavaScript中都是對象,我們這里的實例原型也是對象啊,那么它指向誰呢?
原型鏈
當讀取實例的屬性時,如果實例中找不到,就會查找與對象關(guān)聯(lián)的原型中的屬性,如果還查不到,就去找原型的原型,一直找到最頂層為止。
function Person() {}
var person = new Person();
console.log(person.name) // undefined
Person.prototype.name = 'xiaoming';
person.name = 'hhh';
console.log(person.name) // hhh
delete person.name;
console.log(person.name) // xiaoming
我們先實例化構(gòu)造函數(shù)得到一個對象,然后在沒有添加屬性的時候打印則為 undefined,給實例原型添加屬性同時也給對象添加屬性,打印我們發(fā)現(xiàn)如果能在對象中找到屬性則不會往上查找,當我們刪除對象屬性會發(fā)現(xiàn)會從實例原型上去查找,通過person.__proto__
,如果實例原型上剛好有我們就取到了數(shù)據(jù),但是如果實例原型上也沒有怎么辦呢?這個時候我們就要接著往下走了,來看看實例原型的實例原型是什么 ~
假設沒有擴展原型鏈的情況下我們的原型圖會是這樣的,都會指向 Object.prototype ,那這里就結(jié)束了嗎? Object.prototype 又指向誰呢?打印下發(fā)現(xiàn)指的是null,這里請參考阮老師的 undefined與null的區(qū)別 ,這里并不表示Object的原型是null,而是說Object是最頂層的對象,也就是說我們查到 Object.prototype 就停止查找了,再往上就沒有東西了,最終的原型圖就如下所示,圖中的紅線就是我們沒有提到的原型鏈。
// 這里并不表示Object的原型是null,而是表示沒有原型
console.log(Object.prototype.__proto__ === null) // true
總結(jié):
函數(shù)的prototype屬性指向原型,說prototype是原型略顯不嚴謹,原型鏈通過__proto__
鏈接起來,這里prototype 是函數(shù)的一個屬性,并不表示原型,Person.prototype 是指向 person 實例的原型,也就是 person.__proto__
,__proto__
表示的是原型。