当前位置: 首页 > 后端技术 > Node.js

JavaScript语言解释器的实现(三)

时间:2023-04-03 10:13:37 Node.js

前言上一篇介绍了语法分析的一些基本概念,以及如何通过自定义的DSL语言实现Simple语言解释器的语法树分析。在这一篇和本系列的最后一篇文章中,我将介绍Simple解释器如何执行生成的语法树。evaluatefunction和scope在之前介绍文法分析相关知识的时候已经出现过。事实上,基本上每个AST节点都会有一个对应的evaluate函数。该函数的作用是告诉Simple解释器如何执行当前的AST节点。因此,Simple解释器执行代码的过程是:从根节点开始执行当前节点的求值函数,然后递归执行子节点求值函数的过程。我们知道,在执行JavaScript代码时,有一个概念叫做作用域。当我们访问一个变量时,我们会首先检查该变量是否定义在当前范围内。如果没有,我们将沿着作用域链向上寻找全局作用域。作用域,如果作用域链上没有变量的定义,会抛出UncaughtReferenceError:xxisnotdefined错误。我在实现Simplelanguageinterpreter的时候,参考了JavaScriptscope的概念,实现了一个名为Environment的类。我们看一下Environment类的实现://lib/runtime/Environment.ts//Environment类是简单语言ScopeclassEnvironment{//parent指向当前作用域的父作用域privateparent:Environment=null//values对象会以key-value的形式存储当前作用域变量的引用和值//例如values={a:10},表示有一个变量a在当前作用域内,其值为10个受保护的值:Object={}//当当前作用域内有新的变量定义时,会调用create函数设置值//比如执行时让a=10,env.create('a',10)会被调用create(key:string,value:any){if(this.values.hasOwnProperty(key)){thrownewError(`${key}hasbeeninitialized`)}this.values[key]=value}//如果一个变量被重新赋值,Simple会沿着当前作用域链搜索最近的符合条件的作用域,然后在这个作用域中更新(key:string,value:any){constmatchedEnvironment=this.getEnvironmentWithKey(key)if(!matchedEnvironment){thrownewError(`UncaughtReferenceError:${key}hasn'tbeendefined`)}matchedEnvironment.values={...matchedEnvironment.values,[key]:value}}//在作用域链上寻找一些东西变量,如果没有找到,抛出UncaughtReferenceErrorerrorget(key:string){constmatchedEnvironment=this.getEnvironmentWithKey(key)if(!matchedEnvironment){thrownewError(`UncaughtReferenceError:${key}isnotdefined`)}returnmatchedEnvironment.values[key]}//沿作用域链查找变量的值,如果找不到则返回nullprivategetEnvironmentWithKey(key:string):Environment{if(this.values.hasOwnProperty(key)){returnthis}letcurrentEnvironment=this.parentwhile(currentEnvironment){if(currentEnvironment.values.hasOwnProperty(key)){returncurrentEnvironment}currentEnvironment=currentEnvironment.parent}returnnull}}从上面的代码和注释可以看出so-作用域链实际上是一个由Environment实例组成的单向链表。当解析一个变量值时,它将沿着这个作用域链进行搜索。如果没有找到变量的定义,就会报错。那么我们通过for循环的执行来看看具体的过程:执行的代码:for(leti=0;i<10;i++){console.log(i);};ForStatement代码的执行Process://lib/ast/node/ForStatement.tsclassForStatementextendsNode{...//evaluate函数会接受一个scope对象,表示当前AST节点的执行范围evaluate(env:Environment):any{//上面for循环括号内的内容是一个独立的作用域,所以需要根据父节点传递的作用域新建一个作用域,命名为bridgeEnvironmentconstbridgeEnvironment=newEnvironment(env)//if括号中的变量初始化(leti=0)将在此范围内执行。this.init.evaluate(bridgeEnvironment)//如果当前作用域没有被break语句退出&&return语句返回&&测试表达式(i<10)是否为真值,for循环会继续执行,否则for循环会被打断while(!runtime.isBreak&&!runtime.isReturn&&this.test.evaluate(bridgeEnvironment)){//因为for循环体(console.log(i))是一个新的作用域,所以创建一个新的子作用域基于当前brigeEnvironmentconstexecutionEnvironment=newEnvironment(bridgeEnvironment)this.body.evaluate(executionEnvironment)//循环变量(i++)的更新会在brigeEnvironment中执行this.update.evaluate(bridgeEnvironment)}}}闭包与this绑定了解了evalute函数的大致执行流程后,我们来看看闭包是如何实现的。我们都知道JavaScript是词法作用域,也就是说一个函数的作用域链是在这个函数定义的时候决定的。下面通过函数声明节点FunctionDeclaration的evaluate函数的代码来看看Simple语言的闭包是如何实现的//lib/ast/node/FunctionDeclaration.tsclassFunctionDeclarationextendsNode{...//当函数declaration语句执行完,就会执行evaluate函数,传入的对象为当前执行范围evaluate(env:Environment):any{//生成一个新的FunctionDeclaration对象,因为同一个函数可能会被定义多次(比如asthis当函数嵌套并定义在父函数中时)constfunc=newFunctionDeclaration()//函数复制func.loc=this.locfunc.id=this.idfunc.params=[...this.params]func.body=this.body//声明函数时,会通过parentEnv属性记录当前执行范围,也就是闭包!!!func.parentEnv=env//将函数注册到当前执行范围,可以递归调用该函数env.create(this.id.name,func)}...}从上面的代码我们可以看出,要实现Simple语言的闭包,其实只需要在函数声明时记录当前作用域(parentEnv)即可。那么我们再来看看函数执行时如何判断this绑定了哪个对象://lib/ast/node/FunctionDeclaration.tsclassFunctionDeclarationextendsNode{...//函数执行时,如果有一个调用函数实例,会作为参数传入,比如a.test(),a是test的参数call(args:Array,callerInstance?:any):any{//当传入函数执行时如果参数小于声明的参数,会报错//这是实现闭包的重点,函数执行时的父作用域就是之前定义函数时记录的父作用域!!constcallEnvironment=newEnvironment(this.parentEnv)//函数参数初始化为(leti=0;i