用JavaScript写一个JavaScript解释器【玩具级】

236 阅读5分钟

第一节课

课程目的

  • 对JavaScript是如何运行的,有一个基本的了解
  • 对编译工具有基本的认识
  • 最好有能力去动手开发一些基本的编译工具或插件

编程语言和自然语言的区别

既要让人能看懂,又要机器能准确的认识,但这是冲突的。

抽象符号

名可名,非常名。名,只是当时当下表示事物的符号。

计算机不关心,但是人关心。

Uglify

抽象结构

抽象语法树

image.png

代码和AST的异同

  • 同:同一种信息的两种不同的表达方式
  • 不同:前者给人看,后者给机器看。

image.png

推荐书籍

image.png

image.png

提到的一些东西

  • haskell
  • lisp
  • 编译后端
  • 词法分析器 Parser

第二节课

一些作业问题

  • 编译时和运行时
  • 变量名会出现在哪些地方

表达式

  • 一种完备形式定义的递归结构

函数是一等公民,如同值,是表达式的一部分

所谓的函数是抽象了一个过程

一旦有变量就会依赖环境

作业提交

第三问 超纲挑战

需要改造一个env,把它变成一个类,并且有自己的父节点(实现原型链),这样才能往上找到变量使用或修改它。也在这里才发现自己以前对于引用类型的理解是错的,JavaScript里的引用类型如同C中的指针,挺好用的,但是之前一直把它当成值,所以一开始还纳闷这样子怎么能修改到它真正的父节点,一是以为访问不到,二是以为空间会疯狂复制然后爆炸,但写了个demo才理解了所谓的引用类型。

class Person {
    constructor(name, age, parent) {
        this.name = name
        this.age = age
        this.parent = parent
    }
    setParent(age) {
        this.parent.age = age
        // console.log('zi\'fu',this.parent);
    }
}

let a = new Person('fu',50)
let b = new Person('zi', 5, a)
console.log(a.age); //50
b.setParent(60);
console.log(a.age)  //60

提交代码

创建分支 git branch homework-2

切换分支 git checkout homework-2

提交到本地仓库 add commit

创建远程分支 git push --set-upstream origin homework-2 发现它创建分支时会自动push

第三节课

作业二讲解

第二个case的功能

用匿名函数实现了阶乘

声明、指令、作用域、控制流

声明不跟代码绑定,而是跟作用域绑定

声明只是在某个作用域里声明了一个名,它只做着一件事

指令的三种类型

  • 赋值 => 改变作用域
  • 调用 => 作用域的访问者
  • 控制流 => if for tryCatch
    • 改变代码执行流程
    • 通过作用域(环境)

但在JavaScript中,赋值是一个表达式

作用域

作用:把某一个范围封闭起来,进行分治

JavaScript中的三种作用域

  • global
  • function
  • block (在ES5时没有这个概念)

第四节课 字节码与虚拟机

上一节作业讲解

声明提升问题

return/continue/break的问题

字节码与虚拟机

虚拟机 & 指令

问题:不同CPU的指令集无法通用

C语言最初的目的——跨平台(听起来有点神奇但是又觉得合理哈哈哈哈

JavaScript 和 Java 在执行时的区别

  • JavaScript多了一层“运行时”,如V8/node,完了之后才是跑在OS
  • Java是跑在VM上,VM又跑在操作系统上面

JavaScript是解释性的,Java是编译型的

image.png

分类

  • 寄存器虚拟机,和物理机类似(CPU与内存的交互过程)
  • 栈式虚拟机

指令与环境

字节码

字节码不是机器码

  • 一种从代码到执行的中间表示
  • 一个二进制文件,里面存着数据和虚拟机执行的指令

设计一个虚拟机

环境:代码执行到某个位置的上下文信息/状态信息

image.png

栈的作用:记录入口环境

这部分的内容和《汇编语言》(王爽)讲的物理机的做法是非常类似的,一个道理。

如何实现PC(point current)的跳转,也就是子函数调用导致了PC的变化,子函数执行完后如何跳回父函数?如要在这调用之前记录下从哪里(父函数的某个地方)跳过来的,如果有很多层这个过程,其实就是一个栈的模拟。

image.png

虚拟机有哪些指令?

  • 转移指令
  • 运算指令
  • 跳转指令

提到的一些东西

解释/编译 反射

JS / JIT / AOC

第五讲 线程、协程与异步

异步

异步非常依赖callback,就是结果返回的一个通知。

线程

切换线程的时候需要切换环境

JavaScript的单线程异步

单线程异步可以解决内存安全问题

单线程异步框架的好处:不用去处理任何线程之间的关系和问题

缺点:遇到运算密集型的场景,会出现一核出力,多核围观。

内存安全

协程

用户态的任务调度,解决线程之间高频切换带来的高开销。

由用户的程序来调度,是应用层上的一个任务。

对比

image.png

遇到的困难

构造函数this的指向

解决方案:当函数定义的时候,就在scope里设置一个变量this的值为当前的this

case 'FunctionDeclaration': {
  let childScope = new Scope({}, scope, 'function')
  node.body.body.forEach(v => {
    hoisting(v, childScope)
  })
  let f = function (...args) {
    // 每次使用函数都要去重新获取一下this
    childScope.variables['this'] = this
    childScope.isDefine['this'] = 'let'
    
    node.params.map((v, index) => {
      childScope.variables[v.name] = args[index]
      childScope.isDefine[v.name] = 'let'
    })
    return evaluate(node.body, childScope).value
  }
  return f
}

在使用函数的时候,通过 new (func.bind.apply(func, [null].concat(args))) 来重新改变this的指向,当函数执行的时候,所取得的this就会是新的传入的this

大作业

image.png

历时25天,终于写完了。完结撒花 ~