TypeScript报ReferenceError:初始化前无法访问hasReleased变量
错误原因
这个报错和你有没有写内联初始化没有关系,本质是ES模块循环依赖触发了let/const的暂时性死区(TDZ)规则:
- ES模块加载时会先扫描所有导入导出绑定,遇到
import会优先加载被依赖的模块。如果两个模块存在直接/间接的循环引用(比如A导入B,B又反过来导入A),就可能出现模块代码还没执行完,导出的方法就被外部调用的情况。 let/const声明的变量不存在变量提升,在代码执行到声明赋值行之前,变量会一直处于TDZ中,只要在这之前访问就会抛出你看到的ReferenceError——哪怕你已经给变量写了初始值,只要执行流没走到那一行,访问就是非法的。- 你当前的执行顺序大概率是:
- 调用方模块开始加载,遇到导入
onScreenLockReleasedFirstTime的语句,转去加载ScreenLock.tsx - ScreenLock.tsx加载过程中,顶部存在对调用方模块(或其他依赖调用方的模块)的导入,又转回去加载调用方模块
- 调用方模块此时已经拿到了
onScreenLockReleasedFirstTime的函数引用(ES模块导出是实时绑定),直接执行调用 - 此时ScreenLock.tsx的执行流还停留在处理导入的阶段,根本没走到
let hasReleased = false这一行,访问hasReleased直接触发TDZ报错。
- 调用方模块开始加载,遇到导入
修复方案
方案1:打破循环依赖(根治,最推荐)
梳理两个模块的依赖关系,把互相耦合的公共逻辑抽离到独立的第三个公共模块中,消除循环引用链,保证ScreenLock.tsx的顶层代码全部执行完成后,外部才会调用它导出的方法,从根源上避免TDZ问题。
方案2:调整状态存储方式,规避顶层变量TDZ
如果暂时没法梳理清楚依赖关系,可以把状态从顶层let变量迁移到函数静态属性上,加一层兜底初始化,改完的ScreenLock.tsx代码如下:
const firstTimeCbs: (() => void)[] = []; export function onScreenLockReleasedFirstTime(cb: () => void) { // 首次调用兜底初始化,避免外层赋值没执行的TDZ问题 if (typeof onScreenLockReleasedFirstTime.hasReleased === 'undefined') { onScreenLockReleasedFirstTime.hasReleased = false; } if (!onScreenLockReleasedFirstTime.hasReleased) firstTimeCbs.push(cb); else cb(); } // 你原有触发锁屏释放的逻辑里,把标记位改为true即可 // 例如释放逻辑中加: // onScreenLockReleasedFirstTime.hasReleased = true; // firstTimeCbs.forEach(cb => cb());
函数声明会在模块加载阶段优先完成初始化,调用时函数本身一定是可访问的,状态挂载到函数上再加兜底逻辑,就不会触发TDZ问题。
方案3:临时用var声明(不推荐,仅应急)
把let hasReleased = false改为var hasReleased = false。var声明的变量会提升到作用域顶部,初始值为undefined,调用函数时!undefined结果为true,和你预期的初始状态逻辑一致,等后续执行流走到赋值行时会自动赋值为false,不会影响业务逻辑。但var的变量提升特性容易埋下其他隐患,只适合临时救急。
内容的提问来源于stack exchange,提问作者Alexis Leite Paracampo
相关产品推荐
相关产品推荐

