ES5 與 ES6繼承

寫在前面

之所以想寫這個帐姻,是想為以后學習 react 做個鋪墊稠集,每一個看似理所當然的結(jié)果,實際上推敲過程很耐人尋味饥瓷,就從 ES6 類的實例化和繼承開始剥纷,但是 ES6 的實現(xiàn)是在 ES5 的基礎上,所以需要對 ES5 中關于 構(gòu)造函數(shù)呢铆、繼承這些梳理清晰晦鞋。

關于 new 關鍵字
  • 無論 ES6 還是 ES5 中對于自定義對象的實現(xiàn)都不開 new 這個關鍵詞,所以需要搞清楚它在構(gòu)造函數(shù)實例化過程中的作用棺克,因為透過它你能看清楚構(gòu)造函數(shù)內(nèi)部 this 指向的問題.
  • 有時候文字解釋會讓讀者對一個概念產(chǎn)生各種想象悠垛,所以我比較偏好嘗試用代碼來解釋自己難以理解的地方,代碼如下:
// ES5 中構(gòu)造函數(shù)創(chuàng)建實例過程
function Person (name, age) {
    this.name = name
    this.age = age
}
// 返回一個具有 name = ww, age = 18 的 person 對象
var person = new Person('ww', 19)

// 那么 new 關鍵字做了哪些工作
// 下面是對使用 new 關鍵字配合構(gòu)造函數(shù)創(chuàng)建對象的過程模擬
var person = (function() {
    function Person (name, age) {
        this.name = name
        this.age = age
    }
    // 創(chuàng)建一個隱式空對象
    var object = {}
    // 修正隱式對象的原型
    setPrototypeOf(object, Person.prototype)
    // 執(zhí)行構(gòu)造函數(shù)娜谊,為空對象賦值
    Person.apply(object, arguments)
    // 返回隱式對象
    return object
})('www', 90)
function setPrototypeOf(object, proto) {
    Object.setPrototypeOf ? 
    Object.setPrototypeOf(object, proto) 
    : 
    object.__proto__ = proto
}
// 返回的 person 我們通常會稱之為 Person 的一個實例鼎文。
  • 上述就是對 new 在構(gòu)造函數(shù)實例化中的作用的描述,這里需要注意的一點是因俐,在 ES5 中拇惋,如果沒有使用 new 關鍵字,那么 Person 只是作為 window 的一個普通方法調(diào)用抹剩,所以也不會報錯撑帖,但是在 ES6 的 類 的實例化中必須使用 new ,否則會報錯澳眷,在解讀 ES6 類的實例化會解釋報錯的原因胡嘿。
ES5中繼承的實現(xiàn)
  • ES5 中實現(xiàn)繼承的方式比較多, W3C 標準推薦的做法是有三種ECMAScript 繼承機制實現(xiàn)钳踊,下面直接上代碼來闡述 ES5 實現(xiàn)繼承的過程衷敌,以及優(yōu)缺點
  1. 第一種是通過傳統(tǒng)的原型鏈繼承方式,比較好理解:自身若有被讀取的屬性或者方法的時候拓瞪,自取所需缴罗,如果沒有,就沿著原型鏈一層一層往上找祭埂,直到找到或者找不到返回 undefined 或者報錯
// 01-定義父類
function Human(home) {
    this.home = home
}
// 02-為父類實例添加方法
Human.prototype.say = function () {
    return `I'm a ${this.sex}, I come from ${this.home}`
}
// 03-定義一個子類
function Person(sex) {
    this.sex = sex
}
// 04-子類的原型對象指向父類實例面氓,實現(xiàn)繼承
Person.prototype = new Human('Earth')
var man = new Person('man')
// 調(diào)用實例身上的方法,由于沒有,沿著原型鏈往上找舌界,
// 找到的是父類原型對象上面的 say() 方法
man.say()   // 返回的是 I'm a man, I come from Earth

// 05-子類再實例化一個對象
var female = new Person('female')

// 06-修改原型鏈上的 home 屬性
Object.getPrototypeOf(female).home = 'Mars'

// 07-調(diào)用子類兩個實例的 say() 方法
man.say()        // 返回結(jié)果:I'm a man, I come from Mars
female.say()     // 返回結(jié)果:I'm a female, I come from Mars

// 別人本來是來自地球掘譬,你隨便動動手指,讓人家誕生在火星了呻拌,多少有點說不過去
// 基于這樣的特點葱轩,只要有任何一個子類實例修改了原型對象上的屬性或者父類實例自身修改了屬性
// 將會影響所有繼承它的子類,這個不是我們愿意看到的
// 所以就有了第二種方式:call 或者 apply 實現(xiàn)繼承
  1. 通過 call 或者 apply 實現(xiàn)繼承和 對象冒充 繼承很相似藐握,但是有所不同靴拱,根本原因是 this 總是指向函數(shù)運行時所在的那個對象,下面是 call 方法實現(xiàn)繼承過程
// 01-定義父類
function Human(home) {
    this.home = home
}
// 02-為父類實例添加方法
Human.prototype.say = function () {
    return `I'm a ${this.sex}, I come from ${this.home}`
}
// 03-定義一個子類
function Person(sex, home) {
    // 04-實現(xiàn)借用父類創(chuàng)建子類自身的屬性趾娃,這是最重要的一點
    // 如果你理解了 new 的作用缭嫡,就知道此刻 this 指向是誰了
    Human.call(this, home)
    this.sex = sex
}
// 05-實例化一個具有 sex=man 和 home=earth 特征的 子類實例
var man = new Person('man', 'Earth')

// 06-再實例化一個具有 sex=female 和 home=Mars 特征的 子類實例
var female = new Person('female', 'Mars')

// 07-無論任意子類實例修改 sex 和 home 屬性,或者 父類實例修改 home 屬性
// 都不會影響到其他的子類實例抬闷,因為屬性此刻私有化了

// 08-但是會發(fā)現(xiàn)調(diào)用子類兩個實例的 say() 方法妇蛀,會報錯,因為原型鏈上沒有這個方法
// 扔出錯誤:man.say is not a function
man.say() 
female.say() 

// call 或者 apply 方法實現(xiàn)繼承的最大優(yōu)勢就是能夠?qū)崿F(xiàn)屬性私有化笤成,但是劣勢就是沒有辦法繼承
// 父類原型對象上面的方法评架,所以為了解決原型鏈繼承和call方法繼承的缺點,將兩者的優(yōu)點糅合在一起
// 即混合繼承炕泳,能夠?qū)崿F(xiàn)完美的繼承
  1. 混合繼承纵诞,即通過 call 或者 apply 實現(xiàn)屬性繼承,原型鏈實現(xiàn)方法繼承
// 01-定義父類
function Human(home) {
    this.home = home
}
// 02-為父類實例添加方法
Human.prototype.say = function () {
    return `I'm a ${this.sex}, I come from ${this.home}`
}
// 03-定義一個子類
function Person(sex, home) {
// 04-使用 call 繼承父類的屬性
    Human.call(this, home)
    this.sex = sex
}
// 05-使用原型鏈培遵,繼承父類的方法
Person.prototype = new Human('sun')

// 06-子類實例化一個對象
var man = new Person('man', 'Earth')

// 07-子類再實例化一個對象
var female = new Person('female', 'Mars')

// 08-修改原型鏈上的 home 屬性
Object.getPrototypeOf(female).home = 'Heaven'

// 09-調(diào)用子類兩個實例的 say() 方法浙芙,返回的 home 都是當前實例自身的 home 屬性
// 而不是原型鏈上的 home ,因為當前實例自身有該屬性籽腕,就不再往原型鏈上找了
// 所以通過 call 和 原型鏈能夠?qū)崿F(xiàn)完美繼承
man.say()     // 返回結(jié)果:I'm a man, I come from Earth
female.say()  // 返回結(jié)果:I'm a female, I come from Mars
ES6類實例化中的new
  • 之所以會說這么多 ES5 的繼承嗡呼,是因為 ES6 的繼承實現(xiàn)都是基于前者,可以看做是前者的語法糖皇耗,只有在理解基礎的前提下南窗,才能談進階
  • 關于 ES6class 的一些基礎知識就不提了,現(xiàn)在來說說 class 聲明的變量在實例化過程中為什么必須使用 new 關鍵詞
// 01-聲明一個類
class Human {
    constructor(home) {
        this.home = home
    }
}
// 02-雖然 Human 的本質(zhì)是一個 function郎楼,但是在沒有 new 的情況下調(diào)用類 js 引擎會拋出錯誤
// Class constructor Human cannot be invoked without 'new'万伤,這點跟 ES5 是不同的
const man = Human('Earth')
// 原因很簡單,在 class 聲明的變量內(nèi)部呜袁,默認開啟的是 嚴格模式敌买,所以如果沒有使用 new 
// this 的指向是 undefined,對 undefined 任何屬性或者方法的讀寫都是沒有意義的傅寡,所以直接丟出錯誤

// 03-對 類 進行實例化是否使用 new 的判斷以及實例化過程模擬

// 開啟嚴格模式
'use strict'
function setPrototypeOf(object, proto) {
    Object.setPrototypeOf ? 
    Object.setPrototypeOf(object, proto) 
    : 
    object.__proto__ = proto
}
var person = (function () {
    function _classCallCheck(instance, constructor) {
        if (!(instance instanceof constructor)) {
            throw new TypeError(`Class constructor ${constructor.name} cannot be invoked without 'new'`)
        }
    }
    function Human(home) {
        // 在這一步來判斷當前 this 的指向放妈,如果你理解了 new 的作用北救,
       // 你就會清楚荐操,當前 this 的指向
        _classCallCheck(this, Human)
        this.home = home
    }
    var object = {}
    setPrototypeOf(object, Human.prototype)
    Human.apply(object, arguments)
    return object
})('Earth')
// 以上就解釋了為什么 ES6 必須使用 new芜抒,以及如果做判斷的過程
ES6類實例化過程中如何添加靜態(tài)方法和原型方法
  • ES5 一樣,ES6 有靜態(tài)方法和原型方法托启,兩者都可以通過傳統(tǒng)的 點語法 來添加靜態(tài)屬性和方法宅倒,以及原型方法霸旗,但是在 ES6 中將這層添加方式做了一層封裝逮栅,直接寫在類的內(nèi)部,方法就會添加到原型或者類本身上面了
class Human {
    // constructor 和 say 方法添加到原型上
    constructor(home) {
        this.home = home
    }
    say() {
        return `I come from ${this.home}`
    }
    // 靜態(tài)方法添加到當前類本身上
    static drink() {
        return `human had better drink everyday`
    }
}
const person = new Human('Mars')


// 上述的實現(xiàn)過程可以通過下面代碼模擬

// 開啟嚴格模式
'use strict'
function setPrototypeOf(object, proto) {
    Object.setPrototypeOf ? 
    Object.setPrototypeOf(object, proto) 
    : 
    object.__proto__ = proto
}
function _classCallCheck(instance, constructor) {
    if (!(instance instanceof constructor)) {
        throw new TypeError(`Class constructor ${constructor.name} cannot be invoked without 'new'`)
    }
}
// 01-拓展構(gòu)造函數(shù)的方法
var _createClass = (function () {
    // 02-定義一個將方法添加原型或者構(gòu)造函數(shù)本身的方法缔俄;
    function defineProperties(target, props) {
        for (var i = 0, length = props.length; i < length; i++) {
            // 獲取屬性描述符疗绣,即 Object.defineProperty(object, key, descriptor) 的第三個參數(shù)
            var descriptor = props[i]
            // 指定當前方法默認不可枚舉线召,是為了避免 for...in 循環(huán)拿到原型身上屬性
            descriptor.enumerable = descriptor.enumerable || false
            // 指定當前方法默認是可配置的,因為添加到原型上的方法均是可以修改和刪除的
            descriptor.configurable = true
            // 指定當前方法默認是可重寫多矮,因為自定義的方法可以修改
            if (descriptor.hasOwnProperty('value')) descriptor.writable = true
            // 添加方法到原型或者構(gòu)造函數(shù)本身
            Object.defineProperty(target, descriptor.key, descriptor)
        }
    }
    return function (constructor, protoProps, staticProps) {
        // 原型上添加方法
        if (protoProps) defineProperties(constructor.prototype, protoProps)
        // 構(gòu)造函數(shù)自身添加靜態(tài)方法
        if (staticProps) defineProperties(constructor, staticProps)
        return constructor
    }
})()

// 03-對類往原型以及本身上面添加方法和實例化過程的模擬
var person = (function () {
    // 04-構(gòu)造函數(shù)
    function Human(home) {
        _classCallCheck(this, Human)
        this.home = home
    }
    // 執(zhí)行添加方法的函數(shù)缓淹;并且第二個參數(shù)默認會有一個 key = constructor 的配置對象;
    _createClass(
        Human,
        // 原型方法
        [{
            key: 'constructor',
            value: Human
        }, {
            key: 'say',
            value: function say() {
                return 'I come from ' + this.home
            }
        }],
        // 靜態(tài)方法
        [{
            key: 'play',
            value: function drink() {
                return `human had better drink everyday`
            }
        }]
    )
    var object = {}
    setPrototypeOf(object, Human.prototype)
    Human.apply(object, arguments)
    return object
})('Mars')
  • 可以看出 ES6 中對于原型方法和靜態(tài)方法的處理更加完善了塔逃,因為無論是原型還是靜態(tài)方法讯壶,都將是不可枚舉的,這在你使用 for...in 運算符的時候不需要考慮如何避免查找出原型上面的方法湾盗,但在 ES5 中你需要顯示的調(diào)用 Object.defineProperty() 方法來設置屬性或者方法是不可枚舉的伏蚊,因為通過 點語法 添加的屬性和方法都是可枚舉的
ES6的繼承實現(xiàn)
  • ES6 的繼承和 ES5 繼承并沒有區(qū)別,只是做了一層封裝格粪,讓整個繼承看起來更加清晰躏吊,需要注意的是,作為子類帐萎,其實有兩條原型鏈比伏,分別是 subClass.__proto__subClass.prototype,原因也很好理解
    子類原型鏈.png
  1. 當子類作為對象的時候吓肋,子類原型是父類: subClass.__proto__ = superClass
  2. 當子類作為構(gòu)造函數(shù)的時候凳怨,子類的原型是父類原型的實例:subClass.prototype.__proto__ = superClass.prototype
  3. 這點很重要,因為在 ES6 繼承中有個 extends 關鍵字是鬼,就是用來確定子類的兩條原型鏈肤舞,這個過程也可以來模擬;
// ES6 的繼承
class Human {
    constructor(home) {
        this.home = home
    }
    say() {
        return `I come from ${this.home}`
    }
}
class Person extends Human {}

// 下面是模擬繼承過程
'use strict'
// 是否使用 new 操作符
function _classCallCheck(instance, constructor) {
    if (!(instance instanceof constructor)) {
        throw new TypeError(`Class constructor ${constructor.name} cannot be invoked without 'new'`)
    }
}
// 類型檢測
function _typeCheck(placeholder, dataType) {
    var _toString = Object.prototype.toString
    if (placeholder) {
        if (_toString.call(dataType) !== '[object Function]' && _toString.call(dataType) !== '[object Null]')
            throw new TypeError(`Class extends value ${dataType} is not a constructor or null`)
    } else {
        if (_toString.call(dataType) === '[object Function]' || _toString.call(dataType) === '[object Object]')
            return true
    }
}
// 拓展構(gòu)造函數(shù)
var __createClass = (function () {
    function defineProperties(target, props) {
        for (var i = 0, length = props.length; i < props; i++) {
            var descriptor = props[i]
            descriptor.enumerable = descriptor.enumerable || false
            descriptor.configurable = true
            if (descriptor.hasOwnProperty('value')) descriptor.writable = true
            Object.defineProperty(target, descriptor.key, descriptor)
        }
    }
    return function (constructor, protoProps, staticProps) {
        if (protoProps) defineProperties(constructor.prototype, protoProps)
        if (staticProps) defineProperties(constructor, staticProps)
        return constructor
    }
})()

// 子類繼承父類方法
function _inheriteMethods(subClass, superClass) {
    // 檢測父類類型
    _typeCheck(subClass, superClass)
    // 排除父類為 null均蜜,并修正 constructor 指向李剖,繼承原型方法
    subClass.prototype = Object.create(superClass && superClass.prototype, {
        constructor: {
            value: subClass,
            enumerable: false,
            configurable: true,
            writable: true
        }
    })
    // 排除父類為 null, 繼承父類靜態(tài)方法
    if (superClass) {
        Object.setPrototypeOf ? 
        Object.setPrototypeOf(subClass, superClass)
        : 
        (subClass.__proto__ = superClass)
    }
}

// 繼承父類屬性
function _inheriteProps(instance, constructorToInstance) {
    // 確保父類創(chuàng)建出 this 之后囤耳,子類才能使用 this
    if (!instance) {
        throw new ReferenceError(`this hasn't been initialised - super() hasn't been called`)
    }
    // 在確定父類不是 null 的時候返回繼承父類屬性的子類實例篙顺,否則返回一個由子類創(chuàng)建的一個空實例
    return constructorToInstance && _typeCheck(null, constructorToInstance) ?
           constructorToInstance 
           :
           instance
}

// 創(chuàng)建父類
var Human = (function () {
    function Human(home) {
        _classCallCheck(this, Human)
        this.home = home
    }
    __createClass(Human, [{
        key: 'say',
        value: function say() {
            return "I come from" + this.home
        }
    }])
    return Human
})()

// 創(chuàng)建子類
var Person = (function () {
    // 原型鏈繼承
    _inheriteMethods.call(null, Person, arguments[0])
    // 構(gòu)造函數(shù)
    function Person() {
        _classCallCheck(this, Person)
        // 這步是對通過父類還是子類創(chuàng)建實例的判斷偶芍,取決于 Person 的父類是否為 null,
        // 如果不為 null德玫,Person.__proto__ = Human
        // 如果為 null匪蟀,Person.__proto__ = Function.prototype,
        // 調(diào)用 apply 返回值的 undefined,最終返回由子類創(chuàng)建的空對象
        return _inheriteProps(this, (Person.__proto__ || Object.getPrototypeOf(Person)).apply(this,
            arguments))
    }
    return Person
})(Human)

// 以上就是對子類如何繼承父類屬性和方法的完整實現(xiàn)過程
寫在最后
  • 上述對于 ES6 實例化宰僧、繼承過程的實現(xiàn)是基于 babel官網(wǎng)轉(zhuǎn)換之后材彪,修改了一些代碼得來的,如果你覺得意猶未盡琴儿,自己可以嘗試一下段化。
  • 本文為原創(chuàng)文章,如果需要轉(zhuǎn)載造成,請注明出處显熏,方便溯源,如有錯誤地方晒屎,可以在下方留言喘蟆,歡迎校勘夷磕。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末履肃,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子坐桩,更是在濱河造成了極大的恐慌尺棋,老刑警劉巖,帶你破解...
    沈念sama閱讀 223,002評論 6 519
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件绵跷,死亡現(xiàn)場離奇詭異膘螟,居然都是意外死亡,警方通過查閱死者的電腦和手機碾局,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,357評論 3 400
  • 文/潘曉璐 我一進店門荆残,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人净当,你說我怎么就攤上這事内斯。” “怎么了像啼?”我有些...
    開封第一講書人閱讀 169,787評論 0 365
  • 文/不壞的土叔 我叫張陵俘闯,是天一觀的道長。 經(jīng)常有香客問我忽冻,道長真朗,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,237評論 1 300
  • 正文 為了忘掉前任僧诚,我火速辦了婚禮遮婶,結(jié)果婚禮上蝗碎,老公的妹妹穿的比我還像新娘。我一直安慰自己旗扑,他們只是感情好蹦骑,可當我...
    茶點故事閱讀 69,237評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著肩豁,像睡著了一般脊串。 火紅的嫁衣襯著肌膚如雪辫呻。 梳的紋絲不亂的頭發(fā)上清钥,一...
    開封第一講書人閱讀 52,821評論 1 314
  • 那天,我揣著相機與錄音放闺,去河邊找鬼祟昭。 笑死,一個胖子當著我的面吹牛怖侦,可吹牛的內(nèi)容都是我干的篡悟。 我是一名探鬼主播,決...
    沈念sama閱讀 41,236評論 3 424
  • 文/蒼蘭香墨 我猛地睜開眼匾寝,長吁一口氣:“原來是場噩夢啊……” “哼搬葬!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起艳悔,我...
    開封第一講書人閱讀 40,196評論 0 277
  • 序言:老撾萬榮一對情侶失蹤急凰,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后猜年,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體抡锈,經(jīng)...
    沈念sama閱讀 46,716評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,794評論 3 343
  • 正文 我和宋清朗相戀三年乔外,在試婚紗的時候發(fā)現(xiàn)自己被綠了床三。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,928評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡杨幼,死狀恐怖撇簿,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情差购,我是刑警寧澤四瘫,帶...
    沈念sama閱讀 36,583評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站歹撒,受9級特大地震影響莲组,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜暖夭,卻給世界環(huán)境...
    茶點故事閱讀 42,264評論 3 336
  • 文/蒙蒙 一锹杈、第九天 我趴在偏房一處隱蔽的房頂上張望撵孤。 院中可真熱鬧,春花似錦竭望、人聲如沸邪码。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,755評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽闭专。三九已至,卻和暖如春旧烧,著一層夾襖步出監(jiān)牢的瞬間影钉,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,869評論 1 274
  • 我被黑心中介騙來泰國打工掘剪, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留平委,地道東北人。 一個月前我還...
    沈念sama閱讀 49,378評論 3 379
  • 正文 我出身青樓夺谁,卻偏偏與公主長得像廉赔,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子匾鸥,可洞房花燭夜當晚...
    茶點故事閱讀 45,937評論 2 361

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

  • class的基本用法 概述 JavaScript語言的傳統(tǒng)方法是通過構(gòu)造函數(shù)蜡塌,定義并生成新對象。下面是一個例子: ...
    呼呼哥閱讀 4,103評論 3 11
  • 在ES5繼承的實現(xiàn)非常有趣的勿负,由于沒有傳統(tǒng)面向?qū)ο箢惖母拍盍蟀琂avascript利用原型鏈的特性來實現(xiàn)繼承,這其中...
    Daguo閱讀 25,916評論 10 44
  • 本文先對es6發(fā)布之前javascript各種繼承實現(xiàn)方式進行深入的分析比較笆环,然后再介紹es6中對類繼承的支持以及...
    lazydu閱讀 16,695評論 7 44
  • 基本語法 簡介 JavaScript語言中,生成實例對象的傳統(tǒng)方法是通過構(gòu)造函數(shù). ES6提供更接近傳統(tǒng)語言的寫法...
    JarvanZ閱讀 881評論 0 0
  • 三千難拔去攒至,六欲又橫生。 故趣何時伴躁劣,煮酒到酉征迫吐。
    風雪長閱讀 190評論 1 1