深入學習js之——詞法作用域和動態作用域

Louis發表於2019-02-16

開篇

當我們在開始學習任何一門語言的時候,都會接觸到變數的概念,變數的出現其實是為了解決一個問題,為的是儲存某些值,進而,儲存某些值的目的是為了在之後對這個值進行訪問或者修改,正是這種儲存和訪問變數的能力將狀態給了程式。我們的程式中到處都充斥著對於狀態的判斷,根據不同的狀態執行不同的邏輯。

我們試想一下,如果沒有狀態這個概念,程式雖然也能夠執行一些簡單的任務,但是它會受到很多的限制,所能完成的功能是有限制的,舉個例子,沒有狀態你是如何執行迴圈語句?沒有狀態如何更加優雅地使用邏輯結構

仔細想想,好像是寸步難行,當然引入變數後幫我們解決了這個問題。

但是,引入變數和狀態的概念之後會引起幾個問題:這些變數住在哪裡?換句話說,它們儲存在哪裡?最重要的是,程式需要它們的時候如何找到它們?

今天我們就一起學習一下這套儲存和查詢變數的規則,這套規則我們稱之為:作用域。

作用域

我們來拆解一下這個詞語,所謂的“”我們可以理解為:範圍、區域,加上“作用”兩個字所要表述的問題就是作用的範圍、區域,比如國家的行政區域劃分是為了便於管理,類比到程式原始碼中作用域的出現也是為了便於對於變數做管理。

好,這裡我們簡單做一下總結:

  • 定義:作用域是指程式原始碼中定義變數的區域。
  • 作用:作用域規定了如何查詢變數,也就是確定當前執行程式碼對變數的訪問許可權。
  • 在javaScript中的應用 :JavaScript採用詞法作用域(lexical scoping),也就是靜態作用域

那什麼又是 詞法作用域或者靜態作用域呢?

請繼續往下看

靜態作用域與動態作用域

因為javaScript採用的是詞法作用域,函式的作用域在函式定義的時候就決定了。
而詞法作用域相對的是動態作用域,函式的作用域是在函式呼叫的時候才決定的。
讓我們看一個例子來理解詞法作用域和動態作用域之間的區別:

var value = 1;

function foo() {
  console.log(value);
}

function bar() {
  var value = 2;
  foo();
}

bar();
// 結果是 ???

上面的程式碼中:

  • 1.我們首先定義了一個value,並賦值為1;
  • 2.宣告一個函式foo,函式的功能是列印 value 這個變數的值;
  • 3.宣告一個函式bar,函式內部重新建立了一個變數 value 這個變數賦值為2;
    在函式內部執行了 foo() 這個函式;
  • 4.執行 bar() 這個函式

假設javaScript採用靜態作用域,讓我們分析下執行過程:

執行foo函式,首先從 foo 函式內部查詢是否有變數 value ,如果沒有
就根據書寫的位置,查詢上面一層的程式碼,我們發現value等於1,所以結果會列印 1。

假設javaScript採用動態作用域,讓我們分析下執行過程:

執行foo函式,依然是從 foo 函式內部查詢是否有區域性變數 value。如果沒有,
就從呼叫函式的作用域,也就是 bar 函式內部查詢 value 變數,所以結果會列印 2。

上面在區分靜態作用於和動態作用域的時候,我們已經說了如果是靜態作用域,那麼函式在書寫定義的時候已經確定了,而動態作用域是函式執行過程中才確定的。

JavaScript採用的是靜態作用域,所以這個例子的結果是 1。
我們在控制檯中輸入執行上面的函式,檢驗一下執行結果果然是 1。

動態作用域

那什麼語言是採用的動態的作用域呢? 其實bash 就是動態作用域,
我們可以新建一個 scope.bash 檔案將下列程式碼放進去,執行一下這個指令碼檔案:

#!/bin/bash

value=1
function foo () {
    echo $value;
}
function bar () {
  local value=2;
  foo;
}
bar

上面程式碼執行的結果輸出2很好解釋,雖然在程式碼最上層定義了 value並賦值為1,但是在呼叫foo函式的時候,在查詢 foo 內部沒有 value 變數後,會在foo 函式執行的環境中繼續查詢,也就是在bar 函式中查詢,很幸運我們找到了。

思考

最後,讓我們看一個《JavaScript權威指南》中的例子:

// 例1:
var scope = "global scope";
function checkscope(){
  var scope = "local scope";
  function f(){
    return scope;
  }
  return f();
}
checkscope();

// 例2:
var scope = "global scope";
function checkscope(){
  var scope = "local scope";
  function f(){
    return scope;
  }
  return f;
}
checkscope()();

讓我們來分析一下上面例1的程式碼:

  • 1、定義一個變數 scope 並賦值 global scope;
  • 2、宣告一個函式 checkscope ,在這個函式中 定義一個變數 scope 並賦值 local scope;
  • 3、在checkscope 函式中 又定義一個函式 f ,這個函式 只做了一件事:返回scope 這個變數;
  • 4、最後返回並執行 f 這個函式;
  • 5、呼叫checkscope

按照我們上面解釋的javaScript中靜態作用域理解,在執行 checkscope 這個函式的時候在函式內部執行的是f 這個函式,首先在 f 這個函式內部查詢 scope 這個變數發現沒有,繼續在定義函式f的上面一層查詢,發現在checkscope 這個函式作用域內 找到了scope的值 直接返回,至於 checkscope外面定義的scope沒有理睬。

讓我們來分析一下上面例2的程式碼:

  • 1、定義一個變數 scope 並賦值 global scope;
  • 2、宣告一個函式 checkscope 在這個函式中 定義一個變數 scope 並賦值 local scope;
  • 3、在checkscope 函式中 又定義一個函式 f 這個函式 只做了一件事:返回scope 這個變數;
  • 4、最後單純的返回 f 這個函式;
  • 5、呼叫checkscope

按照我們上面解釋的javaScript中靜態作用域理解,在執行 checkscope 這個函式的時候在函式內返回了函式f實際是在最外面呼叫的f但是由於javaScript是採用的詞法作用域,因此函式的作用域基於函式建立的位置。

而引用《JavaScript權威指南》的回答就是:

JavaScript 函式的執行用到了作用域鏈,這個作用域鏈是在函式定義的時候建立的。巢狀的函式 f() 定義在這個作用域鏈裡,其中的變數 scope 一定是區域性變數,不管何時何地執行函式 f(),這種繫結在執行 f() 時依然有效。

但是在這裡真正想讓大家思考的是:

雖然兩段程式碼執行的結果一樣,但是兩段程式碼究竟有哪些不同呢?

敬請期待下面一篇關於javaScript 中的執行上下文棧的相關內容。

參考:

相關文章