摘要:手摸手教你用寫(xiě)一個(gè)解釋器用來(lái)編譯看起來(lái)是個(gè)高大上的東西,實(shí)際原理其實(shí)很簡(jiǎn)單,無(wú)非就是利用對(duì)象屬性可以用字符串表示這個(gè)特性來(lái)實(shí)現(xiàn)的黑魔法罷了。
手摸手教你用 js 寫(xiě)一個(gè) js 解釋器
用 js 來(lái) 編譯 js 看起來(lái)是個(gè)高大上的東西,實(shí)際原理其實(shí)很簡(jiǎn)單,無(wú)非就是利用 js 對(duì)象屬性可以用字符串表示 這個(gè)特性來(lái)實(shí)現(xiàn)的黑魔法罷了。
之所以看起來(lái)那么 深?yuàn)W, 大概是由于網(wǎng)上現(xiàn)有的教程,都是動(dòng)不動(dòng)就先來(lái)個(gè) babylon / @babel/parser 先讓大家看個(gè)一大串的 AST, 然后再貼出一大串的代碼,
直接遞歸 AST 處理所有類型的節(jié)點(diǎn). 最后成功的把我這樣的新手就被嚇跑了。
那么今天我寫(xiě)這篇的目的,就是給大家一個(gè)淺顯易懂,連剛學(xué) js 的人都能看懂的 js2js 教程。
先來(lái)看一下效果
一個(gè)最簡(jiǎn)單的解釋器上面有提到,js 有個(gè)特性是 對(duì)象屬性可以用字符串表示,如 console.log 等價(jià)于 console["log"], 辣么根據(jù)這個(gè)特性,我們可以寫(xiě)出一個(gè)兼容性極差,極其簡(jiǎn)陋的雛形
function callFunction(fun, arg) { this[fun](arg); } callFunction("alert", "hello world"); // 如果你是在瀏覽器環(huán)境的話,應(yīng)該會(huì)彈出一個(gè)彈窗
既然是簡(jiǎn)易版的,肯定是問(wèn)題一大堆,js 里面得語(yǔ)法不僅僅是函數(shù)調(diào)用,我們看看賦值是如何用黑魔法實(shí)現(xiàn)的
function declareVarible(key, value) { this[key] = value; } declareVarible.call(window, "foo", "bar"); // window.foo = "bar"
Tips: const 可以利用 Object.defineProperty 實(shí)現(xiàn);
如果上面的代碼能看懂,說(shuō)明你已經(jīng)懂得了 js 解釋器 的基本原理了,看不懂那只好怪我咯。
稍微加強(qiáng)一下可以看出,上面為了方便, 我們把函數(shù)調(diào)用寫(xiě)成了 callFunction("alert", "hello world"); 但是著看起來(lái)一點(diǎn)都不像是 js 解釋器,
我們心里想要的解釋器至少應(yīng)該是長(zhǎng)這樣的 parse("alert("hello world")""), 那么我們來(lái)稍微改造一下, 在這里我們要引入 babel 了,
不過(guò)先不用擔(dān)心, 我們解析出來(lái)的語(yǔ)法樹(shù)(AST)也是很簡(jiǎn)單的。
import babelParser from "@babel/parser"; const code = "alert("hello world!")"; const ast = babelParser.parse(code);
以上代碼, 解析出如下內(nèi)容
{ "type": "Program", "start": 0, "end": 21, "body": [ { "type": "ExpressionStatement", "start": 0, "end": 21, "expression": { "type": "CallExpression", "start": 0, "end": 21, "callee": { "type": "Identifier", "start": 0, "end": 5, "name": "alert" }, "arguments": [ { "type": "Literal", "start": 6, "end": 20, "value": "hello world!", "raw": ""hello world!"" } ] } } ], "sourceType": "module" }
上面的內(nèi)容看起來(lái)很多,但是我們實(shí)際有用到到其實(shí)只是很小的一部分, 來(lái)稍微簡(jiǎn)化一下, 把暫時(shí)用不到的字段先去掉
{ "type": "Program", "body": [ { "type": "ExpressionStatement", "expression": { "type": "CallExpression", "callee": { "type": "Identifier", "name": "alert" }, "arguments": [ { "type": "Literal", "value": "hello world!", } ] } } ], }
我們先大概瀏覽一遍 AST 里面的所有屬性名為 type 的數(shù)據(jù)
ExpressionStatement
CallExpression
Identifier
Literal
一共有 4 種類型, 那么接下來(lái)我們把這 4 種節(jié)點(diǎn)分別解析, 從最簡(jiǎn)單的開(kāi)始
Literal{ "type": "Literal", "value": "hello world!", }
針對(duì) Literal 的內(nèi)容, 我們需要的只有一個(gè) value 屬性, 直接返回即可.
if(node.type === "Literal") { return node.value; }
是不是很簡(jiǎn)單?
Identifier{ "type": "Identifier", "name": "alert" },
Identifier 同樣也很簡(jiǎn)單, 它代表的就是我們已經(jīng)存在的一個(gè)變量, 變量名是node.name, 既然是已經(jīng)存在的變量, 那么它的值是什么呢?
if(node.type === "Identifier") { return { name: node.name, value:this[node.name] }; }
上面的 alert 我們從 node.name 里面拿到的是一個(gè)字符, 通過(guò) this["xxxxx"] 可以訪問(wèn)到當(dāng)前作用域(這里是 window)里面的這個(gè)標(biāo)識(shí)符(Identifier)
ExpressionStatement{ "type": "ExpressionStatement", "expression": {...} }
這個(gè)其實(shí)也是超簡(jiǎn)單, 沒(méi)有什么實(shí)質(zhì)性的內(nèi)容, 真正的內(nèi)容都在 expression 屬性里,所以可以直接返回 expression 的內(nèi)容
if(node.type === "ExpressionStatement") { return parseAstNode(node.expression); }CallExpression
CallExpression 按字面的意思理解就是 函數(shù)調(diào)用表達(dá)式,這個(gè)稍微麻煩一點(diǎn)點(diǎn)
{ "type": "CallExpression", "callee": {...}, "arguments": [...] }
CallExpression 里面的有 2 個(gè)我們需要的字段:
callee 是 函數(shù)的引用, 里面的內(nèi)容是一個(gè) Identifier, 可以用上面的方法處理.
arguments 里面的內(nèi)容是調(diào)用時(shí)傳的參數(shù)數(shù)組, 我們目前需要處理的是一個(gè) Literal, 同樣上面已經(jīng)有處理方法了.
說(shuō)到這里,相信你已經(jīng)知道怎么做了
if(node.type === "CallExpression") { // 函數(shù) const callee = 調(diào)用 Identifier 處理器 // 參數(shù) const args = node.arguments.map(arg => { return 調(diào)用 Literal 處理器 }); callee(...args); }代碼
這里有一份簡(jiǎn)單的實(shí)現(xiàn), 可以跑通上面的流程, 但也僅僅可以跑通上面而已, 其他的特性都還沒(méi)實(shí)現(xiàn)。
https://github.com/noahlam/pr...
其他實(shí)現(xiàn)方式除了上面我介紹得這種最繁瑣得方式外,其實(shí) js 還有好幾種可以直接執(zhí)行字符串代碼得方式
插入 script DOM
const script = document.createElement("script"); script.innerText = "alert("hello world!")"; document.body.appendChild(script);
eval
eval("alert("hello world!")")
new Function
new Function("alert("hello world")")();
setTimeout 家族
setTimeout("console.log("hello world")");
不過(guò)這些在小程序里面都被無(wú)情得封殺了...
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/103751.html
摘要:最近項(xiàng)目中遇到一個(gè)需求,需要把一張圖片加上平鋪的水印類似這樣的效果首先想到的是用完成這種功能,因?yàn)槲抑耙矝](méi)有接觸過(guò),所以做這個(gè)功能的時(shí)候,就是一步一步的摸索中學(xué)習(xí),過(guò)程還是挺的,接下來(lái)跟我一步步來(lái)實(shí)現(xiàn)這個(gè)功能以及發(fā)現(xiàn)一些的坑吧。 最近項(xiàng)目中遇到一個(gè)需求,需要把一張圖片加上平鋪的水印 類似這樣的效果showImg(https://segmentfault.com/img/remote/...
摘要:靈活性和針對(duì)性。所以我覺(jué)得大部分組件還是自己封裝來(lái)的更為方便和靈活一些。動(dòng)手開(kāi)干接下來(lái)我們一起手摸手教改造包裝一個(gè)插件,只要幾分鐘就可以封裝一個(gè)專屬于你的。 項(xiàng)目地址:vue-countTo配套完整后臺(tái)demo地址:vue-element-admin系類文章一:手摸手,帶你用vue擼后臺(tái) 系列一(基礎(chǔ)篇)系類文章二:手摸手,帶你用vue擼后臺(tái) 系列二(登錄權(quán)限篇)系類文章三:手摸手,帶...
摘要:只有動(dòng)手,你才能真的理解作者的構(gòu)思的巧妙只有動(dòng)手,你才能真正掌握一門技術(shù)持續(xù)更新中項(xiàng)目地址求求求源碼系列跟一起學(xué)如何寫(xiě)函數(shù)庫(kù)中高級(jí)前端面試手寫(xiě)代碼無(wú)敵秘籍如何用不到行代碼寫(xiě)一款屬于自己的類庫(kù)原理講解實(shí)現(xiàn)一個(gè)對(duì)象遵循規(guī)范實(shí)戰(zhàn)手摸手,帶你用擼 Do it yourself!!! 只有動(dòng)手,你才能真的理解作者的構(gòu)思的巧妙 只有動(dòng)手,你才能真正掌握一門技術(shù) 持續(xù)更新中…… 項(xiàng)目地址 https...
閱讀 1969·2021-11-24 09:39
閱讀 3357·2021-09-22 14:58
閱讀 1198·2019-08-30 15:54
閱讀 3349·2019-08-29 11:33
閱讀 1817·2019-08-26 13:54
閱讀 1628·2019-08-26 13:35
閱讀 2497·2019-08-23 18:14
閱讀 802·2019-08-23 17:04