前端開發:JS中原型和原型鏈的詳解

三掌櫃發表於2023-02-12

前言

在前端開發過程中,涉及到JS原理相關的內容也就是常用的幾大模組,不僅常用而且很重要,但是涉及到原理的話會有點難懂,尤其是對JS接觸不太久的開發者來講。本篇博文就來分享一下關於JS的原型和原型鏈相關的知識點,雖然複雜、難懂但是很重要,值得收藏,方便後期查閱使用。

一、prototype

背景

JS 中,除了基本資料型別(也叫簡單資料型別)之外的型別,都是引用資料型別(也叫複雜資料型別),即物件;也就是說,在JS的宇宙中,一切皆是物件。

在ES6之前,由於JS中沒有類的概念(在ES6的時候引入了class,但其也只是語法糖),在實際開發中想要將所有的物件關聯起來就成了問題,於是原型和原型鏈的概念應運而生。

原型字面釋義(來源於網路)

原型(prototype)這個詞來自拉丁文的詞proto,意謂“最初的”,意義是形式或模型。在非技術類的文中,一個原型是給定種類的一個代表性例子。

在以原型為基礎的程式方面,一個原型是一個最初的物件(object);新的物體藉由複製原型產生。

JS中原型(Prototype)的概念

關於這個JS中的原型概念,網上有太多文章,釋義也是大同小異,那麼在這裡根據網上有用的釋義以及自己的總結,重新總結一下JS中原型的概念,由淺及深的釋義,具體如下所示:

JS中的原型(Prototype)是基於繼承來講的,其實原型就是一個物件,它是JS中繼承的基礎,JS裡面的繼承就是基於原型來繼承的,原型的作用就是為了實現物件的繼承。JS中每個物件在建立的時候都會與之關聯另外一個物件,這個關聯的物件就是原型,每個物件都會從原型中繼承。需要注意的是object比較特殊,它沒有對應的原型。

原型(Prototype)的其他釋義

原型可以理解為是一個JS方法的屬性,每次在建立函式方法的時候,JS會將一個名字為prototype的屬性新增到函式方法上,這個prototype就是該函式方法的原型物件,它預設有一個constructor的屬性指向原來方法的物件,任何新增到prototype的屬性和方法都在這個constructor裡面,所有同類的例項會共享這個原型物件,例項物件__proto__屬性指向這個物件,方法的prototype屬性指向這個物件。

其實,每個函式都會對應有一個prototype屬性,prototype就是使用建構函式建立的物件的原型,它其實是一個指標,指向原型物件,該原型物件包含屬性和方法可以被所有例項共享使用,建構函式中的prototype是顯性原型,原型物件有一個constructor屬性,指向函式本身。

原型物件(prototype)

為什麼上面解釋過原型的內容之後,這裡再寫原型物件呢?是因為網上寫法太多,容易讓不太清楚的開發者出現雲裡霧裡的感覺,所以再單獨提一下。具體如下所示:function Function(){}console.log(Function.prototype) 輸出結果如下所示:
image.png

在JS中,每個函式都有一個prototype屬性,這個屬性指向函式的原型(也叫顯式原型),這個原型是一個物件,所以prototype也叫原型物件,每一個JS物件都會從一個prototype中繼承屬性和方法。

每個例項物件(object)都有一個私有屬性(即__proto__)指向它的建構函式的原型物件,該原型物件也有一個自己的原型物件,逐層向上直到一個物件的原型物件為null的時候,根據定義流程來講,null是沒有原型的,所以到這裡就作為一個原型鏈中的最後一步,原型鏈會在下面介紹。

__proto__屬性(注意proto左右兩邊各是2個下劃線)

JS中的所有物件(null除外)都存在一個__proto__屬性(__proto__不是標準屬性,只適用於區域性的瀏覽器,標準的方式是使用Object.getPrototypeOf()),__proto__指向例項物件的建構函式的原型(即原型物件),__proto__一般也被叫做隱式原型,它也包含一個constructor屬性,該屬性指向的是建立該例項的建構函式,具體示例如下所示:

function Student(name) {
   this.name = name;
}
var student = new  Student("LiMing");
console.log(student._proto === Student.prototype ) //輸出結果為:true

image.png

透過上面示例可以看到,stu是例項物件,Student是student的建構函式,student的__proto__屬性指向建構函式Person的原型。最後,其實絕大多數的瀏覽器都支援__proto__這個非標準的方法訪問原型,但它不存在於 Student.prototype中,實際上是來自於 Object.prototype,相當於是一個getter/setter,在需要使用object.proto的時候就是返回的Object.getPrototypeOf(object)。

注意:如果呼叫例項物件的方法查不到,就會去原型物件裡面繼續查詢。

hasOwnProperty方法

當去訪問一個物件的屬性的時候,該屬性可能來自該物件自己,也可能來自該物件的屬性指向的原型,在不確定這個物件的來源的時候,就要用到hasOwnProperty()方法來判斷一個屬性是否來自物件自身。

注意:透過hasOwnProperty()方法可以判斷一個物件是否在物件自身中新增,但不可以判斷是否存在於原型中,因為極有可能該屬性就不存在,即在原型中不管屬性存在與否都會返回false。

in關鍵字(運算子)

in關鍵字或者說是運算子,是用來判斷一個屬性是否存在於該物件中,但在查詢這個屬性的時候,首先會在物件自身中查詢,如果在物件自身中找不到會再去原型中找。也就是說只要物件和原型中有一個地方存在該屬性,就返回true。

延伸:在判斷一個屬性是否存在於原型中,若該屬性存在,但不在物件自身中,那麼該屬性一定存在於原型中!

原物件的原型

在JS中所有原有引用型別都會在其建構函式的原型上定義方法,也就是透過Object建構函式生成的,使用最原始的方式建立,例項的proto指向建構函式的prototype。

image.png

原型與例項

當讀取例項屬性的時候,若找不到該屬性,就會查詢與該例項關聯的原型中的屬性,若還查不到,就會去找原物件的原型,依此類推,直到找到最上層為止。

原型的作用

1、使用已經繼承的方法解決方法過載的問題;
2、針對類的功能進行擴充套件,增添內建的方法和屬性等。

原型中的this指向

原型中的this指向例項化物件。

原型訪問的方式

關於原型訪問的方式:如果想要訪問一個物件的原型,可以透過ES5中的Object.getPrototypeOf()方法和ES6中的__proto__屬性兩種方式來訪問。

原型物件的使用場景

在實際開發中,開發者可能會使用JS類庫,但是當發現當前的庫中不存在想要的屬性或者方法的時候,不能修改原始碼的情況下且不想給每個例項物件單獨定義相關屬性和方法的時候,此時就可以考慮使用原型物件來進行擴充套件使用。

原型使用的示例

示例一:把移除陣列中的值的方法新增到陣列的原型上,在使用的時候只用呼叫函式填入值即可。
具體如下所示:

let fruits =["apple","banana","cherry","orange","melone"];
Array.prototype.remove = function(v){
this.splice(v,1); // 根據輸入的下標擷取對應的一個元素
return this;
}
fruits.remove(2);  // 輸入陣列的下標,這裡是想要移除陣列的第3個元素console.log("----fruits:",fruits); //輸出結果為:'apple', 'banana', 'orange', 'melone'

image.png

示例二:透過使用原型物件來擴充套件自定義的物件

function Student() {} //定義Student構造器
var stu = new Student(); //例項化物件
stu.name = "zhoujielun";stu.age = 28;/* 此時發現缺少了手機號屬性,使用原型物件進行擴充套件**/
Student.prototype.phone = "185****1111";console.log(stu.phone) //輸出結果為:185****1111

image.png

示例三:關於原型的測試題

function Aaa(){}
function Bbb(value){
this.value = value;
}
function Ccc(value){
if(value){this.value = value;}
}
Aaa.prototype.value = 1;
Bbb.prototype.value = 1;
Ccc.prototype.value = 1;
console.log(new Aaa().value);     //輸出結果為:1
console.log(new Bbb().value);         //輸出結果為:undefinedconsole.log(new Ccc(2).value);     //輸出結果為:2

image.png

上面輸出結果的分析,new Aaa()為建構函式建立的物件,它自身沒有value屬性,所以會向它的原型去找,發現原型的value屬性的屬性值為1,所以輸出值為1;new Bbb() 為建構函式建立的物件,該建構函式有引數value,但該物件沒有傳引數,所以輸出值為undefined;new Ccc()為建構函式建立的物件,該建構函式有引數value,且傳的引數值為2,執行函式內部檢測到if條件為真的時候,執行語句this.value = 2,所以輸出值為2.

建構函式(constructor)

建構函式(constructor)的定義,其實就是透過藉助new關鍵字來呼叫(例項化物件)的函式,就叫做建構函式。JS中每個原型都會對應一個constructor的屬性,指向它關聯的建構函式。

建構函式是一個“真”函式,它在定義的時候需要首字母大寫,任何的函式都可以作為建構函式來使用,建構函式和普通函式的區別在於功能層面來區分的,建構函式的主要功能是初始化物件,且要和new關鍵字一起使用,使用new就是在從無到有的新建物件,建構函式是為初始化的物件新增屬性和方法。

引申:new關鍵字,在申請記憶體、建立物件的時候呼叫new,程式後臺會隱式執行 new Object()來建立物件,所以透過new關鍵字建立的字串、數字不是引用型別,而是非值型別。建構函式示例,具體如下所示:

//這是一個建構函式。
function Student(name) {
this.name = name;
}
var con = new Student("LiMing"); //建構函式
if(con.constructor == String) {console.log("112233");}

image.png

建構函式其他方面

1、一個建構函式會生成一個例項的原型(prototype)屬性,透過它可以指向對應的例項原型(即原型物件);
2、原型物件有一個constructor屬性,透過它可以指向對應的建構函式;
3、建構函式和原型物件,透過屬性可以相互指向;
4、new一個物件指的是new 建構函式,new建立之後,就產生了一個例項物件(這裡的例項物件不是原型物件),例項物件會繼承原型物件的方法。

原型(prototype)、建構函式(constructor)、例項物件(即__proto__)的關係

建構函式和例項物件的關係:在每個例項物件中的__proto__裡面,同時會有一個constructor屬性,這個constructor屬性指向建立該例項的建構函式。

image.png

例項物件和建構函式的關係:每個例項物件中的__proto__指向建構函式中的prototype,二者是相等的。
其他一:原型適合封裝方法,建構函式適合封裝屬性,把這二者結合起來就組成了組合模式。
其他二:將所有的屬性和方法封裝在同一個建構函式中,叫做動態原型模式,只是在需要的時候才會在構造方法中初始化原型,這就整合了建構函式和原型的有點。

函式物件彙總

prototype:JS中所有函式都有的prototype屬性,是一個顯式原型。 __proto__:JS中任何物件都有的__proto__屬性,是一個隱式原型。constructor:JS中所有的prototype和例項物件都有的constructor屬性。也就是,當宣告一個function的方法時,會給該方法新增一個prototype屬性,指向預設的原型物件,而且該prototype的constructor屬性也指向方法物件,這兩個屬性會在建立d物件的時候被物件的屬性引用。

二、原型鏈

訪問物件的過程

在JS中訪問一個物件,首先檢視的是該物件自身是否具想要使用的屬性或方法,如果有則直接使用;若沒有,就在原型鏈上依次查詢是否擁有想要使用的屬性或方法,但是不會查詢自身的prototype;逐級查詢,有則使用,無則繼續向上查詢,直到找到為止,使用遞迴訪問到最終盡頭,找不到的盡頭,值就是null。

原型鏈的概念

原型鏈其實是原型的查詢機制,即一條定址鏈條,原型上的屬性和方法的查詢都是按照一定順序沿著原型鏈進行查詢的。JS中每個物件都對應有一個proto屬性,隱式原型的指向形成的一個線性的鏈條,即原型鏈。

在JS中每個函式都存在一個原型物件,且所有函式的預設原型都是Object例項,每個繼承父類函式的子函式的物件都包含一個內部屬性proto,該屬性包含一個指標指向父類函式的prototype,如果父類函式的原型物件的proto屬性為更上一層的祖父類函式,這個順序過程就是原型鏈。

image.png

上圖,由相互關聯的原型組成的鏈狀結構就是原型鏈,即紅色的這條線的過程。

原型指標是什麼?

原型指標是透過連線原型物件之間的地址橋樑,它是中間連線作用。原型物件其實包含兩部分內容:原型資料和原型指標,原型資料是用來儲存方法和屬性,原型指標是為了檢驗驗證圓形連結串列進行查詢操作。

如果讓原型物件等於另一個型別的例項物件,原型物件將包含一個指向另一個原型物件的指標,對應的另一個原型物件中也包含一個指向另一個建構函式的指標;如果另一個原型物件又是另外一個型別的例項物件,那麼上面描述的關係依然成立;依此層層遞進,就構成了例項物件與原型物件的鏈條,這就是原型鏈的概念。

原型鏈規則

在new關鍵字構建的例項物件之後,它的原型指標指向這個類的原型物件,原型物件指標會預設指向Object原型物件。

原型鏈特點

1、原型鏈的作用就是用來實現繼承的;
2、在包含引用型別值的原型屬性或方法會被所有的例項共享使用;
3、在建立子型別建構函式的時候,無法向超型別的建構函式中傳遞引數;
4、在讀取物件的屬性時,會自動到原型鏈中查詢對應的屬性;
5、給物件的屬性設定值的時候,不會查詢原型鏈,若當前物件中沒有該屬性,則直接新增該屬性並設定對應的值;
6、原型中定義方法,建構函式定義屬性到物件的自身。

原型鏈的作用

在訪問例項物件的屬性和方法的時候,如果該例項物件不存在該屬性或方法,那就會在該例項物件的原型上查詢,如果依然查不到,就會在原型的原型上查詢,以此類推,直到找到原型的終點。透過原型鏈可以讓例項物件能夠獲取到它原型上的屬性或方法。

原型鏈示例

示例一:新建陣列,而陣列方法就是從陣列的原型上繼承

//陣列的原型上含有陣列需要使用的各種方法,要想使用就是透過繼承。       var array = []; //建立新的陣列     
arr.map === Array.prototype.map;  //繼承陣列原型上的map方法,也就是從arr.__proto__上面繼承的,而arr.__proto__也就是Array.prototype。

示例二:輸出下面的各個結果

Fun.prototype.x = 1;
var fun1 = new Fun()
Fun.prototype = {    x:2,    y:3}
var fun2 = new Fun();
console.log(fun1.x,fun1.y)     //輸出結果:1   undefinedconsole.log(fun2.x,fun2.y)     //輸出結果:2  3

示例三:輸出下面的各個結果

var Fun = function Fun(){}
Object.prototype.x = function(){    
console.log('x()')}
Function.prototype.y = function(){   
 console.log('y()')}
var fun = new Fun()
fun.x()
fun.y()
Fun.x()
Fun.y()輸出結果為:x() undefined x() y()

原型鏈的終結

原型鏈的終結就是null,在Object.prototype的原型為null,即Object.prototype就是原型鏈的終結。所以 Object.prototype.__proto__ 的值為 null 和 Object.prototype 沒有原型,其實表達的是同一個意思,即在查詢屬性的時候查到 Object.prototype 就可以停止查詢了。

延伸:實現類的繼承

JS中實現類的繼承(ES6以前)分為兩步:繼承建構函式中的屬性和方法(建構函式繼承);繼承物件原型中的屬性和方法(原型鏈繼承)。

ES6以後的繼承,可利用class關鍵字結合extends關鍵字來實現繼承。ES6中引入了class關鍵字來宣告類, 而class(類)可透過extends來繼承父類中屬性和方法,語法為“class 子類名 extends 父類名{...};”。

最後

透過本文關於JS中原型和原型鏈的介紹,如果認真閱讀並且實踐示例,應該會很好的掌握這些知識點,雖然篇幅的內容不少,但是分開來看會覺得沒那麼複雜,同時也整合了其他的彙總,是一篇值得閱讀的文章,尤其是對於原型和原型鏈還不是太清楚的開發者來說甚為重要,不管是在開發過程中還是在面試求職中,這個知識點是必備的,重要性就不在贅述。歡迎關注,一起交流,共同進步。

本文參與了「SegmentFault 思否寫作挑戰賽」,歡迎正在閱讀的你也加入。

相關文章