理解Babel是如何编译JS代码的及理解抽象语法树(AST)
Babel是如何编译JS代码的及理解抽象语法树(AST)
1. Babel的作用是?
很多浏览器目前还不支持ES6的代码,但是我们可以通过Babel将ES6的代码转译成ES5代码,让所有的浏览器都能理解的代码,这就是Babel的作用。
2. Babel是如何工作的?
Babel的编译过程和大多数其他语言的编译器大致相同,可以分为三个阶段。
1. 解析(PARSE):将代码字符串解析成抽象语法树。
2. 转换(TRANSFORM):对抽象语法树进行转换操作。
3. 生成(GENERATE): 根据变换后的抽象语法树再生成代码字符串。
比如我们在 .babelrc里配置的presets和plugins是在第二步进行的。
我们可以看一下下面的流程图就可以很清晰了:
3. 什么是抽象语法树(AST)?
我们知道javascript程序一般是由一系列的字符组成的,每一个字符都有一些含义,比如我们可以使用匹配的字符([], {}, ()), 或一些其他成对的字符('', "")和代码缩进让程序解析更加简单,但是对计算机并不适用,这些字符在内存中仅仅是个数值,但是计算机并不知道一个程序内部有多少个变量这些高级问题,
这个时候我们需要寻找一些能让计算机理解的方式,这个时候,抽象语法树诞生了。
4. 抽象语法树是如何产生的?
我们通过上面知道,Babel的工作的第一步是 解析操作,将代码字符串解析成抽象语法树,那么抽象语法树就是在解析过程中产生的。其实解析又可以分成两个
步骤:
4-1 分词: 将整个代码字符串分割成 语法单元数组。
4-2 语义分析:在分词结果的基础之上分析 语法单元之间的关系。
分词:
先来理解一下什么是语法单元? 语法单元是被解析语法当中具备实际意义的最小单元,简单的来理解就是自然语言中的词语。
比如我们来看下面的一句话:
2022年亚运会将在杭州举行,下面我们可以把这句话拆分成最小单元:2022年, 亚运会, 将, 在, 杭州, 举行。这就是我们所说的分词。也是最小单元,
如果我们把它再拆分出去的话,那就没有什么实际意义了。
那么JS代码中有哪些语法单元呢?大致有下面这些:
1. 空白。JS中连续的空格,换行,缩进等这些如果不在字符串里面,就没有任何实际的意义,因此我们可以将连续的空白组合在一起作为一个语法单元。
2. 注释。行注释或块注释,对于编写人或维护人注释是有意义的,但是对于计算机来说知道这是个注释就可以了,并不关心注释的含义,因此我们可以将
注释理解为一个不可拆分的语法单元。
3. 字符串。对计算机而言,字符串的内容会参与计算或显示,因此有可以为一个语法单元。
4. 数字。JS中有16,10,8进制以及科学表达式等语法,因此数字也可以理解一个语法单元。
5. 标识符。没有被引号括起来的连续字符,可包含字母 _, $ 及数字,或 true, false等这些内置常量,或 if,return,function等这些关键字。
6. 运算符: +, -, *, /, >, < 等。
7,还有一些其他的,比如括号,中括号,大括号,分号,冒号,点等等。
下面我们来看看代码内是如何分词的?
比如如下代码:
if (1 > 0) {
alert("aa");
}
我们希望得到的分词是如下:
'if' ' ' '(' '1' ' ' '>' ' ' '0' )' ' ' '{' '\n ' 'alert' '(' "aa" ')' ";" '\n' '}'
下面我们就来一个个字符进行遍历,然后分情况判断,如下代码:
<!DOCTYPE html>
<html>
<head>
<title>分词</title>
</head>
<body>
<script>
function tokenizeCode(code) {
var tokens = []; // 保存结果数组
for (var i = 0; i < code.length; i++) {
// 从0开始 一个个字符读取
var currentChar = code.charAt(i);
if (currentChar === ';') {
tokens.push({
type: 'sep',
value: currentChar
});
// 该字符已经得到解析了,直接循环下一个
continue;
}
if (currentChar === '(' || currentChar === ')') {
tokens.push({
type: 'parens',
value: currentChar
});
continue;
}
if (currentChar === '{' || currentChar === '}') {
tokens.push({
type: 'brace',
value: currentChar
});
continue;
}
if (currentChar === '>' || currentChar === '<') {
tokens.push({
type: 'operator',
value: currentChar
});
continue;
}
if (currentChar === '"' || currentChar === '\'') {
// 如果是单引号或双引号,表示一个字符的开始
var token = {
type: 'string',
value: currentChar
};
tokens.push(token);
var closer = currentChar; // 表示下一个字符是不是被转译了
var escaped = false;
// 循环遍历 寻找字符串的末尾
for(i++; i < code.length; i++) {
currentChar = code.charAt(i);
// 将当前遍历到的字符先加到字符串内容中
token.value += currentChar;
if (escaped) {
// 如果当前为true的话,就变为false,然后该字符就不做特殊的处理
escaped = false;
} else if (currentChar === '\\') {
// 如果当前的字符是 \, 将转译状态变为true,下一个字符不会被做处理
escaped = true;
} else if (currentChar === closer) {
break;
}
}
continue;
} // 数字做处理
if (/[0-9]/.test(currentChar)) {
// 如果数字是以 0 到 9的字符开始的话
var token = {
type: 'number',
value: currentChar
};
tokens.push(token);
// 继续遍历,如果下一个字符还是数字的话,比如0到9或小数点的话
for (i++; i < code.length; i++) {
currentChar = code.charAt(i);
if (/[0-9\.]/.test(currentChar)) {
// 先不考虑多个小数点 或 进制的情况下
token.value += currentChar;
} else {
// 如果下一个字符不是数字的话,需要把i值返回原来的位置上,需要减1
i--;
break;
}
}
continue;
}
// 标识符是以字母,$, _开始的 做判断
if (/[a-zA-Z\$\_]/.test(currentChar)) {
var token = {
type: 'identifier',
value: currentChar
};
tokens.push(token);
// 继续遍历下一个字符,如果下一个字符还是以字母,$,_开始的话
for (i++; i < code.length; i++) {
currentChar = code.charAt(i);
if (/[a-zA-Z0-9\$\_]/.test(currentChar)) {
token.value += currentChar;
} else {
i--;
break;
}
}
continue;
} // 连续的空白字符组合在一起
if (/\s/.test(currentChar)) {
var token = {
type: 'whitespace',
value: currentChar
}
tokens.push(token);
// 继续遍历下一个字符
for (i++; i < code.length; i++) {
currentChar = code.charAt(i);
if (/\s/.test(currentChar)) {
token.value += currentChar;
} else {
i--;
break;
}
}
continue;
}
// 更多的字符判断 ......
// 遇到无法理解的字符 直接抛出异常
throw new Error('Unexpected ' + currentChar);
}
return tokens;
}
var tokens = tokenizeCode(`
if (1 > 0) {
alert("aa");
}
`);
console.log(tokens);
</script>
</body>
</html>
打印的结果如下:
/*
[
{type: "whitespace", value: "\n"},
{type: "identifier", value: "if"},
{type: "whitespace", value: " "},
{type: "parens", value: "("},
{type: "number", value: "1"},
{type: "whitespace", value: " "},
{type: "operator", value: ">"},
{type: "whitespace", value: " "},
{type: "number", value: "0"},
{type: "parens", value: ")"},
{type: "whitespace", value: " "},
{type: "brace", value: "{"},
{type: "whitespace", value: "\n"},
{type: "identifier", value: "alert"},
{type: "parens", value: "("},
{type: "string", value: "'aa'"},
{type: "parens", value: ")"},
{type: "sep", value: ";"},
{type: "whitespace", value: "\n"},
{type: "brace", value: "}"},
{type: "whitespace", value: "\n"}
]
*/
语义分析:
语义分析是把词汇进行立体的组合,确定有多重意义的词语最终是什么意思,多个词语之间有什么关系以及又如何在什么地方断句等等。我们对上面的输出代码再进行语义分析了,请看如下代码:
<!DOCTYPE html>
<html>
<head>
<title>分词</title>
</head>
<body>
<script>
var parse = function(tokens) {
let i = -1; // 用于标识当前遍历位置
let curToken; // 用于记录当前符号
// 读取下一个语句
function nextStatement () { // 暂存当前的i,如果无法找到符合条件的情况会需要回到这里
stash(); // 读取下一个符号
nextToken();
if (curToken.type === 'identifier' && curToken.value === 'if') {
// 解析 if 语句
const statement = {
type: 'IfStatement',
};
// if 后面必须紧跟着 (
nextToken();
if (curToken.type !== 'parens' || curToken.value !== '(') {
throw new Error('Expected ( after if');
} // 后续的一个表达式是 if 的判断条件
statement.test = nextExpression(); // 判断条件之后必须是 )
nextToken();
if (curToken.type !== 'parens' || curToken.value !== ')') {
throw new Error('Expected ) after if test expression');
} // 下一个语句是 if 成立时执行的语句
statement.consequent = nextStatement(); // 如果下一个符号是 else 就说明还存在 if 不成立时的逻辑
if (curToken === 'identifier' && curToken.value === 'else') {
statement.alternative = nextStatement();
} else {
statement.alternative = null;
}
commit();
return statement;
} if (curToken.type === 'brace' && curToken.value === '{') {
// 以 { 开头表示是个代码块,我们暂不考虑JSON语法的存在
const statement = {
type: 'BlockStatement',
body: [],
};
while (i < tokens.length) {
// 检查下一个符号是不是 }
stash();
nextToken();
if (curToken.type === 'brace' && curToken.value === '}') {
// } 表示代码块的结尾
commit();
break;
}
// 还原到原来的位置,并将解析的下一个语句加到body
rewind();
statement.body.push(nextStatement());
}
// 代码块语句解析完毕,返回结果
commit();
return statement;
} // 没有找到特别的语句标志,回到语句开头
rewind(); // 尝试解析单表达式语句
const statement = {
type: 'ExpressionStatement',
expression: nextExpression(),
};
if (statement.expression) {
nextToken();
if (curToken.type !== 'EOF' && curToken.type !== 'sep') {
throw new Error('Missing ; at end of expression');
}
return statement;
}
}
// 读取下一个表达式
function nextExpression () {
nextToken();
if (curToken.type === 'identifier') {
const identifier = {
type: 'Identifier',
name: curToken.value,
};
stash();
nextToken();
if (curToken.type === 'parens' && curToken.value === '(') {
// 如果一个标识符后面紧跟着 ( ,说明是个函数调用表达式
const expr = {
type: 'CallExpression',
caller: identifier,
arguments: [],
}; stash();
nextToken();
if (curToken.type === 'parens' && curToken.value === ')') {
// 如果下一个符合直接就是 ) ,说明没有参数
commit();
} else {
// 读取函数调用参数
rewind();
while (i < tokens.length) {
// 将下一个表达式加到arguments当中
expr.arguments.push(nextExpression());
nextToken();
// 遇到 ) 结束
if (curToken.type === 'parens' && curToken.value === ')') {
break;
}
// 参数间必须以 , 相间隔
if (curToken.type !== 'comma' && curToken.value !== ',') {
throw new Error('Expected , between arguments');
}
}
}
commit();
return expr;
}
rewind();
return identifier;
}
if (curToken.type === 'number' || curToken.type === 'string') {
// 数字或字符串,说明此处是个常量表达式
const literal = {
type: 'Literal',
value: eval(curToken.value),
};
// 但如果下一个符号是运算符,那么这就是个双元运算表达式
stash();
nextToken();
if (curToken.type === 'operator') {
commit();
return {
type: 'BinaryExpression',
left: literal,
right: nextExpression(),
};
}
rewind();
return literal;
}
if (curToken.type !== 'EOF') {
throw new Error('Unexpected token ' + curToken.value);
}
}
// 往后移动读取指针,自动跳过空白
function nextToken () {
do {
i++;
curToken = tokens[i] || { type: 'EOF' };
} while (curToken.type === 'whitespace');
}
// 位置暂存栈,用于支持很多时候需要返回到某个之前的位置
const stashStack = [];
function stash () {
// 暂存当前位置
stashStack.push(i);
}
function rewind () {
// 解析失败,回到上一个暂存的位置
i = stashStack.pop();
curToken = tokens[i];
}
function commit () {
// 解析成功,不需要再返回
stashStack.pop();
}
const ast = {
type: 'Program',
body: [],
};
// 逐条解析顶层语句
while (i < tokens.length) {
const statement = nextStatement();
if (!statement) {
break;
}
ast.body.push(statement);
}
return ast;
};
var ast = parse([
{type: "whitespace", value: "\n"},
{type: "identifier", value: "if"},
{type: "whitespace", value: " "},
{type: "parens", value: "("},
{type: "number", value: "1"},
{type: "whitespace", value: " "},
{type: "operator", value: ">"},
{type: "whitespace", value: " "},
{type: "number", value: "0"},
{type: "parens", value: ")"},
{type: "whitespace", value: " "},
{type: "brace", value: "{"},
{type: "whitespace", value: "\n"},
{type: "identifier", value: "alert"},
{type: "parens", value: "("},
{type: "string", value: "'aa'"},
{type: "parens", value: ")"},
{type: "sep", value: ";"},
{type: "whitespace", value: "\n"},
{type: "brace", value: "}"},
{type: "whitespace", value: "\n"}
]);
console.log(ast);
</script>
</body>
</html>
最后输出ast值为如下:
{
"type": "Program",
"body": [
{
"type": "IfStatement",
"test": {
"type": "BinaryExpression",
"left": {
"type": "Literal",
"value": 1
},
"right": {
"type": "Literal",
"value": 0
}
},
"consequent": {
"type": "BlockStatement",
"body": [
{
"type": "ExpressionStatement",
"expression": {
"type": "CallExpression",
"caller": {
"type": "Identifier",
"value": "alert"
},
"arguments": [
{
"type": "Literal",
"value": "aa"
}
]
}
}
]
},
"alternative": null
}
]
}
我们现在再来分析下上面代码的含义:分析如下:
第一步调用parse该方法,传入参数分词中输出的结果,代码如下:
var ast = parse([
{type: "whitespace", value: "\n"},
{type: "identifier", value: "if"},
{type: "whitespace", value: " "},
{type: "parens", value: "("},
{type: "number", value: "1"},
{type: "whitespace", value: " "},
{type: "operator", value: ">"},
{type: "whitespace", value: " "},
{type: "number", value: "0"},
{type: "parens", value: ")"},
{type: "whitespace", value: " "},
{type: "brace", value: "{"},
{type: "whitespace", value: "\n"},
{type: "identifier", value: "alert"},
{type: "parens", value: "("},
{type: "string", value: "'aa'"},
{type: "parens", value: ")"},
{type: "sep", value: ";"},
{type: "whitespace", value: "\n"},
{type: "brace", value: "}"},
{type: "whitespace", value: "\n"}
]);
先初始化如下参数:
let i = -1; // 用于标识当前遍历位置
let curToken; // 用于记录当前符号
function nextStatement() {
// ... 很多代码
}
function nextExpression() {
// ... 很多代码
}
function nextToken() {
// ... 很多代码
}
// 位置暂存栈,用于支持很多时候需要返回到某个之前的位置
const stashStack = [];
function rewind () {
// ... 很多代码
}
function commit () {
// ... 很多代码
}
真正初始化的代码如下:
const ast = {
type: 'Program',
body: [],
};
// 逐条解析顶层语句
while (i < tokens.length) {
const statement = nextStatement();
if (!statement) {
break;
}
ast.body.push(statement);
}
return ast;
先定义ast对象,最顶层的类型为 Program, body为[], 然后依次循环tokens的长度,第一步调用 nextStatement()方法,在该方法内部,先是
存储当前的i值,代码如下:
// 暂存当前的i,如果无法找到符合条件的情况会需要回到这里
stash();
function stash () {
// 暂存当前位置
stashStack.push(i);
}
因此 var stashStack = [-1]了;
接着 调用 nextToken();方法 读取下一个符号,nextToken代码如下:
// 往后移动读取指针,自动跳过空白
function nextToken () {
do {
i++;
curToken = tokens[i] || { type: 'EOF' };
} while (curToken.type === 'whitespace');
}
上面使用到do,while语句,该代码的含义是先执行一次,然后再判断条件是否符合要求,因此此时 i = 0 了,因此 curToken的值变为如下:
var curToken = {type: "whitespace", value: "\n"}; 然后while语句在判断 curToken.type === 'whitespace' 是否相等,
很明显是相等的,因此i++; 然后 var curToken = {type: "identifier", value: "if"}; 这个值了;然后再判断该type是否等于?
可以看到不等于,因此curToken的值就是如上代码的。
然后 就是if语句代码判断如下:
if (curToken.type === 'identifier' && curToken.value === 'if') {
// 解析 if 语句
const statement = {
type: 'IfStatement',
};
// if 后面必须紧跟着 (
nextToken();
if (curToken.type !== 'parens' || curToken.value !== '(') {
throw new Error('Expected ( after if');
} // 后续的一个表达式是 if 的判断条件
statement.test = nextExpression(); // 判断条件之后必须是 )
nextToken();
if (curToken.type !== 'parens' || curToken.value !== ')') {
throw new Error('Expected ) after if test expression');
} // 下一个语句是 if 成立时执行的语句
statement.consequent = nextStatement(); // 如果下一个符号是 else 就说明还存在 if 不成立时的逻辑
if (curToken === 'identifier' && curToken.value === 'else') {
statement.alternative = nextStatement();
} else {
statement.alternative = null;
}
commit();
return statement;
}
var curToken = {type: "identifier", value: "if"}; 因此满足if条件判断语句,定义 statement对象如下:
const statement = {
type: 'IfStatement'
};
调用 nextToken()方法 读取下一个字符,因此先执行一次代码, var curToken = {type: "whitespace", value: " "}; 然后再判断while条件,
最后curToken的值变为如下: var curToken = {type: "parens", value: "("}; 所以if语句后面紧跟着( 是正常的,然后就是需要判断if语句的
表达式了;如上代码:
// 后续的一个表达式是 if 的判断条件
statement.test = nextExpression();
// 判断条件之后必须是 )
nextToken();
if (curToken.type !== 'parens' || curToken.value !== ')') {
throw new Error('Expected ) after if test expression');
}
先是调用 nextExpression 方法,代码如下:
// 读取下一个表达式
function nextExpression () {
nextToken();
if (curToken.type === 'identifier') {
const identifier = {
type: 'Identifier',
name: curToken.value,
};
stash();
nextToken();
if (curToken.type === 'parens' && curToken.value === '(') {
// 如果一个标识符后面紧跟着 ( ,说明是个函数调用表达式
const expr = {
type: 'CallExpression',
caller: identifier,
arguments: [],
}; stash();
nextToken();
if (curToken.type === 'parens' && curToken.value === ')') {
// 如果下一个符合直接就是 ) ,说明没有参数
commit();
} else {
// 读取函数调用参数
rewind();
while (i < tokens.length) {
// 将下一个表达式加到arguments当中
expr.arguments.push(nextExpression());
nextToken();
// 遇到 ) 结束
if (curToken.type === 'parens' && curToken.value === ')') {
break;
}
// 参数间必须以 , 相间隔
if (curToken.type !== 'comma' && curToken.value !== ',') {
throw new Error('Expected , between arguments');
}
}
}
commit();
return expr;
}
rewind();
return identifier;
}
if (curToken.type === 'number' || curToken.type === 'string') {
// 数字或字符串,说明此处是个常量表达式
const literal = {
type: 'Literal',
value: eval(curToken.value),
};
// 但如果下一个符号是运算符,那么这就是个双元运算表达式
stash();
nextToken();
if (curToken.type === 'operator') {
commit();
return {
type: 'BinaryExpression',
left: literal,
right: nextExpression(),
};
}
rewind();
return literal;
}
if (curToken.type !== 'EOF') {
throw new Error('Unexpected token ' + curToken.value);
}
}
在代码内部调用 nextToken方法,curToken的值变为 var curToken = {type: "number", value: "1"};
所以满足上面的第二个if条件语句了,所以先定义 literal的值,如下:
const literal = {
type: 'Literal',
value: eval(curToken.value),
};
所以
const literal = {
type: 'Literal',
value: 1,
};
然后调用 stash()方法保存当前的的值;
const stashStack = [];
function stash () {
// 暂存当前位置
stashStack.push(i);
}
因此stashStack的值变为 const stashStack = [-1, 4]; 接着调用 nextToken()方法,因此此时的curToken的值变为如下:
var curToken = {type: "operator", value: ">"}; 所以它满足 上面代码的 if (curToken.type === 'operator') { 这个条件,
因此 会返回
return {
type: 'BinaryExpression',
left: {
type: 'Literal',
value: 1
},
right: nextExpression(),
};
right的值 使用递归的方式重新调用 nextExpression 函数。且在返回之前调用了 commit()函数,该函数代码如下:
function commit () {
// 解析成功,不需要再返回
stashStack.pop();
}
如上函数使用 数组的pop方法,删除数组的最后一个元素,因此此时的 stashStack 的值变为 const stashStack = [-1];
如上代码,刚刚i = 4的时候,再调用 nextToken()方法,因此此时i就等于6了,递归调用 nextExpression方法后,再调用nextToken();方法,
因此此时 i 的值变为8,因此 curToken的值变为如下;var curToken = {type: "number", value: "0"}; 和上面一样,还是进入了第二个if
语句代码内;此时literal的值变为如下:
const literal = {
type: 'Literal',
value: 0
};
stash(); 调用该方法后,因此 var stashStack = [-1, 8]了,再调用 nextToken(); 方法后,此时 curToken = {type: "parens", value: ")"}; 下面的if语句不满足,直接调用 rewind()方法; 然后返回 return literal;的值;
rewind方法如下代码:
function rewind () {
// 解析失败,回到上一个暂存的位置
i = stashStack.pop();
curToken = tokens[i];
};
我们之前保存的stashStack的值为 [-1, 8]; 因此使用pop方法后,或者i的值为8,因此curToken = {type: "number", value: "0"} 了;
最后就返回成这样的;
return {
type: 'BinaryExpression',
left: {
type: 'Literal',
value: 1
},
right: {
type: 'Literal',
value: 0
}
};
因此 statement.test = {
type: 'BinaryExpression',
left: {
type: 'Literal',
value: 1
},
right: {
type: 'Literal',
value: 0
}
}
我们接着看 nextStatement 语句中的如下代码;
// 下一个语句是 if 成立时执行的语句
statement.consequent = nextStatement();
又递归调用该方法了,因此之前( 的位置是9,因此此时再循环调用,i的值变为11了,因此 curToken = {type: "brace", value: "{"};
所以就进入了第二个if语句的判断条件了,如下: if (curToken.type === 'brace' && curToken.value === '{') {
先定义statement的值如下:
// 以 { 开头表示是个代码块
const statement = {
type: 'BlockStatement',
body: [],
};
while (i < tokens.length) {
// 检查下一个符号是不是 }
stash();
nextToken();
if (curToken.type === 'brace' && curToken.value === '}') {
// } 表示代码块的结尾
commit();
break;
}
// 还原到原来的位置,并将解析的下一个语句加到body
rewind();
statement.body.push(nextStatement());
}
// 代码块语句解析完毕,返回结果
commit();
return statement;
代码如上, 此时i = 11; 进入while循环语句了,调用 stash保存当前的值 因此 var stashStack = [-1, 11]; 调用 nextToken方法后,那么
curToken = {type: "identifier", value: "alert"}; while代码不满足要求,因此 调用 rewind()方法返回到 i = 11位置上了,然后继续
调用nextStatement方法,把返回后的结果 放入 statement.body数组内,调用 nextToken(); 方法后,回到13位置上了,因此此时
var curToken = {type: "identifier", value: "alert"}; 上面的if条件语句都不满足,所以定义如下变量了。
// 尝试解析单表达式语句
const statement = {
type: 'ExpressionStatement',
expression: nextExpression(),
};
调用 nextExpression 该方法,该方法如下:
function nextExpression () {
nextToken();
if (curToken.type === 'identifier') {
const identifier = {
type: 'Identifier',
name: curToken.value,
};
stash();
nextToken();
if (curToken.type === 'parens' && curToken.value === '(') {
// 如果一个标识符后面紧跟着 ( ,说明是个函数调用表达式
const expr = {
type: 'CallExpression',
caller: identifier,
arguments: [],
}; stash();
nextToken();
if (curToken.type === 'parens' && curToken.value === ')') {
// 如果下一个符合直接就是 ) ,说明没有参数
commit();
} else {
// 读取函数调用参数
rewind();
while (i < tokens.length) {
// 将下一个表达式加到arguments当中
expr.arguments.push(nextExpression());
nextToken();
// 遇到 ) 结束
if (curToken.type === 'parens' && curToken.value === ')') {
break;
}
// 参数间必须以 , 相间隔
if (curToken.type !== 'comma' && curToken.value !== ',') {
throw new Error('Expected , between arguments');
}
}
}
commit();
return expr;
}
rewind();
return identifier;
}
if (curToken.type === 'number' || curToken.type === 'string') {
// 数字或字符串,说明此处是个常量表达式
const literal = {
type: 'Literal',
value: eval(curToken.value),
};
// 但如果下一个符号是运算符,那么这就是个双元运算表达式
stash();
nextToken();
if (curToken.type === 'operator') {
commit();
return {
type: 'BinaryExpression',
left: literal,
right: nextExpression(),
};
}
rewind();
return literal;
}
if (curToken.type !== 'EOF') {
throw new Error('Unexpected token ' + curToken.value);
}
}
如上 curToken的值 curToken = {type: "identifier", value: "alert"}; 因此会进入第一个if语句内,identifier的值变为如下:
const identifier = {
type: 'Identifier',
name: alert,
};
调用 stash()方法,此时 stashStack 的值变为 var stashStack = [-1, 13]; 再接着调用 nextToken方法, 因此curToken的值变为如下:
var curToken = {type: "parens", value: "("},因此会进入if条件语句了,如下:
if (curToken.type === 'parens' && curToken.value === '(') {; 的条件判断了;接着定义expr的变量如下代码:
// 如果一个标识符后面紧跟着 ( ,说明是个函数调用表达式
const expr = {
type: 'CallExpression',
caller: identifier,
arguments: [],
};
再调用该方法后,stash(); 此时 stashStack的值变为 [-1, 14], 再调用 nextToken(); 方法后,此时 curToken的值变为如下:
var curToken = {type: "string", value: "'aa'"}; 再接着执行 if (curToken.type === 'parens' && curToken.value === '(')
代码么有找到条件判断,因此在调用 rewind(); 返回再返回14的位置上,此时 curToken = {type: "parens", value: "("};
因此执行后,紧着如下代码:
// 读取函数调用参数
rewind();
while (i < tokens.length) {
// 将下一个表达式加到arguments当中
expr.arguments.push(nextExpression());
nextToken();
// 遇到 ) 结束
if (curToken.type === 'parens' && curToken.value === ')') {
break;
}
// 参数间必须以 , 相间隔
if (curToken.type !== 'comma' && curToken.value !== ',') {
throw new Error('Expected , between arguments');
}
}
, 原理还是和上面一样,这里不一一解析了,太烦了;大家可以自己去理解了。
以上就是语义解析的部分主要思路。
理解Babel是如何编译JS代码的及理解抽象语法树(AST)的更多相关文章
- vue 的模板编译—ast(抽象语法树) 详解与实现
首先AST是什么? 在计算机科学中,抽象语法树(abstract syntax tree或者缩写为AST),或者语法树(syntax tree),是源代码的抽象语法结构的树状表现形式,这里特指编程语言 ...
- 《深入理解Android虚拟机内存管理》示例程序编译阶段生成的各种语法树完整版
1.tokens "int" "int" <SPACES> " &quo ...
- 使用gulp和bable实现实时编译ES6代码
这篇文章最初发表在我自己折腾的博客站点上:使用gulp和bable实现实时编译ES6代码,该博客用了一位前辈开源的源码,基于thinkjs和vuejs开发,欢迎大家来逛逛. 问题描述> 项目开发 ...
- js混淆加密,通过混淆Js代码让别人(很难)无法还原
js混淆加密,通过混淆Js代码让别人(很难)无法还原 使用js的混淆加密,其目的是为了保护我们的前端代码逻辑,对应一些搞技术吃饭的公司来说,为了防止被竞争对手抓取或使用自己的代码,就会考虑如何加密 ...
- 利用 Babel 玩转你的代码
我在团队帮助开发 Node 工具时,遇到了需要对多份相似的代码进行一定的处理,但又不能改变原本仓库的代码,这个非常像我们的编译工具做的事情.在一开始的时候,参考了类似 FIS 的功能,简单参照使用代码 ...
- 前端规范之JS代码规范(ESLint + Prettier)
代码规范是软件开发领域经久不衰的话题,几乎所有工程师在开发过程中都会遇到或思考过这一问题.而随着前端应用的大型化和复杂化,越来越多的前端团队也开始重视代码规范.同样,前段时间,笔者所在的团队也开展了一 ...
- 谈谈我对 js原型链的理解
想要学习 “原型链” 必须要认识什么是 “原型” 和 “原型链” 先理解一下普通的继承和原型的区别,下面写一段js代码来帮助理解: var Animal = function(){ // 动物抽象类 ...
- webpack--css、html 和 js 代码的常用处理
前言 本文来总结下webpack中 css.js.html 代码常见的处理方式,学习笔记仅供参考. 正文 1.css样式文件处理 (1)提取css为一个单独的文件 在我们前面学习了webpack的基础 ...
- JVM-程序编译与代码早期(编译期)优化
早期(编译期)优化 一.Javac编译器 1.Javac的源代码与调试 Javac的源代码放在JDK_SRC_HOME/langtools/src/shares/classes/com/sun/too ...
随机推荐
- Xshell 的安装教程
Xshell就是一个远程控制RHEL的软件:其他的还有很多,用什么都无所谓(根据公司情况). 下面我们来安装下这个工具: 双击exe 点下一步: 选 免费的 然后下一步:(免费的功能足够用了) 点接受 ...
- 熊掌号:"搜索+信息流"双引擎与"百家号+熊掌号"双品牌内容平台
一. 熊掌号是什么?熊掌号简单来说,就是"搜索 + 信息流"双引擎与"百家号 + 熊掌号"双品牌内容平台,上线了,对站长还是企业,都是一件好事.只要写出优质的原 ...
- 最全的iOS数据存储方法
目的 项目准备运用的Core Data进行本地数据存储,本来打算只写一下Core Data的,不过既然说到了数据存储,干脆来个数据存储基础大总结!本文将对以下几个模块进行叙述. 沙盒 Plist Pr ...
- Eclipse中代码整体左移,右移快捷键
1.向右:将要移动的代码选中,然后按TAB键2.向左:将要移动的代码选中,然后按SHIFT+TAB键
- ELK 快速指南
ELK 快速指南 概念 ELK 是什么 ELK 是 elastic 公司旗下三款产品 ElasticSearch .Logstash .Kibana 的首字母组合. ElasticSearch 是一个 ...
- P1132 数字生成游戏
题目请见:传送门 以下为题解,直接从洛谷上搬过来的,还专门改了markdown,(汗) 宽搜 with 一些技巧 由于查询量很大,所以要预先处理所有答案 预处理当然是用BFS,并同时进行delete, ...
- 三、Hadoop学习笔记————从MapReduce到Yarn
Yarn减轻了JobTracker的负担,对其进行了解耦
- border-radio属性
boreder-radio属性是css3的新增属性,可以设置圆角的边框. <head> <style type="text/css"> *{ margin ...
- C语言的scanf函数
一. 变量的内存分析 1. 字节和地址 1> 内存以“字节为单位”,Oxffc1,Oxffc2,Oxffc3,Oxffc4....都是字节 ,0x表示的是十六进制 2> 不同类型占用的字节 ...
- scrapy初试水 day03(递归调用)
import scrapyfrom scrapy.http import Requestfrom scrapy.spider import Rulefrom scrapy.linkextractors ...