解释解释解析器
希望能用这篇文章解释解析器能把解析器解释清楚,先来段绕口令醒醒神 😬
前言
不知道大家有没有想过一个问题:"我们编写的JS代码只是一些字符串,它是怎么被机器执行的?",下面我们带着这个问题进入文章。
概念
首先,我们编写的JS代码对于机器来说只是一个个字符,机器并不是一开始就认识他们。
运行环境
我们知道,JS可以运行在浏览器环境和node环境,这些环境都内置了JS引擎,我们接触较多的是谷歌开源的V8引擎,除此之外其他常见的引擎比如有:
- 由 Mozilla 为 Firefox 开发的 SpiderMonkey
- 为 Safari 浏览器提供支持的 JavaScriptCore
JavaScript定义
JavaScript(JS)是一种具有函数优先特性的轻量级、解释型或者说即时编译型的编程语言
语言类型
从JS定义中看到,提到了解释型、即时编译型语言,其实还有一种类型叫编译型语言
那什么是解析型,什么是编译型,什么是即时编译型?
编译型语言
在程序执行之前,需要经过编译器的编译过程,并且编译之后会直接保留机器能读懂的二进制文件,这样每次运行程序时,都可以直接运行该二进制文件,而不需要再次重新编译了。比如 C/C++、GO 等都是编译型语言。
解释型语言
而由解释型语言编写的程序,在每次运行时都需要通过解释器对程序进行动态解释和执行。比如 Python、JavaScript 等都属于解释型语言。
即时编译型语言
即时编译(JIT),也称为动态翻译或运行时编译,是一种执行计算机代码的方法,这种方法设计在程序执行过程中(在执行期)而不是在执行之前进行编译;实现 JIT 编译器的系统通常会不断地分析正在执行的代码,并确定代码中可被即时编译加速的部分,在这些部分中,由编译或重新编译带来的性能提高将超过编译该代码的开销。
JIT编译是两种传统的机器代码翻译方法——提前编译和解释器的结合,它结合了两者的优点和缺点。
V8执行JS代码流程
我们可以看到V8 在执行过程中既有解释器Ignition,又有编译器TurboFan
小结
我们可以发现,不管是什么类型语言的执行第一步都是源代码到AST,进入本篇文章的重点:怎么得到AST,看图上可以得知,首先是对源代码也就是代码字符进行词法分析和语法分析。
解析器
词法分析
词法分析是计算机科学中将字符序列转换为标记**(token)序列的过程。进行词法分析的程序或者函数叫作词法分析器(lexical analyzer,简称lexer),也叫扫描器(scanner)。
语法分析
在计算机科学和语言学中,语法分析(英语:syntactic analysis,也叫 parsing)是根据某种给定的形式文法对由单词序列(如英语单词序列)构成的输入文本进行分析并确定其语法结构的一种过程。
一个简单的词法解析器例子🌰
伪代码字符串
(add 2 (subtract 4 2))
第一步:词法分析生成token
生成tokens过程是一个字符一个字符的往后遍历分析,处理各种场景;
比如处理上面伪代码字符串中「add」 关键字的场景(代码59-71行):遍历到'a'的时候,会继续往后遍历'd',再遍历'd',直到后一个字符不满足 /[a-z]/i 条件,最后得到'add'为一个token
function tokenizer(input) {
let current = 0;
let tokens = [];
while (current < input.length) {
let char = input[current];
if (char === '(') {
tokens.push({
type: 'paren',
value: '(',
});
current++;
continue;
}
if (char === ')') {
tokens.push({
type: 'paren',
value: ')',
});
current++;
continue;
}
let WHITESPACE = /\s/;
if (WHITESPACE.test(char)) {
current++;
continue;
}
let NUMBERS = /[0-9]/;
if (NUMBERS.test(char)) {
let value = '';
while (NUMBERS.test(char)) {
value += char;
char = input[++current];
}
tokens.push({ type: 'number', value });
continue;
}
if (char === '"') {
let value = '';
char = input[++current];
while (char !== '"') {
value += char;
char = input[++current];
}
char = input[++current];
tokens.push({ type: 'string', value });
continue;
}
let LETTERS = /[a-z]/i;
if (LETTERS.test(char)) {
let value = '';
while (LETTERS.test(char)) {
value += char;
char = input[++current];
}
tokens.push({ type: 'name', value });
continue;
}
throw new TypeError('I dont know what this character is: ' + char);
}
return tokens;
}
- 得到tokens如下
[
{ type: 'paren', value: '(' },
{ type: 'name', value: 'add' },
{ type: 'number', value: '2' },
{ type: 'paren', value: '(' },
{ type: 'name', value: 'subtract' },
{ type: 'number', value: '4' },
{ type: 'number', value: '2' },
{ type: 'paren', value: ')' },
{ type: 'paren', value: ')' }
];
第二步:处理tokens,生成AST
function parser(tokens) {
let current = 0;
function walk() {
let token = tokens[current];
if (token.type === 'number') {
current++;
return {
type: 'NumberLiteral',
value: token.value,
};
}
if (token.type === 'string') {
current++;
return {
type: 'StringLiteral',
value: token.value,
};
}
if (
token.type === 'paren' &&
token.value === '('
) {
token = tokens[++current];
let node = {
type: 'CallExpression',
name: token.value,
params: [],
};
token = tokens[++current];
while (
(token.type !== 'paren') ||
(token.type === 'paren' && token.value !== ')')
) {
node.params.push(walk());
token = tokens[current];
}
current++;
return node;
}
throw new TypeError(token.type);
}
let ast = {
type: 'Program',
body: [],
};
while (current < tokens.length) {
ast.body.push(walk());
}
return ast;
}
- 得到AST如下
{
type: 'Program',
body: [{
type: 'CallExpression',
name: 'add',
params: [{
type: 'NumberLiteral',
value: '2'
}, {
type: 'CallExpression',
name: 'subtract',
params: [{
type: 'NumberLiteral',
value: '4'
}, {
type: 'NumberLiteral',
value: '2'
}]
}]
}]
};
- AST类型节点
我们看到了ast中有一些type,这些就是ast的节点类型,是提前定义好的,包含了这种语言的所有语法类型,文章中例子只是简单定义了几种,AST还有很多其他类型的节点,如下图:
小结
第二步完成,生成好AST之后解析器的工作就完成了,后续我们就可以对这个ast进行自己想要的处理了,比如写babel插件、eslint插件、webpack插件等等;
每种解析器生成的ast都有自己的规范,但是思路都是一样的,比如常用js的解析器有babel/parser,acorn等。
现在市面上有各种各样的语言,每种语言也有各种各样的解析器,感兴趣的同学可以到AST Explorer尝试下不同语言的不同解析器;
总结
回到前言中的问题:"我们编写的JS代码只是一些字符串,它是怎么被机器执行的?",本篇文章只讲了第一步解析器,后续还有很多流程很多概念,比如字节码、机器码、解释器等等,感兴趣的同学可以自己继续了解探索
最后
留个问题讨论,文中提到C/C++、GO 等都是编译型语言,那么这些语言的作者是用什么语言,去编译他的新语言的呢,能自己编译自己么?
参考