實例分析 JavaScript 詞法作用域

GenevaRider 7年前發布 | 19K 次閱讀 JavaScript開發 JavaScript

了解作用域對于編寫代碼至關重要,作用域是在運行時代碼中的某些特定部分中變量,函數和對象的可訪問性。換句話說,作用域決定了代碼區塊中變量和其他資源的可見性。對于JavaScript中作用域我們可能已經了解了很多。今天從其他方面介紹一下 JavaScript 中作用域,以幫助我們更好的完整的了解 JavaScript 作用域。

作用域模型

作用域有兩種常見的模型:詞法作用域(Lexical Scope,通常也叫做 靜態作用域) 和 動態作用域(Dynamic Scope)。其中詞法作用域更常見,被 JavaScript 等大多數語言采用。(愚人碼頭注:這里避開了 with 和 eval 特殊語句,不再做介紹)。

首先了解一下這兩種模型的說明:

  • 詞法作用域:詞法作用域是指在詞法分析階段就確定了,不會改變。變量的作用域是在定義時決定而不是執行時決定,也就是說詞法作用域取決于源碼,通過靜態分析就能確定,因此詞法作用域也叫做靜態作用域。
  • 動態作用域:動態作用域是在運行時根據程序的流程信息來動態確定的,而不是在寫代碼時進行靜態確定的。 動態作用域并不關心函數和作用域是如何聲明以及在何處聲明的,只關心它們在何處調用。

JavaScript 的詞法作用域

如果一個文檔流中包含多個script代碼段(用script標簽分隔的js代碼或引入的js文件),它們的運行順序是:

  1. 讀入第一個代碼段(js執行引擎并非一行一行地分析程序,而是一段一段地分析執行的)
  2. 做詞法分析,有錯則報語法錯誤(比如括號不匹配等),并跳轉到步驟5
  3. 對 var 變量和 function 定義做“預解析“(永遠不會報錯的,因為只解析正確的聲明)
  4. 執行代碼段,有錯則報錯(比如變量未定義)
  5. 如果還有下一個代碼段,則讀入下一個代碼段,重復步驟2
  6. 完成

JavaScript 解析過程

JavaScript 中每個函數都都表示為一個函數對象(函數實例),函數對象有一個僅供 JavaScript 引擎使用的 [[scope]] 屬性。通過語法分析和預解析,將 [[scope]] 屬性指向函數定義時作用域中的所有對象集合。這個集合被稱為函數的作用域鏈(scope chain),包含函數定義時作用域中所有可訪問的數據。

JavaScript 執行過程

執行具體的某個函數時,JS引擎在執行每個函數實例時,都會創建一個執行期上下文(Execution Context)和激活對象(active Object)(它們屬于宿主對象,與函數實例執行的生命周期保持一致,也就是函數執行完成,這些對象也就被銷毀了,閉包例外。)

執行期上下文(Execution Context)定義了一個函數正在執行時的作用域環境。它使用函數 [[scope]] 屬性進行初始化。

隨后,執行期上下文 頂部 的會創建一個激活對象(active Object),這個激活對象保存了函數中的所有形參,實參,局部變量, this 指針等函數執行時函數內部的數據情況。這個時候激活對象中的那些屬性并沒有被賦值,執行函數內的賦值語句,這才會對變量集合中的變量進行賦值處理。也就是說 激活對象是一個可變對象,里面的數據隨著函數執行時的數據變化而變化。

考慮一下下圖中的代碼:

分析過程:

  • 作用域1 (綠色) :即全局作用域,包含變量 foo ;
  • 作用域2 (黃色) : foo 函數的作用域,包含變量 a , bar , b
  • 作用域3 (藍色) : bar 函數的作用域,包含變量 c

bar 作用域里完整的包含了 foo 的作用域, 因為 bar 是定義在 foo 中的,產生嵌套作用域。值得注意的是,一個函數作用域只有可能存在于一個父級作用域中,不會同時存在兩個父級作用域。還有諸如 this , window , document 等全局對象這里就不說了,避免混亂。

執行過程:

  • 語句 console.log 尋找變量 a , b , c ;
  • 其中 c 在自己的作用域中找到,
  • a , b 在自己的作用域中找不到,于是向上級作用域中查找,在 foo 的作用域中找到,并且調用。

函數在執行時,每遇到一個變量,都會去執行期上下文的作用域鏈的頂部,也就是執行函數的激活對象開始搜索,如果在第一個作用域鏈(即,Activation Object 激活對象)中找到了,那么就返回這個變量。如果沒有找到,那么繼續向下查找,直到找到為止。如果在整個執行期上下文中都沒有找到這個變量,在這種情況下,該變量被認為是未定義的。也就是說如果 foo 的作用域中也定義了 c ,但 bar 函數只調用自己作用域里的 c 。這就是我們說的變量取值。

實例分析

上面講了很多概念性的東西。下面我們用實際代碼討論一下變量的作用域。這些可能在一下“坑人”的面試題中很常見。

不同作用域中的同名變量

同名變量可能在存在于多個作用域中,具體取值在執行時變量查找時決定。

示例1,作用域中變量查找規則:

function DoSomething()
{
  var a = 2;
  console.log(a); // 2
  console.log(window.a); // 1
}
var a = 1;
DoSomething();

這個很好理解,全局作用域中定義并賦值了變量 a , DoSomething 函數中也定義并賦值了變量 a 。 DoSomething 函數在執行時,首先找到的是激活對象中的變量 a ,也就是 2 。

console.log(window.a) 也很好理解,就是首先找 window 對象,在激活對象中沒找到,繼續向上搜索,本例中, window 對象在Global Object(全局對象)中找到, window 的 a 屬性值為 1 。

這段代碼我相信對大家來說,理解起來都沒問題。那么我們稍微對代碼做一下修改:

示例2,作用域中沒賦值的變量:

function DoSomething()
{
  var a; // 注意這一行,定義了一個變量 a 但是不賦值。
  console.log(a); // 'undefined'
}
var a = 1;
DoSomething();

DoSomething 函數中定義了變量 a ,但是沒有賦值,那么這里打印結果是什么呢?結果是 undefined ,原因和示例1一樣,只不過 DoSomething 函數在執行時,首先找到的是激活對象中的變量 a ,只不過這里的變量 a 并沒有賦值,所以是 undefined 。

示例3,激活對象是一個可變對象

function DoSomething()
{
  console.log(a); // 'undefined'
  var a = 2; // 注意這一行。
  console.log(a); // 2
}
var a = 1;
DoSomething();

根據作用域中變量查找規則,當前的激活對象中找到了有變量 a 的定義,執行到第一條 console.log(a) 語句時值為 undefined 。執行到第二條 console.log(a) 語句時,變量 a 已經被賦值為 2 。也就是說,激活對象是一個可變對象,里面的數據隨著函數執行時的數據變化而變化。

參數和同名變量

上面3個示例講了不同作用域中的同名變量,這里我們來講講 形參(parameters)、實參(arguments)與同名變量的關系。

首先了解一下形參和實參:

function one(a,b,c) {
    console.log(one.length);//形參數量
}
function two(a,b,c,d,e,f,g){
    console.log(arguments.length);//實參數量
}
one(1)
two(1)

顧名思義,形參就是形式參數,是在定義函數名和函數體的時候使用的參數,目的是用來接收調用該函數時傳遞的參數。實參就是實際參數,是在調用時傳遞給函數的參數,即傳遞給被調用函數的值。實參可以是常量、變量、表達式、函數等,無論實參是何種類型的量,在進行函數調用時,它們都必須具有確定的值,以便把這些值傳送給形參。

那么形參、實參與變量同名又該如何呢?

示例4,形參、實參與同名全局變量的關系

// DoSomething傳入了一個 a 參數。
var b = 2;
function DoSomething(a,b)
{
  var a;
  console.log(a); // 1
  console.log(b); // 'undefined'
}
DoSomething( 1 );

打印的結果為 1 和 undefined ,為什么呢?

首先來看形參 b 和全局變量 b ,這兩者毫無關系,只是同名。完全符合作用域中變量查找規則, DoSomething 函數在執行時,首先找到的是激活對象中的形參 b ,形參 b 沒有值,所以是 undefined 。

接著來說說 參數 a 和局部變量 a ,看上面的代碼似乎并不符合作用域中變量查找規則。如果作用域中變量查找規則去理解,應該是這樣的, DoSomething 函數在執行時,首先找到的是激活對象中的參數 a ,然后定義局部變量 a ,不賦值,所以 console.log(a) 是 undefined 。但是結果恰恰是 1 。這又是為什么呢?

示例4并不能解釋這個問題,讓我們再來看一段代碼。

示例5,形參、實參與同名局部變量的關系

function DoSomething(a)
{
  console.log(a); // 1
  console.log(arguments[0]); // 1
  var a = 2;
  console.log(a); // 2
  console.log(arguments[0]); // 2
}
DoSomething( 1 );

打印的結果是 1 , 1 , 2 , 2 。從上面的代碼可以看到,參數 a 和局部變量 a 值是完全相同的,即使是局部變量 a 重新定義和賦值之后。這樣就好理解了,參數和同名變量之間是 “ 引用 ” 關系,也就是說 JavaScript 引擎的處理參數和同名局部變量是都引用同一個內存地址。所以示例5中修改局部變量會影響到 arguments 的情況出現。

 

 

來自:http://www.css88.com/archives/7300

 

 本文由用戶 GenevaRider 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!