詳解JavaScript中的原型

前言

原型原型鏈應該是被大多數前端er說爛的詞,但是應該還有很多人不能完整的解釋這兩個內容,當然也包括我自己。

最早一篇原型鏈文章寫於2019年07月,那個時候也是費了老大勁才理解到了七八成,到現在基本上忘的差不多了。時隔兩年,興趣所向重新開始復盤一下原型原型鏈的內容。

JavaScript中的對象

JavaScript中,對象被稱為是一系列屬性的集合。

創建對象的方式也有很多種,最常見的一種就是雙花括號的形式:

var obj = {};
obj.name = '小馬鈴薯';
obj.age  = 18;

這種方式實際上是下面這種方式的語法糖

var obj = new Object();
obj.name = '小馬鈴薯';
obj.age  = 18;

除此之外,在JavaScript中也可以通過構造函數自定義對象。

function Cat(){}
var catMimi = new Cat();  // 自定義對象

如果一個函數使用new關鍵字調用,那麼這個函數就可以稱為是構造函數,否則就是普通函數

什麼是原型

一句話簡單總結原型:原型是一個對象

在後面的總結中,原型可能會被描述為原型對象,其等價於原型

原型從哪裡來?原型這個對象存在於哪裡,需要通過代碼去創建嗎?

我們說對象是一系列屬性的集合,那原型這個對象包含什麼屬性呢?

如何操作和使用原型?

接下來我們一個一個問題去探究。

原型從哪裡來

JavaScript會為所有的函數創建一個原型

function Cat(){}

上面的代碼中我們創建了一個Cat函數,那這個Cat函數就有一個原型,用代碼表示就是:Cat.prototype

同樣我們創建一個函數Fn1,函數Fn1就有一個原型,用代碼表示就是Fn1.prototype

函數名稱大寫小寫本質上沒有任何區別

原型包含哪些屬性

前面我們說過以下這兩點:

  • 原型是一個對象
  • 對象是一系列屬性的集合

原型都包含哪些屬性呢?

前面我們已經知道原型用代碼表示就是:functionName.prototype,那我們在代碼中console.log一下。

function Cat(){}
console.log("Cat.prototype:");
console.log(Cat.prototype);

function Dog(){}
console.log("Dog.prototype:");
console.log(Dog.prototype);

Firefox瀏覽器中的輸出結果如下:

可以看到函數的原型默認有兩個屬性:constructor<prototype>

其中,函數原型的constructor屬性指向函數本身。

函數原型的<propotype>屬性稱為隱式原型,後面我們會分出一節單獨介紹隱式原型

如何操作和使用原型

正常我們操作一個普通對象的方式是下面這樣的:

var obj = {};          // 創建對象
obj.name = '小馬鈴薯';    // 為對象添加屬性
obj.age = 18;          // 為對象添加屬性
var name = obj.name;   // 訪問對象屬性

原型既然也是一個對象,所以操作原型的方式和上述的方式相同。

function Cat(){}
Cat.prototype.type = 'cat';
Cat.prototype.color = 'White';
Cat.prototype.sayInfo = function(){
    console.log(this.type + ' is ' + this.color);
}

此時再次打印Cat.prototype就能看到我們添加到原型上的屬性:

訪問原型對象上的方法和屬性:

以上這些操作原型的方法,對於真正的項目開發並沒有什麼參考價值,不過不用着急,後面我們會詳細講解

隱式原型

前面我們在總結函數的原型對象時提到過隱式原型

那實際上,JavaScript會為所有的對象創建叫隱式原型的屬性。我們一直說原型是一個對象,所以在上面的截圖中,原型也有一個隱式原型屬性。

隱式原型的代碼表示

隱式原型是對象的私有屬性,在代碼中可以這樣訪問:obj.__proto__

obj.__proto__這種寫法是非標準的,一些低版本的瀏覽器並不支持這樣的寫法

我們在瀏覽器的控制台中實際訪問一下:

從打印的結果可以看到隱式原型也是一個對象,那隱式原型這個對象裏面又包含什麼屬性呢?下面我們一起來看看。

隱式原型存在的意義

首先我們寫一個簡單的示例:

function Cat(){}
var catMimi = new Cat();
var catJuju = new Cat();

在上面這段代碼中,我們創建了一個Cat函數,並且通過new關鍵字創建了以Cat構造函數的兩個實例對象catMimicatJuju

接下來我們在瀏覽器的console工具中看看這兩個實例對象的隱式原型都包含了那些屬性。

可以看到,catMimi.__proto__catJuju._proto__的結果貌似是一樣的,而且眼尖的同學應該也發現了這個打印結果似乎和前面一節【原型包含那些屬性】中打印的Cat.prototype是一樣的。

那話不多說,我們用==運算符判斷一下即可:

可以看到所有的判斷結果均為true

由於對象catMimicatJuJu都是由Cat函數創建出來的實例,所以總結出來結論就是:對象的隱式原型__proto__指向創建該對象的函數的原型對象

原型鏈:原型和隱式原型存在的意義

前面我們總結了原型隱式原型的概念以及如何使用代碼操作原型隱式原型,總的看來原型隱式原型好像也沒有特別厲害的地方,它們到底有什麼用呢?

所有的實例對象共享原型上定義的屬性和方法

我們來看下面這樣一個示例:

function Cat(name, age){
    this.type = 'RagdollCat';  //布偶貓
    this.eyes = 2;
    this.name = name;
    this.age = age;
    this.sayInfo = function(){
        console.log(this.type + ' ' + this.name + ' is ' + this.age + ' years old');
    }
}

在這個示例中,我們創建了一個Cat函數,同時Cat函數有五個屬性:typeeyesnameagesayInfo,其中typeeyes屬性已經有了初始值,而nameage通過參數傳遞並賦值;sayInfo對應是一個函數,打印出typenameage的值。

接着我們創建Cat的兩個實例對象catMimicatJuju,並傳入不同的nameage參數。

var catMimi = new Cat('Mimi', 1);
var catJuju = new Cat('Juju', 2);

控制台查看一下我們創建的對象:

可以看到這兩個對象有着相同的屬性,由於typeeyes是在Cat函數創建時已經有了固定的初始值,所以這兩個屬性值是相同的;sayInfo函數也都是相同的功能,打印出一些屬性的信息;只有nameage是通過參數傳遞的,各自的值不相同。除此之外呢,catMimicatJuju是兩個不同的對象,兩者的屬性值互相獨立,修改其中任意一個的屬性值並不會影響另外一個對象的屬性值。

假如之後我們有更多這樣的對象,JavaScript還是會為每一個對象創建相同的屬性,而這些所有的對象都擁有着相同的typeeyes屬性值和相同功能的sayInfo函數。這無疑造成了內存浪費,那這個時候我們就可以將這些屬性定義到函數的原型對象上:

function Cat(name, age){
    this.name = name;
    this.age = age;
}

Cat.prototype.type = 'RagdollCat';    //布偶貓
Cat.prototype.eyes = 2;
Cat.prototype.sayInfo = function(){
    console.log(this.type + ' ' + this.name + ' is ' + this.age + ' years old');
}
var catMimi = new Cat('Mimi', 1);
var catJuju = new Cat('Juju', 2);

然後我們再來看看這兩個對象:

可以看到這兩個對象現在只包含了兩個屬性,就是Cat構造函數內容內部定義的兩個屬性:nameage

接着我們在去訪問對象上的typeeyessayInfo

我們的實例對象還是可以正常訪問到屬性,方法也打印出來正確的信息。那到底是怎麼訪問到的呢?

原型鏈

在上一個示例代碼中,我們將一些屬性方法定義到函數的原型上,最後使用該函數創建出來的實例對象可以正常訪問原型上定義的屬性方法,這是怎麼做到的呢?

前面我們說過:對象的隱式原型指向創建該對象的函數的原型對象,所以當實例對象中沒有某個屬性時,JavaScript就會沿着該實例對象隱式原型去查找,這便是我們所說的原型鏈

那既然是鏈,我們想到的應該是一個連着一個的東西,所以應該不僅僅是當前實例對象的隱式原型指向創建該對象的函數的原型對象,所以我們在對catMimi對象做點操作:

在上面的操作,我們調用了catMimihasOwnProperty方法,很明顯我們並沒有為這個對象定義該方法,那這個方法從哪裡來呢?

答案依然是原型鏈

  • 調用catMimi.hasOwnProperty()方法
  • 在實例對象catMimi中查找屬性,發現沒有該屬性
  • catMimi.__proto__中查找,因為catMimi.__proto__=Cat.prototype(實例對象的隱式原型指向創建該實例的函數的原型),也就是在Cat.prototype中查找hasOwnProperty屬性,很明顯Cat.prototype也沒有該屬性
  • 於是繼續沿着Cat.prototype.__proto__查找,又因為Cat.prototype.__proto__ = Object.prototype(我們一直在強調原型是一個對象,既然是對象,就是由Object函數創建的,所以Cat.prototype隱式原型指向Object函數的原型)

我們打印一下Object.prototype的是否包含hasOwnProperty屬性:

可以看到,Object.prototype中存在hasOwnProperty屬性,所以catMimi.hasOwnPrototype實際上調用的是Object.prototype.hasOwnProperty

總結

本篇文章到此基本就基本結束了,相信大家應該對原型原型鏈有了一定的了解。最後呢,我們在對本篇文章做一個總結。

近期文章

詳解Vue中的computed和watch

記一次真實的Webpack優化經歷

JavaScript的執行上下文,真沒你想的那麼難

骨架屏(page-skeleton-webpack-plugin)初探

Vue結合Django-Rest-Frameword實現登錄認證(二)

Vue結合Django-Rest-Frameword實現登錄認證(一)

寫在最後

如果這篇文章有幫助到你,❤️關注+點贊❤️鼓勵一下作者

文章公眾號首發,關注 不知名寶藏程序媛 第一時間獲取最新的文章

筆芯❤️~