/ ECMAScript6

ECMAScript6 之 Class 继承

标签: ECMAScript 6 javaScript

简介

ES6 规定了 extends 关键字,用该关键字就可以实现继承,对比 ES5 通过修改原型链来实现继承,可以说简洁了不少。

class Point {
}

class ColorPoint extends Point {
    constructor(x, y, color) {
        // 调用父类的constructor(x, y)
        super(x, y)
        this.color = color
    }
    
    toString() {
        // 调用父类的toString()
        return this.color + ' ' + super.toString()
    }
}

当然子类如果没有定义 constructor 方法,这个方法会被默认添加。

class ColorPoint extends Point {
}

// 等同于
class ColorPoint extends Point {
    constructor(...args) {
        super(...args)
    }
}

子类必须在 constructor 方法中调用 super 方法,否则新建实例时会报错。这是因为子类自己的 this 对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用 super 方法,子类就得不到 this 对象。

如上所述,如果不调用 super 方法,子类就得不到 this 对象。那么如果在调用 super 方法前使用 this 对象,那么就会报错。

class Point {
    constructor(x, y) {
        this.x = x
        this.y = y
    }
}

class ColorPoint extends Point {
    constructor(x, y, color) {
        this.color = color // ReferenceError
        super(x, y)
        this.color = color // 正确
    }
}

最后父类中的静态方法,同样也会被子类继承。

class A {
    static hello() {
        console.log('hello world')
    }
}

class B extends A {
}

B.hello()  // hello world

getPrototypeOf

用于从子类上获取父类

Object.getPrototypeOf(ColorPoint) === Point
// true

super 关键字

super 这个关键字,既可以当作函数使用,也可以当作对象使用。在这两种情况下,它的用法完全不同。

函数调用

super 作为函数调用时,代表父类的构造函数。
而且作为子类必须调用 super 函数,但是需要注意的是虽然是父类的构造函数,但返回的是子类的实例。

class A {}

class B extends A {
    constructor() {
        super()
    }
}

super 虽然代表了父类 A 的构造函数,但是返回的是子类 B 的实例,即 super 内部的 this 指的是 B ,因此 super() 在这里相当于 A.prototype.constructor.call(this)

作为函数时, super() 只能用在子类的构造函数之中,用在其他地方就会报错。

作为对象使用

super 作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。

  • 在方法中使用
class A {
    p() {
        return 2;
    }
}

class B extends A {
    constructor() {
        super();
        console.log(super.p())
        // 2
    }
}

let b = new B();
  • 在静态方法中使用
class A {
    static p1() {
        return 2
    }
}

class B extends A {
    static p2() {
        super.p1()
        // 2
    }
}

super 当成对象使用,如果在子类方法中使用,那么 super 的指向为父类的 prototype ,如果在子类的静态方法中使用则指向父类。

进一步理解:子类方法其实也是在子类的 prototype 下,所以对应的关系为:在子类的 prototype 中使用 super 那么就是指向父类的 prototype ,在子类下(也就是静态方法)使用 super 那么就是指向父类。

所以 super 的表现和 this 是差不多一致的,唯一不同的是 super 是针对父类的引用。

还需要注意一点的是:如果通过 super 调用父类的方法时,方法内部的 this 是指向子类的(不论是不是在静态方法中使用)。

class Parent {
    static staticMethod() {
        console.log(this.staticType)
    }
    
    constructor() {
        this.type = 'parent'
    }

    method() {
        console.log(this.type)
    }
}
Parent.staticType = 'parent static'

class Child extends Parent {
    static staticMethod2() {
        super.staticMethod()
    }
    
    constructor() {
        super()
        this.type = 'child'
    }

    method2() {
        super.method()
    }
}
Child.staticType = 'child static'

Child.staticMethod2()
// child static

var child = new Child()
child.method2()
// child

__proto__

ES5 中每一个对象都拥有 __proto__ 属性,指向对应构造函数的 prototype 属性。通过 __proto__ 可以实现对象的继承。

ES6 中,出现了类层面上的继承,因此规定了另外一条继承链,构造函数的继承。

class A {
}

class B extends A {
}

// ES6 中新出现的构造函数的继承
B.__proto__ === A // true

// 与 ES5 一致,原型链的继承
B.prototype.__proto__ === A.prototype // true

因此在 ES6 中,__proto__ 即可以用来表示原型链的继承关系,也可以用来表示构造函数的继承关系。

当然出现这条继承链的原因是因为 ES6 中继承是按照下面的模式实现的。

class A {
}

class B {
}

// B 的实例继承 A 的实例
Object.setPrototypeOf(B.prototype, A.prototype)

// B 继承 A 的静态属性
Object.setPrototypeOf(B, A)

const b = new B()

setPrototypeOf 是这样实现的。

Object.setPrototypeOf = function (obj, proto) {
    obj.__proto__ = proto
    return obj
}

所以当 B 继承 A 的静态属性时,就会顺便在 B 类下设置一个 __proto__

当然如果一个类没有继承任何的类那么 __proto__ 会指向哪?

class A {
}

A.__proto__ === Function.prototype // true
A.prototype.__proto__ === Object.prototype // true

这种情况下, A 作为一个基类(即不存在任何继承),就是一个普通函数,所以直接继承 Function.prototype 。但是, A 调用后返回一个空对象(即 Object 实例),所以 A.prototype.__proto__ 指向构造函数( Object )的 prototype 属性。

原生构造函数的继承

目前常用的 ECMAScript 原生构造函数大致如下

  • Boolean()
  • Number()
  • String()
  • Array()
  • Date()
  • Function()
  • RegExp()
  • Error()
  • Object()

ES5 ,这些函数是无法被继承的

function MyArray() {
    Array.apply(this, arguments)
}

MyArray.prototype = Object.create(Array.prototype, {
    constructor: {
        value: MyArray,
        writable: true,
        configurable: true,
        enumerable: true
    }
})

上述代码生成了一个继承的 MyArray 类,但这个类的行为与 Array 完全不一致

var colors = new MyArray();
colors[0] = "red";
colors.length  // 0

colors.length = 0;
colors[0]  // "red"

之所以会发生这种情况,是因为子类无法获得原生构造函数的内部属性,通过 Array.apply() 或者分配给原型对象都不行。原生构造函数会忽略 apply 方法传入的 this ,也就是说,原生构造函数的 this 无法绑定,导致拿不到内部属性。

照成这样的根本原因是 ES5 创建实例时,先由子类生成 this 对象,在用这个对象去添加父类下的方法,而父类的内部属性仅仅只能在父类生成的 this 对象下使用,这样就会导致子类生成的 this 对象访问不到父类的内部属性,导致子类的行为异常。

而在 ES6 中,构造函数生成实例时,是先由父类生成 this 对象,然后用子类的构造函数去修改/添加 this 对象下的方法或属性,这样就使得 this 对象可以获得父类的所有行为。以下例子可以说明

class MyArray extends Array {
    constructor(...args) {
        super(...args)
    }
}

var arr = new MyArray()
arr[0] = 12
arr.length
// 1

arr.length = 0
arr[0]
// undefined

上述代码就完成了对原生对象的继承。

可以定义一个 Error 的子类,指定相应的报错信息

class ExtendableError extends Error {
    constructor(message) {
        super()
        this.message = message
        this.stack = (new Error()).stack
        this.name = this.constructor.name
    }
}

class MyError extends ExtendableError {
    constructor(m) {
        super(m)
    }
}

var myerror = new MyError('ll')
myerror.message // "ll"
myerror instanceof Error // true
myerror.name // "MyError"
myerror.stack
// Error
//     at MyError.ExtendableError
//     ...

继承 Object 构造函数时需要注意

class NewObj extends Object{
  constructor(){
    super(...arguments)
  }
}
var o = new NewObj({attr: true})
o.attr === true  // false

上面代码中,NewObj 继承了 Object ,但是无法通过 super 方法向父类 Object 传参。这是因为 ES6 改变了 Object 构造函数的行为,一旦发现 Object 方法不是通过 new Object() 这种形式调用,ES6 规定 Object 构造函数会忽略参数。

注: 上面的例子大多数来自《ECMAScript 6入门》这本书。