JS&TS 中,类与函数有什么区别?
假如我们有一个 Person 类:
export class Person {
constructor(public name: string, public age: number) {}
sayHi() {
console.log(`Hi I am ${this.name}.`)
}
sayAge() {
console.log(`I am ${this.age} years old.`)
}
}
这是他编译为 es2015 后的代码:
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Person = void 0;
var Person = /** @class */ (function () {
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayHi = function () {
console.log("Hi I am ".concat(this.name, "."));
};
Person.prototype.sayAge = function () {
console.log("I am ".concat(this.age, " years old."));
};
return Person;
}());
exports.Person = Person;
在 JS 中,仍然是用函数去模拟类的,所以 TS 的类在编译后,本质也是函数,还是通过原型链的一套逻辑。
如何避免类中未初始化的字段报错?
class FieldWithoutInitValue {
// 在初始化和构造器中都没有赋值,必须要使用联合类型解决
// public someField: string | undefined
// TS2564: Property 'someField' has no initializer and is not definitely assigned in the constructor.
public someField: string;
// 使用 !: 声明即可
public fixedWay!: string;
constructor() {}
someMethod() {
this.someField = "123";
}
}
什么是函数签名?
函数名称 + 函数参数 + 函数参数类型 + 返回值类型
TS 类中静态方法内部可以使用 this 吗?
TS 中的静态方法和 Java 有点不一样,不过 this 只能引用到静态属性/方法,无法使用到普通的实例属性/方法,这一点还是符合直觉的。
什么是原型链继承?
将子类构造函数的 prototype 指向父类构造函数的 prototype(或者指向一个父类的实例也可以),但是这个方法有缺陷:
- 子类无法复用父类的构造函数。
- 子类如果要保留自己的 constructor 指向,会影响父类的 prototype。(如果子类的 prototype 指向的是父类的一个实例,那么就可以规避掉这个缺陷)
function Person(name, age) {
this.name = name;
this.age = age;
}
function Son(friends) {
this.friends = friends;
}
const person = new Person("zzh", 1);
// 原型链继承
Son.prototype = Person.prototype;
// 由于 prototype 被重新赋值了,所以这里需要挂一下 constructor 属性
// 但是这样会同时覆盖掉 Person.prototype 的值,
Son.prototype.constructor = Son;
const son = new Son(["小红"]);
console.log("person", person);
console.log("son", son);
console.log("Son.prototype", Son.prototype);
什么是借用构造函数继承?
借用构造函数是指在子类的构造函数中,调用父类的构造函数,从而达到复用的目的,但是这个方法也是有缺陷的:
- 子类并没有引用到父类的 prototype,所以无法调用父类的原型方法。
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayHi = () => {
console.log("Hi");
};
function Man(hobbies, name, age) {
// 在构造函数内部调用父类构造函数,从而达到复用的目的
Person.apply(this, [name, age]);
this.hobbies = hobbies;
}
const person = new Person("张三", 18);
console.log(person);
const man = new Man(["play"], "王五", 19);
console.log(man);
如何结合原型链继承和借用构造函数继承?
只需要在借用构造函数继承的时候,加上把子类构造函数的 prototype 指向父类的实例即可,之所以不指向父类的构造函数的 prototype,也是为了防止后续改写子类 prototype 的时候,会影响到父类,此时的父类实例,只是一个临时中间值,并没有什么意义。 这个做法解决了两个方法单独使用时的弊端,但是它仍然是有缺陷的:
- 父类的构造函数被调用了 2 次,其中一次是为了创建临时的中间实例,并没有意义。
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayHi = () => {
console.log("Hi");
};
function Man(hobbies, name, age) {
// 在构造函数内部调用父类构造函数,从而达到复用的目的
Person.apply(this, [name, age]);
this.hobbies = hobbies;
}
const person = new Person("张三", 18);
console.log(person);
// 即使构造 person 的时候传了 name, age,但是在创建 Man 实例的时候,会传递新的值做覆盖
Man.prototype = person;
const man = new Man(["play"], "王五", 19);
console.log(man);
什么是寄生式组合继承?(最佳继承方式 & TS 继承实现)
我们使用了一个中间函数作为中转,来代替之前使用中间父类实例的做法,这个做法也是 ts 中实现继承的做法。
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayHi = () => {
console.log("Hi");
};
function Man(hobbies, name, age) {
// 在构造函数内部调用父类构造函数,从而达到复用的目的
Person.apply(this, [name, age]);
this.hobbies = hobbies;
}
function Middle() {}
Middle.prototype = Person.prototype;
// 使用一个中间函数来代替构造父类实例的做法
Man.prototype = new Middle();
// 完善构造器的指向
Man.prototype.constructor = Man;
const man = new Man(["play"], "王五", 19);
console.log(man);
把上面的步骤封装为 _extend
函数,Object.create
也是类似的思路,这个 API 是构造一个空对象,并把对象的 __proto__
属性指向传入的参数,比如这里其实等价于 Man.prototype = Object.create(Person.prototype)
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayHi = () => {
console.log("Hi");
};
function Man(hobbies, name, age) {
// 在构造函数内部调用父类构造函数,从而达到复用的目的
Person.apply(this, [name, age]);
this.hobbies = hobbies;
}
function _extend(parentClass, subClass) {
function Middle() {
this.constructor = subClass;
}
Middle.prototype = parentClass.prototype;
return new Middle();
}
// 使用封装的继承函数来实现
// 可以使用 Man.prototype = Object.create(Person.prototype) 代替
Man.prototype = _extend(Person, Man);
const man = new Man(["play"], "王五", 19);
const person = new Person("zz", 18);
console.log(man);
console.log(person);
什么是类型守卫?
在语句的块级作用域(if 语句或者是条目运算符表达式内)缩小变量的一种类型推断行为,可以使用以下判断方法:
typeof
in
instanceof
===
,==
,!=
,!==
instanceof 是怎么工作的?
会沿着对象的 __proto__
属性,__proto__.__proto__...
一级级与构造函数的 prototype 做对比,如果相等,则返回 true
。
什么是类型自定义守卫?
在一些场景下,我们无法用原生的语法关键字来判断我们自定义的类型,这个时候可以自定义一个函数,返回值为:形参名 is 类型名 的写法,函数体返回布尔值,这样就可以在块级作用域中正确得到类型提示。
什么是 infer?
infer 是在 extends 条件语句中,以占位符出现的用来修饰数据类型的关键字,被修饰的数据类型等到使用时才可以被推断出来。通常会出现在这些位置:
- extends 条件语句后的函数类型的参数类型位置上。
- extends 条件语句后的函数类型的返回值类型上。
- infer 会出现在类型的泛型具体化类型上。
// infer 写在参数上
type inferParamsType<T> = T extends (params: infer P) => void ? P : string;
type A = inferParamsType<(params: { name: string }) => void>; // {name: string}
type B = inferParamsType<number>; // string
// infer 写在返回值上
type inferReturnType<T> = T extends (params: any) => infer P ? P : string;
type C = inferReturnType<(params: { name: string }) => string>; // string
type D = inferReturnType<number>; // string
// infer 写在泛型上
type inferGenericsType<T> = T extends Array<infer P> ? P : boolean;
const a: string[] = ["a", "b", "c"];
type E = inferGenericsType<typeof a>; // string
type F = inferGenericsType<number>; // boolean
什么是装饰器?
装饰器是一个函数,可以写到类、方法、属性、参数,对象上,并扩展其功能。另外 TS 中还有元数据装饰器,和 JAVA 的注解有点像,是一段描述信息。一般会有装饰器,以及装饰器工厂函数,装饰器一般不接收参数,可以直接使用,而装饰器工厂函数则接收参数,在逻辑中接收,并且返回值是一个新的装饰器。
function FirstClassDecorator(targetCLass: any) {
console.log("targetClass.name:", targetCLass.name);
(new targetCLass()).buy();
}
@FirstClassDecorator
class CustomService {
name: string = "下单";
buy() {
console.log(`${this.name}购买`);
}
}
根据编译生成的 js 代码,我们可以看到,装饰器函数可以通过返回值,来覆盖掉整个类的实现:
我们也可以使用方法装饰器,有点像一些中间件的思想,对方法进行拦截:
function TestMethod(
targetClass: any,
methodName: string,
dataProps: PropertyDescriptor
) {
const tempMethod = dataProps.value;
dataProps.value = function (...params: any[]) {
console.log("插入自定义方法逻辑");
tempMethod.apply(this, params);
};
}
// @FirstClassDecorator
class CustomService {
name: string = "下单";
@TestMethod
buy() {
console.log(`${this.name}购买`);
}
}
new CustomService().buy();
这边有一点要注意下:直接改写 dataProps.value
能够生效,是因为编译出来的 JS 代码中有 defineproperty
的调用,即相当于是复写了一次,
关于装饰器的执行顺序:由于是倒序遍历,所以声明在下面的装饰器会先执行;整体顺序为:属性、方法参数装饰器、方法、类。