原型和原型鏈的深入探索

小玲慕斯發表於2020-05-30

前言

原型和原型鏈這方面的底層原理知識,一直以來都是面試市場上的一塊的肥肉,也是每一位前端開發人員不得不掌握的內功心法。一直以來,我都想要搞懂弄明白的這部分知識,所以,就借這次重學前端將這方面的成果和自己的一些拙見整理一下,分享給大家。現在就從程式設計思想開始講起吧。

本文篇幅較長,如果只想瞭解原型和原型鏈的核心知識,建議可以直接從第三部分看起。

一.程式設計思想

提起程式設計思想,這個概念在我的腦海中一直是一個非常模糊的概念,我所知道的是作為一名開發人員,每個人都應當具備這種能力,並且要不斷地去探索到底怎麼才能提高程式設計思想。那什麼是程式設計思想呢?

我覺得這個問題沒有固定的答案,如果非要給一個定義,那大概就是用計算機來解決人們實際問題的思維方式,即程式設計思想。目前我所瞭解的程式設計思想有程式導向程式設計,結構化程式設計以及物件導向程式設計。

1.程式導向程式設計

程式導向:POP(Process-oriented programming)就是分析出解決問題所需要的的步驟,然後用函式把這些步驟一步一步實現,使用的時候再一個一個的一次呼叫就可以了。這裡舉個例子:將一隻大象裝進冰箱,就可以看做是程式導向的做法。

程式導向:就是按照我們分析好了的步驟,按照這個步驟解決問題。

2.結構化程式設計

結構化程式設計(Structured programming):在程式設計的早期,程式採用流程圖和自上而下的方式進行設計。這種設計方法的主要思路是將一個大問題分解為多個小問題進行解決,再針對每個小問題編寫方法。總體上來說,是先構建一個主過程來啟動程式流程,隨後根據程式走向來呼叫相關的其他過程,這種程式設計思想就是結構化程式設計。

舉個經典的栗子:需要編寫不同的工資計算方法,社保計算方法以及個人所得稅計算方法。而如果從另一個角度看這個程式,則可以從判斷判斷該程式中的物件入手。該程式中的物件,最明顯的就是“員工”!(小玲至今沒弄懂工資咋算的,所以就隨意畫了一個工資計算的流程圖,大家將就看看吧)

3.物件導向程式設計

物件導向是把事物分解成一個個物件,然後由物件之間分工合作。

舉個例子:將大象裝進冰箱,物件導向的做法。(突然有點可憐這隻大象了,老是被裝進冰箱)

先找出物件,並寫出這些物件的功能

(1)大象物件

  • 進去

(2)冰箱物件

  • 開啟

  • 關閉

(3)使用大象和冰箱的功能

物件導向是以物件功能來劃分問題,而不是步驟。

4.三大程式設計思想的對比

程式導向:

優點:效能比物件導向高,適合跟硬體聯絡很緊密的東西,例如微控制器就採用的程式導向程式設計。

缺點:沒有物件導向易維護

物件導向:

優點:易維護,易複用,易擴充套件,由於物件導向有封裝,繼承,多型性的特性,可以設計出低耦合的系統,使系統更加靈活,更加易於維護。

缺點:效能比程式導向低。

結構化程式設計:

優點:程式易於閱讀、理解和維護,能將一個複雜的程式分解成若干個子結構,便於控制、降低程式的複雜性;提高了程式設計工作的效率,降低了軟體開發成本

缺點:

  • 使用者要求難以在系統分析階段準確定義,致使系統在交付使用時產生許多回問題。

  • 用系統開發每個階段的成果來進行控制,不能適應事物變化的要求。

  • 系統的開發週期長。

某大佬總結的:用程式導向的方式寫出來的程式是一份蛋炒飯,而用物件導向寫出來的程式是一份蓋澆飯。(覺得有點意思,拿來用用)

5.深入物件導向

物件導向更貼近我們的實際生活,可以使用物件導向的思想來描述世界事物,但是事物分為具體的事物和抽象的事物。

物件導向程式設計:

程式中先用物件結構儲存現實中一個事物的屬性和功能,然後再按需使用事物的屬性和功能,這種程式設計方法,就叫物件導向程式設計

使用物件導向的原因:

便於大量資料的管理和維護

物件導向的思維特點:

(1)抽取(抽象)物件共用的屬性和行為組織(封裝)成一個類(模板)

(2)對類進行例項化,獲取類的物件

物件導向程式設計我們考慮的是有哪些物件;按照物件導向的思維特點,是不斷的建立物件,使用物件,指揮物件做事情。

物件導向的三大特點:

封裝、繼承和多型

想要對程式設計思想有進一步瞭解的話,可以看看一位前輩寫的一位十年軟體工程師告訴你什麼是程式設計思想,相信你會有新的收穫哦。

二.物件

1.什麼是物件?

廣義來講,都說萬物皆物件。物件是一個具體的事物,看的見摸得著的實物,例如:一本書,一支筆,一個人可以是"物件",一個資料庫、一張網頁等也可以是“物件”。

在JavaScript中,物件是一組無序的相關屬性和方法的集合,所有的實物都是物件,例如字串,數值,陣列,函式等。

許多人都以為“JavaScript 中萬物都是物件”,這是錯誤的。物件是 6 個(或者是 7 個,取 決於你的觀點)基礎型別之一。物件有包括 function 在內的子型別,不同子型別具有不同 的行為,比如內部標籤 [object Array] 表示這是物件的子型別陣列。 物件就是鍵 / 值對的集合。可以通過 .propName 或者 ["propName"] 語法來獲取屬性值。

——選自《你不知道的JavaScript(上卷)》

2.組成

物件是由屬性和方法組成的:

  • 屬性:事物的特徵,在物件中用屬性來表示(常用名詞)

  • 方法:事物的行為,在物件中用方法來表示(常用動詞)

3.何時使用物件?

今後只要使用物件導向的程式設計方式,都要先建立所需的所有物件,作為備用。

4.建立物件的幾種方式

看到前輩們寫的文章中一共提到了有以下五種方式:

  • 工廠模式(物件字面量):用{}建立一個物件

  • 建構函式模式:用new建立

  • 原型物件模式:利用prototype建立

  • 組合使用建構函式模式和原型模式 :建構函式模式用於定義例項屬性,原型模式用於定義方法和共享的屬性(可以看成是重寫原型物件)。

  • 動態原型模式

可以看看javascript中建立物件的幾種方式 或者 JavaScript建立物件的幾種方式,講解的非常詳細。在這裡我就只簡單介紹其中三種

4.1 工廠模式(物件字面量)

今天上班第一天,先把小玲自己的資訊記錄一下~

      
1   var person = {
2             uname:"xiaoling",
3             age:18,
4             intr:function(){
5                  console.log('我是小玲')}
6    };
7    console.log(person);
8    person.intr() //呼叫方法 

 

現在我們來看一下,假如今天老闆開會說,公司要再來三個新同事,讓你記錄一下她們的資訊。

然後你按照這種方式一會就完成了

    
 1         var person1 = {
 2             uname:"小張",
 3             age:20,
 4             intr:function(){
 5                  console.log('我是小張1111')}
 6         };
 7         var person2 = {
 8             uname:"小劉",
 9             age:23,
10             intr:function(){
11                  console.log('我是小劉')}
12         };
13         var person3 = {
14             uname:"小蘇",
15             age:24,
16             intr:function(){
17                  console.log('我是小蘇')}
18         };

 

如果按照這種字面量的方式去建立,不會感覺太傻瓜式了嗎?假如老闆說要增加一個特長 Specialty 的資訊,你是否對要這些建立好的每一個物件都要進行新增修改呢?另外,請注意 person1 這個物件,因為小玲粗心了一下,不小心記錄錯誤了。導致兩個地方的屬性 name 和 intr 方法中的列印的名字內容是不一致的。那麼,這種問題能否在以後開發工作中避免呢?

對於第二個問題,我們可以用this.屬性名來代替方法中引用的物件屬性即

1 console.log('我是'+this.uname)
2 //或者
3 console.log(`我是${this.uname}`)

 

關於this,每個函式都會自動攜帶(可以想象成天生就有),可以直接使用,指向正在呼叫函式的物件(誰呼叫,this就指向誰),所以person.intr(),intr中的this就指向person。

對於第一個問題,我們可以用下面講的建構函式來解決~

4.2 建構函式模式

建構函式

建構函式式是一種特殊的函式,主要用來初始化物件,及為物件成員變數賦初始值,它總是與new一起使用。我們可以把物件中一些公用的屬性和方法抽取出來,然後封裝到這個函式裡面。

new在執行時做的四件事

①在記憶體中建立一個新的空物件

②讓this指向這個新的物件

③執行建構函式裡面的程式碼,給這個新物件新增屬性和方法

④返回這個新物件(所以建構函式裡面不需要return)

    
 1     //2.建構函式模式
 2         //建立建構函式 Person    uname/age/intr 是例項成員
 3       function Person(name,age){
 4           this.uname = name;
 5           this.age = age;
 6  7           this.intr = function(){
 8               console.log(`我是${this.uname}`);
 9           }
10       }
11         //建立例項
12        var p1 = new Person('xiaoling',18),
13            p2 = new Person('小張',20),
14            p3 = new Person('小劉',23);
15 16           console.log(p1); //Person {name: "xiaoling", age: 18, intr: ƒ}
17           console.log(p2); //Person {name: "小張", age: 20, intr: ƒ}
18           console.log(p3); //Person {name: "小劉", age: 23, intr: ƒ}
19         //呼叫方法
20           p1.intr(); //我是xiaoling
21           p2.intr(); //我是小張
22           p3.intr(); //我是小劉
23            console.log(Person.intr); //undefined
24 25            Person.sex = '女';   //sex是靜態成員
26            console.log(Person.sex); //
27            console.log(p1.sex);  //undefined

 

這裡提幾點:

  • 建構函式中的屬性和方法我們稱為成員,成員可以新增

  • 例項成員就是建構函式內部通過this新增的成員 ,如name,age,intr就是例項成員;例項成員只能通過例項化的物件來訪問,如p1.uname。不能通過建構函式來訪問例項成員,如Person.uname (這樣是不允許的)

  • 靜態成員是在建構函式本身新增的成員,如sex就是靜態成員。靜態成員只能通過建構函式來訪問,不能通過例項物件來訪問(不可以用p1.sex訪問)

這樣,就能愉快地解決4.1的第一個問題啦!

建構函式存在的問題

現在我們來看一下p1,p2,p3在記憶體中是怎樣的?

從上圖中我們可以看到,建立多個不同的物件時,會同時在記憶體中開闢多個空間建立多個相同的函式,這些方法都是一樣的。所以就存在浪費記憶體的問題。我們希望所有的物件使用同一個函式,這樣會比較節省記憶體,那麼這個問題改如何解決呢?

利用原型物件的方式建立物件可以解決。

4.3 原型物件模式 -prototype

這節內容比較多,就單獨在第三部分講了~

另:其他建立物件的方式請自行查閱資料瞭解(可以看看javascript中建立物件的幾種方式 或者 JavaScript建立物件的幾種方式),這裡我就不再講了~

三.原型

先來簡單介紹幾個概念

1.建構函式

前面已經提過了,這裡就再簡單概括一下吧。

建構函式式是一種特殊的函式,return會自動返回一個物件。使用時要搭配new使用,並且new會做4件非常重要的事。

2.原型物件

現在想想什麼是原型呢?

一個物件,我們也稱prototype為原型物件,他具有共享方法的作用。

其實在建立每個建構函式時,都會自動附贈一個空物件,名為原型物件(prototype),屬性名為prototype,這個屬性指向函式的原型物件,同時這個屬性是一個物件型別的值。通過 建構函式.prototype 屬性,可獲得這個建構函式對應的一個原型物件。

建構函式通過原型物件分配的函式是所有物件共享的。JavaScript規定,每一個建構函式都有一個prototype屬性,他指向另一個物件。請再次注意這個prototype就是一個物件,這個物件的所有屬性和方法,都會被建構函式所擁有。

借用一張圖來表示建構函式和例項原型之間的關係:

3.物件原型proto

物件都會有一個屬性proto指向建構函式的prototype原型物件,之所以我們的物件可以使用建構函式prototype原型物件的屬性和方法,就是因為物件物件有proto原型的存在,那麼物件的proto是怎麼存在的呢?

我們來看一下這段話:

JavaScript 中的物件有一個特殊的 [[Prototype]] 內建屬性,其實就是對於其他物件的引用。幾乎所有的物件在建立時[[Prototype]] 屬性都會被賦予一個非空的值。 

注意:很快我們就可以看到,物件的 [[Prototype]] 連結可以為空,雖然很少見。

                                                    ——選自《你不知道的JavaScript(上卷)》 第五章

這段話的意思其實就是在告訴我們:每個物件都有“proro”屬性,這個屬性會通過指標的方式指向其他物件,這個“其他物件”就是我們說的原型物件。當然除了頂級Object.prototype.proto為null外,幾乎所有的"proto"都會指向一個非空物件。

當我們用建構函式建立物件時,new的第二步自動為新物件新增"_ proto "屬性,將" _ proto_ _"屬性指向當前建構函式的原型物件。

比如: 如果var p1=new Person("xiaoling",18)

則new會自動: p1._ proto _=Person.prototype。

然後會有以下 結果:

① 凡是這個建構函式建立出的新物件,都是原型物件的孩子(子物件)

②放在原型物件中的屬性值或方法,所有子物件無需重複建立,就可直接使用。

看到這裡,是否有點懵圈呢?別急,我們來畫個圖理一理

如上圖所示:建構函式的prorotype屬性 和proto物件原型指向的是同一個物件。

來證明一下吧在控制檯列印一下這段程式碼:

1 console.log(Person.prototype === p1.__proto__);//true
2 console.log(Person.prototype === p2.__proto__);//true
3 console.log(Person.prototype === p3.__proto__);//true

 

我們發現結果都是true,那麼說明proto物件原型和原型物件prototype是等價的。或者說他們就是同一個物件,即:

建構函式.prototype === 對應例項物件.proto

4.原型物件模式

前面介紹了很多概念,現在來介紹一下原型物件prorotype集體是怎麼實現的,其實很簡單。

建構函式.prototype.方法 = function(){ ... }

具體看程式碼:

 1       //原型物件方式
 2          function Person(name,age){
 3           this.uname = name;
 4           this.age = age;
 5       }
 6       Person.prototype.intr = function(){
 7               console.log(`我是${this.uname}`);
 8           }
 9         //建立例項
10        var p1 = new Person('xiaoling',18),
11            p2 = new Person('小張',20),
12            p3 = new Person('小劉',23);
13            console.log(p1); //Person {name: "xiaoling", age: 18}
14           console.log(p2); //Person {name: "小張", age: 20}
15           console.log(p3); //Person {name: "小劉", age: 23}

 

我們來看控制檯列印:

我們會發現,列印結果並沒有出現intr方法,那我們直接來呼叫一下試試吧

//呼叫intr方法
p1.intr()
p2.intr()
p3.intr()

 

控制檯結果:

我們發現,居然可以直接呼叫,這是怎麼回事呢?我們現在把p1,p2,p3在控制檯列印的結果展開

我們可以看到每個例項物件都有一個物件原型proto,將它展開就能看到intr方法,難道例項物件呼叫成功的intr()是proto上的嗎?。我們知道例項物件的proto就是建構函式的prototype,然而我們的intr方法就是定義在Person.prototype上的,所以,例項物件p1,p2,p3能夠成功呼叫intr方法。

我們來看一下,他們在記憶體中是怎麼樣的(簡單畫了一下)?

我們可以看到,p1,p2,p3的__proto__通過指標指向原型物件。我們知道原型物件是一個物件,所以intr在原型物件中儲存的只是一個地址,這個地址會通過指標指向intr方法儲存的位置。所以p1,p2,p3呼叫的intr方法,其實就是原型物件上的intr,他們共享同一個方法,這也正是前面提到的原型物件的作用:共享方法。

這樣4.2提到的浪費記憶體的問題就完美的解決啦。雖然建立了三個例項物件,但是他們用的都是同一個方法(只佔用了一份記憶體),就算我們們再建立3000,30000個物件,他們用的還是同一個方法,佔用的記憶體依然只有一份,記憶體資源得到了大大的改善和節省,perfect!

那我們再來思考一個問題:p1,p2,p3,自己沒有uname,age,intr這些方法和屬性,為什麼可以使用呢?這就涉及到我們後面會講的繼承以及JavaScript的查詢機制規則了.我們後面會說。

5.三者關係

在上文中,我們總是提到建構函式,原型(原型物件),例項,那麼這三者之間到底有什麼樣的關係呢?

目前我們已經知道的關係是:

  • 建構函式.prototype 指向的是原型物件

  • 例項物件的proto指向的也是原型物件

我們再來看一下這三個例項物件的列印結果,上圖

有看到什麼額外的東西嗎?請再仔細看一下!

我們可以看到每個例項物件展開,他裡面都有一個proto,這個不是重點,重點是,每個proto屬性展開,他不僅僅有我們自己定義的方法intr,還有一個淺粉色的constructor屬性,重點是我們可以看到這個這個constructor的值正好是我們的建構函式Person.

我們們來驗證一下,在控制檯輸出以下程式碼:

1 console.log(Person.prototype.constructor === Person); //true
2 console.log(p1.__proto__.constructor === Person);//true
3 console.log(p2.__proto__.constructor === Person);//true console.log(p3.__proto__.constructor === Person);//true

 

我們得到的結果是4個true,說明我們上文的猜想是正確的,也就是說建構函式也有一個屬性constructor,這個constructor屬性指向的是建構函式。既然建構函式能夠出生的時候就有prototype屬性--建立每個建構函式時,都會自動附贈一個空物件,名為原型物件(prototype),那我們是不是也可以把原型物件上憑空多出來的constructor當成是它天生就有的呢(哎!有些人註定了,出生就是不平凡的,自帶光環)

好了,我們現在再來總結一下:

  • 建立建構函式時,建構函式會附贈一個prototype屬性,這個prototype屬性以指標的方式指向一個物件,這個物件我們稱為原型物件。

  • 原型物件存在的同時,也會自動附贈一個constructor屬性,這個屬性以指標的方式指向他的建構函式。

  • 用建構函式例項化物件時,會通過new這個動作做4件事。

    ①在記憶體中建立一個新的空物件

    ②讓this指向這個新的物件(自動設定新物件的_ proto _指向建構函式的原型物件——繼承)

    ③執行建構函式裡面的程式碼,給這個新物件新增屬性和方法

    ④返回這個新物件(所以建構函式裡面不需要return)

  • 例項物件建立時會自帶一個內建屬性proto,這個proto性會通過指標的方式指向原型物件(我們可以稱為父類)

    再來畫一個圖看看他們之間的關係

再來看一張更詳細的圖

看完這張圖,你是不是又懵圈了呢?沒關係,請跟我一樣拿起筆自己也在紙上畫一畫吧,如果你能畫明白,說明你已經動了一大半了。如果還沒有明白,我們一起再來捋一捋:

  1. 建構函式Person生成的時候會自動存在一個prototype屬性,即Person.prototype,我們稱為原型物件。

  2. 原型物件是一個物件,它存在的同時會自動生成一個constrctor屬性,這個屬性會自動指向他的建構函式,即Person。

  3. 用new生成例項物件時,這個實力物件同時會攜帶一個proto屬性,這個屬性會指向建構函式的原型物件,通過這個proto,例項物件繼承了原型物件上的所有方法,就是可以用原型物件上的方法。

其實不僅僅是例項物件,任何一個物件資料型別都存在這樣的關係。

再來看一張圖,或許你會更加明白

(本圖來自《JavaScript 高階程式設計》的圖 6-1)

好了,建構函式,原型物件,例項物件三者之間的關係就講完了,如果還沒有弄明白,建議多看幾遍,看的同時自己動手在紙上畫一畫哦。

四.JavaScript的成員查詢機制

先直接講一下規則:

①當訪問一個物件的屬性(包括方法)時,首先查詢這個物件有沒有該屬性。

②如果沒有就查詢他的原型(也就是proto指向的prototype原型物件)。

③如果還沒有就查詢原型物件的原型(Object的原型物件)。

④以此類推一直到Object為止(null).

proto物件原型的意義就在於為物件成員查詢機制提供了一個方向,或者說一條路線。

這一部分看不明白沒關係,先大概知道這個規則就行。

五.原型鏈

在JavaScript中萬物都是物件,物件和物件之間也有關係,並不是孤立存在的。物件之間的繼承關係,在JavaScript中是通過prototype物件指向父類物件,直到指向Object物件為止,這樣就形成了一個原型指向的鏈條,專業術語稱之為原型鏈

舉例說明:person → Person → Object ,普通人繼承人類,人類繼承物件類

當我們訪問物件的一個屬性或方法時,它會先在物件自身中尋找,如果有則直接使用,如果沒有則會去原型物件中尋找,如果找到則直接使用。如果沒有則去原型的原型中尋找,直到找到Object物件的原型,Object物件的原型沒有原型,如果在Object原型中依然沒有找到,則返回null。這也是例項物件p1,p2,p3能夠使用intr()方法的最重要的

上文說到“如果在Object原型中依然沒有找到,則返回null”,這個一起來驗證一下。我們先來看一下在瀏覽器執行的這些程式碼:

我們知道任何一個原型物件都是一個物件,而每個物件都有proto屬性,所以Object的原型物件也是一個物件(可以看Object.prototype的列印結果就是一個物件),那麼他的proto就是上圖我們列印的結果null.所以我們得到了驗證結果

即:Object.prototype.proto = null

所以在JavaScript中,Object是一等公民!

現在我們把原型鏈畫出來!

現在再看著這張圖,重新讀一遍第四和第五部分,你就會明白原型鏈了。

六.構造器

1.定義

本文沒有構造器的定義(沒有找到對它的準確定義),真的要說它是什麼只能說:建構函式。

建構函式跟普通函式非常相似,我們已經說過建構函式時一種特殊的函式(第四部分4.2中),我可以通過new關鍵字來使用它們。主要有兩種型別的建構函式,native建構函式(Array,Object等)它們可以在執行環境中自動生成,還有自定義的建構函式,你可以定義自己的方法和屬性,比如我們自定義的Person建構函式。

2.native建構函式

JavaScript中有內建(build-in)構造器/物件共計12個(ES5中新加了JSON):

Object、Function、String、Number、Boolean、Array、RegExp、Data、Error、Math、JSON、Global

3.自定義建構函式

就是我們可以根據需要按照建構函式的方式定義自己的方法和屬性就行。

4.一個重要結論

所有構造器/函式的proto都指向Function.prototype(Function.prototype是一個空函式)

怎麼驗證這句話呢?上程式碼:

 1         console.log(Boolean.__proto__ === Function.prototype); //true
 2         console.log(Number.__proto__ === Function.prototype); // true
 3         console.log(String.__proto__ === Function.prototype);  // true
 4         console.log( Object.__proto__ === Function.prototype); // true
 5         console.log(Function.__proto__ === Function.prototype);  // true
 6         console.log(Array.__proto__ === Function.prototype); // true
 7         console.log(RegExp.__proto__ === Function.prototype); // true
 8         console.log(Error.__proto__ === Function.prototype);  // true
 9         console.log(Date.__proto__ === Function.prototype);// true
10 11 12         console.log(Function.prototype);

 

結果:

再來個自定義構造器:

 1          //自定義構造器
 2          //原型物件方式
 3          function Person(name,age){
 4           this.uname = name;
 5           this.age = age;
 6         }
 7         Person.prototype.intr = function(){
 8               console.log(`我是${this.uname}`);
 9           }
10         
11         console.log(Person.__proto__ === Function.prototype);  //true

 

這說明什麼呢?

①JavaScript中的內建構造器/物件共計12個(ES5中新加了JSON).上面列舉了可訪問的9個構造器。剩下如Global不能直接訪問,Arguments僅在函式呼叫時由JS引擎建立,Math,JSON是以物件形式存在的,無需new。它們的proto是Object.prototype。如下

Math.__proto__ === Object.prototype // true
JSON.__proto__ === Object.prototype // true

②所有的構造器都來自於Function.prototype,甚至包括根構造器Object及Function自身。所有構造器都繼承了Function.prototype的屬性及方法。如length、call、apply、bind(ES5)。

③Function.prototype也是唯一一個typeof XXX.prototype為 “function”的prototype。其它的構造器的prototype都是一個物件。

    
 1 console.log(typeof Function.prototype) // function
 2 console.log(typeof Object.prototype)   // object
 3 console.log(typeof Number.prototype)   // object
 4 console.log(typeof Boolean.prototype)  // object
 5 console.log(typeof String.prototype)   // object
 6 console.log(typeof Array.prototype)    // object
 7 console.log(typeof RegExp.prototype)   // object
 8 console.log(typeof Error.prototype)    // object
 9 console.log(typeof Date.prototype)     // object
10 console.log(typeof Object.prototype)   // object

 

④除了Function.prototype,所有建構函式的prototype都是一個物件

5.一等公民

前面原型鏈部分我們知道Objec是一等公民,那其實Function也是。

我們已經知道了所有構造器(含內建及自定義)的proto都是Function.prototype,那Function.prototype的proto是誰呢?

console.log(Function.prototype.__proto__ === Object.prototype) // true

這說明所有的構造器也都是一個普通JS物件,可以給構造器新增/刪除屬性等。同時它也繼承了Object.prototype上的所有方法:toString、valueOf、hasOwnProperty等。

最後再提一次:Object.prototype的proto是誰?

前面我們已經驗證過是null,到頂了

Object.prototype.__proto__ === null // true

講到這裡,是不是又有點懵圈了呢?上圖幫助你消化吧

看圖,一起理一理:

  • 每個建構函式(不管是內建建構函式還是自定義的建構函式),他的proto都指向Function.Prototype,包括Function他自己(Math和JSON不算在裡面,因為他們的proto指向的是Object.prototype)。

  • 每個建構函式都有一個內建屬性即prototype,他們指向自己的原型物件,除了Function的原型物件是一個空函式外,所有建構函式的prototype都是一個物件。

  • 每個原型物件都有一個內建屬性constructor,屬性,constructor屬性指回原型物件的屬性

  • Object是頂級物件,Object.prototype.proto為null

  • 每個例項物件都有一個proto屬性,通過這個proto屬性,這個例項物件可以按照JavaScript的成員查詢規則(本文第四部分),去使用原型鏈上的屬性和方法,也就是我們說的繼承父類的屬性和方法的本質。這也是我們隨便建立一個陣列或者物件,能夠使用toString()/valueOf() pop()/filter()/sort()等方法的原因。

現在再看這張圖明白了嘛,回國頭再去看一看第四部分:JavaScript的成員查詢規則 是不是也頓悟了很多。

七.一張手繪原型鏈

請忽略我醜陋的字跡,能看懂這張圖並且自己可以畫出來,說明你今天的收穫非常大哦~

 

到這裡我們就講完原型和原型鏈相關的內容了,可能還有些地方沒講到,大家就自行下去再研究了。

另外建議紅寶書和你不知道系列多看幾遍(「書讀百遍其義自見」是很有道理的)。

再推薦幾位前輩寫的不錯的文章,值得多讀幾遍:

 

後記

非常感謝大家能夠認真看完這篇文章。其實在提筆寫這篇文章之前,小玲的內心是非常緊張和忐忑的,因為害怕自己研究的不夠深入和全面,但是一想到可以和大家分享我的收穫,還是非常激動和興奮的。可能有些地方講的還不夠清楚,大家可以自己多思考一下,你自己思考想出來的東西,比別人灌輸給你的更能讓你記憶深刻。如果,若有某個地方存在問題或者不明白的,歡迎大家積極提出寶貴的建議和見解!

 

參考文章:

https://www.cnblogs.com/TRY0929/p/11870385.html

https://www.jianshu.com/p/dee9f8b14771

https://www.cnblogs.com/snandy/archive/2012/09/01/2664134.html

https://www.cnblogs.com/webjoker/p/5319377.html

相關文章