JavaScript中的變量提升

在 ES6 之前,JavaScript 沒有塊級作用域(一對花括號{}即爲一個塊級作用域) ,大致分爲 全局作用域 和 函數作用域 。變量提升即將變量聲明提升到它所在 作用域 的 最開始 的部分。 在 JavaScript 代碼運行之前其實是有一個 編譯階段 的。編譯之後纔是 從上到下 ,一行一行解釋執行。變量提升就發生在 編譯階段 ,它把 變量 和 函數 的聲明提升至作用域的頂端。(編譯階段的工作之一就是將變量與其作用域進行關聯)。我先分開介紹變量提升和函數提升到後面再放到一起比較。 如果想更深入的瞭解 產生變量提升的原因

注意

同一個變量只會 聲明一次 ,其它的會被覆蓋掉。
變量提升/函數提升 是提升到 當前作用域 的頂部,如果遇到特殊的 if(){}/try-cache 作用域,同時也會把也會提升到 特殊作用域 的外部。
函數提升 的優先級是高於 變量提升 的優先級,並且 函數聲明 和 函數定義 的部分一起被提升。
變量提升
我們直接從代碼從最基礎的開始

console.log(a); // undefined
var a = 2;
複製代碼
相信這個大家知道,上面代碼其實就是

var a;
console.log(a); // undefined
a = 2;
複製代碼
他會提前聲明 a,但是不會給 a 賦值。 但是如下代碼會怎麼執行呢?

console.log(a); // Uncaught ReferenceError: a is not defined
a = 2;
複製代碼
如果沒有通過 var 聲明值類型的就不會存在變量提升,而是會報錯。

函數提升
聲明函數 有兩種方式: 一種是 函數表達式 ,另一種是 函數聲明 。

函數表達式
console.log(aa) // undefined
var aa = function () {};

/** 代碼分解 ***/
var aa;
console.log(aa);
aa = function () {};
複製代碼
函數表達式 和 變量 的提升效果基本上是一致的,它會輸出 undefined 。

函數聲明
它和 函數表達式 是有點不一樣的,在沒有 {}作用域 時它們表現是一致的。表現一致的例子

console.log(a); // function a () {}
function a() { };

/** 代碼分解 ***/
function a() { };
console.log(a);
複製代碼
那如果 變量提升 和 函數提升 同時存在,誰先誰後呢? 我們根據上面的注意事項 1 和 3 可以得出結果,根據實例來分析一下。 請看下面的例子:

console.log(aa); // function aa () {}
var aa = 'aaaa';
function aa () {};
console.log(aa); // aaaa

/** 代碼分解 ***/
var aa; // 只會聲明一次的變量
function aa () {}; // 變量別覆蓋爲 aa 字面量函數
console.log(aa); // function aa () {} 輸出字面量函數
aa = 'aaaa'; // aa 重新被覆蓋爲 'aaaa'
console.log(aa); // aaaa 輸出最後的覆蓋值
複製代碼
其實我們可以通過 chrome 瀏覽器調試效果大致如下圖所示:
JavaScript中的變量提升
到這裏就大致知道 變量提升 、 函數提升 它們的大致過程和它們之間的 優先級 。下面我們來說一下它們和 塊級作用域 和 函數作用域
的關係。

作用域
在 ES6 出現之後作用域變得很複雜,有太多種了,這裏只說和本篇文章相關的幾種作用域。我們只看 全局作用域 、 詞法作用域 、 塊級作用域 、 函數作用域 這四種作用域。 全局作用域 基本上沒什麼好說的,上面的樣例基本上都是 全局作用域 ,這裏就不做多的贅述。

詞法作用域/函數作用域
詞法作用域: 函數在定義它們的作用域裏運行,而不是在執行它們的作用域裏運行。 我們直接通過一個例子來分析一下:

在有 作用域 時,我們來看一下 函數聲明 的表現,還是通過一個實例來分析一下,代碼如下:

console.log(aa); // 如果直接輸入 會報錯 VM1778:1 Uncaught ReferenceError: a is not defined
複製代碼
下面修改代碼來分析在 函數作用域 中 函數聲明 的特殊表現。

console.log(aa); // undefined
var aa = 'aaaa';
console.log(aa); // aaaa
function test () {
console.log(aa); // undefined
var aa = 'bbbb';
console.log(aa); // bbbb
}
test();

/** 代碼分解 ***/
var aa;
console.log(aa); // undefined
aa = 'aaaa';
console.log(aa); // aaaa
function test () {
var aa;
console.log(aa); // undefined
aa = 'bbbb';
console.log(aa); // bbbb
}
test();
複製代碼
全局聲明瞭一個名字叫做 aa 的變量,它被提升全局域的頂部聲明,而在 test 函數中我們又聲明瞭一個變量 aa ,這個變量在當前 函數作用 的頂部聲明。在函數的執行的階段,變量的讀取都是就近原則,先從當先的 活動對象 或 作用域 查找,如果沒有才會從 全局對象 或 全局作用域 查找。

稍微加大一點難度,修改代碼如下:

console.log(aa); // undefined
var aa = 'aaaa';
console.log(aa); // aaaa
function test () {
console.log(aa); // aaaa
aa = 'bbbb';
console.log(aa); // bbbb
}
test();

/** 代碼分解 ***/
var aa;
console.log(aa); // undefined
aa = 'aaaa';
console.log(aa); // aaaa
function test () {
console.log(aa); // aaaa
aa = 'bbbb';
console.log(aa); // bbbb
}
test();
複製代碼
我們把 test函數 內部的 var aa = 'bbbb' 修改爲 aa = bbbb ,這樣就 不存在變量提升 只是一個簡單 變量覆蓋賦值 。

塊級作用域
在 ES6 中新增了 塊級作用域 ,我們可以通過 let/const 來創建 塊級作用域 ,只能在當前 塊中訪問 通過 let/const 聲明的變量。 我們簡單的瞭解一下 let 和 塊級作用域 ,請看下方的代碼:

if (true) {
// console.log(aa); // VM439541:1 Uncaught SyntaxError: Identifier 'aa' has already been declared
let aa = 'aaa';
}
console.log(aa); // VM439096:4 Uncaught ReferenceError: aa is not defined
複製代碼
在 if條件語句 內部通過 let aa = 'aaa' 中的 let 關鍵字創建了一個 塊級作用域 ,所以我們在外面不能訪問 aa 變量。

console.log(aa); // VM440010:1 Uncaught ReferenceError: aa is not defined
let aa = 'aaa';
複製代碼
let 聲明的變量同時存在 DTZ(暫時性死區) ,在 let 聲明變量之前使用這個變量,會觸發 DTZ(暫時性死區) 報錯。

let aa = 'aaa';
let aa = 'aaa';
// Uncaught SyntaxError: Identifier 'aa' has already been declared
複製代碼
let 不能多次聲明同一個變量,不然會報錯。

if判斷/try-cache
if(){}/try-cache(){} 它們算一個作用域嗎?我們通過下面的例子一步一步的分析它們,我們以 if 爲分析樣例請看代碼:

console.log(aa) // undefined
if (true) {
var aa = 10;
}
console.log(aa); // 10

/代碼分析/
var a;
console.log(aa); // undefined
if (true) {
aa = 10;
}
console.log(aa); // 10
複製代碼
在 變量提升 時 if 是 不存在作用域 的,它的作用域就是全局作用域。那如果是 函數提升 呢? if會存在作用域 嗎? 通過下面這個實例我們大概會了解 函數提升 和 if 的關係:

console.log(aa); // undefined
if (true) {
console.log(aa); // function aa () {}
function aa () {};
console.log(aa); //function aa () {}
}

/代碼分析/
var aa;
console.log(aa); // undefined
if (true) {
function aa () {};
console.log(aa); // function aa () {}
console.log(aa); //function aa () {}
}
複製代碼
我們通過這個可以看到當前執行的結果和上面所描述的 函數提升 表現並不一致,它只是提升了 aa 的聲明,賦值只是發生在 if 內部的,這也是 函數提升 在 if 中特異的表現。再來一個更特異的 if 和 函數提升 。

var aa = 'aaaa';
if (true) { // 執行序號 5
console.log(aa); //
執行序號 6
aa = 1; // 執行序號 7
function aa () {} //
執行序號 8
console.log(aa);
}
console.log(aa);
/代碼分析 執行順序/
var aa;
aa = 'aaaa';
if (true) {
function aa () {}
console.log(aa); // function aa () {}
aa = 1;
// function aa () {} 再執行一遍
console.log(aa); // 1
}
console.log(aa); // 1 ?這個確定對?
複製代碼
我們主要觀察 if 內部的 aa = 1; function aa () {} 的順序,在當前代碼中 第二個console.log(aa) 會輸出一個 1 ,如果我們把 aa = 1; function aa () {} 改爲 function aa () {}; aa = 1; 它外部的 console.log(aa) 就會變化,看代碼:

var aa = 'aaaa';
if (true) { // 執行序號 1
console.log(aa); // function aa () {}
執行序號 2
function aa () {} // 執行序號 3
aa = 1; //
執行序號 4
console.log(aa); // 1
}
console.log(aa); // function aa () {}

/代碼分析 執行順序/
var aa;
aa = 'aaaa';
if (true) {
function aa () {}
console.log(aa); // function aa () {}
// function aa () {} 再執行一遍
aa = 1;
console.log(aa); // 1
}
console.log(aa); // function aa () {} ?這個確定對?
複製代碼
如果是按上面分析的代碼執行順序是相同的,但是爲什麼結果不太相同,這種資料不太好找,我們直接上代碼去 chrome 中調試一下代碼就一清二楚了,大致調試過程如下:

function aa () {}; aa = 1; 執行過程

執行序號1時: 進入 if 內部執行,在 scope 中會多出來一個 block ,也就是在 作用域鏈 中會多出來一個 block ,這個作用域中有 aa = function aa () {} 。如下圖所示:
JavaScript中的變量提升
這個時候 block 是 function aa() {} 而全局的 window.aa 現在還是 aaaa
執行序號2時: 執行 console.log(aa) ,這個只是一個輸出語法並不會改變變量的值,執行效果沒有變。
JavaScript中的變量提升

執行序號3時: 執行 function aa() {} , 我們可以看到 block 和 全局作用域 的 aa 變量都改變爲 function aa () {} ,如下圖所示:
JavaScript中的變量提升

執行序號4時: 它會執行的代碼 aa = 1 ,這個時候根據作用域鏈的規則,就近獲取和修改變量。所以 block 內的 aa = 1 ,而全局變量 window.aa = function aa () {} 如下圖所示:
JavaScript中的變量提升
aa = 1; function aa () {}; 執行過程
執行序號5時: 進入 if 內部執行,在 scope 中會多出來一個 block ,也就是在 作用域鏈 中會多出來一個 block ,這個作用域中有 aa = function aa () {} 。如下圖所示:
JavaScript中的變量提升
這個時候 block 是 function aa() {} 而全局的 window.aa 現在還是 aaaa
執行序號6時: 執行 console.log(aa) ,這個只是一個輸出語法並不會改變變量的值,執行效果沒有變。
JavaScript中的變量提升

執行序號7時: 執行 aa = 1 , 我們可以看到 block 作用域的變量 aa 被賦值爲了 1 ,而 全局作用域 中的變量 aa 還是 aaaa 。如下圖所示:
JavaScript中的變量提升

執行序號8時: 它會執行的代碼 function aa() {} ,當前代碼執行完成時,我們會發現 全局作用域 中的變量 aa 也被賦值爲 1 . 如下圖所示:
JavaScript中的變量提升
aa = 1; 執行過程 當沒有 function aa () {}; 函數聲明時,我們會發現不會產生一個臨時的 block 作用域,也不會存在奇特的現象。
綜合上面三個實例中我們可以得出以下的結論:

在 if 內部包含了 函數聲明 會在內部產生一個 block作用域 ,在不包含時不會產生 block作用域 。
在當前 if 外部存在和 函數聲明 相同的 變量名稱 時,當執行到 函數聲明 時同時會更新外部 函數作用域or全局作用域 中變量的值,只更新當前執行的這一次。
我們再來一個例子來證明我們得到的結論,例子如下:

function test () {
// debugger
var aa = 'aaaa';
if (true) {
console.log(aa); // 第一個 ƒ aa () {}
aa = 1;
function aa () {}
console.log(aa); // 第二個 1
}
console.log(aa); // 第三個 1
}
test()
console.log(aa) // 第四個 VM5607:13 Uncaught ReferenceError: aa is not defined
複製代碼
第一個 console.log(aa) 會輸出 ƒ aa () {} ,因爲 函數聲明 的提升和賦值都會放到 if 的內部。同時會產生一個 block作用域 。
第二個 console.log(aa) 會輸出 if 內部中的 aa = 1 ,因爲 a = 1 會把 if 產生的 block作用域 中的變量 aa 修改爲了 1 。
第三個 console.log(aa) 會輸出 test函數作用域 中的 aa = 1 ,因爲在執行 function aa () {} 是都會更新外部變量 aa 的值爲 1 ,也就是 test函數作用域 中的 aa = 1 ;
第四個 console.log(aa) 會輸出 全局作用域 中的 aa ,因爲從來沒有聲明過全局變量 aa 所以會報錯, is not defined 。
來兩道題
來兩道題加深一下印象。

第一道題
var a = function() {
console.log(1);
};
var a = function() {
console.log(2);
};
var a;
console.log(a);
a = 1;
console.log(a);
a = 2;
console.log(a);
console.log(typeof a);
複製代碼
如果只能答出來就沒有必要看了。

如果變量提升遇到函數提升,那個優先級更高呢,看下面的代碼。

console.log(a); // function a () {console.log(1);}
var a = 1;
function a() {
console.log(1);
}
console.log(a); // 1
複製代碼
看上面的代碼知道 函數提升 是 高於變量提升 的,因爲在 javascript 中函數是一等公民, 並且不會被變量聲明覆蓋 ,但是會被 變量賦值覆蓋 。其實代碼如下

var a = function() {
console.log(1);
};
var a;
console.log(a); // function a () {console.log(1);}
a = 1;
console.log(a); // 1
複製代碼
我們再來一個稍微複雜一點的,代碼如下:

console.log(a); // function a () {console.log(2);}
var a = 1;
function a() {
console.log(1);
}
console.log(a); // 1
var a = 2;
function a() {
console.log(2);
}
console.log(a); // 2
console.log(typeof a); // number
複製代碼
在多次函數提升的會後一個覆蓋前一個,然後纔是變量提升,其實代碼如下:

var a = function() {
console.log(1);
};
var a = function() {
console.log(2);
};
var a;
console.log(a); // function a () {console.log(2);}
a = 1;
console.log(a); // 1
a = 2;
console.log(a); // 2
console.log(typeof a); // number
複製代碼
第二道題
第二道題會比第一道題難一點點,代碼如下:

console.log(aa);
var aa = 'aaa';
if (true) {
console.log(aa);
aa = 1;
function aa () {}
aa = 2;
console.log(aa);
}
console.log(aa);
複製代碼
如果上面的內容看懂了,大概這個題就會感覺很簡單,大致過程如下:

第一個 console.log(aa) 會輸出 全局作用域 中的 aa 值爲 undefined ,因爲 var aa = 'aaa' 會產生變量提升,會把 var aa; 放到全局作用域中的頂端,所以會輸出 undefined 。
第二個 console.log(aa) 會輸出 if 內部中的 aa = ƒ aa () {} , if 內部執行產生 block作用域 ,並且 block作用域 內部的 ƒ aa () {} 被提升到頂部,所以會輸出 ƒ aa () {} 。
第三個 console.log(aa) 會輸出 block作用域 中的 aa = 2 ,因爲在執行 function aa () {} 是都會更新外部變量 aa 的值爲 1 ,也就是 全局作用域 中的 aa = 1 ;
第四個 console.log(aa) 會輸出 全局作用域 中的 aa ,因爲在上一步中我們知道了 全局作用域 中的 aa = 1 ,所以會輸出 1 。
undefined
ƒ aa () {}
2
1
複製代碼
到此結束JavaScript中的變量提升,如果發現本篇文章沒有涉及的變量提升的知識點和錯誤的地方,請大家多多指正、探討。

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