强烈不推荐在前端开发中使用类(class),因为类/OOP在设计理念上和前端优化有很大的冲突。如今的前端优化有很多点,其中有2点是minification和tree shake。但是类/OOP在设计的时候讲究的是SOLID,目的在于一个类写好之后是自解释的,独立的。
例如,在实际Stream这个类的时候,思考它需要和可以提供什么功能——readStream和writeStream。然后基于这个设计,把Stream作为一个类实现好了。但是在使用过程中,我可能只需要使用readStream,没有writeStream的场景。那在最后webpack或者其他工具链优化的时候,能否把writeStream tree shake掉呢?答案是,肯定不行的。从工具链的角度,writeStream是Stream作为类的完整性表现,随后通过复杂的静态分析可能判断哪个dead code,但是天知道其他外部代码会不会依赖于Stream,需要它的writeStream。另外一方,就算readStream被用到了,打包了。它也影响minification。因为Stream作为一个类,需要时刻保持它的完整性,方法名是这其中的很重要的表现。Animal可以eat/walk,但如果压缩了,A可以a1/b2。这个类A在设计语言上说不过去,它不是一个合格的类,或者说不应该成为类。
另外,类的实例在运行过程中还会比plain object占用更多的内存。这个可能之后会被浏览器优化解决。不属于设计理念上的冲突,我就不多说了。
解决方法?说起来也很容易,那就是不要用类,回到plain object+function的实现。
export type Stream = {
__streamBrand: void;
array: Uint8Array;
index: number;
};
export function createStream(array: Uint8Array): Stream {
return { array, index: 0 } as ReadableStream;
}
export function readByte(stream: Stream): number {
// ...
}
export function writeByte(stream: Stream): void {
// ...
}
在上层设计中使用:
export type Reader = {
__readerBrand: void;
stream: Stream;
};
export function createReader(array: Uint8Array): Reader {
return { stream: createStream(array) } as Reader;
}
export function inspectReader(reader: Reader) {
// ...
}
export function readBool(reader: Reader): boolean {
return Boolean(readByte(reader.stream));
}
TypeScript的Nominal Type可以确保类型正确的情况下,只使用plain object。上面例子中,虽然Type和function是逻辑上放在了同一个文件,但是他们并没有任何运行时的依赖(唯一的依赖来自于类型声明和检查,在运行时已去掉)。工具链可以尽情地做minification和tree shake(在concatenateModules开启情况下),来保证生成代码尽量短小。
另外,每个Type都定义了一个createType的方法来解决Nominal Type的初始化时候类型正确问题。表明上为,这好像会造成模板代码。但是webpack等工具链会只能智能解压这种小函数,直接把实现嵌到调用代码里面去的。所以,经过优化之后的代码是没有这个问题的。