js ...运算符_「 giao-js 」用js写一个js解释器
前言
在這篇文章中,我們將通過 JS 構建我們自己的 JS 解釋器,用 JS 寫 JS,這聽起來很奇怪,盡管如此,這樣做我們將更熟悉 JS,也可以學習 JS 引擎是如何工作的!什么是解釋器 (Interpreter) ?
解釋器是在運行時運行的語言求值器,它動態地執行程序的源代碼。 解釋器解析源代碼,從源代碼生成 AST(抽象語法樹),遍歷 AST 并逐個計算它們。解釋器 (Interpreter) 工作原理
- 詞法分析 (Tokenization)
- 語法解析 (Parsing)
- 求值 (Evaluating)
詞法分析 (Tokenization)
將源代碼分解并組織成一組有意義的單詞,這一過程即為詞法分析(Token)。在英語中,當我們遇到這樣一個語句時:
Javascript is the best language in the world我們會下意識地把句子分解成一個個單詞:
+----------------------------------------------------------+ | Javascript | is | the | best | language | in |the |world | +----------------------------------------------------------+這是分析和理解句子的第一階段。
詞法分析是由詞法分析器完成的,詞法分析器會掃描(scanning)代碼,提取詞法單元。
var a = 1;[("var": "keyword"),("a": "identifier"),("=": "assignment"),("1": "literal"),(";": "separator"), ];詞法分析器將代碼分解成 Token 后,會將 Token 傳遞給解析器進行解析,我們來看下解析階段是如何工作的。
語法解析 (Parsing)
將詞法分析階段生成的 Token 轉換為抽象語法樹(Abstract Syntax Tree),這一過程稱之為語法解析(Parsing)。在英語中,Javascript is the best language 被分解為以下單詞:
+------------------------------------------+ | Javascript | is | the | best | language | +------------------------------------------+這樣我們就可以挑選單詞并形成語法結構:
"Javascript": Subject "is the best language": Predicate "language": ObjectJavascript 在語法中是一個主語名詞,其余的是一個沒有什么意義的句子叫做謂語,language 是動作的接受者,也就是賓語。結構是這樣的:
Subject(Noun) -> Predicate -> Object語法解析是由語法解析器完成的,它會將上一步生成的 Token,根據語法規則,轉為抽象語法樹(AST)。
{type: "Program",body: [{type: "VariableDeclaration",declarations: [{type: "VariableDeclarator",id: {type: "Identifier",name: "sum"},init: {type: "Literal",value: 30,raw: "30"}}],kind: "var"}], }求值階段 (Evaluating)
解釋器將遍歷 AST 并計算每個節點。- 求值階段1 + 2 ||v +---+ +---+ | 1 | | 2 | +---+ +---+///+---+| + |+---+ {lhs: 1,op: '+'.rhs: 2 }解釋器解析 Ast,得到 LHS 節點,接著收集到操作符(operator)節點+,+操作符表示需要進行一次加法操作,它必須有第二個節點來進行加法操作.接著他收集到 RHS 節點。它收集到了有價值的信息并執行加法得到了結果,3。
{type: "Program",body: [{type: "ExpressionStatement",expression: {type: "BinaryExpression",left: {type: "Literal",value: 1,raw: "1"},operator: "+",right: {type: "Literal",value: 2,raw: "2"}}}], }實踐
前面我們已經介紹了解釋器的工作原理,接下來我們來動動手松松筋骨吧,實現一個 Mini Js Interpreter~
實踐準備
- Acorn.js
本次實踐我們將使用 acorn.js ,它會幫我們進行詞法分析,語法解析并轉換為抽象語法樹。
Webpack/Rollup/Babel(@babel/parser) 等第三方庫也是使用 acorn.js 作為自己 Parser 的基礎庫。(站在巨人的肩膀上啊!)
- The Estree Spec
最開始 Mozilla JS Parser API 是 Mozilla 工程師在 Firefox 中創建的 SpiderMonkey 引擎輸出 JavaScript AST 的規范文檔,文檔所描述的格式被用作操作 JAvaScript 源代碼的通用語言。
隨著 JavaScript 的發展,更多新的語法被加入,為了幫助發展這種格式以跟上 JavaScript 語言的發展。The ESTree Spec 就誕生了,作為參與構建和使用這些工具的人員的社區標準。
acorn.js parse 返回值符合 ESTree spec 描述的 AST 對象,這里我們使用@types/estree 做類型定義。
- Jest
號稱令人愉快的 JavaScript 測試...我們使用它來進行單元測試.
- Rollup
Rollup 是一個 JavaScript 模塊打包器,我們使用它來打包,以 UMD 規范對外暴露模塊。
項目初始化
// visitor.ts 創建一個Visitor類,并提供一個方法操作ES節點。 import * as ESTree from "estree"; class Visitor {visitNode(node: ESTree.Node) {// ...} } export default Visitor; // interpreter.ts 創建一個Interpreter類,用于運行ES節點樹。 // 創建一個Visitor實例,并使用該實例來運行ESTree節點 import Visitor from "./visitor"; import * as ESTree from "estree"; class Interpreter {private visitor: Visitor;constructor(visitor: Visitor) {this.visitor = visitor;}interpret(node: ESTree.Node) {this.visitor.visitNode(node);} } export default Interpreter; // vm.ts 對外暴露run方法,并使用acorn code->ast后,交給Interpreter實例進行解釋。 const acorn = require("acorn"); import Visitor from "./visitor"; import Interpreter from "./interpreter";const jsInterpreter = new Interpreter(new Visitor());export function run(code: string) {const root = acorn.parse(code, {ecmaVersion: 8,sourceType: "script",});return jsInterpreter.interpret(root); }實踐第 1 彈: 1+1= ?
我們這節來實現 1+1 加法的解釋。首先我們通過AST explorer,看看 1+1 這段代碼轉換后的 AST 結構。
我們可以看到這段代碼中存在 4 種節點類型,下面我們簡單的介紹一下它們:
Program
根節點,即代表一整顆抽象語法樹,body 屬性是一個數組,包含了多個 Statement 節點。
interface Program {type: "Program";sourceType: "script" | "module";body: Array<Directive | Statement | ModuleDeclaration>;comments?: Array<Comment>; }ExpressionStatement
表達式語句節點,expression 屬性指向一個表達式節點對象
interface ExpressionStatement {type: "ExpressionStatement";expression: Expression; }BinaryExpression
二元運算表達式節點,left 和 right 表示運算符左右的兩個表達式,operator 表示一個二元運算符。 本節實現的重點,簡單理解,我們只要拿到 operator 操作符的類型并實現,然后對 left,right 值進行求值即可。
interface BinaryExpression {type: "BinaryExpression";operator: BinaryOperator;left: Expression;right: Expression; }Literal
字面量,這里不是指 [] 或者 {} 這些,而是本身語義就代表了一個值的字面量,如 1,“hello”, true 這些,還有正則表達式,如 /d?/。
type Literal = SimpleLiteral | RegExpLiteral;interface SimpleLiteral {type: "Literal";value: string | boolean | number | null;raw?: string; }interface RegExpLiteral {type: "Literal";value?: RegExp | null;regex: {pattern: string;flags: string;};raw?: string; }廢話少說,開擼!!!
// standard/es5.ts 實現以上節點方法import Scope from "../scope"; import * as ESTree from "estree"; import { AstPath } from "../types/index";const es5 = {// 根節點的處理很簡單,我們只要對它的body屬性進行遍歷,然后訪問該節點即可。Program(node: ESTree.Program) {node.body.forEach((bodyNode) => this.visitNode(bodyNode));},// 表達式語句節點的處理,同樣訪問expression 屬性即可。ExpressionStatement(node: ESTree.ExpressionStatement>) {return this.visitNode(node.expression);},// 字面量節點處理直接求值,這里對正則表達式類型進行了特殊處理,其他類型直接返回value值即可。Literal(node: ESTree.Literal>) {if ((<ESTree.RegExpLiteral>node).regex) {const { pattern, flags } = (<ESTree.RegExpLiteral>node).regex;return new RegExp(pattern, flags);} else return node.value;},// 二元運算表達式節點處理// 對left/node兩個節點(Literal)進行求值,然后實現operator類型運算,返回結果。BinaryExpression(node: ESTree.BinaryExpression>) {const leftNode = this.visitNode(node.left);const operator = node.operator;const rightNode = this.visitNode(node.right);return {"+": (l, r) => l + r,"-": (l, r) => l - r,"*": (l, r) => l * r,"/": (l, r) => l / r,"%": (l, r) => l % r,"<": (l, r) => l < r,">": (l, r) => l > r,"<=": (l, r) => l <= r,">=": (l, r) => l >= r,"==": (l, r) => l == r,"===": (l, r) => l === r,"!=": (l, r) => l != r,"!==": (l, r) => l !== r,}[operator](leftNode, rightNode);}, }; export default es5; // visitor.ts import Scope from "./scope"; import * as ESTree from "estree"; import es5 from "./standard/es5";const VISITOR = {...es5, }; class Visitor {// 實現訪問節點方法,通過節點類型訪問對應的節點方法visitNode(node: ESTree.Node) {return {visitNode: this.visitNode,...VISITOR,}[node.type](node);} } export default Visitor;就這樣,普通的二元運算就搞定啦!!!
實踐第 2 彈: 怎么找到變量?
Javascript 的作用域與作用域鏈的概念想必大家都很熟悉了,這里就不再啰嗦了~
是的,我們需要通過實現作用域來訪問變量,實現作用域鏈來搜尋標識符。
在這之前,我們先實現 Variable 類,實現變量的存取方法。
// variable.ts export enum Kind {var = "var",let = "let",const = "const", } export type KindType = "var" | "let" | "const"; export class Variable {private _value: any;constructor(public kind: Kind, val: any) {this._value = val;}get value() {return this._value;}set value(val: any) {this._value = val;} } import { Variable, Kind, KindType } from "./variable";class Scope {// 父作用域private parent: Scope | null;// 當前作用域private targetScope: { [key: string]: any };constructor(public readonly type, parent?: Scope) {this.parent = parent || null;this.targetScope = new Map();}// 是否已定義private hasDefinition(rawName: string): boolean {return Boolean(this.search(rawName));}// var類型變量定義public defineVar(rawName: string, value: any) {let scope: Scope = this;// 如果不是全局作用域且不是函數作用域,找到全局作用域,存儲變量// 這里就是我們常說的Hoisting (變量提升)while (scope.parent && scope.type !== "function") {scope = scope.parent;}// 存儲變量scope.targetScope.set(rawName, new Variable(Kind.var, value));}// let類型變量定義public defineLet(rawName: string, value: any) {this.targetScope.set(rawName, new Variable(Kind.let, value));}// const類型變量定義public defineConst(rawName: string, value: any) {this.targetScope.set(rawName, new Variable(Kind.const, value));}// 作用域鏈實現,向上查找標識符public search(rawName: string): Variable | null {if (this.targetScope.get(rawName)) {return this.targetScope.get(rawName);} else if (this.parent) {return this.parent.search(rawName);} else {return null;}}// 變量聲明方法,變量已定義則拋出語法錯誤異常public declare(kind: Kind | KindType, rawName: string, value: any) {if (this.hasDefinition(rawName)) {console.error(`Uncaught SyntaxError: Identifier '${rawName}' has already been declared`);return true;}return {[Kind.var]: () => this.defineVar(rawName, value),[Kind.let]: () => this.defineLet(rawName, value),[Kind.const]: () => this.defineConst(rawName, value),}[kind]();} }export default Scope;以上就是變量對象,作用域及作用域鏈的基礎實現了,接下來我們就可以定義及訪問變量了。
實踐第 3 彈: var age = 18
從語法樹中我們可以看到三個陌生的節點類型,來看看它們分別代表什么意思:
VariableDeclaration
變量聲明,kind 屬性表示是什么類型的聲明,因為 ES6 引入了 const/let。 declarations 表示聲明的多個描述,因為我們可以這樣:let a = 1, b = 2;。
interface VariableDeclaration {type: "VariableDeclaration";declarations: Array<VariableDeclarator>;kind: "var" | "let" | "const"; }VariableDeclarator
變量聲明的描述,id 表示變量名稱節點,init 表示初始值的表達式,可以為 null。
interface VariableDeclarator {type: "VariableDeclarator";id: Pattern;init?: Expression | null; }Identifier
顧名思義,標識符節點,我們寫 JS 時定義的變量名,函數名,屬性名,都歸為標識符。
interface Identifier {type: "Identifier";name: string; }了解了對應節點的含義后,我們來進行實現:
// standard/es5.ts 實現以上節點方法import Scope from "../scope"; import * as ESTree from "estree";type AstPath<T> = {node: T;scope: Scope; };const es5 = {// ...// 這里我們定義了astPath,新增了scope作用域參數VariableDeclaration(astPath: AstPath<ESTree.VariableDeclaration>) {const { node, scope } = astPath;const { declarations, kind } = node;// 上面提到,生聲明可能存在多個描述(let a = 1, b = 2;),所以我們這里對它進行遍歷:// 這里遍歷出來的每個item是VariableDeclarator節點declarations.forEach((declar) => {const { id, init } = <ESTree.VariableDeclarator>declar;// 變量名稱節點,這里拿到的是ageconst key = (<ESTree.Identifier>id).name;// 判斷變量是否進行了初始化 ? 查找init節點值(Literal類型直接返回值:18) : 置為undefined;const value = init ? this.visitNode(init, scope) : undefined;// 根據不同的kind(var/const/let)聲明進行定義,即var age = 18scope.declare(kind, key, value);});},// 標識符節點,我們只要通過訪問作用域,訪問該值即可。Identifier(astPath: AstPath<ESTree.Identifier>) {const { node, scope } = astPath;const name = node.name;// walk identifier// 這個例子中查找的是age變量const variable = scope.search(name);// 返回的是定義的變量對象(age)的值,即18if (variable) return variable.value;}, }; export default es5;實踐第 4 彈: module.exports = 6
我們先來看看 module.exports = 6 對應的 AST。
從語法樹中我們又看到兩個陌生的節點類型,來看看它們分別代表什么意思:
AssignmentExpression
賦值表達式節點,operator 屬性表示一個賦值運算符,left 和 right 是賦值運算符左右的表達式。
interface AssignmentExpression {type: "AssignmentExpression";operator: AssignmentOperator;left: Pattern | MemberExpression;right: Expression; }MemberExpression
成員表達式節點,即表示引用對象成員的語句,object 是引用對象的表達式節點,property 是表示屬性名稱,computed 如果為 false,是表示 . 來引用成員,property 應該為一個 Identifier 節點,如果 computed 屬性為 true,則是 [] 來進行引用,即 property 是一個 Expression 節點,名稱是表達式的結果值。
interface MemberExpression {type: "MemberExpression";object: Expression | Super;property: Expression;computed: boolean;optional: boolean; }我們先來定義 module.exports 變量。
import Scope from "./scope"; import Visitor from "./visitor"; import * as ESTree from "estree"; class Interpreter {private scope: Scope;private visitor: Visitor;constructor(visitor: Visitor) {this.visitor = visitor;}interpret(node: ESTree.Node) {this.createScope();this.visitor.visitNode(node, this.scope);return this.exportResult();}createScope() {// 創建全局作用域this.scope = new Scope("root");// 定義module.exportsconst $exports = {};const $module = { exports: $exports };this.scope.defineConst("module", $module);this.scope.defineVar("exports", $exports);}// 模擬commonjs,對外暴露結果exportResult() {// 查找module變量const moduleExport = this.scope.search("module");// 返回module.exports值return moduleExport ? moduleExport.value.exports : null;} } export default Interpreter;ok,下面我們來實現以上節點函數~
// standard/es5.ts 實現以上節點方法import Scope from "../scope"; import * as ESTree from "estree";type AstPath<T> = {node: T;scope: Scope; };const es5 = {// ...// 這里我們定義了astPath,新增了scope作用域參數MemberExpression(astPath: AstPath<ESTree.MemberExpression>) {const { node, scope } = astPath;const { object, property, computed } = node;// property 是表示屬性名稱,computed 如果為 false,property 應該為一個 Identifier 節點,如果 computed 屬性為 true,即 property 是一個 Expression 節點// 這里我們拿到的是exports這個key值,即屬性名稱const prop = computed? this.visitNode(property, scope): (<ESTree.Identifier>property).name;// object 表示對象,這里為module,對module進行節點訪問const obj = this.visitNode(object, scope);// 訪問module.exports值return obj[prop];},// 賦值表達式節點(astPath: AstPath<ESTree.>) {const { node, scope } = astPath;const { left, operator, right } = node;let assignVar;// LHS 處理if (left.type === "Identifier") {// 標識符類型 直接查找const value = scope.search(left.name);assignVar = value;} else if (left.type === "MemberExpression") {// 成員表達式類型,處理方式跟上面差不多,不同的是這邊需要自定義一個變量對象的實現const { object, property, computed } = left;const obj = this.visitNode(object, scope);const key = computed? this.visitNode(property, scope): (<ESTree.Identifier>property).name;assignVar = {get value() {return obj[key];},set value(v) {obj[key] = v;},};}// RHS// 不同操作符處理,查詢到right節點值,對left節點進行賦值。return {"=": (v) => {assignVar.value = v;return v;},"+=": (v) => {const value = assignVar.value;assignVar.value = v + value;return assignVar.value;},"-=": (v) => {const value = assignVar.value;assignVar.value = value - v;return assignVar.value;},"*=": (v) => {const value = assignVar.value;assignVar.value = v * value;return assignVar.value;},"/=": (v) => {const value = assignVar.value;assignVar.value = value / v;return assignVar.value;},"%=": (v) => {const value = assignVar.value;assignVar.value = value % v;return assignVar.value;},}[operator](this.visitNode(right, scope));}, }; export default es5;ok,實現完畢,是時候驗證一波了,上 jest 大法。
// __test__/es5.test.tsimport { run } from "../src/vm"; describe("giao-js es5", () => {test("assign", () => {expect(run(`module.exports = 6;`)).toBe(6);}); }實踐第 5 彈: for 循環
var result = 0; for (var i = 0; i < 5; i++) {result += 2; } module.exports = result;到這一彈大家都發現了,不同的語法其實對應的就是不同的樹節點,我們只要實現對應的節點函數即可.我們先來看看這幾個陌生節點的含義.
ForStatement
for 循環語句節點,屬性 init/test/update 分別表示了 for 語句括號中的三個表達式,初始化值,循環判斷條件,每次循環執行的變量更新語句(init 可以是變量聲明或者表達式)。 這三個屬性都可以為 null,即 for(;;){}。
body 屬性用以表示要循環執行的語句。
UpdateExpression
update 運算表達式節點,即 ++/--,和一元運算符類似,只是 operator 指向的節點對象類型不同,這里是 update 運算符。
interface UpdateExpression {type: "UpdateExpression";operator: UpdateOperator;argument: Expression;prefix: boolean; }BlockStatement
塊語句節點,舉個例子:if (...) { // 這里是塊語句的內容 },塊里邊可以包含多個其他的語句,所以有一個 body 屬性,是一個數組,表示了塊里邊的多個語句。
interface BlockStatement {0;type: "BlockStatement";body: Array<Statement>;innerComments?: Array<Comment>; }廢話少說,盤它!!!
// standard/es5.ts 實現以上節點方法import Scope from "../scope"; import * as ESTree from "estree";type AstPath<T> = {node: T;scope: Scope; };const es5 = {// ...// for 循環語句節點ForStatement(astPath: AstPath<ESTree.ForStatement>) {const { node, scope } = astPath;const { init, test, update, body } = node;// 這里需要注意的是需要模擬創建一個塊級作用域// 前面Scope類實現,var聲明在塊作用域中會被提升,const/let不會const forScope = new Scope("block", scope);for (// 初始化值// VariableDeclarationinit ? this.visitNode(init, forScope) : null;// 循環判斷條件(BinaryExpression)// 二元運算表達式,之前已實現,這里不再細說test ? this.visitNode(test, forScope) : true;// 變量更新語句(UpdateExpression)update ? this.visitNode(update, forScope) : null) {// BlockStatementthis.visitNode(body, forScope);}},// update 運算表達式節點// update 運算表達式節點,即 ++/--,和一元運算符類似,只是 operator 指向的節點對象類型不同,這里是 update 運算符。UpdateExpression(astPath: AstPath<ESTree.UpdateExpression>) {const { node, scope } = astPath;// update 運算符,值為 ++ 或 --,配合 update 表達式節點的 prefix 屬性來表示前后。const { prefix, argument, operator } = node;let updateVar;// 這里需要考慮參數類型還有一種情況是成員表達式節點// 例: for (var query={count:0}; query.count < 8; query.count++)// LHS查找if (argument.type === "Identifier") {// 標識符類型 直接查找const value = scope.search(argument.name);updateVar = value;} else if (argument.type === "MemberExpression") {// 成員表達式的實現在前面實現過,這里不再細說,一樣的套路~const { object, property, computed } = argument;const obj = this.visitNode(object, scope);const key = computed? this.visitNode(property, scope): (<ESTree.Identifier>property).name;updateVar = {get value() {return obj[key];},set value(v) {obj[key] = v;},};}return {"++": (v) => {const result = v.value;v.value = result + 1;// preifx? ++i: i++;return prefix ? v.value : result;},"--": (v) => {const result = v.value;v.value = result - 1;// preifx? --i: i--;return prefix ? v.value : result;},}[operator](updateVar);},// 塊語句節點// 塊語句的實現很簡單,模擬創建一個塊作用域,然后遍歷body屬性進行訪問即可。BlockStatement(astPath: AstPath<ESTree.BlockStatement>) {const { node, scope } = astPath;const blockScope = new Scope("block", scope);const { body } = node;body.forEach((bodyNode) => {this.visitNode(bodyNode, blockScope);});}, }; export default es5;上 jest 大法驗證一哈~
test("test for loop", () => {expect(run(`var result = 0;for (var i = 0; i < 5; i++) {result += 2;}module.exports = result;`)).toBe(10); });你以為這樣就結束了嗎? 有沒有想到還有什么情況沒處理? for 循環的中斷語句呢?
var result = 0; for (var i = 0; i < 5; i++) {result += 2;break; // break,continue,return } module.exports = result;感興趣的小伙伴可以自己動手試試,或者戳源碼地址
結語
giao-js目前只實現了幾個語法,本文只是提供一個思路。
有興趣的同學可以查看完整代碼。
覺得有幫助到你的話,點個 star 支持下作者 ?? ~
參考
bramblex/jsjs
使用 Acorn 來解析 JavaScript
Build a JS Interpreter in JavaScript Using Acorn as a Parser
總結
以上是生活随笔為你收集整理的js ...运算符_「 giao-js 」用js写一个js解释器的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 直接用自己服务器做图床可以吗_图床有什么
- 下一篇: 未定义函数或变量_变量提升:JavaSc