JavaScript实现状态模式反向流转时抛出构造函数不存在TypeError
状态模式下订单反向流转报错
TypeError: EstadoRecibido is not a constructor排查与修复 问题背景
开发应用多设计模式的线上门店管理平台时,销售订单(Venta)需按如下规则流转状态:
- 初始默认进入*已接收(EstadoRecibido / received state)*状态
- 已接收状态可流转至*已受理(EstadoAceptado / accepted state)*状态
- 已受理状态可流转至*已配送(EstadoEntregado / delivered state)*状态
- 额外支持从已受理状态反向回退至已接收状态
实际执行反向回退操作时抛出固定错误:TypeError: EstadoRecibido is not a constructor。其中Venta.js是状态模式的上下文(Context)类,内部持有当前订单状态的引用。
关联代码
1. 状态基类 Estado.js
class Estado { recibirVenta () { return new Error('subclass responsibility') } aceptarVenta () { return new Error('subclass responsibility') } cancelarVenta () { return new Error('subclass responsibility') } entregarVenta () { return new Error('subclass responsibility') } } module.exports = Estado
2. 已接收状态类 EstadoRecibido.js
const Estado = require('./estado') const EstadoAceptado = require('./estadoAceptado') class EstadoRecibido extends Estado { constructor () { super() this.nombre = 'Recibido' } aceptarVenta (aVenta) { aVenta.setEstadoEnvio(new EstadoAceptado()) } } module.exports = EstadoRecibido
3. 已受理状态类 EstadoAceptado.js
const Estado = require('./estado') const EstadoEntregado = require('./estadoEntregado') const EstadoRecibido = require('./estadoRecibido') class EstadoAceptado extends Estado { constructor () { super() this.nombre = 'Aceptada' } entregarVenta (aVenta) { aVenta.setEstadoEnvio(new EstadoEntregado()) } recibirVenta (aVenta) { aVenta.setEstadoEnvio(new EstadoRecibido()) } } module.exports = EstadoAceptado
4. 订单上下文类 Venta.js
class Venta { constructor (cliente, items, tipoEnvio) { // 省略其余属性初始化逻辑 this.estadoEnvio = new EstadoRecibido() } aceptarVenta () { this.estadoEnvio.aceptarVenta(this) } recibirVenta () { this.estadoEnvio.recibirVenta(this) } entregarVenta () { this.estadoEnvio.entregarVenta(this) } setEstadoEnvio (aEstado) { this.estadoEnvio = aEstado } getEstadoEnvio () { return this.estadoEnvio } } module.exports = Venta
错误复现流程
执行以下代码可稳定触发报错:
const venta = new Venta() // 实例化完成后,venta初始状态为已接收 venta.aceptarVenta() // 执行后状态正常切换为已受理 venta.recibirVenta() // 执行回退操作时报错:TypeError: EstadoRecibido is not a constructor
从已受理正向流转到已配送状态无任何异常,仅反向回退时触发错误。
根因分析
问题确实由CommonJS模块的循环依赖加载机制导致,加载链路如下:
- 首次加载
EstadoRecibido.js时,Node会先为该模块创建空的module.exports缓存,再开始执行模块内代码 - 代码执行到顶部
const EstadoAceptado = require('./estadoAceptado')时,暂停当前模块执行,转而去加载EstadoAceptado.js - 加载
EstadoAceptado.js时,执行到顶部const EstadoRecibido = require('./estadoRecibido'),此时Node发现EstadoRecibido模块已经在加载队列(缓存)中,但还没执行完成(仍停留在等待EstadoAceptado加载的阶段),因此直接返回当前缓存里的空导出对象给EstadoAceptado模块,而不是最终定义好的构造函数 - 等
EstadoAceptado加载完成回到EstadoRecibido模块继续执行,后续才会把EstadoRecibido类赋值给module.exports,但此时EstadoAceptado里拿到的EstadoRecibido引用还是之前的空对象,调用new EstadoRecibido()时自然会抛出"不是构造函数"的错误
正向流转到EstadoEntregado不报错,是因为EstadoEntregado和其余两个状态类没有形成循环依赖链路,加载时可以拿到完整的构造函数引用。
修复方案
方案1:延迟加载依赖(改动最小)
去掉模块顶部的互相依赖引入,改为在实际调用状态流转方法时再require对应状态类,打破模块加载阶段的循环引用:
- 修改
EstadoRecibido.js:
const Estado = require('./estado') class EstadoRecibido extends Estado { constructor () { super() this.nombre = 'Recibido' } aceptarVenta (aVenta) { const EstadoAceptado = require('./estadoAceptado') aVenta.setEstadoEnvio(new EstadoAceptado()) } } module.exports = EstadoRecibido
- 修改
EstadoAceptado.js:
const Estado = require('./estado') class EstadoAceptado extends Estado { constructor () { super() this.nombre = 'Aceptada' } entregarVenta (aVenta) { const EstadoEntregado = require('./estadoEntregado') aVenta.setEstadoEnvio(new EstadoEntregado()) } recibirVenta (aVenta) { const EstadoRecibido = require('./estadoRecibido') aVenta.setEstadoEnvio(new EstadoRecibido()) } } module.exports = EstadoAceptado
这种方式下,所有模块都完成初始化加载后,才会在实际触发状态流转时执行require,此时拿到的都是已经完成导出的有效构造函数,不会出现空对象问题。
方案2:统一状态入口(适合状态较多的场景)
新建状态索引文件统一加载、导出所有状态类,避免状态模块之间两两直接引用,从根源消除循环依赖。
内容的提问来源于stack exchange,提问作者Mauro Alvarez
相关产品推荐
相关产品推荐

