博客 / 詳情

返回

JS中的塊作用域和遮蔽效應

JS中的代碼塊是什麼?

我們的程序是由一條條語句構成的,語句是按照自上而下的順序一條條執行的。代碼塊(block)也被稱為複合語句(coumpound statement),其作用是通過“{ }”將多條語句組合在一起。

{
    // 複合語句
    var a = 10;
    console.log(a);
}

那麼為什麼要將多條語句組合在一起呢?

因為我們可以將多條語句“打包”在一起,放在一個JS只期待單一語句的地方。這句話看上去有些晦澀,但是配合代碼理解很容易:

在下面代碼中,條件判斷語句if (true) 後面,可以只寫一條語句,不寫{ }也可以正常運行。

if (true) console.log('hello'); // 只寫一行語句,程序也可以正常運行

// 輸出 hello

但是如果我們想在條件判斷後做更多事情,就需要用到{ },在這個JS只期待單一語句的地方放上更多語句:

if (true) {
    console.log('hello');
    console.log('world');
    console.log('bye'); 
}

塊級作用域是什麼?

塊級作用域(block scope)指的是大括號中的所有變量和函數只可以在這個大括號包含的區域中訪問到,在括號外是訪問不到的。上代碼:

{
    var a = 10; // var處於全局作用域
    let b = 20;
    const c = 30; // let和const都處於代碼塊作用域
    console.log(a); 
    console.log(b); 
    console.log(c); 
}
console.log(a); 
console.log(b); 
console.log(c); 

將上面的代碼放到調試工具中,可以看到var和let、const聲明的變量屬於不同的作用域。當代碼在大括號中執行時,程序可以訪問到2個作用域(代碼塊和全局)中的變量,如下圖:

但是當代碼執行到大括號之外,就會報錯,因為當代碼塊中的語句執行完後,代碼塊作用域就會消失(從棧中彈出),因此只留下一個全局作用域,其中只綁定了一個變量a,無法訪問到變量b和c:

因此,該程序最終在控制枱的輸出結果是:

由於在塊級作用域外打印b出錯,程序會在此處停止運行,不過可想而知,打印c也會出現同樣的錯誤。


遮蔽效應是什麼?

不同作用域中相同名稱的變量就會觸發遮蔽效應(shadowing)。

var的遮蔽效應

看代碼,猜結果:

var a = 100;
{
    var a = 10;
    let b = 20;
    const c = 30;
    console.log(a);
    console.log(b);
    console.log(c);
}

控制枱打印如下:

在大括號中的變量a會使得第1行聲明的變量a無效,這就叫遮蔽效應。因為這兩處a指向的都是全局作用域中的a。

那麼,如果在括號外再打印一下a呢?

var a = 100;
{
    var a = 10;
    let b = 20;
    const c = 30;
    console.log(a);
    console.log(b);
    console.log(c);
}
console.log(a); // 猜結果

讓我們來到調試工具中加入斷點:

當程序執行完第1行時,這時全局變量a的值為100:

當程序執行完第3行時,全局變量a的值被二次聲明的var a = 10重新賦值為10,這兩行語句中的var a指向的都是同一個全局變量a:

因此,控制枱打印如下:

let的遮蔽效應

看代碼,猜結果:

let b = 100;
{
    var a = 10;
    let b = 20;
    const c = 30;
    console.log(a);
    console.log(b);
    console.log(c);
}
console.log(b);

控制枱打印如下:

在大括號中的變量b同樣會使第1行聲明的變量b無效,形成變量遮蔽。但是與var不同的是,let擁有塊級作用域,因此最後一行的b打印出來的仍是第1行的聲明的b。

讓我們再次進入調試工具中加上斷點:

可以清楚的看到,程序中共存在3個作用域,1個是全局作用域,剩下2個是塊級作用域(代碼塊和腳本)。儘管第1行的let聲明處於全局,但是let關鍵詞會自己創造一個塊級作用域(圖中的腳本),處於內存中獨立的空間,儲存了值為100的變量b。在大括號中的變量b,處於內存中另一個獨立的空間,儲存了另一個值為20的變量b。

因此,程序第7行的b訪問的是代碼塊中的變量b,第10行訪問的是腳本(也可以理解成另一個代碼塊)中的變量b。

const的遮蔽效應

const的遮蔽效應和let一致,將之前的代碼稍作修改:

const c = 100;
{
    var a = 10;
    let b = 20;
    const c = 30;
    console.log(a);
    console.log(b);
    console.log(c);
}
console.log(c);

控制枱打印如下:

函數中的遮蔽效應

遮蔽效應不僅發生在塊級作用域中,也發生在函數中。看代碼:

const c = 100;
function x () {
    const c = 30;
    console.log(c);
}
x();
console.log(c);

控制枱打印如下:


非法遮蔽是什麼?

如果使用var關鍵字觸發遮蔽效應,是完全可行的,如下:

var a = 100;
{
    var a = 20;
    console.log(a);  // 20
}

同理,使用let關鍵字觸發遮蔽效應也是有效的:

let a = 100;
{
     let a = 20;
    console.log(a);  // 20
}

但是如果用var去遮蔽let,在程序運行之前就會報錯:

let a = 100;
{
    var a = 20;
    console.log(a);  // 語法錯誤
}

那麼,如果用let去遮蔽var是否也會報錯呢?看代碼:

var a = 100;
{
    let a = 20;
    console.log(a); 
}

控制枱打印結果如下:

程序並不會報錯。那麼為什麼用var去遮蔽let就會報錯呢?

因為遮蔽效應的原則是:重複聲明的語句不可以超出自己所處的作用域。也就是,當我們使用var在塊級作用域中重新聲明變量a時,因為var只有全局作用域或者函數作用域,因此塊級作用域無法限制var的聲明,這時var a就會超出自己所在的作用域,而let關鍵字又不允許在同一個作用域中重複聲明,因此程序報錯。

因此,只需要將上面代碼中的塊級作用域改成函數作用域,程序就不會報錯了:

var a = 100;
function x () {
    var a = 20;
    console.log(a); 
}
x(); // 20
console.log(a); // 100

用const觸發遮蔽效應和let的情況同理,因此就不贅述了。

const b = 100;
{
    const b = 20;
    console.log(b); // 20
}

塊級作用域和詞法作用域

詞法作用域就是定義在詞法階段的作用域。換句話説,詞法作用域是由你在寫代碼時將變量和塊作用域寫在哪裏來決定的。

詞法作用域的變量查找規則是:如果在當前作用域中找不到該變量,就會去上一層作用域中進行查找,以此類推。塊級作用域也享有同樣的查找規則:

const a = 20;
{
    const a = 100;
    {
        const a = 200;
        console.log(a); // 200
    }
}
const a = 20;
{
    const a = 100;
    {
        const a = 200;
    }
}
console.log(a); // 20
const a = 20;
{
    const a = 100;
    {
        console.log(a); // 100 往上一級作用域進行查找
    }
}

以上就是JavaScript中塊級作用域和遮蔽效應的知識。

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.