乐趣区

关于javascript:实现JavaScript语言解释器三

前言

上篇文章我为大家介绍了 语法解析 的一些基本概念,以及如何通过自定义的 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.ts
class 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.ts
class 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.ts
class 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 语言解释器 - 二

集体技术动静

文章首发于我的博客平台

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

退出移动版