此頁面由社群從英文翻譯而來。了解更多並加入 MDN Web Docs 社群。

View in English Always switch to English

重新介紹 JavaScript

介紹

為何需要重新介紹?因為 JavaScript 堪稱是全世界最被人誤解的程式語言。儘管 JavaScript 再怎麼的被嘲諷為小兒科,在它誤導人的簡潔下隱藏著強大的語言功能。2005 年是個許多知名 JavaScript 應用程式推出的年度,在在證明:更加瞭解這項科技對任何網頁開發者來說皆是重要的技能。

先從該語言的歷史說起。1995 年,Brendan Eich,一位 Netscape (網景)的工程師,創造了 JavaScript。1996 年初,JavaScript 隨著 Netscape 2 首次推出。它原本要被命名為 LiveScript,結果因為行銷策略為了強調昇陽的 Java 程式語言的普遍性,而不幸的被改名 — 即便兩者之間沒有太大的關係。從此之後,這便成為了混淆的元兇。

Microsoft 在幾個月後隨著 IE 3 推出了跟該語言大致上相容的 JScript。Netscape 在 1997 年將該語言送交 ECMA International,一個歐洲標準化組織,而在 1997 年的時候產生了初版的 ECMAScript。該標準在 1999 年的時候以 ECMAScript 第三版的形式推出了更新,從此之後大致上都相當穩定,不過近期有在研發第四版。

這個穩定性對開發者來說是相當好的事情,因為它讓不少實作 (implementation) 有時間慢慢趕上。我會把重點放在第三版的語法。為了避免混淆,我會繼續使用 JavaScript 這個名稱。

與其他程式語言大大不同的是,JavaScript 沒有任何輸入或輸出的觀念。它是被設計成在一個宿主 (host) 環境下執行的腳本 (script) 語言,所以任何與外界通訊的方式,都是宿主環境的責任。瀏覽器是最常見的宿主環境,不過有些程式也有 JavaScript 解釋器,如 Adobe Acrobat、Photoshop、以及 Yahoo! Widget Engine 等等。

概要

先從任何語言最基本的方面講起:型態 (type)。JavaScript 程式可以改變「值」(value),而這些值各自有其歸屬的型態。JavaScript 的型態有:

...以及稍微怪一點的 undefined (未定義)和 null (空)。還有,算是一種特殊物件的 Array (陣列)。還有,額外的特殊物件 Date (日期)以及 Regular Expression。另外,如果真的要達到技術上的準確性,連函式也只算是一種特殊的物件。所以型態分佈表看起來應該像這樣:

  • Number (數字)

  • String (字串)

  • Boolean (布林)

  • Object (物件)

    • Function (函式)
    • Array (陣列)
    • Date (日期)
    • RegExp
  • Null (空)

  • Undefined (未定義)

其實也有內建的 Error (錯誤)類型,不過,先把重點放在上面的分佈表比較容易。

數字

根據規格,JavaScript 數字算是「雙精確度 64 位元格式 IEEE 754 值」("double-precision 64-bit format IEEE 754 values")。這能造成一些有趣的後果。JavaScript 沒有所謂的整數,所以你在做算術的時候得小心一點,尤其是假如你習慣了 C 或 Java 的數學。小心類似下的的事情:

js
0.1 + 0.2 = 0.30000000000000004

JavaScript 支援標準的數字運算子,包含加法、減法、取餘數、等算術。另外還有一個之前忘記提的內建物件,Math (數學),用以處理較為進階的數學函數和常數:

js
Math.sin(3.5);
d = Math.PI * r * r;

你可以用內建的 parseInt() 函式將字串轉成整數。這個函式有個可選用的第二個參數(在此建議你一定要指定),用以指定進位數。

js
> parseInt("123", 10)
123
> parseInt("010", 10)
10

如果你不指定進位數,就有可能得到意想不到的結果:

js
> parseInt("010")
8

這是因為 parseInt 函數把字串當成八進位的數字,因為開頭有個 0。

如果要把二進位的數字轉成整數,只要把進位數改掉就行了:

js
> parseInt("11", 2)
3

有個特殊的數字,叫做 NaN (「Not a Number」,「非數字」的簡稱),假如遞進去的字串不是數字,則會回傳 NaN

js
> parseInt("hello", 10)
NaN

NaN 很毒:將其進行任何數學運算,結果仍會是 NaN

js
> NaN + 5
NaN

你可以用內建的 isNaN() 函式來判斷一個值是否為 NaN

js
> isNaN(NaN)
true

JavaScript 也有特殊的值 Infinity (無限)以及 -Infinity (負無限):

js
> 1 / 0
Infinity
> -1 / 0
-Infinity

字串

JavaScript 的字串是一序列的字元。更精確的說,是一序列的 Unicode 字元,每個字元皆以一個 16 位元的數字作為代表。這讓任何人不需要為國際化感到擔心。

如果你要代表一個單一字元,用長度為 1 的字串即可。

要得知一個字串的長度,請存取該字串的 length(長度)屬性:

js
> "hello".length
5

剛剛可是與 JavaScript 物件的第一次接觸呢!字串也是物件喔。字串甚至也有方法 (method)

js
> "hello".charAt(0) //位置 0 的字元
h
> "hello, world".replace("hello", "goodbye") //把 hello 換成 goodbye
goodbye, world
> "hello".toUpperCase() //轉成大寫
HELLO

其他類型

JavaScript 對下列兩者是有分別的:null (空),屬於「object」型態的一種物件,用以明言表示無數值,以及 undefined (無定義),屬於「undefined」類型的一種物件,用以表示未初始化的值,也就是說,根本還沒指定數值。雖然姑且先不論變數,但在 JavaScript 你可以宣告一個變數但不指定其值。如果你這麼做的話,那該變數的型態便是 undefined

JavaScript 有布林 (boolean) 型態,可能的值有 true (真)與 false (假)且兩者皆為關鍵字。根據下列規則,任何值都可以被轉換成布林值:

  1. false0、空字串 ("")、NaNnull、以及 undefined 都會成為 false
  2. 所有其他的值都會成為 true

你可以用 Boolean() 函式特別進行轉換:

js
> Boolean("")
false
> Boolean(234)
true

不過很少需要這樣,因為假如 JavaScript 遇到需要接收布林值的時候,如 if 陳述式(見下),便會無聲無息的進行布林轉換。有鑑於此,常常會有「真值」("true values") 與「假值」("false values") 等說法,意思是指值在布林轉換過程中會被轉成 true 或是 false。這種值也有被稱為「真的」("truthy") 或「假的」("falsy")。

JavaScript 支援布林運算,如 && (邏輯「」,英稱 and)、|| (邏輯「」,英稱 or)、以及 ! (邏輯「」,英稱 not),請見下。

變數

在 JavaScript,要宣告新變數,使用的是 var 關鍵字:

js
var a;
var name = "simon";

如果你宣告一個變數但不指定任何值,其型態便為 undefined (未定義)。 應該注意關於 JS 的區塊不會構成新變數作用域

運算子

JavaScript 的數字運算子有 +-*/、以及 % - 最後一個是取餘數的運算子(英稱 mod)。用來指定值的運算子是 =,另外還有複合指定陳述式,如 += 以及 -=。這些是用以延伸 x = x 運算子 y

js
x += 5;
x = x + 5;

你可以用 ++-- 來分別增加或是減少數值。這些運算子可以放在變數的開頭或結尾。

+ 運算子也能把字串連接 (concatenate) 起來:

js
> "hello" + " world"
hello world

如果你把一字串加到一個數字(或其他數值),會先把所有的東西轉成字串。這會讓你意想不到:

js
> "3" + 4 + 5
345
> 3 + 4 + "5"
75

把一個空字串加到一個東西是個將其轉成字串的好方法之一。

JavaScript 中進行比較可以用 <><=、以及 >=。這些對字串和數字都有用。等值比較 (equality) 比較沒那麼直接。雙等號運算子(等於)會進行型態強制轉換,假如比較的資料型態不一樣,有時結果會相當有趣:

js
> "dog" == "dog"
true
> 1 == true
true

要避免型態強制轉換,要用三等號運算子(絕對等於):

js
> 1 === true
false
> true === true
true

另外還有 != (不等於)以及 !== (絕對不等於)運算子。

假如你需要用的話,JavaScript 也有逐位元 (bitwise) 運算子

控制結構

JavaScript 跟其他同屬 C 家族的程式語言有類似的控制結構。條件陳述式是靠 if 以及 else 來表示。如果你喜歡還可以串起來:

js
var name = "貓咪";
if (name == "狗狗") {
  name += "!";
} else if (name == "貓咪") {
  name += "!!";
} else {
  name = "!" + name;
}
name == "貓咪!!";

JavaScript 有 while 迴圈以及 do-while 迴圈。前者適合做基本的迴圈,而後者是當你要迴圈至少執行一次就需要用到:

js
while (true) {
  // 無限迴圈!
}

do {
  var input = get_input();
} while (inputIsNotValid(input));

JavaScript 的 for 迴圈跟 C 和 Java 的一樣:可以讓你用一行就提供控制的條件與資訊。

js
for (var i = 0; i < 5; i++) {
  // 會執行 5 次
}

&& 以及 || 運算子用的是「短路邏輯」(short-circuit logic),也就是說,第二個運算值是否會被執行靠的是第一個運算值。這用來在存取一個物件的屬性前檢查物件是否為空 (null) 非常有用:

js
var name = o && o.getName();

或是用來設預設值:

js
var name = otherName || "預設";

JavaScript 也有三元運算子 (tertiary operator),可以用來寫單行的條件陳述式:

js
var allowed = age > 18 ? "是" : "否";

switch 陳述式可以根據一個數字或字串做不同決定:

js
switch (action) {
  case "畫":
    drawit(); //開始畫
    break; //中斷
  case "吃":
    eatit(); //開始吃
    break; //中斷
  default: //預設
    donothing(); //不做任何事
}

如果你不加個 break (中斷)陳述式,執行的程式碼會往下「掉」一層。你很少會需要這樣 - 不過假如你真的要這樣,最好用個註解說明一下,這樣才方便除錯:

js
switch (a) {
  case 1: //往下「掉」一層
  case 2:
    eatit(); //開始吃
    break; //中斷
  default: //預設
    donothing(); //不做任何事
}

default 子句 (clause) 是選擇性的,可有可無。如果你喜歡的話,你可以在 switch 部分或 case 部分放表達式 (expression);兩者之間的比較使用的會是 === 運算子:

js
switch (1 + 3) {
  case 2 + 2:
    yay(); //耶!
    break; //中斷
  default: //預設
    neverhappens(); //根本不會發生
}

物件

JavaScript 物件是一系列的「名稱對數值組合」(name-value pair)。有鑑於此,它們和下列的東西很相近:

  • Python 的 dictionary (字典)
  • Perl 和 Ruby 的 hash (雜湊)
  • C 和 C++ 的 hash table (雜湊表)
  • Java 的 HashMap (雜湊地圖)
  • PHP 的 associative array (聯合陣列)

這種資料結構的普遍性證明了其多樣性。由於 JavaScript 的任何東西(核心類型以外)都是物件,任何 JavaScript 程式都自然而然的用到許多雜湊表查詢。還好這些查詢的速度都很快!

「名稱」的部分是個 JavaScript 字串,而「數值」可以是任何 JavaScript 值--包括物件。這可以讓你隨心所欲的建構複雜的資料結構。

建立空物件有種基本方法:

js
var obj = new Object();

以及:

js
var obj = {};

這兩者在意義上相等;後者叫做物件實體語法 (object literal),比較方便。早期並沒有物件實體語法,也就是為何許多程式碼用的還是舊的方法。

一旦建立了,一個物件的屬性可以用兩種方法存取:

js
obj.name = "小明";
var name = obj.name;

還有...

js
obj["name"] = "小明";
var name = obj["name"];

這兩者也是在意義上相等。第二種方法的優點是屬性的名稱可以在執行的時候以字串提供,也就是說可以動態的變動。這也可以用來取得和設定名稱是保留關鍵字 (reserved keyword) 的屬性:

js
obj.for = "Simon"; //語法錯誤
obj["for"] = "Simon"; //沒問題

物件實體語法可以一次把物件完全初始化:

js
var obj = {
  name: "胡蘿蔔", //名稱
  for: "小華", //給誰
  details: {
    //詳細資訊
    color: "橘", //顏色
    size: 12, //大小
  },
};

存取屬性也可以連在一起:

js
> obj.details.color
橘
> obj["details"]["size"]
12

陣列

JavaScript 的陣列其實是一種特殊的物件。它們的運作方式跟正常的物件很像(數字性的屬性只能透過 [] 語法進行存取),不過有個神奇的屬性,叫做「length」(長度)。這個屬性一定是陣列最高索引數加一。

建立陣列的舊方法如下:

js
> var a = new Array();
> a[0] = "狗";
> a[1] = "貓";
> a[2] = "雞";
> a.length
3

比較方便的語法便是使用陣列實體語法:

js
> var a = ["狗", "貓", "雞"];
> a.length
3

在陣列實體語法結尾留個空逗點在各瀏覽器間結果參差不齊,所以最好不要這樣:

js
> var a = ["狗", "貓", "雞", ]; //最好不要這麼做

注意--array.length 不一定是陣列的項目數。比方說:

js
> var a = ["狗", "貓", "雞"];
> a[100] = "狐";
> a.length
101

別忘了--陣列的 length 就是最高索引數加一。

如果你查詢一個不存在的陣列索引,得到的就是 undefined

js
> typeof(a[90])
undefined

利用上述,便可以像下列一樣在陣列上做迴圈:

js
for (var i = 0; i < a.length; i++) {
  //處理 a[i]
}

這樣不是很有效率,因為每迴圈一次就會查詢一次 length 屬性。比較好的做法是:

js
for (var i = 0, len = a.length; i < len; i++) {
  //處理 a[i]
}

一個更棒的寫法是:

js
for (var i = 0, item; (item = a[i]); i++) {
  //處理 item
}

這裡設定了兩個變數。for 迴圈中間指定變數值的部分會被測試是否為「真的」(truthy)--如果成功了,迴圈便會繼續。由於 i 每次都會加一,陣列內的每個項目會被照順序指定到變數 item。當偵測到「假的」(falsy) 項目時(如 undefined)迴圈便會停止。

注意--這個小技巧只該用在你確定不會含有「假的」值的陣列(比如說一陣列的物件或 DOM 節點)。假如你在可能含有 0 的數字資料或可能含有空字串的字串資料上做迴圈,最好還是用 i, j 的方式。

另外一個做迴圈的方法是用 for...in 迴圈。不過,假如有人用 Array.prototype 新增新的屬性,那些屬性也會被這種迴圈讀到:

js
for (var i in a) {
  //處理 a[i]
}

假如你要在陣列結尾加入項目,最安全的方法是這樣:

js
a[a.length] = item; //同 a.push(item);

由於 a.length 一定是最高索引數加一,你可以很確定你指定到的是陣列結尾的空間。

陣列附有一些方法 (method):

js
a.toString(), a.toLocaleString(), a.concat(item, ..), a.join(sep),
a.pop(), a.push(item, ..), a.reverse(), a.shift(), a.slice(start, end),
a.sort(cmpfn), a.splice(start, delcount, [item]..), a.unshift([item]..)
  • concat 結合,會傳回加入了新項目的新陣列
  • pop 會移除最後一個項目並將其傳回
  • push 會在結尾加入一或多個項目(就像前面提的 ar.length 方法)
  • slice 傳回副陣列
  • sort 進行排序,可選擇性的接受「比較性函數」(comparison function)
  • splice 讓你透過刪除一個區塊並以更多項目代替來修改陣列
  • unshift 會在開頭加入一或多個項目

函式

如同物件,函式 (function) 是瞭解 JavaScript 的核心元件。最基本的函式再簡單不過了:

js
function add(x, y) {
  var total = x + y;
  return total;
}

這示範了基本函式的一切。一個 JavaScript 函式可以接受零或多個有名 (named) 參數。函式內文 (body) 要有多少陳述式就有多少陳述式,且可以宣告對於函式而言本地 (local) 的變數。return 陳述式可以在任何時候傳回一值並終止函式。如果沒有 return 陳述式(或者光是 return,沒有值),JavaScript 便會傳回 undefined

有名參數比較像是做為參考,而非強制性的。你可以呼叫一個函式但不提供其要求的參數,引此傳入的便是 undefined

js
> add()
NaN // undefined 不能進行加法

你也可以傳入超過函式要求的參數數目:

js
> add(2, 3, 4)
5 // 加了前兩數,不理 4

這或許有些可笑,但函式在內文內還可以存取一個叫做 arguments 的變數,一個類似陣列的物件,內含所有遞給函式的值。改寫一下 add 函式便可以使其接受無限量的值:

js
function add() {
    var sum = 0;
    for (var i = 0, j = arguments.length; i < j; i++) {
        sum += arguments[i];
    }
    return sum;
}

> add(2, 3, 4, 5)
14

這樣並沒有比直接寫 2 + 3 + 4 + 5 來得有用。寫個算平均的函式吧:

js
function avg() {
    var sum = 0;
    for (var i = 0, j = arguments.length; i < j; i++) {
        sum += arguments[i];
    }
    return sum / arguments.length;
}
> avg(2, 3, 4, 5)
3.5

這樣滿有用的,不過又有新問題了。avg() 函式接受的是個逗號分隔的參數清單--不過如果你要取一陣列的平均值呢?你可以把函式重寫成這樣:

js
function avgArray(arr) {
    var sum = 0;
    for (var i = 0, j = arr.length; i < j; i++) {
        sum += arr[i];
    }
    return sum / arr.length;
}
> avgArray([2, 3, 4, 5])
3.5

但是最好是可以重複利用已經建立好的函式。幸運的是,JavaScript 可以讓你以一陣列的參數來呼叫一個函式。這靠的是使用任何函式物件的 apply() 方法。

js
> avg.apply(null, [2, 3, 4, 5])
3.5

apply() 的第二個參數便是做為一系列參數的陣列;第一個參數稍後才會討論。重點是,函式也是物件。

JavaScript 可以讓你建立匿名 (anonymous) 函式。

js
var avg = function () {
  var sum = 0;
  for (var i = 0, j = arguments.length; i < j; i++) {
    sum += arguments[i];
  }
  return sum / arguments.length;
};

這個語法上和 function avg() 形式相等。這是非常強大的功能,因為你可以藉此在平常該放表達式的地方塞入一個完整的函式定義。這可以讓你用在各種令人拍案叫絕的技巧上。下列可以把本地 (local) 參數「藏」起來--像 C 的 block scope 一樣:

js
> var a = 1;
> var b = 2;
> (function() {
    var b = 3;
    a += b;
})();
> a
4
> b
2

JavaScript 能讓你遞迴地呼叫函式。這在處理樹狀結構的時候特別有用,比如瀏覽器的 DOM

js
function countChars(elm) {
  if (elm.nodeType == 3) {
    // TEXT_NODE
    return elm.nodeValue.length;
  }
  var count = 0;
  for (var i = 0, child; (child = elm.childNodes[i]); i++) {
    count += countChars(child);
  }
  return count;
}

以上揭露了一個使用匿名函式的潛在問題:如果匿名函式沒有名稱,那要怎麼樣遞迴地自我呼叫?答案是使用 arguments 物件。該物件除了提供一系列的參數以外,還提供了一個叫做 arguments.callee 的屬性。這個屬性所指向的是目前的函式,因此可以用來做遞迴的呼叫:

js
var charsInBody = (function (elm) {
  if (elm.nodeType == 3) {
    // TEXT_NODE
    return elm.nodeValue.length;
  }
  var count = 0;
  for (var i = 0, child; (child = elm.childNodes[i]); i++) {
    count += arguments.callee(child);
  }
  return count;
})(document.body);

由於 arguments.callee 是目前的函式,而所有的函式都是物件,你可以因此用 arguments.callee 來在多次呼叫同個函式之間儲存資料。這個函式會記得它被呼叫過多少次:

js