1. 程式人生 > 實用技巧 >JavaScript中作用域鏈的概念及用途

JavaScript中作用域鏈的概念及用途

引言

之前我寫過一篇關於JavaScript中的物件的一篇文章,裡面也提到了作用域鏈的概念,相信大家對這個概念還是沒有很深的理解,並且這個概念也是面試中經常問到的,因為這個概念實在太重要了,在我們平時寫程式碼時,也可能會因為作用域鏈的問題,而出現莫名其妙的bug,導致我們花費大量的時間都查找不出原因。所以我就準備單獨寫一篇關於作用域鏈的文章,來幫大家更好地理解這個概念。正文

一、執行環境

首先,我們要引入一個概念,叫做執行環境(下面簡稱環境)。在一個執行環境中,有一個與之關聯的變數物件(下面簡稱物件),在該物件中,儲存著這個執行環境中定義的變數和函式。但這個物件只是個形式上的物件,並不能被外界所訪問到。

例如,在瀏覽器中,我們在全域性執行下列程式碼,那麼當前的執行環境就是window,也就是全域性,並且與該全域性環境關聯的物件中儲存著定義的變數fruit

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <script>
        let fruit = 'banana'
        alert(fruit)
    </script>
</body>
</html>

那麼,在JavaScript中,函式也會形成一個環境,例如下列的程式碼中,函式的內部就是一個區域性的環境,與該環境關聯的物件中儲存著變數my_favorite;而此時全域性環境window中,也有一個與之關聯的物件,該物件中儲存著變數fruit和函式fn1

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <script>
        let fruit = 'banana'
        alert(fruit)
        function fn1() {
            let my_favorite = 'apple'
            return my_favorite
        }
        fn1()
    </script>
</body>
</html>

二、作用域鏈

看了上面兩個例子,我們對執行環境應該有了一定的瞭解,那麼這裡就將引入作用域鏈的概念了,當代碼執行在一個環境中時,會針對環境中儲存變數和函式的物件建立一個作用域鏈,作用域鏈的最前端就是當前環境的物件,如果當前環境是個函式,則作用域鏈的下一部分就是全域性的window環境的變數物件

先來看一下程式碼部分

<script>
    let fruit = 'banana'
    function fn() {
        let color = 'red'
        //返回  '我最喜歡的水果是banana,我最喜歡的顏色是red'
        console.log('我最喜歡的水果是' + fruit + ',我最喜歡的顏色是' + color)
    }
    fn()
    //報錯, color is undefined
    console.log('我最喜歡的水果是' + fruit + ',我最喜歡的顏色是' + color)
</script>

首先執行了函式fn,此時函式內的作用域鏈就是這樣的

我們看到,在函式fn中,我們使用了變數fruit和color,所以此時會從作用域鏈的頭部開始,從第一個活動變數(本例中第一個變數物件就是函式fn的活動變數)中,尋找變數fruit和color,發現該變數物件中存在變數color,於是就成功引用了變數color,但是因為沒有找到變數fruit,於是再沿著作用域鏈往下找到下一個變數物件(本例中第二個活動變數就是全域性window的變數物件),發現該變數物件中有我們想要的變數fruit,則引用該變數fruit,同時,因為找到了需要引用的變數,就不會繼續沿著作用域鏈繼續向下尋找了。我們再來看在函式外,也就是全域性window中,也執行了console.log('我最喜歡的水果是' + fruit + ',我最喜歡的顏色是' + color),此時在全域性環境中的作用域鏈是這樣的

此時也使用了變數 fruit 和 color,所以這時會從作用域鏈的頭部開始,找到第一個變數物件(本例中第一個活動變數就是window全域性變數物件),發現該變數物件中有變數 fruit,所以成功引用該變數物件中的 fruit,但因為沒找到變數 color,所以繼續沿著作用域鏈向下尋找下一個活動變數,但此時已經找到了作用域鏈的尾部,並沒有別的變數物件了,所以也就無法找到變數 color了,所以最後返回的就是 undefined。在本例中我們可以看到,當代碼處於全域性環境中時,是沒有訪問函式fn執行環境中的變數color的權力的,這裡我們可以這種現象看成是變數color的作用域只是在函式fn的執行環境內。

三、塊級作用域

在JavaScript中是沒有塊級作用域的,也就是說,由花括號或小括號封閉起來的區域內沒有自己的作用域,例如這兩個例子

if(true) {
    var fruit = 'banana'
}
console.log(fruit)    //返回  banana

我們可以看到,if語句中的花括號內,使用var定義了一個變數fruit,按照作用域鏈的規則來說,外部是無法訪問到該變數的,但是我們可以看到,確實返回了這個變數的值banana。

再來看下一個例子

for(var i=0; i<4; i++) {
    alert(i)
}
console.log(i)    //返回4

在使用for語句時,我們在小括號裡使用var定義了一個臨時變數i,同樣的的,在for迴圈結束以後,在外部訪問該變數,也成功返回了相應的值。

以上兩個例子,都是因為JavaScript沒有塊級作用域引起的,所以有時會因為這種情況,導致一些不必要的麻煩。在ES6中,出現了使用let和const宣告變數的方式,來解決了JavaScript中沒有塊級作用域的問題。

四、其他情況

其實,還有一種情況,會影響變數的訪問順序,那就是在宣告變數時,直接給一個未宣告的變數賦值,例如這樣

function fn() {
    sum = 1 + 2
}
fn()

console.log(sum)      //返回 3

按照我們本文前面講解的作用域鏈的知識,當執行到最後一局程式碼時,此時處於全域性執行環境中,查詢不到變數sum,所以應當會報錯undefined,但這裡卻返回了 3。

這是因為,在我們使用var宣告變數時,會自動將該變數放到離該程式碼最近的活動變數中去,也就是函式fn的活動變數中,所以在全域性執行環境中的程式碼就無法訪問到該變數。但是如果不使用var,而是像這個例子中一樣,直接給一個未定義的變數賦值,這時會自動地將該變數放到全域性的活動變數中去,這就是導致本例中在全域性環境中還能訪問到變數sum的原因。

品牌vi設計公司http://www.maiqicn.com 辦公資源網站大全https://www.wode007.com

五、總結

  1. 作用域鏈可以看成是將變數物件按順序連線起來的一根鏈子
  2. 每個執行環境中的作用域鏈都是不同的
  3. 當我們引用變數時,會順著當前執行環境的作用域鏈,從作用域鏈的開頭開始依次往下尋找對應的變數,直到找到作用域鏈的尾部,報錯undefined
  4. 作用域鏈保證了變數的有序訪問