深入學習js系列是自己階段性成長的見證,希望通過文章的形式更加嚴謹、客觀地梳理js的相關知識,也希望能夠幫助更多的前端開發的朋友解決問題,期待我們的共同進步。
如果覺得本系列不錯,歡迎點贊、評論、轉發,您的支援就是我堅持的最大動力。
開篇
當我們在開始學習任何一門語言的時候,都會接觸到變數的概念,變數的出現其實是為了解決一個問題,為的是儲存某些值,進而,儲存某些值的目的是為了在之後對這個值進行訪問或者修改,正是這種儲存和訪問變數的能力將狀態給了程式。我們的程式中到處都充斥著對於狀態的判斷,根據不同的狀態執行不同的邏輯。
我們試想一下,如果沒有狀態這個概念,程式雖然也能夠執行一些簡單的任務,但是它會受到很多的限制,所能完成的功能是有限制的,舉個例子,沒有狀態你是如何執行迴圈語句?沒有狀態如何更加優雅地使用邏輯結構?
仔細想想,好像是寸步難行,當然引入變數後幫我們解決了這個問題。
但是,引入變數和狀態的概念之後會引起幾個問題:這些變數住在哪裡?換句話說,它們儲存在哪裡?最重要的是,程式需要它們的時候如何找到它們?
今天我們就一起學習一下這套儲存和查詢變數的規則,這套規則我們稱之為:作用域。
作用域
我們來拆解一下這個詞語,所謂的“域”我們可以理解為:範圍、區域,加上“作用”兩個字所要表述的問題就是作用的範圍、區域,比如國家的行政區域劃分是為了便於管理,類比到程式原始碼中作用域的出現也是為了便於對於變數做管理。
好,這裡我們簡單做一下總結:
- 定義:作用域是指程式原始碼中定義變數的區域。
- 作用:作用域規定了如何查詢變數,也就是確定當前執行程式碼對變數的訪問許可權。
- 在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() 這個函式
1、假設javaScript採用靜態作用域,讓我們分析下執行過程:
執行foo函式,首先從 foo 函式內部查詢是否有變數 value ,如果沒有 就根據書寫的位置,查詢上面一層的程式碼,我們發現value等於1,所以結果會列印 1。
作用域查詢會在找到第一個匹配的識別符號時停止。作用域查詢始終從執行時所處的最內部作用域開始,逐級向外或者說向上進行,直到遇見第一個匹配的識別符號為止。
無論函式在哪裡被呼叫,也無論它如何被呼叫,它的詞法作用域都只由函式被申明時所處的位置決定。
2、假設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深入之詞法作用域和動態作用域
深入學習JavaScript系列目錄
- #1 【深入學習js之——原型和原型鏈】
- #2 【深入學習js之——詞法作用域和動態作用域】
- #3 【深入學習js之——執行山下文棧】
- #4 【深入學習js之——變數物件】
- #5 【深入學習js之——作用域鏈】
- #6 【深入學習js之——實際開發場景中的this指向】
- #7 【深入學習js之——執行上下文】
- #8 【深入學習js之——閉包】
- #9 【深入學習js之——引數按值傳遞】
歡迎新增我的個人微信討論技術和個體成長。
歡迎關注我的個人微信公眾號——指尖的宇宙,更多優質思考乾貨