[译] 暂时性死区,换句话说为什么 TypeScript 代码库充斥着 var 语句

35 阅读3分钟

原标题:The Temporal Dead Zone, or why the TypeScript codebase is littered with var statements
原文地址:vincentrolfs.dev/blog/ts-var
译者声明:为了让译文流畅、得体,译者调整了部分句子的人称、语序和表达方式。对有官方中文译文或具备官方性质的中文译文的原文链接,已进行替换。译文不代表、不暗示译者观点和/或立场

凡是使用过 JavaScript 一段时间的人,都知道初始化变量有几种不同方法。如今我们通常用

const password = "hunter2"

偶尔,我们需要一些可变状态:

let password = "hunter2"

(业界)这么声明(变量)已经好长时间了,这样做(让变量)符合合理的作用域规则:

function example(measurement) {
  console.log(calculation); // 引用错误
  console.log(anotherCalc); // 引用错误

  if (measurement > 1) {
    const calculation = measurement + 1;
    let anotherCalc = measurement * 2;
    // ...
  } else {
    // ...
  }

  console.log(calculation); // 引用错误
  console.log(anotherCalc); // 引用错误
}

然而,从泥盆纪混到现在的前辈可能还记得只能用 var 的时代。用 var 可是够遭罪的。这玩意儿不仅是可变的,想要弄成不可变的一点儿办法也没有。而且它还会从(大括弧包裹的)作用域中泄露出去。

function example(measurement) {
  console.log(calculation); // undefined - 能访问到! calculation 泄漏
  console.log(i); // undefined - 能访问到! i 泄漏

  if (measurement > 1) {
    var calculation = measurement + 1;
    // ...
  } else {
    // ...
  }

  console.log(calculation); // 1 - 能访问到! calculation 泄漏

  for (var i = 0; i < 3; i++) {
    // ...
  }

  console.log(i); // 3 - 能访问到! i 泄漏
}

忒可怕了!

笔者看到 TypeScript 代码库(现在就是 TypeScript 开发的)里满天飞的 var 语句,也吓了一激灵。(它怎么就)跟2003年写成的一样:

/** @internal */
export function createSourceMapGenerator(
  host: EmitHost,
  file: string,
  sourceRoot: string,
  sourcesDirectoryPath: string,
  generatorOptions: SourceMapGeneratorOptions
): SourceMapGenerator {
  /* eslint-disable no-var */
  var { enter, exit } = generatorOptions.extendedDiagnostics
    ? performance.createTimer("Source Map", "beforeSourcemap", "afterSourcemap")
    : performance.nullTimer;

  // 当前源映射文件及其在源列表中的索引
  var rawSources: string[] = [];
  var sources: string[] = [];
  var sourceToSourceIndexMap = new Map<string, number>();
  var sourcesContent: (string | null)[] | undefined; // eslint-disable-line no-restricted-syntax

  var names: string[] = [];
  var nameToNameIndexMap: Map<string, number> | undefined;
  var mappingCharCodes: number[] = [];
  var mappings = "";

  // 最终记录和编码的映射关系
  var lastGeneratedLine = 0;
  var lastGeneratedCharacter = 0;
  var lastSourceIndex = 0;
  var lastSourceLine = 0;
  var lastSourceCharacter = 0;
  var lastNameIndex = 0;
  var hasLast = false;

  // ... 其他代码
}

要是没听说过暂时性死区的话,可能还不知道为啥呢。(大概意思就是)代码里的每个变量都有一个已声明未初始化的区域。直接看(代码)示例可能更清楚:

function example() {
  const result = Math.random() < 0.5 ? useX() : 1; // 有五成概率抛 ReferenceError
  const x = 10;

  return result;

  function useX() {
    return x;
  }
}

在上面的例子中,把 useX (函数)声明在函数末尾是完全有效的。问题出现在 x 尚未初始化时就调用它。说白了,当解释器仍处于 x 的暂时性死区时,解释器会拒绝我们访问 x 并抛出错误。

这其实贼棒!因为如果这个例子中使用 var 而非 const,就不会报错。函数只会简单地返回 undefined!

function example() {
  var result = useX(); // undefined
  var x = 10;

  return result; // undefined

  function useX() {
    return x;
  }
}

console.log(example()); // undefined

忒可怕了!

综上所述,暂时性死区实际上是 constlet 带来的贼好用的特性。那 TypeScript 凭啥不乐意使呢?原因在于性能

就为判断变量是不是在暂时性死区,解释器得耗贼多资源。我们上文讲过,这种判断无法通过静态分析完成,而是依赖于非确定性的运行时行为。这些损耗对 TypeScript 代码库影响显著。把大量声明语句迁移至 var 后,部分基准测试呈现出8%的性能提升

就笔者来说,非常庆幸现已无需使用 var 了。而对 TypeScript(团队)来说,想必又给迁移至 Go 添了条理由。