Shane Jix

TypeScript - ES6 中的 Class 继承

create:November 13, 2021  update:April 12, 2022  ☕️☕️ 12 min read

同步链接: https://www.shanejix.com/posts/TypeScript - ES6 中的 Class 继承/

typescript 中的 class 继承是基于 ES6 中 class 的扩展。因此可以类比 vanillajs 中基于原型的继承和 ES6 中的 class 继承的变化。其实,ES6 中的 class 继承其实就是 vanillajs 的语法糖,但又不仅仅是语法糖。

Class 基本语法

基本的类语法看起来像这样:

class MyClass {

  // 属性; class 字段 prop 会在在每个独立对象中被设好,而不是设在 Myclass.prototype
  prop = value;

  // 属性; class 字段 prop 更优雅的绑定方法
  prop = () => { }

  // 构造器
  constructor(...) { }

  // method
  method(...) { }

  // getter 方法
  get something(...) { }

  // setter 方法
  set something(...) { }

  // 有计算名称(computed name)的方法(此处为 symbol)
  [Symbol.iterator]() { }

}

需要注意:

  • MyClass 是一个函数(提供作为 constructor 的那个)
  • **methodsgetterssettors 都被写入了 MyClass.prototype **
  • prop 每个实例都有一份

ES6 class 中没有直接定义到 prototype 上的属性的实现,可以借助 getter 和 setter 模拟共享属性

// ES6中

class MyClass {
  	_name = 'shanejix'

    get name() {
        return this._name;
    }

    set name(newName) {
        this.name = newName;
    }
}


// ** getter 和 setter 会被写到 MyClass.prototype上 **


// ES5中对应实现

function MyClass(){
  this._name = 'shanejix'
};

MyClass.prototype =  {

    get name() {
        return this._name;
    }

    set name(newName) {
        this.name = newName;
    }
}

Class 继承

扩展一个类:class Child extends Parent

* 在内部,关键字 extends 使用了很好的旧的原型机制进行工作

- 它将 Child.prototype.[[Prototype]] 设置为 Parent.prototype

在 extends 后允许任意表达式:

function f(phrase) {
  return class {
    sayHi() {
      alert(phrase);
    }
  };
}

class User extends f("Hello") {}

new User().sayHi(); // Hello

// 这对于高级编程模式,例如当根据许多条件使用函数生成类,并继承它们时来说可能很有用

有时不希望完全替换父类的方法,而是希望在父类方法的基础上进行调整或扩展其功能

重写一个方法

* 默认情况下,所有未在 class child 中指定的方法均从 class Parent 中直接获取

Class 为此提供了 “super” 关键字:

- 执行 super.method(...) 来调用一个父类方法

- 执行 super(...) 来调用一个父类 constructor(只能在子类的 constructor 中)

补充:箭头函数没有 super 和 this

重写一个 constructor

// 根据 规范,如果一个类扩展了另一个类并且没有 constructor,那么将生成下面这样的 constructor:

class Child extends Parent {
  // 为没有自己的 constructor 的扩展类生成的
  constructor(...args) {
    super(...args);
  }
}

”继承类的 constructor 必须调用 super(…),并且一定要在使用 this 之前调用”

💡 为什么呢?

* 在 JavaScript 中,【继承类的构造函数】(所谓的“派生构造器”,英文为 “derived constructor”)与其他函数之间是有区别的

- 派生构造器具有特殊的内部属性 [[ConstructorKind]]:"derived"; // 这是一个特殊的内部标签


该标签会【影响它的 new 行为】:

  - 当通过 new 执行一个常规函数时,它将创建一个空对象,并将这个空对象赋值给 this ;

  - 但是,当继承的 constructor 执行时,它不会执行此操作;

  - 它期望父类的 constructor 来完成这项工作;


* 因此,派生的 constructor 必须调用 super 才能执行其父类(base)的 constructor,否则 this 指向的那个对象将不会被创建

重写类字段

😨 一个棘手的注意要点;可以重写方法,也可以重写字段:

class Animal {
  name = "animal";

  constructor() {
    alert(this.name); // (*)
  }
}

class Rabbit extends Animal {
  name = "rabbit";
}

new Animal(); // animal
new Rabbit(); // animal

// 两种情况下:new Animal() 和 new Rabbit(),在 (*) 行的 alert 都打印了 animal

// 有点懵逼,用方法来进行比较:

class Animal {
  showName() {
    // 而不是 this.name = 'animal'
    alert("animal");
  }

  constructor() {
    this.showName(); // 而不是 alert(this.name);
  }
}

class Rabbit extends Animal {
  showName() {
    alert("rabbit");
  }
}

new Animal(); // animal
new Rabbit(); // rabbit

// 请注意:这时的输出是不同的

// 这才是本来所期待的结果。当父类构造器在派生的类中被调用时,它会使用被重写的方法;……但对于类字段并非如此。正如前文所述,父类构造器总是使用父类的字段

为什么会有这样的区别呢?

原因在于类字段初始化的顺序:

- 对于基类(还未继承任何东西的那种),在构造函数调用前初始化

- 对于派生类,在 super() 后立刻初始化

”这种字段与方法之间微妙的区别只特定于 JavaScript;这种行为仅在一个被重写的字段被父类构造器使用时才会显现出来;可以通过使用方法或者 getter/setter 替代类字段,来修复这个问题”

静态方法

把一个方法赋值给类的函数本身,而不是赋给它的 "prototype"


这样的方法被称为 静态的(static)
class User {
  static staticMethod() {
    alert(this === User);
  }
}

User.staticMethod(); // true

// 和作为属性赋值的作用相同

class User {}

User.staticMethod = function () {
  alert(this === User);
};

User.staticMethod(); // true
静态方法被用于实现属于整个类的功能;它与具体的类实例无关

静态属性

静态属性类似静态方法
class Article {
  static publisher = "Levi Ding";
}

alert(Article.publisher); // Levi Ding

// 等同于直接给 Article 赋值:

Article.publisher = "Levi Ding";
静态属性被用于想要存储类级别的数据时,而不是绑定到实例

继承静态属性和方法

- 静态属性和方法是可被继承的

- 继承对常规方法和静态方法都有效
class Animal {
  static planet = "Earth";

  constructor(name, speed) {
    this.speed = speed;
    this.name = name;
  }

  run(speed = 0) {
    this.speed += speed;
    alert(`${this.name} runs with speed ${this.speed}.`);
  }

  static compare(animalA, animalB) {
    return animalA.speed - animalB.speed;
  }
}

// 继承于 Animal
class Rabbit extends Animal {
  hide() {
    alert(`${this.name} hides!`);
  }
}

let rabbits = [new Rabbit("White Rabbit", 10), new Rabbit("Black Rabbit", 5)];

rabbits.sort(Rabbit.compare);

rabbits[0].run(); // Black Rabbit runs with speed 5.

alert(Rabbit.planet); // Earth

它是如何工作的?再次,使用原型 😱。extends 让 Rabbit 的 [[Prototype]] 指向了 Animal

Rabbit extends Animal 创建了两个 [[Prototype]] 引用:

- 1. Rabbit 函数原型继承自 Animal 函数

- 2. Rabbit.prototype 原型继承自 Animal.prototype

校验

class Animal {}
class Rabbit extends Animal {}

// 对于静态的
alert(Rabbit.__proto__ === Animal); // true

// 对于常规方法
alert(Rabbit.prototype.__proto__ === Animal.prototype); // true

Babel 编译

之前已经对 ES5 中继承 有了深入的了解,Typescript、ES6 的代码会被编译成什么样子呢?可以在 Babel 官网的Try it out页面查看

es6

class Person {
  constructor(name) {
    this.name = name;
  }
}

编译后

function _classCallCheck(instance, Constructor) {
  if (!_instanceof(instance, Constructor)) {
    throw new TypeError("Cannot call a class as a function");
  }
}

var Person = /*#__PURE__*/ _createClass(function Person(name) {
  _classCallCheck(this, Person);

  this.name = name;
});

_classCallCheck 的作用是检查 Person 是否是通过 new 的方式调用,类必须使用 new 调用,否则会报错。当使用 var person = Person() 的形式调用的时候,this 指向 window,所以 instance instanceof Constructor 就会为 false

这也片面的说明 class 不仅仅是 原型继承的语法糖

es6

class Person {
  constructor(name) {
    this.name = name;
  }

  sayHello() {
    return "hello, I am " + this.name;
  }

  static onlySayHello() {
    return "hello";
  }

  get name() {
    return "shane";
  }

  set name(newName) {
    console.log("new name 为:" + newName);
  }
}

编译后

"use strict";

var _createClass = (function () {
  function defineProperties(target, props) {
    for (var i = 0; i < props.length; i++) {
      var descriptor = props[i];
      descriptor.enumerable = descriptor.enumerable || false;
      descriptor.configurable = true;
      if ("value" in descriptor) {
        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 _classCallCheck(instance, Constructor) {
  if (!(instance instanceof Constructor)) {
    throw new TypeError("Cannot call a class as a function");
  }
}

var Person = (function () {
  function Person(name) {
    _classCallCheck(this, Person);

    this.name = name;
  }

  _createClass(
    Person,
    [
      {
        key: "sayHello",
        value: function sayHello() {
          return "hello, I am " + this.name;
        },
      },
      {
        key: "name",
        get: function get() {
          return "kevin";
        },
        set: function set(newName) {
          console.log("new name 为:" + newName);
        },
      },
    ],
    [
      {
        key: "onlySayHello",
        value: function onlySayHello() {
          return "hello";
        },
      },
    ]
  );

  return Person;
})();

es6

class Parent {
  constructor(name) {
    this.name = name;
  }
}

class Child extends Parent {
  constructor(name, age) {
    super(name);
    this.age = age;
  }
}

var child1 = new Child("kevin", "18");

console.log(child1);

编译后

"use strict";

function _possibleConstructorReturn(self, call) {
  if (!self) {
    throw new ReferenceError(
      "this hasn't been initialised - super() hasn't been called"
    );
  }
  return call && (typeof call === "object" || typeof call === "function")
    ? call
    : self;
}

function _inherits(subClass, superClass) {
  if (typeof superClass !== "function" && superClass !== null) {
    throw new TypeError(
      "Super expression must either be null or a function, not " +
        typeof superClass
    );
  }

  subClass.prototype = Object.create(superClass && superClass.prototype, {
    constructor: {
      value: subClass,
      enumerable: false,
      writable: true,
      configurable: true,
    },
  });

  if (superClass) {
    Object.setPrototypeOf
      ? Object.setPrototypeOf(subClass, superClass)
      : (subClass.__proto__ = superClass);
  }
}

function _classCallCheck(instance, Constructor) {
  if (!(instance instanceof Constructor)) {
    throw new TypeError("Cannot call a class as a function");
  }
}

var Parent = function Parent(name) {
  _classCallCheck(this, Parent);

  this.name = name;
};

var Child = (function (_Parent) {
  _inherits(Child, _Parent);

  function Child(name, age) {
    _classCallCheck(this, Child);

    // 调用父类的 constructor(name)
    var _this = _possibleConstructorReturn(
      this,
      (Child.__proto__ || Object.getPrototypeOf(Child)).call(this, name)
    );

    _this.age = age;
    return _this;
  }

  return Child;
})(Parent);

var child1 = new Child("kevin", "18");

console.log(child1);

_inherits

function _inherits(subClass, superClass) {
  // extend 的继承目标必须是函数或者是 null
  if (typeof superClass !== "function" && superClass !== null) {
    throw new TypeError(
      "Super expression must either be null or a function, not " +
        typeof superClass
    );
  }

  // 类似于 ES5 的寄生组合式继承,
  // 使用 Object.create,设置子类 prototype 属性的 __proto__ 属性指向父类的 prototype 属性
  // 并给子类添加一个可配置可写不可枚举的 constructor 属性,该属性值为 subClass
  subClass.prototype = Object.create(superClass && superClass.prototype, {
    constructor: {
      value: subClass,
      enumerable: false,
      writable: true,
      configurable: true,
    },
  });

  // 设置子类的 __proto__ 属性指向父类
  if (superClass) {
    Object.setPrototypeOf
      ? Object.setPrototypeOf(subClass, superClass)
      : (subClass.__proto__ = superClass);
  }
}

Object.create() 的第二个参数表示要添加到新创建对象的属性

_possibleConstructorReturn

function _possibleConstructorReturn(self, call) {
  if (!self) {
    throw new ReferenceError(
      "this hasn't been initialised - super() hasn't been called"
    );
  }
  return call && (typeof call === "object" || typeof call === "function")
    ? call
    : self;
}

为啥要判断 parent return 呢? 因为 在 constructor 函数中可以 return 例如:

class Parent {
  constructor() {
    this.xxx = xxx;
  }
}

// 没有显示的 return 默认 return undefined

class Parent {
  constructor() {
    return null;
  }
}

// 可以 return 各种类型 比如 null

总体实现

var Child = (function (_Parent) {
  _inherits(Child, _Parent);

  function Child(name, age) {
    _classCallCheck(this, Child);

    var _this = _possibleConstructorReturn(
      this,
      (Child.__proto__ || Object.getPrototypeOf(Child)).call(this, name)
    );

    _this.age = age;
    return _this;
  }

  return Child;
})(Parent);
  1. 首先执行 _inherits(Child, Parent),建立 Child 和 Parent 的原型链关系,即 Object.setPrototypeOf(Child.prototype, Parent.prototype) 和 Object.setPrototypeOf(Child, Parent)
  2. 然后调用 Parent.call(this, name),根据 Parent 构造函数的返回值类型确定子类构造函数 this 的初始值 _this
  3. 最终,根据子类构造函数,修改 _this 的值,然后返回该值

references

作者:shanejix 出处:https://www.shanejix.com/posts/TypeScript - ES6 中的 Class 继承/ 版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。 声明:转载请注明出处!

Edit on GitHubDiscuss on GitHub


Shane Jix

Personal blog by Shane Jix. I explain with words and code.

LinksTools
© 2019 - 2022, Built withGatsby