楔子
众所周知,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的工作原理大致可以总结为:
- TypeScript 源码经过扫描器扫描之后变成一系列 Token;
- 解析器解析 token,得到一棵 AST 语法树;
- 绑定器遍历 AST 语法树,生成一系列 Symbol,并将这些 Symbol 连接到对应的节点上;
- 检查器再次扫描 AST,检查类型,并将错误收集起来;
- 发射器根据 AST 生成 JavaScript 代码。
在这里引申出几个概念,token、AST语法树、Symbol
什么是token
将TS字符流转换成token流是“编译原理”最开始的部分,这一块流程叫做“词法分析”,也称之为扫描(scanner); 先说什么是token,token其实是个结构体,每个token都是一个不可分割的最小单元,举个例子: I love China. 词法分析给出的4个token是:(I, 代词)、(love, 及物动词)、(China, 专有名词)、(., 句号)。
放在代码里就是:
token的属性
每一块都是一个token,每一个token都有自己的四个属性,分别是:type
、lexeme
、literal
、line
。
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的工作原理可以总结为:
- TypeScript 源码经过扫描器扫描之后变成一系列 Token;
- 解析器解析 token,得到一棵 AST 语法树;
- 绑定器遍历 AST 语法树,生成一系列 Symbol,并将这些 Symbol 连接到对应的节点上;
- 检查器再次扫描 AST,检查类型,并将错误收集起来;
- 发射器根据 AST 生成 JavaScript 代码。
这里再推荐一下深入理解TypeScript这本书,链接放在下面的参考文章了,有兴趣的可以去看一下。
参考文章
《深入理解ES6》