Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
husky-dot opened this issue Jan 5, 2020 · 0 comments

Comments

@husky-dot
Copy link
Owner

作者:Marius Schulz
译者:前端小智
来源:https://mariusschulz.com/

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

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提供支持)。


代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug

原文:

https://mariusschulz.com/blog/downlevel-iteration-for-es3-es5-in-typescript

https://mariusschulz.com/blog/type-checking-javascript-files-with-checkjs-in-typescript

https://www.tslang.cn/docs/release-notes/typescript-2.3.html


交流

干货系列文章汇总如下,觉得不错点个Star,欢迎 加群 互相学习。

https://github.com/qq449245884/xiaozhi

我是小智,公众号「大迁世界」作者,对前端技术保持学习爱好者。我会经常分享自己所学所看的干货,在进阶的路上,共勉!

关注公众号,后台回复福利,即可看到福利,你懂的。

clipboard.png

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant