接触 Javascript 很长一段时间了, 但一直浮在语言的表面, 今天决定重头开始更深入的学习 Javascript, 先从 Javascript 的编译原理开始.
在程序的执行方式中有编译型和解释型, 以前学习的 C 语言就是编译型语言, 它需要提前把所有源代码翻译成机器能识别的指令进行运行. JavaScript 就是解释型语言, 它可以翻译一条执行一条. 如何更通俗的理解编译和解释的区别呢?
打个比喻: 编译相当于做好一桌子菜再开吃; 解释就是吃火锅.
尽管通常将 Javascript 归类为 "动态" 或 "稀释执行" 语言, 但事实上他是一门编译语言, 但与传统的编译语言不同, 它不是提前编译的, 编译的结果也不能在分布式系统中进行移植. 例如像 V8(Chrome 的 JS 引擎), 它其实为了提高 JS 的运行性能, 在运行之前会先将 JS 编译为本地的机器码, 然后再去执行机器码.
一段源代码在执行之前经历三个步骤:
分词 / 词法分析
解析 / 语法分析
代码生成
分词 / 词法分析(Tokenizing/Lexing)
将字符串分解成有意义的代码块, 这些代码块被称为
词法单元(token)
. 例如: var a = 2, 这段程序的词法单元就是: var,a,=,2.
解析 / 语法分析(Parsing)
将词法单元 (数组) 转换成一个由元素逐级嵌套所组成的代表了程序语法结构的数. 这个数称为 "抽象语法树"(Abstract Syntax Tree), 简称 "AST".
代码生成
将 AST 转换为可执行代码的过程称为代码生成.
以上只是进行宏观, 简单的介绍, JavaScript 引擎实际上做的事情比这个复杂的多, 在 JIT(Just-in-time)就是对代码进行各种优化.
其实理解编译的过程, 自己动手实现一个简单的编译器能更好的理解这个过程. 下面是一个用户 JavaScript 实现的简单编译器:
- /**
- * sum 加
- * sub 减
- * mul 乘
- * div 除
- * ##### 以下是我们编译器要实现的原理 ###############
- * (sum 3 3) 2+2
- * (sub 3 2) 3-2
- * (mul (sum 3 3) (sub 3 2)) (3+3)*(3-2)
- * (div (mul (sum 3 3) (sub 3 2)) 1) ((3+3)*(3-2))/1
- */
- /**
- * 分词 / 词法分析
- * @argument {string} str 接收一个代码组成的字符串
- * @return {array} 返回一个词法单元组成的数组
- */
- function tokenizer(str) {
- // 当前字符的位置
- let current = 0;
- // 存放词法单元的敌法
- const tokens = [];
- const regSpace = /\s/;
- const regNumber = /[0-9]/;
- const regLetter = /[a-z]/i;
- const len = str.length;
- while (current <len) {
- let char = str[current];
- if (char === '(') {
- tokens.push({
- type: 'paren',
- value: '('
- });
- current++;
- continue;
- }
- if (char === ')') {
- tokens.push({
- type: 'paren',
- value: ')'
- });
- current++;
- continue;
- }
- // 检测类型为空
- if (regSpace.test(char)) {
- current++;
- continue;
- }
- // 检测类型为数值
- if (regNumber.test(char)) {
- let value = '';
- while (regNumber.test(char)) {
- value += char;
- char = str[++current];
- }
- tokens.push({
- type: 'number',
- value: value
- });
- continue;
- }
- // 检测类型为字符串
- if (regLetter.test(char)) {
- let value = '';
- while (regLetter.test(char)) {
- value += char;
- char = str[++current];
- }
- tokens.push({
- type: 'operator',
- value: value,
- });
- continue;
- }
- throw new TypeError('I dont know what this character is:' + char);
- }
- return tokens;
- }
- /**
- * 语法分析器
- * @argument {array} 接收一个词法单元 (token) 组成的数组
- * @return {object} 返回一个把词法单元流转换成一个 "抽象语法树"AST
- */
- function parser(tokens) {
- let current = 0;
- function parse() {
- let token = tokens[current];
- if (token.type === 'number') {
- current++;
- return {
- type: 'NumberLiteral',
- value: token.value
- };
- }
- if ( token.type === 'paren' && token.value === '(') {
- token = tokens[++current];
- const node = {
- type: 'CallExpression',
- name: token.value,
- params: []
- };
- token = tokens[++current];
- while (
- (token.type !== 'paren') ||
- (token.type === 'paren' && token.value !== ')')
- ) {
- node.params.push(parse());
- token = tokens[current];
- }
- current++;
- return node;
- }
- throw new TypeError(token.type)
- }
- return parse();
- }
- /** 将抽象语法树生成代码
- * @argument {object} 抽象语法树
- * @return {string} 可执行代码字符串
- */
- function codeGenerator(ast) {
- const operator = {
- sum: '+',
- sub: '-',
- mul: '*',
- div: '/'
- };
- const compileNum = ast => ast.value;
- const compileOp = ast => `(${ast.params.map(compile).join('' + operator[ast.name] +' ')})`;
- const compile = ast => ast.type === 'NumberLiteral' ? compileNum(ast) : compileOp(ast);
- return compile(ast);
- }
- // 编译器: 词法分析 + 语法分析 + 代码生成
- function compiler(code) {
- return codeGenerator(parser(tokenizer(code)));
- }
- const codeResult = compiler('(div (mul (sum 3 3) (sub 3 2)) 1)')
- console.log(codeResult) //=> (((3 + 3) * (3 - 2)) / 1)
- console.log(eval(codeResult)) //=> 6
上面的解析器, 将词法分析器把词法单元分类: "paren", "number", "operator" 三种类型进行存储, 再通过语法分析器将数值和运算符区分出来, 通过递归解析把词法单元流转换成 AST, 最后通过代码生成器, 根据 AST 中节点的类型转换可执行的代码.
总结
通过实现一个简单的 DLS 解释器, 我们能宏观的了解 JavaScript 在代码执行前几微秒发生的编译过程, 当然这只是最简单的介绍, 具体可以了解去 JIT 做了什么.
用 25 行 JavaScript 语句实现一个简单的编译器 https://juejin.im/entry/59cdbe11f265da06633d4ac2
https://github.com/starkwang/the-super-tiny-compiler-cn
来源: http://www.jianshu.com/p/65aee3ce90ed