前端与编译原理——用JS写一个JS解释器

开发
说起编译原理,印象往往只停留在本科时那些枯燥的课程和晦涩的概念。作为前端开发者,编译原理似乎离我们很远,对它的理解很可能仅仅局限于“抽象语法树(AST)”。但这仅仅是个开头而已。编译原理的使用,甚至能让我们利用JS直接写一个能运行JS代码的解释器。

[[251469]] 

说起编译原理,印象往往只停留在本科时那些枯燥的课程和晦涩的概念。作为前端开发者,编译原理似乎离我们很远,对它的理解很可能仅仅局限于“抽象语法树(AST)”。但这仅仅是个开头而已。编译原理的使用,甚至能让我们利用JS直接写一个能运行JS代码的解释器。

项目地址:https://github.com/jrainlau/c...

在线体验:https://codepen.io/jrainlau/p...

一、为什么要用JS写JS的解释器

接触过小程序开发的同学应该知道,小程序运行的环境禁止new Function,eval等方法的使用,导致我们无法直接执行字符串形式的动态代码。此外,许多平台也对这些JS自带的可执行动态代码的方法进行了限制,那么我们是没有任何办法了吗?既然如此,我们便可以用JS写一个解析器,让JS自己去运行自己。

在开始之前,我们先简单回顾一下编译原理的一些概念。

二、什么是编译器

说到编译原理,肯定离不开编译器。简单来说,当一段代码经过编译器的词法分析、语法分析等阶段之后,会生成一个树状结构的“抽象语法树(AST)”,该语法树的每一个节点都对应着代码当中不同含义的片段。

比如有这么一段代码: 

  1. const a = 1  
  2. console.log(a)  

经过编译器处理后,它的AST长这样:

  1.   "type""Program"
  2.   "start": 0, 
  3.   "end": 26, 
  4.   "body": [ 
  5.     { 
  6.       "type""VariableDeclaration"
  7.       "start": 0, 
  8.       "end": 11, 
  9.       "declarations": [ 
  10.         { 
  11.           "type""VariableDeclarator"
  12.           "start": 6, 
  13.           "end": 11, 
  14.           "id": { 
  15.             "type""Identifier"
  16.             "start": 6, 
  17.             "end": 7, 
  18.             "name""a" 
  19.           }, 
  20.           "init": { 
  21.             "type""Literal"
  22.             "start": 10, 
  23.             "end": 11, 
  24.             "value": 1, 
  25.             "raw""1" 
  26.           } 
  27.         } 
  28.       ], 
  29.       "kind""const" 
  30.     }, 
  31.     { 
  32.       "type""ExpressionStatement"
  33.       "start": 12, 
  34.       "end": 26, 
  35.       "expression": { 
  36.         "type""CallExpression"
  37.         "start": 12, 
  38.         "end": 26, 
  39.         "callee": { 
  40.           "type""MemberExpression"
  41.           "start": 12, 
  42.           "end": 23, 
  43.           "object": { 
  44.             "type""Identifier"
  45.             "start": 12, 
  46.             "end": 19, 
  47.             "name""console" 
  48.           }, 
  49.           "property": { 
  50.             "type""Identifier"
  51.             "start": 20, 
  52.             "end": 23, 
  53.             "name""log" 
  54.           }, 
  55.           "computed"false 
  56.         }, 
  57.         "arguments": [ 
  58.           { 
  59.             "type""Identifier"
  60.             "start": 24, 
  61.             "end": 25, 
  62.             "name""a" 
  63.           } 
  64.         ] 
  65.       } 
  66.     } 
  67.   ], 
  68.   "sourceType""module" 
  69.  

常见的JS编译器有babylon,acorn等等,感兴趣的同学可以在AST explorer这个网站自行体验。

可以看到,编译出来的AST详细记录了代码中所有语义代码的类型、起始位置等信息。这段代码除了根节点Program外,主体包含了两个节点VariableDeclaration和ExpressionStatement,而这些节点里面又包含了不同的子节点。

正是由于AST详细记录了代码的语义化信息,所以Babel,Webpack,Sass,Less等工具可以针对代码进行非常智能的处理。

三、什么是解释器

如同翻译人员不仅能看懂一门外语,也能对其艺术加工后把它翻译成母语一样,人们把能够将代码转化成AST的工具叫做“编译器”,而把能够将AST翻译成目标语言并运行的工具叫做“解释器”。

在编译原理的课程中,我们思考过这么一个问题:如何让计算机运行算数表达式1+2+3:

  1. 1 + 2 + 3 

当机器执行的时候,它可能会是这样的机器码: 

  1. 1 PUSH 1  
  2. 2 PUSH 2  
  3. ADD  
  4. 4 PUSH 3  
  5. ADD  

而运行这段机器码的程序,就是解释器。

在这篇文章中,我们不会搞出机器码这样复杂的东西,仅仅是使用JS在其runtime环境下去解释JS代码的AST。由于解释器使用JS编写,所以我们可以大胆使用JS自身的语言特性,比如this绑定、new关键字等等,完全不需要对它们进行额外处理,也因此让JS解释器的实现变得非常简单。

在回顾了编译原理的基本概念之后,我们就可以着手进行开发了。

四、节点遍历器

通过分析上文的AST,可以看到每一个节点都会有一个类型属性type,不同类型的节点需要不同的处理方式,处理这些节点的程序,就是“节点处理器(nodeHandler)”

定义一个节点处理器:

  1. const nodeHandler = { 
  2.   Program () {}, 
  3.   VariableDeclaration () {}, 
  4.   ExpressionStatement () {}, 
  5.   MemberExpression () {}, 
  6.   CallExpression () {}, 
  7.   Identifier () {} 
  8.  

关于节点处理器的具体实现,会在后文进行详细探讨,这里暂时不作展开。

有了节点处理器,我们便需要去遍历AST当中的每一个节点,递归地调用节点处理器,直到完成对整棵语法书的处理。

定义一个节点遍历器(NodeIterator):

  1. class NodeIterator { 
  2.   constructor (node) { 
  3.     this.node = node 
  4.     this.nodeHandler = nodeHandler 
  5.   } 
  6.  
  7.   traverse (node) { 
  8.     // 根据节点类型找到节点处理器当中对应的函数 
  9.     const _eval = this.nodeHandler[node.type] 
  10.     // 若找不到则报错 
  11.     if (!_eval) { 
  12.       throw new Error(`canjs: Unknown node type "${node.type}".`) 
  13.     } 
  14.     // 运行处理函数 
  15.     return _eval(node) 
  16.   } 
  17.  
  18.  

理论上,节点遍历器这样设计就可以了,但仔细推敲,发现漏了一个很重要的东西——作用域处理。

回到节点处理器的VariableDeclaration()方法,它用来处理诸如const a = 1这样的变量声明节点。假设它的代码如下:

  1. VariableDeclaration (node) { 
  2.     for (const declaration of node.declarations) { 
  3.       const { name } = declaration.id 
  4.       const value = declaration.init ? traverse(declaration.init) : undefined 
  5.       // 问题来了,拿到了变量的名称和值,然后把它保存到哪里去呢? 
  6.       // ... 
  7.     } 
  8.   },  

问题在于,处理完变量声明节点以后,理应把这个变量保存起来。按照JS语言特性,这个变量应该存放在一个作用域当中。在JS解析器的实现过程中,这个作用域可以被定义为一个scope对象。

改写节点遍历器,为其新增一个scope对象

  1. class NodeIterator { 
  2.   constructor (node, scope = {}) { 
  3.     this.node = node 
  4.     this.scope = scope 
  5.     this.nodeHandler = nodeHandler 
  6.   } 
  7.  
  8.   traverse (node, options = {}) { 
  9.     const scope = options.scope || this.scope 
  10.     const nodeIterator = new NodeIterator(node, scope) 
  11.     const _eval = this.nodeHandler[node.type] 
  12.     if (!_eval) { 
  13.       throw new Error(`canjs: Unknown node type "${node.type}".`) 
  14.     } 
  15.     return _eval(nodeIterator) 
  16.   } 
  17.  
  18.   createScope (blockType = 'block') { 
  19.     return new Scope(blockType, this.scope) 
  20.   } 
  21.  

然后节点处理函数VariableDeclaration()就可以通过scope保存变量了: 

  1. VariableDeclaration (nodeIterator) { 
  2.     const kind = nodeIterator.node.kind 
  3.     for (const declaration of nodeIterator.node.declarations) { 
  4.       const { name } = declaration.id 
  5.       const value = declaration.init ? nodeIterator.traverse(declaration.init) : undefined 
  6.       // 在作用域当中定义变量 
  7.       // 如果当前是块级作用域且变量用var定义,则定义到父级作用域 
  8.       if (nodeIterator.scope.type === 'block' && kind === 'var') { 
  9.         nodeIterator.scope.parentScope.declare(name, value, kind) 
  10.       } else { 
  11.         nodeIterator.scope.declare(name, value, kind) 
  12.       } 
  13.     } 
  14.   },  

关于作用域的处理,可以说是整个JS解释器最难的部分。接下来我们将对作用域处理进行深入的剖析。

五、作用域处理

考虑到这样一种情况: 

  1. const a = 1 
  2.   const b = 2 
  3.   console.log(a) 
  4. console.log(b)  

运行结果必然是能够打印出a的值,然后报错:Uncaught ReferenceError: b is not defined

这段代码就是涉及到了作用域的问题。块级作用域或者函数作用域可以读取其父级作用域当中的变量,反之则不行,所以对于作用域我们不能简单地定义一个空对象,而是要专门进行处理。

定义一个作用域基类Scope: 

  1. class Scope { 
  2.   constructor (type, parentScope) { 
  3.     // 作用域类型,区分函数作用域function和块级作用域block 
  4.     this.type = type 
  5.     // 父级作用域 
  6.     this.parentScope = parentScope 
  7.     // 全局作用域 
  8.     this.globalDeclaration = standardMap 
  9.     // 当前作用域的变量空间 
  10.     this.declaration = Object.create(null
  11.   } 
  12.  
  13.   /* 
  14.    * get/set方法用于获取/设置当前作用域中对应name的变量值 
  15.      符合JS语法规则,优先从当前作用域去找,若找不到则到父级作用域去找,然后到全局作用域找。 
  16.      如果都没有,就报错 
  17.    */ 
  18.   get (name) { 
  19.     if (this.declaration[name]) { 
  20.       return this.declaration[name
  21.     } else if (this.parentScope) { 
  22.       return this.parentScope.get(name
  23.     } else if (this.globalDeclaration[name]) { 
  24.       return this.globalDeclaration[name
  25.     } 
  26.     throw new ReferenceError(`${nameis not defined`) 
  27.   } 
  28.  
  29.   set (name, value) { 
  30.     if (this.declaration[name]) { 
  31.       this.declaration[name] = value 
  32.     } else if (this.parentScope[name]) { 
  33.       this.parentScope.set(name, value) 
  34.     } else { 
  35.       throw new ReferenceError(`${nameis not defined`) 
  36.     } 
  37.   } 
  38.  
  39.   /** 
  40.    * 根据变量的kind调用不同的变量定义方法 
  41.    */ 
  42.   declare (name, value, kind = 'var') { 
  43.     if (kind === 'var') { 
  44.       return this.varDeclare(name, value) 
  45.     } else if (kind === 'let') { 
  46.       return this.letDeclare(name, value) 
  47.     } else if (kind === 'const') { 
  48.       return this.constDeclare(name, value) 
  49.     } else { 
  50.       throw new Error(`canjs: Invalid Variable Declaration Kind of "${kind}"`) 
  51.     } 
  52.   } 
  53.  
  54.   varDeclare (name, value) { 
  55.     let scope = this 
  56.     // 若当前作用域存在非函数类型的父级作用域时,就把变量定义到父级作用域 
  57.     while (scope.parentScope && scope.type !== 'function') { 
  58.       scope = scope.parentScope 
  59.     } 
  60.     this.declaration[name] = new SimpleValue(value, 'var'
  61.     return this.declaration[name
  62.   } 
  63.  
  64.   letDeclare (name, value) { 
  65.     // 不允许重复定义 
  66.     if (this.declaration[name]) { 
  67.       throw new SyntaxError(`Identifier ${name} has already been declared`) 
  68.     } 
  69.     this.declaration[name] = new SimpleValue(value, 'let'
  70.     return this.declaration[name
  71.   } 
  72.  
  73.   constDeclare (name, value) { 
  74.     // 不允许重复定义 
  75.     if (this.declaration[name]) { 
  76.       throw new SyntaxError(`Identifier ${name} has already been declared`) 
  77.     } 
  78.     this.declaration[name] = new SimpleValue(value, 'const'
  79.     return this.declaration[name
  80.   } 
  81.  

这里使用了一个叫做simpleValue()的函数来定义变量值,主要用于处理常量: 

  1. class SimpleValue { 
  2.   constructor (value, kind = '') { 
  3.     this.value = value 
  4.     this.kind = kind 
  5.   } 
  6.  
  7.   set (value) { 
  8.     // 禁止重新对const类型变量赋值 
  9.     if (this.kind === 'const') { 
  10.       throw new TypeError('Assignment to constant variable'
  11.     } else { 
  12.       this.value = value 
  13.     } 
  14.   } 
  15.  
  16.   get () { 
  17.     return this.value 
  18.   } 
  19.  

处理作用域问题思路,关键的地方就是在于JS语言本身寻找变量的特性——优先当前作用域,父作用域次之,全局作用域***。反过来,在节点处理函数VariableDeclaration()里,如果遇到块级作用域且关键字为var,则需要把这个变量也定义到父级作用域当中,这也就是我们常说的“全局变量污染”。

JS标准库注入

细心的读者会发现,在定义Scope基类的时候,其全局作用域globalScope被赋值了一个standardMap对象,这个对象就是JS标准库。

简单来说,JS标准库就是JS这门语言本身所带有的一系列方法和属性,如常用的setTimeout,console.log等等。为了让解析器也能够执行这些方法,所以我们需要为其注入标准库: 

  1. const standardMap = {  
  2. console: new SimpleValue(console)  
  3.  

这样就相当于往解析器的全局作用域当中注入了console这个对象,也就可以直接被使用了。

六、节点处理器

在处理完节点遍历器、作用域处理的工作之后,便可以来编写节点处理器了。顾名思义,节点处理器是专门用来处理AST节点的,上文反复提及的VariableDeclaration()方法便是其中一个。下面将对部分关键的节点处理器进行讲解。

在开发节点处理器之前,需要用到一个工具,用于判断JS语句当中的return,break,continue关键字。

关键字判断工具Signal

定义一个Signal基类:

  1. class Signal { 
  2.   constructor (type, value) { 
  3.     this.type = type 
  4.     this.value = value 
  5.   } 
  6.  
  7.   static Return (value) { 
  8.     return new Signal('return', value) 
  9.   } 
  10.  
  11.   static Break (label = null) { 
  12.     return new Signal('break', label) 
  13.   } 
  14.  
  15.   static Continue (label) { 
  16.     return new Signal('continue', label) 
  17.   } 
  18.  
  19.   static isReturn(signal) { 
  20.     return signal instanceof Signal && signal.type === 'return' 
  21.   } 
  22.  
  23.   static isContinue(signal) { 
  24.     return signal instanceof Signal && signal.type === 'continue' 
  25.   } 
  26.  
  27.   static isBreak(signal) { 
  28.     return signal instanceof Signal && signal.type === 'break' 
  29.   } 
  30.  
  31.   static isSignal (signal) { 
  32.     return signal instanceof Signal 
  33.   } 
  34.  

有了它,就可以对语句当中的关键字进行判断处理,接下来会有大用处。

1、变量定义节点处理器——VariableDeclaration()

最常用的节点处理器之一,负责把变量注册到正确的作用域。 

  1. VariableDeclaration (nodeIterator) { 
  2.    const kind = nodeIterator.node.kind 
  3.    for (const declaration of nodeIterator.node.declarations) { 
  4.      const { name } = declaration.id 
  5.      const value = declaration.init ? nodeIterator.traverse(declaration.init) : undefined 
  6.      // 在作用域当中定义变量 
  7.      // 若为块级作用域且关键字为var,则需要做全局污染 
  8.      if (nodeIterator.scope.type === 'block' && kind === 'var') { 
  9.        nodeIterator.scope.parentScope.declare(name, value, kind) 
  10.      } else { 
  11.        nodeIterator.scope.declare(name, value, kind) 
  12.      } 
  13.    } 
  14.  },  

2、标识符节点处理器——Identifier()

专门用于从作用域中获取标识符的值。 

  1. Identifier (nodeIterator) { 
  2.     if (nodeIterator.node.name === 'undefined') { 
  3.       return undefined 
  4.     } 
  5.     return nodeIterator.scope.get(nodeIterator.node.name).value 
  6.   },  

3、字符节点处理器——Literal()

返回字符节点的值。

  1. Literal (nodeIterator) { 
  2.     return nodeIterator.node.value 
  3.   }  

4、表达式调用节点处理器——CallExpression()

用于处理表达式调用节点的处理器,如处理func(),console.log()等。 

  1. CallExpression (nodeIterator) { 
  2.     // 遍历callee获取函数体 
  3.     const func = nodeIterator.traverse(nodeIterator.node.callee) 
  4.     // 获取参数 
  5.     const args = nodeIterator.node.arguments.map(arg => nodeIterator.traverse(arg)) 
  6.  
  7.     let value 
  8.     if (nodeIterator.node.callee.type === 'MemberExpression') { 
  9.       value = nodeIterator.traverse(nodeIterator.node.callee.object) 
  10.     } 
  11.     // 返回函数运行结果 
  12.     return func.apply(value, args) 
  13.   },  

5、表达式节点处理器——MemberExpression()

区分于上面的“表达式调用节点处理器”,表达式节点指的是person.say,console.log这种函数表达式。

  1. MemberExpression (nodeIterator) { 
  2.     // 获取对象,如console 
  3.     const obj = nodeIterator.traverse(nodeIterator.node.object) 
  4.     // 获取对象的方法,如log 
  5.     const name = nodeIterator.node.property.name 
  6.     // 返回表达式,如console.log 
  7.     return obj[name
  8.   }  

6、块级声明节点处理器——BlockStatement()

非常常用的处理器,专门用于处理块级声明节点,如函数、循环、try...catch...当中的情景。

  1. BlockStatement (nodeIterator) { 
  2.    // 先定义一个块级作用域 
  3.    let scope = nodeIterator.createScope('block'
  4.  
  5.    // 处理块级节点内的每一个节点 
  6.    for (const node of nodeIterator.node.body) { 
  7.      if (node.type === 'VariableDeclaration' && node.kind === 'var') { 
  8.        for (const declaration of node.declarations) { 
  9.          scope.declare(declaration.id.name, declaration.init.value, node.kind) 
  10.        } 
  11.      } else if (node.type === 'FunctionDeclaration') { 
  12.        nodeIterator.traverse(node, { scope }) 
  13.      } 
  14.    } 
  15.  
  16.    // 提取关键字(return, break, continue) 
  17.    for (const node of nodeIterator.node.body) { 
  18.      if (node.type === 'FunctionDeclaration') { 
  19.        continue 
  20.      } 
  21.      const signal = nodeIterator.traverse(node, { scope }) 
  22.      if (Signal.isSignal(signal)) { 
  23.        return signal 
  24.      } 
  25.    } 
  26.  }  

可以看到这个处理器里面有两个for...of循环。***个用于处理块级内语句,第二个专门用于识别关键字,如循环体内部的break,continue或者函数体内部的return。

7、函数定义节点处理器——FunctionDeclaration()

往作用当中声明一个和函数名相同的变量,值为所定义的函数:

  1. FunctionDeclaration (nodeIterator) { 
  2.     const fn = NodeHandler.FunctionExpression(nodeIterator) 
  3.     nodeIterator.scope.varDeclare(nodeIterator.node.id.name, fn) 
  4.     return fn     
  5.   }  

8、函数表达式节点处理器——FunctionExpression()

用于定义一个函数:

  1. FunctionExpression (nodeIterator) { 
  2.     const node = nodeIterator.node 
  3.     /** 
  4.      * 1、定义函数需要先为其定义一个函数作用域,且允许继承父级作用域 
  5.      * 2、注册`this`, `arguments`和形参到作用域的变量空间 
  6.      * 3、检查return关键字 
  7.      * 4、定义函数名和长度 
  8.      */ 
  9.     const fn = function () { 
  10.       const scope = nodeIterator.createScope('function'
  11.       scope.constDeclare('this', this) 
  12.       scope.constDeclare('arguments', arguments) 
  13.  
  14.       node.params.forEach((param, index) => { 
  15.         const name = param.name 
  16.         scope.varDeclare(name, arguments[index]) 
  17.       }) 
  18.  
  19.       const signal = nodeIterator.traverse(node.body, { scope }) 
  20.       if (Signal.isReturn(signal)) { 
  21.         return signal.value 
  22.       } 
  23.     }  

9、this表达式处理器——ThisExpression()

该处理器直接使用JS语言自身的特性,把this关键字从作用域中取出即可。 

  1. ThisExpression (nodeIterator) { 
  2.     const value = nodeIterator.scope.get('this'
  3.     return value ? value.value : null 
  4.   }  

10、new表达式处理器——NewExpression()

和this表达式类似,也是直接沿用JS的语言特性,获取函数和参数之后,通过bind关键字生成一个构造函数,并返回。 

  1. NewExpression (nodeIterator) { 
  2.    const func = nodeIterator.traverse(nodeIterator.node.callee) 
  3.    const args = nodeIterator.node.arguments.map(arg => nodeIterator.traverse(arg)) 
  4.    return new (func.bind(null, ...args)) 
  5.  }  

11、For循环节点处理器——ForStatement()

For循环的三个参数对应着节点的init,test,update属性,对着三个属性分别调用节点处理器处理,并放回JS原生的for循环当中即可。

  1. ForStatement (nodeIterator) { 
  2.     const node = nodeIterator.node 
  3.     let scope = nodeIterator.scope 
  4.     if (node.init && node.init.type === 'VariableDeclaration' && node.init.kind !== 'var') { 
  5.       scope = nodeIterator.createScope('block'
  6.     } 
  7.  
  8.     for ( 
  9.       node.init && nodeIterator.traverse(node.init, { scope }); 
  10.       node.test ? nodeIterator.traverse(node.test, { scope }) : true
  11.       node.update && nodeIterator.traverse(node.update, { scope }) 
  12.     ) { 
  13.       const signal = nodeIterator.traverse(node.body, { scope }) 
  14.        
  15.       if (Signal.isBreak(signal)) { 
  16.         break 
  17.       } else if (Signal.isContinue(signal)) { 
  18.         continue 
  19.       } else if (Signal.isReturn(signal)) { 
  20.         return signal 
  21.       } 
  22.     } 
  23.   }  

同理,for...in,while和do...while循环也是类似的处理方式,这里不再赘述。

12、If声明节点处理器——IfStatemtnt()

处理If语句,包括if,if...else,if...elseif...else。

  1. IfStatement (nodeIterator) { 
  2.    if (nodeIterator.traverse(nodeIterator.node.test)) { 
  3.      return nodeIterator.traverse(nodeIterator.node.consequent) 
  4.    } else if (nodeIterator.node.alternate) { 
  5.      return nodeIterator.traverse(nodeIterator.node.alternate) 
  6.    } 
  7.  }  

同理,switch语句、三目表达式也是类似的处理方式。

---

上面列出了几个比较重要的节点处理器,在es5当中还有很多节点需要处理,详细内容可以访问这个地址一探究竟。

七、定义调用方式

经过了上面的所有步骤,解析器已经具备处理es5代码的能力,接下来就是对这些散装的内容进行组装,最终定义一个方便用户调用的办法。

  1. const { Parser } = require('acorn'
  2. const NodeIterator = require('./iterator'
  3. const Scope = require('./scope'
  4.  
  5. class Canjs { 
  6.   constructor (code = '', extraDeclaration = {}) { 
  7.     this.code = code 
  8.     this.extraDeclaration = extraDeclaration 
  9.     this.ast = Parser.parse(code) 
  10.     this.nodeIterator = null 
  11.     this.init() 
  12.   } 
  13.  
  14.   init () { 
  15.     // 定义全局作用域,该作用域类型为函数作用域 
  16.     const globalScope = new Scope('function'
  17.     // 根据入参定义标准库之外的全局变量 
  18.     Object.keys(this.extraDeclaration).forEach((key) => { 
  19.       globalScope.addDeclaration(key, this.extraDeclaration[key]) 
  20.     }) 
  21.     this.nodeIterator = new NodeIterator(null, globalScope) 
  22.   } 
  23.  
  24.   run () { 
  25.     return this.nodeIterator.traverse(this.ast) 
  26.   } 
  27.  

这里我们定义了一个名为Canjs的基类,接受字符串形式的JS代码,同时可定义标准库之外的变量。当运行run()方法的时候就可以得到运行结果。

八、后续

至此,整个JS解析器已经完成,可以很好地运行ES5的代码(可能还有bug没有发现)。但是在当前的实现中,所有的运行结果都是放在一个类似沙盒的地方,无法对外界产生影响。如果要把运行结果取出来,可能的办法有两种。***种是传入一个全局的变量,把影响作用在这个全局变量当中,借助它把结果带出来;另外一种则是让解析器支持export语法,能够把export语句声明的结果返回,感兴趣的读者可以自行研究。

***,这个JS解析器已经在我的Github上开源,欢迎前来交流~

https://github.com/jrainlau/c... 

责任编辑:庞桂玉 来源: segmentfault
相关推荐

2021-04-23 16:40:49

Three.js前端代码

2022-10-20 11:49:49

JS动画帧,CSS

2012-08-14 10:44:52

解释器编程

2022-06-05 13:52:32

Node.jsDNS 的原理DNS 服务器

2022-10-08 00:06:00

JS运行V8

2020-10-29 16:00:03

Node.jsweb前端

2021-06-25 10:38:05

JavaScript编译器前端开发

2021-11-10 09:10:46

JS 录屏功能JavaScript

2023-12-20 21:30:26

2022-07-11 22:53:59

JavaScrip编译信小程序

2022-03-07 09:20:00

JavaScripThree.jsNFT

2024-05-15 10:07:11

Agents人工智能CSV

2021-04-11 09:00:13

Fes.js前端

2021-11-16 12:25:14

jsPPT前端

2013-03-18 10:31:22

JS异常

2023-04-10 14:20:47

ChatGPTRESTAPI

2024-02-04 19:15:09

Nest.js管理项目

2011-06-17 10:29:04

Nodejavascript

2022-03-04 14:17:08

JS工具库录音

2021-02-10 07:31:12

VuejsElementUI
点赞
收藏

51CTO技术栈公众号