TS/ES6继承子类中箭头函数属性访问为undefined问题
定义了基础类MyEndpoint,以及继承自该类的子类MyCustomEndpoint,业务代码中通过new MyCustomEndpoint(router);的方式实例化子类。异常现象为:在子类重写的initMyRoutes方法中,访问handleIsNotOk时得到的值为undefined。
import express from 'express'; export class MyEndpoint { constructor(router: express.Router) { this.initMyRoutes(router); } protected initMyRoutes(router: express.Router) { router.get(`/v1/isok`, this.handleIsOk); } protected handleIsOk = (req: express.Request, res: express.Response) => { res.send(200); } } export class MyCustomEndpoint extends MyEndpoint{ private test = "Test"; protected initMyRoutes(router: express.Router) { console.log(this.handleIsNotOk); // undefined console.log(this.handleIsNotOkFn); router.get(`/v1/isokcustom`, this.handleIsOk); // 正常工作 router.get(`/v1/isnotok`,this.handleIsNotOk); // 异常 router.get(`/v1/isnotokfn`, this.handleIsNotOkFn); // 异常 router.get(`/v1/isnotokfn`, (req, res) => this.handleIsNotOkFn(req,res)); // 可运行但风格不统一 } protected handleIsNotOk = (req: express.Request, res: express.Response) => { res.send(200); } protected handleIsNotOkFn(req: express.Request, res: express.Response){ console.log(this.test); // undefined res.send(200); } } // 实例化逻辑 const router = express.Router(); new MyCustomEndpoint(router);
- 子类
initMyRoutes方法中同步打印this.handleIsNotOk结果为undefined - 直接将
this.handleIsNotOk、this.handleIsNotOkFn传入router.get作为路由回调时无法正常工作,其中普通方法handleIsNotOkFn执行时内部访问this.test也为undefined - 仅通过箭头函数包裹后调用
this.handleIsNotOkFn(req,res)的写法可以正常运行,但该写法和其他路由注册的风格不一致,不够优雅
问题由两个JavaScript/TypeScript类的核心机制共同导致:
1. ES6类继承的实例字段初始化时序问题
ES6类继承的实例初始化顺序是严格固定的:
- 创建空实例对象,绑定原型链
- 初始化父类定义的所有实例字段(包括箭头函数属性、普通实例属性)
- 执行父类
constructor内的全部代码 - 父类构造逻辑完全执行结束后,才会初始化子类定义的所有实例字段(子类中写的
private test = "Test"、protected handleIsNotOk = (req,res)=>{}都属于这一阶段才会赋值到实例上) - 若子类自定义了constructor,最后执行子类constructor中
super()之后的代码
代码中父类constructor直接调用了this.initMyRoutes(router),由于子类重写了initMyRoutes方法,实际执行的是子类的路由注册逻辑,但此时子类的实例字段还完全没有开始初始化,this.handleIsNotOk、this.test这类子类实例属性根本没被赋值,因此同步打印this.handleIsNotOk会得到undefined。
父类的handleIsOk之所以能正常访问,是因为它是父类的实例字段,在父类constructor执行前就已经完成初始化,所以调用initMyRoutes时可以正常拿到引用。
2. 普通类方法的this绑定丢失
handleIsNotOkFn是定义在类原型上的普通方法,不是实例自身的属性。当直接把this.handleIsNotOkFn作为函数引用传给router.get时,Express后续收到请求触发回调时,只会单独调用这个函数,不会自动把类实例绑定为函数内的this,导致方法内部的this指向丢失(严格模式下为undefined,非严格模式下指向全局对象),自然访问不到实例上的this.test属性。
箭头函数属性本身不会有这个问题:箭头函数没有自己的this,会捕获定义时上下文中的this(也就是类实例),只要箭头函数属性完成初始化,直接把引用传给router.get也不会丢失this指向——但代码中调用initMyRoutes时子类的箭头函数属性还没初始化,所以拿不到引用。
箭头函数包裹写法生效的原因
(req, res) => this.handleIsNotOkFn(req,res)是一个箭头函数,会在定义时捕获类实例的this。路由注册阶段只是把这个箭头函数存到Express的路由表中,不会立刻执行;等后续请求真正进来触发回调时,类实例早就完成了全部初始化流程,此时通过this.handleIsNotOkFn()的形式显式调用方法,this会正确指向类实例,因此可以正常访问所有实例属性。
内容的提问来源于stack exchange,提问作者Zerotwelve

