本文用于幫助理解舊腳本
本文所講的內容對于幫助理解舊腳本很有用。
但這不是我們編寫新代碼的方式。
在本教程最開始那部分的 變量 這章中,我們提到了變量聲明的三種方式:
let
const
var
var
聲明與 let
相似。大部分情況下,我們可以用 let
代替 var
或者 var
代替 let
,都能達到預期的效果:
var message = "Hi"; alert(message); // Hi
但實際上 var
卻是壹頭非常不同的,源自遠古時代的怪獸。在現代腳本中壹般不再使用它,但它仍然潛伏在舊腳本中。
如果妳不打算接觸這樣的腳本,妳甚至可以跳過本章或推遲閱讀本章。
另壹方面,了解將舊腳本從 var
遷移到 let
時的區別,以避免奇怪的錯誤,是很重要的。
用 var
聲明的變量,不是函數作用域就是全局作用域。它們在代碼塊外也是可見的(譯注:也就是說,var
聲明的變量只有函數作用域和全局作用域,沒有塊級作用域)。
舉個例子:
if (true) { var test = true; // 使用 "var" 而不是 "let" } alert(test); // true,變量在 if 結束後仍存在
由于 var
會忽略代碼塊,因此我們有了壹個全局變量 test
。
如果我們在第二行使用 let test
而不是 var test
,那麽該變量將僅在 if
內部可見:
if (true) { let test = true; // 使用 "let" } alert(test); // ReferenceError: test is not defined
對于循環也是這樣的,var
聲明的變量沒有塊級作用域也沒有循環局部作用域:
for (var i = 0; i < 10; i++) { var one = 1; // ... } alert(i); // 10,"i" 在循環結束後仍可見,它是壹個全局變量 alert(one); // 1,"one" 在循環結束後仍可見,它是壹個全局變量
如果壹個代碼塊位于函數內部,那麽 var
聲明的變量的作用域將爲函數作用域:
function sayHi() { if (true) { var phrase = "Hello"; } alert(phrase); // 能正常工作 } sayHi(); alert(phrase); // ReferenceError: phrase is not defined
可以看到,var
穿透了 if
,for
和其它代碼塊。這是因爲在早期的 JavaScript 中,塊沒有詞法環境,而 var
就是這個時期的代表之壹。
如果我們用 let
在同壹作用域下將同壹個變量聲明兩次,則會出現錯誤:
let user; let user; // SyntaxError: 'user' has already been declared
使用 var
,我們可以重複聲明壹個變量,不管多少次都行。如果我們對壹個已經聲明的變量使用 var
,這條新的聲明語句會被忽略:
var user = "Pete"; var user = "John"; // 這個 "var" 無效(因爲變量已經聲明過了) // ……不會觸發錯誤 alert(user); // John
當函數開始的時候,就會處理 var
聲明(腳本啓動對應全局變量)。
換言之,var
聲明的變量會在函數開頭被定義,與它在代碼中定義的位置無關(這裏不考慮定義在嵌套函數中的情況)。
那麽看壹下這段代碼:
function sayHi() { phrase = "Hello"; alert(phrase); var phrase; } sayHi();
……從技術上講,它與下面這種情況是壹樣的(var phrase
被上移至函數開頭):
function sayHi() { var phrase; phrase = "Hello"; alert(phrase); } sayHi();
……甚至與這種情況也壹樣(記住,代碼塊是會被忽略的):
function sayHi() { phrase = "Hello"; // (*) if (false) { var phrase; } alert(phrase); } sayHi();
人們將這種行爲稱爲“提升”(英文爲 “hoisting” 或 “raising”),因爲所有的 var
都被“提升”到了函數的頂部。
所以,在上面的例子中,if (false)
分支永遠都不會執行,但沒關系,它裏面的 var
在函數剛開始時就被處理了,所以在執行 (*)
那行代碼時,變量是存在的。
聲明會被提升,但是賦值不會。
我們最好用例子來說明:
function sayHi() { alert(phrase); var phrase = "Hello"; } sayHi();
var phrase = "Hello"
這行代碼包含兩個行爲:
使用 var
聲明變量
使用 =
給變量賦值。
聲明在函數剛開始執行的時候(“提升”)就被處理了,但是賦值操作始終是在它出現的地方才起作用。所以這段代碼實際上是這樣工作的:
function sayHi() { var phrase; // 在函數剛開始時進行變量聲明 alert(phrase); // undefined phrase = "Hello"; // ……賦值 — 當程序執行到這壹行時。 } sayHi();
因爲所有的 var
聲明都是在函數開頭處理的,我們可以在任何地方引用它們。但是在它們被賦值之前都是 undefined。
上面兩個例子中,alert
運行都不會報錯,因爲變量 phrase
是存在的。但是它還沒有被賦值,所以顯示 undefiend
。
在之前,JavaScript 中只有 var
這壹種聲明變量的方式,並且這種方式聲明的變量沒有塊級作用域,程序員們就發明了壹種模仿塊級作用域的方法。這種方法被稱爲“立即調用函數表達式”(immediately-invoked function expressions,IIFE)。
如今,我們不應該再使用 IIFE 了,但是妳可以在舊腳本中找到它們。
IIFE 看起來像這樣:
(function() { var message = "Hello"; alert(message); // Hello })();
這裏,創建了壹個函數表達式並立即調用。因此,代碼立即執行並擁有了自己的私有變量。
函數表達式被括號 (function {...})
包裹起來,因爲當 JavaScript 引擎在主代碼中遇到 "function"
時,它會把它當成壹個函數聲明的開始。但函數聲明必須有壹個函數名,所以這樣的代碼會導致錯誤:
// 嘗試聲明並立即調用壹個函數 function() { // <-- SyntaxError: Function statements require a function name var message = "Hello"; alert(message); // Hello }();
即使我們說:“好吧,那我們加壹個名稱吧”,但它仍然不工作,因爲 JavaScript 不允許立即調用函數聲明:
// 下面的括號會導致語法錯誤 function go() { }(); // <-- 不能立即調用函數聲明
因此,需要使用圓括號把該函數表達式包起來,以告訴 JavaScript,這個函數是在另壹個表達式的上下文中創建的,因此它是壹個函數表達式:它不需要函數名,可以立即調用。
除了使用括號,還有其他方式可以告訴 JavaScript 在這我們指的是函數表達式:
// 創建 IIFE 的方法 (function() { alert("Parentheses around the function"); })(); (function() { alert("Parentheses around the whole thing"); }()); !function() { alert("Bitwise NOT operator starts the expression"); }(); +function() { alert("Unary plus starts the expression"); }();
在上面的所有情況中,我們都聲明了壹個函數表達式並立即運行它。請再注意壹下:如今我們沒有理由來編寫這樣的代碼。
var
與 let/const
有兩個主要的區別:
var
聲明的變量沒有塊級作用域,它們僅在當前函數內可見,或者全局可見(如果變量是在函數外聲明的)。
var
變量聲明在函數開頭就會被處理(腳本啓動對應全局變量)。
涉及全局對象時,還有壹個非常小的差異,我們將在下壹章中介紹。
這些差異使 var
在大多數情況下都比 let
更糟糕。塊級作用域是這麽好的壹個東西。這就是 let
在幾年前就被寫入到標准中的原因,並且現在(與 const
壹起)已經成爲了聲明變量的主要方式。