- 文档
- 教程
【免费下载链接】learnxinyminutes-docs
Code documentation written as code! How novel and totally my idea!
本篇技术指南以本仓库 zh-tw/javascript.md(及其英文原版 javascript.md)为骨架,系统讲解 JavaScript 这门语言的完整知识体系:从数字、字符串与运算符,到变量、数组与对象,再到函数、作用域、闭包、原型链与 ES6 新语法。读完本文,你将掌握 JavaScript 与 C/Java 在语法上的异同、其独特的对象模型(原型继承)以及现代 ES6 编码方式,并能在 Node.js 或浏览器控制台中直接运行文中的所有示例进行验证。
本仓库(learnxinyminutes-docs)的核心理念是"把代码文档写成代码本身"(见 README.md):每份教程都以可直接运行的带注释代码呈现,边写代码边讲解。因此本文延续这一风格,所有代码块均可复制到 Node.js 环境或浏览器开发者工具中逐行执行,注释即讲解。
一、JavaScript 的历史定位与运行环境
JavaScript 由網景公司(Netscape)的布蘭登·艾克(Brendan Eich)於 1995 年創建。它最初被設計為一種更簡單的網站腳本語言,用於補足 Java 在更複雜的網路應用程式中的使用;但由於它與網頁的高度整合,以及瀏覽器對它的原生支援,使得它在網頁前端的應用遠比 Java 更加普及。
JavaScript 並不僅限於網頁瀏覽器:Node.js 提供了一個可執行 Google Chrome 的 V8 JavaScript 引擎的獨立運行環境,正變得越來越熱門。這意味著本文的絕大多數示例既可以在瀏覽器控制台運行,也可以透過node命令在終端執行——後者是驗證本文代碼最便捷的方式。
JavaScript 具備類似 C 語言的語法,所以若您曾使用過 C 或 Java,許多基本語法會相當熟悉。但請特別注意:雖然在語法和名稱上與 Java 很像,JavaScript 的物件模型(原型繼承)卻與 Java 有顯著不同——這是本文後半部分的重點。
二、註解、陳述式與分號
// 這是單行註解 /* 這是 多行註解 */ // 陳述式可以用分號(;)終止 doStuff(); // ... 然而不一定要加分號,當換行時會自動插入分號(除了某些特殊情況)。 doStuff() // 避免意外結果的情況,本文會繼續使用分號這裡的"自動插入分號"(Automatic Semicolon Insertion,簡稱 ASI)是初學者最容易踩的坑之一。ASI 的觸發時機與換行位置密切相關:如果某一行以可能結束陳述式的字符結尾,解析器會在換行處補入分號。後文第 5 節會展示一個由 ASI 導致的經典陷阱(return換行導致回傳undefined),請務必留意。
三、數字、字串和運算子
3.1 唯一的數字型別
// JavaScript 只有一種數字型別(也就是 64 位元 IEEE 754 雙精度浮點數)。 // 雙精度浮點數有一個 52 位的尾數,足以精確儲存整數最大至 9✕10¹⁵ 的整數。 3; // = 3 1.5; // = 1.5這是 JavaScript 與 C/Java 最大的差異之一:沒有int、float、double之分,一切數字都是雙精度浮點數。52 位尾數意味著超過9✕10¹⁵的整數可能無法精確表示,這在處理大數(例如時間戳、ID)時要格外小心。
3.2 基本算術與浮點精度
// 所有基本算術運算都如您預期。 1 + 1; // = 2 0.1 + 0.2; // = 0.30000000000000004 8 - 1; // = 7 10 * 2; // = 20 35 / 5; // = 7 // 包括無法整除的除法運算。 5 / 2; // = 2.5 // 以及餘數運算。 10 % 2; // = 0 30 % 4; // = 2 18.5 % 7; // = 4.5 // 位元運算也是如此,當執行位元運算時,浮點數會轉換「最多」32位元的有符號整數 1 << 2; // = 4 // 以括號決定運算優先級。 (1 + 3) * 2; // = 8注意0.1 + 0.2的結果是0.30000000000000004,這是 IEEE 754 浮點數表示法的固有現象,並非 JavaScript 的 bug;任何使用雙精度浮點數的語言(包括 Java 的double)都會有同樣的表現。另外,餘數運算%對浮點數同樣有效(如18.5 % 7)。位元運算時浮點數會被截斷為最多 32 位元的有符號整數。
3.3 特殊數值與布林值
// 有三個非數值的值: Infinity; // 1/0 的結果 -Infinity; // -1/0 的結果 NaN; // 0/0 的結果 // 也有布林值。 true; false;NaN(Not a Number)是"非數值"的結果,注意NaN不等於自身(NaN === NaN為false),判斷時應使用Number.isNaN()或全域isNaN()。
3.4 比較、相等與型別強制轉型
// 以驚嘆號(!)執行否定運算 !true; // = false !false; // = true // 相等運算 `===` 1 === 1; // = true 2 === 1; // = false // 不相等運算 !== 1 !== 1; // = false 2 !== 1; // = true // 更多比較運算子 1 < 10; // = true 1 > 10; // = false 2 <= 2; // = true 2 >= 2; // = true // 以加號(+)進行字串的串接 "Hello " + "world!"; // = "Hello world!" // 也可以串接字串以外的資料型別 "1, 2, " + 3; // = "1, 2, 3" "Hello " + ["world", "!"]; // = "Hello world,!" // 這可能導致一些奇怪的行為 13 + !0; // 14 "13" + !0; // '13true' // 以 `<` 和 `>` 進行比較運算 "a" < "b"; // = true // 使用「兩個等號」(==)做運算時,會執行資料強制轉型 "5" == 5; // = true null == undefined; // = true // 除非使用「三個等號」(===) "5" === 5; // = false null === undefined; // = false這是 JavaScript 中最需要警惕的語義之一:
===/!==(嚴格相等):比較值與型別,不會做任何強制轉型,是推薦的寫法;==/!=(寬鬆相等):會執行型別強制轉型(type coercion),例如"5" == 5為true、null == undefined為true,容易產生非直覺的結果。
+同時用於數值加法與字串串接,當操作數中混有字串時,數值會被轉成字串(如"1, 2, " + 3),甚至產生13 + !0(!0為true,相加得 14)與"13" + !0(字串串接得'13true')這類怪異行為——這正是後文要強調的隱含型別轉換的威力與危險。
3.5 字串操作與特殊值
// 您可以使用 `charAt` 來取得字串中的字符 "This is a string".charAt(0); // = 'T' // 或使用 `substring` 獲得更大的區塊。 "Hello world".substring(0, 5); // = "Hello" // `length` 是一個屬性,因此不要使用 `()`。 "Hello".length; // = 5 // 還有 `null` 和 `undefined` 兩個特殊值。 null; // 用於表示刻意指定為空值 undefined; // 用來表示目前尚未指定值(儘管 `undefined` 實際上本身是一個值) // `false`、`null`、`undefined`、`NaN`、`0` 及 `""`(空字串)皆為偽值(falsy), // 其他的皆為真值(truthy)。 // 特別注意,`0` 是偽值,`"0"` 則是真值,儘管 0 == "0"。(因為隱含地轉型)關鍵知識點:
charAt(0)取第一個字符,substring(0, 5)取前 5 個字符(含頭不含尾);length是屬性而非方法,不要加();null表示"刻意設定為空",undefined表示"尚未指定值"(兩者通過==相等、通過===不相等,見上文);- 偽值(falsy)完整清單:
false、null、undefined、NaN、0、""(空字串)。其餘皆為真值。特別注意0是偽值而"0"是真值——儘管0 == "0"因隱含轉型而成立。
四、變數、陣列與物件
4.1 變數宣告與作用域陷阱
// 以 `var` 關鍵字宣告變數。JavaScript 是動態型別,因此無需指定型別。 // 使用一個等號 `=` 來賦值。 var someVar = 5; // 若忽略使用 `var` 關鍵字,也不會得到錯誤 someOtherVar = 10; // ...但是您定義的變數將在自動建立在全域,而不在您定義的作用域中。 // 若在定義變數時未賦予初始值,則預設為 `undefined`。 var someThirdVar; // = undefined // 若要一次宣告多個變數,可以使用逗號分隔 var someFourthVar = 2, someFifthVar = 4; // 變數的數學運算有一些簡寫法: someVar += 5; // 等效於 somevar = somevar + 5,現在為 10 someVar *= 10; // 現在 someVar 為 100 // 對於增減 1 的運算,還有更簡略的寫法: someVar++; // 現在 someVar 為 101 someVar--; // 回到 100重要警示:宣告變數時不要省略var。省略後雖然不會報錯,但變數會被建立在全域作用域,而不是你所在的作用域——這會造成命名污染,是經典的 bug 來源(後文的 IIFE 模式正是為了解決這個問題)。未賦初值的變數預設為undefined。+=、*=、++、--等簡寫與 C/Java 一致。
4.2 陣列:有序列表
// 陣列是以任何型別的資料所組成、有順序性的列表。 var myArray = ["Hello", 45, true]; // 可使用中括號(方括號)`[]` 語法存取其成員。 // 陣列的索引值(index)從 0 開始。 myArray[1]; // = 45 // 陣列是可變的,並有隨成員數量變動的長度 `length`。 myArray.push("World"); myArray.length; // = 4 // 於指定的索引值新增/修改陣列的成員 myArray[3] = "Hello"; // 從陣列的最前端或最後端新增或刪除元素 myArray.unshift(3); // 新增元素至最前端 someVar = myArray.shift(); // 移除第一個元素並回傳 myArray.push(3); // 新增元素至最後端 someVar = myArray.pop(); // 移除最後一個元素並回傳 // 以分號 `;` 結合陣列的所有元素 var myArray0 = [32, false, "js", 12, 56, 90]; myArray0.join(";"); // = "32;false;js;12;56;90" // 取得索引 1(包括)到 4(排除)元素的子陣列 myArray0.slice(1, 4); // = [false, "js", 12] // 從索引 2 開始刪除 4 個元素,然後在那裡插入字串 "hi"、"wr" 和"ld",並回傳刪除的子陣列 myArray0.splice(2, 4, "hi", "wr", "ld"); // = ["js", 12, 56, 90] // myArray0 === [32, false, "hi", "wr", "ld"]陣列操作速查:
| 方法 | 作用 | 是否修改原陣列 |
|---|---|---|
push(x)/pop() | 在尾端新增 / 移除並回傳 | 是 |
unshift(x)/shift() | 在頭端新增 / 移除並回傳 | 是 |
join(sep) | 以分隔符串接所有元素為字串 | 否 |
slice(a, b) | 取[a, b)子陣列(含頭不含尾) | 否 |
splice(a, n, ...items) | 從索引a刪除n個並插入新元素,回傳被刪除的子陣列 | 是 |
特別注意slice與splice的區別:slice是非破壞性的取子陣列,splice是破壞性的刪除+插入,兩者參數語義不同,極易混淆。
4.3 物件:鍵值對集合
// JavaScript 的物件等同於其他語言中的「字典」(Dictionary)或「映射」(Map): // 無順序性的鍵值對(key value pair)集合。 var myObj = { key1: "Hello", key2: "World" }; // 鍵的名稱是字串,若它們是有效的 JavaScript 標識字,則不需要使用引號。 // 值則可以是任何資料型別。 var myObj = { myKey: "myValue", "my other key": 4 }; // 物件屬性也可以使用下標語法存取 myObj["my other key"]; // = 4 // ...或使用 `.` 來存取,前提是該鍵值必須是有效的標識字。 myObj.myKey; // = "myValue" // 物件是可變的;其值可修改,並可新增新的鍵值。 myObj.myThirdKey = true; // 如果您嘗試存取不存在的值,將得到 `undefined` 值。 myObj.myFourthKey; // = undefined物件的鍵是字串,但若鍵名是有效的標識字(identifier,如myKey),可省略引號。存取有兩種方式:點語法(obj.key,要求鍵是有效標識字)與下標語法(obj["my other key"],可處理含空格等特殊字符的鍵)。物件是可變的,可隨時修改值或新增鍵;存取不存在的鍵回傳undefined。
五、邏輯與控制結構
5.1 if / while / do-while / for
// `if` 結構,如同其他語言 var count = 1; if (count == 3){ // count 等於 3 時執行 } else if (count == 4){ // count 等於 4 時執行 } else { // 其他情況下執行 } // `while` 迴圈 while (true){ // 無窮迴圈! } // Do-while 迴圈類似 While 迴圈,但它至少執行一次 var input; do { input = getInput(); } while (!isValid(input)); // `for` 迴圈和 C、Java 語言一樣: // 初始化; 繼續條件; 迭代。 for (var i = 0; i < 5; i++){ // 會執行 5 次 }if、while、do-while、for的語法與 C/Java 幾乎完全相同。do-while與while的區別在於至少執行一次再檢查條件,適合"先取輸入再驗證"的場景(如上面的getInput()示例)。
5.2 迴圈標籤(label)與 break
// 類似 Java,利用迴圈標籤(label)來終止外層的迴圈 outer: for (var i = 0; i < 10; i++) { for (var j = 0; j < 10; j++) { if (i == 5 && j ==5) { break outer; // 不但終止內部迴圈,也終止外部迴圈 } } }在巢狀迴圈中,單獨的break只能跳出最內層迴圈。透過在迴圈前加標籤(如outer:),break outer可以直接終止外層迴圈——這與 Java 的行為一致,在需要提前退出多層迴圈時非常實用。
5.3 for/in 與 for/of
// for/in 陳述式可以迭代物件中的所有屬性。 var description = ""; var person = { fname: "Paul", lname: "Ken", age: 18 }; for (var x in person){ description += person[x] + " "; } // description 為 'Paul Ken 18 ' // for/of 陳述式允許迭代可迭代物件(包括內建的字串、陣列、例如類陣列(array-like)參數 // 或 NodeList 物件、Typedarray 物件、映射(Map)和集合(Set), // 及用戶自定義的可迭代物件(iterables))。 var myPets = ""; var pets = ["cat", "dog", "hamster", "hedgehog"]; for (var pet of pets){ myPets += pet + " "; } // myPets 為 'cat dog hamster hedgehog '兩個迭代陳述式的分工不同:
for/in:迭代物件的屬性名(key),常配合下標語法person[x]取值;它會沿著原型鏈向上查找(見第 7 節原型鏈的說明);for/of:迭代可迭代物件(iterable)的值,支援字串、陣列、類陣列的arguments/NodeList、TypedArray、Map、Set以及用戶自定義的 iterable。
5.4 邏輯運算子、短路求值與 switch
// `&&` 是邏輯且(and), `||` 是邏輯或(or) if (house.size == "big" && house.colour == "blue"){ house.contains = "bear"; } if (colour == "red" || colour == "blue"){ // colour 等於 "red" 或 "blue" 時執行 } // `||` 可用來設定初始值,稱做「短路」(short circuit)陳述式 var name = otherName || "default"; // `switch` 陳述式使用 `===` 來檢查相等性。 // 在每個 case 後使用 `break`,否則之後的 case 也會被執行。 grade = 'B'; switch (grade) { case 'A': console.log("Great job"); break; case 'B': console.log("OK job"); break; case 'C': console.log("You can do better"); break; default: console.log("Oy vey"); break; }**短路求值(short-circuit)**是極常用的技巧:&&在左側為假時不會求值右側,||在左側為真時不會求值右側。因此var name = otherName || "default"可以在otherName為偽值(如undefined、空字串)時,自動回退到"default"——這是最經典的設定默認值方式。
switch使用===嚴格相等判斷;每個case結尾必須break,否則會"穿透"(fall-through)執行後續所有 case——這是有意設計但也容易出錯的特性。
六、函式、作用域與閉包
6.1 函式宣告與 ASI 陷阱
// JavaScript 函式是使用 `function` 關鍵字來宣告的。 function myFunction(thing) { return thing.toUpperCase(); } myFunction("foo"); // = "FOO" // 值得注意的是,要回傳的值必須開始於關鍵字 `return` 那一行, // 否則會因為自動插入分號,而將回傳 `undefined`。 // 在使用 Allman 程式碼風格時要特別注意。 function myFunction() { return // <- 分號在此自動插入 { thisIsAn: 'object literal' }; } myFunction(); // = undefined第二個例子是 ASI(自動插入分號)的經典陷阱:由於return後緊跟換行,解析器在return之後自動插入分號,函式立即回傳undefined,後面的物件字面量永遠不會被執行。return的回傳值必須與return關鍵字在同一行。使用 Allman 風格(大括號另起一行)時尤其容易踩坑。
6.2 函式是一等物件(first-class)
// JavaScript 函式為一等物件(first-class objects), // 所以它們可以被重新賦值給不同的變數, // 並作為參數傳遞給其他函式 - 例如一個事件處理函式: function myFunction() { // 這段程式碼將在 5 秒後執行 } setTimeout(myFunction, 5000); // 註:setTimeout 並不是 JS 語言的一部分,而是由瀏覽器和 Node.js 提供的 API。 // setInterval 是瀏覽器提供的另一個 API function myFunction() { // 這段程式碼每 5 秒執行一次 } setInterval(myFunction, 5000); // 函式物件甚至不需要用名稱來宣告 - 你可以直接在另一個函數的參數中直接定義匿名函式。 setTimeout(function(){ // 這段程式碼將在 5 秒後執行 }, 5000);"一等物件"意味著函式可以:賦值給變數、作為參數傳遞、作為回傳值回傳。setTimeout(延遲執行一次)與setInterval(週期執行)是瀏覽器與 Node.js 提供的 API,它們接受函式作為回呼(callback)。匿名函式可以直接內聯在參數中,這是事件驅動編程的基礎。
6.3 函式作用域與 IIFE 模式
// JavaScript 具有函式作用域(scope);函式擁有自己的作用域,但其他區塊(block)則沒有。 if (true){ var i = 5; } i; // = 5 - 並非在其他語言所預期的 `undefined` // 這導致了一種常見的使用方式,即「立即執行匿名函式」,它可以防止臨時變數洩漏到全域作用域。 (function(){ var temporary = 5; // 我們可以透過賦值給「全域物件」來訪問全域作用域, // 在網頁瀏覽器中,這個全域物件始終是 `window`。 // 在非瀏覽器環境中(例如 Node.js),全域物件可能有不同的名稱。 window.permanent = 10; })(); temporary; // 拋出錯誤 ReferenceError permanent; // = 10這是理解 JavaScript 作用域的關鍵:
- 函式作用域(function scope):
var宣告的變數在整個函式內可見,但if、for等區塊不會建立獨立作用域——所以if (true) { var i = 5; }之後i依然是 5,而非其他區塊作用域語言中的undefined; - IIFE(立即執行函式):
(function(){ ... })()將臨時變數封閉在函式作用域內,避免洩漏到全域。在全域物件(瀏覽器中是window,Node.js 中名稱不同)上顯式賦值,才能建立全域變數。
6.4 閉包(Closure):JavaScript 最強大的特性
// 閉包:JavaScript 最強大的特性之一。 // 若一個函式在另一個函式內部定義,即使外部函式已經執行結束, // 內部函式仍可以存取外部函式的所有變數。 function sayHelloInFiveSeconds(name){ var prompt = "Hello, " + name + "!"; // 預設情況下,內部函式會被置於局部作用域中,就如同它們是以 `var` 宣告。 function inner(){ alert(prompt); } setTimeout(inner, 5000); // setTimeout 是非同步的,所以 sayHelloInFiveSeconds 函式會立即退出。 // 而 setTimeout 會在之後呼叫 inner 函式。 // 然而,由於 inner 函式被「閉合包含」(closed over)在 sayHelloInFiveSeconds 函式中, // 因此當它最終被呼叫時,仍然可以存取 `prompt` 變數。 } sayHelloInFiveSeconds("Adam"); // 將在 5 秒後跳出 "Hello, Adam!" 訊息閉包的運作原理:inner函式被定義在sayHelloInFiveSeconds內部,因此它捕獲(closed over)了外部函式的整個詞法環境,包括prompt變數。setTimeout是非同步的,sayHelloInFiveSeconds會立即返回,但 5 秒後inner被呼叫時,依然能讀取到prompt的值——這就是閉包:內部函式記住了它被定義時所在的作用域。閉包被廣泛用於事件處理、回呼、模組封裝與狀態保存。
七、物件進階:this、call/apply/bind 與建構函式
7.1 this 的動態綁定
// 物件可以包含函式 var myObj = { myFunc: function(){ return "Hello world!"; } }; myObj.myFunc(); // 回傳 "Hello world!" // 當物件裡的函式被呼叫時,它們可以使用 `this` 關鍵字來存取所屬物件的其他成員。 myObj = { myString: "Hello world!", myFunc: function() { return this.myString; } }; myObj.myFunc(); // 回傳 "Hello world!" // `this` 的設定與函式如何被呼叫有關,而非定義的位置。 // 因此,若我們的函式不是在物件的脈絡(context)中被呼叫,就無法正常運作。 var myFunc = myObj.myFunc; myFunc(); // = undefined // 反之,一個函式可以被賦值給物件並透過 `this` 獲得對它的存取權限, // 即使它在定義時並未依附於該物件上。 var myOtherFunc = function() { return this.myString.toUpperCase(); }; myObj.myOtherFunc = myOtherFunc; myObj.myOtherFunc(); // 回傳 "HELLO WORLD!"this的取值取決於函式如何被呼叫,而非在何處定義——這是 JavaScript 初學者最大的困惑點。當myObj.myFunc()以"物件方法"形式呼叫時,this指向myObj;但把函式取出賦值給變數後再呼叫(myFunc()),this不再指向myObj(非嚴格模式下指向全域物件,因此this.myString為undefined)。反之,任何函式只要被賦值為物件的方法再以物件方法形式呼叫,就能透過this存取該物件。
7.2 call / apply / bind:顯式控制 this
// 我們也可以在呼叫函式時使用 `call` 或 `apply` 來指定函式的脈絡。 var anotherFunc = function(s) { return this.myString + s; }; anotherFunc.call(myObj, " And Hello Moon!"); // = "Hello World! And Hello Moon!" // `apply` 函式的用法幾乎一樣,差別在於要用陣列的格式傳遞參數 anotherFunc.apply(myObj, [" And Hello Sun!"]); // = "Hello World! And Hello Sun!" // 這在處理接受一系列參數時很有用,特別是當您想要傳遞一個陣列時。 Math.min(42, 6, 27); // = 6 Math.min([42, 6, 27]); // = NaN(噢!) Math.min.apply(Math, [42, 6, 27]); // = 6 // 然而 `call` 和 `apply` 只是暫時的。當我們希望它永久有效時,我們可以使用 `bind`。 var boundFunc = anotherFunc.bind(myObj); boundFunc(" And Hello Saturn!"); // = "Hello World! And Hello Saturn!" // `bind` 也可以用於部分應用,例如:柯里化(curry)的函式。 var product = function(a, b) { return a * b; }; var doubler = product.bind(this, 2); doubler(8); // = 16三種顯式指定this的方式:
| 方法 | 參數傳遞方式 | 是否永久綁定 |
|---|---|---|
func.call(ctx, arg1, arg2, ...) | 逐一列出 | 否(僅本次呼叫) |
func.apply(ctx, [arg1, arg2, ...]) | 陣列形式 | 否(僅本次呼叫) |
func.bind(ctx, ...) | 可同時做部分應用 | 是(產生新函式) |
apply特別適合把陣列展開為參數列表:Math.min不接受陣列,直接傳陣列會得到NaN,但Math.min.apply(Math, [42, 6, 27])可以正確得到 6。bind還會產生**柯里化(currying)**效果:product.bind(this, 2)把第一個參數固定為 2,得到"加倍器"doubler。
7.3 建構函式與 new
// 當您使用 `new` 關鍵字呼叫一個函式時,會建立一個新的物件, // 並透過 `this` 關鍵字使該物件可用於該函式。用此方式呼叫的函式被稱為「建構函式」。 var MyConstructor = function(){ this.myNumber = 5; }; myNewObj = new MyConstructor(); // = { myNumber: 5 } myNewObj.myNumber; // = 5使用new呼叫函式時:建立一個新物件 → 透過this讓該函式可以初始化它 → 回傳該物件。這類函式被稱為建構函式(constructor)。注意此時this.myNumber = 5實際上就是在初始化新物件的屬性。
八、原型(Prototype)與繼承:JavaScript 的物件模型核心
8.1 原型鏈的查找機制
// 與大多數其他流行的物件導向語言不同,JavaScript 沒有從類別(class)藍圖建立 // 實例(instance)的概念;相反地,JavaScript 將實例化(instantiation)和 // 繼承(inheritance)結合成單一的概念:「原型」(prototype)。 // 每個 JavaScript 物件都有一個「原型」(prototype)。 // 當你試圖存取一個物件上不存在的屬性時,直譯器(interpreter)會嘗試查找它的原型。 // 某些 JS 實作允許你透過屬性 `__proto__` 存取原型。雖然這對於解釋原型很有幫助, // 但它不是標準的操作方式;稍後會介紹使用原型的標準方法。 var myObj = { myString: "Hello world!" }; var myPrototype = { meaningOfLife: 42, myFunc: function() { return this.myString.toLowerCase(); } }; myObj.__proto__ = myPrototype; myObj.meaningOfLife; // = 42 // 函式照常運作。 myObj.myFunc(); // = "hello world!" // 當然,若您的屬性不在原型上,則會搜尋原型的原型,以此類推。 myPrototype.__proto__ = { myBoolean: true }; myObj.myBoolean; // = true // 這裡沒有涉及任何複製;每個物件都儲存了一個指向其原型的參考。 // 這意味著我們可以修改原型,而此修改將反映在任何地方。 myPrototype.meaningOfLife = 43; myObj.meaningOfLife; // = 43JavaScript 的物件模型與 Java 的類別繼承有本質區別:
- 沒有"類別藍圖 → 實例"的概念,而是把實例化與繼承合而為一:每個物件都有一個原型;
- 當存取物件上不存在的屬性時,解釋器沿著**原型鏈(prototype chain)**逐層查找(物件的原型、原型的原型……直到
null); - 這裡沒有任何複製:物件儲存的是指向原型的參考,因此修改原型(如把
meaningOfLife從 42 改成 43)會即時反映到所有繼承它的物件上; __proto__是部分實作提供的非標準屬性,僅適合教學演示,生產代碼應使用下文標準方式。
8.2 for/in 與 hasOwnProperty
// for/in 陳述句允許循覽物件的屬性,沿著原型鏈向上查找,直到遇到 `null` 原型為止。 for (var x in myObj){ console.log(myObj[x]); } // 印出: // Hello world! // 43 // [Function: myFunc] // true // 若只考慮直接附加在物件本身而非其原型上的屬性,請使用 `hasOwnProperty()` 做檢查。 for (var x in myObj){ if (myObj.hasOwnProperty(x)){ console.log(myObj[x]); } } // 印出: // Hello world!for/in會沿著原型鏈一路向上列出屬性(包括繼承來的meaningOfLife、myFunc、myBoolean)。若只想處理物件自身的屬性,必須用hasOwnProperty(x)過濾——這是遍歷物件的標準防護寫法。
8.3 指定原型建立物件的兩種標準方式
// 我們提到 `__proto__` 是非標準用法,而且沒有標準的方法來修改現有物件的原型。 // 然而,有兩種方法可以建立具有指定原型的新物件。 // 第一種方法是 `Object.create`。 var myObj = Object.create(myPrototype); myObj.meaningOfLife; // = 43 // 第二種方法與建構函式有關,可在任何地方使用。建構函式有一個稱為 `prototype` 的屬性。 // 這「不是」建構函式本身的原型;而是當使用該建構函式和 `new` 關鍵字建立新物件時,所賦予的原型。 MyConstructor.prototype = { myNumber: 5, getMyNumber: function(){ return this.myNumber; } }; var myNewObj2 = new MyConstructor(); myNewObj2.getMyNumber(); // = 5 myNewObj2.myNumber = 6; myNewObj2.getMyNumber(); // = 6建立"帶指定原型的新物件"有兩種標準方法:
Object.create(proto):直接建立一個以proto為原型的新物件;- 建構函式的
prototype屬性:MyConstructor.prototype不是建構函式自身的原型,而是每次用new MyConstructor()建立新物件時賦予新物件的原型。因此myNewObj2可以透過原型繼承getMyNumber方法,同時可以覆寫自有屬性(myNewObj2.myNumber = 6)而不影響原型。
8.4 內建型別的包裝物件與原型擴充
// 內建型別如字串和數字也有建構函式,可以建立等效的包裝物件(wrapper object)。 var myNumber = 12; var myNumberObj = new Number(12); myNumber == myNumberObj; // = true // 然而,它們並非嚴格相等。 typeof myNumber; // = 'number' typeof myNumberObj; // = 'object' myNumber === myNumberObj; // = false if (0){ // 這段程式碼不會執行,因為 0 是偽值。 } if (new Number(0)){ // 這段程式碼將會執行,因為包裝過的數字是物件,而物件皆為真值。 } // 然而,包裝物件和常見的內建型別共享一個原型,所以你實際上可以為字串擴充功能,例如: String.prototype.firstCharacter = function(){ return this.charAt(0); }; "abc".firstCharacter(); // = "a"內建型別(Number、String等)也有建構函式,可建立包裝物件。包裝物件與原始值透過==相等,但typeof分別是'object'與'number',因此===不相等;且包裝物件永遠是真值(new Number(0)為真,而0為偽)。關鍵技巧是:包裝物件與普通原始值共享同一個原型,因此可以透過修改String.prototype來為所有字串增加方法(如firstCharacter)。
8.5 Polyfill:用原型擴充實現新功能
// 此技巧常被用於「填充」(polyfilling),即在舊版的 JavaScript 中 // 實作新版 JavaScript 才有的功能,以便它們可以在較舊的環境(例如過時的瀏覽器)中使用。 // 舉例來說,方才提到的 `Object.create` 並非在舊瀏覽器上使用, // 但我們仍然可以透過這個 polyfill 來補足其功能: if (Object.create === undefined){ // 若此方法存在,則不覆蓋 Object.create = function(proto){ // 製作一個具有正確原型的暫時建構函式 var Constructor = function(){}; Constructor.prototype = proto; // 然後使用它來建立一個具有適當原型的新物件 return new Constructor(); }; }**Polyfill(填充)**是前端兼容性的核心技巧:用舊版 JavaScript 的子集重新實現新版本才有的功能,使舊瀏覽器也能運行。上述Object.create的 polyfill 是極佳範例——它巧妙地利用"臨時建構函式 + 設定其prototype+new"來合成一個具有指定原型的新物件,完全基於我們在前文學到的原型機制。注意開頭的if (Object.create === undefined)保護:若環境已原生支援則不覆蓋。
九、ES6 新語法:let、const 與箭頭函式
9.1 let:真正的區塊作用域
// `let` 關鍵字讓您在語彙作用域(lexical scope)中定義變數, // 而不像 `var` 關鍵字那樣在函式作用域(function scope)中定義。 let name = "Billy"; // 以 `let` 關鍵字定義的變數,可以賦予新值。 name = "William";let解決了第 6.3 節提到的問題:它提供詞法(區塊)作用域,if、for等區塊內宣告的變數不會洩漏出去——這是對var函式作用域的重大改進,也是現代 JavaScript 的首選宣告方式。
9.2 const:不可重新賦值
// `const` 關鍵字如同 `let` 可在語彙作用域中定義變數,差別在於,一旦賦值後就不能更改其值。 const pi = 3.14; pi = 4.13; // 此操作並不合法。const在let的詞法作用域基礎上,增加了不可重新賦值的約束(對pi重新賦值會拋出錯誤)。注意const保證的是"綁定不可變",而非"值不可變"——物件的屬性仍可修改。
9.3 箭頭函式(lambda 語法)
// ES6 中有一種新的函式語法,稱為「lambda 語法」(lambda syntax)。 // 這允許函式在語彙作用域中定義,如同 `const` 和 `let` 來定義變數。 const isEven = (number) => { return number % 2 === 0; }; isEven(7); // 回傳 false 值 // 「等效」於以下傳統函式的宣告方式: function isEven(number) { return number % 2 === 0; }; // 前面特別強調「等效」一詞,是因為使用 lambda 語法定義的函式不能在定義之前被呼叫。 // 以下爲錯誤示範: add(1, 8); const add = (firstNumber, secondNumber) => { return firstNumber + secondNumber; };箭頭函式((params) => { ... })是 ES6 的核心新語法,讓函式可以像const/let變數一樣在詞法作用域中宣告。與傳統function宣告的關鍵差異:傳統函式宣告會被"提升"(hoisting),可在定義前呼叫;而箭頭函式是賦值給const的,不能在定義之前呼叫(上面的add(1, 8)會拋出錯誤)。此外,箭頭函式不綁定自己的this(詞法綁定),這在回呼函式場景中有重要意義。
十、延伸閱讀與實戰驗證
本指南聚焦 JavaScript語言本身。若想深入,可依以下方向延伸(均為本倉庫內相關資源,便於對照學習):
- 多語言對照:若您熟悉其他語言,可對比閱讀倉庫內 python.md、java.md、c++.md 等同風格教程,快速理解 JavaScript 在語法與物件模型上的異同;
- 簡體中文版本:對照閱讀 zh-cn/javascript.md 可獲得不同譯法與表述;
- Typescript 進階:學習 typescript.md 可了解如何在 JavaScript 之上引入靜態型別;
- Node.js 生態:閱讀 nodejs.md(若倉庫內存在)或 npm.md(若倉庫內存在)可掌握伺服器端 JavaScript 的開發流程——請以倉庫實際文件清單為準。
驗證方式:本文所有代碼示例都是可執行的。最便捷的方式是安裝 Node.js 後,將任一節的程式碼存入test.js並執行node test.js,或在瀏覽器開發者工具的 Console 面板中逐行貼上執行,即可即時觀察註解中標注的結果值。建議重點動手驗證三個易錯點:0.1 + 0.2的浮點精度、return換行的 ASI 陷阱、以及this在不同呼叫方式下的取值。
本指南的內容結構(基礎語法 → 資料結構 → 控制流程 → 函式與閉包 → 物件模型與原型 → ES6)完整對應 zh-tw/javascript.md 的章節脈絡,讀者可隨時回到原文逐段核對;若希望參與完善本教程(修訂翻譯、補充示例),請遵循 CONTRIBUTING.md 中的貢獻規範提交 Pull Request。
- 文档
- 教程
【免费下载链接】learnxinyminutes-docs
Code documentation written as code! How novel and totally my idea!
相关推荐
learnxinyminutes-docs 项目 JavaScript 快速入门:从基础语法到原型链与 ES6 的完整实战指南
learnxinyminutes docs 项目 JavaScript 快速入门:从基础语法到原型链与 ES6 的完整实战指南 导读 本篇以 learnxiny
文档教程learnxinyminutes-docs 西班牙语版 JavaScript 入门指南:从基础语法到原型链的完整实战解析
learnxinyminutes docs 西班牙语版 JavaScript 入门指南:从基础语法到原型链的完整实战解析 本指南以 learnxinyminut
文档教程learnxinyminutes-docs 仓库 JavaScript 速成指南:从基础语法到作用域、闭包与原型链
learnxinyminutes docs 仓库 JavaScript 速成指南:从基础语法到作用域、闭包与原型链 本篇技术指南以 learnxinyminut
文档教程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考