JavaScript變量提升(Hoisting)詳解


文章出自個人博客https://knightyun.github.io/2019/09/02/js-hoisting,轉載請申明。


概念

變量提升是 JavaScript 的一種執行機制,大致就是字面意思,將聲明的變量提前,但並不是指在編譯時改變語句的順序,而是將變量提前放入內存中,供後續操作,下面通過實例進行分析;

函數申明

在 JavaScript 中,聲明一個函數並執行的話,通常會是以下形式:

function fn() {
    console.log('run');
}

fn();  // run

上面是正常的思維順序,但是包括其他一些編程語言在內,通常會使用如下形式:

fn();

function fn() {
    console.log('run');
}
// run

這樣做在執行上是沒用問題的,同時可以在包含大量語句和函數申明的情況下,也可以使用這種特性將普通語句和函數申明分開,提高可讀性;

以上情況便是一種常見的提升(Hoisting),即編譯時提前將當前執行上下文包含的申明的函數,提前放入內存中,供全文語句執行時調用,爲了方便理解而抽象成一種提升行爲;

但是如果使用下面的方式申明函數並執行:

fn();

var fn = function() {
    console.log('run');
}
// TypeError: fn is not a function

這裏就沒有像上面一樣的結果了,這屬於下面將介紹的變量提升行爲;

變量申明

當然函數只是一種類型的變量,還存在其他的變量類型,例如考慮以下語句:

var a = 1;
console.log(a);
// 1

邏輯和執行都是正常的,輸出結果也是預期的,但是如果變一下順序:

console.log(a);
var a = 1;

這種情況,通常可能會認爲第一行調用了一個未定義的變量,然後輸出 Uncaught ReferenceError: a is not defined 這樣的錯誤,但是呢,並非如此,輸出信息如下:

undefined

沒錯,就只有一個單獨的 undefined,這種輸出情況就類似於以下代碼的執行:

var a;
console.log(a);
// undefined

從這裏便可以大致分析出,前面的順序怪異的代碼,相當於在編譯時提前將後面出現的變量申明提前,然後執行就輸出了一個已申明但未 初始化(賦值) 的值,這便是其他類型的變量的提升行爲,即在當前執行上下文中,將後面申明的變量提前放入內存,供前面的語句調用;

注意,前面的代碼最後一行的語句是 var a = 1,即對變量進行了申明並賦值,但是最後輸出仍然是 undefined 而不是 1,證明變量提升行爲只會對變量進行申明操作,並不會對其初始化賦值,不管原語句是否有賦值操作;

然後便能解釋之前的代碼:

fn();

var fn = function() {
    console.log('run');
}
// TypeError: fn is not a function

這種情況便是將變量 fn 提升,值爲 undefined,所以執行 fn() 語句會提示 fn is not a function 而不是 fn is not defined,與使用關鍵字 function 申明函數情況不一樣;

var, let, const的區別

JavaScript 中申明變量的方式以及對應效果如下:

a = 0;       // 全局變量
var b = 1;   // 局部作用域變量(當前上下文)
let c = 2;   // 塊級作用域變量(當前塊級上下文)
const d = 3; // 常量

作用域

這裏解釋一下,變量 a 申明時沒有帶任何關鍵字,默認其爲全局變量;變量 b 申明帶有關鍵字 var,爲當前上下文的局部作用域,如果用在全局則爲全局變量;變量 c 使用關鍵字關鍵字 let,d 使用關鍵字 const,二者都是ES6中新增的塊級作用域申明,只不過 const 申明的是常量,值不可更改;

通過例子看一下它們的區別;

var a = '全局';
function fn() {
    var aa = '局部'
    console.log(aa);
}
if (true) {
    var b = '全局';
    let bb = '塊級';
    const bbb = '塊級';
}
for (i = 0; i < 1; i++) {
    var c = '全局'
    let cc = '塊級';
    const ccc = '塊級';
}

console.log(a);  // “全局”
fn();  // “局部”
console.log(aa); // aa is not defined
console.log(b, c); // “全局” “全局”
console.log(bb, cc); // bb is not defined  cc is not defined
console.log(bbb, ccc); // bbb is not defined  ccc is not defined

可以看出,var 的局部限於全局或者函數內部上下文,而 let 和 const 的塊級的意思則是被 塊(block) 所包含的上下文,也就是包含在花括號 {} 內部的作用域中,所以也包括函數在內,加上 if, for, while, switch 等情況,且不能被外部作用域訪問;

全局變量提升

首先看申明全局變量時的提升行爲:

console.log(a);
a = 0;
// ReferenceError: a is not defined

證明不帶關鍵字的申明全局變量,似乎並沒有執行變量的提升行爲,與以下代碼的執行無異:

console.log(a); // a 前面未申明
// ReferenceError: a is not defined

局部變量提升

使用關鍵字 var 申明的情況:

function fn() {
    console.log(aa);
    var aa = 1;
}
console.log(a);
var a = 1;
// undefined
fn();
// undefined

前面已解釋,不再贅述,只是需要注意下面這種情況:

if (false) {
    var a = 1;
}
console.log(a);
// undefined

正常思維可能會理解 if 條件判斷爲假所以不會執行內部語句,最後會輸出 a is not defined,然而並非如此,仍然將申明的變量執行了提升機制;這裏可以簡單理解爲存在即提升,也就是爲了避免以上問題的影響,所以出現了塊級變量申明 letconst

塊級變量提升

使用 letconst 的情況:

if (true) {
    console.log(a);
    let a = 1;
    console.log(aa);
}
// ReferenceError: Cannot access 'a' before initialization
// ReferenceError: aa is not defined

if (true) {
    console.log(b);
    const b = 1;
    console.log(bb);
}
// ReferenceError: Cannot access 'b' before initialization
// ReferenceError: bb is not defined

可以看出,塊級變量申明似乎也執行了類似提升的機制,但是處理卻與 var 有區別,這裏是直接以錯誤的形式處理輸出,提示該變量未進行初始化,而沒有變量的申明語句的情況,則是提示未定義的錯誤,且 letconst 的處理情況一致;


技術文章推送
手機、電腦實用軟件分享
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章