開篇
當我們在開始學習任何一門語言的時候,都會接觸到變量的概念,變量的出現其實是爲了解決一個問題,爲的是存儲某些值,進而,存儲某些值的目的是爲了在之後對這個值進行訪問或者修改,正是這種存儲和訪問變量的能力將狀態給了程序。我們的程序中到處都充斥着對於狀態的判斷,根據不同的狀態執行不同的邏輯。
我們試想一下,如果沒有狀態這個概念,程序雖然也能夠執行一些簡單的任務,但是它會受到很多的限制,所能完成的功能是有限制的,舉個例子,沒有狀態你是如何執行循環語句?沒有狀態如何更加優雅地使用邏輯結構?
仔細想想,好像是寸步難行,當然引入變量後幫我們解決了這個問題。
但是,引入變量和狀態的概念之後會引起幾個問題:這些變量住在哪裏?換句話說,它們存儲在哪裏?最重要的是,程序需要它們的時候如何找到它們?
今天我們就一起學習一下這套存儲和查找變量的規則,這套規則我們稱之爲:作用域。
作用域
我們來拆解一下這個詞語,所謂的“域”我們可以理解爲:範圍、區域,加上“作用”兩個字所要表述的問題就是作用的範圍、區域,比如國家的行政區域劃分是爲了便於管理,類比到程序源代碼中作用域的出現也是爲了便於對於變量做管理。
好,這裏我們簡單做一下總結:
- 定義:作用域是指程序源代碼中定義變量的區域。
- 作用:作用域規定了如何查找變量,也就是確定當前執行代碼對變量的訪問權限。
- 在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 中的執行上下文棧的相關內容。
參考:
- 1、《你不知道的Javascript上卷》
- 2、JavaScript深入之詞法作用域和動態作用域