JS写一个JS解释器

本文介绍了为何要在JavaScript中编写JS解释器,主要讨论了编译器和解释器的概念,以及如何通过遍历抽象语法树(AST)、处理作用域和编写节点处理器来实现JS解释器。文章还涉及到节点遍历器、作用域处理、关键字判断工具和各种节点处理器的实现细节。
摘要由CSDN通过智能技术生成

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

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

二、什么是编译器

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

const a =1
console.log(a)
经过编译器处理后,它的AST长这样:
{
   
  "type":"Program",
  "start":0,
  "end":26,
  "body":[
    {
   
      "type":"VariableDeclaration",
      "start":0,
      "end":11,
      "declarations":[
        {
   
          "type":"VariableDeclarator",
          "start":6,
          "end":11,
          "id":{
   
            "type":"Identifier",
            "start":6,
            "end":7,
            "name":"a"
          },
          "init":{
   
            "type":"Literal",
            "start":10,
            "end":11,
            "value":1,
            "raw":"1"
          }
        }
      ],
      "kind":"const"
    },
    {
   
      "type":"ExpressionStatement",
      "start":12,
      "end":26,
      "expression":{
   
        "type":"CallExpression",
        "start":12,
        "end":26,
        "callee":{
   
          "type":"MemberExpression",
          "start":12,
          "end":23,
          "object":{
   
            "type":"Identifier",
            "start":12,
            "end":19,
            "name":"console"
          },
          "property":{
   
            "type":"Identifier",
            "start":20,
            "end":23,
            "name":"log"
          },
          "computed":false
        },
        "arguments":[
          {
   
            "type":"Identifier",
            "start":24,
            "end":25,
            "name":"a"
          }
        ]
      }
    }
  ],
  "sourceType":"module"}

常见的JS编译器有babylon,acorn等等,感兴趣的同学可以在AST explorer这个网站自行体验。
可以看到,编译出来的AST详细记录了代码中所有语义代码的类型、起始位置等信息。这段代码除了根节点Program外,主体包含了两个节点VariableDeclaration和ExpressionStatement,而这些节点里面又包含了不同的子节点。
正是由于AST详细记录了代码的语义化信息,所以Babel,Webpack,Sass,Less等工具可以针对代码进行非常智能的处理。

三、什么是解释器

如同翻译人员不仅能看懂一门外语,也能对其艺术加工后把它翻译成母语一样,人们把能够将代码转化成AST的工具叫做“编译器”,而把能够将AST翻译成目标语言并运行的工具叫做“解释器”。
在编译原理的课程中,我们思考过这么一个问题:如何让计算机运行算数表达式1+2+3:
1+2+3
当机器执行的时候,它可能会是这样的机器码:
1 PUSH 12 PUSH 23 ADD
4 PUSH 35 ADD
而运行这段机器码的程序,就是解释器。
在这篇文章中,我们不会搞出机器码这样复杂的东西,仅仅是使用JS在其runtime环境下去解释JS代码的AST。由于解释器使用JS编写,所以我们可以大胆使用JS自身的语言特性,比如this绑定、new关键字等等,完全不需要对它们进行额外处理,也因此让JS解释器的实现变得非常简单。
在回顾了编译原理的基本概念之后,我们就可以着手进行开发了。

四、节点遍历器

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

const nodeHandler ={
   
  Program(){
   },
  VariableDeclaration(){
   },
  ExpressionStatement(){
   },
  MemberExpression(){
   },
  CallExpression(){
   },
  Identifier(){
   }}

关于节点处理器的具体实现,会在后文进行详细探讨,这里暂时不作展开。
有了节点处理器,我们便需要去遍历AST当中的每一个节点,递归地调用节点处理器,直到完成对整棵语法书的处理。
定义一个节点遍历器(NodeIterator):

classNodeIterator{
   
  constructor (node){
   
    this.node = node
    this.nodeHandler = nodeHandler
  }

  traverse (node){
   
    // 根据节点类型找到节点处理器当中对应的函数
    const _eval =this.nodeHandler[node.type]
    // 若找不到则报错
    if(!_eval){
   
      thrownewError(`canjs: Unknown node type "${
     node.type}".`)
    }
    // 运行处理函数
    return _eval(node)
  }}

理论上,节点遍历器这样设计就可以了,但仔细推敲,发现漏了一个很重要的东西——作用域处理。
回到节点处理器的VariableDeclaration()方法,它用来处理诸如const a = 1这样的变量声明节点。假设它的代码如下:

VariableDeclaration(node){
   
    for(const declaration of node.declarations){
   
      const{
   
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

X W F

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值