【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

相關文章
相關標籤/搜索