__proto__和prototype

J-Zin發表於2018-05-13

前言

該系列文章將帶你全面理解js物件和原型鏈,並用es5去實現類以及認識es6中class的美妙。該系列一共有3篇文章:

  • __proto__prototype來深入理解JS物件和原型鏈
  • javascript實現類與繼承
  • 初始ES6class

此篇為第一篇__proto__prototype來深入理解JS物件和原型鏈

prototype__proto__

何為原型

引用《JavaScript權威指南》的一段描述

Every JavaScript object has a second JavaScript object (or null ,
but this is rare) associated with it. This second object is known as a prototype, and the first object inherits properties from the prototype.
複製程式碼

翻譯過來就是:每一個JS物件一定關聯著另外一個JS物件(也許是null,但是它一定是唯一的)。這個另外的物件就是所謂的原型物件。每個物件從它的原型物件中繼承屬性和方法。

如果你是初學者,這句話肯定很繞吧(不過它確實描述得很精闢)。沒關係,你只需要先把握以下兩點就好了:

  • 在JS裡,萬物皆物件。方法(Function)是一個物件,方法的原型(Function.prototype)是物件。

  • JS有三種構造物件的方式

    • 通過物件字面量

      var person1 = {
          name: 'Jzin',
          sex: 'male'
      }
      複製程式碼
    • 通過建構函式

      function Person(name, sex) {
          this.name = name;
          this.sex = sex;
      }
      var person1 = new Person('Jzin', 'male');
      複製程式碼

      所謂建構函式就是:可以通過它來new出一個物件例項的函式。通常建構函式裡都用了this,因為這樣子才會給呼叫它的物件繫結屬性。

    • 由函式Object.create構造

      var person1 = {
          name: 'Jzin',
          sex: 'male'
      }
      var person2 = Object.create(person1);
      複製程式碼

    這三種方法的異同到後面還會繼續分析,現在你只需掌握如何構建物件就好啦。

以上兩點就是這小結你要掌握的東西:

  • 萬物皆物件的思想
  • 如何構造物件

至此,我還沒介紹什麼是原型,不過沒關係,我們先看看原型的分類,慢慢你就會理解了。

原型的分類

JS的原型分成兩類:隱式原型和顯示原型

顯式原型(explicit prototype property)

當你建立一個函式時,JS會為這個函式(別忘了:JS一切皆物件)自動新增prototype屬性,這個屬性的值是一個物件,也就是原型物件(即函式名.prototype)。這個物件並不是空物件,它擁有constructor屬性,屬性值就是原函式。當然,你也可以自己在原型物件中新增你需要的屬性(即函式名.prototype.屬性名=屬性值).

那麼原型物件(prototype)的作用是什麼呢?可以用原型物件來實現繼承,即通過函式構造出來的例項可以直接訪問其建構函式的原型物件中的屬性。可能有點繞,但是讀到後面你就會理解啦。

需要注意的是:

  • 顯式原型(prototype)只有函式才擁有。我們後面講的隱式原型則是所有物件都有。
  • 通過Function.prototype.bind方法構造出來的函式是個例外,它沒有prototype屬性。
隱式原型( implicit prototype link)

JavaScript中任意物件都有一個內建屬性[[prototype]],在ES5之前沒有標準的方法訪問這個內建屬性,但是大多數瀏覽器都支援通過__proto__來訪問。現在,所謂的隱式原型就是__proto__ 了。

  • 隱式原型的指向

    隱式原型指向建立這個物件的函式的prototypeObject.create函式構造出來的例項有點例為,後面會說明。其實也不是例為,只是它經過了一定的封裝)。看下面的例子

    function person(name) {
        this.name = name;
    }
    person.prototype.class = 'Human';
    var person1 = new person('Jzin');
    
    console.log(person1.__proto__); //person { class: 'Human' }
    console.log(person.__proto__);  //[Function]
    複製程式碼

    person1__proto__很容易理解:它是由person方法構造的例項,它的__proto自然就是person.prototype

    person__proto__呢?其實每一個方法的構造方法都是Function方法,也就是所有方法的__proto__都是Function.prototype。如果現在還不理解也沒關係,後面會有一副圖幫你理解。

  • 隱式原型的作用

    • 構成原型鏈,同樣用於實現基於原型的繼承。舉個例子,當我們訪問obj這個物件中的x屬性時,如果在obj中找不到,那麼就會沿著__proto__依次查詢。這也是protorype可以實現繼承的原因。
    • 可以用來判斷一個物件(L)是否是某個函式(R)的例項:只需判斷L.__proto__.__proto__ ..... === R.prototype這個是否為真就行了。這也是instanceof運算子的原理。後面會講到。

一張圖帶你形象理解__proto__prototype

先上圖,如果圖片顯示不了可以點選這裡:傳送門

__proto__和prototype

我們來理解一下這幅圖:

  1. 建構函式Foo()
    • 建構函式Foo的原型屬性prototype指向了它的原型物件Foo.prototype。原型物件Foo.protoype中有預設屬性constructor指向了原函式Foo
    • 建構函式Foo建立的例項f2,f1__proto__指向了其建構函式的原型物件Foo.prototype,所以Foo的所有例項都可以共享其原型物件的屬性。
    • 建構函式Foo其實是Function函式建立的例項物件,所以它的__proto__就是Function函式的原型物件Function.prototype
    • 建構函式Foo的原型物件其實是Object函式建立的例項物件,所以它的__proto__就是Object函式的原型物件Object.prototype
  2. Funtion函式
    • 你所寫的所有函式,其實都是Function函式構造的例項物件。所以所有函式的__proto__都指向Fucntion.prototype
    • Function函式物件是由它本身建立(姑且可以這麼理解),所以Function.__proto__d等Function_prototype
    • Function函式的原型物件其實是Object函式建立的例項物件,所以它的__proto__就是Object函式的原型物件Object.prototype
  3. Object函式
    • Object函式其實是Function函式建立的例項物件,所以它的__proto__就是Function函式的原型物件Function.prototype
    • 需要注意的是:Object.prototype__proto__是指向null的!!!

相信你通過這幅圖,已經對原型有自己的理解了,我們來總結一下:

  • 物件有__proto__屬性,指向該物件的建構函式的原型物件。
  • 方法除了有__proto__屬性,還有prototype屬性,prototype指向該方法的原型物件。

深入理解__proto__的指向

相信經過上面的介紹,你已經能很好地掌握__proto__的指向了。本節通過一些實際的例子讓你更加深入地理解__proto__的指向。

在一開始,我們瞭解了構造物件的三種方式:(1)物件字面量的方式 (2)new的方式 (3)ES5中的Object.create()。其實,這三種方式在我看來都是一種的,即通過new來構建。為什麼這麼說呢?我們來仔細分析分析:

  1. 通過字面量構造物件

    var person1 = {
        name: 'Jzin',
        sex: 'male'
    }
    複製程式碼

    其實這種方式只是為了開發人員更方便建立物件的一個語法糖(語法糖:顧名思義,就是很甜的糖,經過程式碼封裝,讓語法更加人性化,實際的內部實現是一樣的)。

    上面也就等價於:

    var person1 = new Object();
    person1.name = 'Jzin';
    person1.sex = 'male';
    複製程式碼

    person1Object函式構造的物件,所以person1.__ptoto__就指向Object.prototype

    也就是說,通過物件字面量構造出來的物件,其__proto__都是指向Object.prototype

  2. 通過建構函式

    function Person(name, sex) {
        this.name = name;
        this.sex = sex;
    }
    var person1 = new Person('Jzin', 'male');
    複製程式碼

    通過new操作符呼叫的函式就是建構函式。由建構函式構造的物件,其__proto__指向其建構函式的原型物件。

    在本例中,person1.__proto__就指向Person.prototype

  3. 由函式Object.create構造

    var person1 = {
        name: 'Jzin',
        sex: 'male'
    }
    var person2 = Object.create(person1);
    複製程式碼

    由函式Object.create(obj)構造出來的物件,其隱式原型有點特殊:指向obj.prototype。在本例中,person2.__proto__指向person1

    這是為什麼呢?我們來分析一下。在沒有Object.create函式的日子裡,為了實現這一功能,我們需要這樣子做:

    Object.create = function(p) {
        function F(){}
        F.prototype = p;
        return new F();
    }
    var f = Object.create(p);
    複製程式碼

    這樣子也就是實現了其功能,分析如下:

    // 以下是用於驗證的虛擬碼
    var f = new F();	//var f = Object.create(p);
    // 於是有
    f.__proto__ === F.prototype	//true
    // 又因為
    F.prototype === p;	//true
    // 所以
    f.__proto__ === o	//true
    複製程式碼

    因此由Object.create(p)建立出來的物件它的隱式原型指向p。

通過上面的分析,相信你對原型又進一步理解啦。我們再來幾題玩玩。

  1. 建構函式的顯式原型的隱式原型

    • 內建物件(built-in object)的的隱式原型

      比如Array()Array.prototype.__proto__指向什麼?

      Array.prototype.__proto__ === Object.prototype //true
      複製程式碼

      比如Function()Function.prototype.__proto__指向什麼?

      Function.prototype.__proto__ === Object.prototype //true
      複製程式碼

      根據上面那幅圖,這些也很簡單啦。

  2. 自定義物件

    • 預設情況下

      function Foo(){}
      var foo = new Foo()
      Foo.prototype.__proto__ === Object.prototype //true 
      foo.prototype.__proto__ === Foo.prototype //true 
      複製程式碼

      理由,就不必解釋了吧

    • 其他情況

      1. function Bar(){}
        function Foo(){}
        //這時我們想讓Foo繼承Bar
        Foo.prototype = new Bar()
        
        Foo.prototype.__proto__ === Bar.prototype //true
        console.log(Foo.prototype.constructor);	//[Function: Bar]
        複製程式碼
      2. function Foo(){}
        //我們不想讓Foo繼承誰,但是我們要自己重新定義Foo.prototype
        Foo.prototype = {
          a:10,
          b:-10
        }
        //這種方式就是用了物件字面量的方式來建立一個物件,根據前文所述 
        Foo.prototype.__proto__ === Object.prototype
        console.log(Foo.prototype.constructor);	//[Function: Object]
        複製程式碼

      注意:以上兩種情況都等於完全重寫了Foo.prototype,所以Foo.prototype.constructor也跟著改變了,於是constructor這個屬性和原來的建構函式Foo也就切斷了聯絡。

instanceof

instanceof的左值一般是一個物件,右值一般是一個建構函式,用來判斷左值是否是右值的例項。instanceof操作符的內部實現機制和隱式原型、顯式原型有直接的關係,它的內部實現原理是這樣的:

//設 L instanceof R 
//通過判斷
 L.__proto__.__proto__ ..... === R.prototype ?
//最終返回true or false
複製程式碼

也就是沿著L的__proto__一直尋找到原型鏈末端,直到等於R.prototype為止。知道了這個也就知道為什麼以下這些奇怪的表示式為什麼會得到相應的值了

Function instanceof Function //true
Function instanceof Object // true 
Object instanceof Function // true 
Object instanceof Object // true
Number instanceof Number //false
Number instanceof Function //true
Number instanceof Object //true
複製程式碼

你發現沒有:這就是原型鏈啊!!!

L1.__proto__指向R1.prototype

R1.prototype.__proto__指向R2.prototype

...

Rn.prototype.__proto__指向Object.prototype

Object.prototype.__proto__指向null

這樣子就把原型串起來啦,也就是實現了繼承。也就是為什麼所有物件都要toString方法,因為這個方法在Object.prototype上面啊啊啊啊。

總結

至此,相信你已經完全理解原型和原型鏈了。當然,只是理解不實踐是沒用的。在下一篇,我們將利用原型來實現類與繼承。

相關文章