JavaScript深度理解——作用域

樸所羅門發表於2020-10-27

一、作用域是什麼?

1. 編譯原理

JavaScript是一門編譯語言,而並不是通常所說的“動態”或“解釋執行語言”。它不是提前編譯的,編譯結果也不能在分散式系統中進行移植。
編譯語言執行之前的三個步驟:

1.分詞/詞法分析:

將由字元組成的字串分解成有意義的程式碼塊,這些程式碼塊被稱為詞法單元。示例:var a = 3; 在這個過程中會分解成下面這些詞法單元: var、a、=、3、; 。

2. 解析/語法分析:

將詞法單元流(陣列)轉換成一個由元素逐漸巢狀所組成的代表了程式語法結構的樹。這個樹被稱為“抽象語法樹”(AST)。

3. 程式碼生成

將抽象語法樹(AST)轉化成可執行程式碼的過程。
JavaScript引擎不止上面的三個步驟,它的步驟要複雜得多。

注意:

  • JavaScript引擎不會用大量的時間來進行最佳化(相對於其他編譯語言那麼多的時間),因為JavaScript的編譯過程不是發生在構建之前的。
  • JavaScript中大部分情況下編譯發生在執行程式碼之前的幾微秒,並且任何的JavaScript程式碼片段在執行前都會進行編譯操作。

2. 理解作用域

誰參與JavaScript執行?

引擎: 從頭到尾負責整個JavaScript程式的編譯以及執行過程。
編譯器: 負責語法分析以及程式碼生成。
作用域: 收集並維護由所有的識別符號(及變數)組成的一系列查詢,並實施一套嚴格的規則,確定當前執行的程式碼對這些變數的訪問許可權。

變數賦值執行的兩個操作:

編譯器在當前作用域中宣告該變數(如果之前沒有宣告過)。
執行時引擎會在作用域中查詢該變數,如果找到就對它進行賦值操作。

引擎查詢變數的兩個型別:

LHS: 變數出現在賦值操作的左側執行LHS查詢。(找到目標變數)
RHS: 變數出現在賦值操作的右側執行RHS查詢。(取到它的源值)

3. 作用域巢狀

當一個塊或函式在另一個塊或函式中,就發生了作用域的巢狀。
當在當前作用域中無法找到某個變數時,引擎就會在外層巢狀的作用域中繼續查詢,直到找到該變數或者抵達最外層的作用域(全域性作用域)。

4. 異常

如果RHS查詢在所有的巢狀作用域中都找不到所需的變數,引擎就會丟擲  ReferenceError異常
如果LHS查詢在所有的巢狀作用域中都找不到所需的變數,則會在全域性作用域中建立一個具有該名稱的變數,並將其返回給引擎。(程式在非嚴格模式下)。
如果RHS查詢找到了一個變數,但是你對其進行不合理的操作,比如:對一個非函式型別的值進行函式呼叫;引用null或undefined型別中的屬性,引擎會丟擲  TypeError的異常

二、詞法作用域

作用域的工作模型:詞法作用域和動態作用域(少數程式語言使用)。

1. 什麼是詞法作用域?

詞法作用域是由你在寫程式碼時將變數和塊作用域寫在哪裡決定的。因此詞法分析器在處理程式碼時會保持作用域不變。也就是我們在JavaScript中所說的作用域。

2. 作用域查詢

作用域查詢會在找到第一個匹配的識別符號時停止。在多層的巢狀作用域中可以定義同名的識別符號(遮蔽效應)。
注意:
無論函式在哪裡被呼叫,也無論它如何被呼叫,它的詞法作用域都只由函式宣告時所處的位置決定。
詞法作用域只會查詢一級識別符號。

3. 欺騙詞法

1. eval

eval(…)函式可以接受一個字串作用引數,並將其中內容好像在書寫程式碼時就存在於程式中的這個位置。
示例:

function foo(str, a) {
    eval(str) //欺騙的目的
    console.log(a, b);
}
var b = 2;
foo("var b = 3", 10); //執行結果10,3  
123456

上面的程式碼中b不會輸出全域性作用域中的2。因為傳進去的是一個var b = 3;在執行到eval函式時,eval裡面的程式碼會被執行,從而在foo函式作用域中建立了一個變數b。從而遮蔽了全域性作用域中的b。
注意:在嚴格模式中,eval(…)函式在執行時有其自己的詞法作用域,所以此時無法修改所在的作用域。

2. with

with通常被當作重複引用同一個物件的多個屬性的快捷方式,可以不需要重複引用物件本身。
示例: 

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) //2123456789101112131415161718
上述程式碼在執行13行時,將物件o1傳進了函式foo,此時進行了with操作,裡面的a=2,會在全域性作用域中建立一個變數a,並把2賦值給它。1

3. 效能

嚴重影響執行效能。(不要使用它們)

三、函式作用域

1. 基本概念及用法 

  1. 含義: 屬於這個函式的全域性變數都可以在整個函式的範圍內使用及複用(在巢狀的作用域中也可以使用)。
  2. 外部作用域中無法訪問內部作用域中的識別符號。內部作用域中可以訪問外部作用域中的識別符號。
    最小特權原則(最小授權或最小暴露原則):指在軟體設計過程中,應該最小限度地暴露必要內容,而將其他內容都“隱藏”起來,比如某個模組或物件的API設計。
    函式作用域的  好處:規避同名識別符號之間的衝突。
    如何有效的  規避衝突
  3. 全域性名稱空間 
    用一個物件用作庫的名稱空間,所有需要暴露給外界的功能都會成為這個物件(名稱空間)的屬性,而不是將自己的識別符號暴露在頂級的此法作用域中。
  4. 模組管理
    透過依賴管理器的機制將庫的識別符號顯示地匯入到另外一個特定的作用域中。

函式宣告和函式表示式:

  1. function關鍵字是宣告中第一個詞的是函式宣告。否則是函式表示式。
  2. 函式表示式只能在內部作用域訪問,外部作用域不能訪問。
  3. 函式表示式不會非必要的汙染外部作用域。

2. 函式類別

  1. 匿名函式表示式:沒有名稱識別符號。
    缺點
    除錯困難
    引用自身只能使用已經過期的arguments.callee引用。
    程式碼可讀性,可理解性差
  2. 立即執行函式表示式(IIFE)
    形式:
    (function foo(){…})()

四、塊作用域 

1. ES6之前的塊作用域

1. with

用with從物件中建立出的作用域僅在with宣告中而非外部作用域中有效。

2. try/catch

catch分句會建立一個塊級作用域,其中宣告的變數僅在catch內部有效。

2. ES6之後新增的塊級作用域

1. let

let關鍵字可以將變數繫結到所在的任意作用域中(通常是{ … }內部),也就是let關鍵字為其宣告的變數隱式地劫持了所在的塊作用域。
示例: 

var foo = true;
if(foo){
    let bar = foo * 2;
    bar = something(bar);
    console.log(bar);
}
console.log(bar) //ReferenceError1234567

let宣告的bar變數,在外部訪問時會報ReferenceError的錯誤。
只要宣告是有效的,在宣告中的任意位置都可以使用一對大括號{ … }來為let建立一個用於繫結的塊。
let宣告不會提升,即不會像var一樣被提前進行說明。

2. const

ES6中引入了sonst,同樣可以用來建立塊作用域變數,其值是固定的(常量)。定義之後去修改它的值會引起錯誤。

五、作用域提升

引擎會在解釋JavaScript程式碼之前首先對其進行編譯,編譯的一部分工作就是找到所有的宣告,並用合適的作用域將它們關聯起來 

作用域提升的過程就好像變數和函式宣告從它們在程式碼中出現的位置被“被移動”到了最上面。所以是先有宣告,後有賦值。
注意
只有宣告本身會被提升,而賦值或其他執行邏輯會留在原地。如果提升改變了程式碼的執行順序,會造成非常嚴重的破壞。
每個作用域都會進行提升操作。函式表示式不會被提升。
即使具名的函式表示式,名稱識別符號在賦值之前也無法在所在作用域中使用。

foo(); //TypeError(非法操作)
bar(); //ReferenceError(bar未宣告)
var foo = function bar(){
    //...
}1234567

提升之後可以看出這樣的形式: 

var foo;
foo(); //TypeError(非法操作)
bar(); //ReferenceError(bar未宣告)
foo = function bar(){
    //...
}12345678

函式優先原則:
函式宣告和變數宣告都會被提升。二者都存在時,是函式首先會被提升,然後才是變數。

foo(); //1
var foo; //重複的宣告(被忽略)
function foo(){
    console.log(1);
}
foo = function(){
    console.log(2);
}12345678

會輸出1,而不是2。
上述程式碼被引擎理解為: 

function foo(){
    console.log(1);
}
foo(); //1
foo = function(){ 
    console.log(2);
}1234567

函式比變數先提升。
後面的函式宣告會覆蓋前面的函式宣告。

總結:  zhengzhou/

本文全方面的概括了JavaScript中的作用域,從JS編譯原理出發,分析JS程式碼執行過程,然後詳細介紹了js的詞法作用域、函式作用域、塊作用域等,最後對作用域提升做了詳細分析。
但願這篇文章能解決你的某些問題與困惑。如有不妥之處,及時指出!


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69978212/viewspace-2730115/,如需轉載,請註明出處,否則將追究法律責任。

相關文章