JS解析器執行原理和聲明提升機制

JS解析器執行原理和聲明提升機制

一.概念

瀏覽器中有一套專門解析JS代碼的程序,這個程序稱爲JS的解析器

瀏覽器運行整個頁面文檔時,遇到< script > 標籤時JS解析器開始解析JS代碼

二.JS解析器的工作步驟

1.預解析代碼

主要找一些關鍵字如 var,function,以及函數的參數等,並存儲進倉庫裏面,也就是內存

先掃描全局的代碼,在函數執行的時候,然後掃描局部的,也就是函數內部的

  • 變量的初始值是undefind

    //聲明一個a變量,放進倉庫scope
    console.log(a);//undefined
    var a = 1;
    
  • 函數的初始值就是該函數的代碼塊(而不是undefined)

    console.log(test);//打印函數代碼塊
    //聲明一個函數,放進倉庫scope
    function test() {...}
    
    
    //控制檯打印函數代碼塊
    ƒ test() {
        return 1;
    }
    
  • 當變量和函數重名時,不管順序誰前誰後,只留下函數的值

    console.log(test);//打印函數代碼塊
    function test() {
        return 1;
    }
    var test=2;
    
    //控制檯打印函數代碼塊
    ƒ test() {
        return 1;
    }
    

    注意:這裏是預解析的時候,函數是一等公民,比變量優先級高

  • 當函數和函數重名的時候,只會留下後面的那個函數,會遵從上下文機制

    console.log(test);
    function test() {
        return 1;
    }
    
    //重名時,預解析只會留下後面這個函數
    function test() {
        return 234;
    }
    var test=2;
    
    //控制檯打印函數代碼塊
    ƒ test() {
        return 234;
    }
    

2.逐行執行代碼

當預解析完成之後,就開始執行代碼,倉庫中變量的值,隨時都可能發生變化

1. alert(a);// function a(){alert(3);}
2. var a = 1;
3. alert(a);//1
4. function a() { alert(2); }
5. alert(a);//1
6. var a = 3;
7. alert(a);//3
8. function a() { alert(3); }
9. alert(a);//3

/*
解讀代碼
  預解析過程:	
  		第2行:找到了一個var 關鍵字,聲明一個變量a
  		第4行:找到了一個function關鍵字 把a變成了一個函數
  		第6行:找到了一個var關鍵字,但是由於聲明變量名字與函數重名,所以不起作用,a的值還是一個函數,因爲
  			 函數比變量的優先級高
  	    第8行:找到了一個function關鍵字,a還是一個函數,但是把第4行的給覆蓋了,因爲函數重名,會遵從上下
  	         文機制
		
	    最後預解析得到的結果是:
        a => fn  -> function a(){alert(3);}
        
  原因:  1.當變量和函數重名時,不管順序誰前誰後,只留下函數的值
  	    2.當函數和函數重名的時候,只會留下後面的那個函數,會遵從上下文機制
  	  
  逐行代碼執行過程:
		 第1行: 彈出最後的名字叫a的函數代碼塊
		 alert(a);// function a(){alert(3);}
		 第3行: 因爲第2行將a從函數變成了變量,並賦值爲1
		 alert(a);//1
		 第5行: 因爲第4行只是聲明瞭一個函數,不會自己執行,所以a的值保持不變
		 alert(a);//1
		 第7行:因爲第6行將a的值變成了3
		 alert(a);//3
		 第9行:因爲第8行只是聲明一函數,不會自己執行,所以a的值保持不變
*/

3. 示例解讀

像這種示例,如果一眼無法看出來的話,使用解析器原理去看待題目,就沒有解析不出來的,如果熟了,基本上就一眼就可以看出來,因爲一步一步解析太麻煩了.

3.1 示例1(全局變量和函數內部變量名相同)
1.var a = 1;
2.function test(x) {
3.    alert(x);
4.    alert(a);
5.    var a = 2;
6.    alert(a);
7. }
8. test();

/*
  解讀代碼
  預解析過程:掃描代碼,尋找var,function,以及函數的參數,放進倉庫
  		   先掃描全局的var function
 		   global scope=>{
 		  		a => undefined
 		  		text =>function test(){} 
 		   }
   逐行代碼執行過程:
   		 	第1行,a賦值爲1
   		 	第2行,只是聲明函數,不會執行
   		 	第8行,調用函數,開始進行局部掃描,也就是函數內部進行預解析
   		 	test scope=>{
   		 		x => undefined
   		 		a => undefined
   		 	}
   		 	test()逐行執行函數內部的代碼:
   		 	第3行:alert(x)=> undefined
   		 	第4行:alert(a)=> undefined
   		 	第5行,a賦值爲2
   		 	第6行:alert(a)=> 2
*/
3.2 示例2 (變量和函數重名)

當變量和函數重名時,不管順序誰前誰後,只留下函數的值

1.alert(typeof fn);//function
2.var fn = 10;
3.function fn() { };
4.alert(typeof fn);//number

/*
解析代碼
  預解析:
      global  scope=>{
          fn=> function fn() { };
      }
      
   執行代碼:
      1.alert(typeof fn); 會彈出function
      2.賦值fn=10,此時fn類型變成了number
      3.不會執行
      4.alert(typeof fn); //number
*/
3.3 示例3(函數內部使用全局變量)
1.var a = 1;
2.function fn() {
3.    alert(a);//1
4.    a = 2;
5.}
6.fn();
7.alert(a);//2

/*
解析代碼
  預解析:
      global  scope=>{
      	  a=>undefined
          fn=> function fn() { };
          
          fn scope=>{
          	 a=>undefined
          }
      }
      
   執行代碼:
      1.a賦值等於1
      2.不會執行
      6.執行函數,函數沒有參數,也沒有var關鍵字進行聲明,不會預解析
      3.第3行:alert(a) 訪問的是全局變量 a  彈出1
      4.第4行:將全局變量a 賦值爲了2
      7.第7行:彈出2
*/
3.4 示例4(局部變量不會改變全局變量)
1.var a = 1;
2.function fn(a) {
3.    alert(a);//undefined
4.    a = 2;
5.    alert(a);//2
6.}
7.fn();
8.alert(a);//1

/*
解析代碼
    預解析:
    	global scope=>{
    		a=> undefined
    		fn=>function fn(a) {...}
    		
    		fn scope=>{
    		   a=>undefined//預解析形參
    		}
    		
    	}
     執行代碼:
        先調用fn函數 fn函數內部彈出a爲undefined
        函數內部 a賦值爲2
        函數內部 執行第二個alert(a) 爲2
        
        函數執行完之後,繼續執行下一步 alert(a) 爲1因爲函數函數內部雖然給了a=2
        但是至少改的函數內部的值,不會改全局變量 的a
        
*/
3.5 示例5
console.log(num);// undefined
var num = 24;
console.log(num);// 24
func(100, 200);
function func(num1, num2) {
    var total = num1 + num2;
    console.log(total);// 300
}

很自然的一段代碼,不要受前面的影響,而不知道最最簡單的代碼了

3.6 示例6.(同名函數)
fn();//2
function fn() { console.log(1); }
fn();//2
var fn = 10;
fn();//報錯 fn is not a function
function fn() { console.log(2); }
fn();

/*
解析代碼
   預解析:
   global scope=>{
   	  fn =>function fn() { console.log(2); }
   }
   
   同名函數時,後面覆蓋前面
   變量和函數同名時,函數是一等公民,優先級高
   
   逐行執行代碼時:
   第4行:將fn變成了一個number類型,再執行fn()時,函數不存在了,會直接報錯,停止執行
*/

三.聲明提升機制

在 JavaScrip 中變量聲明和函數聲明,聲明會被提升到當前作用域的頂部。

講聲明提升機制之前,要先說說JavaScript中的作用域,這裏排除ES6

1.作用域

在Javascript中,作用域爲可訪問變量(包含對象和函數)的集合

也就是說:作用域就是起作用的範圍

1.全局作用域

整個頁面起作用,在script內部都能訪問到

全局作用域中有全局對象window,代表一個瀏覽器窗口,可以直接調用

全局作用域中聲明的變量和函數,會作爲window對象的屬性和方法保存

變量在所有函數外聲明,也就是全局變量,擁有全局作用域

<script>
    var a = 123;//全局變量
    function fn() {
        console.log(a);//123
    }
    fn();
    console.log(a);//123
</script>

在JavaScript中,函數是唯一擁有自身作用域的代碼塊

2.局部作用域

局部作用域內的變量只能在函數內部使用,所以也叫函數作用域

變量在函數內聲明,即爲局部變量,擁有局部作用域

<script>
    function fn() {
    	var a = 123;//全局變量
        console.log(a);//123
    }
    fn();
    console.log(a);//報錯:Uncaught ReferenceError: a is not defined
</script>

注意:

  • 可以直接給一個未聲明的變量賦值(全局變量),但不能直接使用未聲明的變量!
  • 由於局部變量只作用於函數內部,所以不同的函數內部可以相同名稱的變量
  • 當全局與局部有同名變量的時候,訪問該變量將遵循"就近原則"

2.變量的生命週期

全局變量在頁面打開時創建,在頁面關閉後銷燬

局部變量在函數開始執行時創建,函數執行完之後局部變量自動銷燬

3.聲明提升機制

JavaScript 的變量聲明具有聲明提升機制,JavaScript引擎在執行的時候,會把所有變量的聲明都提升到當前作用域的頂部

我們可以從JavaScript解析器的執行原理來理解聲明提升機制,就很好理解了,同樣,上述解析器執行的示例代碼中,都是聲明提升機制的一種體現.當然了,這裏是不考慮ES6的.

總結一下:

  • javascript是沒有塊級作用域的,函數是javascript中唯一擁有自身作用域的結構

  • 聲明變量,實際上就是定義了一個名字,在內存中開闢了存儲空間,並且初始爲undefined,提升到當前作用域頂部

  • 函數的參數是原始類型的值(數值,字符串,布爾值),傳值方式是傳值傳遞,也就是在函數體內修改參數值,不會影響到函數外部

    var a = 1;
    function fn(a) {
        console.log(a);//3
    }
    fn(3);
    console.log(a);//1 不會改變原始值
    
  • 函數的參數是複合類型,也就是引用類型(數組,對象,函數)的時候,傳值方式是傳址傳遞,傳入的是原始值的地址,所以在函數內部修改參數,會改變原始的值(可參考深淺拷貝原理)

    var obj = { a: 1, b: 2 };
    function fn(obj) {
        obj.a=2;
    }
    fn(obj);
    console.log(obj);//{a: 2, b: 2}
    

    注意:如果函數內部修改的,不是參數對象的某個屬性,而是替換掉整個參數,這時不會影響到原始值!

    比如數組:

    var arr = [1, 2, 3]
    function fn(array) 
        array = [1, 2];
    }
    fn(arr);
    console.log(arr);//[1, 2, 3]
    

    4.聲明提升機制示例

    • var a 變量聲明,提升到作用域頂部,
    • var a=1; 變量聲明,提升到作用域頂部,但是賦值部分不會被提升
    • var a=function(){ … } 函數表達式,但也只是變量的聲明提升,並不會提升函數值
    • function a() {…} 函數聲明,會全部提升,而且如果函數名字和變量名字相同的話.優先級比變量高,因爲是一等公民

    總之呢,理解聲明提升機制,好好去理解JS解析器的原理就好了.

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