天天看點

JavaScript學習:執行環境及作用域

1、執行環境

     執行環境是 JavaScript中最為重要的一個概念。執行環境定義了變量或函數有權通路的其他資料,決定了它們各自的行為。每個執行環境都有一個 與之關聯的變量對象(variable object),環境中定義的所有變量和函數都儲存在這個對象中。雖然我們編寫的代碼無法通路這個對象,但解析器在處理資料時會在背景使用它。

    每個函數都有自己的執行環境。當執行流進入一個函數時,函數的環境就會被推入一個環境棧中。 而在函數執行之後,棧将其環境彈出,把控制權傳回給之前的執行環境。

2、作用域

   當代碼在一個環境中執行時,會建立變量對象的一個作用域鍊(scope chain)。作用域鍊的用途,是保證對執行環境有權通路的所有變量和函數的有序通路。作用域鍊的前端,始終都是目前執行的代碼所在環境的變量對象。如果這個環境是函數,則将其活動對象(activation object)作為變量對象。活動對象在最開始時隻包含一個變量,即 arguments 對象(這個對象在全局環境中是不存在的)。作用域鍊中的下一個變量對象來自包含(外部)環境,而再下一個變量對象則來自下一個包含環境。這樣,一直延續到全局執行環境;全局執行環境的變量對象始終都是作用域鍊中的最後一個對象。

  • 延長作用域鍊 

        雖然執行環境的類型總共隻有兩種——全局和局部(函數),但還是有其他辦法來延長作用域鍊。 這麼說是因為有些語句可以在作用域鍊的前端臨時增加一個變量對象,該變量對象會在代碼執行後被移除。

      在兩種情況下會發生這種現象。具體來說,就是當執行流進入下列任何一個語句時,作用域鍊就會 得到加長:

       try-catch 語句的 catch 塊;

       with 語句。

       這兩個語句都會在作用域鍊的前端添加一個變量對象。對 with 語句來說,會将指定的對象添加到作用域鍊中。對 catch 語句來說,會建立一個新的變量對象,其中包含的是被抛出的錯誤對象的聲明。 

  • 沒有塊級作用域

        JavaScript 沒有塊級作用域經常會導緻了解上的困惑。在其他類 C 的語言中,由花括号封閉的代碼塊都有自己的作用域(如果用 ECMAScript的話來講,就是它們自己的執行環境),因而支援根據條件來定義變量。例如,下面的代碼在 JavaScript中并不會得到想象中的結果:  

if (true) {
    var color = "blue"; 
}  
console.log(color);    //"blue" 
           

      這裡是在一個 if 語句中定義了變量 color。如果是在 C、C++或 Java中,color 會在 if 語句執 行完畢後被銷毀。但在 JavaScript中,if 語句中的變量聲明會将變量添加到目前的執行環境(在這裡是 全局環境)中。

在使用 for 語句時尤其要牢記這一差異,例如: 

for (var i=0; i < 10; i++){   
  doSomething(i); 
}  
alert(i);      //10 
           

       對于有塊級作用域的語言來說,for 語句初始化變量的表達式所定義的變量,隻會存在于循環的環境之中。而對于 JavaScript來說,由 for 語句建立的變量 i 即使在 for 循環執行結束後,也依舊會存在 于循環外部的執行環境中。 

es6中的解決方案:

    ES6 新增了

let

指令,用來聲明變量。它的用法類似于

var

,但是所聲明的變量,隻在

let

指令所在的代碼塊内有效。

{
  let a = 10;
  var b = 1;
}

a // ReferenceError: a is not defined.
b // 1
           

上面代碼在代碼塊之中,分别用

let

var

聲明了兩個變量。然後在代碼塊之外調用這兩個變量,結果

let

聲明的變量報錯,

var

聲明的變量傳回了正确的值。這表明,

let

聲明的變量隻在它所在的代碼塊有效。

for

循環的計數器,就很合适使用

let

指令。

for (let i = 0; i < 10; i++) {
  // ...
}

console.log(i);
// ReferenceError: i is not defined
           

上面代碼中,計數器

i

隻在

for

循環體内有效,在循環體外引用就會報錯。

下面的代碼如果使用

var

,最後輸出的是

10

var a = [];
for (var i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 10
           

上面代碼中,變量

i

var

指令聲明的,在全局範圍内都有效,是以全局隻有一個變量

i

。每一次循環,變量

i

的值都會發生改變,而循環内被賦給數組

a

的函數内部的

console.log(i)

,裡面的

i

指向的就是全局的

i

。也就是說,所有數組

a

的成員裡面的

i

,指向的都是同一個

i

,導緻運作時輸出的是最後一輪的

i

的值,也就是 10。

如果使用

let

,聲明的變量僅在塊級作用域内有效,最後輸出的是 6。

var a = [];
for (let i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 6
           

上面代碼中,變量

i

let

聲明的,目前的

i

隻在本輪循環有效,是以每一次循環的

i

其實都是一個新的變量,是以最後輸出的是

6

。你可能會問,如果每一輪循環的變量

i

都是重新聲明的,那它怎麼知道上一輪循環的值,進而計算出本輪循環的值?這是因為 JavaScript 引擎内部會記住上一輪循環的值,初始化本輪的變量

i

時,就在上一輪循環的基礎上進行計算。

另外,

for

循環還有一個特别之處,就是設定循環變量的那部分是一個父作用域,而循環體内部是一個單獨的子作用域。

for (let i = 0; i < 3; i++) {
  let i = 'abc';
  console.log(i);
}
// abc
// abc
// abc
           

上面代碼正确運作,輸出了 3 次

abc

。這表明函數内部的變量

i

與循環變量

i

不在同一個作用域,有各自單獨的作用域。

文章内容出自

《JavaScript進階程式設計(第3版)》

阮一峰《ECMAScript 6 入門》

繼續閱讀