用本身的方式(圖)理解constructor、prototype、__proto__和原型鏈

在看了網上不少相關的文章,不少都是懵逼看完,並非說各位前輩們寫得很差,而是說實在不容易在一兩次閱讀中理解透。我在閱讀了一些文章後,本身整理總結和繪製了一些相關的圖,我的認爲會更容易接受和理解,因此分享在此。也所以如下的全部的理解和圖解都是出於我的的理解,若是有錯誤的地方,請各位前輩務必見諒,並辛苦在下方提出和糾錯,我實在擔憂本身不成熟的理論底子會誤導了其他的小兄弟。

一開始,先說說爲什麼這個知識點爲何理解起來這麼亂

我的感受緣由有三:javascript

  1. JS內函數即對象。
  2. Function對象和Object對象這兩個內置對象的特殊性。
  3. 不少講解圖的指向一眼下去花裏胡哨,看着都頭疼[手動狗頭]。

再說說,爲什麼網上各位前輩的相關文章都難以參透

不少前輩在講解相關知識點的時候都是從__proto__開始講起,但在我看來,__proto__與prototype關係之密切是沒法單獨提出來說的(單獨講就意味着難以理解);而prototype與constructor又有密切關係,這就形成一種很尷尬的處境,要先講__proto__就必然須要同時講解prototype和constructor屬性,這個也就是爲什麼對於小白的咱們而言這些概念是那麼的難以理解。(以上我的見解,僅供參考)java

而後在講講我我的採起的理解方式

爲了更輕鬆、更有動力地理解透,我採用從constructor到__proto__原型鏈一步步「拆解」的方式去理解,但願有好的效果。文章內容以下:bash

  1. 先理解爲何「函數即對象」
  2. constructor其實很純粹
  3. prototype是爲什麼而出現
  4. 真正的constructor屬性藏在哪
  5. __proto__讓實例能找到本身的原型對象
  6. 究竟何爲原型鏈
  7. 原型鏈引出新的繼承方式 
  8. 學了要用系列 | 手寫一個new
  9. 總結

最後,講講往下看須要知道的那些小知識:

① 當任意一個普通函數用於建立一類對象時,它就被稱做構造函數,或構造器。

function Person() {}
var person1 = new Person()
var person2 = new Person()
複製代碼

上面代碼Person( )就是person1和person2的構造函數。

② 能夠經過對象.constructor拿到建立該實例對象的構造函數。app

console.log(person1.constructor) // 結果輸出: [Function: Person]
複製代碼

Person函數就是person1對象的構造函數。

③ Function函數和Object函數是JS內置對象,也叫內部類,JS本身封裝好的類,因此不少莫名其妙、意想不到的設定其實無需過度糾結,官方動做,神仙操做。ide

④ 原型對象即實例對象本身構造函數內的prototype對象。函數

1、先理解爲何「函數即對象」

先看如下代碼:性能

function Person() {...}
console.log(Person.constructor) // 輸出結果:[Function: Function]
// 上面是普通函數聲明方法,生成具名函數,在聲明時就已經生成對象模型。
console.log(Function.constructor) // 輸出結果:[Function: Function]
console.log(Object.constructor) // 輸出結果:[Function: Function]
複製代碼

上面的代碼構造了一個Person函數,咱們能看出那些信息?測試

  1. Person雖被聲明爲一個函數,但它一樣能夠經過Person.constructor輸出內容。輸出內容說明Function函數是Person函數[普通聲明的函數]的構造函數。
  2. Function函數同時是本身的構造函數。
  3. Function函數一樣是Object這類內置對象的構造函數。
其實上面三點總結下來就是一句:在JS裏,函數就是Function函數的實例對象。也就是咱們說的函數即對象。上面的聲明函數的代碼其實幾乎等同於下面代碼:

// 使用Function構造器建立Function對象
var Person = new Function('...')
// 幾乎?由於這種方式生成的函數是匿名函數[anonymous],而且只在真正調用時才生成對象模型。複製代碼

在JS裏,函數和對象包含關係以下:ui


總結:對象由函數建立,函數都是Function對象實例。

2、constructor其實很純粹

先忽略__proto__和prototype,直接理解constructor,代碼例子:this

function Person() {}
var person1 = new Person()
var person2 = new Person()
複製代碼

下面一張圖就畫出了它們constructor的指向(忽略了__proto__和prototype):


圖中,藍色底是Person的實例對象,而Person、Function是函數(也是對象)。

首先,咱們已經知道每一個對象均可以經過對象.constructor指向建立該對象的構造函數。咱們先假設每一個對象上都有這麼個constructor屬性,而後理解以下:

注意:constructor屬性不必定是對象自己的屬性,這裏只爲方便理解將其泛化成對象自己屬性,因此用虛線框,第三大點細講。
  1. person1與person2是Person對象的實例,他們的constructor指向建立它們的構造函數,即Person函數;
  2. Person是函數,但同時也是Function實例對象,它的constructor指向建立它的構造函數,即Function函數;
  3. 至於Function函數,它是JS的內置對象,在第一點咱們就已經知道它的構造函數是它自身,因此內部constructor屬性指向本身。

因此constructor屬性其實就是一個拿來保存本身構造函數引用的屬性,沒有其餘特殊的地方。

在接下來的全部例子都將把Function對象視爲Function對象本身的實例對象,經過去掉它的特殊性來更好理解相關概念。

3、prototype是爲什麼而出現

上一步理解是很容易的,而後這時要求你去給Person的兩個實例對象加上一個效果相同的方法,你寫了如下代碼:

// 下面是給person1和person2實例添加了同一個效果的方法sayHello
person1.sayHello = function() {
    console.log('Hello!')
}
person2.sayHello = function() {
    console.log('Hello!')
}
console.log(person1.sayHello === person2.sayHello) // false,它們不是同一個方法,各自佔有內存複製代碼

圖示以下:


當你去對比這兩個方法的時候,你會發現它們只是效果相同、名字相同,本質上倒是各自都佔用了部份內存的不一樣方法。這時候就出問題了,若是這時候有千千萬萬個實例(誇張)要這樣效果一樣的方法,那內存豈不是要炸。這時,prototype就出現解決問題了。

當須要爲大量實例添加相同效果的方法時,能夠將它們存放在prototype對象中,並將該prototype對象放在這些實例的構造函數上,達到共享、公用的效果。代碼以下:

Person.prototype.sayHello = function() {
    console.log('Hello!')
}
console.log(person1.sayHello === person2.sayHello) // true,同一個方法複製代碼

圖示以下:


而之因此這種形式能夠減小內存的浪費,是因爲無需再拿出部份內存爲同一類的實例單純建立相關同一效果的屬性或方法,而能夠直接去構造函數的prototype對象上找並調用。

總結:prototype對象用於放某同一類型實例的共享屬性和方法,實質上是爲了內存着想。

講到這裏,你須要知道的是,全部函數自己是Function函數的實例對象,因此Function函數中一樣會有一個prototype對象放它本身實例對象的共享屬性和方法。因此上面的圖示是不完整的,應改爲下圖:


其實裏面的sayHello也是個函數,也有本身的prototype,但不畫出來了,省得頭疼。

注意:接下來的用【原型對象】表示【建立本身的構造函數內部的prototype】!

4、真正的constructor屬性藏在哪

看到上面,有些小夥伴就頭疼了,你說的constructor屬性爲何我就沒在console出來的對象數據中看到呢?

思考個問題: new Person( )出來的千千萬萬個實例中若是都有constructor屬性,而且都指向建立本身的構造函數,那豈不又出現了第三點的問題,它們都擁有一個效果相同但卻都各自佔用一部份內存的屬性?

我相信大家懂個人意思了,constructor是徹底能夠被當成一個共享屬性存放在原型對象中,做用也依然是指向本身的構造函數,而實際上也是這麼處理的。對象的constructor屬性就是被當作共享屬性放在它們的原型對象中,即下圖:


總結:默認constructor其實是被當作共享屬性放在它們的原型對象中。

這時候有人會拿個反例來問:若是是共享屬性,那我將兩個實例其中一個屬性改了,爲啥第二個實例沒同步?以下面代碼:

function Person() {}
var person1 = new Person()
var person2 = new Person()
console.log(person1.constructor) // [Function: Person]
console.log(person2.constructor) // [Function: Person]
person1.constructor = Function
console.log(person1.constructor) // [Function: Function]
console.log(person2.constructor) // [Function: Person] !不是同步爲[Function: Function]複製代碼

這個是由於person1.constructor = Function改的並非原型對象上的共享屬性constructor,而是給實例person1加了一個constructor屬性。以下:

console.log(person1) // 結果:Function { constructor: [Function: Function] }複製代碼

你能夠看到person1實例中多了constructor屬性。它原型對象上的constructor是沒有改的。

嗯。嗯?嗯?!搞事?!! 這下共享屬性能理解了,但上面的圖解明顯會形成很大的問題,咱們根本不能經過一個對象.constructor找回建立本身的構造函數(之間沒有箭頭連接)!

好的,不急,第四點只是告訴你爲何constructor要待在建立本身的構造函數prototype上。接下來是該__proto__屬性亮相了。

5、__proto__讓實例能找到本身的原型對象

帶着第四點的疑問,咱們若是要去解決這個問題,咱們天然會想到在對象內部建立一個屬性直接指向本身的原型對象,那就能夠找到共享屬性constructor了,也就是下面的關係:

  1. 實例對象.__proto__ = 建立本身的構造函數內部的prototype(原型對象)
  2. 實例對象.__proto__.constructor = 建立本身的構造函數
也以下圖所示:


上面說的__proto__屬性實際上也的確是這樣的設置的,對象的__proto__屬性就是指向本身的原型對象。這裏要注意,由於JS內全部函數都是Function函數的實例對象,因此Person函數也有個__proto__屬性指向本身的原型對象,即Function函數的prototype。至於Function函數爲什麼有個__proto__屬性指向本身(藍色箭頭)也不用解釋了吧,它拿自身做爲本身的構造函數,反正就是個特例,不講道理。

疑惑來了:實例對象.constructor 等於 實例對象.__proto__.constructor?

這個就是JS內部的操做了,當在一個實例對象上找不到某個屬性時,JS就會去它的原型對象上找是否有相關的共享屬性或方法,因此上面的例子中,person1對象內部雖然沒有本身的constructor屬性,但它的原型對象上有,因此能實現咱們上面提到的效果。固然後面還涉及原型鏈,你只要知道上面一句話能暫時回答這個問題就好。

疑惑來了:prototype也是個對象吧,它確定也有個__proto__吧?

的確,它也是個對象,也的確有個__proto__指向本身的原型對象。那咱們嘗試用代碼找出它的構造函數,以下:

function Person() {}
console.log(Person.prototype.__proto__.constructor) // [Function: Object]複製代碼

由於__proto__指向原型對象,原型對象中的constructor又指向構造函數,因此Person.prototype.__proto__.constructor指向的就是Person中prototype對象的構造函數,上面的輸出結果說明了prototype的構造函數就是Object函數(對象)。

總結:這麼說的話其實函數內的prototype也不過是個普通的對象,而且默認也都是Object對象的實例。

下面一張圖就畫出了文章例子中全部__proto__指向,咱們試試從中找出它的貓膩。


貓膩1、全部函數的__proto__指向他們的原型對象,即Function函數的prototype對象

在第一點咱們就講了全部的函數都是Function函數的實例(包括Function本身),因此他們的__proto__天然也就都指向Function函數的prototype對象。

貓膩2、最後一個prototype對象是Object函數內的prototype對象。

Object函數做爲JS的內置對象,也是充當了很重要的角色。Object函數是全部對象經過原型鏈追溯到最根的構造函數。換句話說,就是官方動做,不講道理的神仙操做。

貓膩3、Object函數的prototype中的__proto__指向null。

這是因爲Object函數的特殊性,有人會想,爲何Object函數不能像Function函數同樣讓__proto__屬性指向本身的prototype?答案就是若是指向本身的prototype,那當找不到某一屬性時沿着原型鏈尋找的時候就會進入死循環,因此必須指向null,這個null其實就是個跳出條件。

上面談到原型鏈,有些小兄弟還不知道是什麼東西,那接下來看看何爲原型鏈,看懂了再回來從新理解一下貓膩三的解釋。

6、究竟何爲原型鏈

在讓我告訴你何爲原型鏈時,我先給你畫出上面那個例子中全部的原型鏈,你看看能不能看出一些規律。上面的例子中一共有四條原型鏈,紅色線鏈接起來的一串就是原型鏈


左邊的圖:原型鏈也就是將原型對象像羊肉串同樣串起來成爲一條鏈,好粗暴的解釋,但的確很形象。

右邊的圖:以前說過Person函數(全部函數)實際上是Function函數的實例,假設把它當作一個普通的實例對象,忽略它函數身份以及prototype對象,其實它和左邊圖中的person1沒什麼區別,只是它們的__proto__屬性指向了各自的的原型對象。


左邊的圖:Function函數由於是個特殊的例子,它的構造函數就是本身,因此__proto__屬性也指向本身的prototype對象;但它的特殊性並不影響它的prototype對象依然不出意外的是Object函數的實例

右邊的圖:這個理解起來就很難受,由於Object函數和別的函數同樣也是Function函數的實例,因此它的__proto__屬性毫無例外地是指向Function函數的prototype對象,可是問題是Function函數中的prototype自己又是Object函數的實例對象,因此Function函數中的prototype對象中的__proto__屬性就指向Object函數的prototype對象,這就造成「我中有你,你中有我」的狀況,也是形成難以理解的緣由之一。

爲了更好地理解原型鏈,我打算忽略掉那討厭的特例,Function函數。


忽略掉Function函數後你會發現好清爽!相信你們也發現了,__proto__屬性在其中起着關鍵做用,它將一個個實例和原型對象關聯在一塊兒,但因爲所關聯的原型對象也有多是別人的實例對象,因此就造成了串連的形式,也就造成了咱們所說的原型鏈。

7、原型鏈引出新的繼承方式

我的認爲原型鏈的出現只是一次巧合,不是特別刻意的存在。可是這種巧合確實有它本身的意義。還記得我以前說過的兩點嗎:

  1. prototype對象保存着構造函數給它的實例們調用的共享屬性和方法。
  2. 實例對象當沒有某一屬性時,會經過__proto__屬性去找到建立它們的構造函數的prototype對象,並在裏面找有沒有相關的共享屬性或方法。
那這時就頗有趣了。prototype對象自己也有一個__proto__屬性指向它本身的原型對象,上面有着構造函數留下的共享屬性和方法。那這麼說的話,假如當在本身原型對象上找不到相關的共享屬性或方法時,對於它如今所在的prototype對象而言,也是一次尋值失敗的狀況,那它天然也會去它本身的原型對象上找,世紀大片圖示以下:


如今來想一想,假如Object函數內的prototype對象中__proto__屬性不指向空,而指向本身的prototype?那不完了咯,死循環。

可能這時有小兄弟會問,這不就是一個不斷找值的過程嗎,有什麼意義?可是就由於這種巧合,讓一些可愛的人想到了一種新的繼承方式:原型鏈繼承

請看下面代碼:

function GrandFather() {
    this.name = 'GrandFather'
}
function Father() {
    this.age = 32
}
Father.prototype = new GrandFather() // Father函數改變本身的prototype指向
function Son() {}
Son.prototype = new Father() // Son函數改變本身的prototype指向

var son = new Son()
console.log(son.name) // 結果輸出:GrandFather
console.log(son.age)  // 結果輸出:32
console.log(Son.prototype.constructor) // 結果輸出:[Function: GrandFather]複製代碼

相關指向圖以下:


兩邊的圖都是忽略了Function函數的,同時將一些沒有必要展現出來的屬性給忽略了,如各大函數的__proto__屬性。

左邊的圖:在沒有改變各個函數的prototype的指向時,默認就是左邊的圖片所示。每一個函數的prototype都是默認狀況下將它們內部的__proto__指向Object函數的(黑色箭頭)。

右邊的圖:Father函數和Son函數都丟棄了它們各自的prototype對象,指向一個新的對象。這造成了三個新的有趣現象:

  1. Father函數中的prototype指向了GrandFather的實例對象,這時候這個實例對象就成爲了Father函數之後實例的原型對象,順其天然GrandFather實例對象內的私有屬性name就變成了Father函數之後實例的共享屬性;
  2. 一樣的,Son函數中的prototype指向了Father的實例對象,將Father的實例對象內的私有屬性age就變成了Son函數之後實例的共享屬性。
  3. 它們的__proto__屬性將它們串了起來,造成一條新的原型鏈。
上面的操做咱們能看到Son函數之後的實例都能經過原型鏈找到name和age屬性,也就是實現了咱們所說的繼承,繼承了父類的屬性。不過相信眼尖的咱們會發現這種繼承方式問題很大:
  1. constructor的指向不可靠了,像Son實例對象.constructor最後獲得的值是沿着原型鏈找到的GrandFather函數。可咱們本身清楚Son實例對象就該是Son函數,但卻不在咱們的意料之中。
  2. 全部所謂繼承下來的屬性全都是共享屬性,好致命的問題。
因此,Emmm,瞭解一下就好。

8、學了要用系列 | 手寫一個new

new關鍵詞的做用一句話來講就是建立一個用戶定義的對象類型的實例或具備構造函數的內置對象的實例。而咱們要去手動實現new關鍵詞,無非就是組織一場認親活動,環節有兩個:

  1. 讓一個對象認可本身的構造函數(爹)就是該構造函數
  2. 讓這個構造函數認可這個對象就是它本身的實例(子)

① 先造個Person構造函數(爹)作例子

function Person(identity){
    this.identity = identity || 'Person'
}複製代碼

② 爹有了,得有個子吧,那就建立一個空對象

var obj = {}
複製代碼

上面的語句爲字面式建立對象,實則等同於下面一句

var obj = new Object()
複製代碼

也即說明建立的空對象其實都是Object函數的實例,這麼一看,完了吧,子不認爹。

還記得咱們上面講的嗎,所謂的「空對象「內部並非真正空空如也,它們內部都有一個__proto__屬性指向本身的原型對象。而上面代碼中的obj對象也是絕不例外有個__proto__屬性指向Object對象中的prototype。

咱們知道當建立某一構造函數的實例,建立出的實例應該將__proto__屬性指向該構造函數內的prototype對象,那咱們就走走形式,讓它從新認爹。

③ 手動將實例中的__proto__屬性指向相應原型對象。

obj.__proto__ = Person.prototype複製代碼

圖解以下:


你能夠看到當指向變化後,Person函數中的prototype成爲實例對象obj的原型對象,而天然而然咱們拿到的obj.constructor就對應變成了Person函數。換句話說,obj已經認可Person函數是它本身的構造函數,也就說咱們完成了認親活動的第一環節。

那問題來了,Person函數認可這個實例(子)嗎?

若是Person函數內部沒有設置像:this.identity = identity || 'Person'這些語句(設置私有屬性/方法),其實它也就認可了,由於成爲它兒子不須要別的資格。可是不巧,Person函數確實有設置,而這些語句就像在說:

「你要成爲我兒子就須要有這個資格:擁有我設置的私有屬性。但我認了你後,你改不改那個屬性、要不要那個屬性,我就無論了。

因此如今得進入第二環節:

④ 在實例的執行環境內調用構造函數,添加構造函數設置的私有屬性/方法。

Person.apply(obj, arguments) // arguments就是參數複製代碼

咱們先要知道構造函數爲啥叫構造函數:

構造函數是一種特殊的方法,主要用來在建立對象時初始化對象, 即爲對象成員變量賦初始值。

看到關鍵做用了嗎?「爲對象成員變量賦初始值」。

再看回「老爹」,Person函數:

function Person(identity){
    this.identity = identity || 'Person' 
}
console.log(Person.identity) // 結果輸出:undefined
// 注意不要拿name這個屬性作例子,每一個函數聲明後都自帶一個name屬性用來保存函數名
複製代碼

疑惑:這裏的this不是指向構造函數自身的嗎?爲何Person函數沒有identity屬性?

感受說來話長,簡化成一句就是:函數聲明後函數體內的語句並不會當即執行,而是在真正調用時才執行。因此裏面的this在沒有調用時壓根沒指向,或者根本沒被當成屬性,只是個代碼段,因此天然也不會當即給本身賦一個identity屬性。其實說這麼多,就是爲了引出實例經過apply方法調用構造函數,讓構造函數體內此時真實存在的this指向本身,併爲本身賦相應的初始屬性值。至於arguments就是相應的參數,能夠當作用於調整初始值如何設置的參數。

整個過程結束後,實例也擁有了構造函數Person內部要求設置的屬性和方法,以下圖:


這時咱們就完成了讓這個Person構造函數認可這個obj對象就是它本身的實例,也就是第二環節順利完成。

⑤ 整個過程代碼以下:

// 構造函數登場
function Person(identity){
    this.identity = identity || 'Person'
}
// 實例對象登場
var obj = {}
// 環節一:讓obj認可本身的構造函數(爹)就是Person函數
obj.__proto__ = Person.prototype
// 環節二:obj調用Person,擁有Person給孩子們設置的屬性/方法
// 讓Person函數認可這個對象就是它本身的實例(子)
Person.apply(obj, ['son'])
// End 完成,驗證
console.log(obj.constructor) // 輸出結果:[Function: Person]
console.log(obj.identity) // 輸出結果:son複製代碼

上面只是一個實例對象new出來的過程,真正實現new方法還須要咱們將它封裝起來,以下:

⑥ 封裝成new方法

// 構造函數登場
function Person(identity){
  this.identity = identity || 'Person'
}
// 封裝本身的new
function _new(Fuc) {
  return function() {
    var obj = {
      __proto__: Fuc.prototype
    }
    Fuc.apply(obj, arguments)
    return obj
  }
}
// 封裝完成,測試以下
var obj = _new(Person)('son')
console.log(obj.constructor) // 輸出結果:[Function: Person]
console.log(obj.identity) // 輸出結果:son
複製代碼

完美,皆大歡喜!鼓掌! 

9、總結

最近在學思惟導圖怎麼作,因此嘗試直接拿思惟導圖作總結了:




寫完這篇文章後,本身是以爲清晰了不少,固然本人並不肯定內部的一些觀點是否正確,大部分觀點都是我結合各位前輩文章並加上本身的思考總結出來的一些比較能自圓其說的說法。感謝各位大佬前輩的閱讀,若是有什麼嚴重的錯誤,務必諒解和提出。

最後,感謝一個讓我基本搞懂這些概念的博客文章:

幫你完全搞懂JS中的prototype、__proto__與constructor(圖解)

若是以爲我寫得太爛能夠去看幾遍上面那篇文章。

End

相關文章
相關標籤/搜索