手写 JS 引擎来解释一道赋值面试题

关注公众号 前端开发博客,领27本电子书

回复加群,自助秒进前端群

有这样一道面试题:

let a = { n: 1};

a.x = a = { n: 2};

console.log(a.x);

问输出的是啥。

这道题输出的是 undefined,因为赋值是从左往右进行的,也就是先把 {n: 2} 赋值给 a.x 再赋值给 a。

再加个变量 preA 引用赋值之前的 a 就能看出来:

137e2c90dc216c657c846ad85afb1448.png

这就是运算符优先级的问题,就像加减乘除运算符也是从左往右按照优先级来算一样。 . 运算符的优先级比 = 高

写这篇文章不是为了讲运算符优先级问题,而是想自己实现一个 JS 引擎来解释执行这段代码。

怎么实现 JS 引擎呢?

JS 引擎的构成

一个 JS 引擎由四部分构成:Parser(解析器)、Interperter(解释器)、JIT Compiler(JIT 编译器)、Garbage Collector(垃圾收集器)。

f336d30de6cdf68ec9a92af4fd0ed7ca.png

源码经过 Parser 解析成 AST,也就是计算机能处理的对象树的结构,然后用解释器递归的解释每个节点,这就是解释执行的过程。

但是解释执行比较慢,为了优化速度又引入了 JIT 编译器,会把经常执行的热点代码编译成机器码直接执行。

同时,内存是有限制的,要不断的循环清除不再被使用的内存,也就是垃圾回收,这是 GC 做的事情。

如果我们自己实现简单的 JS 引擎,那可以省略掉 JIT 编译器和 GC,这两个分别是优化时间和空间的,不是必须的。

也就是只要实现 Parser 和 Interpreter(解释器)就行:

8fa6a26adeff36cb2d08d1990746c4ba.png

这就是我们要实现的 JS 引擎的结构。

简易 JS 引擎实现思路分析

Parser 可以是任意的 JS Parser,我们直接用 @babel/parser。

它产生的 AST 可以用 astexplorer.net 可视化的查看:

6a38570ee16ac85550fb4645a9630cd1.png

怎么解释执行呢?

解释 AST 也就是递归解释每个 AST 节点,那具体的 AST 节点又怎么解释呢?

比如这个 {n:1} 的对象表达式,它对应的是 ObjectExpression 节点,它有 ObjectProperty 属性的子节点,子节点有 key、value 属性。

自然可以想到,解释 ObjectExpression 节点就是取出 AST 中的数据构造一个对象返回:

c5d19133ea307e5354a310c6f2393c90.png

再比如 let a = { n: 1} 这条赋值语句,它对应的是 VariableDeclaration 节点,下面有多个 VariableDeclarator 子节点,这是因为一条声明语句可以声明多个变量,比如 let a = 1, b =1; 而具体的声明 VariableDeclarator 里分别有 id 和 init 部分。

c7fe732cf4ba78e24859b505f97b445e.png

init 部分就是 ObjectExpression,解释它就是构造一个对象返回。那么解释整个声明自然就是在作用域中放一个名字为 id 节点的 value 为名字的变量,值就是 init 节点的解释执行的结果。

Identifier 是标识符的意思,也就是这里的 a。

对象表达式 ObjectExpression 和声明语句 VariableDeclaration 的解释执行就是这样,不难吧。

其他 AST 节点的解释也是这样,递归的解释每个节点,这就是解释执行 JS 代码的实现原理。

知道了该怎么做,那我们来写代码实现下吧:

声明语句的解释执行

我们先实现声明语句的解释执行,后面再实现赋值语句的解释执行:

Parser 使用 babel parser,用它的 parse 方法把源码转成 AST,然后解释执行 AST:

const parser = require('@babel/parser');

function eval() {
    const ast = parser.parse(code);
    evaluate(ast.program);
}

const scope = new Map();

function evaluate(node) {

}

const code = `
let a = { dong: 111};
let b = { guang: 222};
let c = b;
`
eval(code);

console.log(scope);

我们声明了一个 Map 对象作为全局作用域。

接下来就是实现解释器了,也就是 evaluate 方法。

我们前面分析过了,解释执行就是递归处理每个 AST,每种 AST 的处理方式不同:

const astInterpreters = {
    Program(node) {
        node.body.forEach(item => {
            evaluate(item);
        })
    },
    VariableDeclaration(node) {
    },
    VariableDeclarator(node) {
    },
    ObjectExpression(node) {
    },
    Identifier(node) {
        return node.name;
    },
    NumericLiteral(node) {
        return node.value;
    }
}

function evaluate(node) {
    try {
        return astInterpreters[node.type](node);
    } catch(e) {
        console.error('不支持的节点类型:', e);
    }
}

我们实现了几个简单的 AST 的解释:Program 根节点,它的解释执行就是解释执行 body 的每一条语句。Identifier(标识符) 就是取 name 属性,NumericLiteral(数字字面量)就是取 value 属性。

然后是对象表达式 ObjectExpression 的解释了,这个就是构造一个对象返回:

ObjectExpression(node) {
    const obj = {};
    node.properties.forEach(prop => {
        const key = evaluate(prop.key);
        const value = evaluate(prop.value);
        obj[key] = value;
    });
    return obj;
}

也就是取 properties 中的每个节点,拿到 key 和 value 的解释执行结果,设置到对象中,最后把对象返回。

声明语句的解释就是在作用域(我们声明的 Map)中设置下就行:

VariableDeclaration(node) {
    node.declarations.forEach((item) => {
        evaluate(item);
    });
},
VariableDeclarator(node) {
    const declareName = evaluate(node.id);
    if (scope.get(declareName)) {
        throw Error('duplicate declare variable:' + declareName);
    } else {
        const valueNode = node.init;
        let value;
        if (valueNode.type === 'Identifier') {
            value = scope.get(valueNode.name);
        } else {
            value = evaluate(valueNode);
        }
        scope.set(declareName, value);
    }
},

VariableDeclaration 是声明语句,因为具体的声明可能有多个,所以要循环执行每个声明。

具体的声明 VariableDeclarator 就是在 scope 中设置变量名和它的值。

变量名是 node.id 的执行结果,如果声明过就报错,因为只能声明一次。

否则就取 node.init 的值设置到 scope 中,也就是 scope.set(declarationName, value)。

但是值的处理要注意下,如果是 Identifier 也就是标识符,它其实是一个引用,比如变量 a,那么我们要先从作用域中拿到它的具体值。

这些节点的解释执行逻辑写好了,那么我们就能解释这段代码了:

let a = { dong: 111};
let b = { guang: 222};
let c = b;

声明了 a、b、c 三个变量,a、b 初始值都是对象字面量、c 是引用自 b。

执行之后我们打印下 scope:

4f43877f31daebf3cae06a833ec87610.png

执行成功!我们实现了最简单的 JS 引擎!

当然,只是声明还不够,接下来再实现赋值语句的解释:

赋值语句的解释执行

赋值语句的解释也就是解释 AssignmentExpression 节点,用 astexplorer.net 看下它的结构:

c7f4307e77d152997dedf46a82cccccf.png

它外面怎么还包裹了个 ExpressionStatement 节点呢?

因为表达式不能直接执行,语句才是执行的基本单位,那么表达式包裹一层表达式语句(ExpressionStatement)就可以了。

AssignmentExpression 有 left 和 right 属性,分别是 = 左右部分对应的节点。

如果 right 还是 AssignmentExpression 呢?

那么要继续取 right,知道拿到不是 AssignmentExpression 的节点,这就是要赋值的值。

而它左边的所有的节点都是赋值的目标,从左到右依次赋值即可。

所以,AssignmentExpression 节点的解释执行是这样的:

我们不是要顺着 right 往右找么,那就声明 curNode 代表当前节点,然后 while 循环往右找,过程中的所有 left 都放到一个数组里:

let curNode = node;
const targetArr = [curNode.left];
while(curNode.right.type === 'AssignmentExpression') {
    curNode = curNode.right;
    targetArr.push(curNode.left);
}

最后一个 right 就是赋值的值的 AST,解释执行之后拿到它的值。

const value = evaluate(curNode.right);

然后把 value 赋值给 targetArr 中的所有变量就行了,也就是从左往右依次赋值:

这里要区分下 a 和 a.x 的赋值:

如果是 a,也就是 Identifier,那么设置道作用域中就行,也就是 scope.set(varName, value)。

如果是 a.x,也就是 MemberExpression,那么要从作用域中拿到对象的部分也就是 scope.get(objName) 的部分,然后再设置属性。

也就是:

targetArr.forEach(target => {
    if (target.type === 'Identifier'){
        const varName = evaluate(target);
        scope.set(varName, value);
    } else if (target.type === 'MemberExpression') {
        const objName = evaluate(target.object);
        const obj = scope.get(objName);

        const propName = evaluate(target.property);
        obj[propName] = value;
    }  
})

赋值语句的解释执行的完整代码如下:

AssignmentExpression(node) {
    let curNode = node;
    const targetArr = [curNode.left];
    while(curNode.right.type === 'AssignmentExpression') {
        curNode = curNode.right;
        targetArr.push(curNode.left);
    }
    const value = evaluate(curNode.right);

    targetArr.forEach(target => {
        if (target.type === 'Identifier'){
            const varName = evaluate(target);
            scope.set(varName, value);
        } else if (target.type === 'MemberExpression') {
            const objName = evaluate(target.object);
            const obj = scope.get(objName);

            const propName = evaluate(target.property);
            obj[propName] = value;
        }  
    })
}

实现了声明语句、赋值语句的解释其实就达到我们的目标了。我们执行下开头代码:

let a = { n: 1};
let preA = a;
a.x = a = { n: 2};

看下 scope 中的值:

50f50e8a2564dd460cd48d48718fbcd4.png

为啥 a.x 是 undefined 不就解释清楚了么。

全部代码上传到了 github:https://github.com/QuarkGluonPlasma/babel-article-code

总结

我们做了一道赋值语句的面试题,它考察的就是运算符从左到右执行。

但是只是知道赋值运算符怎么执行的还不够,我们自己写了一个 JS 引擎来执行它。

JS 引擎由 Parser、Interpreter(解释器)、JIT Compiler、Garbage Collector 四部分构成。其中 JIT 编译器和 GC 分别是优化时间和空间用的,不是必须的。所以,我们实现了只有 Parser 和 Interpreter 的 JS 引擎。

Parser 使用任何 JS parser 都行,我们使用了 babel parser,解释器的实现就是递归解释每个节点,我们分别实现了声明语句和赋值语句的解释执行。

最终,我们得到了最开始的结果,并且还清楚的知道了赋值语句是怎么解释执行的。

(当然,因为只需要解释这几行代码,解释器并不完善,更完善的解释器可以看 《Babel 插件通关秘籍》小册中的 JS 解释器的案例。)

往期推荐:

2021年前端各大公司都考了那些手写题(附带代码)

一文帮你搞定90%的JS手写题

万字长文:分享前端性能优化知识体系

面试常见的7个高频正则表达式,没你想的那么难!

另外欢迎大家围观我的朋友圈,搞搞技术,吹吹牛逼。关注我,秒添加,回复加群,可以进入 500人前端群。

d848980f668b49549a2d4806cba8b1c0.png

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
题目描述: 给定一篇手写笔记的图片,要求用代码实现将其还原成文字。 示例: 输入: ![handwriting](https://cdn.luogu.com.cn/upload/image_hosting/jz2h2y1f.png) 输出: ``` Hello, world! This is a handwriting recognition problem. Can you solve it? Let's see. 1. First, let's import some libraries. - numpy: a popular library for numerical computing - PIL: Python Imaging Library, for image processing - tensorflow: a popular library for machine learning 2. Next, let's load the image and preprocess it. - Convert it to grayscale - Resize it to a fixed size - Invert the colors 3. Now we can use a pre-trained OCR model to recognize the text. - We'll use Tesseract, an open-source OCR engine. - It works best on clean, high-quality images, so we'll need to do some post-processing. 4. Finally, let's print the recognized text and see how well we did. - We should be able to get pretty good accuracy if the image is clear and the text is legible. ``` 解题思路: 1. 首先需要加载一些常用的 Python 库,如 numpy、PIL 和 tensorflow。 2. 接着需要读取手写笔记的图片,并对其进行预处理。具体来说,需要将其转换为灰度图像、调整大小并反转颜色。 3. 接下来,可以使用一个预训练的 OCR 模型来识别文本。这里使用的是 Tesseract,一个开源的 OCR 引擎。需要注意的是,Tesseract 在处理高质量、清晰的图像时表现最佳,因此在处理后需要进行一些后处理。 4. 最后,将识别出的文本打印出来,以便进行检查。 代码实现: ```python import numpy as np from PIL import Image import pytesseract # Step 1: Load the image and preprocess it img = Image.open('handwriting.png').convert('L') img = img.resize((int(img.size[0] * 2), int(img.size[1] * 2))) img = ImageOps.invert(img) # Step 2: Use Tesseract to recognize the text text = pytesseract.image_to_string(img) # Step 3: Post-process the recognized text text = text.replace('\n\n', '\n') text = text.strip() # Step 4: Print the recognized text print(text) ``` 其中,`pytesseract` 库需要提前安装。在 Ubuntu 系统下,可以使用以下命令进行安装: ``` sudo apt install tesseract-ocr sudo apt install libtesseract-dev pip install pytesseract ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值