1. 首页

【TypeScript 进化史 — 12】ES5-ES3 的生成器和迭代支持及 –checkJS选项下 .js 文件中的错误

TypeScript 2.3 引入了一个新的--downlevelIteration标志,为以 ES3 和 ES5 目标添加了对 ES6 迭代协议的完全支持。for...of循环现在可以用正确的语义进行向下编译。

使用 for...of 遍历数组

假设咱们现在的tsconfig.json 设置 target 为 es5:


{ "compilerOptions": { "target": "es5" } }

创建 indtx.ts 文件并输入以下内容:


const numbers = [4, 8, 15, 16, 23, 42]; for (const number of numbers) { console.log(number); }

因为它包含任何 TypeScript 特定的语法,所以不需要先通过TypeScript编译器就可以直接运行ts文件:


$ node index.ts 4 8 15 16 23 42

现在将index.ts文件编译成index.js


tsc -p .

查看生成的 JS 代码,可以看 到TypeScript 编译器生成了一个传统的基于索引的for循环来遍历数组:


var numbers = [4, 8, 15, 16, 23, 42]; for (var _i = 0, numbers_1 = numbers; _i < numbers_1.length; _i++) { var number = numbers_1[_i]; console.log(number); }

如果运行这段代码,可以正常工作:


$ node index.js 4 8 15 16 23 42

运行node index.tsnode index.js是完全相同的,这说明咱们没有通过运行 TypeScript 编译器来改变程序的行为。

使用 for…of 遍历字符串

在来看看 for...of的另外一个例子,这次咱们遍历的是字符串而不是数组:


const text = "Booh! 👻"; for (const char of text) { console.log(char); }

同样,咱们可以直接运行 node index.ts,因为咱们的代码仅使用ES2015语法,而没有TypeScript专用。


$ node index.ts B o o h ! 👻

现在将index.ts文件编译成index.js。当以 ES3 或 ES5 为目标时,TypeScript 编译器将为上述代码生成一个基于索引的for循环的代码:


var text = "Booh! 👻"; for (var _i = 0, text_1 = text; _i < text_1.length; _i++) { var char = text_1[_i]; console.log(char); }

不幸的是,生成的 JS 代码的行为与原始的 TypeScript 版本明显不同:


$ node index.js B o o h ! � �

幽灵表情符号或代码 U+1F47B,更准确地说是由两个代码单元U+D83DU+DC7B组成。因为对字符串进行索引将返回该索引处的代码单元(而不是代码点),所以生成的for循环将幽灵表情符分解为单独的代码单元。

另一方面,字符串迭代协议遍历字符串的每个代码点,这就是两个程序的输出不同的原因。通过比较字符串的length 属性和字符串迭代器生成的序列的长度,可以确定它们之间的差异。


const ghostEmoji = "\u{1F47B}"; console.log(ghostEmoji.length); // 2 console.log([...ghostEmoji].length); // 1

简单的说:当目标为 ES3 或 ES5 时,使用for...of循环遍历字符串并不总是正确。这也是 TypeScript 2.3引入的新--downlevelIteration标志原因。

--downlevelIteration 标志

咱们之前的index.ts


const text = "Booh! 👻"; for (const char of text) { console.log(char); }

现在咱们修改tsconfig.json文件,并将新的downlevelIteration标志设为true


{ "compilerOptions": { "target": "es5", "downlevelIteration": true } }

再次运行编译器,将生成以下 JS 代码


var __values = (this && this.__values) || function (o) { var m = typeof Symbol === "function" && o[Symbol.iterator], i = 0; if (m) return m.call(o); return { next: function () { if (o && i >= o.length) o = void 0; return { value: o && o[i++], done: !o }; } }; }; var text = "Booh! 👻"; try { for (var text_1 = __values(text), text_1_1 = text_1.next(); !text_1_1.done; text_1_1 = text_1.next()) { var char = text_1_1.value; console.log(char); } } catch (e_1_1) { e_1 = { error: e_1_1 }; } finally { try { if (text_1_1 && !text_1_1.done && (_a = text_1.return)) _a.call(text_1); } finally { if (e_1) throw e_1.error; } } var e_1, _a;

如你所见,生成的代码比简单的for循环复杂得多,这是因为它包含正确的迭代协议实现:

  • __values帮助器函数将查找[Symbol.iterator]方法,如果找到该方法,则将其调用。如果不是,它将在对象上创建一个合成数组迭代器。
  • for 循环无需遍历每个代码单元,而是调用迭代器的next()方法,直到耗尽为止,此时,donetrue

为了根据ECMAScript规范实现迭代协议,会生成try/catch/finally块以进行正确的错误处理。

如果现在再次执行index.js文件,会得到正确的结果:


$ node index.js B o o h ! 👻

请注意,如果咱们的代码是在没有本地定义该symbol的环境中执行的,则仍然需要Symbol.iterator的填充程序。例如,在 ES5 环境,如果未定义Symbol.iterator,则将强制__values帮助器函数创建不遵循正确迭代协议的综合数组迭代器。

在 ES2015 系列中使用 downlevelIteration

ES2015 增加了新的集合类型,比如MapSet到标准库。在本节中,将介绍如何使用for...of循环遍历Map

在下面的示例中,咱创建了一个从数字和它们各自的英文名称的数组。在构造函数中使用十个键值对(表示为两个元素的数组)初始化Map。然后使用for...of循环和数组解构模式将键值对分解为digitname

const digits = new Map([
  [0, "zero"],
  [1, "one"],
  [2, "two"],
  [3, "three"],
  [4, "four"],
  [5, "five"],
  [6, "six"],
  [7, "seven"],
  [8, "eight"],
  [9, "nine"]
]);

for (const [digit, name] of digits) {
  console.log(`${digit} -> ${name}`);
}


这是完全有效的 ES6 代码,可以正常运行:


$ node index.ts 0 -> zero 1 -> one 2 -> two 3 -> three 4 -> four 5 -> five 6 -> six 7 -> seven 8 -> eight 9 -> nine

然而,TypeScript 编译器并不会这样认为,说它找不到Map

Js中文网 · 前端进阶资源教程 www.javascriptc.com

这是因为咱们的目标设置为ES5,它没有实现 Map 。假设咱们已经为Map提供了一个polyfill,这样程序就可以在运行时运行,那么咱们该如何编译这段代码呢

解决方案是将"es2015.collection""es2015.iterable"值添加到咱们的tsconfig.json文件中的lib选项中。这告诉 TypeScript 编译器可以假定在运行时查找 es6 集合实现和 Symbol.iterator

但是,一旦明确指定lib选项,其默认值将不再适用,因此,还要添加"dom""es5",以便可以访问其他标准库方法。

这是生成的tsconfig.json


{ "compilerOptions": { "target": "es5", "downlevelIteration": true, "lib": [ "dom", "es5", "es2015.collection", "es2015.iterable" ] } }

现在,TypeScript 编译器不再报错并生成以下 JS 代码:


var __values = (this && this.__values) || function (o) { var m = typeof Symbol === "function" && o[Symbol.iterator], i = 0; if (m) return m.call(o); return { next: function () { if (o && i >= o.length) o = void 0; return { value: o && o[i++], done: !o }; } }; }; var __read = (this && this.__read) || function (o, n) { var m = typeof Symbol === "function" && o[Symbol.iterator]; if (!m) return o; var i = m.call(o), r, ar = [], e; try { while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); } catch (error) { e = { error: error }; } finally { try { if (r && !r.done && (m = i["return"])) m.call(i); } finally { if (e) throw e.error; } } return ar; }; var digits = new Map([ [0, "zero"], [1, "one"], [2, "two"], [3, "three"], [4, "four"], [5, "five"], [6, "six"], [7, "seven"], [8, "eight"], [9, "nine"] ]); try { for (var digits_1 = __values(digits), digits_1_1 = digits_1.next(); !digits_1_1.done; digits_1_1 = digits_1.next()) { var _a = __read(digits_1_1.value, 2), digit = _a[0], name_1 = _a[1]; console.log(digit + " -> " + name_1); } } catch (e_1_1) { e_1 = { error: e_1_1 }; } finally { try { if (digits_1_1 && !digits_1_1.done && (_b = digits_1.return)) _b.call(digits_1); } finally { if (e_1) throw e_1.error; } } var e_1, _b;

在次执行就能正确输出了。

不过,咱们还要注意一件事,现在,生成的 JS 代码包括两个辅助函数__values__read,它们增加了代码大小,接下来咱们尝试削它一下。

使用--importHelperstslib减少代码大小

在上面的代码示例中,__values__read 辅助函数被内联到生成的 JS 代码中。如果要编译包含多个文件的 TypeScript 项目,这是很不好的,每个生成的 JS 文件都包含执行该文件所需的所有帮助程序,从而大大的增加了代码的大小。

在较好的的项目配置中,咱们会使用诸如 webpack 之类的绑定器将所有模块捆绑在一起。如果 webpack 不止一次地包含一个帮助函数,那么它生成的包就会不必要地大。

解决方案是使用--importHelpers编译器选项和tslib 包。当指定时,--importHelpers 会告诉TypeScript 编译器从tslib导入所有帮助函数。像 webpack 这样的捆绑器可以只内联一次 npm 包,从而避免代码重复。

为了演示--importHelpers 的效果,首先打开index.ts文件并将函数导出到模块中

const digits = new Map([
  [0, "zero"],
  [1, "one"],
  [2, "two"],
  [3, "three"],
  [4, "four"],
  [5, "five"],
  [6, "six"],
  [7, "seven"],
  [8, "eight"],
  [9, "nine"]
]);

export function printDigits() {
  for (const [digit, name] of digits) {
    console.log(`${digit} -> ${name}`);
  }
}

现在咱们需要修改编译器配置并将importHelpers设置为true,如下所示:


{ "compilerOptions": { "target": "es5", "downlevelIteration": true, "importHelpers": true, "lib": [ "dom", "es5", "es2015.collection", "es2015.iterable" ] } }

下面经过编译器运行后得到的JS代码:


"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var tslib_1 = require("tslib"); var digits = new Map([ [0, "zero"], [1, "one"], [2, "two"], [3, "three"], [4, "four"], [5, "five"], [6, "six"], [7, "seven"], [8, "eight"], [9, "nine"] ]); function printDigits() { try { for (var digits_1 = tslib_1.__values(digits), digits_1_1 = digits_1.next(); !digits_1_1.done; digits_1_1 = digits_1.next()) { var _a = tslib_1.__read(digits_1_1.value, 2), digit = _a[0], name_1 = _a[1]; console.log(digit + " -> " + name_1); } } catch (e_1_1) { e_1 = { error: e_1_1 }; } finally { try { if (digits_1_1 && !digits_1_1.done && (_b = digits_1.return)) _b.call(digits_1); } finally { if (e_1) throw e_1.error; } } var e_1, _b; } exports.printDigits = printDigits;

注意,代码不再包含内联的帮助函数,相反,是从tslib导入。

--checkJS 选项下 .js 文件中的错误

在 TypeScript 2.2 之前,类型检查和错误报告只能在.ts文件中使用。从 TypeScript 2.3 开始,编译器现在可以对普通的.js文件进行类型检查并报告错误。


let foo = 42; // [js] Property 'toUpperCase' does not exist on type 'number'. let upperFoo = foo.toUpperCase();

这里有一个新的--checkJs标志,它默认支持所有.js文件的类型检查。另外,三个以注释形式出现的新指令允许对应该检查哪些 JS 代码片段进行更细粒度的控制:

  • 使用// @ ts-check注释对单个文件的类型检查。
  • 使用// @ts-nocheck注释来跳过对某些文件的检查
  • 使用// @ ts-ignore注释为单行选择不进行类型检查。

这些选项使咱们可以使用黑名单方法和白名单方法。请注意,无论哪种方式,都应将--allowJs选项设置为true,以便首先允许在编译中包含 JS 文件。

黑名单的方法

黑名单方法背后的实现方式是默认情况下对每个 JS 文件进行类型检查。这可以通过将--checkJs编译器选项设置为true来实现。也可以通过在每个文件的顶部添加// @ ts-nocheck注释来将特定文件列入黑名单。

如果你想要一次检查一下 JS 代码库,则建议使用这种方法。如果报告了错误,则可以立即修复它,使用// @ ts-ignore忽略导致错误的行,或使用// @ ts-nocheck忽略整个文件。

白名单的方法

白名单方法背后的实现方式是默认情况下只对选定的 JS 文件进行类型检查。这可以通过将- checkJs编译器选项设置为false并在每个选定文件的顶部添加// @ts-check注释来实现。

如果你想要在大型 JS代码库中逐步引入类型检查,推荐这种方法。这样,将不会一次被太多错误淹没。每当在处理文件时,请考虑先添加// @ ts-check并修复潜在的类型错误,以有效地实现蠕变迁移。

从 JS迁移到 TypeScript

一旦对整个代码库进行了类型检查,从 JS (和.js文件)迁移到 TypeScript (和.ts文件)就容易多了。使用白名单或黑名单方法,咱们可以很快的移到,同时准备迁移到完全静态类型的代码库(由TypeScript提供支持)。

往期阅读

作者:前端小智
链接:https://mariusschulz.com/blog/downlevel-iteration-for-es3-es5-in-typescript

看完两件小事

如果你觉得这篇文章对你挺有启发,我想请你帮我两个小忙:

  1. 关注我们的 GitHub 博客,让我们成为长期关系
  2. 把这篇文章分享给你的朋友 / 交流群,让更多的人看到,一起进步,一起成长!
  3. 关注公众号 「画漫画的程序员」,公众号后台回复「资源」 免费领取我精心整理的前端进阶资源教程

JS中文网是中国领先的新一代开发者社区和专业的技术媒体,一个帮助开发者成长的社区,目前已经覆盖和服务了超过 300 万开发者,你每天都可以在这里找到技术世界的头条内容。欢迎热爱技术的你一起加入交流与学习,JS中文网的使命是帮助开发者用代码改变世界

本文著作权归作者所有,如若转载,请注明出处

转载请注明:文章转载自「 Js中文网 · 前端进阶资源教程 」https://www.javascriptc.com

标题:【TypeScript 进化史 — 12】ES5-ES3 的生成器和迭代支持及 –checkJS选项下 .js 文件中的错误

链接:https://www.javascriptc.com/3350.html

« 深入理解JavaScript系列(24)- JavaScript与DOM(下
带你详细了解Proxy 的妙处之巧用»
Flutter 中文教程资源

相关推荐

QR code