实现JavaScript语言解释器-三

前言

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

evaluate函数和作用域

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// 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循环执行的过程来看一下具体过程是怎么样的:

被执行的代码:

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

ForStatement代码的执行过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 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语言的闭包是如何实现的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 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绑定的是哪个对象的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 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/XiaocongDong/simple。

最后希望大家经过这三篇系列文章的学习可以对编译原理和JavaScript一些比较难懂的语言特性有一定的了解,也希望后面我可以继续给大家带来优质的内容来让我们共同进步。

个人技术动态

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

实现JavaScript语言解释器-(二)

前言

在上一篇文章中我为大家介绍了Simpe项目的一些背景知识以及如何使用有限状态机来实现词法解析,在本篇文章中我将会为大家介绍语法分析的相关内容,并且通过设计一门内部DSL语言来实现Simple语言的语法解析。

实现JavaScript语言解释器(一)

前言

对于大多数前端开发者来说JavaScript可谓是我们最熟悉的编程语言了,它十分强大可是有些语言特性却十分难以理解,例如闭包this绑定等概念往往会让初学者摸不着头脑。网上有很多诸如《你看完这篇还不懂this绑定就来砍我》之类的文章来为大家传道解惑。可是在我看来这些文章大多流于表面,你读了很多可能还是会被面试官问倒。 那么如何才能彻彻底底理解这些语言特性,从而在面试的时候立于不败之地呢?在我看来要想真的理解一样东西,最好的途径就是实现这样东西,这也是西方程序员非常喜欢说的learning by implementing。例如,你想更好地理解React,那么最好的办法就是你自己动手实现一个React。因此为了更好地理解JavaScript的语言特性,我就自己动手实现了一个叫做Simple的JavaScript语言解释器,这个解释器十分简单,它基于TypeScript实现了JavaScript语法的子集,主要包括下面这些功能:

React Hook测试指南

React为什么需要Hook中我们探讨了React为什么需要引入Hook这个属性,在React Hook实战指南中我们深入了解了各种Hook的详细用法以及会遇到的问题,在本篇文章中我将带大家了解一下如何通过为自定义hook编写单元测试来提高我们的代码质量,它会包含下面的内容:

  • 什么是单元测试
    • 单元测试的定义
    • 为什么需要编写单元测试
    • 单元测试需要注意什么
  • 如何对自定义Hook进行单元测试
    • Jest
    • React-hooks-testing-library
    • 例子

React Hook实践指南

React为什么需要Hook这篇文章中我们探讨了React开发团队为什么要为Function Component添加Hook的原因,在本篇文章中我将会为大家提供一份较为全面的React Hook实践指南,其中包括以下方面的内容:

React为什么需要Hook

自从React 16.8发布Hook之后,笔者已经在实际项目中使用Hook快一年了,虽然Hook在使用中存在着一些坑,但是总的来说它是一个很好的功能,特别是在减少模板代码和提高代码复用率这些方面特别有用。为了让更多的人了解和使用Hook,我决定写一系列和Hook相关的文章,本篇文章就是这个系列的第一篇,主要和大家聊一下React为什么需要Hook

进击的JAMStack

最近在研究JAMStack的一些相关内容,发现这的确是个好东西,所以想写一篇文章把这个概念分享给还不了解JAMStack的同学。本篇文章主要包含以下的内容:

  • 什么是JAMStack
  • JAMStack有什么优势
  • JAMStack适合什么应用
  • 我的个人思考

什么是JAMStack

概念

JAMStack中的JAM其实是三个词的缩写,它们分别是JavaScript, APIs以及Markdown。而Stack用中文的说法就是技术栈(Tech Stack),也就是我们在构建应用的时候具体使用到的技术的集合。举个例子,国外现在比较火的一个Stack叫做Mean Stack,它表示使用MongoDB + Express.js + AngularJS + Node.js这些技术来构建一个Web应用。因此用最通俗易懂的话来描述JAMStack就是:使用JavaScript,APIs和Markdown三种技术来构建Web应用。所以JAMStack是一种问题解决方案,而不是一个具体的实现。

接着我们再具体看一下JavaScript,APIs和Markdown这三种技术在JAMStack的世界中是起到什么作用的。

Yarn v2介绍

Yarn作为JavaScript生态的一个强大的依赖管理工具在今年1月24日的时候正式发布了v2版本。在本篇文章中,我将会为大家介绍以下内容:

备注:如果你想知道如何直接使用v2版本可以查看Getting Started,如果你想从v1版本迁移到v2版本可以查看Migrations

CSS in JS的好与坏

是什么

CSS-in-JS是一种技术(technique),而不是一个具体的库实现(library)。简单来说CSS-in-JS就是将应用的CSS样式写在JavaScript文件里面,而不是独立为一些.css.scss或者less之类的文件,这样你就可以在CSS中使用一些属于JS的诸如模块声明,变量定义,函数调用和条件判断等语言特性来提供灵活的可扩展的样式定义。值得一提的是,虽然CSS-in-JS不是一种很新的技术,可是它在国内普及度好像并不是很高,它当初的出现是因为一些component-based的Web框架(例如React,Vue和Angular)的逐渐流行,使得开发者也想将组件的CSS样式也一块封装到组件中去以解决原生CSS写法的一系列问题。还有就是CSS-in-JS在React社区的热度是最高的,这是因为React本身不会管用户怎么去为组件定义样式的问题,而Vue和Angular都有属于框架自己的一套定义样式的方案。

本文将通过分析CSS-in-JS这项技术带来的好处以及它存在的一些问题来帮助大家判断自己是不是要在项目中使用CSS-in-JS。

窥探现代浏览器架构-三

前言

本文是笔者对Mario Kosaka写的inside look at modern web browser系列文章的翻译。这里的翻译不是指直译,而是结合个人的理解将作者想表达的意思表达出来,而且会尽量补充一些相关的内容来帮助大家更好地理解。

渲染进程里面发生的事

这篇文章是探究Chrome内部工作原理的四集系列文章中的第三篇。之前我们分别探讨了Chrome的多进程架构以及导航的过程都发生了什么。在本篇文章中,我们将要窥探一下渲染进程在渲染页面的时候具体都发生了什么事情。

渲染进程会影响到Web性能的很多方面。页面渲染的时候发生的东西实在太多了,本篇文章只能作一个大体的介绍。如果你想要了解更多相关的内容,Web Fundamentals的Performance栏目有很多资源可以查看。

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×