用js 來編譯js
看起來是個高大上的東西,實際原理其實很簡單,無非就是利用js 物件屬性可以用字串表示
這個特性來實現的黑魔法罷了。
之所以看起來那麼深奧
, 大概是由於網上現有的教程,都是動不動就先來個babylon / @babel/parser
先讓大家看個一大串的AST
, 然後再貼出一大串的程式碼,
直接遞歸AST 處理所有類型的節點. 最後新手就成功被嚇跑了。
那麼今天我寫這篇的目的,就是給大家一個淺顯易懂,連剛學 js 的人都能看懂的 js2js 教學。
先來看看效果
上面有提到,js 有個特性是物件屬性可以用字串表示
,如console.log 等價於console['log'], 辣麼根據這個特性,我們可以寫出一個兼容性極差,極其簡陋的雛形
function callFunction(fun, arg) { this[fun](arg); } callFunction('alert', 'hello world'); // 如果你是在浏览器环境的话,应该会弹出一个弹窗
既然是簡易版的,肯定是問題一大堆,js 裡面得語法不僅僅是函數調用,我們看看賦值是如何用黑魔法實現的
function declareVarible(key, value) { this[key] = value; } declareVarible.call(window, 'foo', 'bar'); // window.foo = 'bar'
Tips: const可以利用Object.defineProperty 實作;
如果上面的程式碼能看懂,說明你已經懂得了js 解釋器
的基本原理了,看不懂那隻好怪我咯。
可以看出,上面為了方便, 我們把函數呼叫寫成callFunction('alert', 'hello world');
但是著看起來一點都不像是js 解釋器
,
我們心裡想要的解釋器至少應該是長這樣的parse('alert("hello world")'')
, 那我們來稍微改造一下, 在這裡我們要引入babel 了,
不過先不用擔心, 我們解析出來的語法樹(AST)也是很簡單的。
import babelParser from '@babel/parser'; const code = 'alert("hello world!")'; const ast = babelParser.parse(code);
以上程式碼, 解析出如下內容
{ "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" }
上面的內容看起來很多,但是我們實際有用到到其實只是很小的一部分, 來稍微簡化一下, 把暫時用不到的欄位先去掉
{ "type": "Program", "body": [ { "type": "ExpressionStatement", "expression": { "type": "CallExpression", "callee": { "type": "Identifier", "name": "alert" }, "arguments": [ { "type": "Literal", "value": "hello world!", } ] } } ], }
我們大概先瀏覽一次AST 裡面的所有屬性都名為type
的資料
{ "type": "Literal", "value": "hello world!", }
if(node.type === 'Literal') { return node.value; }是不是很簡單? Identifier
{ "type": "Identifier", "name": "alert" },
if(node.type === 'Identifier') { return { name: node.name, value:this[node.name] }; }上面的
alert 我們從
node.name 裡面拿到的是一個字元, 透過
this['xxxxx'] 可以訪問到目前作用域(這裡是window)裡面的這個標識符(Identifier)
{ "type": "ExpressionStatement", "expression": {...} }
expression 屬性裡,所以可以直接回傳expression 的內容
if(node.type === 'ExpressionStatement') { return parseAstNode(node.expression); }CallExpressionCallExpression 按字面的意思理解就是函數呼叫表達式,這個稍微麻煩一點點
{ "type": "CallExpression", "callee": {...}, "arguments": [...] }CallExpression 裡面的有2 個我們需要的欄位:
if(node.type === 'CallExpression') { // 函数 const callee = 调用 Identifier 处理器 // 参数 const args = node.arguments.map(arg => { return 调用 Literal 处理器 }); callee(...args); }程式碼這裡有一份簡單的實作, 可以跑通上面的流程, 但也僅僅可以跑通上面而已, 其他的特性都還沒實現。 https://github.com/noahlam/pr...其他實作方式除了上面我介紹得這種最繁瑣得方式外,其實js還有好幾種可以直接執行字串程式碼得方式
const script = document.createElement("script"); script.innerText = 'alert("hello world!")'; document.body.appendChild(script);
eval('alert("hello world!")')
new Function('alert("hello world")')();
setTimeout('console.log("hello world")');
最後,給大家推薦一個
前端學習進階內推交流群685910553(前端資料分享),不管你在地球哪個方位,不管你參加工作幾年都歡迎你的入駐! (群組內會定期免費提供一些群主收藏的免費學習書籍資料以及整理好的面試題和答案文檔!)
願大家都能在程式設計這條路,越走越遠。
推薦教學:《JS教學》
以上是用JavaScript寫一個js解釋器的詳細內容。更多資訊請關注PHP中文網其他相關文章!