JavaScript原型與繼承(三)

前言

寫這篇筆記的初衷秘狞,是想進(jìn)一步了解ES6的特性extends是如何實現(xiàn)了繼承角撞,查看源碼后發(fā)現(xiàn)核心的一段不能理解

// 賦值原型 
subClass.prototype = Object.create(superClass && superClass.prototype)

// 這一步是作甚锦秒?根據(jù)組合繼承的邏輯听盖,完全沒有必要這一步杨拐?
if (superClass) 
    Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;

由于不太清楚__proto__與原型prototype的含義棕所,以及Object糊肠,Function的關(guān)系辨宠,便做進(jìn)一步深究。

題外話:我并不覺得深究一件事有何不值得货裹,就像是“走火入魔”般嗤形,何況這件事是很多人都曾做過的。我相信我會喜歡上深究一個問題這樣一個過程而不是結(jié)果泪酱。

概述

這里我想要理清楚的問題是:
1派殷、ObjectFunction墓阀,prototype毡惜,__proto__究竟是怎樣的關(guān)系,怎么得來斯撮?
2经伙、ES5合理繼承的方式(即前篇文章所提到的組合繼承)與ES6extends有何異同?ES6源碼剖析


一、Object帕膜,F(xiàn)unction枣氧,prototype,__proto__之間的關(guān)系

1.理解對象

我們常說垮刹,“js是面向?qū)ο蟮拇锿蹋驗樗部梢該碛凶约旱膶傩院头椒?..”,其實說了跟沒說沒啥區(qū)別荒典,反正我還是沒理解酪劫。

我覺得最能解釋它的是:Object.prototype是一切對象和函數(shù)的根源,一張圖來證明我的觀點:

Object.prototype

為什么這么說寺董?我們會發(fā)現(xiàn)覆糟,每當(dāng)我們在控制臺打印一個對象的時候,都能順著原型鏈找到圖中所有的內(nèi)容遮咖。并且:

// 說明Object.prototype不是任何一個構(gòu)造函數(shù)的實例
Object.prototype.__proto__ === null
2.誰構(gòu)造了誰

js原生內(nèi)置了部分構(gòu)造函數(shù)滩字,其中就包含了Object和Function

Object、Function御吞、Boolean麦箍、Number、String陶珠、Array内列、Date、RegExp背率、Error

我們知道话瞧,當(dāng)我們想要一個子類擁有父類的屬性和方法時,會先創(chuàng)建一個父類Parent寝姿,其實這個構(gòu)造函數(shù)Parent形式等價于內(nèi)置的構(gòu)造函數(shù)

Parent和這些內(nèi)置構(gòu)造函數(shù)繼承的誰呢交排?答案是Function本身,換句話說所有的構(gòu)造函數(shù)都是Function的實例饵筑,如圖所示:

![HWC41$DF0P)]X66P7Y{M%2X.png](http://upload-images.jianshu.io/upload_images/3637499-448654f044fe6fc6.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

依照我們前面的說法埃篓,實例的__proto__始終指向構(gòu)造函數(shù)的prototype沒錯。

Function又是誰構(gòu)造而來根资?我發(fā)現(xiàn)我真是蠢到了極點架专,圖中不是答案么,Function是自己的實例玄帕,也就是Function由自己構(gòu)造的部脚,聽起來像是科幻小說。

既然這樣裤纹,那構(gòu)造Function的時候委刘,它的原型從哪繼承而來?這里我不得不引用別人的原文:

Function.prototype是Object的實例對象

雖然我們從控制臺印證了這個觀點
![G]HSRKI{}M8I~8E5JK0JF.png](http://upload-images.jianshu.io/upload_images/3637499-fc8dfd11d19fd018.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

但是這里我還是得先弄清楚prototype__proto__。最終锡移,在別人的文章中找到了似乎可以印證的觀點

prototype是函數(shù)的一個屬性(每個函數(shù)都有一個prototype屬性)呕童,指向一個對象

我們先接受這個觀點,那么這個對象指向的誰呢淆珊?創(chuàng)建函數(shù)的方式有3種

  • 通過Function構(gòu)造函數(shù)
  • 字面量創(chuàng)建
  • 直接聲明

![{A3}PS6C3HTGRJ_0YJ5]4W.png](http://upload-images.jianshu.io/upload_images/3637499-8d44d6ab3065fb68.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 從圖中我們可以看到夺饲,所有function實例的prototype都是指向Object.prototype`

// 這里的a其實形式上等價于Function
a.prototype.__proto__ === Function.prototype.__proto__ === Object.prototype

__proto__是一個對象擁有的內(nèi)置屬性,指向于它所對應(yīng)的原型對象施符,原型鏈正是基于__proto__才得以形成(note:不是基于函數(shù)對象的屬性prototype)

3.小總結(jié)

我們知道了__proto__prototype的區(qū)別钞支,那我們自然而然的得出:

// 所有構(gòu)造函數(shù)都是Function的實例
Object.__proto__ === Function.prototype
Function.__proto__  === Function.prototype

// 所有Function都有一個prototype指向Object.prototype
// 也就是說,F(xiàn)unction.prototype都是Object.prototype的實例
// 這一點我不是很確定操刀,但從控制臺看到確實如此
Function.prototype.__proto__ === Object.prototype

// Object.prototype到達(dá)了根源,不指向任何誰婴洼,原型鏈到此就結(jié)束了
Object.prototype.__proto__ === null

因此骨坑,關(guān)于誰構(gòu)造了誰這個問題,答案是:

所以柬采,是先有的Object.prototype,再有的Function.prototype欢唾,再有的Function和Object函數(shù)對象

最后再帶上一張圖來加深理解,此圖來源Javascript中Function,Object,Prototypes,proto等概念詳解

71M99JF8BL0ZKYZ0LXIB(O.png

此小節(jié)參考
js 原型的問題 Object 和 Function 到底是什么關(guān)系粉捻?
Js中Prototype礁遣、proto、Constructor肩刃、Object祟霍、Function關(guān)系介紹

4.疑問

當(dāng)我們通過3種方式創(chuàng)建函數(shù)的時候,它們的構(gòu)造函數(shù)是一樣的嗎盈包?

![LR3J6$(]NL7@T~M7@5A8Q@C.png](http://upload-images.jianshu.io/upload_images/3637499-4b45ba659b03c618.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

由上圖可以發(fā)現(xiàn)沸呐,通過3種方式創(chuàng)建函數(shù),它們實質(zhì)是一樣的呢燥,都是調(diào)用Function構(gòu)造函數(shù)來創(chuàng)建崭添,只不過創(chuàng)建的時候匿名不匿名的問題

a.constructor === b.constructor === c.constructor === Function

包括通過字面量創(chuàng)建的對象和通過構(gòu)造函數(shù)創(chuàng)建對象他們的區(qū)別,只不過是多了一層中間構(gòu)造函數(shù)而已

// 1.通過構(gòu)造函數(shù)創(chuàng)建對象
function Extra(name) {
    this.name = name
}

var instance = new Extra('hehe')

// 2.通過字面量創(chuàng)建
var instance2 = {
    name: 'hehe2'
}

// instance與instance2的區(qū)別
instance.__proto__ === Extra.prototype
Extra.prototype.__proto__ === Object.prototype

// 而
instance2.__proto__ === Object.prototype

// 可以發(fā)現(xiàn)instance就多了一層構(gòu)造函數(shù)Extra的原型叛氨,我們還可以知道呼渣,原型鏈就是基于__proto__才得以依次往上查找

區(qū)別我想肯定不止這些,因為我還不知道創(chuàng)建函數(shù)是怎樣的一個過程寞埠,包括new的時候屁置,具體發(fā)生了哪些細(xì)節(jié),大致我只知道仁连,new一個構(gòu)造函數(shù)的時候缰犁,在其內(nèi)部完成了原型鏈的連接(即繼承至Object.prototype,可能是通過this = {}實現(xiàn)的),并且賦值了this的指向帅容。而通過字面量創(chuàng)建的時候是沒有這些過程的颇象。

二、ES5合理繼承的方式(即組合繼承)與ES6 extends的異同

阮一峰老師的教程里有說到

ES5 的繼承并徘,實質(zhì)是先創(chuàng)造子類的實例對象this遣钳,然后再將父類的方法添加到this上面(Parent.apply(this))。
ES6 的繼承機(jī)制完全不同麦乞,實質(zhì)是先創(chuàng)造父類的實例對象this(所以必須先調(diào)用super方法)蕴茴,然后再用子類的構(gòu)造函數(shù)修改this。

也就是說ES6中子類是沒有this的姐直,必須通過super得到父類的實例對象this倦淀,這是結(jié)果,那么實現(xiàn)呢声畏?

// 簡單的extends繼承
 class Parent{
   // static屬性和方法 
   static sex = 'man'
   static getSex() {
     console.log(Parent.sex)
   }
   
   // 構(gòu)造函數(shù) 
   constructor(name) {
     this.name = name
   }
   
   // 非靜態(tài)方法
   say() {
     console.log(this.name)
   }
 }
 
 class Child extends Parent {
   static sex = 'women'
   
   constructor(name, age) {
      super(name)
      
      this.age = age
   }
   
   say() {
     console.log(this.name, this.age)
   }
 }

編譯后查看源碼

RE6OQ6VQ@CDU}SK4@8UWU9.png

會發(fā)現(xiàn)es6 class 繼承實現(xiàn)通過這4個方法:_createClass撞叽,_possibleConstructorReturn_inherits, _classCallCheck插龄,那么extends是如何繼承愿棋,包括super是如何獲取this的,我們來一步步解析均牢。

1.編譯后的Parent

var Parent = function () {
  
  // 函數(shù)內(nèi)部聲明一個同名的構(gòu)造函數(shù)
  function Parent(name) {

    // 檢查當(dāng)前的this是否是構(gòu)造函數(shù)的實例糠雨,也就是說必須通過new的方式調(diào)用構(gòu)造函數(shù)
    // 而不能是像調(diào)用函數(shù)一樣直接調(diào)用,因為這樣是不會生成實例的this
    _classCallCheck(this, Parent);

    // 將構(gòu)造函數(shù)的屬性賦值給當(dāng)前實例的this
    this.name = name;
  }

  // 將Parent中的靜態(tài)方法直接賦值Parent構(gòu)造函數(shù)
  _createClass(Parent, null, [{
    key: 'getSex',
    value: function getSex() {
      console.log(Parent.sex);
    }
  }]);
  
  // 將Parent中的非靜態(tài)方法賦值Parent的原型徘跪,也就是Parent.prototype
  _createClass(Parent, [{
    key: 'say',
    value: function say() {
      console.log(this.name);
    }
  }]);

  return Parent;
}();

Parent做了2件事情甘邀,也可以說所有的通過Class關(guān)鍵字聲明的函數(shù)做了2件事

  • 在函數(shù)里面聲明創(chuàng)建了一個同名的構(gòu)造函數(shù),將constructor以外的static聲明的方法和非static聲明的方法分別掛載到構(gòu)造函數(shù)本身和構(gòu)造函數(shù)的原型上
  • 該函數(shù)會返回這個同名的構(gòu)造函數(shù)垮庐,這里是采用寄生模式創(chuàng)建的構(gòu)造函數(shù)鹃答。這里還做了校驗,必須通過new來調(diào)用突硝,否則拋出異常

2.constructor以外的方法是如何掛載的

// 用該方法實現(xiàn)
var _createClass = function () { 

    // 重寫了es5的defineProperties方法测摔,至于為什么會重寫,后面解釋
    // 該方法就是遍歷props解恰,然后利用defineProperty锋八,依次給target定義屬性
    function defineProperties(target, props) { 
        for (var i = 0; i < props.length; i++) { 
            var descriptor = props[i]; 

            // 是否可枚舉,默認(rèn)為false 护盈?也就是說把所有的方法都置為不可枚舉
            descriptor.enumerable = descriptor.enumerable || false; 
            descriptor.configurable = true; 

            if ("value" in descriptor) 
                descriptor.writable = true; 

            Object.defineProperty(target, descriptor.key, descriptor); 
        } 
    } 

    // 返回一個函數(shù)挟纱,如果是static方法就定義在Constructor上,否則就定義在Constructor.prototype上
    // 我們可以對應(yīng)到Parent里調(diào)用_createClass 時腐宋,對于靜態(tài)和非靜態(tài)方法的傳參
    return function (Constructor, protoProps, staticProps) { 
        if (protoProps) 
            defineProperties(Constructor.prototype, protoProps); 

        if (staticProps) 
            defineProperties(Constructor, staticProps); 

        return Constructor; 
    }; 
}();

我們可以看到紊服,ES6把所有定義在構(gòu)造函數(shù)或原型中的方法都定義為不可枚舉檀轨,而屬性是通過默認(rèn)賦值可枚舉的。為什么欺嗤?誰能解釋下参萄。。煎饼。讹挎。

3.插播一條小廣告,弄清楚數(shù)據(jù)屬性

數(shù)據(jù)屬性有4個(這里只是簡要的帶過)

  • value 屬性的值
  • enumerable 是否可枚舉
  • configurable 是否可修改
  • writable 是否可寫

我們在定義對象或者賦值對象屬性的時候吆玖,通常是不知道這些數(shù)據(jù)屬性的筒溃,因為默認(rèn)情況下,都是true

YUZ(5J3(Y_1UG@A%7@02TG9.png

但是在有些時候沾乘,我們是不希望屬性是可枚舉的怜奖,就像前一章說過的組合繼承,手動賦值constructor

// 此時constructor也是可枚舉的
Child.prototype.constructor = Child

另外翅阵,通過Object.defineProperty定義屬性時歪玲,如果不指定數(shù)據(jù)屬性,默認(rèn)情況下都為false

)I35E_)UJ@Y412DL(VBJP$5.png

根據(jù)這些解釋怎顾,就可以理解前文中_createClass為什么要重寫defineProperties方法了。

4.編譯后的Child

同理漱贱,Child與Parent一樣槐雾,都會有相同的2個步驟(前面說的2件事情),不同的是

var Child = function (_Parent) {
  function Child(name, age) {
    // ...
    // 2.這里我想就是阮一峰老師文章里有說的
    // 拿到Parent實例的this幅狮,封裝成Child子類的this
    var _this = _possibleConstructorReturn(this, (Child.__proto__ || Object.getPrototypeOf(Child)).call(this, name));
    // ...
  }
  
  // 1.這一步是關(guān)鍵的繼承
  _inherits(Child, _Parent);
  // ...
}(Parent);

我們看到Child多做了2件事情募强,先說第1件事:

_inherits繼承

_inherits也做了2件事

  1. 就是我們組合繼承中提到的,利用Object.create將子類的原型指向父類的原型
  2. 將子類的__proto__指向父類構(gòu)造函數(shù)崇摄,也就是說擎值,認(rèn)為子類是父類構(gòu)造而來。
    這里有點繞逐抑,其實就是這么個意思:你還記得組合繼承中鸠儿,在Child的構(gòu)造函數(shù)里callParent一下么,將Parent上的屬性都復(fù)制一份到Child的this中厕氨。那么在這里进每,ES6并不知道要call誰啊,所以只好將父類的構(gòu)造函數(shù)指給子類的__proto__命斧,這樣后面就只需要Child.__proto__.call(this)了田晚。
function _inherits(subClass, superClass) { 
    // 校驗代碼...
    
    // 利用 Object.create創(chuàng)建實例對象,并將實例賦值給subClass.prototype
    // 并手動賦值constructor
    subClass.prototype = Object.create(superClass && superClass.prototype, { 
        constructor: { 
            value: subClass, 
            enumerable: false, 
            writable: true, 
            configurable: true 
        } 
    }); 

    // 這一步就是上面說的第2件事国葬,將superClass構(gòu)造函數(shù)賦值給subClass.__proto__贤徒,方便后面調(diào)用
    // 因為我們知道芹壕,所有的函數(shù)都是由Function構(gòu)造而來
    // 也就是說如果這里不賦值,subClass.__proto__ === Function.prototype
    if (superClass) 
        Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; 
}

Child做的第2件事情

_possibleConstructorReturn調(diào)用父類構(gòu)造函數(shù)

其實這里的表述有誤接奈,調(diào)用父類構(gòu)造函數(shù)并不是_possibleConstructorReturn做的踢涌,_possibleConstructorReturn只是做了一個簡單的校驗。

// 調(diào)用父類構(gòu)造函數(shù)
(Child.__proto__ || Object.getPrototypeOf(Child)).call(this, name)

看到這一句就證明我前面的理解是對的鲫趁,這一句在es5組合繼承中就是

Parent.call(this, name)

只不過它不知道Parent是誰斯嚎,所以就先把Parent賦值給了Child.__proto__。由于ES6中都是通過寄生模式來創(chuàng)建構(gòu)造函數(shù)挨厚,這里call之后堡僻,返回的是Parent的實例,與組合繼承的call并不一致疫剃。

5.總結(jié)

我們再回過頭來看ES5與ES6繼承的區(qū)別钉疫,其實ES5與ES6的繼承沒有太大的區(qū)別,其原理都是采用了組合繼承巢价,核心唯一不同的就是這個this值的問題牲阁,另外就是對定義靜態(tài)方法做了封裝(staitc)。

es5的繼承壤躲,是我們手動寫父類城菊,子類手動call父類碉克。

但是es6中的繼承是抽象出來的語法糖凌唬,并不知道你這里哪個是父類哪個是子類,所以它得通過一個巧妙的方法來知道這個是父類這個是子類漏麦。Child.__proto__ === Parent或者調(diào)用Object.setPrototypeOf()客税。

之所以this不一樣,是因為它們創(chuàng)建構(gòu)造函數(shù)的機(jī)制不一樣

  • es5是直接聲明撕贞,那樣this就直接被初始化了更耻,所以只能在子類里通過call來“豐富”它的this
  • es6則是通過寄生模式(非工廠模式),返回的一個新的構(gòu)造函數(shù)捏膨,當(dāng)你call的時候秧均,相當(dāng)于new了這個新的構(gòu)造函數(shù),此時父類的構(gòu)造函數(shù)看起來就是個閉包号涯,因為他還要返回new后的實例對象熬北。所以在子類中是直接就拿到了父類的實例對象,那么就將this指向了他诚隙,再賦值自己的屬性讶隐。

后話

這是自己第一次系統(tǒng)的去理清其中的關(guān)系,并不是很熟練的掌握了個中的原理久又,有誤之處還望指出巫延!

相關(guān)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末效五,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子炉峰,更是在濱河造成了極大的恐慌畏妖,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件疼阔,死亡現(xiàn)場離奇詭異戒劫,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)婆廊,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進(jìn)店門迅细,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人淘邻,你說我怎么就攤上這事茵典。” “怎么了宾舅?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵统阿,是天一觀的道長。 經(jīng)常有香客問我筹我,道長扶平,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任蔬蕊,我火速辦了婚禮结澄,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘袁串。我一直安慰自己概而,他們只是感情好呼巷,可當(dāng)我...
    茶點故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布囱修。 她就那樣靜靜地躺著,像睡著了一般王悍。 火紅的嫁衣襯著肌膚如雪破镰。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天压储,我揣著相機(jī)與錄音鲜漩,去河邊找鬼。 笑死集惋,一個胖子當(dāng)著我的面吹牛孕似,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播刮刑,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼喉祭,長吁一口氣:“原來是場噩夢啊……” “哼养渴!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起泛烙,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤理卑,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后蔽氨,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體藐唠,經(jīng)...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年鹉究,在試婚紗的時候發(fā)現(xiàn)自己被綠了宇立。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡坊饶,死狀恐怖泄伪,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情匿级,我是刑警寧澤蟋滴,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站痘绎,受9級特大地震影響津函,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜孤页,卻給世界環(huán)境...
    茶點故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一尔苦、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧行施,春花似錦允坚、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至鲜结,卻和暖如春展运,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背精刷。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工拗胜, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人怒允。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓埂软,卻偏偏與公主長得像,于是被迫代替她去往敵國和親纫事。 傳聞我的和親對象是個殘疾皇子勘畔,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,592評論 2 353

推薦閱讀更多精彩內(nèi)容