從一道面試題了解js作用域及作用域鏈

程序員百里青山 發佈 2024-05-13T15:31:00.657059+00:00

昨天看了一道面試題,沒想到竟然答錯了,特別基礎的問題還能答錯,所以特別記錄一下,因為考的是js的作用域的知識點,所以關於js的作用域也簡單總結一下。

昨天看了一道面試題,沒想到竟然答錯了,特別基礎的問題還能答錯,所以特別記錄一下,因為考的是js的作用域的知識點,所以關於js的作用域也簡單總結一下。

作用域【廢話部分,有基礎直接看面試題部分】

什麼是作用域

啥是作用域呢,簡單的說,就是變量可以生效的地方,就叫做作用域,也叫執行環境,大家也可以理解為變量可以發生作用的地方。舉個栗子,我們國家頒布《刑法》,那麼刑法就相當於一個變量,存儲的值,就是刑法的內容,生效的範圍就是作用域,也就是全國。

全局作用域

全局作用域就是代碼運行時最外圍的執行環境,比如在我國,最大的範圍就是全國,全國就是全局作用域,而在js中,全局作用域被認為是window對象,而在上一篇文章中我們也說到了,在全局作用域中聲明的變量為全局變量,而如果省略聲明關鍵字聲明的變量,默認也為全局變量。就像我們在國家的任何一個地方都需要遵從國家法律一樣,我們在js的任何一個地方也都可以訪問到全局作用域。

局部作用域

假如我們的國家是一個全局作用域,那它下面還有好多個省份和地區,在js
里也一樣,全局作用域下面還有好多小的作用域,我們稱之為局部作用域。在es6之前,js還不支持塊作用域,所以在es6之前所謂的局部作用域就是指的函數作用域,也就是我們聲明一個函數時這個函數內部的作用域。

1-1

如圖所示,函數foo內部就是它所生成的局部作用域,變量bar就是這個局部作用域裡的局部變量。

作用域嵌套與作用域鏈

上面我們說了,聲明一個函數的同時就會創建屬於它的函數作用域,那麼函數可能會存在嵌套的情況,這時候就產生了作用域嵌套,這時候我們執行代碼的話,就會產生一個作用域鏈,作用域鏈的前端,始終都是當前執行的代碼距離最近的作用域。

1-2

如圖所示就是一個嵌套的作用域,js解析變量的時候會遵循自下而上(自內而外)的規則沿著作用域鏈一級一級的查找,直到找到為止,如果查找到全局作用域(window)時依然沒找到,就會報錯。注意,作用域鏈是不可逆的,就是說我們在內層的作用域裡可以訪問外層作用域裡的變量,但是在外層作用域裡不能訪問到內層作用域的變量。可以看下面的例子

var text = 'hello'
function foo() {
    var text1 = 'foo'
    function bar() {
        var text1 = 'bar'
        console.log(text1) // bar
        console.log(text) // hello
        console.log(text3) // text3 is not defined
    }
    bar()
}
foo()

上面的例子中,作用域鏈為 bar作用域 -> foo作用域 -> 全局作用域,列印text1的時候會先在最近的bar作用域中查找,如果可以找到就不會再往外找了,列印text的時候先在最近的bar作用域中查找,沒找到就往foo作用域查找,還沒找到,就往全局作用域查找,找到之後進行輸出。列印text3的時候和前面一樣,按照作用域鏈的順序進行查找,都沒找到,然後報錯。由於作用域鏈是不可逆的,所以我們可以在bar作用域裡訪問全局作用域,但如果我們在全局作用域裡列印text1,則會報錯,因為全局作用域無法訪問foo作用域。

另外要說一點,在我們講this的那一篇文章中說了,this是在函數調用時決定的,在函數被定義時並沒有this。而作用域則剛好相反,作用域是在函數定義時決定的,跟函數在哪裡被調用沒有關係。所以無論我們在哪裡調用函數,都不會改變他的作用域鏈。

塊作用域

上面我們說了,在es6之前,js中是沒有塊作用域的,在es6中,添加了let關鍵字實現了對塊級作用域的支持。那麼什麼是塊級作用域呢,其實就是兩個大括號包裹的作用域。而且在我們日常的代碼中非常常見,比如if語句後跟的大括號,for循環後跟的大括號。那有的同學會說,這不是有塊級作用域嗎,那為什麼又說沒有塊級作用域呢?我們又怎麼區分有沒有塊級作用域呢?其實很簡單,我們來看看代碼就知道了。

if (true) {
    var test = 'hello'
}
console.log(test) // hello

看,我們在大括號外也訪問到了大括號裡面的變量,上面我們說了,在局部作用域(塊級作用域也屬於局部作用域)外面是訪問不到作用域裡面的變量的,所以這裡的『塊作用域』其實並沒有真正的形成作用域,只不過是徒有其表罷了,這樣子的危害就是容易污染全局作用域,而且容易給我們造成一定程度上的誤解。比如下面這樣子。

var index = 5
for (var index = 0; index < 10; index++) {
    /**/
}
console.log(index) // 10

很明顯,上面代碼中for循環中的index污染了全局作用域中的index,如果我們不小心的話很容易造成意想不到的後果,當然我們也可以儘量小心的去給變量命名,細心的檢查代碼,或者使用try...catch(感興趣可以去搜一下,或者看js高級程序設計)去實現塊作用域,以便代碼如我們想像般的運行,可那樣就會花費更多的精力,好在es6推出了let關鍵字,從代碼層面支持了塊作用域,減少了我們很多的工作量,來看看let的效果

var index = 5
for (let index = 0; index < 10; index++) {
    /**/
}
console.log(index) // 5

看,代碼完全按照我們想像那樣執行,let聲明的變量支持塊作用域,僅在塊作用域內可訪問,不會影響全局變量。

相關面試題

下面我們就來看看很經典的一道面試題

for (var index = 0; index < 6; index++) {
    setTimeout(function(){
        console.log(index)
    })
}

知道了塊作用域再理解這道題就很簡單了吧,因為這裡用的是var關鍵字,所以這裡沒有塊作用域,也就是說這裡的index其實是一個全局變量,然後每次對index進行++的操作其實都是操作的同一個變量——全局變量index,然後我們裡面又用的是setTimeout,一個異步函數,雖然我們這裡沒有設置定時時間,但它還是一個異步函數,需要等到for循環全部結束後才會運行,這時候index就已經是6了,所以會列印出來6個6。那如果我們把var換成let呢?

for (let index = 0; index < 6; index++) {
    setTimeout(function(){
        console.log(index)
    })
}

列印結果:0 1 2 3 4 5

完全符合我們的預期,這裡我們使用的是letlet聲明的變量支持塊作用域,也就是僅在當前作用域內有效,所以這裡我們循環中的每一個setTimeout引用的index其實都是單獨的變量,互不影響。

很多人都知道,上面的問題除了用let還有另一種方法可以解決,就是立即執行函數

for (var i = 0; i < 5; i++) {
    (function (i) {
        setTimeout(function () {
            console.log(i)
        })
    })(i)
}

列印結果:0 1 2 3 4 5

這樣子也可以符合預期,但如果我們把這道題再改一下呢

for (var i = 0; i < 5; i++) {
    (function (i) {
        setTimeout(function (i) {
            console.log(i)
        })
    })(i)
}

這時候會列印什麼呢,答案是5個 undefined,我最開始也有點懵,為什麼呢,但仔細一看其實很簡單,因為setTimeout裡面那個未命名函數也有自己的作用域,它接收一個參數i,其實就是在自己的作用域裡定義了一個空的變量i,所以列印的時候在當前作用域裡可以找到變量i,它就不會再繼續往外找了,又因為找到的變量i是空值,所以會是undefined。

關鍵字: