關於js作用域問題詳解

  • 2019 年 10 月 4 日
  • 筆記

執行上下文

函數表達式和函數聲明

1.  console.log(a); // ReferenceError: a is not defined  // ReferenceError(引用錯誤)對象表明一個不存在的變數被引用。  2.  console.log(a); // undefined  var a;  3.  console.log(a); // undefined  var a = 10;  4.  var a = 10;  console.log(a); // 10

在一段js程式碼拿過來真正一句一句運行之前,瀏覽器已經做了一些「準備工作」,其中就包括對變數的聲明,而不是賦值。變數賦值是在賦值語句執行的時候進行的。可用下圖模擬:第一句報錯,a未定義,很正常。第二句、第三句輸出都是undefined,說明瀏覽器在執行console.log(a)時,已經知道了a是undefined,但卻不知道a是10(第三句中)。

接下來的這段程式碼需要注意程式碼注釋中的兩個名詞——「函數表達式」和「函數聲明」。雖然兩者都很常用,但是這兩者在「準備工作」時,卻是兩種待遇。

「準備工作」

1.  console.log(a); // ReferenceError: a is not defined  // ReferenceError(引用錯誤)對象表明一個不存在的變數被引用。  2.  console.log(a); // undefined  var a;  3.  console.log(a); // undefined  var a = 10;  4.  var a = 10;  console.log(a); // 10

在「準備工作」中,對待函數表達式就像對待「 var a = 10 」這樣的變數一樣,只是聲明。看以上程式碼。「函數聲明」時我們看到了第二種情況,而「函數表達式」時我們看到了第一種情況。

而對待函數聲明時,卻把函數整個賦值了。

總結一下,在「準備工作」中完成了哪些工作:

  1. 變數、函數表達式——變數聲明,默認賦值為undefined;
  2. this——賦值;
  3. 函數聲明——把函數整個賦值;

這三種數據的準備情況我們稱之為「執行上下文」或者「執行上下文環境」。

function fn(x) {  console.log(arguments); //Arguments { 0: 10, 等 2 項… }  console.log(x); //10  }  fn(10);

以上程式碼展示了在函數體的語句執行之前,arguments變數和函數的參數都已經被賦值。從這裡可以看出,函數每被調用一次,都會產生一個新的執行上下文環境。因為不同的調用可能就會有不同的參數。

另外一點不同在於,函數在定義的時候(不是調用的時候),就已經確定了函數體內部自由變數的作用域。用一個例子說明一下:

var a = 10;    function fn() {  console.log(a); //a是自由變數  //函數創建時,就確定了a要取值的作用域  }    function bar() {  var a = 20;  fn(); //列印10不是20  }  bar(fn); //10

執行上下文棧給執行上下文環境下一個通俗的定義——在執行程式碼之前,把將要用到的所有的變數都事先拿出來,有的直接賦值了,有的先用undefined佔個空。

執行全局程式碼時,會產生一個執行上下文環境,每次調用函數都又會產生執行上下文環境。當函數調用完成時,這個上下文環境以及其中的數據都會被消除,再重新回到全局上下文環境。處於活動狀態的執行上下文環境只有一個。

其實這是一個壓棧出棧的過程——執行上下文棧。

1 var a = 10, //1.進入全局上下文環境  2 fn,  3 bar = function(x) {  4 var b = 5;  5 fn(x + b); //3.進入fn函數上下文環境  6 };  7  8 fn = function(y) {  9 var c = 5;  10 console.log(y + c);  11 }  12  13 bar(10); //2.進入bar函數上下文環境

在執行程式碼之前,首先將創建全局上下文環境。

全局

上下文環境

a

undefined

fn

undefined

bar

undefined

this

window

然後是程式碼執行。程式碼執行到第12行之前,上下文環境中的變數都在執行過程中被賦值。

全局

上下文環境

a

10

fn

function

bar

function

this

window

執行到第13行,調用bar函數。

跳轉到bar函數內部,執行函數體語句之前,會創建一個新的執行上下文環境。 全局 | 上下文環境 —|— b | undefined x | 10 arguments | [10] this | window

並將這個執行上下文環境壓棧,設置為活動狀態。

執行到第5行,又調用了fn函數。進入fn函數,在執行函數體語句之前,會創建fn函數的執行上下文環境,並壓棧,設置為活動狀態。

待第5行執行完畢,即fn函數執行完畢後,此次調用fn所生成的上下文環境出棧,並且被銷毀(已經用完了,就要及時銷毀,釋放記憶體)。

同理,待第13行執行完畢,即bar函數執行完畢後,調用bar函數所生成的上下文環境出棧,並且被銷毀(已經用完了,就要及時銷毀,釋放記憶體)。

好了,給大家介紹了一段簡短程式碼的執行上下文環境的變化過程,一個完整的閉環。其中上下文環境的變數賦值過程我省略了許多,因為那些並不難,一看就知道。

作用域

基礎認識

「javascript沒有塊級作用域」。所謂「塊」,就是大括弧「{}」中間的語句。

比如一個if語句

var i = 10;  if (i > 1) {  var name = "yzh";  }  console.log(name); //yzh
for (var i = 0; i < 10; i++) {    }  console.log(i); //10

for語句

我們在編寫程式碼的時候,不要在「塊」裡面聲明變數,要在程式碼的一開始就聲明好了。以避免發生歧義

var i;  for (i = 0; i < 10; i++) {    }  console.log(i);

我們在聲明變數時,全局程式碼要在程式碼前端聲明,函數中要在函數體一開始就聲明好。除了這兩個地方,其他地方都不要出現變數聲明。而且建議用「單var」形式你光知道「javascript沒有塊級作用域」是完全不夠的,你需要知道的是——javascript除了全局作用域之外,只有函數可以創建的作用域。

概念

如上圖,全局程式碼和fn、bar兩個函數都會形成一個作用域。而且,作用域有上下級的關係,上下級關係的確定就看函數是在哪個作用域下創建的。例如,fn作用域下創建了bar函數,那麼「fn作用域」就是「bar作用域」的上級。

作用域最大的用處就是隔離變數,不同作用域下同名變數不會有衝突

例如以上程式碼中,三個作用域下都聲明了「a」這個變數,但是他們不會有衝突。各自的作用域下,用各自的「a」。

作用域和上下文環境

如上圖,我們在上文中已經介紹了,除了全局作用域之外

每個函數都會創建自己的作用域,作用域在函數定義時就已經確定了。而不是在函數調用時確定。

下面我們將按照程式執行的順序,一步一步把各個上下文環境加上

第一步,在載入程式時,已經確定了全局上下文環境,並隨著程式的執行而對變數就行賦值。

第二步,程式執行到第27行,調用fn(10),此時生成此次調用fn函數時的上下文環境,壓棧,並將此上下文環境設置為活動狀態。

第三步,執行到第23行時,調用bar(100),生成此次調用的上下文環境,壓棧,並設置為活動狀態。

第四步,執行完第23行,bar(100)調用完成。則bar(100)上下文環境被銷毀。接著執行第24行,調用bar(200),則又生成bar(200)的上下文環境,壓棧,設置為活動狀態。

第五步,執行完第24行,則bar(200)調用結束,其上下文環境被銷毀。此時會回到fn(10)上下文環境,變為活動狀態。

第六步,執行完第27行程式碼,fn(10)執行完成之後,fn(10)上下文環境被銷毀,全局上下文環境又回到活動狀態。

最後我們可以把以上這幾個圖片連接起來看看。

作用域只是一個「地盤」,一個抽象的概念,其中沒有變數。要通過作用域對應的執行上下文環境來獲取變數的值。

同一個作用域下,不同的調用會產生不同的執行上下文環境,繼而產生不同的變數的值。所以,作用域中變數的值是在執行過程中產生的確定的,而作用域卻是在函數創建時就確定了。

如果要查找一個作用域下某個變數的值,就需要找到這個作用域對應的執行上下文環境,再在其中尋找變數的值。

自由變數

在A作用域中使用的變數x,卻沒有在A作用域中聲明(即在其他作用域中聲明的),對於A作用域來說,x就是一個自由變數。 例:

var x = 50;    function fn() {  var b = 20;  console.log(x + b);  }  fn(); //70
var x = 50;    function fn() {  console.log(x);  }    function show(f) {  var x = 20;  (function() {  f(); //50 不是20  })();  }  show(fn); //50 不是20

在調用fn()函數時,函數體中第6行。取b的值就直接可以在fn作用域中取,因為b就是在這裡定義的。而取x的值時,就需要到另一個作用域中取。到哪個作用域中取呢? 有人說過要到父作用域中取,其實有時候這種解釋會產生歧義 例如:

不要在用以上說法了。相比而言,用這句話描述會更加貼切——要到創建這個函數的那個作用域中取值——是「創建」,而不是「調用」,切記切記——其實這就是所謂的「靜態作用域」。

對於本文第一段程式碼,在fn函數中,取自由變數x的值時,要到哪個作用域中取?——要到創建fn函數的那個作用域中取——無論fn函數將在哪裡調用。

上面描述的只是跨一步作用域去尋找。

如果跨了一步,還沒找到呢?——接著跨!——一直跨到全局作用域為止。要是在全局作用域中都沒有找到,那就是真的沒有了。

這個一步一步「跨」的路線,我們稱之為——作用域鏈。

全局環境

changeColor()的局部環境

swapColors()的局部環境

變數color

變數anotherColor

變數tempColor

函數changeColor()

函數swapColors()

以上程式碼共涉及3個執行環境:全局環境、changeColor()的局部環境和swapColors()的局部環境。

內部環境可以通過作用域鏈訪問所有的外部環境,但外部環境不能訪問內部環境中的任何變數和函數。

每個環境都可以向上搜索作用域鏈,以查詢變數和函數名;但任何環境都不能通過向下搜索作用域鏈而進入另一個執行環境。

最後這個例子是從書上找到的,比較經典和簡單。

var color = "blue";    function changeColor() {  var anotherColor = "red";    function swapColors() {  var tempColor = anotherColor;  anotherColor = color;  color = tempColor;    //這裡可以訪問color,anotherColor和tempColor  }    //這裡可以訪問color和anotherColor,但不能訪問tempColor  swapColors();  }    changeColor(); //注釋後alert顯示為blue    //這裡只能訪問color  alert("Color is now " + color); //red

Tags: