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

做者:Marius Schulz
译者:前端小智
来源: https://mariusschulz.com/
点赞再看,养成习惯

本文 GitHub https://github.com/qq44924588... 上已经收录,更多往期高赞文章的分类,也整理了不少个人文档,和教程资料。欢迎Star和完善,你们面试能够参照考点复习,但愿咱们一块儿有点东西。javascript

TypeScript 2.3 引入了一个新的--downlevelIteration标志,为以 ES3 和 ES5 目标添加了对 ES6 迭代协议的彻底支持。for...of循环如今能够用正确的语义进行向下编译。html

使用 for...of 遍历数组

假设我们如今的tsconfig.json 设置 target 为 es5:前端

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

建立 indtx.ts 文件并输入如下内容:java

const numbers = [4, 8, 15, 16, 23, 42];

for (const number of numbers) {
  console.log(number);
}

由于它包含任何 TypeScript 特定的语法,因此不须要先经过TypeScript编译器就能够直接运行ts文件:node

$ node index.ts
4
8
15
16
23
42

如今将index.ts文件编译成index.jswebpack

tsc -p .

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

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);
}

若是运行这段代码,能够正常工做:es6

$ node index.js
4
8
15
16
23
42

运行node index.tsnode index.js是彻底相同的,这说明我们没有经过运行 TypeScript 编译器来改变程序的行为。github

使用 for...of 遍历字符串

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

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...

https://mariusschulz.com/blog...

https://www.tslang.cn/docs/re...


交流

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

https://github.com/qq44924588...

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

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

clipboard.png

相关文章
相关标签/搜索