__proto__和prototype

前言

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

  • __proto__prototype來深刻理解JS對象和原型鏈
  • javascript實現類與繼承
  • 初始ES6class

此篇爲第一篇__proto__prototype來深刻理解JS對象和原型鏈java

prototype__proto__

何爲原型

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

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,可是它必定是惟一的)。這個另外的對象就是所謂的原型對象。每一個對象從它的原型對象中繼承屬性和方法。瀏覽器

若是你是初學者,這句話確定很繞吧(不過它確實描述得很精闢)。不要緊,你只須要先把握如下兩點就行了:bash

  • 在JS裏,萬物皆對象。方法(Function)是一個對象,方法的原型(Function.prototype)是對象。函數

  • JS有三種構造對象的方式ui

    • 經過對象字面量this

      var person1 = {
          name: 'Jzin',
          sex: 'male'
      }
      複製代碼
    • 經過構造函數es5

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

      所謂構造函數就是:能夠經過它來new出一個對象實例的函數。一般構造函數裏都用了this,由於這樣子纔會給調用它的對象綁定屬性。spa

    • 由函數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

先上圖,若是圖片顯示不了能夠點擊這裏:傳送門

咱們來理解一下這幅圖:

  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上面啊啊啊啊。

總結

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

相關文章
相關標籤/搜索