ES5的繼承和ES6的繼承有什么區(qū)別?讓Babel來告訴你

如果以前問我ES5的繼承和ES6的繼承有什么區(qū)別假瞬,我一定會自信的說沒有區(qū)別省店,不過是語法糖而已,充其量也就是寫法有區(qū)別笨触,但是現(xiàn)在我會假裝思考一下,然后說雖然只是語法糖雹舀,但也是有點(diǎn)小區(qū)別的芦劣,那么具體有什么區(qū)別呢,不要走開说榆,下文更精彩虚吟!

本文會先回顧一下ES5的寄生組合式繼承的實(shí)現(xiàn)寸认,然后再看一下ES6的寫法,最后根據(jù)Babel的編譯結(jié)果來看一下到底有什么區(qū)別串慰。

ES5:寄生組合式繼承

js有很多種繼承方式偏塞,比如大家耳熟能詳?shù)?code>原型鏈繼承、構(gòu)造繼承邦鲫、組合繼承灸叼、寄生繼承等,但是這些或多或少都有一些不足之處庆捺,所以筆者認(rèn)為我們只要記住一種就可以了古今,那就是寄生組合式繼承

首先要明確繼承到底要繼承些什么東西滔以,一共有三部分捉腥,一是實(shí)例屬性/方法、二是原型屬性/方法你画、三是靜態(tài)屬性/方法抵碟,我們分別來看。

先來看一下我們要繼承的父類的函數(shù):

// 父類
function Sup(name) {
    this.name = name// 實(shí)例屬性
}
Sup.type = '午'// 靜態(tài)屬性
// 靜態(tài)方法
Sup.sleep =  function () {
    console.log(`我在睡${this.type}覺`)
}
// 實(shí)例方法
Sup.prototype.say = function() {
    console.log('我叫 ' + this.name)
}

繼承實(shí)例屬性/方法

要繼承實(shí)例屬性/方法坏匪,明顯要執(zhí)行一下Sup函數(shù)才行拟逮,并且要修改它的this指向,這使用call剥槐、apply方法都行:

// 子類
function Sub(name, age) {
    // 繼承父類的實(shí)例屬性
    Sup.call(this, name)
    // 自己的實(shí)例屬性
    this.age = age
}
image-20210824173830421.png

能這么做的原理又是另外一道經(jīng)典面試題:new操作符都做了什么唱歧,很簡單,就4點(diǎn):

1.創(chuàng)建一個(gè)空對象

2.把該對象的__proto__屬性指向Sub.prototype

3.讓構(gòu)造函數(shù)里的this指向新對象粒竖,然后執(zhí)行構(gòu)造函數(shù)颅崩,

4.返回該對象

所以Sup.call(this)this指的就是這個(gè)新創(chuàng)建的對象,那么就會把父類的實(shí)例屬性/方法都添加到該對象上蕊苗。

繼承原型屬性/方法

我們都知道如果一個(gè)對象它本身沒有某個(gè)方法沿后,那么會去它構(gòu)造函數(shù)的原型對象上,也就是__proto__指向的對象上查找朽砰,如果還沒找到尖滚,那么會去構(gòu)造函數(shù)原型對象的__proto__上查找,這樣一層一層往上瞧柔,也就是傳說中的原型鏈漆弄,所以Sub的實(shí)例想要能訪問到Sup的原型方法,就需要把Sub.prototypeSup.prototype關(guān)聯(lián)起來造锅,這有幾種方法:

1.使用Object.create

Sub.prototype = Object.create(Sup.prototype)
Sub.prototype.constructor = Sub

2.使用__proto__

Sub.prototype.__proto__ = Sup.prototype

3.借用中間函數(shù)

function Fn() {}
Fn.prototype = Sup.prototype
Sub.prototype = new Fn()
Sub.prototype.constructor = Sub

以上三種方法都可以撼唾,我們再來覆蓋一下繼承到的Say方法,然后在該方法里面再調(diào)用父類原型上的say方法:

Sub.prototype.say = function () {
    console.log('你好')
    // 調(diào)用父類的該原型方法
    // this.__proto__ === Sub.prototype哥蔚、Sub.prototype.__proto__ === Sup.prototype
    this.__proto__.__proto__.say.call(this)
    console.log(`今年${this.age}歲`)
}
image-20210824182416678.png

繼承靜態(tài)屬性/方法

也就是繼承Sup函數(shù)本身的屬性和方法倒谷,這個(gè)很簡單蛛蒙,遍歷一下父類自身的可枚舉屬性,然后添加到子類上即可:

Object.keys(Sup).forEach((prop) => {
    Sub[prop] = Sup[prop]
})
image-20210824182459876.png

ES6:使用class繼承

接下來我們使用ES6class關(guān)鍵字來實(shí)現(xiàn)上面的例子:

// 父類
class Sup {
    constructor(name) {
        this.name = name
    }
    
    say() {
        console.log('我叫 ' + this.name)
    }
    
    static sleep() {
        console.log(`我在睡${this.type}覺`)
    }
}
// static只能設(shè)置靜態(tài)方法渤愁,不能設(shè)置靜態(tài)屬性牵祟,所以需要自行添加到Sup類上
Sup.type = '午'
// 另外,原型屬性也不能在class里面設(shè)置抖格,需要手動設(shè)置到prototype上诺苹,比如Sup.prototype.xxx = 'xxx'

// 子類,繼承父類
class Sub extends Sup {
    constructor(name, age) {
        super(name)
        this.age = age
    }
    
    say() {
        console.log('你好')
        super.say()
        console.log(`今年${this.age}歲`)
    }
}
Sub.type = '懶'
image-20210824182650898.png

可以看到一樣的效果他挎,使用class會簡潔明了很多筝尾,接下來我們使用babel來把這段代碼編譯回ES5的語法,看看和我們寫的有什么不一樣办桨,由于編譯完的代碼有200多行筹淫,所以不能一次全部貼上來,我們先從父類開始看:

編譯后的父類

// 父類
var Sup = (function () {
  function Sup(name) {
    _classCallCheck(this, Sup);

    this.name = name;
  }

  _createClass(
    Sup,
    [
      {
        key: "say",
        value: function say() {
          console.log("我叫 " + this.name);
        },
      },
    ],
    [
      {
        key: "sleep",
        value: function sleep() {
          console.log("\u6211\u5728\u7761".concat(this.type, "\u89C9"));
        },
      },
    ]
  );

  return Sup;
})(); // static只能設(shè)置靜態(tài)方法呢撞,不能設(shè)置靜態(tài)屬性

Sup.type = "午"; // 子類损姜,繼承父類
// 如果我們之前通過Sup.prototype.xxx = 'xxx'設(shè)置了原型屬性,那么跟靜態(tài)屬性一樣殊霞,編譯后沒有區(qū)別摧阅,也是這么設(shè)置的

可以看到是個(gè)自執(zhí)行函數(shù),里面定義了一個(gè)Sup函數(shù)绷蹲,Sup里面先調(diào)用了一個(gè)_classCallCheck(this, Sup)函數(shù)棒卷,我們轉(zhuǎn)到這個(gè)函數(shù)看看:

function _classCallCheck(instance, Constructor) {
  if (!(instance instanceof Constructor)) {
    throw new TypeError("Cannot call a class as a function");
  }
}

instanceof運(yùn)算符是用來檢測右邊函數(shù)的prototype屬性是否出現(xiàn)在左邊的對象的原型鏈上,簡單說可以判斷某個(gè)對象是否是某個(gè)構(gòu)造函數(shù)的實(shí)例祝钢,可以看到如果不是的話就拋錯(cuò)了比规,錯(cuò)誤信息是不能把一個(gè)類當(dāng)做函數(shù)調(diào)用,這里我們就發(fā)現(xiàn)第一個(gè)區(qū)別了:

區(qū)別1:ES5里的構(gòu)造函數(shù)就是一個(gè)普通的函數(shù),可以使用new調(diào)用,也可以直接調(diào)用荣倾,而ES6的class不能當(dāng)做普通函數(shù)直接調(diào)用,必須使用new操作符調(diào)用

繼續(xù)看自執(zhí)行函數(shù)灾常,接下來調(diào)用了一個(gè)_createClass方法:

function _createClass(Constructor, protoProps, staticProps) {
  if (protoProps) _defineProperties(Constructor.prototype, protoProps);
  if (staticProps) _defineProperties(Constructor, staticProps);
  return Constructor;
}

該方法接收三個(gè)參數(shù),分別是構(gòu)造函數(shù)铃拇、原型方法钞瀑、靜態(tài)方法(注意不包含原型屬性和靜態(tài)屬性),后面兩個(gè)都是數(shù)組慷荔,數(shù)組里面每一項(xiàng)代表一個(gè)方法對象仔戈,不管是實(shí)例方法還是原型方法,都是通過_defineProperties方法設(shè)置,先來看該方法:

function _defineProperties(target, props) {
  for (var i = 0; i < props.length; i++) {
    var descriptor = props[i];
    // 設(shè)置該屬性是否可枚舉监徘,設(shè)為false則for..in、Object.keys遍歷不到該屬性
    descriptor.enumerable = descriptor.enumerable || false;
    // 默認(rèn)可配置吧碾,即能修改和刪除該屬性
    descriptor.configurable = true;
    // 設(shè)為true時(shí)該屬性的值能被賦值運(yùn)算符改變
    if ("value" in descriptor) descriptor.writable = true;
    Object.defineProperty(target, descriptor.key, descriptor);
  }
}

可以看到它是通過Object.defineProperty方法來設(shè)置原型方法和靜態(tài)方法凰盔,而且enumerable默認(rèn)為false,這就來到了第二個(gè)區(qū)別:

區(qū)別2:ES5的原型方法和靜態(tài)方法默認(rèn)是可枚舉的倦春,而class的默認(rèn)不可枚舉户敬,如果想要獲取不可枚舉的屬性可以使用Object.getOwnPropertyNames方法

接下來看子類編譯后的代碼:

編譯后的子類

// 子類,繼承父類
var Sub = (function (_Sup) {
  _inherits(Sub, _Sup);

  var _super = _createSuper(Sub);

  function Sub(name, age) {
    var _this;

    _classCallCheck(this, Sub);

    _this = _super.call(this, name);
    _this.age = age;
    return _this;
  }

  _createClass(Sub, [
    {
      key: "say",
      value: function say() {
        console.log("你好");

        _get(_getPrototypeOf(Sub.prototype), "say", this).call(this);

        console.log("\u4ECA\u5E74".concat(this.age, "\u5C81"));
      }
    }
  ]);

  return Sub;
})(Sup);

Sub.type = "懶";

同樣也是一個(gè)自執(zhí)行方法睁本,把要繼承的父類構(gòu)造函數(shù)作為參數(shù)傳進(jìn)去了尿庐,進(jìn)來先調(diào)用了_inherits(Sub, _Sup)方法,雖然Sub函數(shù)是在后面定義的呢堰,但是函數(shù)聲明是存在提升的抄瑟,所以這里是可以正常訪問到的:

function _inherits(subClass, superClass) {
  // 被繼承對象的必須是一個(gè)函數(shù)或null
  if (typeof superClass !== "function" && superClass !== null) {
    throw new TypeError("Super expression must either be null or a function");
  }
  // 設(shè)置原型
  subClass.prototype = Object.create(superClass && superClass.prototype, {
    constructor: { value: subClass, writable: true, configurable: true }
  });
  if (superClass) _setPrototypeOf(subClass, superClass);
}

這個(gè)方法先檢查了父類是否合法,然后通過Object.create方法設(shè)置了子類的原型枉疼,這個(gè)和我們之前的寫法是一樣的皮假,只是今天我才發(fā)現(xiàn)Object.create居然還有第二個(gè)參數(shù),第二個(gè)參數(shù)必須是一個(gè)對象骂维,對象的自有可枚舉屬性(即其自身定義的屬性惹资,而不是其原型鏈上的枚舉屬性)將為新創(chuàng)建的對象添加指定的屬性值和對應(yīng)的屬性描述符。

這個(gè)方法的最后為我們揭曉了第三個(gè)區(qū)別:

區(qū)別3:子類可以直接通過__proto__找到父類航闺,而ES5是指向Function.prototype

ES6:Sub.__proto__ === Sup

ES5:Sub.__proto__ === Function.prototype

為啥會這樣呢褪测,看看_setPrototypeOf方法做了啥就知道了:

function _setPrototypeOf(o, p) {
    _setPrototypeOf =
        Object.setPrototypeOf ||
        function _setPrototypeOf(o, p) {
            o.__proto__ = p;
            return o;
        };
    return _setPrototypeOf(o, p);
}

可以看到這個(gè)方法把Sub.__proto__設(shè)置為了Sup,這樣同時(shí)也完成了靜態(tài)方法和屬性的繼承潦刃,因?yàn)楹瘮?shù)也是對象侮措,自身沒有的屬性和方法也會沿著__proto__鏈查找。

_inherits方法過后緊接著調(diào)用了一個(gè)_createSuper(Sub)方法福铅,拉出來看看:

function _createSuper(Derived) {
    return function _createSuperInternal() {
        // ...
    };
}

這個(gè)函數(shù)接收子類構(gòu)造函數(shù)萝毛,然后返回了一個(gè)新函數(shù),我們先跳到后面的子類構(gòu)造函數(shù)的定義:

function Sub(name, age) {
    var _this;

    // 檢查是否當(dāng)做普通函數(shù)調(diào)用滑黔,是的話拋錯(cuò)
    _classCallCheck(this, Sub);

    _this = _super.call(this, name);
    _this.age = age;
    return _this;
}

同樣是先檢查了一下是否是使用new調(diào)用笆包,然后我們發(fā)現(xiàn)這個(gè)函數(shù)返回了一個(gè)_this,前面介紹了new操作符都做了什么略荡,我們知道會隱式創(chuàng)建一個(gè)對象庵佣,并且會把函數(shù)內(nèi)的this指向該對象,如果沒有顯式的指定構(gòu)造函數(shù)返回什么汛兜,那么就會默認(rèn)返回這個(gè)新創(chuàng)建的對象巴粪,而這里顯然是手動指定了要返回的對象,而這個(gè)_this來自于_super函數(shù)的執(zhí)行結(jié)果,_super就是前面_createSuper返回的新函數(shù):

function _createSuper(Derived) {
    // _isNativeReflectConstruct會檢查Reflect.construct方法是否可用
    var hasNativeReflectConstruct = _isNativeReflectConstruct();
    return function _createSuperInternal() {
        // _getPrototypeOf方法用來獲取Derived的原型肛根,也就是Derived.__proto__
        var Super = _getPrototypeOf(Derived),
            result;
        if (hasNativeReflectConstruct) {
            // NewTarget === Sub
            var NewTarget = _getPrototypeOf(this).constructor;
            // Reflect.construct的操作可以簡單理解為:result = new Super(...arguments)辫塌,第三個(gè)參數(shù)如果傳了則作為新創(chuàng)建對象的構(gòu)造函數(shù),也就是result.__proto__ === NewTarget.prototype派哲,否則默認(rèn)為Super.prototype
            result = Reflect.construct(Super, arguments, NewTarget);
        } else {
            result = Super.apply(this, arguments);
        }
        return _possibleConstructorReturn(this, result);
    };
}

Super代表的是Sub.__proto__臼氨,根據(jù)前面的繼承操作,我們知道子類的__proto__指向了父類芭届,也就是Sup储矩,這里會優(yōu)先使用Reflect.construct方法,相當(dāng)于創(chuàng)建了一個(gè)父類的實(shí)例褂乍,并且這個(gè)實(shí)例的__proto__又指回了Sub.prototype持隧,不得不說這個(gè)api真是神奇。

我們就不考慮降級情況了逃片,那么最后會返回這個(gè)父類的實(shí)例對象屡拨。

回到Sub構(gòu)造函數(shù),_this指向的就是這個(gè)通過父類創(chuàng)建的實(shí)例對象题诵,為什么要這么做呢洁仗,這其實(shí)就是第四個(gè)區(qū)別了,也是最重要的區(qū)別:

區(qū)別4:ES5的繼承性锭,實(shí)質(zhì)是先創(chuàng)造子類的實(shí)例對象this赠潦,然后再執(zhí)行父類的構(gòu)造函數(shù)給它添加實(shí)例方法和屬性(不執(zhí)行也無所謂)。而ES6的繼承機(jī)制完全不同草冈,實(shí)質(zhì)是先創(chuàng)造父類的實(shí)例對象this(當(dāng)然它的__proto__指向的是子類的prototype)她奥,然后再用子類的構(gòu)造函數(shù)修改this

這就是為啥使用class繼承在constructor函數(shù)里必須調(diào)用super怎棱,因?yàn)樽宇悏焊鶝]有自己的this哩俭,另外不能在super執(zhí)行前訪問this的原因也很明顯了,因?yàn)檎{(diào)用了super后拳恋,this才有值凡资。

子類自執(zhí)行函數(shù)的最后一部分也是給它設(shè)置原型方法和靜態(tài)方法,這個(gè)前面講過了谬运,我們重點(diǎn)看一下實(shí)例方法編譯后的結(jié)果:

function say() {
    console.log("你好");

    _get(_getPrototypeOf(Sub.prototype), "say", this).call(this);

    console.log("\u4ECA\u5E74".concat(this.age, "\u5C81"));
}

猜你們也忘了編譯前的原函數(shù)是啥樣的了隙赁,請看:

say() {
    console.log('你好')
    super.say()
    console.log(`今年${this.age}歲`)
}

ES6classsuper有兩種含義,當(dāng)做函數(shù)調(diào)用的話它代表父類的構(gòu)造函數(shù)梆暖,只能在constructor里面調(diào)用伞访,當(dāng)做對象使用時(shí)它指向父類的原型對象,所以_get(_getPrototypeOf(Sub.prototype), "say", this).call(this)這行大概相當(dāng)于Sub.prototype.__proto__.say.call(this)轰驳,跟我們最開始寫的ES5版本也差不多厚掷,但是顯然在class的語法要簡單很多弟灼。

到此,編譯后的代碼我們就分析的差不多了冒黑,不過其實(shí)還有一個(gè)區(qū)別不知道大家有沒有發(fā)現(xiàn)田绑,那就是為啥要使用自執(zhí)行函數(shù),一當(dāng)然是為了封裝一些變量抡爹,二其實(shí)是因?yàn)榈谖鍌€(gè)區(qū)別:

區(qū)別5:class不存在變量提升辛馆,所以父類必須在子類之前定義

不信你把父類放到子類后面試試,不出意外會報(bào)錯(cuò)豁延,你可能會覺得直接使用函數(shù)表達(dá)式也可以達(dá)到這樣的效果,非也:

// 會報(bào)錯(cuò)
var Sub = function(){ Sup.call(this) }
new Sub()
var Sup = function(){}

// 不會報(bào)錯(cuò)
var Sub = function(){ Sup.call(this) }
var Sup = function(){}
new Sub()

但是Babel編譯后的無論你在哪里實(shí)例化子類腊状,只要父類在它之后聲明都會報(bào)錯(cuò)诱咏。

總結(jié)

本文通過分析Babel編譯后的代碼來總結(jié)了ES5ES6繼承的5個(gè)區(qū)別,可能還有一些其他的缴挖,有興趣可以自行了解袋狞。

關(guān)于class的詳細(xì)信息可以看這篇繼承class繼承

示例代碼在https://github.com/wanglin2/es5-es5-inherit-example映屋。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末苟鸯,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子棚点,更是在濱河造成了極大的恐慌早处,老刑警劉巖,帶你破解...
    沈念sama閱讀 210,978評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件瘫析,死亡現(xiàn)場離奇詭異砌梆,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)贬循,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評論 2 384
  • 文/潘曉璐 我一進(jìn)店門咸包,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人杖虾,你說我怎么就攤上這事烂瘫。” “怎么了奇适?”我有些...
    開封第一講書人閱讀 156,623評論 0 345
  • 文/不壞的土叔 我叫張陵坟比,是天一觀的道長。 經(jīng)常有香客問我滤愕,道長温算,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,324評論 1 282
  • 正文 為了忘掉前任间影,我火速辦了婚禮注竿,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己巩割,他們只是感情好裙顽,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,390評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著宣谈,像睡著了一般愈犹。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上闻丑,一...
    開封第一講書人閱讀 49,741評論 1 289
  • 那天漩怎,我揣著相機(jī)與錄音,去河邊找鬼嗦嗡。 笑死勋锤,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的侥祭。 我是一名探鬼主播叁执,決...
    沈念sama閱讀 38,892評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼矮冬!你這毒婦竟也來了谈宛?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,655評論 0 266
  • 序言:老撾萬榮一對情侶失蹤胎署,失蹤者是張志新(化名)和其女友劉穎吆录,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體硝拧,經(jīng)...
    沈念sama閱讀 44,104評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡径筏,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了障陶。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片滋恬。...
    茶點(diǎn)故事閱讀 38,569評論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖抱究,靈堂內(nèi)的尸體忽然破棺而出恢氯,到底是詐尸還是另有隱情,我是刑警寧澤鼓寺,帶...
    沈念sama閱讀 34,254評論 4 328
  • 正文 年R本政府宣布勋拟,位于F島的核電站,受9級特大地震影響妈候,放射性物質(zhì)發(fā)生泄漏敢靡。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,834評論 3 312
  • 文/蒙蒙 一苦银、第九天 我趴在偏房一處隱蔽的房頂上張望啸胧。 院中可真熱鬧赶站,春花似錦、人聲如沸纺念。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,725評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽陷谱。三九已至烙博,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間烟逊,已是汗流浹背渣窜。 一陣腳步聲響...
    開封第一講書人閱讀 31,950評論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留宪躯,地道東北人图毕。 一個(gè)月前我還...
    沈念sama閱讀 46,260評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像眷唉,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子囤官,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,446評論 2 348

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