ES5 與 ES6繼承

寫在前面

之所以想寫這個(gè),是想為以后學(xué)習(xí) react 做個(gè)鋪墊,每一個(gè)看似理所當(dāng)然的結(jié)果,實(shí)際上推敲過程很耐人尋味,就從 ES6 類的實(shí)例化和繼承開始,但是 ES6 的實(shí)現(xiàn)是在 ES5 的基礎(chǔ)上,所以需要對 ES5 中關(guān)于 構(gòu)造函數(shù)繼承這些梳理清晰。

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

// 那么 new 關(guān)鍵字做了哪些工作
// 下面是對使用 new 關(guān)鍵字配合構(gòu)造函數(shù)創(chuàng)建對象的過程模擬
var person = (function() {
    function Person (name, age) {
        this.name = name
        this.age = age
    }
    // 創(chuàng)建一個(gè)隱式空對象
    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 我們通常會(huì)稱之為 Person 的一個(gè)實(shí)例。
  • 上述就是對 new 在構(gòu)造函數(shù)實(shí)例化中的作用的描述,這里需要注意的一點(diǎn)是,在 ES5 中,如果沒有使用 new 關(guān)鍵字,那么 Person 只是作為 window 的一個(gè)普通方法調(diào)用,所以也不會(huì)報(bào)錯(cuò),但是在 ES6 的 類 的實(shí)例化中必須使用 new ,否則會(huì)報(bào)錯(cuò),在解讀 ES6 類的實(shí)例化會(huì)解釋報(bào)錯(cuò)的原因。
ES5中繼承的實(shí)現(xiàn)
  • ES5 中實(shí)現(xiàn)繼承的方式比較多, W3C 標(biāo)準(zhǔn)推薦的做法是有三種ECMAScript 繼承機(jī)制實(shí)現(xiàn),下面直接上代碼來闡述 ES5 實(shí)現(xiàn)繼承的過程,以及優(yōu)缺點(diǎn)
  1. 第一種是通過傳統(tǒng)的原型鏈繼承方式,比較好理解:自身若有被讀取的屬性或者方法的時(shí)候,自取所需,如果沒有,就沿著原型鏈一層一層往上找,直到找到或者找不到返回 undefined 或者報(bào)錯(cuò)
// 01-定義父類
function Human(home) {
    this.home = home
}
// 02-為父類實(shí)例添加方法
Human.prototype.say = function () {
    return `I'm a ${this.sex}, I come from ${this.home}`
}
// 03-定義一個(gè)子類
function Person(sex) {
    this.sex = sex
}
// 04-子類的原型對象指向父類實(shí)例,實(shí)現(xiàn)繼承
Person.prototype = new Human('Earth')
var man = new Person('man')
// 調(diào)用實(shí)例身上的方法,由于沒有,沿著原型鏈往上找,
// 找到的是父類原型對象上面的 say() 方法
man.say()   // 返回的是 I'm a man, I come from Earth

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

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

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

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

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

// 07-無論任意子類實(shí)例修改 sex 和 home 屬性,或者 父類實(shí)例修改 home 屬性
// 都不會(huì)影響到其他的子類實(shí)例,因?yàn)閷傩源丝趟接谢?
// 08-但是會(huì)發(fā)現(xiàn)調(diào)用子類兩個(gè)實(shí)例的 say() 方法,會(huì)報(bào)錯(cuò),因?yàn)樵玩溕蠜]有這個(gè)方法
// 扔出錯(cuò)誤:man.say is not a function
man.say() 
female.say() 

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

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

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

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

// 09-調(diào)用子類兩個(gè)實(shí)例的 say() 方法,返回的 home 都是當(dāng)前實(shí)例自身的 home 屬性
// 而不是原型鏈上的 home ,因?yàn)楫?dāng)前實(shí)例自身有該屬性,就不再往原型鏈上找了
// 所以通過 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類實(shí)例化中的new
  • 之所以會(huì)說這么多 ES5 的繼承,是因?yàn)?ES6 的繼承實(shí)現(xiàn)都是基于前者,可以看做是前者的語法糖,只有在理解基礎(chǔ)的前提下,才能談進(jìn)階
  • 關(guān)于 ES6class 的一些基礎(chǔ)知識就不提了,現(xiàn)在來說說 class 聲明的變量在實(shí)例化過程中為什么必須使用 new 關(guān)鍵詞
// 01-聲明一個(gè)類
class Human {
    constructor(home) {
        this.home = home
    }
}
// 02-雖然 Human 的本質(zhì)是一個(gè) function,但是在沒有 new 的情況下調(diào)用類 js 引擎會(huì)拋出錯(cuò)誤
// Class constructor Human cannot be invoked without 'new',這點(diǎn)跟 ES5 是不同的
const man = Human('Earth')
// 原因很簡單,在 class 聲明的變量內(nèi)部,默認(rèn)開啟的是 嚴(yán)格模式,所以如果沒有使用 new 
// this 的指向是 undefined,對 undefined 任何屬性或者方法的讀寫都是沒有意義的,所以直接丟出錯(cuò)誤

// 03-對 類 進(jìn)行實(shí)例化是否使用 new 的判斷以及實(shí)例化過程模擬

// 開啟嚴(yán)格模式
'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) {
        // 在這一步來判斷當(dāng)前 this 的指向,如果你理解了 new 的作用,
       // 你就會(huì)清楚,當(dāng)前 this 的指向
        _classCallCheck(this, Human)
        this.home = home
    }
    var object = {}
    setPrototypeOf(object, Human.prototype)
    Human.apply(object, arguments)
    return object
})('Earth')
// 以上就解釋了為什么 ES6 必須使用 new,以及如果做判斷的過程
ES6類實(shí)例化過程中如何添加靜態(tài)方法和原型方法
  • ES5 一樣,ES6 有靜態(tài)方法和原型方法,兩者都可以通過傳統(tǒng)的 點(diǎn)語法 來添加靜態(tài)屬性和方法,以及原型方法,但是在 ES6 中將這層添加方式做了一層封裝,直接寫在類的內(nèi)部,方法就會(huì)添加到原型或者類本身上面了
class Human {
    // constructor 和 say 方法添加到原型上
    constructor(home) {
        this.home = home
    }
    say() {
        return `I come from ${this.home}`
    }
    // 靜態(tài)方法添加到當(dāng)前類本身上
    static drink() {
        return `human had better drink everyday`
    }
}
const person = new Human('Mars')


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

// 開啟嚴(yá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è)將方法添加原型或者構(gòu)造函數(shù)本身的方法;
    function defineProperties(target, props) {
        for (var i = 0, length = props.length; i < length; i++) {
            // 獲取屬性描述符,即 Object.defineProperty(object, key, descriptor) 的第三個(gè)參數(shù)
            var descriptor = props[i]
            // 指定當(dāng)前方法默認(rèn)不可枚舉,是為了避免 for...in 循環(huán)拿到原型身上屬性
            descriptor.enumerable = descriptor.enumerable || false
            // 指定當(dāng)前方法默認(rèn)是可配置的,因?yàn)樘砑拥皆蜕系姆椒ň强梢孕薷暮蛣h除的
            descriptor.configurable = true
            // 指定當(dāng)前方法默認(rèn)是可重寫,因?yàn)樽远x的方法可以修改
            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-對類往原型以及本身上面添加方法和實(shí)例化過程的模擬
var person = (function () {
    // 04-構(gòu)造函數(shù)
    function Human(home) {
        _classCallCheck(this, Human)
        this.home = home
    }
    // 執(zhí)行添加方法的函數(shù);并且第二個(gè)參數(shù)默認(rèn)會(huì)有一個(gè) 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)方法的處理更加完善了,因?yàn)闊o論是原型還是靜態(tài)方法,都將是不可枚舉的,這在你使用 for...in 運(yùn)算符的時(shí)候不需要考慮如何避免查找出原型上面的方法,但在 ES5 中你需要顯示的調(diào)用 Object.defineProperty() 方法來設(shè)置屬性或者方法是不可枚舉的,因?yàn)橥ㄟ^ 點(diǎn)語法 添加的屬性和方法都是可枚舉的
ES6的繼承實(shí)現(xiàn)
  • ES6 的繼承和 ES5 繼承并沒有區(qū)別,只是做了一層封裝,讓整個(gè)繼承看起來更加清晰,需要注意的是,作為子類,其實(shí)有兩條原型鏈,分別是 subClass.__proto__subClass.prototype,原因也很好理解
    子類原型鏈.png
  1. 當(dāng)子類作為對象的時(shí)候,子類原型是父類: subClass.__proto__ = superClass
  2. 當(dāng)子類作為構(gòu)造函數(shù)的時(shí)候,子類的原型是父類原型的實(shí)例:subClass.prototype.__proto__ = superClass.prototype
  3. 這點(diǎn)很重要,因?yàn)樵?ES6 繼承中有個(gè) extends 關(guān)鍵字,就是用來確定子類的兩條原型鏈,這個(gè)過程也可以來模擬;
// 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 的時(shí)候返回繼承父類屬性的子類實(shí)例,否則返回一個(gè)由子類創(chuàng)建的一個(gè)空實(shí)例
    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)建實(shí)例的判斷,取決于 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)

// 以上就是對子類如何繼承父類屬性和方法的完整實(shí)現(xiàn)過程
寫在最后
  • 上述對于 ES6 實(shí)例化、繼承過程的實(shí)現(xiàn)是基于 babel官網(wǎng)轉(zhuǎn)換之后,修改了一些代碼得來的,如果你覺得意猶未盡,自己可以嘗試一下。
  • 本文為原創(chuàng)文章,如果需要轉(zhuǎn)載,請注明出處,方便溯源,如有錯(cuò)誤地方,可以在下方留言,歡迎??薄?/li>
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

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

友情鏈接更多精彩內(nèi)容