JavaScript 語言核心(3)你的變數 var 了嗎?



在程式語言的分類中,依據是在編譯時期或執行時期進行型別檢查,可區分為靜態定型(Statically-typed)語言與動態(Dynamically-typed)語言。
Java、C/C++ 等皆為靜態定型語言,其變數必然帶有型態。以 Java 為例:
int number = 10;
String id = "caterpillar";
在上例中,number 變數本身帶有 int 型態資訊,而 id 變數帶有 String 型態資訊,在指定時,變數型態資訊與值的型態資訊必須符合,否則會發生編譯失敗。例如以下就會因型態不符而編譯失敗:
int number = 10;
number = "caterpillar";
JavaScript 則為動態定型語言,其變數本身使用者無需宣告型態,型態資訊僅在值或物件本身,變數只用來作為取得值或物件的參考。例如:
var some = 10;
some = 'caterpillar';
由於變數本身不帶型態資訊,同一個變數可以指定不同型態的值,實際操作時,是在執行時期才透過變數來參考至物件或值,才得知物件或值上有操作之方法。
靜態定型語言由於變數本身帶有型態資訊,好處就是編譯時期,可由編譯器確認變數與實際參考之值是否符合,可在編譯時期就檢查出許多型態指定不符的錯誤。相對地,動態定型語言就必須等到執行時期,才能發現所操作的對象並非預期型態之錯誤,這是靜態定型語言優點動態定型語言的地方。
然而,靜態定型語言宣告變數時,必須同時宣告型態,指定值給變數時亦需符合型態,或者是使用轉型(CAST)語法,讓編譯器忽略型態不符問題,因而容易造成語法上的冗長。例如在 Java 中,若要使用同一陣列儲存多種物件,則一個例子如下:
Object[] objects = {"caterpillar", new Integer(100), new Date()};
String name = (String) objects[0];
Integer score = (Integer) objects[1];
Data time = (Date) objects[2];
反觀 JavaScript 若要達到相同目的,所需的程式碼較為簡短。例如:
var objects = ['caterpillar', 100, new Date()];
var name = objects[0];
var score = objects[1];
var time = objects[2];
就程式撰寫速度上,動態定型語言著實有著比靜態定型語言快速的優點。
再回頭看看 JavaScript 變數宣告的討論。在 JavaScript 中要宣告變數,可以使用 var 來宣告。這是先前一直都有看到的,事實上,你也可以不用 var 宣告,直接指定某個名稱的值,該名稱會自動成為全域範圍,其實也就是在全域(global)物件上建立特性。例如:
> some = 10;
10
> some;
10
>
這很方便,也很危險,因為是在全域物件上建立特性。全域變數若在瀏覽器中,就是 window 物件,在 Node.js 中,也可以在全域範圍中使用 this 來取得。例如:
> this.some;
10
> this;
{ ArrayBuffer: [Function: ArrayBuffer],
  Int8Array: { [Function: Int8Array] BYTES_PER_ELEMENT: 1 },
  Uint8Array: { [Function: Uint8Array] BYTES_PER_ELEMENT: 1 },
  ...略 }
關於 this,之後還會有一篇文章討論。
使用 var 宣告的變數,作用範圍是在當時所在環境,不使用 var 直接指定值而建立的變數,則是全域物件上的一個特性,也就是俗稱的全域範圍。你可以先以這樣的觀念理解,如果你寫下:
some = 10;
執行時暫時可先以直譯器會直接這麼作來理解:
this.some = 10;
例如,可觀察以下在函式中使用 var 與不使用 var 宣告的變數之差別:
function func() {
    x = 10;
    var y = 20;
}

func();

console.log(x);
console.log(y);
執行時這個 .js 檔案中的程式碼時,x 在函式中未使用 var 宣告,是全域物件上的特性,在函式外依舊可見,俗稱全域範圍,y 使用了 var 宣告,所以在函式外不可見,為函式中的區域變數。
如果你在全域使用 var 宣告變數,也相當於在全域物件上建立特性。例如:
> var x = 10;
undefined
> this.x;
10
>
如果全域與區域中有同名的變數,則區域會暫時覆蓋全域:
var x = 10;

function func() {
    var x = 20;
    console.log(x);
}

func();

console.log(x);
你可以使用 delete 來刪除物件上的特性。由於未使用 var 宣告的變數,會是全域物件上的特性,就某些意義來說,對未使用 var 宣告的變數使用 delete,就相當於所謂刪除變數。例如:
> a = 20;
20
> delete a;
true
> a;
ReferenceError: a is not defined
    at repl:1:1
    at REPLServer.self.eval (repl.js:110:21)
    at repl.js:249:20
    at REPLServer.self.eval (repl.js:122:7)
    at Interface.<anonymous> (repl.js:239:12)
    at Interface.EventEmitter.emit (events.js:95:17)
    at Interface._onLine (readline.js:202:10)
    at Interface._line (readline.js:531:8)
    at Interface._ttyWrite (readline.js:760:14)
    at ReadStream.onkeypress (readline.js:99:10)
> this.a;
undefined
>
delete 會傳回 true 表示特性刪除成功,false 表示無法刪除。使用 var 宣告的變數就無法用 delete 刪除。例如:
> var b = 10;
undefined
> delete b;
false
>
你可以重複使用 var 宣告變數,但不會覆蓋原有的指定值。例如:
> var x = 10;
undefined
> var x;
undefined
> x
10
>
要注意的是,var 宣告的變數是當時作用範圍中整個都是有作用的,並沒有所謂區塊範圍。例如:
function func() {
    if(true) {
        var x = 10;
    }
    return x;
}

console.log(func());
執行結果會顯示 10。var 宣告的變數是當時作用範圍中整個都是有作用的,這會產生令人驚奇的結果。例如下例不意外的,會產生直譯錯誤:
function func() {
    console.log(x);
}

func();
但下例中並不會直譯錯誤:
function func() {
    console.log(m);
    var m = 10;
    console.log(m);
}

func();
結果會顯示 undefined 與 10。所有 var 宣告的變數,在整個函式區塊中都是可見的,因而在上例中 console.log 時是可找到 m 特性,只不過是 undefined 的值,這行為稱為提昇(Hoisting)。
不使用 var 宣告就直接賦值的變數,實際上會是全域範圍,也就是全域(global)物件上建立特性,這會引發許多名稱衝突問題,在 ECMAScript 5 的嚴格模式下,禁止了這類的行為,因此以下的 js 檔案,執行時會引發 ReferenceError: x is not defined 的錯誤:
'use strict';
x = 10;
變數未經 var 宣告就使用,無論是在哪個範疇,都會引發這類錯誤。嚴格模式中,對 var 宣告的變數使用 delete 會直接拋出 SyntaxError,而不是傳回 false,例如執行以下的 js 檔案就會發生錯誤:
'use strict';
var x = 10;
delete x;
在嚴格模式下,有一些字被保留作未來使用,你不可以使用這些保留字作為變數名稱:
  • implements
  • interface
  • package
  • private
  • protected
  • public
  • static
  • yield
除此之外,因為 JavaScript 本身有個 eval 函式,而每個函式中 arguments 也用來參考至引數清單,在嚴格模式下 eval 與 arguments 也不能作為變數名稱。