前言

上篇文章我为大家介绍了语法解析的一些基本概念,以及如何通过自定义的DSL语言实现Simple语言解释器的语法树解析。在本篇也是这个系列最初一篇文章中我将为大家介绍Simple解释器是如何执行生成的语法树的。

evaluate函数和作用域

后面在介绍语法解析相干常识的时候有呈现过evaluate函数,其实根本每一个AST节点都会有一个对应的evaluate函数,这个函数的作用就是通知Simple解释器如何执行以后AST节点。因而Simple解释器执行代码的过程就是:从根节点开始执行以后节点的evaluate函数而后递归地执行子节点evalute函数的过程

咱们晓得JavaScript代码执行的时候有一个概念叫做作用域,当咱们拜访一个变量的时候,会先看看以后作用域有没有定义这个变量,如果没有就会沿着作用域链向上始终寻找到全局作用域,如果作用域链上都没有该变量的定义的话就会抛出一个Uncaught ReferenceError: xx is not defined的谬误。在实现Simple语言解释器的时候,我参照了JavaScript作用域的概念实现了一个叫做Environment的类,咱们来看看Evironment类的实现:

// lib/runtime/Environment.ts// Environment类就是Simple语言的作用域class Environment {  // parent指向以后作用域的父级作用域  private parent: Environment = null  // values对象会以key-value的模式存储以后作用域变量的援用和值  // 例如values = {a: 10},代表以后作用域有一个变量a,它的值是10  protected values: Object = {}  // 以后作用域有新的变量定义的时候会调用create函数进行值的设置  // 例如执行 let a = 10 时,会调用env.create('a', 10)  create(key: string, value: any) {    if(this.values.hasOwnProperty(key)) {      throw new Error(`${key} has been initialized`)    }    this.values[key] = value  }  // 如果某个变量被从新赋值,Simple会沿着以后作用域链进行寻找,找到最近的符合条件的作用域,而后在该作用域上进行从新赋值  update(key: string, value: any) {    const matchedEnvironment = this.getEnvironmentWithKey(key)    if (!matchedEnvironment) {      throw new Error(`Uncaught ReferenceError: ${key} hasn't been defined`)    }    matchedEnvironment.values = {      ...matchedEnvironment.values,      [key]: value    }  }  // 在作用域链上寻找某个变量,如果没有找到就抛出Uncaught ReferenceError的谬误  get(key: string) {    const matchedEnvironment = this.getEnvironmentWithKey(key)    if (!matchedEnvironment) {      throw new Error(`Uncaught ReferenceError: ${key} is not defined`)    }    return matchedEnvironment.values[key]  }  // 沿着作用域链向上寻找某个变量的值,如果没有找到就返回null  private getEnvironmentWithKey(key: string): Environment {    if(this.values.hasOwnProperty(key)) {      return this    }      let currentEnvironment = this.parent    while(currentEnvironment) {      if (currentEnvironment.values.hasOwnProperty(key)) {        return currentEnvironment      }      currentEnvironment = currentEnvironment.parent    }    return null  }}

从下面的代码以及正文能够看出,所谓的作用域链其实就是由Environment实例组成的单向链表。解析某个变量值的时候会沿着这个作用域链进行寻找,如果没有找到该变量的定义就会报错。接着咱们以for循环执行的过程来看一下具体过程是怎么样的:

被执行的代码:

for(let i = 0; i < 10; i++) {  console.log(i);};

ForStatement代码的执行过程:

// lib/ast/node/ForStatement.tsclass ForStatement extends Node {  ...  // evaluate函数会承受一个作用域对象,这个对象代表以后AST节点的执行作用域  evaluate(env: Environment): any {    // 下面for循环括号外面的内容是在一个独立的作用域外面的,所以须要基于父级节点传递过去的作用域新建一个作用域,取名为bridgeEnvironment    const bridgeEnvironment = new Environment(env)    // if括号内的变量初始化(let i = 0)会在这个作用域外面进行    this.init.evaluate(bridgeEnvironment)    // 如果以后作用域没有被break语句退出 && return语句返回 && 测试表达式(i < 10)是真值,for循环就会继续执行,否则for循环中断    while(!runtime.isBreak && !runtime.isReturn && this.test.evaluate(bridgeEnvironment)) {      // 因为for循环体(console.log(i))是一个新的作用域,所以要基于以后的brigeEnvironment新建一个子作用域      const executionEnvironment = new Environment(bridgeEnvironment)      this.body.evaluate(executionEnvironment)      // 循环变量的更新(i++)会在brigeEnvironment外面执行      this.update.evaluate(bridgeEnvironment)    }  }}

闭包和this绑定

在了解了evalute函数的个别执行过程后,咱们再来看看闭包是如何实现的。咱们都晓得JavaScript是词法作用域,也就是说一个函数的作用域链在这个函数被定义的时候就决定了。咱们通过函数申明节点FunctionDeclaration的evaluate函数的代码来看一下Simple语言的闭包是如何实现的:

// lib/ast/node/FunctionDeclaration.tsclass FunctionDeclaration extends Node {  ...  // 当函数申明语句被执行的时候,这个evaluate函数会被执行,传进来的对象就是以后的执行作用域  evaluate(env: Environment): any {    // 生成一个新的FunctionDeclaration对象,因为同一个函数可能被屡次定义(例如这个函数被嵌套定义在某个父级函数的时候)    const func = new FunctionDeclaration()    // 函数复制    func.loc = this.loc    func.id = this.id    func.params = [...this.params]    func.body = this.body        // 函数被申明的时候会通过parentEnv属性记录下以后的执行作用域,这就是闭包了!!!    func.parentEnv = env    // 将函数注册到以后的执行作用域下面,该函数就能够被递归调用了    env.create(this.id.name, func)  }  ...}

从下面的代码能够看出,要实现Simple语言的闭包,其实只须要在函数申明的时候记录一下以后作用域(parentEnv)就能够了

接着咱们再来看一下函数执行的时候是如何判断this绑定的是哪个对象的:

// lib/ast/node/FunctionDeclaration.tsclass FunctionDeclaration extends Node {  ...  // 函数执行的时候,如果存在调用函数的实例,该实例会被当做参数传进来,例如a.test(),a就是test的这个参数  call(args: Array<any>, callerInstance?: any): any {    // 函数执行时传进来的参数如果少于申明的参数会报错    if (this.params.length !== args.length) {      throw new Error('function declared parameters are not matched with arguments')    }    // 这是实现闭包的重点,函数执行时的父级作用域是之前函数被定义的时候记录下来的父级作用域!!    const callEnvironment = new Environment(this.parentEnv)        // 函数参数进行初始化    for (let i = 0; i < args.length; i++) {      const argument = args[i]      const param = this.params[i]      callEnvironment.create(param.name, argument)    }    // 创立函数的arguments对象    callEnvironment.create('arguments', args)    // 如果以后函数有调用实例,那么这个函数的this将会是调用实例    if (callerInstance) {      callEnvironment.create('this', callerInstance)    } else {      // 如果函数没有调用实例,就会沿着函数的作用域链就行寻找,直到全局的process(node)或者window(browser)对象      callEnvironment.create('this', this.parentEnv.getRootEnv().get('process'))    }    // 函数体的执行    this.body.evaluate(callEnvironment)  }}

下面的代码大略给大家介绍了Simple语言的this是如何绑定的,实际上JavaScript的实现可能和这个有比拟大的出入,这里只是给大家一个参考而已。

总结

在本篇文章中我给大家介绍了Simple解释器是如何执行代码的,其中包含闭包和this绑定的内容,因为篇幅限度这里疏忽了很多内容,例如for和while循环的break语句是如何退出的,函数的return语句是如何将值传递给父级函数的,大家如果感兴趣能够看一下我的源码:
https://github.com/XiaocongDo...

最初心愿大家通过这三篇系列文章的学习能够对编译原理和JavaScript一些比拟难懂的语言个性有肯定的理解,也心愿前面我能够持续给大家带来优质的内容来让咱们共同进步。

  • 实现JavaScript语言解释器-一
  • 实现JavaScript语言解释器-二

集体技术动静

文章首发于我的博客平台

欢送关注公众号进击的大葱一起学习成长