Javascript 老舊的 "var"

2023-02-17 10:50 更新

本文用于幫助理解舊腳本

本文所講的內(nèi)容對于幫助理解舊腳本很有用。

但這不是我們編寫新代碼的方式。

在本教程最開始那部分的 變量 這章中,我們提到了變量聲明的三種方式:

  1. ?let?
  2. ?const?
  3. ?var?

var 聲明與 let 相似。大部分情況下,我們可以用 let 代替 var 或者 var 代替 let,都能達(dá)到預(yù)期的效果:

var message = "Hi";
alert(message); // Hi

但實(shí)際上 var 卻是一頭非常不同的,源自遠(yuǎn)古時代的怪獸。在現(xiàn)代腳本中一般不再使用它,但它仍然潛伏在舊腳本中。

如果你不打算接觸這樣的腳本,你甚至可以跳過本章或推遲閱讀本章。

另一方面,了解將舊腳本從 var 遷移到 let 時的區(qū)別,以避免奇怪的錯誤,是很重要的。

“var” 沒有塊級作用域

用 var 聲明的變量,不是函數(shù)作用域就是全局作用域。它們在代碼塊外也是可見的(譯注:也就是說,var 聲明的變量只有函數(shù)作用域和全局作用域,沒有塊級作用域)。

舉個例子:

if (true) {
  var test = true; // 使用 "var" 而不是 "let"
}

alert(test); // true,變量在 if 結(jié)束后仍存在

由于 var 會忽略代碼塊,因此我們有了一個全局變量 test。

如果我們在第二行使用 let test 而不是 var test,那么該變量將僅在 if 內(nèi)部可見:

if (true) {
  let test = true; // 使用 "let"
}

alert(test); // ReferenceError: test is not defined

對于循環(huán)也是這樣的,var 聲明的變量沒有塊級作用域也沒有循環(huán)局部作用域:

for (var i = 0; i < 10; i++) {
  var one = 1;
  // ...
}

alert(i);   // 10,"i" 在循環(huán)結(jié)束后仍可見,它是一個全局變量
alert(one); // 1,"one" 在循環(huán)結(jié)束后仍可見,它是一個全局變量

如果一個代碼塊位于函數(shù)內(nèi)部,那么 var 聲明的變量的作用域?qū)楹瘮?shù)作用域:

function sayHi() {
  if (true) {
    var phrase = "Hello";
  }

  alert(phrase); // 能正常工作
}

sayHi();
alert(phrase); // ReferenceError: phrase is not defined

可以看到,var 穿透了 if,for 和其它代碼塊。這是因?yàn)樵谠缙诘?JavaScript 中,塊沒有詞法環(huán)境,而 var 就是這個時期的代表之一。

“var” 允許重新聲明

如果我們用 let 在同一作用域下將同一個變量聲明兩次,則會出現(xiàn)錯誤:

let user;
let user; // SyntaxError: 'user' has already been declared

使用 var,我們可以重復(fù)聲明一個變量,不管多少次都行。如果我們對一個已經(jīng)聲明的變量使用 var,這條新的聲明語句會被忽略:

var user = "Pete";

var user = "John"; // 這個 "var" 無效(因?yàn)樽兞恳呀?jīng)聲明過了)
// ……不會觸發(fā)錯誤

alert(user); // John

“var” 聲明的變量,可以在其聲明語句前被使用

當(dāng)函數(shù)開始的時候,就會處理 var 聲明(腳本啟動對應(yīng)全局變量)。

換言之,var 聲明的變量會在函數(shù)開頭被定義,與它在代碼中定義的位置無關(guān)(這里不考慮定義在嵌套函數(shù)中的情況)。

那么看一下這段代碼:

function sayHi() {
  phrase = "Hello";

  alert(phrase);

  var phrase;
}
sayHi();

……從技術(shù)上講,它與下面這種情況是一樣的(var phrase 被上移至函數(shù)開頭):

function sayHi() {
  var phrase;

  phrase = "Hello";

  alert(phrase);
}
sayHi();

……甚至與這種情況也一樣(記住,代碼塊是會被忽略的):

function sayHi() {
  phrase = "Hello"; // (*)

  if (false) {
    var phrase;
  }

  alert(phrase);
}
sayHi();

人們將這種行為稱為“提升”(英文為 “hoisting” 或 “raising”),因?yàn)樗械?nbsp;var 都被“提升”到了函數(shù)的頂部。

所以,在上面的例子中,if (false) 分支永遠(yuǎn)都不會執(zhí)行,但沒關(guān)系,它里面的 var 在函數(shù)剛開始時就被處理了,所以在執(zhí)行 (*) 那行代碼時,變量是存在的。

聲明會被提升,但是賦值不會。

我們最好用例子來說明:

function sayHi() {
  alert(phrase);

  var phrase = "Hello";
}

sayHi();

var phrase = "Hello" 這行代碼包含兩個行為:

  1. 使用 ?var? 聲明變量
  2. 使用 ?=? 給變量賦值。

聲明在函數(shù)剛開始執(zhí)行的時候(“提升”)就被處理了,但是賦值操作始終是在它出現(xiàn)的地方才起作用。所以這段代碼實(shí)際上是這樣工作的:

function sayHi() {
  var phrase; // 在函數(shù)剛開始時進(jìn)行變量聲明

  alert(phrase); // undefined

  phrase = "Hello"; // ……賦值 — 當(dāng)程序執(zhí)行到這一行時。
}

sayHi();

因?yàn)樗械?nbsp;var 聲明都是在函數(shù)開頭處理的,我們可以在任何地方引用它們。但是在它們被賦值之前都是 undefined。

上面兩個例子中,alert 運(yùn)行都不會報(bào)錯,因?yàn)樽兞?nbsp;phrase 是存在的。但是它還沒有被賦值,所以顯示 undefiend

IIFE

在之前,JavaScript 中只有 var 這一種聲明變量的方式,并且這種方式聲明的變量沒有塊級作用域,程序員們就發(fā)明了一種模仿塊級作用域的方法。這種方法被稱為“立即調(diào)用函數(shù)表達(dá)式”(immediately-invoked function expressions,IIFE)。

如今,我們不應(yīng)該再使用 IIFE 了,但是你可以在舊腳本中找到它們。

IIFE 看起來像這樣:

(function() {

  var message = "Hello";

  alert(message); // Hello

})();

這里,創(chuàng)建了一個函數(shù)表達(dá)式并立即調(diào)用。因此,代碼立即執(zhí)行并擁有了自己的私有變量。

函數(shù)表達(dá)式被括號 (function {...}) 包裹起來,因?yàn)楫?dāng) JavaScript 引擎在主代碼中遇到 "function" 時,它會把它當(dāng)成一個函數(shù)聲明的開始。但函數(shù)聲明必須有一個函數(shù)名,所以這樣的代碼會導(dǎo)致錯誤:

// 嘗試聲明并立即調(diào)用一個函數(shù)
function() { // <-- SyntaxError: Function statements require a function name

  var message = "Hello";

  alert(message); // Hello

}();

即使我們說:“好吧,那我們加一個名稱吧”,但它仍然不工作,因?yàn)?JavaScript 不允許立即調(diào)用函數(shù)聲明:

// 下面的括號會導(dǎo)致語法錯誤
function go() {

}(); // <-- 不能立即調(diào)用函數(shù)聲明

因此,需要使用圓括號把該函數(shù)表達(dá)式包起來,以告訴 JavaScript,這個函數(shù)是在另一個表達(dá)式的上下文中創(chuàng)建的,因此它是一個函數(shù)表達(dá)式:它不需要函數(shù)名,可以立即調(diào)用。

除了使用括號,還有其他方式可以告訴 JavaScript 在這我們指的是函數(shù)表達(dá)式:

/ 創(chuàng)建 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");
}();

在上面的所有情況中,我們都聲明了一個函數(shù)表達(dá)式并立即運(yùn)行它。請?jiān)僮⒁庖幌拢喝缃裎覀儧]有理由來編寫這樣的代碼。

總結(jié)

var 與 let/const 有兩個主要的區(qū)別:

  1. ?var? 聲明的變量沒有塊級作用域,它們僅在當(dāng)前函數(shù)內(nèi)可見,或者全局可見(如果變量是在函數(shù)外聲明的)。
  2. ?var? 變量聲明在函數(shù)開頭就會被處理(腳本啟動對應(yīng)全局變量)。

涉及全局對象時,還有一個非常小的差異,我們將在下一章中介紹。

這些差異使 var 在大多數(shù)情況下都比 let 更糟糕。塊級作用域是這么好的一個東西。這就是 let 在幾年前就被寫入到標(biāo)準(zhǔn)中的原因,并且現(xiàn)在(與 const 一起)已經(jīng)成為了聲明變量的主要方式。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號