<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          理解Javascript執(zhí)行過(guò)程

          共 11942字,需瀏覽 24分鐘

           ·

          2020-09-23 03:39

          來(lái)源 |?https://www.cnblogs.com/tugenhua0707/p/11980566.html

          Javascript是一種解釋型的動(dòng)態(tài)語(yǔ)言。
          在程序中,有編譯型語(yǔ)言和解釋型語(yǔ)言。那么什么是編譯型語(yǔ)言,什么是解釋型語(yǔ)言呢?
          編譯型語(yǔ)言: 它首先將源代碼編譯成機(jī)器語(yǔ)言,再由機(jī)器運(yùn)行機(jī)器碼(二進(jìn)制)。
          解釋型語(yǔ)言: 相對(duì)于編譯型語(yǔ)言而存在的,源代碼不是直接編譯為目標(biāo)代碼,而是將源代碼翻譯成中間代碼,再由解釋器對(duì)中間代碼進(jìn)行解釋運(yùn)行的。
          比如javascript/python等都是解釋型語(yǔ)言(但是javascript是先編譯完成后,再進(jìn)行解釋的)。
          主要的編譯型語(yǔ)言有c++, 解釋型語(yǔ)言有Javascript, 和半解釋半編譯(比如java)。
          一. 了解代碼是如何運(yùn)行的?
          我們都知道,代碼是由CPU執(zhí)行的,但是CPU不能直接執(zhí)行我們的if...else這樣的代碼,它只能執(zhí)行二進(jìn)制指令,但是二進(jìn)制對(duì)應(yīng)我們的可讀性來(lái)說(shuō)并不友好,比如二進(jìn)制 11100000 這樣的,我們并不知道它代表的是什么含義, 因此科學(xué)家們就發(fā)明了匯編語(yǔ)言。
          匯編語(yǔ)言
          什么是匯編語(yǔ)言? 它解決了什么問(wèn)題?
          匯編語(yǔ)言是二進(jìn)制指令的文本形式,和二進(jìn)制指令是一一對(duì)應(yīng)的關(guān)系,比如加法指令 00000011 寫成匯編語(yǔ)言就是ADD。那么匯編語(yǔ)言的作用就是將ADD這樣的還原成二進(jìn)制,那么二進(jìn)制就可以直接被CPU執(zhí)行。它最主要的作用就是解決二進(jìn)制指令的可讀性問(wèn)題。
          但是匯編語(yǔ)言也有缺點(diǎn):
          1. 編寫的代碼非常難懂,不好維護(hù)。
          2. 它是一種非常低的語(yǔ)言,它只針對(duì)特定的體系結(jié)構(gòu)和處理器進(jìn)行優(yōu)化。
          3. 開(kāi)發(fā)效率低。容易出現(xiàn)bug,不好調(diào)試。
          因此這個(gè)時(shí)候就發(fā)明了高級(jí)語(yǔ)言。
          高級(jí)語(yǔ)言
          為什么我們叫它是高級(jí)語(yǔ)言? 因?yàn)樗衔覀內(nèi)祟惖乃季S和閱讀習(xí)慣,因?yàn)榇a是寫給人看的,不是寫給機(jī)器看的,只是我們的計(jì)算機(jī)能運(yùn)行而已,比如我們之前寫的 if...else這樣的代碼 比我們之前的 二進(jìn)制 11100000 可讀性好很多,但是我們的計(jì)算機(jī)并不能直接執(zhí)行高級(jí)語(yǔ)言。
          所以我們需要把高級(jí)語(yǔ)言轉(zhuǎn)化為編譯語(yǔ)言/機(jī)器指令,我們計(jì)算機(jī)CPU才能執(zhí)行。那么這個(gè)過(guò)程就叫做編譯。
          我們的javascript是一種高級(jí)語(yǔ)言,因此我們的javascript也需要編譯后才能執(zhí)行,但是我們前面說(shuō)過(guò),javascript也是一種解釋型語(yǔ)言, 那么它和編譯型語(yǔ)言有什么區(qū)別呢? 因此我們可以先從編譯說(shuō)起。
          了解編譯
          上面了解了編譯的概念,那么我們來(lái)了解下我們的js代碼為什么需要編譯? 比如同樣一份C++代碼在windows上會(huì)編譯成 .obj文件,但是在Linux上則會(huì)生成.o文件。他們兩者生成的文件是不能通用的。
          這是因?yàn)榭蓤?zhí)行文件除了代碼以外還需要操作系統(tǒng),API,內(nèi)存,線程,進(jìn)程等系統(tǒng)資源。
          但是不同的操作系統(tǒng)他們實(shí)現(xiàn)的方式也是不相同的。因此針對(duì)不同的操作系統(tǒng)我們需要使用編譯型語(yǔ)言對(duì)他們分別進(jìn)行編譯等。
          了解解釋型語(yǔ)言
          先看下編譯型語(yǔ)言, 編譯型語(yǔ)言是代碼在 運(yùn)行前 編譯器將人類可以理解的語(yǔ)言轉(zhuǎn)換成機(jī)器可以理解的語(yǔ)言。
          解釋型語(yǔ)言: 也是將人類可以理解的語(yǔ)言轉(zhuǎn)換成機(jī)器可以理解的語(yǔ)言,但是它是在 運(yùn)行時(shí) 轉(zhuǎn)換的。
          最主要的區(qū)別是: 編譯型語(yǔ)言編寫的代碼在編譯后直接可以被CPU執(zhí)行及運(yùn)行的。但是解釋型語(yǔ)言需要在環(huán)境中安裝解釋器才能被解析。
          打個(gè)比方說(shuō): 我現(xiàn)在要演講一篇中文文稿,但是演講現(xiàn)場(chǎng)有個(gè)外國(guó)人,他只懂英文,因此我們事先把整個(gè)文章翻譯成英文給他們聽(tīng)(這就是編譯型語(yǔ)言),我們也可以同聲傳譯的方法一句一句邊讀邊翻譯給他們聽(tīng)。(這就是解釋型語(yǔ)言)。
          二:了解javascript執(zhí)行過(guò)程
          1. 了解javascript解析引擎
          javascript的引擎的作用簡(jiǎn)單的來(lái)講,就是能夠讀懂javascript代碼,并且準(zhǔn)確地給出運(yùn)行結(jié)果的程序,比如說(shuō),當(dāng)我們寫 var temp = 1+1;?這樣一段代碼的時(shí)候,javascript引擎就能解析我們這段代碼,并且將temp的值變?yōu)?。
          Javascript引擎的基本原理是: 它可以把JS的源代碼轉(zhuǎn)換成高效,優(yōu)化的代碼,這樣就可以通過(guò)瀏覽器解析甚至可以被嵌入到應(yīng)用當(dāng)中。
          每個(gè)javascript引擎都實(shí)現(xiàn)了一個(gè)版本的ECMAScript, javascript只是它的一個(gè)分支,那么ECMAScript在不斷的發(fā)展,那么javascript的引擎也會(huì)在不斷的改變。
          為什么會(huì)有那么多引擎,那是因?yàn)樗麄兠總€(gè)都被設(shè)計(jì)到不同的web瀏覽器或者像Node.js那樣的運(yùn)行環(huán)境當(dāng)中。
          他們唯一的目的是讀取和編譯javascript代碼。
          那么常見(jiàn)的javascript引擎有如下:
          Mozilla瀏覽器 -----> 解析引擎為 Spidermonkey(由c語(yǔ)言實(shí)現(xiàn)的)
          Chrome瀏覽器 ------> 解析引擎為 V8(它是由c++實(shí)現(xiàn)的)
          Safari瀏覽器 ------> 解析引擎為 JavaScriptCore(c/c++)
          IE and Edge ------> 解析引擎為 Chakra(c++)
          Node.js ------> 解析引擎為 V8
          解析引擎是根據(jù) ECMAScript定義的語(yǔ)言標(biāo)準(zhǔn)來(lái)動(dòng)態(tài)執(zhí)行javascript字符串的。
          那么解析引擎是如何解析JS的呢?
          解析JS分為2個(gè)階段:如下所示:
          如上圖我們可知: javascript解析分為:語(yǔ)法解析階段 和 運(yùn)行階段,其中語(yǔ)法解析階段又分為2種,分別為:?詞法分析和語(yǔ)法分析。
          運(yùn)行階段分為:預(yù)解析 和 運(yùn)行階段。
          注意:在javascript解析過(guò)程中,如果遇到錯(cuò)誤,會(huì)直接跳出當(dāng)前的代碼塊,直接執(zhí)行下一個(gè)script代碼段,因此在同一個(gè)script內(nèi)的代碼段有錯(cuò)誤的話就不會(huì)執(zhí)行下去。但是它不會(huì)影響下一個(gè)script內(nèi)的代碼段。
          1. 語(yǔ)法解析階段
          語(yǔ)法解析階段 包括 詞法分析 和 語(yǔ)法分析。
          1.1. 詞法分析
          詞法分析會(huì)將js代碼中的字符串分割為有意義的代碼塊,這些代碼塊我們可以稱之為 "詞法單元"。比如簡(jiǎn)單的如下代碼:
          var a = 1; 那么這行代碼會(huì)被分為以下詞法單元:var、a、=、1 那么這些零散的詞法單元會(huì)組成一個(gè)詞法單元流進(jìn)行解析。
          比如上面詞義分析后結(jié)果變成如下:
          [ { "type": "Keyword", "value": "var" }, { "type": "Identifier", "value": "a" }, { "type": "Punctuator", "value": "=" }, { "type": "Numeric", "value": "1" }]
          上面的轉(zhuǎn)換結(jié)果,我們可以使用這個(gè)在線的網(wǎng)址轉(zhuǎn)換(https://esprima.org/demo/parse.html)
          我們可以把babel編譯器的代碼拿過(guò)來(lái)使用下,看下如何使用javascript來(lái)封裝詞法分析,僅供參考代碼如下:
          效果執(zhí)行打印如下所示:
          可以看到,和上面的打印效果是一樣的。
          代碼我們可以簡(jiǎn)單的分析下如下:
          首先代碼調(diào)用如下:
          var str = 'var a = 1';console.log(tokenizer(str));
          如上可以看到,該str的長(zhǎng)度為9,current從0開(kāi)始,也就是說(shuō)從第一個(gè)字符開(kāi)始進(jìn)行解析,判斷該字符是否為 符號(hào)、空格、數(shù)字、字母等操作。
          如果是字母的話,繼續(xù)判斷下一個(gè)字符是否是字母,依次類推,直到下一個(gè)字符不是字母的話,就獲取該值,因此獲取到的 value為 'var';
          然后會(huì)判斷該字符串是否為關(guān)鍵字,如關(guān)鍵字: var KEYWORD = /function|var|return|let|const|if|for/;這些其中的一個(gè),如果是的話,直接標(biāo)記為關(guān)鍵字,存入tokens數(shù)組中,如下代碼:
          tokens.push({ type: 'Keyword', value: value });
          因此?
          tokens = [{ type: 'Keyword', value: 'var' }];
          然后繼續(xù)循環(huán),此時(shí) current = 3了; 因此是空格,如果是空格的話,代碼會(huì)跳過(guò)該循環(huán),進(jìn)行執(zhí)行下一次循環(huán), 因此current=4了; 因此vaule = a 了;因此就執(zhí)行標(biāo)記變量的代碼,如下所示:
          // 標(biāo)記變量tokens.push({ type: 'Identifier', value: value});
          因此tokens的值為 = [{ type: 'Keyword', value: 'var' }, { type: 'Identifier', value: 'a' }];
          繼續(xù)下一次循環(huán) current = 5; 可知,也是一個(gè)空格,跳過(guò)該空格,繼續(xù)下一次循環(huán),因此current = 6; 此時(shí)的value = '=';??因此會(huì)進(jìn)入 檢查是否是符號(hào) 是代碼內(nèi)部,因此 tokens 值變?yōu)槿缦?
          tokens = [ { type: 'Keyword', value: 'var' }, { type: 'Identifier', value: 'a' }, { type: 'Punctuator', value: '=' }];
          同理 current = 7 也是一個(gè)空格,因此跳過(guò)循環(huán), 此時(shí)current = 8; 字符就變?yōu)?1;即 value = 1; 因此會(huì)進(jìn)入 檢查是否是數(shù)字if語(yǔ)句內(nèi)部,該內(nèi)部也會(huì)依次循環(huán)下一個(gè)字符是否為數(shù)字,直到不是數(shù)字為止。因此 value = 1; 最后的tokens的值變?yōu)?
          tokens = [ { type: 'Keyword', value: 'var' }, { type: 'Identifier', value: 'a' }, { type: 'Punctuator', value: '=' }, { type: 'Numeric', value: '1' }];
          如上就是一個(gè)詞法分析的一個(gè)整個(gè)過(guò)程。
          1.2. 語(yǔ)法分析
          語(yǔ)法分析在這個(gè)過(guò)程中會(huì)將詞法單元流轉(zhuǎn)換成一顆 抽象語(yǔ)法樹(AST)。比如 var a = 1; 的詞法單元流就會(huì)被解析成下面的AST;
          我們也可以使用這個(gè)在線的網(wǎng)址轉(zhuǎn)換(https://esprima.org/demo/parse.html),結(jié)果變?yōu)槿缦滤荆?
          { "type": "Program", "body": [ { "type": "VariableDeclaration", "declarations": [ { "type": "VariableDeclarator", "id": { "type": "Identifier", "name": "a" }, "init": { "type": "Literal", "value": 1, "raw": "1" } } ], "kind": "var" } ], "sourceType": "script"}
          基本的解析成AST函數(shù)代碼如下:
          // 接收tokens作為參數(shù), 生成抽象語(yǔ)法樹ASTfunction parser(tokens) { // 記錄當(dāng)前解析到詞的位置 var current = 0; // 通過(guò)遍歷來(lái)解析token節(jié)點(diǎn) function walk() { // 從token中第一項(xiàng)進(jìn)行解析 var token = tokens[current]; // 檢查是不是數(shù)字類型 if (token.type === 'Numeric') { // 如果是數(shù)字類型的話,把current當(dāng)前的指針移動(dòng)到下一個(gè)位置 current++; // 然后返回一個(gè)新的AST節(jié)點(diǎn) return { type: 'Literal', value: Number(token.value), row: token.value } } // 檢查是不是變量類型 if (token.type === 'Identifier') { // 如果是,把current當(dāng)前的指針移動(dòng)到下一個(gè)位置 current++; // 然后返回我們一個(gè)新的AST節(jié)點(diǎn) return { type: 'Identifier', name: token.value } } // 檢查是不是運(yùn)輸符類型 if (token.type === 'Punctuator') { // 如果是,current自增 current++; // 判斷運(yùn)算符類型,根據(jù)類型返回新的AST節(jié)點(diǎn) if (/[\+\-\*/]/im.test(token.value)) { return { type: 'BinaryExpression', operator: token.value } } if (/\=/.test(token.value)) { return { type: 'AssignmentExpression', operator: token.value } } } // 檢查是不是關(guān)鍵字 if (token.type === 'Keyword') { var value = token.value; // 檢查是不是定義的語(yǔ)句 if (value === 'var' || value === 'let' || value === 'const') { current++; // 獲取定義的變量 var variable = walk(); // 判斷是否是賦值符號(hào) var equal = walk(); var rightVar; if (equal.operator === '=') { // 獲取所賦予的值 rightVar = walk(); } else { // 不是賦值符號(hào), 說(shuō)明只是定義的變量 rightVar = null; current--; } // 定義聲明 var declaration = { type: 'VariableDeclarator', id: variable, // 定義的變量 init: rightVar }; // 定義要返回的節(jié)點(diǎn) return { type: 'VariableDeclaration', declarations: [declaration], kind: value } } } // 遇到一個(gè)未知類型就拋出一個(gè)錯(cuò)誤 throw new TypeError(token.type); } // 現(xiàn)在,我們創(chuàng)建一個(gè)AST,根節(jié)點(diǎn)是一個(gè)類型為 'Program' 的節(jié)點(diǎn) var ast = { type: 'Program', body: [], sourceType: 'script' }; // 循環(huán)執(zhí)行walk函數(shù),把節(jié)點(diǎn)放入到ast.body中 while(current < tokens.length) { ast.body.push(walk()); } // 最后返回我們的AST return ast;}var tokens = [ { type: 'Keyword', value: 'var' }, { type: 'Identifier', value: 'a' }, { type: 'Punctuator', value: '=' }, { type: 'Numeric', value: '1' }];console.log(parser(tokens));
          結(jié)果如下所示:
          我們可以對(duì)比下,打印的結(jié)果和上面的結(jié)果是一樣的, 代碼是從babel拿過(guò)來(lái)的,簡(jiǎn)單的理解下就好了。
          轉(zhuǎn)換
          我們對(duì)生成的AST樹節(jié)點(diǎn)需要進(jìn)行處理下,比如我們使用ES6編寫的代碼,比如用到了let,const這樣的,我們需要轉(zhuǎn)換成var。
          因此我們需要對(duì)AST樹節(jié)點(diǎn)進(jìn)行轉(zhuǎn)換操作。
          轉(zhuǎn)換AST的時(shí)候,我們可以添加、移動(dòng)、替換及刪除AST抽象樹中的節(jié)點(diǎn)操作。
          基本的代碼如下:
          /* 為了修改AST抽象樹,我們首先要對(duì)節(jié)點(diǎn)進(jìn)行遍歷 @param AST語(yǔ)法樹 @param visitor定義轉(zhuǎn)換函數(shù),也可以使用visitor函數(shù)進(jìn)行轉(zhuǎn)換*/function traverser(ast, visitor) { // 遍歷樹中的每個(gè)節(jié)點(diǎn) function traverseArray(array, parent) { if (typeof array.forEach === 'function') { array.forEach(function(child) { traverseNode(child, parent); }); } } function traverseNode(node, parent) { // 看下 vistory中有沒(méi)有對(duì)應(yīng)的type處理函數(shù) var method = visitor[node.type]; if (method) { method(node, parent); } switch(node.type) { // 從頂層的Program開(kāi)始 case 'Program': traverseArray(node.body, node); break; // 如下的是不需要轉(zhuǎn)換的 case 'VariableDeclaration': case 'VariableDeclarator': case 'AssignmentExpression': case 'Identifier': case 'Literal': break; default: throw new TypeError(node.type) } } traverseNode(ast, null)}
          /* 下面是轉(zhuǎn)換器,它用于遍歷過(guò)程中轉(zhuǎn)換數(shù)據(jù), 我們接收之前的AST樹作為參數(shù),最后會(huì)生成一個(gè)新的AST抽象樹*/function transformer(ast) { // 創(chuàng)建新的ast抽象樹 var newAst = { type: 'Program', body: [], sourceType: 'script' }; ast._context = newAst.body; // 我們把AST 和 vistor 作為參數(shù)傳入進(jìn)去 traverser(ast, { VariableDeclaration: function(node, parent) { var variableDeclaration = { type: 'VariableDeclaration', declarations: node.declarations, kind: 'var' }; // 把新的 VariableDeclaration 放入到context中 parent._context.push(variableDeclaration); } }); // 最后返回創(chuàng)建號(hào)的新AST return newAst;}
          var ast = {"type": "Program","body": [ { "type": "VariableDeclaration", "declarations": [ { "type": "VariableDeclarator", "id": { "type": "Identifier", "name": "a" }, "init": { "type": "Literal", "value": 1, "raw": "1" } } ], "kind": "const" }],"sourceType": "script"}console.log(ast);console.log('轉(zhuǎn)換后的-------');console.log(transformer(ast));
          打印結(jié)果如下所示,可以看到,ES6的語(yǔ)法已經(jīng)被轉(zhuǎn)換了,如下所示:
          代碼生成
          我們會(huì)根據(jù)上面生成的AST樹來(lái)生成一個(gè)很大的字符串當(dāng)中。
          基本代碼如下所示:
          var newAst = { "type": "Program", "body": [ { "type": "VariableDeclaration", "declarations": [ { "type": "VariableDeclarator", "id": { "type": "Identifier", "name": "a" }, "init": { "type": "Literal", "value": 1, "raw": "1" } } ], "kind": "var" } ], "sourceType": "script"}function codeGenerator(node) { console.log(node.type); // 對(duì)于不同類型的節(jié)點(diǎn)分開(kāi)處理\ switch (node.type) {
          // 如果是Program節(jié)點(diǎn),我們會(huì)遍歷它的body屬性中的每一個(gè)節(jié)點(diǎn) case 'Program': return node.body.map(codeGenerator).join('\n'); // VariableDeclaration節(jié)點(diǎn) case 'VariableDeclaration': return node.kind + ' ' + node.declarations.map(codeGenerator); // VariableDeclarator 節(jié)點(diǎn) case "VariableDeclarator": return codeGenerator(node.id) + ' = ' + codeGenerator(node.init);
          // 處理變量 case 'Identifier': return node.name;
          // case 'Literal': return node.value;
          default: throw new TypeError(node.type); }}console.log(codeGenerator(newAst));
          如上最后打印了 var a = 1; 如上就是js整個(gè)編譯的過(guò)程。
          2. 運(yùn)行階段
          運(yùn)行階段包括 預(yù)解析 和 運(yùn)行階段。
          2.1 預(yù)解析
          如上我們已經(jīng)編譯完成了,那么現(xiàn)在我們需要對(duì)js代碼進(jìn)行預(yù)解析,那么什么是預(yù)解析呢,它的作用是什么呢?
          預(yù)解析指的是:在js文件或script里面的代碼在正式開(kāi)始執(zhí)行之前,會(huì)進(jìn)行一些解析工作。比如上在全局中尋找var關(guān)鍵字聲明的變量和通過(guò)function關(guān)鍵字聲明的函數(shù)。
          找到全局變量或函數(shù)后,我們會(huì)對(duì)該進(jìn)行作用域提升,但是在變量提升聲明的情況下不會(huì)賦值操作,因此它的默認(rèn)值是undefined。
          通過(guò)聲明提升,對(duì)于函數(shù)來(lái)講,函數(shù)可以在聲明函數(shù)體之上進(jìn)行調(diào)用。變量也可以在賦值之前進(jìn)行輸出,只是變量輸出的值為undefined而已。
          比如如下代碼:
          var a = 1;function abc() { console.log(a); var a = 2;}abc();
          如上代碼,我們?cè)谌肿兞恐卸x了一個(gè)變量a = 1; 在函數(shù)abc中先打印a,然后給 var a = 2; 進(jìn)行賦值,但是最后打印的結(jié)果為undefined;那是因?yàn)関ar在作用域中有提升的。上面的代碼在預(yù)解析的時(shí)候,會(huì)被解析成如下代碼:
          var a = 1;function abc() { var a; console.log(a); a = 2;}abc();
          預(yù)編譯需要注意如下幾個(gè)問(wèn)題:
          1. 預(yù)編譯首先是全局預(yù)編譯,函數(shù)體未調(diào)用時(shí)是不進(jìn)行預(yù)編譯的。
          2. 只有var 和 function 聲明會(huì)被提升。
          3. 在所在的作用域會(huì)被提升,不會(huì)擴(kuò)展到其他的作用域。
          4. 預(yù)編譯后會(huì)順序執(zhí)行代碼。
          2.2 運(yùn)行階段
          在瀏覽器環(huán)境中,javascript引擎會(huì)按照
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                    <th id="afajh"><progress id="afajh"></progress></th>
                    日本色视频 | 精品国产乱码一区二区三区小黄书 | 美女自慰在线 | 91精品国产成人观看 | 色秘 乱码一区二区三区男奴-百度 |