面试题答案
一键面试运行时与编译时行为特性差异
- 编译时差异
- 抽象类:
- TypeScript的抽象类不能被实例化,仅作为其他类的基类。它可以包含抽象方法,这些方法只有声明,没有实现,子类必须实现这些抽象方法。在编译阶段,TypeScript编译器会检查子类是否实现了抽象类中的抽象方法,如果未实现则会报错。例如:
- 抽象类:
abstract class Animal {
abstract speak(): void;
move(): void {
console.log('Moving along!');
}
}
class Dog extends Animal {
speak(): void {
console.log('Woof!');
}
}
- 普通类:普通类可以直接实例化,类中的方法和属性在定义后即可使用。在编译时,只要类的语法正确,不会像抽象类那样对子类有必须实现某些方法的严格要求。例如:
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
greet(): void {
console.log(`Hello, I'm ${this.name}`);
}
}
let john = new Person('John');
john.greet();
- 运行时差异
- 抽象类:在运行时,由于抽象类不能被实例化,所以不会直接在内存中创建抽象类的实例对象。当创建子类实例时,子类会继承抽象类的属性和方法,并根据需要实现抽象方法。从内存角度看,抽象类本身不占用实例内存空间,只有子类实例会占用内存。
- 普通类:普通类在实例化时,会在内存中创建对象,分配内存空间来存储对象的属性和方法。每个实例对象都有自己独立的内存区域来存储其状态。
性能优化策略
- 内存管理方面
- 抽象类:
- 复用抽象类逻辑:在大型项目中,如果多个子类共享抽象类的部分逻辑(如非抽象方法),通过继承抽象类可以避免重复实现这些逻辑,减少代码冗余,从而间接优化内存。例如,多个不同类型的图形类(圆形、矩形等)继承自一个抽象的图形类,抽象图形类中定义了计算面积的通用逻辑(如果有的话),子类只需要实现特定于自身的部分(如圆形计算面积需实现半径相关计算)。
- 抽象类:
abstract class Shape {
abstract calculateArea(): number;
commonFunction(): void {
console.log('This is a common function for shapes');
}
}
class Circle extends Shape {
radius: number;
constructor(radius: number) {
super();
this.radius = radius;
}
calculateArea(): number {
return Math.PI * this.radius * this.radius;
}
}
class Rectangle extends Shape {
width: number;
height: number;
constructor(width: number, height: number) {
super();
this.width = width;
this.height = height;
}
calculateArea(): number {
return this.width * this.height;
}
}
- **延迟实例化**:由于抽象类本身不实例化,利用这一特性可以将创建子类实例的操作延迟到真正需要的时候,减少初始内存占用。比如在一个游戏场景管理系统中,抽象类`GameObject`定义了游戏对象的通用属性和方法,具体的游戏对象(如`Player`、`Enemy`等子类)可以在游戏场景加载到相应部分时再实例化。
- 普通类:
- 对象池模式:对于频繁创建和销毁的普通类实例,可以使用对象池模式。在对象池中预先创建一定数量的对象实例,当需要时从对象池中获取,使用完毕后再放回对象池,避免频繁的内存分配和释放。例如在一个实时对战游戏中,子弹类
Bullet
可能频繁创建和销毁,可以使用对象池管理。
- 对象池模式:对于频繁创建和销毁的普通类实例,可以使用对象池模式。在对象池中预先创建一定数量的对象实例,当需要时从对象池中获取,使用完毕后再放回对象池,避免频繁的内存分配和释放。例如在一个实时对战游戏中,子弹类
class Bullet {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
fire(): void {
console.log(`Bullet fired from (${this.x}, ${this.y})`);
}
}
class BulletPool {
private pool: Bullet[] = [];
constructor(private initialSize: number) {
for (let i = 0; i < initialSize; i++) {
this.pool.push(new Bullet(0, 0));
}
}
getBullet(): Bullet {
return this.pool.length > 0? this.pool.pop()! : new Bullet(0, 0);
}
returnBullet(bullet: Bullet): void {
this.pool.push(bullet);
}
}
- 代码执行效率方面
- 抽象类:
- 减少抽象方法调用开销:由于抽象方法在运行时需要根据子类的实现来确定具体行为,尽量减少在性能关键路径上对抽象方法的调用次数。例如,在一个图形渲染系统中,如果
draw
方法是抽象方法,在渲染循环中尽量将非抽象的计算逻辑提前执行,减少draw
方法内部的复杂计算。
- 减少抽象方法调用开销:由于抽象方法在运行时需要根据子类的实现来确定具体行为,尽量减少在性能关键路径上对抽象方法的调用次数。例如,在一个图形渲染系统中,如果
- 普通类:
- 方法内联:对于一些短小且频繁调用的方法,可以考虑使用内联函数的方式,减少函数调用的开销。现代JavaScript引擎通常会自动进行内联优化,但在某些情况下显式内联可以提高性能。例如:
- 抽象类:
class MathUtils {
// 不内联
add(a: number, b: number): number {
return a + b;
}
// 内联
addInline = (a: number, b: number) => a + b;
}
在性能关键代码段使用addInline
可能会有更好的执行效率。
通过合理利用抽象类和普通类在运行时与编译时的差异,采取上述优化策略,可以有效提升大型前端项目的性能。