1. 程式人生 > 實用技巧 >JS學習筆記1

JS學習筆記1

針對var a = 1;的流程分析

在執行前,編譯器會做以下工作:

  • 分詞:把字串分解成多個有意義的詞法單元。
    • 對var a = 1;來說,分詞階段後,這條語句會被分成 var/a/=/1/;這些詞法單元。
  • 解析:用多個詞法單元生成一個代表程式語法結構的樹。
    • 對var a=1;來說,解析結束後,會生成一個樹,這個樹以VariableDeclaration為根節點,這個根節點有兩個子節點,其中一個是值為a的Identifier結點,另外一個是AssignmentExpression的子節點,這個子節點下還有一個值為2的NumericLiteral的結點。
  • 程式碼生成
    • 編譯器首先會詢問作用域,是否已經有一個名稱為a的變數存在於同一個作用域集合中
      • 如果已經存在了,編譯器會忽略這條宣告;
      • 如果不存在,編譯器會要求作用域在當前作用域集合中宣告一個新的變數a
    • 編譯器為引擎生成a=2這條程式碼

引擎開始執行後,執行到a=2這行程式碼時,會先詢問作用域中是否包含名稱為a的變數(LHS查詢),如果沒找到會依次往上層的作用域找,直到找到或者報錯。找到後,會把2賦值給這個變數。

總結下,整個編譯執行流程中 編譯器 作用域 引擎這三方會相互配合,編譯器負責讓作用域宣告變數,引擎通過作用域查詢變數

LHS查詢和RHS查詢

LHS查詢指的是,引擎想作用域查詢容器,然後為容器賦值。

RHS查詢指的是,引擎想要獲取容器中的值。

舉個例子:

function foo(a){
    console.log(a);
}
foo(2);

針對上面這段程式碼,使用到的查詢如下:

  1. foo:引擎需要向作用域進行RHS查詢

  2. 作用域把foo的值(是一個函式)還給引擎

  3. a:引擎執行這個函式,引擎需要向作用域進行LHS查詢(引數a賦值)

  4. 作用域返回給引擎a的容器,引擎把2賦值到a容器中

  5. console:引擎需要向作用域進行RHS查詢

  6. 作用域把console的值(是一個物件)還給引擎

  7. 引擎在console的物件中查詢是否有log方法

  8. a:引擎需要向作用域進行RHS查詢

  9. 作用域把a的值還給引擎

    ......

相關異常

ReferenceError

如果對一個變數做RHS查詢時,這個變數還沒有宣告過,會丟擲此異常。

如果對一個變數做LHS查詢時,嚴格模式下,也會丟擲此異常;非嚴格模式下,作用域會自動建立一個全域性變數

TypeError

拿到變數後,如果做出了一些不合理的操作,那麼會拋此異常。比如:

  • 對一個非函式的值進行函式呼叫
  • 引用null或者undefined型別值中的屬性

作用域規則-詞法決定

詞法作用域的意思是,作用域由書寫時函式宣告的位置決定。

編譯的詞法分析階段其實就已經確定了識別符號的位置以及如何宣告的。

簡單理解,上面的三大模組關注的是整體的互動過程,詞法作用域模組關注的是 作用域的生成規則

在JS中,有兩種方式可以在執行時修改或者影響已經決定好的作用域

  • eval:在執行時執行一段程式碼,如果eval包含宣告語句並且是非嚴格模式,那麼eval所在位置的作用域就會被改變。

    function foo(str,a){
        eval(str);
        console.log(a,b);
    }
    foo("var b=3;",1); //1,3
    

    eval所處的foo作用域中,在執行時被增加了b的宣告,所以可以打印出1和3的結果。

    在嚴格模式下,eval有自己獨立的作用域,不會影響所在的作用域。

  • with:重複引用同一個物件的多個屬性的快捷方式,with把傳入的obj當作一個作用域,如果不存在某個屬性,就會向上查詢,在非嚴格模式下,會建立全域性變數。

    function foo(obj){
        with(obj){
            a=2;
        }
    }
    
    
    var o1 = {
        a:3
    };
    
    var o2 = {
        b:3
    };
    
    foo(o1);
    console.log(o1.a);//2
    
    foo(o2);
    console.log(o2.a);//undefined
    console.log(a);//2 這裡建立了全域性變數
    

這兩種方式都不推薦使用,因為它們會被嚴格模式限制,並且會有效能問題。

作用域規則-函式作用域和塊作用域

在上一個模組詞法規則中介紹了作用域是由宣告的位置決定。

具體來說,宣告一個函式就會建立一個新的作用域,這種作用域叫做函式作用域。

函式作用域

在函式作用域中宣告的變數只能在函式內部或者巢狀內部函式中被訪問到,外部無法直接訪問到(特例:閉包可以做到)。

為什麼需要把變數和函式隱藏在函式作用域內部?

  • 最小特權原則:在軟體設計中應最小限度的暴露必要內容。
  • 規避變數衝突

但是將部分變數和函式封裝為一個新的函式,其實還是會在全域性作用域中引入一個新的識別符號,並且需要通過這個識別符號去呼叫這部分被封裝好的函式,在具體實踐中,我們可以使用IIFE立即執行表示式的方式來避免引入新的識別符號,並且簡化呼叫。

IIFE

var a=2;

(function foo(){
    var a = 3;
    console.log(a);
})();//IIFE

console.log(a);

除了函式作用域以外,還可以宣告程式碼塊的作用域,叫做塊作用域。

塊作用域

在塊級作用域中宣告的變數只能在塊內部以及巢狀塊或者函式中被訪問到,塊外部無法訪問。

塊作用域的實現方式有下面幾種:

  • try/catch:catch部分會建立一個塊級作用域,其中宣告的變數僅在塊中有效。
  • let:用let關鍵字來宣告變數,它會去找最近的一個塊,並把這個塊當作作用域;或者顯示的建立一個塊來決定作用域(推薦這種做法)
    • 注意:let塊作用域中的宣告的let變數不會被自動提升!
    • for迴圈中定義的變數應該使用let;大物件應該用let宣告,以便促使垃圾回收。
  • const:const宣告的是變數在賦值後不允許再修改。

提升

函式宣告和變數宣告都會被提升到作用域的頂部,注意,只是宣告被提升了,但是表示式沒有被提升

函式宣告的優先順序更高,所以函式宣告在前,var變數在後,那麼var會被忽略掉。

程式碼例子如下:

foo();

var foo = function(){
    console.log("2");
};

function foo(){
    console.log("1");
}

這個程式碼等同於

function foo(){
    console.log("1");
}

foo();

foo = function(){
    console.log("2");
};

多個相同識別符號的函式宣告,後面的會覆蓋前面的。

程式碼例子如下:

foo();

function foo(){
     console.log("1");
}
function foo(){
     console.log("2");
}

程式碼等同於

function foo(){
     console.log("2");
}

foo();