TypeScript - ES6 中的 Class 继承

Nov 13, 2021
·

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