TypeScript是如何工作的(简单讲讲)

1,373 阅读4分钟
楔子

众所周知,TS是一门在JS基础上拓展的静态弱类型语言,从名字上来看,ts强调了它的核心是“type”,即“类型”。JS是一门解释型语言,只有在运行时才能收到类型检查的报错,举个例子:

let foo = 1;
foo.split(' ');
// Uncaught TypeError: foo.split is not a function
// 在运行时报错(foo.split 不是一个函数)

而TS在运行前会先编译成JS,编译阶段会进行类型检查,所以:

let foo: number = 1;
foo.split(' ');
// Property 'split' does not exist on type 'number'.
// 编译时会报错(数字没有 split 方法),无法通过编译
工作原理归纳

TS编译器分为以下几个关键部分:

  • Scanner 扫描器(scanner.ts
  • Parser 解析器(parser.ts
  • Binder 绑定器(binder.ts
  • Checker 检查器(checker.ts
  • Emitter 发射器(emitter.ts

TS的工作原理大致可以总结为:

  1. TypeScript 源码经过扫描器扫描之后变成一系列 Token;
  2. 解析器解析 token,得到一棵 AST 语法树;
  3. 绑定器遍历 AST 语法树,生成一系列 Symbol,并将这些 Symbol 连接到对应的节点上;
  4. 检查器再次扫描 AST,检查类型,并将错误收集起来;
  5. 发射器根据 AST 生成 JavaScript 代码。

在这里引申出几个概念,token、AST语法树、Symbol

什么是token

将TS字符流转换成token流是“编译原理”最开始的部分,这一块流程叫做“词法分析”,也称之为扫描(scanner); 先说什么是token,token其实是个结构体,每个token都是一个不可分割的最小单元,举个例子: I love China. 词法分析给出的4个token是:(I, 代词)、(love, 及物动词)、(China, 专有名词)、(., 句号)。

放在代码里就是: image.png

token的属性

每一块都是一个token,每一个token都有自己的四个属性,分别是:typelexemeliteralline

type就是token的类型,token的类型有如下几种:

public enum TokenType {
    // Single-character tokens.
    LEFT_PAREN, RIGHT_PAREN, LEFT_BRACE, RIGHT_BRACE, COMMA, DOT, MINUS, PLUS, SEMICOLON, SLASH, STAR,

    // One or two character tokens.
    BANG, BANG_EQUAL, EQUAL, EQUAL_EQUAL, GREATER, GREATER_EQUAL, LESS, LESS_EQUAL,

    // Literals.
    IDENTIFIER, STRING, NUMBER,

    // Keywords.
    AND, CLASS, ELSE, FALSE, FUN, FOR, IF, NIL, OR, PRINT, RETURN, SUPER, THIS, TRUE, VAR, WHILE,
    
    EOF
}

lexeme 用来保存这个 token 的字符串,比如 var 这个 token。它的 lexeme 就是 "var"。

而 line 是 token 所在的行,用来报错。

literal这个属性比较难搞,首先这个单词的解释是“字面常量”,但是这其实不是特别准确。在计算机领域,literal被定义为: A letter or symbol that stands for itself as opposed to a feature, function, or entity associated with it in a programming language.

翻译一下就是:

一种字母或符号,代表其本身,而不是与之相关联的编程语言中的特性、函数或实体。

简单来说,就是这个看起来像什么,它就是什么,表示一看就知道这是这个量本身嘛。

什么是AST语法树

AST全称是“Abstract Syntaxt Tree”抽象语法树。其实它就是一个json,比如说我写了一个求平方的函数:

function square(n) {
  return n * n;
}

上面的程序可以被表示成如下JavaScript Object的形式:

{
  type: "FunctionDeclaration",
  id: {
    type: "Identifier",
    name: "square"
  },
  params: [{
    type: "Identifier",
    name: "n"
  }],
  body: {
    type: "BlockStatement",
    body: [{
      type: "ReturnStatement",
      argument: {
        type: "BinaryExpression",
        operator: "*",
        left: {
          type: "Identifier",
          name: "n"
        },
        right: {
          type: "Identifier",
          name: "n"
        }
      }
    }]
  }
}

AST的每一层都拥有相同的结构:

{
  type: "FunctionDeclaration",
  id: {...},
  params: [...],
  body: {...}
}

这样的每一层结构也被叫做节点(Node)。一个AST可以由单一节点或者多个节点构成,它们组合在一起可以描述用于静态分析的程序语法。

具体关于AST的内容可以参考这篇文章:深入理解TypeScript,在这里就不详细展开了。

什么是Symbol

Symbol是个很有意思的特性(我不知道用什么词汇来描述比较合适),我们知道,在ES5早期的时候,语言包括5种原始类型,分别是:string、number、boolean、null和undefine,在ES6中引入了第六种原始类型:Symbol。

在Symbol出现以前,人们一直通过属性名来访问所有属性,无论属性名由什么元素构成,全部通过一个字符串类型的名称来访问,而Symbol则是用来打破这种常规思路,用于创建必须通过Symbol才能引用的属性,使它们比较难以被意外覆写而改变,非常适用于那些需要一定程度保护的功能。

在TS编译器中,Symbol是绑定的结果,Symbol将AST中的声明节点与其他相同实体的其他声明相连。

总结

这里再回顾一下开头, TS的工作原理可以总结为:

  1. TypeScript 源码经过扫描器扫描之后变成一系列 Token;
  2. 解析器解析 token,得到一棵 AST 语法树;
  3. 绑定器遍历 AST 语法树,生成一系列 Symbol,并将这些 Symbol 连接到对应的节点上;
  4. 检查器再次扫描 AST,检查类型,并将错误收集起来;
  5. 发射器根据 AST 生成 JavaScript 代码。

这里再推荐一下深入理解TypeScript这本书,链接放在下面的参考文章了,有兴趣的可以去看一下。

参考文章

TypeScript是如何工作的

编译原理——如何将字符流转换为 token 流

深入理解TypeScript

什么是Literal

《深入理解ES6》