ES6的Class类继承, super关键字用法即注意点

文章目录

  • Class 的继承
    • 1.0 简介
    • 2.0 Object.getPrototypeOf()
    • 3. super 关键字
    • 4.0 类的 prototype属性和__proto__属性
      • 4.1 实例的 `__proto__` 属性
  • 总结:

Class 的继承

1.0 简介

Class 可以通过 extends 关键字实现继承, 这边 ES5通过原型链实现继承, 要清晰和方便很多

class Point {
}

class ColorPoint extends Point {
}

上面代码定义了一个ColorPoint类,该类通过extends关键字,继承了Point类的所有属性和方法。但是由于没有部署任何代码,所以这两个类完全一样,等于复制了一个Point类。下面,我们在ColorPoint内部加上代码。

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

  toString() {
    return this.color + ' ' + super.toString(); // 调用父类的toString()
  }
}

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

class Point { /* ... */ }

class ColorPoint extends Point {
  constructor() {
  }
}

let cp = new ColorPoint(); // ReferenceError

上面代码中,ColorPoint继承了父类Point,但是它的构造函数没有调用super方法,导致新建实例时报错。

ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。

ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this

如果子类没有定义constructor方法,这个方法会被默认添加,代码如下。也就是说,不管有没有显式定义,任何一个子类都有constructor方法。

class ColorPoint extends Point {
}

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

另一个需要注意的地方是,在子类的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错。这是因为子类实例的构建,基于父类实例,只有super方法才能调用父类实例。

也就是说, super() 方法必须在 constructor 方法体内的最上面调用

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 Animal {
    eat = this.eat // 父类的类上的方法(也就是父类的原型上的方法)不会被继承, 除非添加到实例 this上
    constructor(name, age, sex) {
        this.name = name
        this.age = age
        this.sex = sex
    }

    sayHello() { // 父类的类上的普通方法不能被继承
        console.log('父类的类上的普通方法sayHello...');
    }
    eat(){
        console.log('父类类上的普通方法, 显示添加到实例原型this 上 eat...');
    }
    static staticFn(){
        console.log('父类的静态方法,可以被继承,但是只能被子类调用, 不能被子类实例调用 staticFn...');
    }
   
}

class Dog extends Animal {
    constructor(name, age, sex, color) {
        super(name, age, sex)
        this.color = color
    }
    sayHi() {
        console.log('-------- Dog类', this)
    }
}

const dog = new Dog('d', 12, 'b', 'yellow')
dog.sayHi()
dog.eat() // 父类的方法添加到 this 实例上之后可以被继承
Dog.staticFn() // 父类的静态方法可以被继承, 但是只能子类的类调用
dog.sayHello() // 父类的类上普通方法能被继承, 但是不在子类 this实例上

2.0 Object.getPrototypeOf()

Object.getPrototypeOf 方法可以用来从子类获取父类

class Animal {
    constructor(name, age, sex) {
        this.name = name
    }

    sayHello() { // 父类的类上的普通方法不能被继承
        console.log('父类的类上的普通方法sayHello...');
    }
}

class Dog extends Animal {
    constructor(name, age, sex, color) {
        super(name, age, sex)
        this.color = color
    }
}

# console.log(Object.getPrototypeOf(Dog) === Animal); // true

因此,可以使用这个方法判断,一个类是否继承了另一个类。

3. super 关键字

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

第一种情况: ==super 作为函数调用时, 代表父类的构造函数==ES6要求, 子类的构造函数必须执行一次 super 函数

class A {}

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

上面代码中,子类B的构造函数之中的super(),代表调用父类的构造函数。这是必须的,否则 JavaScript 引擎会报错。
注意,super虽然代表了父类A的构造函数,但是返回的是子类B的实例,即super内部的this指的是B的实例,因此super()在这里相当于

``A.prototype.constructor.call(this)

super()内部的this指向的是B

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

class A {}

class B extends A {
  m() {
    super(); // 报错
  }
}

上面代码中,super()用在B类的m方法之中,就会造成语法错误。

第二种情况: super 作为对象时.

  1. 在普通方法中, 指向父类的原型对象.
  2. 在静态方法中, 指向父类.
class Animal {
    constructor() {
    }
    sayHiParent(){
        console.log('我是父类的普通方法... ...');
    }
}

class Dog extends Animal {
    constructor() {
        super()
    }
   sayHi(){
        super.sayHiParent() // 我是父类的普通方法
       Animal.prototype.sayHiParent() // // 我是父类的普通方法
   }
}
const dog = new Dog()
dog.sayHi()

上面代码中, 子类 Dog 当中的 super.sayHiParent(), 就是将 super 当做一个对象使用, 此时, super 在普通方法之中, 指向A.prototype, , 所以super.sayHiParent() 就相当于 Animal.prototype.sayHiParent()

这里需要注意: 由于 super 指向父类的原型对象, 所以定义在父类实例上的方法或属性, 是无法通过 super 调用的.

class Animal {
    constructor() {
        this.name = 'super实例上的属性'
    }
}

class Dog extends Animal {
   sayHi(){
       return super.name
   }
}
const dog = new Dog()
console.log(dog.sayHi()); // undefined 所以定义在父类实例上的属性, 无法通过super调用

在上面代码中 name 是父类 Animal 实例的属性, super.name 就引用不到它.

如果属性定义在父类的原型对象上, super 就可以取到

class Animal {

}

Animal.prototype.name = 'super实例上的属性'

class Dog extends Animal {
    sayHi() {
        return super.name
    }
}

const dog = new Dog()
console.log(dog.sayHi()); // super实例上的属性

上面代码中, 属性 name 是定义在 Animal.prototype 上面上的, 所以 super.name 可以取到它的值.

ES6 规定, 在子类普通方法中, 通过 super 调用父类的方法时, 方法内部的 this 指向当前子类的实例.

class A {
  constructor() {
    this.x = 1;
  }
  print() {
    console.log(this.x);
  }
}

class B extends A {
  constructor() {
    super();
    this.x = 2;
  }
  m() {
    super.print();
  }
}

let b = new B();
b.m() // 2

上面代码中,super.print()虽然调用的是A.prototype.print(),但是A.prototype.print()内部的this指向子类B的实例,导致输出的是2,而不是1。也就是说,实际上执行的是super.print.call(this)

由于this指向子类实例,所以如果通过super对某个属性赋值,这时super就是this,赋值的属性会变成子类实例的属性。

class A {
  constructor() {
    this.x = 1;
  }
}

class B extends A {
  constructor() {
    super();
    this.x = 2;
    super.x = 3;
    console.log(super.x); // undefined
    console.log(this.x); // 3
  }
}

let b = new B();

上面代码中,super.x赋值为3,这时等同于对this.x赋值为3。而当读取super.x的时候,读的是A.prototype.x,所以返回undefined

如果 super 作为对象, 用在静态方法之中, 这时super将指向父类 , 而不是父类的原型对象.

class Parent {
    static myMethod(msg) {
        console.log('static', msg);
    }

    myMethod(msg) {
        console.log('instance', msg);
    }
}

class Child extends Parent {
    static myMethod(msg) {
        super.myMethod(msg); // super在静态方法中指向父类, 而不是父类的原型
    }

    myMethod(msg) {
        super.myMethod(msg); // super在普通方法中指向父类的原型
    }
}

Child.myMethod(1); // static 1

var child = new Child();
child.myMethod(2); // instance 2

上面代码中,super在静态方法之中指向父类,在普通方法之中指向父类的原型对象。

另外,**在子类的静态方法中通过super调用父类的方法时,方法内部的this指向当前的子类,**而不是子类的实例。

class A {
  constructor() {
    this.x = 1;
  }
  static print() {
    console.log(this.x);
  }
}

class B extends A {
  constructor() {
    super();
    this.x = 2;
  }
  static m() {
    super.print();
  }
}

B.x = 3;
B.m() // 3

上面代码中,静态方法B.m里面,super.print指向父类的静态方法。这个方法里面的this指向的是B,而不是B的实例

注意:

使用super的时候,必须显式指定是作为函数、还是作为对象使用,否则会报错。

class A {}

class B extends A {
  constructor() {
    super();
    console.log(super); // 报错
  }
}

上面代码中,console.log(super)当中的super,无法看出是作为函数使用,还是作为对象使用,所以 JavaScript 引擎解析代码的时候就会报错。这时,如果能清晰地表明super的数据类型,就不会报错。

class A {}

class B extends A {
  constructor() {
    super();
    console.log(super.valueOf() instanceof B); // true
  }
}

let b = new B();

上面代码中,super.valueOf()表明super是一个对象,因此就不会报错。同时,由于super使得this指向B的实例,所以super.valueOf()返回的是一个B的实例。

4.0 类的 prototype属性和__proto__属性

大多数浏览器的 ES5 实现之中, 每一个对象都有__proto__属性, 指向对一个的构造函数的prototype属性, Class 作为构造函数的语法糖, 同时有prototype属性和proto 属性, 因此同时存在两条继承链

  1. 子类的 __proto__ 属性, 表示构造函数的继承, 总是指向父类.
  2. 子类的 prototype 属性的__proto__ 属性, 表示方法的继承, 总是指向父类的 prototype 属性.
class A {
}

class B extends A {
}

B.__proto__ === A // true
B.prototype.__proto__ === A.prototype // true

上面代码中,子类B__proto__属性指向父类A,子类Bprototype属性的__proto__属性指向父类Aprototype属性。

这样的结果是因为,类的继承是按照下面的模式实现的。

class A {
}

class B {
}

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

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

const b = new B();

这两条继承链,可以这样理解:

  • 作为一个对象,子类(B)的原型(__proto__属性)是父类(A);
  • 作为一个构造函数,子类(B)的原型对象(prototype属性)是父类的原型对象(prototype属性)的实例。

4.1 实例的 __proto__ 属性

子类实例的 __proto__ 属性的 __proto__属性, 指向父类实例的__proto__ 属性, 也就是说, 子类的原型的原型, 就是父类的原型.

var p1 = new Point(2, 3);
var p2 = new ColorPoint(2, 3, 'red');

p2.__proto__ === p1.__proto__ // false
p2.__proto__.__proto__ === p1.__proto__ // true

总结:

上一章: ES6类Class 的基本语法, 静态方法, 实例属性新写法

下一章:

交流学习添加微信(备注技术交流学习): Gene199302
ES6的Class类继承, super关键字用法即注意点_第1张图片

该博客为学习阮一峰 ES6入门课所做的笔记记录, 仅用来留作笔记记录和学习理解

你可能感兴趣的:(ES6)