理解Javascript執行過程

文章來源 | https://www.cnblogs.com/tugenhua0707/p/11980566.html

Javascript是一種解釋型的動態語言。

在程序中,有編譯型語言和解釋型語言。那麼什麼是編譯型語言,什麼是解釋型語言呢?

編譯型語言: 它首先將源代碼編譯成機器語言,再由機器運行機器碼(二進制)。

解釋型語言: 相對於編譯型語言而存在的,源代碼不是直接編譯爲目標代碼,而是將源代碼翻譯成中間代碼,再由解釋器對中間代碼進行解釋運行的。
比如javascript/python等都是解釋型語言(但是javascript是先編譯完成後,再進行解釋的)。

主要的編譯型語言有c++, 解釋型語言有Javascript, 和半解釋半編譯(比如java)。

一、瞭解代碼是如何運行的?

我們都知道,代碼是由CPU執行的,但是CPU不能直接執行我們的if...else這樣的代碼,它只能執行二進制指令,但是二進制對應我們的可讀性來說並不友好,比如二進制 11100000 這樣的,我們並不知道它代表的是什麼含義, 因此科學家們就發明了彙編語言。

彙編語言

什麼是彙編語言? 它解決了什麼問題?

彙編語言是二進制指令的文本形式,和二進制指令是一一對應的關係,比如加法指令 00000011 寫成彙編語言就是ADD。那麼彙編語言的作用就是將ADD這樣的還原成二進制,那麼二進制就可以直接被CPU執行。它最主要的作用就是解決二進制指令的可讀性問題。

但是彙編語言也有缺點:

1. 編寫的代碼非常難懂,不好維護。
2. 它是一種非常低的語言,它只針對特定的體系結構和處理器進行優化。
3. 開發效率低。容易出現bug,不好調試。

因此這個時候就發明了高級語言。

高級語言

爲什麼我們叫它是高級語言? 因爲它更符合我們人類的思維和閱讀習慣,因爲代碼是寫給人看的,不是寫給機器看的,只是我們的計算機能運行而已,比如我們之前寫的 if...else這樣的代碼 比我們之前的 二進制 11100000 可讀性好很多,但是我們的計算機並不能直接執行高級語言。所以我們需要把高級語言轉化爲編譯語言/機器指令,我們計算機CPU才能執行。那麼這個過程就叫做編譯。

我們的javascript是一種高級語言,因此我們的javascript也需要編譯後才能執行,但是我們前面說過,javascript也是一種解釋型語言, 那麼它和編譯型語言有什麼區別呢? 因此我們可以先從編譯說起。

瞭解編譯

上面瞭解了編譯的概念,那麼我們來了解下我們的js代碼爲什麼需要編譯? 比如同樣一份C++代碼在windows上會編譯成 .obj文件,但是在Linux上則會生成.o文件。他們兩者生成的文件是不能通用的。這是因爲可執行文件除了代碼以外還需要操作系統,API,內存,線程,進程等系統資源。但是不同的操作系統他們實現的方式也是不相同的。因此針對不同的操作系統我們需要使用編譯型語言對他們分別進行編譯等。

瞭解解釋型語言

先看下編譯型語言, 編譯型語言是代碼在 運行前 編譯器將人類可以理解的語言轉換成機器可以理解的語言。

解釋型語言: 也是將人類可以理解的語言轉換成機器可以理解的語言,但是它是在 運行時 轉換的。

最主要的區別是: 編譯型語言編寫的代碼在編譯後直接可以被CPU執行及運行的。但是解釋型語言需要在環境中安裝解釋器才能被解析。

打個比方說: 我現在要演講一篇中文文稿,但是演講現場有個外國人,他只懂英文,因此我們事先把整個文章翻譯成英文給他們聽(這就是編譯型語言),我們也可以同聲傳譯的方法一句一句邊讀邊翻譯給他們聽。(這就是解釋型語言)。

二、瞭解javascript執行過程

1、 瞭解javascript解析引擎

javascript的引擎的作用簡單的來講,就是能夠讀懂javascript代碼,並且準確地給出運行結果的程序,比如說,當我們寫 var temp = 1+1; 這樣一段代碼的時候,javascript引擎就能解析我們這段代碼,並且將temp的值變爲2。

Javascript引擎的基本原理是:它可以把JS的源代碼轉換成高效,優化的代碼,這樣就可以通過瀏覽器解析甚至可以被嵌入到應用當中。

每個javascript引擎都實現了一個版本的ECMAScript, javascript只是它的一個分支,那麼ECMAScript在不斷的發展,那麼javascript的引擎也會在不斷的改變。

爲什麼會有那麼多引擎,那是因爲他們每個都被設計到不同的web瀏覽器或者像Node.js那樣的運行環境當中。他們唯一的目的是讀取和編譯javascript代碼。

那麼常見的javascript引擎有如下:

Mozilla瀏覽器 -----> 解析引擎爲 Spidermonkey(由c語言實現的)Chrome瀏覽器 ------> 解析引擎爲 V8(它是由c++實現的)Safari瀏覽器 ------> 解析引擎爲 JavaScriptCore(c/c++)IE and Edge ------> 解析引擎爲 Chakra(c++)Node.js     ------> 解析引擎爲 V8

解析引擎是根據 ECMAScript定義的語言標準來動態執行javascript字符串的。

那麼解析引擎是如何解析JS的呢?

解析JS分爲2個階段:如下所示:

如上圖我們可知: javascript解析分爲:語法解析階段 和 運行階段,其中語法解析階段又分爲2種,分別爲: 詞法分析和語法分析。
運行階段分爲:預解析 和 運行階段。

注意:在javascript解析過程中,如果遇到錯誤,會直接跳出當前的代碼塊,直接執行下一個script代碼段,因此在同一個script內的代碼段有錯誤的話就不會執行下去。但是它不會影響下一個script內的代碼段。

1、語法解析階段

語法解析階段 包括 詞法分析 和 語法分析。

1.1. 詞法分析

詞法分析會將js代碼中的字符串分割爲有意義的代碼塊,這些代碼塊我們可以稱之爲 "詞法單元"。比如簡單的如下代碼:

var a = 1; 那麼這行代碼會被分爲以下詞法單元:var、a、=、1 那麼這些零散的詞法單元會組成一個詞法單元流進行解析。
比如上面詞義分析後結果變成如下:

[    {        "type": "Keyword",        "value": "var"    },    {        "type": "Identifier",        "value": "a"    },    {        "type": "Punctuator",        "value": "="    },    {        "type": "Numeric",        "value": "1"    }]

上面的轉換結果,我們可以使用這個在線的網址轉換(https://esprima.org/demo/parse.html)

我們可以把babel編譯器的代碼拿過來使用下,看下如何使用javascript來封裝詞法分析,僅供參考代碼如下:

<!DOCTYPE html><html><head>  <title></title>  <meta charset="utf-8">  <meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=0"></head><body>  <div id="app">  </div>  <script type="text/javascript">      function tokenizer(input) {        // 記錄當前解析到詞的位置        var current = 0;        // tokens 用來保存我們解析的token        var tokens = [];        // 利用循環進行解析        while(current < input.length) {            // 提取出當前要解析的字符            var char = input[current];
            // 處理符號: 檢查是否是符號        var PUNCTUATOR = /[`~!@#$%^&*()_\-+=<>?:"{}|,.\/;'\\[\]·~!@#¥%……&*()——\-+={}|《》?:“”【】、;‘’,。、]/im;        if (PUNCTUATOR.test(char)) {          // 創建變量用於保存匹配的符號          var punctuators = char;          // 判斷是否是箭頭函數的符號          if (char === '=' && input[current + 1] === '>') {              punctuators += input[++current];          }              current++;          // 最後把數據存入到tokens中          tokens.push({              type: 'Punctuator',              value: punctuators          });          // 進入下一次循環          continue;        }        // 下面是處理空格,如果是空格的話,則直接進入下一個循環        var WHITESPACE = /\s/;        if (WHITESPACE.test(char)) {          current++;          continue;            }        // 處理數字,檢查是否是數字        var NUMBERS = /[0-9]/;        if (NUMBERS.test(char)) {          // 創建變量,用於保存匹配的數字          var number = '';          // 循環當前的字符及下一個字符,直到不是數字爲止          while(NUMBERS.test(char)) {              number += char;              char = input[++current];          }          // 最後我們把數據更新到tokens中          tokens.push({              type: 'Numeric',              value: number          });          // 進入下一個循環          continue;            }        // 檢查是否是字符        var LETTERS = /[a-z]/i;        if (LETTERS.test(char)) {          // 創建一個臨時變量保存該字符          var value = '';          // 循環遍歷所有的字母          while(LETTERS.test(char)) {              value += char;              char = input[++current];          }          // 判斷當前的字符串是否是關鍵字          var KEYWORD = /function|var|return|let|const|if|for/;          if (KEYWORD.test(value)) {              // 標記關鍵字              tokens.push({                type: 'Keyword',                value: value                  })          } else {              // 標記變量              tokens.push({                type: 'Identifier',                value: value              })          }          // 進入下一次循環          continue;            }        // 如果我們沒有匹配上任何類型的token; 那麼就拋出一個錯誤        throw new TypeError('I dont konw what this character is:' + char);        }            // 最後我們返回詞法單元數組        return tokens;      }      var str = 'var a = 1';      console.log(tokenizer(str));</script></body></html>

效果執行打印如下所示:

可以看到,和上面的打印效果是一樣的。
代碼我們可以簡單的分析下如下:
首先代碼調用如下:

var str = 'var a = 1';console.log(tokenizer(str));

如上可以看到,該str的長度爲9,current從0開始,也就是說從第一個字符開始進行解析,判斷該字符是否爲 符號、空格、數字、字母等操作。

如果是字母的話,繼續判斷下一個字符是否是字母,依次類推,直到下一個字符不是字母的話,就獲取該值,因此獲取到的 value爲 'var';

然後會判斷該字符串是否爲關鍵字,如關鍵字: var KEYWORD = /function|var|return|let|const|if|for/;這些其中的一個,如果是的話,直接標記爲關鍵字,存入tokens數組中,如下代碼:

tokens.push({  type: 'Keyword',  value: value    });
因此 tokens = [{ type: 'Keyword', value: 'var' }];

然後繼續循環,此時 current = 3了; 因此是空格,如果是空格的話,代碼會跳過該循環,進行執行下一次循環, 因此current=4了; 因此vaule = a 了;因此就執行標記變量的代碼,如下所示:

// 標記變量tokens.push({  type: 'Identifier',  value: value});

因此tokens的值爲 = [{ type: 'Keyword', value: 'var' }, { type: 'Identifier', value: 'a' }];

繼續下一次循環 current = 5; 可知,也是一個空格,跳過該空格,繼續下一次循環,因此current = 6; 此時的value = '=';  因此會進入 檢查是否是符號 是代碼內部,因此 tokens 值變爲如下:

tokens = [  { type: 'Keyword', value: 'var' },   { type: 'Identifier', value: 'a' },  { type: 'Punctuator', value: '=' }];

同理 current = 7 也是一個空格,因此跳過循環, 此時current = 8; 字符就變爲 1;即 value = 1; 因此會進入 檢查是否是數字if語句內部,該內部也會依次循環下一個字符是否爲數字,直到不是數字爲止。因此 value = 1; 最後的tokens的值變爲:

tokens = [ 
 { type: 'Keyword', value: 'var' },   
{ type: 'Identifier', value: 'a' }, 
 { type: 'Punctuator', value: '=' }, 
  { type: 'Numeric', value: '1' }];

如上就是一個詞法分析的一個整個過程。

1.2、語法分析

語法分析在這個過程中會將詞法單元流轉換成一顆 抽象語法樹(AST)。比如 var a = 1; 的詞法單元流就會被解析成下面的AST;
我們也可以使用這個在線的網址轉換(https://esprima.org/demo/parse.html),結果變爲如下所示: 

{    "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函數代碼如下:

// 接收tokens作爲參數, 生成抽象語法樹ASTfunction parser(tokens) {  // 記錄當前解析到詞的位置  var current = 0;  // 通過遍歷來解析token節點  function walk() {      // 從token中第一項進行解析      var token = tokens[current];      // 檢查是不是數字類型      if (token.type === 'Numeric') {        // 如果是數字類型的話,把current當前的指針移動到下一個位置        current++;        // 然後返回一個新的AST節點        return {            type: 'Literal',            value: Number(token.value),            row: token.value        }          }      // 檢查是不是變量類型      if (token.type === 'Identifier') {        // 如果是,把current當前的指針移動到下一個位置        current++;        // 然後返回我們一個新的AST節點        return {            type: 'Identifier',            name: token.value        }      }      // 檢查是不是運輸符類型      if (token.type === 'Punctuator') {        // 如果是,current自增        current++;        // 判斷運算符類型,根據類型返回新的AST節點        if (/[\+\-\*/]/im.test(token.value)) {            return {              type: 'BinaryExpression',              operator: token.value            }        }        if (/\=/.test(token.value)) {            return {              type: 'AssignmentExpression',              operator: token.value                }        }          }      // 檢查是不是關鍵字      if (token.type === 'Keyword') {        var value = token.value;        // 檢查是不是定義的語句        if (value === 'var' || value === 'let' || value === 'const') {            current++;            // 獲取定義的變量            var variable = walk();            // 判斷是否是賦值符號            var equal = walk();            var rightVar;            if (equal.operator === '=') {              // 獲取所賦予的值              rightVar = walk();                } else {              // 不是賦值符號, 說明只是定義的變量              rightVar = null;              current--;                }            // 定義聲明            var declaration = {              type: 'VariableDeclarator',              id: variable, // 定義的變量              init: rightVar            };            // 定義要返回的節點            return {              type: 'VariableDeclaration',              declarations: [declaration],              kind: value            }        }          }      // 遇到一個未知類型就拋出一個錯誤      throw new TypeError(token.type);  }  // 現在,我們創建一個AST,根節點是一個類型爲 'Program' 的節點  var ast = {      type: 'Program',      body: [],      sourceType: 'script'  };  // 循環執行walk函數,把節點放入到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));

結果如下所示:

我們可以對比下,打印的結果和上面的結果是一樣的, 代碼是從babel拿過來的,簡單的理解下就好了。

轉換

我們對生成的AST樹節點需要進行處理下,比如我們使用ES6編寫的代碼,比如用到了let,const這樣的,我們需要轉換成var。
因此我們需要對AST樹節點進行轉換操作。

轉換AST的時候,我們可以添加、移動、替換及刪除AST抽象樹中的節點操作。

基本的代碼如下:

/*  爲了修改AST抽象樹,我們首先要對節點進行遍歷  @param AST語法樹  @param visitor定義轉換函數,也可以使用visitor函數進行轉換*/function traverser(ast, visitor) {  // 遍歷樹中的每個節點  function traverseArray(array, parent) {      if (typeof array.forEach === 'function') {        array.forEach(function(child) {            traverseNode(child, parent);        });          }  }  function traverseNode(node, parent) {      // 看下 vistory中有沒有對應的type處理函數      var method = visitor[node.type];      if (method) {        method(node, parent);          }      switch(node.type) {        // 從頂層的Program開始        case 'Program':            traverseArray(node.body, node);            break;        // 如下的是不需要轉換的        case 'VariableDeclaration':        case 'VariableDeclarator':        case 'AssignmentExpression':        case 'Identifier':        case 'Literal':            break;        default:            throw new TypeError(node.type)          }  }  traverseNode(ast, null)}
/*  下面是轉換器,它用於遍歷過程中轉換數據,  我們接收之前的AST樹作爲參數,最後會生成一個新的AST抽象樹*/function transformer(ast) {  // 創建新的ast抽象樹  var newAst = {      type: 'Program',      body: [],      sourceType: 'script'  };  ast._context = newAst.body;  // 我們把AST 和 vistor 作爲參數傳入進去  traverser(ast, {      VariableDeclaration: function(node, parent) {        var variableDeclaration = {            type: 'VariableDeclaration',            declarations: node.declarations,            kind: 'var'        };        // 把新的 VariableDeclaration 放入到context中        parent._context.push(variableDeclaration);           }  });  // 最後返回創建號的新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('轉換後的-------');console.log(transformer(ast));

打印結果如下所示,可以看到,ES6的語法已經被轉換了,如下所示:

代碼生成

我們會根據上面生成的AST樹來生成一個很大的字符串當中。

基本代碼如下所示:

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);  // 對於不同類型的節點分開處理\  switch (node.type) {
      // 如果是Program節點,我們會遍歷它的body屬性中的每一個節點      case 'Program':          return node.body.map(codeGenerator).join('\n');      // VariableDeclaration節點      case 'VariableDeclaration':          return node.kind + ' ' + node.declarations.map(codeGenerator);      // VariableDeclarator 節點      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整個編譯的過程。

2、 運行階段

運行階段包括 預解析 和 運行階段。

2.1、 預解析

如上我們已經編譯完成了,那麼現在我們需要對js代碼進行預解析,那麼什麼是預解析呢,它的作用是什麼呢?

預解析指的是:在js文件或script裏面的代碼在正式開始執行之前,會進行一些解析工作。

比如上在全局中尋找var關鍵字聲明的變量和通過function關鍵字聲明的函數。

找到全局變量或函數後,我們會對該進行作用域提升,但是在變量提升聲明的情況下不會賦值操作,因此它的默認值是undefined。

通過聲明提升,對於函數來講,函數可以在聲明函數體之上進行調用。變量也可以在賦值之前進行輸出,只是變量輸出的值爲undefined而已。

比如如下代碼:

var a = 1;function abc() {  console.log(a);  var a = 2;}abc();

如上代碼,我們在全局變量中定義了一個變量a = 1; 在函數abc中先打印a,然後給 var a = 2; 進行賦值,但是最後打印的結果爲undefined;那是因爲var在作用域中有提升的。上面的代碼在預解析的時候,會被解析成如下代碼:

var a = 1;function abc() {  var a;  console.log(a);  a = 2;    }abc();

預編譯需要注意如下幾個問題:

1. 預編譯首先是全局預編譯,函數體未調用時是不進行預編譯的。
2. 只有var 和 function 聲明會被提升。
3. 在所在的作用域會被提升,不會擴展到其他的作用域。
4. 預編譯後會順序執行代碼。

2.2 、運行階段

在瀏覽器環境中,javascript引擎會按照 <script>標籤代碼塊從上到下的順序加載並立即解釋執行。

本文完~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章