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
的那个)- **
methods
、getters
和settors
都被写入了 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);
-
首先执行 _inherits(Child, Parent),建立 Child 和 Parent 的原型链关系,即 Object.setPrototypeOf(Child.prototype, Parent.prototype) 和 Object.setPrototypeOf(Child, Parent)
-
然后调用 Parent.call(this, name),根据 Parent 构造函数的返回值类型确定子类构造函数 this 的初始值 _this
-
最终,根据子类构造函数,修改 _this 的值,然后返回该值
references
- https://zh.javascript.info/classes
- https://www.cnblogs.com/shanejix/p/15220858.html#class-%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95
- https://www.cnblogs.com/shanejix/p/15220858.html#%E7%B1%BB%E7%BB%A7%E6%89%BF
- https://www.cnblogs.com/shanejix/p/15220858.html#%E9%9D%99%E6%80%81%E5%B1%9E%E6%80%A7%E5%92%8C%E9%9D%99%E6%80%81%E6%96%B9%E6%B3%95
- https://github.com/mqyqingfeng/Blog/issues/105
- https://github.com/mqyqingfeng/Blog/issues/106
- https://babeljs.io/repl/#?browsers=&build=&builtIns=false&corejs=3.21&spec=false&loose=false&code_lz=Q&debug=false&forceAllTransforms=false&shippedProposals=false&circleciRepo=&evaluate=true&fileSize=false&timeTravel=false&sourceType=module&lineWrap=false&presets=es2015&prettier=false&targets=&version=7.17.6&externalPlugins=&assumptions=%7B%7D
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)