TypeScript

【TypeScript 演化史 — 第十二章】ES5/ES3 的生成器和迭代支持及 –checkJS选项下 .js 文件中的错误

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

使用 for...of 遍历数组

假设咱们现在的tsconfig.json 设置 targetes5:

{
  "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

clipboard.png

这是因为咱们的目标设置为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

(32)

本文由 Web秀 作者:Javan 发表,转载请注明来源!

关键词:, , , , , ,

热评文章

发表评论

电子邮件地址不会被公开。 必填项已用*标注