日本综合一区二区|亚洲中文天堂综合|日韩欧美自拍一区|男女精品天堂一区|欧美自拍第6页亚洲成人精品一区|亚洲黄色天堂一区二区成人|超碰91偷拍第一页|日韩av夜夜嗨中文字幕|久久蜜综合视频官网|精美人妻一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務(wù)時(shí)間:8:30-17:00
你可能遇到了下面的問題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營(yíng)銷解決方案
手寫JS引擎來解釋一道賦值面試題

有這樣一道面試題:

城口網(wǎng)站制作公司哪家好,找創(chuàng)新互聯(lián)!從網(wǎng)頁(yè)設(shè)計(jì)、網(wǎng)站建設(shè)、微信開發(fā)、APP開發(fā)、響應(yīng)式網(wǎng)站建設(shè)等網(wǎng)站項(xiàng)目制作,到程序開發(fā),運(yùn)營(yíng)維護(hù)。創(chuàng)新互聯(lián)于2013年創(chuàng)立到現(xiàn)在10年的時(shí)間,我們擁有了豐富的建站經(jīng)驗(yàn)和運(yùn)維經(jīng)驗(yàn),來保證我們的工作的順利進(jìn)行。專注于網(wǎng)站建設(shè)就選創(chuàng)新互聯(lián)。

let a = { n: 1};

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

console.log(a.x);

問輸出的是啥。

這道題輸出的是 undefined,因?yàn)橘x值是從左往右進(jìn)行的,也就是先把 {n: 2} 賦值給 a.x 再賦值給 a。

再加個(gè)變量 preA 引用賦值之前的 a 就能看出來:

這就是運(yùn)算符優(yōu)先級(jí)的問題,就像加減乘除運(yùn)算符也是從左往右按照優(yōu)先級(jí)來算一樣。

寫這篇文章不是為了講運(yùn)算符優(yōu)先級(jí)問題,而是想自己實(shí)現(xiàn)一個(gè) JS 引擎來解釋執(zhí)行這段代碼。

怎么實(shí)現(xiàn) JS 引擎呢?

JS 引擎的構(gòu)成

一個(gè) JS 引擎由四部分構(gòu)成:Parser(解析器)、Interperter(解釋器)、JIT Compiler(JIT 編譯器)、Garbage Collector(垃圾收集器)。

源碼經(jīng)過 Parser 解析成 AST,也就是計(jì)算機(jī)能處理的對(duì)象樹的結(jié)構(gòu),然后用解釋器遞歸的解釋每個(gè)節(jié)點(diǎn),這就是解釋執(zhí)行的過程。

但是解釋執(zhí)行比較慢,為了優(yōu)化速度又引入了 JIT 編譯器,會(huì)把經(jīng)常執(zhí)行的熱點(diǎn)代碼編譯成機(jī)器碼直接執(zhí)行。

同時(shí),內(nèi)存是有限制的,要不斷的循環(huán)清除不再被使用的內(nèi)存,也就是垃圾回收,這是 GC 做的事情。

如果我們自己實(shí)現(xiàn)簡(jiǎn)單的 JS 引擎,那可以省略掉 JIT 編譯器和 GC,這兩個(gè)分別是優(yōu)化時(shí)間和空間的,不是必須的。

也就是只要實(shí)現(xiàn) Parser 和 Interpreter(解釋器)就行:

這就是我們要實(shí)現(xiàn)的 JS 引擎的結(jié)構(gòu)。

簡(jiǎn)易 JS 引擎實(shí)現(xiàn)思路

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

它產(chǎn)生的 AST 可以用 astexplorer.net 可視化的查看:

怎么解釋執(zhí)行呢?

解釋 AST 也就是遞歸解釋每個(gè) AST 節(jié)點(diǎn),那具體的 AST 節(jié)點(diǎn)又怎么解釋呢?

比如這個(gè) {n:1} 的對(duì)象表達(dá)式,它對(duì)應(yīng)的是 ObjectExpression 節(jié)點(diǎn),它有 ObjectProperty 屬性的子節(jié)點(diǎn),子節(jié)點(diǎn)有 key、value 屬性。

自然可以想到,解釋 ObjectExpression 節(jié)點(diǎn)就是取出 AST 中的數(shù)據(jù)構(gòu)造一個(gè)對(duì)象返回:

再比如 let a = { n: 1} 這條賦值語(yǔ)句,它對(duì)應(yīng)的是 VariableDeclaration 節(jié)點(diǎn),下面有多個(gè) VariableDeclarator 子節(jié)點(diǎn),這是因?yàn)橐粭l聲明語(yǔ)句可以聲明多個(gè)變量,比如 let a = 1, b =1; 而具體的聲明 VariableDeclarator 里分別有 id 和 init 部分。

init 部分就是 ObjectExpression,解釋它就是構(gòu)造一個(gè)對(duì)象返回。那么解釋整個(gè)聲明自然就是在作用域中放一個(gè)名字為 id 節(jié)點(diǎn)的 value 為名字的變量,值就是 init 節(jié)點(diǎn)的解釋執(zhí)行的結(jié)果。

Identifier 是標(biāo)識(shí)符的意思,也就是這里的 a。

對(duì)象表達(dá)式 ObjectExpression 和聲明語(yǔ)句 VariableDeclaration 的解釋執(zhí)行就是這樣,不難吧。

其他 AST 節(jié)點(diǎn)的解釋也是這樣,遞歸的解釋每個(gè)節(jié)點(diǎn),這就是解釋執(zhí)行 JS 代碼的實(shí)現(xiàn)原理。

知道了該怎么做,那我們來寫代碼實(shí)現(xiàn)下吧:

聲明語(yǔ)句的解釋執(zhí)行

我們先實(shí)現(xiàn)聲明語(yǔ)句的解釋執(zhí)行,后面再實(shí)現(xiàn)賦值語(yǔ)句的解釋執(zhí)行:

Parser 使用 babel parser,用它的 parse 方法把源碼轉(zhuǎn)成 AST,然后解釋執(zhí)行 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);

我們聲明了一個(gè) Map 對(duì)象作為全局作用域。

接下來就是實(shí)現(xiàn)解釋器了,也就是 evaluate 方法。

我們前面分析過了,解釋執(zhí)行就是遞歸處理每個(gè) 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('不支持的節(jié)點(diǎn)類型:', e);
}
}
我們實(shí)現(xiàn)了幾個(gè)簡(jiǎn)單的 AST 的解釋

我們實(shí)現(xiàn)了幾個(gè)簡(jiǎn)單的 AST 的解釋:Program 根節(jié)點(diǎn),它的解釋執(zhí)行就是解釋執(zhí)行 body 的每一條語(yǔ)句。Identifier(標(biāo)識(shí)符) 就是取 name 屬性,NumericLiteral(數(shù)字字面量)就是取 value 屬性。

然后是對(duì)象表達(dá)式 ObjectExpression 的解釋了,這個(gè)就是構(gòu)造一個(gè)對(duì)象返回:

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

也就是取 properties 中的每個(gè)節(jié)點(diǎn),拿到 key 和 value 的解釋執(zhí)行結(jié)果,設(shè)置到對(duì)象中,最后把對(duì)象返回。

聲明語(yǔ)句的解釋就是在作用域(我們聲明的 Map)中設(shè)置下就行:

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 是聲明語(yǔ)句,因?yàn)榫唧w的聲明可能有多個(gè),所以要循環(huán)執(zhí)行每個(gè)聲明。

具體的聲明 VariableDeclarator 就是在 scope 中設(shè)置變量名和它的值。

變量名是 node.id 的執(zhí)行結(jié)果,如果聲明過就報(bào)錯(cuò),因?yàn)橹荒苈暶饕淮巍?/p>

否則就取 node.init 的值設(shè)置到 scope 中,也就是 scope.set(declarationName, value)。

但是值的處理要注意下,如果是 Identifier 也就是標(biāo)識(shí)符,它其實(shí)是一個(gè)引用,比如變量 a,那么我們要先從作用域中拿到它的具體值。

這些節(jié)點(diǎn)的解釋執(zhí)行邏輯寫好了,那么我們就能解釋這段代碼了:

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

聲明了 a、b、c 三個(gè)變量,a、b 初始值都是對(duì)象字面量、c 是引用自 b。

執(zhí)行之后我們打印下 scope:

執(zhí)行成功!我們實(shí)現(xiàn)了最簡(jiǎn)單的 JS 引擎!

當(dāng)然,只是聲明還不夠,接下來再實(shí)現(xiàn)賦值語(yǔ)句的解釋:

賦值語(yǔ)句的解釋執(zhí)行

賦值語(yǔ)句的解釋也就是解釋 AssignmentExpression 節(jié)點(diǎn),用 astexplorer.net 看下它的結(jié)構(gòu):

它外面怎么還包裹了個(gè) ExpressionStatement 節(jié)點(diǎn)呢?

因?yàn)楸磉_(dá)式不能直接執(zhí)行,語(yǔ)句才是執(zhí)行的基本單位,那么表達(dá)式包裹一層表達(dá)式語(yǔ)句(ExpressionStatement)就可以了。

AssignmentExpression 有 left 和 right 屬性,分別是 = 左右部分對(duì)應(yīng)的節(jié)點(diǎn)。

如果 right 還是 AssignmentExpression 呢?

那么要繼續(xù)取 right,知道拿到不是 AssignmentExpression 的節(jié)點(diǎn),這就是要賦值的值。

而它左邊的所有的節(jié)點(diǎn)都是賦值的目標(biāo),從左到右依次賦值即可。

所以,AssignmentExpression 節(jié)點(diǎn)的解釋執(zhí)行是這樣的:

我們不是要順著 right 往右找么,那就聲明 curNode 代表當(dāng)前節(jié)點(diǎn),然后 while 循環(huán)往右找,過程中的所有 left 都放到一個(gè)數(shù)組里:

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

最后一個(gè) right 就是賦值的值的 AST,解釋執(zhí)行之后拿到它的值。

const value = evaluate(curNode.right);

然后把 value 賦值給 targetArr 中的所有變量就行了,也就是從左往右依次賦值:

這里要區(qū)分下 a 和 a.x 的賦值:

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

如果是 a.x,也就是 MemberExpression,那么要從作用域中拿到對(duì)象的部分也就是 scope.get(objName) 的部分,然后再設(shè)置屬性。

也就是:

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;
}
})

賦值語(yǔ)句的解釋執(zhí)行的完整代碼如下:

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;
}
})
}

實(shí)現(xiàn)了聲明語(yǔ)句、賦值語(yǔ)句的解釋其實(shí)就達(dá)到我們的目標(biāo)了。我們執(zhí)行下開頭代碼:

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

看下 scope 中的值:

為啥 a.x 是 undefined 不就解釋清楚了么。

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

總結(jié)

我們做了一道賦值語(yǔ)句的面試題,它考察的就是運(yùn)算符從左到右執(zhí)行。

但是只是知道賦值運(yùn)算符怎么執(zhí)行的還不夠,我們自己寫了一個(gè) JS 引擎來執(zhí)行它。

JS 引擎由 Parser、Interpreter(解釋器)、JIT Compiler、Garbage Collector 四部分構(gòu)成。其中 JIT 編譯器和 GC 分別是優(yōu)化時(shí)間和空間用的,不是必須的。所以,我們實(shí)現(xiàn)了只有 Parser 和 Interpreter 的 JS 引擎。

Parser 使用任何 JS parser 都行,我們使用了 babel parser,解釋器的實(shí)現(xiàn)就是遞歸解釋每個(gè)節(jié)點(diǎn),我們分別實(shí)現(xiàn)了聲明語(yǔ)句和賦值語(yǔ)句的解釋執(zhí)行。

最終,我們得到了最開始的結(jié)果,并且還清楚的知道了賦值語(yǔ)句是怎么解釋執(zhí)行的。

(當(dāng)然,因?yàn)橹恍枰忉屵@幾行代碼,解釋器并不完善,更完善的解釋器可以看 《Babel 插件通關(guān)秘籍》小冊(cè)中的 JS 解釋器的案例。)


標(biāo)題名稱:手寫JS引擎來解釋一道賦值面試題
文章鏈接:http://www.dlmjj.cn/article/cdgghjp.html