Javascript作用域和變量提升

首先大家看一下下面的代碼,判斷下會輸出什麼結果:

var foo = 1;
function bar() {
    if(!foo) {
        var foo = 10;
    }
    alert(foo);
}
bar();

答案是10,
你是否會疑惑條件語句if(!foo)並不會被執行呀,爲什麼foo會被賦值爲10.

再來看看第二個例子:

var a = 1;
function b() {
    a = 10;
    return;
    function a() {}
}
b();
alert(a);

答案是10嗎?顯然不是,alert輸出了1。
如果你仍然對上面兩個輸出摸不着頭腦,那麼請認真閱讀下面這篇文章。


Javascript作用域

Javascript作用域的問題已經是老生常談了,但是不一定每一個人都能準確理解。
讓我們先來看一下Java中的一個例子:

public int result() {
    int x = 1;
    System.out.println(x);  //1
    if(true) {
        int x = 2;
        System.out.println(x);  //2
    }
    System.out.println(x);  //1
}

程序一次輸出了1、2、1,
爲什麼第3個輸出了1而不是2呢?因爲在Java中,我們有塊級作用域(block-level scope)。在一個代碼塊中的變量不會覆蓋掉代碼塊外面的變量。我們不妨試一下在Javascript中的表現:

var x = 1;
console.log(x);  //1
if(true) {
    var x = 2;
    console.log(x);  //2
}
console.log(x)  //2

結果if代碼塊中的變量覆蓋了全局變量,那是因爲Javascript是一種函數級作用域(function-level scope),所以if中並沒有獨立維護一個scope,變量x影響到了全局變量x。
C/C++/C#和JAVA都是塊級作用域語言,那麼在Javascript中,我們怎麼實現一種塊級作用域的效果呢?答案是閉包:

function foo() {
    var x = 1;
    if(x) {
        (function () {
            var x = 2;
        }());
    }
    x is still 1.

上面代碼在if條件塊中創建了一個閉包,它是一個立即執行函數,所以相當於我們又創建了一個函數作用域,所以此時內部的x不會對外部產生影響。


Javascript變量提升

在Javascript中,變量進入一個作用域可以通過下面四種方式:

  1. 語言自定義變量:所有的作用域中都存在this和arguments這兩個默認變量
  2. 函數形參:函數的形參存在於函數作用域中
  3. 函數聲明:function foo() {}
  4. 變量定義: var foo

其中,在代碼運行前,函數聲明和變量定義通常會被解釋器移動到其所在作用域的最頂部,如何理解這句話呢?

function foo() {
    bar();
    var x = 1;
}

上面這段代碼,被代碼解釋器編譯完後,將變成下面的形式:

function foo() {
    var x;
    bar();
    x = 1;
}

我們注意到,變量x的定義被移動到函數的最頂部,在bar()後,再對其進行賦值。
再來看一個栗子,下面這兩段代碼其實是等價的:

function foo() {
    if(false) {
        var x = 1;
    }
    return;
    var y = 1;
}
function foo() {
    var x,y;
    if(false) {
        x = 1;
    }
    return;
    y = 1;
}

所以變量的上升(Hoisting)只是其定義的上升,而變量的賦值並不會上升。

我們都知道,創建一個函數的方法有兩種,一種是通過函數聲明function foo() {},
另一種是通過定義一個變量 var foo = function() {}
那麼這兩種在代碼執行上有什麼區別呢?

來看下面的栗子:

function test() {
    foo();  //Type error, foo is not a function
    bar();  //will alert 'bar'
    var foo = function() {
        alert('foo');
    }
    function bar() {
        alert('bar');
    }
}
test();

在這個例子中,bar能夠正常調用,而foo調用時報錯了
我們前面說過變量會上升,所以var foo首先會上升到函數體頂部,然而此時的foo尚未定義,爲undefined,所以執行會報錯。
而對於函數bar,函數本身也是一種變量,所以也存在變量上升的現象,但是它是上升了整個函數,所以bar()才能夠順利執行。

再回到我們一開始提出的兩個例子,能理解其輸出原理了嘛

同時這也就是爲什麼,我們寫代碼的時候,變量定義總要寫在最前面。


ES6的變化

在ES6中,存在let關鍵字,它所聲明的變量存在塊級作用域。
而且函數本身的作用域,只存在其所在的塊級作用域之內,例如:

function f() {console.log('I'm outside!')}
if(true) {
    //重複聲明一次函數f
    function f() {console.log('I'm inside!')}
}
f();

上面這段代碼在ES5中的輸出結果爲’I’m inside!’,因爲f被條件語句中的f上升覆蓋了。
而在ES6中的輸出是’I’m outside!’,因爲塊級中定義的函數不會影響到外部。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章