Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TypeScript - ES6 中的 Class 继承 #80

Open
shanejix opened this issue Nov 13, 2021 · 0 comments
Open

TypeScript - ES6 中的 Class 继承 #80

shanejix opened this issue Nov 13, 2021 · 0 comments

Comments

@shanejix
Copy link
Owner

shanejix commented Nov 13, 2021

同步链接: 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 国际」许可协议进行许可。
声明:转载请注明出处!

@shanejix shanejix changed the title TypeScript 中的 Class 继承 TypeScript - ES6 中的 Class 继承 Mar 2, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant