diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..851287c8e84438af1b6a6b2ad8e6092a5e9bf3e5 --- /dev/null +++ b/README.md @@ -0,0 +1,340 @@ +# 从零开始写一个Javascript解析器 + +版权 +最近在研究 AST, 之前有一篇文章 面试官: 你了解过 Babel 吗?写过 Babel 插件吗? 答: 没有。卒 为什么要去了解它? 因为懂得 AST 真的可以为所欲为 + +简单点说,使用 Javascript 运行Javascript代码。 + +这篇文章来告诉你,如何写一个最简单的解析器。 + +前言(如果你很清楚如何执行自定义 js 代码,请跳过) +在大家的认知中,有几种执行自定义脚本的方法?我们来列举一下: + +Web +创建 script 脚本,并插入文档流 +function runJavascriptCode(code) { + const script = document.createElement("script"); + script.innerText = code; + document.body.appendChild(script); +} + +runJavascriptCode("alert('hello world')"); +复制代码 +eval +无数人都在说,不要使用eval,虽然它可以执行自定义脚本 + +eval("alert('hello world')"); +复制代码 +参考链接: Why is using the JavaScript eval function a bad idea? + +setTimeout +setTimeout 同样能执行,不过会把相关的操作,推到下一个事件循环中执行 + +setTimeout("console.log('hello world')"); +console.log("I should run first"); + +// 输出 +// I should run first +// hello world' +复制代码 +new Function +new Function("alert('hello world')")(); +复制代码 +参考链接: Are eval() and new Function() the same thing? + +NodeJs +require +可以把 Javascript 代码写进一个 Js 文件,然后在其他文件 require 它,达到执行的效果。 + +NodeJs 会缓存模块,如果你执行 N 个这样的文件,可能会消耗很多内存. 需要执行完毕后,手动清除缓存。 + +Vm +const vm = require("vm"); + +const sandbox = { + animal: "cat", + count: 2 +}; + +vm.runInNewContext('count += 1; name = "kitty"', sandbox); +复制代码 +以上方式,除了 Node 能优雅的执行以外,其他都不行,API 都需要依赖宿主环境。 + +解释器用途 +在能任何执行 Javascript 的代码的平台,执行自定义代码。 + +比如小程序,屏蔽了以上执行自定义代码的途径 + +那就真的不能执行自定义代码了吗? + +非也 + +工作原理 +基于 AST(抽象语法树),找到对应的对象/方法, 然后执行对应的表达式。 + +这怎么说的有点绕口呢,举个栗子console.log("hello world"); + +原理: 通过 AST 找到console对象,再找到它log函数,最后运行函数,参数为hello world + +准备工具 +Babylon, 用于解析代码,生成 AST +babel-types, 判断节点类型 +astexplorer, 随时查看抽象语法树 +开始撸代码 +我们以运行console.log("hello world")为例 + +打开astexplorer, 查看对应的 AST + +由图中看到,我们要找到console.log("hello world"),必须要向下遍历节点的方式,经过File、Program、ExpressionStatement、CallExpression、MemberExpression节点,其中涉及到Identifier、StringLiteral节点 + +我们先定义visitors, visitors是对于不同节点的处理方式 + +const visitors = { + File(){}, + Program(){}, + ExpressionStatement(){}, + CallExpression(){}, + MemberExpression(){}, + Identifier(){}, + StringLiteral(){} +}; +复制代码 +再定义一个遍历节点的函数 + +/** + * 遍历一个节点 + * @param {Node} node 节点对象 + * @param {*} scope 作用域 + */ +function evaluate(node, scope) { + const _evalute = visitors[node.type]; + // 如果该节点不存在处理函数,那么抛出错误 + if (!_evalute) { + throw new Error(`Unknown visitors of ${node.type}`); + } + // 执行该节点对应的处理函数 + return _evalute(node, scope); +} +复制代码 +下面是对各个节点的处理实现 + +const babylon = require("babylon"); +const types = require("babel-types"); + +const visitors = { + File(node, scope) { + evaluate(node.program, scope); + }, + Program(program, scope) { + for (const node of program.body) { + evaluate(node, scope); + } + }, + ExpressionStatement(node, scope) { + return evaluate(node.expression, scope); + }, + CallExpression(node, scope) { + // 获取调用者对象 + const func = evaluate(node.callee, scope); + + // 获取函数的参数 + const funcArguments = node.arguments.map(arg => evaluate(arg, scope)); + + // 如果是获取属性的话: console.log + if (types.isMemberExpression(node.callee)) { + const object = evaluate(node.callee.object, scope); + return func.apply(object, funcArguments); + } + }, + MemberExpression(node, scope) { + const { object, property } = node; + + // 找到对应的属性名 + const propertyName = property.name; + + // 找对对应的对象 + const obj = evaluate(object, scope); + + // 获取对应的值 + const target = obj[propertyName]; + + // 返回这个值,如果这个值是function的话,那么应该绑定上下文this + return typeof target === "function" ? target.bind(obj) : target; + }, + Identifier(node, scope) { + // 获取变量的值 + return scope[node.name]; + }, + StringLiteral(node) { + return node.value; + } +}; + +function evaluate(node, scope) { + const _evalute = visitors[node.type]; + if (!_evalute) { + throw new Error(`Unknown visitors of ${node.type}`); + } + // 递归调用 + return _evalute(node, scope); +} + +const code = "console.log('hello world')"; + +// 生成AST树 +const ast = babylon.parse(code); + +// 解析AST +// 需要传入执行上下文,否则找不到``console``对象 +evaluate(ast, { console: console }); +复制代码 +在 Nodejs 中运行试试看 + +$ node ./index.js +hello world +复制代码 +然后我们更改下运行的代码 const code = "console.log(Math.pow(2, 2))"; + +因为上下文没有Math对象,那么会得出这样的错误 TypeError: Cannot read property 'pow' of undefined + +记得传入上下文evaluate(ast, {console, Math}); + +再运行,又得出一个错误Error: Unknown visitors of NumericLiteral + +原来Math.pow(2, 2)中的 2,是数字字面量 + +节点是NumericLiteral, 但是在visitors中,我们却没有定义这个节点的处理方式. + +那么我们就加上这么个节点: + +NumericLiteral(node){ + return node.value; + } +复制代码 +再次运行,就跟预期结果一致了 + +$ node ./index.js +4 +复制代码 +到这里,已经实现了最最基本的函数调用了 + +进阶 +既然是解释器,难道只能运行 hello world 吗?显然不是 + +我们来声明个变量吧 +var name = "hello world"; +console.log(name); +复制代码 +先看下 AST 结构 + +visitors中缺少VariableDeclaration和VariableDeclarator节点的处理,我们给加上 + +VariableDeclaration(node, scope) { + const kind = node.kind; + for (const declartor of node.declarations) { + const {name} = declartor.id; + const value = declartor.init + ? evaluate(declartor.init, scope) + : undefined; + scope[name] = value; + } + }, + VariableDeclarator(node, scope) { + scope[node.id.name] = evaluate(node.init, scope); + } +复制代码 +运行下代码,已经打印出hello world + +我们再来声明函数 +function test() { + var name = "hello world"; + console.log(name); +} +test(); +复制代码 +根据上面的步骤,新增了几个节点 + +BlockStatement(block, scope) { + for (const node of block.body) { + // 执行代码块中的内容 + evaluate(node, scope); + } + }, + FunctionDeclaration(node, scope) { + // 获取function + const func = visitors.FunctionExpression(node, scope); + + // 在作用域中定义function + scope[node.id.name] = func; + }, + FunctionExpression(node, scope) { + // 自己构造一个function + const func = function() { + // TODO: 获取函数的参数 + // 执行代码块中的内容 + evaluate(node.body, scope); + }; + + // 返回这个function + return func; + } +复制代码 + +然后修改下CallExpression + +// 如果是获取属性的话: console.log +if (types.isMemberExpression(node.callee)) { + const object = evaluate(node.callee.object, scope); + return func.apply(object, funcArguments); +} else if (types.isIdentifier(node.callee)) { + // 新增 + func.apply(scope, funcArguments); // 新增 +} +复制代码 +运行也能过打印出hello world + +完整示例代码 + +其他 +限于篇幅,我不会讲怎么处理所有的节点,以上已经讲解了基本的原理。 + +对于其他节点,你依旧可以这么来,其中需要注意的是: 上文中,作用域我统一用了一个 scope,没有父级/子级作用域之分 + +也就意味着这样的代码是可以运行的 + +var a = 1; +function test() { + var b = 2; +} +test(); +console.log(b); // 2 +复制代码 +处理方法: 在递归 AST 树的时候,遇到一些会产生子作用域的节点,应该使用新的作用域,比如说function,for in等 + +最后 +以上只是一个简单的模型,它连玩具都算不上,依旧有很多的坑。比如: + +变量提升, 作用域应该有预解析阶段 +作用域有很多问题 +特定节点,必须嵌套在某节点下。比如 super()就必须在 Class 节点内,无论嵌套多少层 +this 绑定 +... +连续几个晚上的熬夜之后,我写了一个比较完善的库vm.js,基于jsjs修改而来,站在巨人的肩膀上。 + +与它不同的是: + +重构了递归方式,解决了一些没法解决的问题 +修复了多项 bug +添加了测试用例 +支持 es6 以及其他语法糖 +目前正在开发中, 等待更加完善之后,会发布第一个版本。 + +欢迎大佬们拍砖和 PR. + +小程序今后变成大程序,业务代码通过 Websocket 推送过来执行,小程序源码只是一个空壳,想想都刺激. + +项目地址: github.com/axetroy/vm.… + +在线预览: axetroy.github.io/vm.js/ + +原文: axetroy.xyz/#/post/172 \ No newline at end of file diff --git a/index.js b/index.js index 2d7e6834fb6366b3120c7a37cc5f637bc4a33928..6922f45c0691fe596bfc1958805644d859afde19 100644 --- a/index.js +++ b/index.js @@ -1 +1,82 @@ -console.log("欢迎来到 InsCode"); \ No newline at end of file + +const babylon = require("babylon"); +const types = require("babel-types"); + +const visitors = { + File(node, scope) { + evaluate(node.program, scope); + }, + Program(program, scope) { + for (const node of program.body) { + evaluate(node, scope); + } + }, + NumericLiteral(node){ + return node.value; + }, + ExpressionStatement(node, scope) { + return evaluate(node.expression, scope); + }, + CallExpression(node, scope) { + // 获取调用者对象 + const func = evaluate(node.callee, scope); + + // 获取函数的参数 + const funcArguments = node.arguments.map(arg => evaluate(arg, scope)); + + // 如果是获取属性的话: console.log + if (types.isMemberExpression(node.callee)) { + const object = evaluate(node.callee.object, scope); + return func.apply(object, funcArguments); + } + }, + MemberExpression(node, scope) { + const { object, property } = node; + + // 找到对应的属性名 + const propertyName = property.name; + + // 找对对应的对象 + const obj = evaluate(object, scope); + + // 获取对应的值 + const target = obj[propertyName]; + + // 返回这个值,如果这个值是function的话,那么应该绑定上下文this + return typeof target === "function" ? target.bind(obj) : target; + }, + Identifier(node, scope) { + // 获取变量的值 + return scope[node.name]; + }, + StringLiteral(node) { + return node.value; + } +}; +/** + * 遍历一个节点 + * @param {Node} node 节点对象 + * @param {*} scope 作用域 + */ +function evaluate(node, scope) { + const _evalute = visitors[node.type]; + // 如果该节点不存在处理函数,那么抛出错误 + if (!_evalute) { + throw new Error(`Unknown visitors of ${node.type}`); + } + // 执行该节点对应的处理函数 + return _evalute(node, scope); +} + + +const code = "console.log(Math.pow(2, 2))"; + +// 生成AST树 +const ast = babylon.parse(code); + +console.log('ast', ast); + +// 解析AST +// 需要传入执行上下文,否则找不到``console``对象 +evaluate(ast, { console: console, Math }); + diff --git a/package.json b/package.json index 72caa1750a1c44c18460a496d258fbd3c51c673a..ce3040b8d58b6790076625ab3d8b35b3afe63064 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,19 @@ { - "name": "nodejs", - "version": "1.0.0", - "description": "", - "main": "index.js", - "scripts": { - "dev": "node index.js", - "test": "echo \"Error: no test specified\" && exit 1" - }, - "keywords": [], - "author": "", - "license": "ISC", - "dependencies": { - "@types/node": "^18.0.6", - "node-fetch": "^3.2.6" - } + "name": "nodejs", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "dev": "node index.js", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@types/node": "^18.0.6", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "node-fetch": "^3.2.6" } - \ No newline at end of file +} diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000000000000000000000000000000000000..91897d150deb8ac50162a9804eb83489e825a98a --- /dev/null +++ b/yarn.lock @@ -0,0 +1,95 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@types/node@^18.0.6": + version "18.16.0" + resolved "http://mirrors.csdn.net.cn/repository/csdn-npm-mirrors/@types/node/-/node-18.16.0.tgz#4668bc392bb6938637b47e98b1f2ed5426f33316" + integrity sha512-BsAaKhB+7X+H4GnSjGhJG9Qi8Tw+inU9nJDwmD5CgOmBLEI6ArdhikpLX7DjbjDRDTbqZzU2LSQNZg8WGPiSZQ== + +babel-runtime@^6.26.0: + version "6.26.0" + resolved "http://mirrors.csdn.net.cn/repository/csdn-npm-mirrors/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" + integrity sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g== + dependencies: + core-js "^2.4.0" + regenerator-runtime "^0.11.0" + +babel-types@^6.26.0: + version "6.26.0" + resolved "http://mirrors.csdn.net.cn/repository/csdn-npm-mirrors/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497" + integrity sha512-zhe3V/26rCWsEZK8kZN+HaQj5yQ1CilTObixFzKW1UWjqG7618Twz6YEsCnjfg5gBcJh02DrpCkS9h98ZqDY+g== + dependencies: + babel-runtime "^6.26.0" + esutils "^2.0.2" + lodash "^4.17.4" + to-fast-properties "^1.0.3" + +babylon@^6.18.0: + version "6.18.0" + resolved "http://mirrors.csdn.net.cn/repository/csdn-npm-mirrors/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" + integrity sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ== + +core-js@^2.4.0: + version "2.6.12" + resolved "http://mirrors.csdn.net.cn/repository/csdn-npm-mirrors/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec" + integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ== + +data-uri-to-buffer@^4.0.0: + version "4.0.1" + resolved "http://mirrors.csdn.net.cn/repository/csdn-npm-mirrors/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz#d8feb2b2881e6a4f58c2e08acfd0e2834e26222e" + integrity sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A== + +esutils@^2.0.2: + version "2.0.3" + resolved "http://mirrors.csdn.net.cn/repository/csdn-npm-mirrors/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +fetch-blob@^3.1.2, fetch-blob@^3.1.4: + version "3.2.0" + resolved "http://mirrors.csdn.net.cn/repository/csdn-npm-mirrors/fetch-blob/-/fetch-blob-3.2.0.tgz#f09b8d4bbd45adc6f0c20b7e787e793e309dcce9" + integrity sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ== + dependencies: + node-domexception "^1.0.0" + web-streams-polyfill "^3.0.3" + +formdata-polyfill@^4.0.10: + version "4.0.10" + resolved "http://mirrors.csdn.net.cn/repository/csdn-npm-mirrors/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz#24807c31c9d402e002ab3d8c720144ceb8848423" + integrity sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g== + dependencies: + fetch-blob "^3.1.2" + +lodash@^4.17.4: + version "4.17.21" + resolved "http://mirrors.csdn.net.cn/repository/csdn-npm-mirrors/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +node-domexception@^1.0.0: + version "1.0.0" + resolved "http://mirrors.csdn.net.cn/repository/csdn-npm-mirrors/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5" + integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== + +node-fetch@^3.2.6: + version "3.3.1" + resolved "http://mirrors.csdn.net.cn/repository/csdn-npm-mirrors/node-fetch/-/node-fetch-3.3.1.tgz#b3eea7b54b3a48020e46f4f88b9c5a7430d20b2e" + integrity sha512-cRVc/kyto/7E5shrWca1Wsea4y6tL9iYJE5FBCius3JQfb/4P4I295PfhgbJQBLTx6lATE4z+wK0rPM4VS2uow== + dependencies: + data-uri-to-buffer "^4.0.0" + fetch-blob "^3.1.4" + formdata-polyfill "^4.0.10" + +regenerator-runtime@^0.11.0: + version "0.11.1" + resolved "http://mirrors.csdn.net.cn/repository/csdn-npm-mirrors/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" + integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== + +to-fast-properties@^1.0.3: + version "1.0.3" + resolved "http://mirrors.csdn.net.cn/repository/csdn-npm-mirrors/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47" + integrity sha512-lxrWP8ejsq+7E3nNjwYmUBMAgjMTZoTI+sdBOpvNyijeDLa29LUn9QaoXAHv4+Z578hbmHHJKZknzxVtvo77og== + +web-streams-polyfill@^3.0.3: + version "3.2.1" + resolved "http://mirrors.csdn.net.cn/repository/csdn-npm-mirrors/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz#71c2718c52b45fd49dbeee88634b3a60ceab42a6" + integrity sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==