在前端与 JavaScript 世界中,let 和 const 的引入被视为语言进化的重要一步,它们带来了更清晰的块级作用域和更严格的语义,从而帮助开发者规避一类常见的犯错场景。然而,即便在现代代码规范普遍鼓励使用 let/const 的今天,你仍然会在一些大型、性能敏感的代码库里看到大量的 var 声明。TypeScript 源码便是一个典型例子:在高度工程化的仓库中,为什么会出现看似"过时"的 var?把原因归结为"历史习惯"或"不够现代"都太简单,背后涉及到的其实既有语言语义(尤其是时序死区 Temporal Dead Zone,简称 TDZ)的复杂性,也有运行时性能、JavaScript 引擎实现与大规模工程的现实权衡。本文将从原理、引擎实现、性能实测与工程实践四个层面,系统剖析为何会出现这种现象,并给出在实际项目中如何取舍的建议。 首先理解时序死区的本质。TDZ 是由 let 和 const 引入的语义属性:在变量声明所在的作用域开始到变量被初始化的那段时间里,变量存在但不可访问,任何读取都会导致运行时 ReferenceError。
TDZ 的好处显而易见:它把一类潜在的使用未初始化变量的 bug 提前暴露出来,提高代码的可靠性。与之对照,var 有"变量提升"的语义 - - 变量名在整个函数或全局作用域内可见,但在实际赋值之前其值为 undefined。虽然这种语义在某些场景下带来惊人的反直觉行为,但也让变量访问变得更为简单,可预测为"总能读取到值(哪怕是 undefined)"。 为什么 TDZ 会带来性能问题?问题的关键在于引擎要在运行时判断某次变量访问是否落入 TDZ。这个判断在语言规范层面是必须要满足的行为,但在实现层面意味着在每次访问 let/const 声明的变量时,需要额外的检查逻辑。现代 JavaScript 引擎(如 V8、SpiderMonkey)通过各种优化技巧尽量把这类检查内联、消除或在 JIT 编译阶段优化掉,但这些优化不是在任何代码路径都能生效,尤其是在复杂的控制流、嵌套闭包或高度动态的执行场景中,引擎往往不得不保留检查逻辑以保证语义正确性。
相对而言,var 没有 TDZ 的约束,引擎在很多情况下可以生成更简单、更快的访问路径。 在大型代码库里,这种差异被放大。TypeScript 的代码基数庞大,许多函数位于性能敏感的核心路径,任何微小的运行时开销累积起来都会对整体性能产生可观影响。工程团队经过测量后发现,把某些关键模块的 let/const 替换为 var,能够降低运行时的检查开销,使得热路径的执行更快,从而在整体基准测试中看到明显的性能提升。值得注意的是,这种优化并不是针对可读性或语义正确性做出折衷,而是基于精确的性能分析:只在确实会影响关键性能的地方采用 var,而在其他地方继续使用 let/const 保持更严格的语义。 另一个重要因素是作用域和闭包的分配成本。
let/const 的块级作用域语义在一些实现上会导致更多的作用域对象或环境记录被分配,特别是在循环与闭包结合的场景中。虽然引擎也有逃逸分析和优化以避免不必要的分配,但在某些复杂控制流中,运行时仍需为每个迭代或每个块创建独立的环境记录,从而带来内存和时间上的开销。而 var 的函数级作用域和"提升"语义常常让这些分配可以被省去或合并,使得执行更紧凑。 这些实现细节在抽象层面看上去微妙,但在现实项目的基准测试中会被放大。TypeScript 团队及其他大型项目的工程师通常会对关键路径进行大量的基准测试与剖析,在找到性能瓶颈后采取权衡。性能优化的哲学里,有一句常见的话:优先测量再优化。
对于语言层面的语义替换也是同样的原则。直接把全仓库的 let/const 全部替换为 var 并不可取,因为会丢失 TDZ 带来的安全性。但是在明确定义的热点路径上,权衡过后的替换却是合理的工程决策。 除了性能与内存分配外,工具链与静态分析也会影响变量选择。在 TypeScript 自身的构建过程中,类型信息、编译器优化和运行时代码之间有复杂的互相影响。有时候在编译器的早期阶段使用 var 可以避免某些类型检查或代码生成路径的复杂化,从而简化运行时代码的形态。
此外,历史遗留代码、外部 API 的兼容要求以及团队的代码规范也会促成在特定模块保留 var。值得强调的是,出现大量 var 并不意味着整个项目忽视现代最佳实践,而是反映了在工程尺度上对多重目标(正确性、可维护性、性能、兼容性)的综合权衡。 在现代 JavaScript 引擎持续演进的背景下,let/const 相关的性能差距在不断缩小。引擎开发者长期投入大量精力优化 TDZ 检查、减少环境记录生成并改进 JIT 策略。随着这些优化的落地,过去必须用 var 才能达到的性能提升会逐步减少。但大型代码库对稳定性和可预测性的要求极高,它们往往不会仅凭单次引擎改进就大规模改变已经验证过的性能策略。
这也是为什么即使在如今,某些核心模块仍然保持使用 var,直到足够可靠的替代方案经过充分验证。 那么对于日常开发者,应该如何在 TDZ 的安全性与 var 的性能之间做出选择?首先,除非在经过基准测试后确认存在性能瓶颈,否则应当优先使用 const 与 let,因为它们能帮助捕获逻辑错误并提高代码可读性。其次,对于确实位于热路径或低延迟要求的模块,可以采用"先测后改"的策略:通过性能剖析确定瓶颈,然后在局部范围内尝试使用 var 并对比效果。如果确实可观,可以在代码中增加注释说明为何在此处使用 var,以便日后维护者理解历史决策。最后,可以考虑通过重构将性能敏感逻辑隔离为小而明确的函数或模块,便于有针对性地优化并降低全局语义变化带来的风险。 工程治理层面还可以采取自动化措施来平衡安全与性能。
一种常见做法是采用静态分析与 lint 规则默认禁止 var,同时对某些目录或文件启用例外白名单,配合基准测试流程与代码审查将变更控制在可度量范围内。对于开源项目,明确的注释与贡献指南也能帮助外部贡献者理解为何在特定区域出现 var。 总体而言,TypeScript 代码库中大量 var 的存在并不是对现代语法的否定,而是谨慎的工程妥协结果。TDZ 为开发者提供了重要的防护,但也引入了实现复杂性和潜在的性能开销。在性能敏感的场景中,var 的简单语义能够减少运行时检查和内存分配,从而带来显著的速度提升。未来随着引擎不断改进、静态分析与编译器优化的提升,这种权衡可能会改变,但在当下,对性能有严格要求的团队仍然有充分理由在受控范围内使用 var。
理解这种权衡有助于我们成为更成熟的工程师:学会在安全性、可维护性与性能之间做出基于数据的决策,而不是简单地遵循教条。对每一处语法选择进行度量与记录,既能保证产品体验,也能维持代码的可理解性。对于大多数应用,let/const 提供的静态保证远比那点微小的性能损失更有价值;但在系统的少量关键路径上,工程师仍应保留使用 var 的权利,并通过测试与注释将这种选择透明化,确保团队在未来有条件地逐步改进和恢复更安全的语义。 。