首頁 > 軟體

Javascript 原型與原型鏈深入詳解

2022-07-29 14:01:39

前言

在前端這塊領域,原型與原型鏈是每一個前端 er 必須掌握的概念。多次在面試或者一些技術部落格里面看見這個概念。由此可見,這個玩意對於前端來說有多重要。其實它本身理解起來不難,但是很多剛入行前端的同學,看到prototype__proto__理解起來還是有點吃力,然後腦子裡面就亂成一鍋粥,就像我一樣。 但是這是很正常的事情,沒什麼大不了的,就像我們想要學會跑步,那麼我們就必須先學會走路。任何事情都是有個過程的。所以現在就跟我一起來攻克這個難點吧。

通過這篇文章你將掌握以下知識點:

  • 理解 __proto_;
  • 理解 prototype;
  • 理解javascript物件的概念;
  • 理解原型和原型鏈;
  • 理解javascript的概念;
  • 理解new的實現;
  • 理解instanceof的實現;
  • 理解javascript的繼承;
  • 加深對javascript這門語言的理解。

這也是本篇文章的寫作思路。

物件

那麼我們就從物件這一概念開始說起,其實物件這一概念相信大家並不陌生。有一種說法是“javasrcript 中萬物皆是物件”,其實這個說法是錯誤的,一個很簡單的例子,javasript中簡單基本型別(string、boolean、number、null、undefined、symbol)本身就不是物件。其實javasript中物件主要分為函數物件普通物件

其中:

  • String
  • Number
  • Boolean
  • Object
  • Function
  • Array
  • Date
  • RegExp
  • Error

這些都是函數物件,他們同時也被稱為內建物件函數物件本身其實就是一個純函數,javascript用他們來模擬

普通物件就很簡單了,就是我們常見的物件:

const obj = {
  name: "juefei",
  desc: "cool",
};

可能說到這,你還是無法理解到底啥是函數物件,啥是普通物件那我們就一起來看看下面的程式碼:

const obj1 = {};
const obj2 = new Object();
function func1() {}
const obj3 = new func1();
const func2 = new (function () {})();
const func3 = new Function();

接著我們來分別列印一下他們:

console.log(obj1); // object
console.log(obj2); // object
console.log(obj3); // object
console.log(func1); // function
console.log(func2); // function
console.log(func3); // function

所以可以看見,obj1obj2、,obj3是普通物件,他們都是Object的範例,而func1func2func3則都是Function的範例,稱為函數物件

我們再看看:

console.log(typeof Object); //f unction
console.log(typeof Function); // function

你是不是驚呆了,原來ObjectFunction都是 Function的範例。

所以我們得出一個結論就是:

  • 只要是Function的範例,那就是函數物件,其餘則為普通物件

同樣我們也可以看出,不僅 Object函數物件,就連 Function本身也是函數物件,因為我們通過 console.log(typeof Function); 得知 Function是 Function的範例。是不是又開始有點繞了?沒事,到這一步你就記住我們剛剛的結論就算完成目標:

  • 只要是Function的範例,那就是函數物件,其餘則為普通物件

那麼說到物件,我們從上面可以看出,一個物件是通過建構函式 new 出來的,這其實跟原型原型鏈有很大的關係,那麼原型原型鏈到底是用來幹嘛的呢?

原型

涉及到這兩個概念,我們就必須先來介紹兩個東西: __proto__prototype ,這兩個變數可以說,在 javascript這門語言裡面隨處可見,我們不管他三七二十一,我們先來看一張表:

物件型別__proto__prototype
普通物件
函數物件

所以,請你先記住以下結論:

  • 只有函數物件有 prototype屬性,普通物件沒有這個屬性。
  • 函數物件普通物件都有 __proto__這個屬性。
  • prototype 和 __proto__都是在建立一個函數或者物件會自動生成的屬性。

接著我們來驗證一下:

function func() {
  //func稱為建構函式
}
console.log(typeof func.prototype); // object
console.log(typeof func.__proto__); // function
const obj = {};
console.log(typeof obj.__proto__); //object
console.log(typeof obj.prototype); //undefined (看見了吧,普通物件真的沒有 prototype 屬性)

所以就驗證了我們剛剛的結論:

  • 只有函數物件prototype屬性,普通物件沒有這個屬性
  • 函數物件 和 普通物件都有__proto__這個屬性。
  • prototype__proto__都是在建立一個函數或者物件會自動生成的屬性。

你看我又重複寫了一遍,我不是為了湊字數,是為了你加深記憶,這對於我們接下來的篇幅很重要。

接著我們來看看下面的程式碼:

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

所以我們又得出如下結論:

  • 範例的__proto__屬性主動指向構造的 prototype;
  • prototype屬性被__proto__屬性 所指向。

這就是prototype屬性和__proto__屬性的區別與聯絡。 這可能又有點繞了,來多看幾遍這一節,多背一下我們的結論。我們繼續。
那麼問題來了,既然func是一個函數物件,函數物件是有 prototype 屬性的,那麼func.prototype.__proto__等於啥呢?
為了解決這個問題,我們來思考一下:
首先,我們看看func.prototype 是啥:

console.log(typeof func.prototype); //object

好,我們知道了,func.prototype是一個物件,那既然是物件,那 func.prototype那不就是 Object的範例嗎?那也就是說,func.prototype.__proto__屬性肯定是指向Object.prototype咯! 好,我們來驗證一下:

console.log(func.prototype.__proto__ === Object.prototype); //true

看見沒有,就是這樣的。那看到這裡,我們應該也知道當我們這建立一個建構函式的時候,javascript 是如何幫我們自動生成__proto__prototype屬性的。哈哈沒錯就是這樣:

//我們手動建立func函數
function func() {}
//javascript悄悄咪咪執行以下程式碼:
func._proto = Function.prototype; //範例的 __proto__ 屬性主動指向構造的 prototype
func.prototype = {
  constructor: func,
  __proto: Object.prototype, //我們剛剛才在上面驗證的,你別又忘記了
};

我還專門為你畫了個圖(夠貼心不老鐵):

所以prototype又被稱為顯式原型物件,而__proto__又被稱為隱式原型物件。

hi,看到這裡,你是不是有種腦子開了光的感覺。哈哈,所以到現在你應該已經理解原型的概念了,如果你還不理解,那就把上述章節再看一遍。最好拿個紙筆出來跟著畫一畫,順便拿出電腦把範例程式碼敲一敲。好,整理一下頭腦,接下來我們來看看什麼又是原型鏈

原型鏈

再介紹這個概念之前,我們先來看如下程式碼:

function Person = function(name,desc){
    this.name = name;
    this.desc = desc;
} //***1****//
Person.prototype.getName = function(){
    return this.name;
}//***2****//
Person.prototype.getDesc = function(){
    return this.desc;
}//***3****//
const obj = new Person('juefei','cool');//***4****//
console.log(obj);//***5****//
console.log(obj.getName);//***6****//

接下來我們來逐步解析一下:

  • 建立了一個建構函式 Person,此時,Person.portotype自動建立,其中包含了constructor 和__proto__兩個屬性;
  • 給物件 Person.prototype新增了一個方法 getName;
  • 給物件 Person.prototype新增了一個方法 getDesc;
  • 根據建構函式Person新建一個範例: obj(在建立範例的時候,建構函式會自動執行);
  • 列印範例 obj :
{
    name: 'juefei',
    desc: 'cool'
}

根據上面一節的結論,我們得出:

obj.__proto__ = Person.prototype;
  • 執行到第 6 步時,由於在範例 obj上面找不到 getName()這個方法,所以它就會自動去通過自身的 __proto__ 繼續向上查詢,結果找到了Person.prototype ,接著它發現,剛好 Person.prototype 上面有getName()方法,於是找到了這個方法,它就停止了尋找。 怎麼樣,是不是有一種環環相扣的感覺?他們形成一個鏈了,沒錯,這就是原型鏈

我們得出如下結論:
在存取一個物件(假設這個物件叫 obj)的屬性/方法時,若在當前的物件上面找不到,則會嘗試通過obj.__proto__去尋找,而 obj.__proto__ 又指向其建構函式(假設叫objCreated)的 prototype,所以它又自動去 objCreated.prototype 的屬性/方法上面去找,結果還是沒找到,那麼就存取 objCreated.prototype.__proto__繼續往上面尋找,直到找到,則停止對原型鏈對尋找,若最終還是沒能找到,則返回 undefined 。 一直沿著原型鏈尋找下去,直到找到 Object.prototype.__proto__,指向 null,於是返回 undefined了。

是不是自然而然就理解了。我又給你畫了個圖:

接下來我們再來增加一些概念:

  • 任何內建函數物件本身的 __proto__屬性都指向 Function的原型物件,即: Function.prototype;
  • 除了 Object.prototype.__proto__指向 null ,所有的內建函數物件的原型物件的 __proto__屬性 ( 內建函數物件.prototype.__proto__),都指向Object

我們得出如下終極原型鏈的圖: 

針對這個圖,我最終給出我們經常看見那個原型鏈的圖:

好好對比一下,拿出紙和筆畫一畫,根據上面章節的講解,相信你很容易就能明白。

javascript中的類

剛剛我們終於明白什麼是 原型和 原型鏈。下面我們根據上面的概念來講解一下javascript中的。 我們知道,在物件導向的語言中,類可以被範例化多次,這個範例化是指我們可以根據建構函式去獨立複製多個獨立的範例,這些範例之間是獨立的。但是實際上在 javascript卻不是這樣的,因為它不是這種複製機制。我們不能建立一個類的多個範例,我們只能建立這個類的多個物件,因為他們都是通過原型原型鏈關聯到同一個物件。所以在 javascript中 ,都是通過原型原型鏈來實現的,它其實是一種委託方式

new的實現

瞭解了上面javascript中的的概念,那我們應該很容易就理解new的過程,其核心無非就是執行原型鏈的連結:

function myNew(Cons, ...args) {
  let obj = {};
  obj.__proto__ = Cons.prototype; //執行原型連結
  let res = Cons.call(obj, args);
  return typeof res === "object" ? res : obj;
}

instanceof的實現

那麼學習了原型原型鏈instanceof的實現肯定也很簡單了,它也是通過原型原型鏈來實現的:

function myInstanceof(left, right) {
  let rightProto = right.prototype;
  let leftValue = left.__proto__;
  while (true) {
    if (leftValue === null) {
      return false;
    }
    if (leftValue === rightProto) {
      return true;
    }
    leftValue = leftValue.__proto__;
  }
}

我就不講解過程了,因為我知道你肯定能看懂,哈哈。

javascript的繼承

我們都知道繼承也是通過原型原型鏈來實現的,那我在這裡介紹兩種常見的繼承方式:

組合繼承:

//組合式繼承
//通過call繼承Parent的屬性,並傳入引數
//將Child的原型物件指向Parent的範例,從而繼承Parent的函數
function Parent(value) {
  this.val = value;
}
Parent.prototype.getValue = function () {
  console.log(this.val);
};
function Child(value) {
  Parent.call(this, value); //繼承Parentd的屬性
}
Child.prototype = new Parent();

寄生組合式繼承:

//寄生組合式繼承
//通過call繼承Parent的屬性,並傳入引數
//通過Object.create()繼承Parent的函數
function Parent(value) {
  this.val = value;
}
Parent.prototype.getValue = function () {
  console.log(this.val);
};
function Child(value) {
  //繼承Parentd的屬性
  Parent.call(this, value);
}
Child.prototype = Object.create(Parent.prototype, {
  constructor: {
    value: Child,
    writable: true,
    configurable: true,
    enumerable: false,
  },
});

總結

  • 若 A 通過 new 建立了 B,則 B.__proto__ = A.prototype
  • 執行B.a,若在 B 中找不到 a,則會在B.__proto__中,也就是A.prototype中查詢,若A.prototype中仍然沒有,則會繼續向上查詢,最終,一定會找到Object.prototype,倘若還找不到,因為Object.prototype.__proto__指向null,因此會返回undefined
  • 原型鏈的頂端,一定有 Object.prototype.__proto__ ——> null。

由此可見,原型原型鏈是如此的強大

到此這篇關於Javascript 原型與原型鏈深入詳解的文章就介紹到這了,更多相關Javascript 原型鏈內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com