ES6+类继承问题:构造函数返回对象致子类方法丢失
问题根源分析
当父类构造函数显式返回一个对象时,用new创建子类实例时,最终得到的是父类构造函数返回的对象,而非子类的实例对象。这个返回对象的原型链和子类完全无关,因此子类原型上的方法(比如你的saySomething或isUnmatchedPrecondition)无法被访问到。
以你提供的基础示例为例:
class Base { constructor() { return {name: "base"} // 显式返回普通对象 } } class Extension extends Base { constructor() { super() } saySomething() { console.log("What do you want me to say?") } } const ext = new Extension(); console.log(ext instanceof Extension); // false,ext是{name: "base"},不是Extension实例 ext.saySomething(); // 报错:saySomething is not a function
在你的NestJS+Prisma场景中,PrismaExtended的构造函数返回了client(...args)(即Prisma扩展后的客户端实例),这个实例的原型链完全属于Prisma,和PrismaService无关,因此PrismaService中定义的自定义方法自然无法在运行时被找到。
解决方案
方案1:使用组合模式(推荐)
放弃继承,改用组合的方式,将Prisma客户端作为PrismaService的内部属性,自定义方法直接放在PrismaService中。这种方式更清晰,避免了原型链带来的问题,符合依赖注入的设计思想。
修改后的代码示例:
@Injectable() export class PrismaService implements OnModuleInit, OnModuleDestroy { private readonly prisma: ReturnType<typeof createExtendedPrismaClient>; private retries: number = 0; constructor( private readonly logger: AppLogger, configService: ConfigService, ) { // 单独定义扩展Prisma客户端的逻辑 const createExtendedPrismaClient = (...args: ConstructorParameters<typeof PrismaClient>) => new PrismaClient(args).$extends({ result: { $allModels: { toJSON: { compute(data: BaseEntity) { return <T extends typeof data = typeof data>() => { return Object.assign({}, data, { sn: typeof data.sn === 'bigint' ? data.sn.toString() : data.sn, }) as unknown as Omit<T, 'sn'> & { sn: string }; }; }, }, }, }, }); // 初始化Prisma客户端 this.prisma = createExtendedPrismaClient({ log: pickFrom(configService, 'app.status') !== NodeEnv.PROD ? ['query'] : undefined, transactionOptions: { timeout: 60_000 }, }); } async onModuleInit() { await this.prisma.$connect(); } async onModuleDestroy() { await this.prisma.$disconnect(); } // 自定义方法,直接属于PrismaService实例 isUnmatchedPrecondition(e: unknown): e is PrismaClientKnownRequestError { return e instanceof PrismaClientKnownRequestError && e.code === PrismaErrorCode.UNMATCHED_PRECONDITION; } // 可选:通过Proxy自动代理Prisma客户端的方法,避免重复编写转发代码 get(prop: string | symbol) { if (prop in this) { return Reflect.get(this, prop); } return Reflect.get(this.prisma, prop); } }
使用时,你可以直接通过this.prisma.user.findMany()调用Prisma方法,或者通过Proxy直接this.user.findMany()(如果添加了Proxy逻辑),自定义方法直接this.isUnmatchedPrecondition(e)调用即可。
方案2:修改返回对象的原型链(备选)
如果一定要保留继承的写法,需要将子类的方法和原型链挂载到父类返回的对象上。修改PrismaExtended的构造函数:
function PrismaExtended() { type Params = ConstructorParameters<typeof PrismaClient<Prisma.PrismaClientOptions, Prisma.LogLevel>>; const client = (...args: Params) => new PrismaClient<Prisma.PrismaClientOptions, Prisma.LogLevel>(...args).$extends({ result: { $allModels: { toJSON: { compute(data: BaseEntity) { return <T extends typeof data = typeof data>() => { return Object.assign({}, data, { sn: typeof data.sn === 'bigint' ? data.sn.toString() : data.sn, }) as unknown as Omit<T, 'sn'> & { sn: string }; }; }, }, }, }, }); class PrismaExtended { constructor(...args: Params) { const prismaInstance = client(...args); // 将当前实例的可枚举方法复制到Prisma实例上 Object.assign(prismaInstance, this); // 将Prisma实例的原型指向子类的原型,确保原型方法能被访问 Object.setPrototypeOf(prismaInstance, Object.getPrototypeOf(this)); return prismaInstance; } } return PrismaExtended as new (...args: Params) => ReturnType<typeof client>; }
这种方式通过Object.assign复制实例方法,Object.setPrototypeOf修改原型链,让Prisma实例能访问到PrismaService的自定义方法。但需要注意,Prisma客户端本身的原型链可能会被修改,存在潜在的兼容性风险,因此不推荐作为首选方案。
内容的提问来源于stack exchange,提问作者calebpitan

