總結javascript基礎概念系列計劃分為三個部分:作用域,事件循環,原型鏈。
主要問題:
1、javaScript代碼的編譯和執行過程,詞法作用域規則?
2、this的動態綁定方式有幾種?
3、全局和函數之外是不是還有其他的作用域?
4、為什麽代碼規範多禁止with、eval?
一、js編譯 執行
(一)、(預)編譯期
JS的執行過程分為兩個階段:編譯期(預處理)與執行期。報錯可以據此分為兩類,編譯期錯誤和運行期錯誤:
SyntaxError是解析代碼時發生的語法錯誤\
ReferenceError是引用一個不存在的變量時發生的錯誤。
RangeError是當一個值超出有效範圍時發生的錯誤。
TypeError是變量或參數不是預期類型時發生的錯誤。
JavaScript引擎,不是逐條解釋執行javaScript代碼,而是按照代碼塊一段段編譯解釋執行。
1、代碼塊
JavaScript中的代碼塊是指由<script>標簽分割的代碼段,這樣可以提高引擎的執行效率。JS是按照代碼塊來進行編譯和執行的,代碼塊間相互獨立,但變量和方法共享。
step 1. 讀入第一個代碼塊。
step 2. 做語法分析,有錯則報語法錯誤(比如括號不匹配等),並跳轉到step 5。此時的報錯類型為語法錯誤(比如括號不匹配等)、中英文等,不影響下面代碼塊的編譯執行
step 3. 對var變量和function定義做“預編譯處理”(永遠不會報錯的,因為只解析正確的聲明)。預編譯階段,變量聲明在已存在語法樹中,復制移動至變量對象中。
step 4. 執行代碼段,有錯則報錯(比如變量未定義引用錯誤、變量類型錯誤)。
step 5. 如果還有下一個代碼段,則讀入下一個代碼段,重復step2。
step 6. 結束。
(二)、執行過程
作用域:一套變量,數據查詢的規則。
JavaScript語法采用的是詞法作用域(lexcical scope),也就是說JavaScript的變量和函數作用域是在寫代碼定義時決定的,所以 JavaScript解釋器只需要通過靜態分析就能確定每個變量、函數的作用域,這種作用域也稱為靜態作用域(static scope)。
執行環境、變量對象、內部變量表、函數表等概念都很抽象。可以用一些代碼試著解釋一下執行過程。
<script> hi = ‘hello‘; var num = 1; function fn(a){ console.log(this.num); console.log(hi); console.log(a); console.log(arguments[0]); } var fn2 = function(){ var b = 2; console.log(‘fn2‘); return function(){ console.log(b); } } fn(2); var fn3 = fn2(); fn3(); </script>
執行過程如下:
<script> //解釋型語言,以代碼塊為單位進行翻譯、執行 // ##step 1: 語法報錯 報錯類型 // debugger; //GEC = { //全局執行環境 // ##step 2: vo變量對象與ao函數內的活動對象 // ##step 2.1: 變量提升,函數聲明優先,兩種命名方式差別 // vo:{ // fn:{ // type:function, // ao:{ // arguments:[], // a:undefined, // this:undefined // //##step 3.1: this 指向的動態綁定 幾種使用形式 // //##step 3.2: 形參與arguments間聯動 // }, // scopeChain:[GEC.vo] // }, // num:undefined, // fn2:undefined, // fn3:undefined, // this:window // }, // scopeChain:[GEC.vo], // ##step 3: 作用域鏈,包含各級變量對象指針的鏈表 // callStack : [GEC] // ##step 4: 調用棧,作用控制代碼執行流 //} hi = ‘hello‘; //順著作用域鏈查找變量對象上是否已有hi,有則賦值 ,沒有且在非嚴格模式下會聲明一個最外層變量 var num = 1; //num 賦值 function fn(a){ console.log(this.num); console.log(hi); console.log(a); console.log(arguments[0]); } //創建函數並賦值 var fn2 = function(){ var b = 2; console.log(‘fn2‘); return function(){ console.log(b); } } // GEC = { // 此時的全局執行環境 // vo:{ // fn:{ // type:function, // ao:{ // arguments:[], // a:undefined, // this:undefined // }, // scopeChain:[GEC.vo] // }, // num:1, // fn2:{ // type:function, // ao:{ // arguments:[], // b:undefined, // this:undefined // }, // scopeChain:[GEC.vo] // } // hi:‘hello‘ // fn3:undefined, // this:window // }, // scopeChain:[GEC.vo], // callStack : [GEC] //} fn(2); // FNEC = { // vo:{ // arguments:[2], // a:2, // this:window // }, // scopeChain:[GEC.vo,FNEC.vo], // callStack : [GEC,FNEC] // } // 函數的執行環境執行結束後銷毀 var fn3 = fn2(); // FN2EC = { // vo:{ // arguments:[], // b:2, // ##step 5: 匿名函數創建,調用的全局性,賦值則閉包形成 // anonymous:{ // type:function, // ao:{ // arguments:[], // this:undefined // }, // scopeChain:[GEC.vo,FN2EC.vo] // }, // this:window // }, // scopeChain:[GEC.vo,FN2EC.vo], // callStack : [GEC,FN2EC] // } // 執行後返回一個匿名函數,回到全局環境 賦值給fn3 ,FN2EC.vo留在內存中。生成閉包占用內存,易造成內存泄漏 // GEC = { //全局執行環境 // vo:{ // fn:{ // type:function, // ao:{ // arguments:[], // a:undefined, // this:undefined // }, // scopeChain:[GEC.vo] // }, // num:1, // fn2:{ // type:function, // ao:{ // arguments:[], // b:undefined, // this:undefined // }, // scopeChain:[GEC.vo] // } // hi:‘hello‘ // fn3:{ // type:function, // ao:{ // arguments:[], // this:undefined // } // scopeChain:[GEC.vo,FN2EC.vo] // } // this:window // }, // scopeChain:[GEC.vo], // callStack : [GEC] // } // FN3EC = { // vo:{ // arguments:[], // this:window // }, // scopeChain:[GEC.vo,FN2EC.vo,FN3EC.vo], // callStack : [GEC,FN3EC] // } </script>
(三)、作用域欺騙
欺騙詞法作用域:with,eval
with 會在作用域鏈前增加一個對象,會從對象屬性中查找,修改賦值。但無法新增屬性;對於查找不到的賦值會向外層查詢。
eval();
可以傳入執行字符串代碼,就像本就在那個位置。
兩個最大的問題是會影響引擎的代碼優化,性能下降。
塊級作用域
全局和函數作用域之外,存在另外的作用域
Catch 捕獲的變量只在內部有意義
<script> // ES6代碼: { let a = 2; console.log(a); }; console.log(a); // 轉為ES5代碼: try{ throw undefined; }catch(a){ a = 2; console.log(a); } console.log(a); </script>
總結javascript基礎概念系列計劃分為三個部分:作用域,事件循環,原型鏈。