ES5 与 ES6继承

写在前面

之所以想写这个,是想为以后学习 react 做个铺垫,每一个看似理所当然的结果,实际上推敲过程很耐人寻味,就从 ES6 类的实例化和继承开始,但是 ES6 的实现是在 ES5 的基础上,所以需要对 ES5 中关于 构造函数继承这些梳理清晰。

关于 new 关键字
  • 无论 ES6 还是 ES5 中对于自定义对象的实现都不开 new 这个关键词,所以需要搞清楚它在构造函数实例化过程中的作用,因为透过它你能看清楚构造函数内部 this 指向的问题.
  • 有时候文字解释会让读者对一个概念产生各种想象,所以我比较偏好尝试用代码来解释自己难以理解的地方,代码如下:
// ES5 中构造函数创建实例过程
function Person (name, age) {
    this.name = name
    this.age = age
}
// 返回一个具有 name = ww, age = 18 的 person 对象
var person = new Person('ww', 19)

// 那么 new 关键字做了哪些工作
// 下面是对使用 new 关键字配合构造函数创建对象的过程模拟
var person = (function() {
    function Person (name, age) {
        this.name = name
        this.age = age
    }
    // 创建一个隐式空对象
    var object = {}
    // 修正隐式对象的原型
    setPrototypeOf(object, Person.prototype)
    // 执行构造函数,为空对象赋值
    Person.apply(object, arguments)
    // 返回隐式对象
    return object
})('www', 90)
function setPrototypeOf(object, proto) {
    Object.setPrototypeOf ? 
    Object.setPrototypeOf(object, proto) 
    : 
    object.__proto__ = proto
}
// 返回的 person 我们通常会称之为 Person 的一个实例。
  • 上述就是对 new 在构造函数实例化中的作用的描述,这里需要注意的一点是,在 ES5 中,如果没有使用 new 关键字,那么 Person 只是作为 window 的一个普通方法调用,所以也不会报错,但是在 ES6 的 类 的实例化中必须使用 new ,否则会报错,在解读 ES6 类的实例化会解释报错的原因。
ES5中继承的实现
  • ES5 中实现继承的方式比较多, W3C 标准推荐的做法是有三种ECMAScript 继承机制实现,下面直接上代码来阐述 ES5 实现继承的过程,以及优缺点
  1. 第一种是通过传统的原型链继承方式,比较好理解:自身若有被读取的属性或者方法的时候,自取所需,如果没有,就沿着原型链一层一层往上找,直到找到或者找不到返回 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-子类的原型对象指向父类实例,实现继承
Person.prototype = new Human('Earth')
var man = new Person('man')
// 调用实例身上的方法,由于没有,沿着原型链往上找,
// 找到的是父类原型对象上面的 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-调用子类两个实例的 say() 方法
man.say()        // 返回结果:I'm a man, I come from Mars
female.say()     // 返回结果:I'm a female, I come from Mars

// 别人本来是来自地球,你随便动动手指,让人家诞生在火星了,多少有点说不过去
// 基于这样的特点,只要有任何一个子类实例修改了原型对象上的属性或者父类实例自身修改了属性
// 将会影响所有继承它的子类,这个不是我们愿意看到的
// 所以就有了第二种方式:call 或者 apply 实现继承
  1. 通过 call 或者 apply 实现继承和 对象冒充 继承很相似,但是有所不同,根本原因是 this 总是指向函数运行时所在的那个对象,下面是 call 方法实现继承过程
// 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-实现借用父类创建子类自身的属性,这是最重要的一点
    // 如果你理解了 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-但是会发现调用子类两个实例的 say() 方法,会报错,因为原型链上没有这个方法
// 扔出错误:man.say is not a function
man.say() 
female.say() 

// call 或者 apply 方法实现继承的最大优势就是能够实现属性私有化,但是劣势就是没有办法继承
// 父类原型对象上面的方法,所以为了解决原型链继承和call方法继承的缺点,将两者的优点糅合在一起
// 即混合继承,能够实现完美的继承
  1. 混合继承,即通过 call 或者 apply 实现属性继承,原型链实现方法继承
// 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-调用子类两个实例的 say() 方法,返回的 home 都是当前实例自身的 home 属性
// 而不是原型链上的 home ,因为当前实例自身有该属性,就不再往原型链上找了
// 所以通过 call 和 原型链能够实现完美继承
man.say()     // 返回结果:I'm a man, I come from Earth
female.say()  // 返回结果:I'm a female, I come from Mars
ES6类实例化中的new
  • 之所以会说这么多 ES5 的继承,是因为 ES6 的继承实现都是基于前者,可以看做是前者的语法糖,只有在理解基础的前提下,才能谈进阶
  • 关于 ES6class 的一些基础知识就不提了,现在来说说 class 声明的变量在实例化过程中为什么必须使用 new 关键词
// 01-声明一个类
class Human {
    constructor(home) {
        this.home = home
    }
}
// 02-虽然 Human 的本质是一个 function,但是在没有 new 的情况下调用类 js 引擎会抛出错误
// Class constructor Human cannot be invoked without 'new',这点跟 ES5 是不同的
const man = Human('Earth')
// 原因很简单,在 class 声明的变量内部,默认开启的是 严格模式,所以如果没有使用 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类实例化过程中如何添加静态方法和原型方法
  • ES5 一样,ES6 有静态方法和原型方法,两者都可以通过传统的 点语法 来添加静态属性和方法,以及原型方法,但是在 ES6 中将这层添加方式做了一层封装,直接写在类的内部,方法就会添加到原型或者类本身上面了
class Human {
    // constructor 和 say 方法添加到原型上
    constructor(home) {
        this.home = home
    }
    say() {
        return `I come from ${this.home}`
    }
    // 静态方法添加到当前类本身上
    static drink() {
        return `human had better drink everyday`
    }
}
const person = new Human('Mars')


// 上述的实现过程可以通过下面代码模拟

// 开启严格模式
'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-拓展构造函数的方法
var _createClass = (function () {
    // 02-定义一个将方法添加原型或者构造函数本身的方法;
    function defineProperties(target, props) {
        for (var i = 0, length = props.length; i < length; i++) {
            // 获取属性描述符,即 Object.defineProperty(object, key, descriptor) 的第三个参数
            var descriptor = props[i]
            // 指定当前方法默认不可枚举,是为了避免 for...in 循环拿到原型身上属性
            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
    }
})()

// 03-对类往原型以及本身上面添加方法和实例化过程的模拟
var person = (function () {
    // 04-构造函数
    function Human(home) {
        _classCallCheck(this, Human)
        this.home = home
    }
    // 执行添加方法的函数;并且第二个参数默认会有一个 key = constructor 的配置对象;
    _createClass(
        Human,
        // 原型方法
        [{
            key: 'constructor',
            value: Human
        }, {
            key: 'say',
            value: function say() {
                return 'I come from ' + this.home
            }
        }],
        // 静态方法
        [{
            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 中对于原型方法和静态方法的处理更加完善了,因为无论是原型还是静态方法,都将是不可枚举的,这在你使用 for...in 运算符的时候不需要考虑如何避免查找出原型上面的方法,但在 ES5 中你需要显示的调用 Object.defineProperty() 方法来设置属性或者方法是不可枚举的,因为通过 点语法 添加的属性和方法都是可枚举的
ES6的继承实现
  • ES6 的继承和 ES5 继承并没有区别,只是做了一层封装,让整个继承看起来更加清晰,需要注意的是,作为子类,其实有两条原型链,分别是 subClass.__proto__subClass.prototype,原因也很好理解
    子类原型链.png
  1. 当子类作为对象的时候,子类原型是父类: subClass.__proto__ = superClass
  2. 当子类作为构造函数的时候,子类的原型是父类原型的实例: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
    }
}
// 拓展构造函数
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, 继承父类静态方法
    if (superClass) {
        Object.setPrototypeOf ? 
        Object.setPrototypeOf(subClass, superClass)
        : 
        (subClass.__proto__ = superClass)
    }
}

// 继承父类属性
function _inheriteProps(instance, constructorToInstance) {
    // 确保父类创建出 this 之后,子类才能使用 this
    if (!instance) {
        throw new ReferenceError(`this hasn't been initialised - super() hasn't been called`)
    }
    // 在确定父类不是 null 的时候返回继承父类属性的子类实例,否则返回一个由子类创建的一个空实例
    return constructorToInstance && _typeCheck(null, constructorToInstance) ?
           constructorToInstance 
           :
           instance
}

// 创建父类
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
})()

// 创建子类
var Person = (function () {
    // 原型链继承
    _inheriteMethods.call(null, Person, arguments[0])
    // 构造函数
    function Person() {
        _classCallCheck(this, Person)
        // 这步是对通过父类还是子类创建实例的判断,取决于 Person 的父类是否为 null,
        // 如果不为 null,Person.__proto__ = Human
        // 如果为 null,Person.__proto__ = Function.prototype,
        // 调用 apply 返回值的 undefined,最终返回由子类创建的空对象
        return _inheriteProps(this, (Person.__proto__ || Object.getPrototypeOf(Person)).apply(this,
            arguments))
    }
    return Person
})(Human)

// 以上就是对子类如何继承父类属性和方法的完整实现过程
写在最后
  • 上述对于 ES6 实例化、继承过程的实现是基于 babel官网转换之后,修改了一些代码得来的,如果你觉得意犹未尽,自己可以尝试一下。
  • 本文为原创文章,如果需要转载,请注明出处,方便溯源,如有错误地方,可以在下方留言,欢迎校勘。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容

  • class的基本用法 概述 JavaScript语言的传统方法是通过构造函数,定义并生成新对象。下面是一个例子: ...
    呼呼哥阅读 4,067评论 3 11
  • 在ES5继承的实现非常有趣的,由于没有传统面向对象类的概念,Javascript利用原型链的特性来实现继承,这其中...
    Daguo阅读 25,889评论 10 44
  • 本文先对es6发布之前javascript各种继承实现方式进行深入的分析比较,然后再介绍es6中对类继承的支持以及...
    lazydu阅读 16,661评论 7 44
  • 基本语法 简介 JavaScript语言中,生成实例对象的传统方法是通过构造函数. ES6提供更接近传统语言的写法...
    JarvanZ阅读 874评论 0 0
  • 三千难拔去,六欲又横生。 故趣何时伴,煮酒到酉征。
    风雪长阅读 183评论 1 1