var a = 1;
function foo() {
if (!a) {
var a = 2;
}
alert(a);
};
foo();
上面這段代碼在運作時會産生什麼結果?
盡管對于有經驗的程式員來說這隻是小菜一碟,不過我還是順着初學者常見的思路做一番描述:
- 建立了全局變量
,定義其值為a
1
- 建立了函數
foo
- 在
的函數體内,foo
語句将不會執行,因為if
會将變量!a
轉變成布爾的假值,也就是a
false
- 跳過條件分支,
變量alert
,最終的結果應該是輸出a
1
嗯,看起來無懈可擊的推理啊,但讓人驚訝的是:答案竟然是
2
!為什麼?
别着急,我會解釋給你聽。首先我要告訴你這不是什麼錯誤,而是 JavaScript 語言解釋器的一個(非官方的)特性,某人(
Ben Cherry)把這個特性叫做:
Hoisting(目前尚未有标準的翻譯,比較常見的是提升)。
聲明與定義
為了了解 Hoisting,我們先來看一個簡單的情況:
var a = 1;
你是否想過,上面這句代碼在運作的時候到底發生了什麼?
你是否知道,就這句代碼而言,“聲明變量
a
” 和 “定義變量
a
”這兩個說法哪一個才是正确的?
- 下例叫做 “聲明變量”:
-
下例叫做 “定義變量”:var a ;
-
var a = 1;
- 聲明:是指你聲稱某樣東西的存在,比如一個變量或一個函數;但你沒有說明這樣東西到底是什麼,僅僅是告訴解釋器這樣東西存在而已;
- 定義:是指你指明了某樣東西的具體實作,比如一個變量的值是多少,一個函數的函數體是什麼,确切的表達了這樣東西的意義。
var a; // 這是聲明
a = 1; // 這是定義(指派)
var a = 1; // 合二為一:聲明變量的存在并指派給它
重點來了:當你以為你隻做了一件事情的時候(
var a = 1
),實際上解釋器把這件事情分解成了兩個步驟,一個是聲明(
var a
),另一個是定義(
a = 1
)。
這和 Hoisting 有何關系?
回到最開始的那個令人困惑的例子,我告訴你解釋器是如何分析你的代碼的:
var a;
a = 1;
function foo() {
var a; // 關鍵在這裡
if (!a) {
a = 2;
}
alert(a); // 此時的 a 并非函數體外的那個全局變量
}
如代碼所示,在進入函數體後解釋器聲明了新的變量
a
,當時其值為
undefined
,于是
if
語句條件判斷結果為真,接着為新的變量
a
指派為
2
。你若不相信可以在函數體外面
alert(a)
,然後再執行
foo()
對比一下結果就知道了。
Scoping(作用域)
有人可能會問了:“為什麼不是在
if
語句内聲明變量
a
?”
因為 JavaScript 沒有塊級作用域(Block Scoping),隻有函數作用域(Function Scoping),是以說不是看見一對花括号
{}
就代表産生了新的作用域,和 C 不一樣!
當解析器讀到
if
語句的時候,它發現此處有一個變量聲明和指派,于是解析器會将其聲明提升至目前作用域的頂部(這是預設行為,并且無法更改),這個行為就叫做 Hoisting。
OK,大家都懂了,你懂了嗎……
懂了不代表就會用了,就拿最開始的例子來說,如果我就是想要
alert(a)
出那個
1
可咋整呢?
建立新的作用域
alert(a)
在執行的時候,會去尋找變量
a
的位置,它從目前作用域開始向上(或者說向外)一直查找到頂層作用域為止,若是找不到就報
undefined
。
因為在
alert(a)
的同級作用域裡,我們再次聲明了本地變量
a
,是以它報
2
;是以我們可以把本地變量
a
的聲明向下(或者說向内)移動,這樣
alert(a)
就找不到它了。
記住:JavaScript 隻有函數作用域!
var a = 1;
function foo() {
if (!a) {
(function() { // 這是上一篇說到過的 IIFE,它會建立一個新的函數作用域
var a = 2; // 并且該作用域在 foo() 的内部,是以 alert 通路不到
}()); // 不過這個作用域可以通路上層作用域哦,這就叫:“閉包”
};
alert(a);
};
foo();
你或許在無數的 JavaScript 書籍和文章裡讀到過:“請始終保持作用域内所有變量的聲明放置在作用域的頂部”,現在你應該明白為什麼有此一說了吧?因為這樣可以避免 Hoisting 特性給你帶來的困擾(我不是很情願這麼說,因為 Hoisting 本身并沒有什麼錯),也可以很明确的告訴所有閱讀代碼的人(包括你自己)在目前作用域内有哪些變量可以通路。但是,變量聲明的提升并非 Hoisting 的全部。在 JavaScript 中,有四種方式可以讓命名進入到作用域中(按優先級):
- 語言定義的命名:比如
或者this
,它們在所有作用域内都有效且優先級最高,是以在任何地方你都不能把變量命名為arguments
之類的,這樣是沒有意義的this
- 形式參數:函數定義時聲明的形式參數會作為變量被 hoisting 至該函數的作用域内。是以形式參數是本地的,不是外部的或者全局的。當然你可以在執行函數的時候把外部變量傳進來,但是傳進來之後就是本地的了
- 函數聲明:函數體内部還可以聲明函數,不過它們也都是本地的了
- 變量聲明:這個優先級其實還是最低的,不過它們也都是最常用的
另外,還記得之前我們讨論過 聲明 和 定義 的差別吧?當時我并沒有說為什麼要了解這個差別,不過現在是時候了,記住:Hosting 隻提升了命名,沒有提升定義
Hosting 隻提升了命名,沒有提升定義
這一點和我們接下來要講到的東西息息相關,請看:
先看兩個例子:
function test() {
foo();
function foo() {
alert("我是會出現的啦……");
}
}
test();
//////////////////////////////
function test() {
foo();
var foo = function() {
alert("我不會出現的哦……");
}
}
test();
同學,在了解了 Scoping & Hoisting 之後,你知道怎麼解釋這一切了吧?
在第一個例子裡,函數
foo
是一個聲明,既然是聲明就會被提升(我特意包裹了一個外層作用域,因為全局作用域需要你的想象,不是那麼直覺,但是道理是一樣的),是以在執行
foo()
之前,作用域就知道函數
foo
的存在了。這叫做函數聲明(Function Declaration),函數聲明會連通命名和函數體一起被提升至作用域頂部。
然而在第二個例子裡,被提升的僅僅是變量名
foo
,至于它的定義依然停留在原處。是以在執行
foo()
之前,作用域隻知道
foo
的命名,不知道它到底是什麼,是以執行會報錯(通常會是:
undefined is not a function
)。這叫做函數表達式(Function Expression),函數表達式隻有命名會被提升,定義的函數體則不會。
原文釋出時間為:2018年03月19日
原文作者:
張澤立本文來源:
開源中國如需轉載請聯系原作者