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.ts
和node 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+D83D
和U+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()
方法,直到耗尽为止,此时,done
为true
。
为了根据ECMAScript规范实现迭代协议,会生成try/catch/finally
块以进行正确的错误处理。
如果现在再次执行index.js
文件,会得到正确的结果:
$ node index.js B o o h !
请注意,如果咱们的代码是在没有本地定义该symbol
的环境中执行的,则仍然需要Symbol.iterator
的填充程序。例如,在 ES5 环境,如果未定义Symbol.iterator
,则将强制__values
帮助器函数创建不遵循正确迭代协议的综合数组迭代器。
在 ES2015 系列中使用 downlevelIteration
ES2015 增加了新的集合类型,比如Map
和Set
到标准库。在本节中,将介绍如何使用for...of
循环遍历Map
。
在下面的示例中,咱创建了一个从数字和它们各自的英文名称的数组。在构造函数中使用十个键值对(表示为两个元素的数组)初始化Map
。然后使用for...of
循环和数组解构模式将键值对分解为digit
和name
:
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
:
这是因为咱们的目标设置为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
,它们增加了代码大小,接下来咱们尝试削它一下。
使用--importHelpers
和tslib
减少代码大小
在上面的代码示例中,__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