理解 JavaScript Scoping & Hoisting(二)_javascript技巧

來源:互聯網
上載者:User

Scoping & Hoisting

var a = 1;function foo() {  if (!a) {    var a = 2;  }  alert(a);};foo();

上面這段代碼在運行時會產生什麼結果?

儘管對於有經驗的程式員來說這隻是小菜一碟,不過我還是順著初學者常見的思路做一番描述:

1.建立了全域變數 a,定義其值為 1
2.建立了函數 foo
3.在 foo 的函數體內,if 語句將不會執行,因為 !a 會將變數 a 轉變成布爾的假值,也就是 false
4.跳過條件分支,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,而無論 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 中,有四種方式可以讓命名進入到範圍中(按優先順序):

1.語言定義的命名:比如 this 或者 arguments,它們在所有範圍內都有效且優先順序最高,所以在任何地方你都不能把變數命名為 this 之類的,這樣是沒有意義的
2.形式參數:函數定義時聲明的形式參數會作為變數被 hoisting 至該函數的範圍內。所以形式參數是本地的,不是外部的或者全域的。當然你可以在執行函數的時候把外部變數傳進來,但是傳進來之後就是本地的了
3.函式宣告:函數體內部還可以聲明函數,不過它們也都是本地的了
4.變數聲明:這個優先順序其實還是最低的,不過它們也都是最常用的

另外,還記得之前我們討論過 聲明 和 定義 的區別吧?當時我並沒有說為什麼要理解這個區別,不過現在是時候了,記住:

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),函數運算式只有命名會被提升,定義的函數體則不會。

尾記:Ben Cherry 的原文解釋的更加詳細,只不過是英文而已。我這篇是借花獻佛,主要是更淺顯的解釋給初學者聽,若要看更多的樣本,請移步原作,謝謝。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.