[基礎課程] JavaScript 教學(二):資料處理
JavaScript 是現代網頁開發不可或缺的程式語言,它讓原本靜態的 HTML 網頁能夠擁有動態互動功能。本教學將從零開始,循序漸進地介紹 JavaScript 的基礎觀念。
JavaScript 是一種「腳本語言」(Scripting Language),最初設計用於網頁瀏覽器中,讓開發者能夠對瀏覽器下達指令,動態操作網頁內容。透過 JavaScript,我們可以根據特定的語法,要求瀏覽器執行各種動作,例如:
- 動態修改畫面上的 HTML 結構或樣式
- 偵測並回應使用者的互動(如點擊、輸入、滑鼠移動等事件)
- 執行各種運算與資料處理
- 與伺服器進行資料交換(如 AJAX 技術)
小技巧:現代 JavaScript 的應用
除了在瀏覽器中操作網頁,現代 JavaScript 也能在伺服器端(如 Node.js)、行動裝置、桌面應用程式等多種環境中運行,應用範圍非常廣泛。
學習環境準備:
- 現代瀏覽器(Chrome、Firefox、Safari、Edge)
- 使用 F12 開啟開發者工具
- 在 Console 分頁中練習程式碼
- 建議安裝 Visual Studio Code 作為程式編輯器
大型資料型別與處理
當我們需要處理大量相關資料時,單純的變數就不夠用了。JavaScript 提供了陣列和物件兩種重要的資料結構,讓我們能夠有效地組織和管理資料。
陣列
陣列是用來儲存多個值的資料結構,就像一個有編號的清單。每個元素都有一個索引(從 0 開始),我們可以透過索引來存取特定的元素。
建立陣列
在 JavaScript 中,Array(陣列)是一種用來儲存有序資料集合的物件。陣列的每個元素都可以透過索引(從 0 開始)來存取。現代 JavaScript 提供了許多強大的陣列方法,讓資料處理更加方便與彈性。
[]
與 new Array()
差異說明
- 使用
[]
(陣列字面量)建立陣列是最常見且推薦的方式,例如:let arr = [1, 2, 3];
- 使用
new Array()
建立陣列時,如果只傳一個數字參數,會建立指定長度但內容為空的陣列,例如:let arr = new Array(3);
會產生長度為 3 的空陣列(內容皆為 empty slot)。 - 若傳入多個參數,
new Array()
會將這些參數作為陣列元素,例如:let arr = new Array(1, 2, 3);
結果與[1, 2, 3]
相同。
通常建議使用 []
來建立「陣列實例」(Array instance),這樣建立出來的物件會自動擁有所有陣列方法(如 push、pop、forEach 等),語法簡潔且不易混淆。只有建立出「陣列實例」時,才能使用這些陣列專屬的方法。
// 方法 1:使用方括號(推薦) |
兩種建立陣列方式的差異:
方括號方式(推薦):
- 語法簡潔:
let arr = [];
- 效能較好
- 更直觀易懂
- 現代 JavaScript 的標準做法
new Array() 方式:
- 語法較長:
let arr = new Array();
- 因為是從 Array 建構式(Constructor)繼承出來的物件,建立時會經過額外的建構流程效能稍差,通常不如直接使用方括號
[]
來得簡潔高效 - 需要注意特殊情況
特殊情況:
// 使用 new Array() 時要注意 |
建議:
- 一般情況使用方括號
[]
- 只有在需要動態建立指定長度陣列時才考慮
new Array()
存取陣列元素
let colors = ["紅", "綠", "藍"]; |
陣列與迴圈
let students = ["小明", "小華", "小李", "小王"]; |
陣列實例
Array(陣列)是 JavaScript 中用來儲存多個資料的資料結構。每個元素都有一個索引(從 0 開始),可以用來快速存取、修改、刪除資料。
在 JavaScript 中,陣列(Array)其實是一種特殊的物件(Object),它除了可以用數字索引存取元素外,還繼承了 Array.prototype 上的各種原生方法(function),例如
push()
、pop()
、map()
、filter()
等,讓我們能夠方便地進行資料處理與操作。這些方法大幅提升了陣列的實用性與靈活性,是現代 JavaScript 程式設計不可或缺的工具。
常用方法
在 JavaScript 中,陣列(Array)本身是一種線性資料結構,允許我們根據不同需求,利用內建方法實現「先進先出」(FIFO, First-In-First-Out)或「後進先出」(FILO, First-In-Last-Out,也稱 LIFO)等資料存取行為。例如,push()
和 pop()
方法可讓陣列像堆疊(stack)一樣運作(LIFO),而 push()
搭配 shift()
則可模擬佇列(queue)的行為(FIFO)。
let animals = ["狗", "貓"]; |
除了基本的新增與刪除操作外,陣列也提供了檢查元素是否存在、尋找特定資料的位置,以及將整個陣列快速轉換為字串等常用功能。
let numbers = [1, 2, 3, 4, 5]; |
陣列實例的常用方法速查表
方法 | 功能說明 | 使用語法 | 範例程式碼 |
---|---|---|---|
push() |
在陣列尾端新增元素 | array.push(item) |
fruits.push("橘子") |
pop() |
移除並回傳尾端元素 | array.pop() |
let last = fruits.pop() |
unshift() |
在陣列開頭新增元素 | array.unshift(item) |
fruits.unshift("蘋果") |
shift() |
移除並回傳開頭元素 | array.shift() |
let first = fruits.shift() |
splice() |
新增/移除指定位置元素 | array.splice(start, deleteCount, ...items) |
fruits.splice(1, 1, "梨子") |
slice() |
擷取陣列的一部分 | array.slice(start, end) |
let part = fruits.slice(1, 3) |
concat() |
合併兩個或多個陣列 | array1.concat(array2) |
let all = fruits.concat(vegetables) |
join() |
將陣列轉為字串 | array.join(separator) |
fruits.join(", ") |
sort() |
排序陣列元素 | array.sort([compareFn]) |
fruits.sort() |
includes() |
判斷陣列是否包含某個值 | array.includes(value) |
fruits.includes("蘋果") |
indexOf() |
尋找元素的索引(找不到回傳 -1) | array.indexOf(value) |
fruits.indexOf("橘子") |
資料處理方法
這些方法都需要傳入一個「函式」作為參數,等學到 function 章節會詳細介紹。
- forEach(fn):對每個元素執行操作,不會回傳新陣列,常用於遍歷或執行副作用。
- map(fn):將每個元素轉換後產生新陣列,原陣列不變,常用於資料格式轉換。
- filter(fn):篩選出符合條件的元素,回傳新陣列。
- find(fn):尋找第一個符合條件的元素,回傳該元素(找不到則回傳
undefined
)。 - some(fn):檢查是否有任一元素符合條件,回傳布林值(true/false)。
- every(fn):檢查是否所有元素都符合條件,回傳布林值(true/false)。
- reduce(fn, init):將陣列元素依序累加、合併或歸納為單一值,常用於總和、計數、物件合併等情境。
Array 建構函式
在 JavaScript 中,Array
是一個「建構函式(Constructor)」也是一個「全域物件(Global Object)」。你可以用 new Array()
建立新的陣列實體,也可以直接呼叫 Array
上的靜態方法(如 Array.from()
、Array.isArray()
、Array.of()
)來進行各種陣列相關操作。
new Array()
:使用建構函式語法建立新的陣列實體。Array.from()
、Array.isArray()
、Array.of()
:這些是 Array 物件本身的「靜態方法」,不是陣列實例的方法。
兩者的「Array」都是指內建的 Array
物件(全域的 Array function),只是用法和用途不同。
Array.from
Array.from()
可以將「類陣列物件」或「可迭代物件」轉換成真正的陣列。
- 語法:
Array.from(arrayLike[, mapFn[, thisArg]])
- 常見應用:
- 將 Set、Map 轉成陣列
- 將字串轉成字元陣列
- 產生指定長度的陣列
// 將 Set 轉成陣列 |
Array.isArray
Array.isArray()
用來判斷某個值是否為陣列,回傳布林值。
Array.isArray([1,2,3]); // true |
Array.of
Array.of()
根據傳入的參數建立一個新陣列(和 new Array()
不同,單一數字也會變成元素)。
Array.of(3); // [3] |
小技巧:Array 的靜態方法與陣列實例方法
Array.from()
、Array.isArray()
、Array.of()
必須用Array.
呼叫,不能用在陣列實例上。push()
、sort()
、map()
等是「陣列實例」的方法,必須用在陣列變數上。Array.from(obj)
與[...obj]
都能將可迭代物件轉成陣列,Array.from
可搭配 map 函式直接轉換內容。
物件
物件(Object)是 JavaScript 中用來儲存多個相關資料的資料結構。與陣列(Array)只能用索引(數字)存取元素不同,物件是以「鍵值對」(key-value pair)的形式儲存資料,每個值(value)都對應一個具意義的名稱(鍵,key),讓資料更有結構且易於理解與存取。
建立物件
// 建立空物件 |
存取物件屬性
let car = { |
處理不存在的屬性
let user = { |
關於 undefined 和可選鏈運算子:
- undefined:當存取不存在的物件屬性時,JavaScript 會返回
undefined
- 可選鏈運算子(?.):ES6 的新語法,當屬性不存在時會返回
undefined
而不是報錯 - 使用時機:當你不確定物件是否有某個屬性時,使用
?.
可以避免程式崩潰 - 相容性:可選鏈運算子是較新的語法,在舊版瀏覽器中可能不支援
新增和刪除屬性
let book = { |
物件與迴圈
let student = { |
Object 建構函式
在 JavaScript 中,Object
是一個內建的建構函式,它提供了許多有用的靜態方法來處理物件。我們已經知道可以用 {}
來建立物件,但也可以用 new Object()
來建立:
// 兩種建立物件的方式 |
補充說明:constructor 屬性是什麼?
- 每個 JavaScript 物件都有一個
constructor
屬性,指向建立該物件的建構函式(Constructor Function)。 - 例如:用物件字面量
{}
或new Object()
建立的物件,其constructor
都指向內建的Object
函式。 - 這個屬性可以用來判斷物件是由哪個建構函式產生的,也常用於物件型別的檢查。
let arr = []; |
注意事項:
constructor
屬性可被覆蓋或修改,並非絕對安全的型別判斷方式。- 更嚴謹的型別判斷可用
instanceof
或Object.prototype.toString.call(obj)
。
let arr = []; |
應用場景:
- 檢查物件來源、建立自訂類別時理解原型鏈、進行物件型別判斷等。
Object 建構函式:
Object
是 JavaScript 的內建建構函式- 所有物件都繼承自
Object.prototype
- 提供許多靜態方法來操作物件
- 通常使用物件字面量語法
{}
更簡潔
Object 的靜態方法
Object
建構函式提供了許多有用的靜態方法,讓我們能夠更方便地處理物件:
let car = { |
Object 靜態方法說明:
- Object.keys():返回物件所有可列舉屬性名稱的陣列
- Object.values():返回物件所有可列舉屬性值的陣列
- Object.entries():返回物件所有可列舉鍵值對的陣列
這些方法讓我們能夠將物件轉換為陣列,方便使用陣列的方法來處理物件資料。在後續學習陣列的函式式方法時,這些轉換技巧會非常有用。
注意事項:
Object.keys()
、Object.values()
、Object.entries()
只會返回可列舉的屬性- 繼承的屬性不會被包含在結果中
- 這些方法返回的是陣列,可以使用所有陣列方法
函式
當我們需要處理複雜的資料操作時,函式是必不可少的工具。函式讓我們能夠將程式碼組織成可重複使用的區塊,同時 JavaScript 也提供了許多內建函式來處理常見的任務。函式是一段可以重複使用的程式碼,用來完成特定的任務。學會使用函式是程式設計的重要里程碑。
當我們需要重複執行相同的程式碼時,函式可以讓程式更簡潔、更容易維護:
// 沒有使用函式的重複程式碼 |
函式的基本語法
函式是 JavaScript 中最重要的概念之一,讓我們來學習函式的基本語法和各種用法:
// 函式宣告 |
函式參數的使用
函式可以接收參數來處理不同的資料:
// 單一參數 |
函式的回傳值
函式可以使用 return
語句來回傳結果:
// 有回傳值的函式 |
函式的預設參數
ES6 引入了預設參數,讓我們可以為參數設定預設值:
// 預設參數 |
函式語法重點:
- 函式宣告:使用
function
關鍵字定義函式 - 參數:函式可以接收一個或多個參數
- 回傳值:使用
return
語句回傳結果 - 預設參數:ES6 語法,為參數設定預設值
- 呼叫函式:使用函式名稱加括號來呼叫函式
注意事項:
- 函式宣告(Function Declaration)在 JavaScript 中會被「提升」(Hoisting),也就是說你可以在函式定義之前就呼叫該函式,這是因為 JavaScript 會在執行前先將所有函式宣告提升到該作用域頂端。
- 函式的參數名稱應該具備語意,能夠清楚表達該參數的用途,這樣不僅方便自己閱讀,也方便他人維護程式碼。
- 當函式執行到
return
語句時,會立即結束該函式的執行,並將return
後的值回傳給呼叫者。return
之後的程式碼將不會被執行。 - 在設定函式的預設參數時,必須將預設值的參數放在參數列表的最後面,否則可能會導致預期外的行為或錯誤,這是因為 JavaScript 會依照參數順序對應傳入的值。
JSDoc 樣板註解
JSDoc 是 JavaScript 常用的標準化註解格式,主要用於說明「函式、參數、回傳值」等資訊。這種註解不僅讓程式碼更容易閱讀,也能被工具自動產生 API 文件,提升團隊協作效率。
基本語法:
/** |
常用標籤:
@param {型別} 參數名稱
:說明每個參數的型別與用途@returns {型別}
:說明回傳值的型別與內容@example
:提供實際使用範例,讓團隊成員一看就懂@author
、@version
、@date
:作者、版本、日期
VSCode 快速鍵小技巧:
- 輸入
/**
然後按 Enter,可自動產生 JSDoc 樣板
範例:
/** |
箭頭函式
箭頭函式是 ES6 引入的新語法,提供更簡潔的函式撰寫方式:
// 傳統函式 |
匿名函式與函式表達式
除了函式宣告外,JavaScript 還支援匿名函式(函式表達式)的寫法。這種方式讓我們能夠將函式賦值給變數:
// 函式宣告(提升) |
函式宣告 vs 函式表達式
// 函式宣告 - 會被提升 (hoisting) |
匿名函式的實際應用
// 1. 作為回調函式 |
匿名函式的特點:
- 沒有名稱:函式本身沒有名稱,但可以賦值給變數
- 不會提升:必須先定義才能使用
- 靈活性高:可以動態建立和傳遞
- 立即執行:可以定義後立即執行 (IIFE)
- 作為值:函式可以像其他值一樣傳遞和使用
注意事項:
- 函式宣告(Function Declaration)會被提升(Hoisting):在 JavaScript 中,使用
function
關鍵字宣告的函式,會在程式執行前自動被提升到目前作用域的最上方,因此你可以在函式定義之前就呼叫該函式。例如:greet(); // 可以正常執行
function greet() {
console.log("Hello!");
} - 函式表達式(Function Expression)不會被提升:將函式賦值給變數的寫法(如
let fn = function() {}
),必須先定義後才能使用,否則會出現錯誤。這是因為只有變數名稱會被提升,函式內容不會被提升。例如:sayHi(); // 這裡會出錯
let sayHi = function() {
console.log("Hi!");
}; - 匿名函式(Anonymous Function)常用於回呼函式(Callback)與事件處理:匿名函式沒有名稱,通常直接作為參數傳遞給其他函式,例如陣列的
forEach
、事件監聽(addEventListener
)等,讓程式更靈活。 - IIFE(Immediately Invoked Function Expression,立即執行函式表達式)常用於建立私有作用域:IIFE 是一種定義後立即執行的匿名函式,常用來避免變數污染全域命名空間,或建立私有變數。例如:
(function() {
// 這裡的變數只在這個函式內有效
let secret = "隱藏資訊";
console.log(secret);
})();
console.log(secret); // 這裡會出錯,因為 secret 只存在於 IIFE 內
變數作用域
在一開始的變數章節中,我們已經學習了變數的基本概念。現在讓我們深入探討函式中的變數作用域,了解全域變數和區域變數在函式中的行為:
// 全域變數 |
函式中的變數作用域規則:
- 全域變數:在函式外宣告的變數,可以在函式內外存取
- 區域變數:在函式內宣告的變數,只能在函式內存取
- 參數變數:函式的參數也是區域變數,只在函式內有效
- 作用域鏈:函式可以存取外層作用域的變數,但外層無法存取內層變數
閉包
閉包(Closure)是 JavaScript 中一個重要的概念,它允許函式「記住」並存取其詞法作用域(lexical scope)中的變數,即使該函式在其原始作用域之外執行。
閉包的核心概念:
- 函式可以存取其被定義時的作用域中的變數
- 即使外部函式已經執行完畢,內部函式仍然可以存取這些變數
- 閉包常用於資料隱藏、模組化程式設計
閉包的三個關鍵要素:
- 外部函式:
outerFunction
建立作用域 - 內部函式:
innerFunction
存取外部變數 - 變數保持:即使外部函式結束,內部函式仍能存取變數
基本閉包範例
function outerFunction() { |
重要理解:
message
變數在outerFunction
執行完畢後仍然存在innerFunction
透過閉包機制「記住」了message
的值- 這就是閉包的核心:函式可以存取其被定義時的作用域
作用域層級結構
graph TD
subgraph "全域作用域 Global Scope"
A["let myFunction = outerFunction()<br/>全域變數,接收返回的函式"]
end
subgraph "outerFunction 作用域 Function Scope"
B["let message = 'Hello from outer function'<br/>外部函式的私有變數"]
C["function innerFunction() {<br/>console.log(message);<br/>}<br/>內部函式定義"]
D["return innerFunction<br/>返回內部函式"]
end
subgraph "innerFunction 作用域 Closure Scope"
E["console.log(message)<br/>存取外部變數<br/>形成閉包引用"]
end
A --> B
B --> C
C --> D
D --> E
B -.->|"閉包引用"| E
C -.->|"形成閉包"| E
style A fill:#e3f2fd,stroke:#1565c0,stroke-width:4px
style B fill:#f3e5f5,stroke:#7b1fa2,stroke-width:4px
style C fill:#e8f5e8,stroke:#2e7d32,stroke-width:4px
style D fill:#fff3e0,stroke:#ef6c00,stroke-width:4px
style E fill:#fce4ec,stroke:#c2185b,stroke-width:4px
逐行分析
讓我們逐行分析這個基本閉包範例的執行過程:
第 1 行:function outerFunction() {
- 動作:宣告一個名為
outerFunction
的函式 - 思考:這是外部函式,建立一個新的作用域
- 作用域:創建一個函式作用域,內部變數對外部不可見
第 2 行:let message = "Hello from outer function";
- 動作:在
outerFunction
作用域內宣告變數message
- 思考:這是外部函式的區域變數
- 作用域:
message
只在outerFunction
內部可見
第 4-6 行:function innerFunction() { console.log(message); }
- 動作:在
outerFunction
內部定義innerFunction
- 思考:這是內部函式,形成閉包
- 閉包特性:
innerFunction
可以存取outerFunction
的變數message
- 詞法作用域:JavaScript 根據函式定義的位置決定作用域
第 8 行:return innerFunction;
- 動作:返回
innerFunction
函式本身(不是執行結果) - 思考:將內部函式暴露給外部
- 閉包形成:
innerFunction
會「記住」其被定義時的作用域
第 11 行:let myFunction = outerFunction();
- 動作:呼叫
outerFunction()
並將返回的函式賦值給myFunction
- 思考:此時
outerFunction
執行完畢,但innerFunction
被保存 - 閉包保持:
innerFunction
仍然保持對message
變數的引用
第 12 行:myFunction();
- 動作:執行
myFunction
(實際上是innerFunction
) - 思考:即使
outerFunction
已經結束,innerFunction
仍能存取message
- 閉包作用:
console.log(message)
成功輸出"Hello from outer function"
閉包形成過程
graph TD
A["1. outerFunction 執行"] --> B["2. message 變數被宣告"]
B --> C["3. innerFunction 被定義,形成閉包"]
C --> D["4. innerFunction 記住 message 變數"]
D --> E["5. outerFunction 返回 innerFunction"]
E --> F["6. outerFunction 執行完畢,但 message 仍被 innerFunction 引用"]
style A fill:#f1f8e9,stroke:#388e3c,stroke-width:2px
style B fill:#e0f2f1,stroke:#00695c,stroke-width:2px
style C fill:#fff8e1,stroke:#f57f17,stroke-width:2px
style D fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px
style E fill:#e8f5e8,stroke:#2e7d32,stroke-width:2px
style F fill:#fff3e0,stroke:#ef6c00,stroke-width:2px
執行階段流程
graph TD
A["7. myFunction() 被呼叫"] --> B["8. innerFunction 執行"]
B --> C["9. 透過閉包存取 message 變數"]
C --> D["10. 輸出:Hello from outer function"]
style A fill:#fce4ec,stroke:#c2185b,stroke-width:2px
style B fill:#f1f8e9,stroke:#388e3c,stroke-width:2px
style C fill:#e0f2f1,stroke:#00695c,stroke-width:2px
style D fill:#fff8e1,stroke:#f57f17,stroke-width:2px
作用域與閉包關係圖
graph TD
subgraph "作用域層級結構"
subgraph "全域作用域 Global Scope"
A["let myFunction = outerFunction()"]
end
subgraph "outerFunction 作用域 Function Scope"
B["let message = 'Hello from outer function'"]
C["function innerFunction() { console.log(message); }"]
D["return innerFunction"]
end
subgraph "innerFunction 作用域 Closure Scope"
E["console.log(message) - 存取外部變數"]
end
end
subgraph "閉包引用關係"
F["innerFunction 的閉包環境"]
G["包含:message 變數的引用"]
H["即使 outerFunction 結束,引用仍存在"]
end
subgraph "執行時的作用域鏈"
I["innerFunction 執行時"]
J["1. 檢查自己的作用域"]
K["2. 找不到 message,向上查找"]
L["3. 在閉包環境中找到 message"]
M["4. 成功存取並輸出"]
end
A --> B
B --> C
C --> D
D --> E
E -.->|"閉包引用"| F
F --> G
G --> H
I --> J
J --> K
K --> L
L --> M
style A fill:#e3f2fd,stroke:#1565c0,stroke-width:3px
style B fill:#f3e5f5,stroke:#7b1fa2,stroke-width:3px
style C fill:#e8f5e8,stroke:#2e7d32,stroke-width:3px
style D fill:#fff3e0,stroke:#ef6c00,stroke-width:3px
style E fill:#fce4ec,stroke:#c2185b,stroke-width:3px
style F fill:#fff8e1,stroke:#f57f17,stroke-width:3px
style G fill:#f1f8e9,stroke:#388e3c,stroke-width:3px
style H fill:#e0f2f1,stroke:#00695c,stroke-width:3px
style I fill:#f3e5f5,stroke:#7b1fa2,stroke-width:3px
style J fill:#e8f5e8,stroke:#2e7d32,stroke-width:3px
style K fill:#fff3e0,stroke:#ef6c00,stroke-width:3px
style L fill:#fce4ec,stroke:#c2185b,stroke-width:3px
style M fill:#fff8e1,stroke:#f57f17,stroke-width:3px
計數器閉包
function createCounter() { |
計數器閉包作用域結構
graph TD
subgraph "createCounter 函式作用域"
A["let count = 0<br/>私有計數變數"]
B["function increment() {<br/>count++;<br/>console.log(`計數:${count}`);<br/>}"]
C["function getCount() {<br/>return count;<br/>}"]
D["return {<br/>increment,<br/>getCount<br/>}"]
end
subgraph "閉包引用關係"
E["increment 函式<br/>引用 count 變數<br/>形成閉包"]
F["getCount 函式<br/>引用 count 變數<br/>形成閉包"]
G["count 變數被兩個函式共享<br/>狀態在函式間保持"]
end
A --> B
A --> C
B --> D
C --> D
B -.->|"閉包引用"| E
C -.->|"閉包引用"| F
E --> G
F --> G
style A fill:#e3f2fd,stroke:#1565c0,stroke-width:4px
style B fill:#f3e5f5,stroke:#7b1fa2,stroke-width:4px
style C fill:#e8f5e8,stroke:#2e7d32,stroke-width:4px
style D fill:#fff3e0,stroke:#ef6c00,stroke-width:4px
style E fill:#fce4ec,stroke:#c2185b,stroke-width:3px
style F fill:#f1f8e9,stroke:#388e3c,stroke-width:3px
style G fill:#e0f2f1,stroke:#00695c,stroke-width:3px
計數器狀態變化
graph TD
A["初始狀態:count = 0"] --> B["counter.increment()"]
B --> C["狀態:count = 1"]
C --> D["counter.increment()"]
D --> E["狀態:count = 2"]
E --> F["counter.getCount()"]
F --> G["返回:2"]
style A fill:#e8f5e8,stroke:#2e7d32,stroke-width:2px
style B fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px
style C fill:#fff3e0,stroke:#ef6c00,stroke-width:2px
style D fill:#fce4ec,stroke:#c2185b,stroke-width:2px
style E fill:#f1f8e9,stroke:#388e3c,stroke-width:2px
style F fill:#e0f2f1,stroke:#00695c,stroke-width:2px
style G fill:#fff8e1,stroke:#f57f17,stroke-width:2px
模組化閉包
函式裡面宣告兩個小函式,透過物件的寫法回傳給外部。
function Module() { |
模組化閉包作用域結構
graph TD
subgraph "Module 函式作用域"
A["let animalCount = 0<br/>共享計數變數"]
B["let lastAnimal = ''<br/>共享狀態變數"]
C["function dogsay() {<br/>animalCount++;<br/>lastAnimal = '狗';<br/>console.log(...);<br/>}"]
D["function catsay() {<br/>animalCount++;<br/>lastAnimal = '貓';<br/>console.log(...);<br/>}"]
E["function getStats() {<br/>return {<br/>totalCalls: animalCount,<br/>lastAnimal: lastAnimal<br/>};<br/>}"]
F["return {<br/>dog: dogsay,<br/>cat: catsay,<br/>stats: getStats<br/>}"]
end
subgraph "閉包引用關係"
G["dogsay 函式<br/>引用 animalCount 和 lastAnimal<br/>形成閉包"]
H["catsay 函式<br/>引用 animalCount 和 lastAnimal<br/>形成閉包"]
I["getStats 函式<br/>引用 animalCount 和 lastAnimal<br/>形成閉包"]
J["三個函式共享同一個作用域<br/>狀態在函式間保持"]
end
A --> C
A --> D
A --> E
B --> C
B --> D
B --> E
C --> F
D --> F
E --> F
C -.->|"閉包引用"| G
D -.->|"閉包引用"| H
E -.->|"閉包引用"| I
G --> J
H --> J
I --> J
style A fill:#e3f2fd,stroke:#1565c0,stroke-width:4px
style B fill:#f3e5f5,stroke:#7b1fa2,stroke-width:4px
style C fill:#e8f5e8,stroke:#2e7d32,stroke-width:4px
style D fill:#fff3e0,stroke:#ef6c00,stroke-width:4px
style E fill:#fce4ec,stroke:#c2185b,stroke-width:4px
style F fill:#f1f8e9,stroke:#388e3c,stroke-width:4px
style G fill:#e0f2f1,stroke:#00695c,stroke-width:3px
style H fill:#fff8e1,stroke:#f57f17,stroke-width:3px
style I fill:#f3e5f5,stroke:#7b1fa2,stroke-width:3px
style J fill:#e8f5e8,stroke:#2e7d32,stroke-width:3px
函式內的 this
this
是 JavaScript 中一個重要的關鍵字,它代表當前執行環境的上下文物件。this
的值取決於函式如何被呼叫,而不是函式如何被定義。
this 的核心概念:
this
的值在函式執行時才決定this
指向呼叫函式的物件- 不同的呼叫方式會產生不同的
this
值
基本 this 範例
function room() { |
this 的呼叫方式
graph TD
subgraph "this 的決定方式"
A["函式呼叫方式"] --> B["this 指向的物件"]
B --> C["函式執行時的上下文"]
end
subgraph "呼叫方式分類"
D["1. 直接呼叫<br/>room()"]
E["2. 物件方法呼叫<br/>obj.method()"]
F["3. 建構函式呼叫<br/>new Constructor()"]
G["4. 箭頭函式<br/>() => {}"]
end
subgraph "this 指向"
H["全域物件<br/>(window/global)"]
I["呼叫的物件<br/>(obj)"]
J["新建立的實例<br/>(instance)"]
K["外層的 this<br/>(lexical)"]
end
A --> D
A --> E
A --> F
A --> G
D --> H
E --> I
F --> J
G --> K
style A fill:#e3f2fd,stroke:#1565c0,stroke-width:4px
style B fill:#f3e5f5,stroke:#7b1fa2,stroke-width:4px
style C fill:#e8f5e8,stroke:#2e7d32,stroke-width:4px
style D fill:#fff3e0,stroke:#ef6c00,stroke-width:3px
style E fill:#fce4ec,stroke:#c2185b,stroke-width:3px
style F fill:#f1f8e9,stroke:#388e3c,stroke-width:3px
style G fill:#e0f2f1,stroke:#00695c,stroke-width:3px
style H fill:#fff8e1,stroke:#f57f17,stroke-width:3px
style I fill:#f3e5f5,stroke:#7b1fa2,stroke-width:3px
style J fill:#e8f5e8,stroke:#2e7d32,stroke-width:3px
style K fill:#fff3e0,stroke:#ef6c00,stroke-width:3px
現代 JavaScript 中的 this
// 1. 全域環境中的 this |
this 的執行環境圖解
graph TD
subgraph "this 的執行環境"
A["全域環境<br/>this = window/global"]
B["物件方法<br/>this = 呼叫的物件"]
C["建構函式<br/>this = 新建立的實例"]
D["箭頭函式<br/>this = 外層的 this"]
end
subgraph "呼叫方式"
E["直接呼叫<br/>func()"]
F["物件方法<br/>obj.method()"]
G["建構函式<br/>new Constructor()"]
H["箭頭函式<br/>() => {}"]
end
subgraph "this 指向"
I["全域物件<br/>window/global"]
J["呼叫物件<br/>obj"]
K["新實例<br/>instance"]
L["詞法 this<br/>outer this"]
end
E --> I
F --> J
G --> K
H --> L
A --> E
B --> F
C --> G
D --> H
style A fill:#e3f2fd,stroke:#1565c0,stroke-width:4px
style B fill:#f3e5f5,stroke:#7b1fa2,stroke-width:4px
style C fill:#e8f5e8,stroke:#2e7d32,stroke-width:4px
style D fill:#fff3e0,stroke:#ef6c00,stroke-width:4px
style E fill:#fce4ec,stroke:#c2185b,stroke-width:3px
style F fill:#f1f8e9,stroke:#388e3c,stroke-width:3px
style G fill:#e0f2f1,stroke:#00695c,stroke-width:3px
style H fill:#fff8e1,stroke:#f57f17,stroke-width:3px
style I fill:#f3e5f5,stroke:#7b1fa2,stroke-width:3px
style J fill:#e8f5e8,stroke:#2e7d32,stroke-width:3px
style K fill:#fff3e0,stroke:#ef6c00,stroke-width:3px
style L fill:#fce4ec,stroke:#c2185b,stroke-width:3px
注意事項:
this
的值在函式執行時才決定,不是定義時- 箭頭函式沒有自己的
this
,會繼承外層的this
- 在嚴格模式下,全域函式的
this
是undefined
- 使用
call
、apply
、bind
可以明確控制this
的值
陣列資料處理
現在你知道匿名函式與箭頭函式,你可以在陣列處理時,透過函式返回一個結果。
- 陣列資料處理方法都需要傳入一個「函式」作為參數,通常用匿名函式或箭頭函式
- forEach 只做動作不回傳新陣列,map/filter/find/some/every/reduce 會根據邏輯回傳新資料或結果
- 常用於資料轉換、篩選、搜尋、統計等情境,是現代 JavaScript 處理資料的核心工具
forEach
對陣列中的每個元素執行指定的動作,常用於遍歷或輸出資料。
let fruits = ["蘋果", "香蕉", "橘子"]; |
map
將每個元素轉換為新值,並回傳一個新陣列,常用於資料格式轉換。
let numbers = [1, 2, 3]; |
filter
篩選出符合條件的元素,回傳新陣列,常用於資料過濾。
let scores = [80, 55, 90, 70]; |
find
尋找第一個符合條件的元素,回傳該元素本身,常用於搜尋特定資料。
let users = [ |
some
檢查陣列中是否有任何一個元素符合條件,回傳布林值,常用於條件判斷。
let arr = [1, 3, 5, 8]; |
every
檢查陣列中是否所有元素都符合條件,回傳布林值,常用於整體驗證。
let arr = [2, 4, 6]; |
reduce
將陣列歸納為單一值,常用於加總、統計等彙總運算。
let nums = [1, 2, 3, 4]; |
展開運算子
展開運算子(Spread Operator) ...
可以將陣列或物件的元素展開,方便用來合併、擴充或重組資料。
陣列的重組
let ary = [1, 2, 3]; |
物件的重組
let obj = { name: "小明", age: 18 }; |
應用場景
- 陣列或物件的「淺拷貝」
- 新增或覆蓋資料時不改變原本的資料(不可變資料設計)
- React/Vue 等框架中常用於狀態更新
展開運算子小技巧
- 陣列展開時順序很重要,
[...ary, x]
會把 x 加在最後,[x, ...ary]
則加在最前面 - 物件展開時,後面的屬性會覆蓋前面的同名屬性
- 展開運算子只能做「淺拷貝」,巢狀物件或陣列仍是參考同一份資料
解構賦值
在現代 JavaScript 中,「解構賦值」(Destructuring Assignment)是一種方便的語法,能夠快速從陣列或物件中取出資料並賦值給變數,讓程式碼更簡潔易讀。
陣列的解構賦值
let arr = [10, 20, 30]; |
…rest 的應用(陣列)
- 可以快速取得陣列中「剩下」的元素,常用於分割資料、批次處理等情境。
- 注意:…rest 只能放在最後一個解構變數。
小技巧:陣列解構與 …rest
[a, b, ...rest] = ary
會將 ary 的前兩個元素分別賦值給 a、b,其餘元素組成新陣列 rest- 常用於只關心前幾個元素,或需要將剩下的資料批次處理時
物件的解構賦值
let person = { name: "小明", age: 18, city: "台北", job: "學生" }; |
…rest 的應用
- 可以快速取得物件中「剩下」的屬性,常用於過濾、拆分資料、React/Vue props 處理等情境。
- 注意:…rest 只能放在最後一個解構變數。
小技巧:物件解構與 …rest
- 物件解構時,
{ a, b, ...rest } = obj
會將 obj 內除了 a、b 以外的所有屬性收集到 rest 物件中。 - 常用於需要排除部分屬性、或將剩餘屬性傳遞給其他元件/函式時。
巢狀解構
let user = { |
參數解構
解構也常用於函式參數,讓函式更易讀且靈活:
function printUser({ name, age }) { |
交換變數
let a = 1, b = 2; |
解構賦值小技巧與注意事項
- 陣列解構依照「順序」對應,物件解構依照「屬性名稱」對應
- 可搭配預設值、重新命名、巢狀結構
- 物件解構時,變數名稱必須與屬性名稱一致,除非使用冒號重新命名
- 解構常用於函式參數、React/Vue 等框架的 props 處理、API 回傳資料解析等
- 若來源為 undefined/null,解構會報錯,建議搭配預設值或安全判斷
物件下的其他屬性值
物件的屬性值不僅可以是基本資料類型(字串、數字、布林值),還可以是更複雜的資料結構,包括函式、陣列、巢狀物件等。這讓物件成為 JavaScript 中非常強大的資料結構。
物件屬性值的類型:
- 基本資料類型:字串、數字、布林值、null、undefined
- 複雜資料類型:函式、陣列、物件
- 特殊值:Symbol、BigInt
物件屬性值的各種類型
const person = { |
物件方法定義的兩種語法
const calculator = { |
物件屬性值的動態操作
const user = { |
物件屬性值的進階應用
// 1. 計算屬性名稱(ES6+) |
物件屬性值的類型檢查
const mixedObject = { |
注意事項:
- 箭頭函式作為物件方法時,
this
不會指向物件本身 - 使用
Object.freeze()
可以防止物件被修改 - 深層巢狀物件的修改需要特別注意
- 動態屬性名稱需要使用方括號語法
其他的建構函式與物件
JavaScript 提供了許多功能強大的「內建物件」與「建構函式」,讓我們可以方便地處理數學運算、字串操作、日期時間、陣列資料等各種常見需求。本章將介紹幾個最常用的內建物件(如 Date、Math、String、Array 等),並說明它們的基本用法與常見應用情境,幫助你在開發過程中靈活運用這些工具。
Set
Set
是 ES6 新增的資料結構,用來儲存「不重複」的值集合。每個值在 Set 中只會出現一次,且元素的順序依插入順序排列。常見應用場景包括:
- 陣列去重(移除重複值)
- 資料集合判斷(如是否包含某元素)
- 集合運算(如交集、聯集、差集)
建立 Set
new Set()
:建立空集合new Set(iterable)
:由可迭代物件(如陣列)建立集合
常用方法
add(value)
:新增元素delete(value)
:刪除元素has(value)
:判斷是否包含某值clear()
:清空所有元素size
:元素個數forEach(fn)
:遍歷所有元素
範例
let s = new Set(); |
小技巧:用 Set 快速陣列去重複
let unique = Array.from(new Set(arr));
- 這種寫法可快速將陣列重複值去除,常用於資料清理
Map
Map
是 ES6 新增的建構函式,用來建立「鍵值對」資料結構。它和傳統的物件(Object)類似,但有更多彈性與現代化的特性。
Map 的特性
- 任何型別都能當 key:不只字串,物件、數字、函式等都可以當 key。
- 保留插入順序:遍歷時會依照 key 加入的順序。
- 可直接迭代:支援
for...of
、forEach
等迭代方式。 - 高效查找與刪除:適合大量動態增刪查的場景。
建立 Map
- 空 Map:
let map = new Map();
- 由陣列初始化:
let map = new Map([[key1, value1], [key2, value2]]);
常用方法
set(key, value)
:新增或更新鍵值對get(key)
:取得指定 key 的值has(key)
:判斷是否有指定 keydelete(key)
:刪除指定 keyclear()
:清空所有鍵值對size
:Map 內元素個數forEach(fn)
:遍歷所有鍵值對
範例
let map = new Map(); |
Map vs Object 差異
特性 | Object | Map |
---|---|---|
key 型別 | 字串、Symbol | 任何型別 |
順序 | 無明確順序 | 保留插入順序 |
可迭代性 | 需用 Object.keys() |
直接可用 for...of 、forEach |
長度 | 需手動計算 | map.size |
性能 | 大量增刪查較慢 | 更適合頻繁操作 |
小技巧:Map 的應用場景
- 適合需要「物件當 key」、保留插入順序、或大量動態增刪查的情境。
- 若 key 都是字串且結構簡單,Object 也能勝任。
Math
Math
物件是 JavaScript 提供的數學工具箱,包含各種常用的數學常數與運算方法。它不是建構函式,無法用 new Math()
建立,只能直接使用其屬性與方法。
常見屬性
Math.PI
:圓周率 πMath.E
:自然常數 e
常用方法
Math.abs(x)
:取絕對值Math.round(x)
:四捨五入Math.floor(x)
:無條件捨去Math.ceil(x)
:無條件進位Math.max(a, b, ...)
:取最大值Math.min(a, b, ...)
:取最小值Math.random()
:產生 0~1 之間的隨機小數Math.pow(x, y)
:x 的 y 次方Math.sqrt(x)
:開根號
範例
console.log(Math.PI); // 3.141592653589793 |
小技巧:Math.random() 產生隨機整數
- 產生 1~10 的隨機整數:
let n = Math.floor(Math.random() * 10) + 1;
- 產生指定範圍的隨機整數:
function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
練習
請寫一個 JavaScript 函式,產生 1~46 之間隨機且不重複的 6 個號碼,並將結果由小到大排序,回傳一個陣列。
- 輸出範例:
[3, 8, 15, 22, 34, 45]
- 提示:可用
Math.random()
、Set
、Array
、sort()
等方法
Set + while 寫法(有放回抽樣)
// 產生 1~46 的大樂透號碼,隨機取 6 個不重複並排序 (有放回抽樣) |
抽球寫法(無放回抽樣)
function getLottoNumbers() { |
Date
Date
物件用來處理日期與時間。可以取得現在時間、格式化日期、計算時間差等。
建立日期物件
new Date()
:取得現在時間,並回傳一個Date
物件。這個物件內建許多原生方法(如getFullYear()
、getMonth()
等),可用來取得或操作日期與時間的各種資訊。new Date(年,月,日,時,分,秒)
:指定年月日時分秒(注意:月從 0 開始,0 代表 1 月)new Date(字串)
:由字串(建議使用 ISO 格式,如2024-08-01T12:00:00
)建立日期。ISO 格式(YYYY-MM-DD 或 YYYY-MM-DDTHH:mm:ss)最能確保跨瀏覽器解析正確。
// 建立 Date 物件的三種常見寫法範例 |
get 取得方法系列
getFullYear()
:取得年份getMonth()
:取得月份(0~11)getDate()
:取得日期(1~31)getDay()
:取得星期(0~6,0 代表星期日)getHours()
:取得小時getMinutes()
:取得分鐘getSeconds()
:取得秒數getTime()
:取得自 1970/1/1 00:00:00 UTC 以來的「時間戳記」(Timestamp,單位為毫秒,millisecond)toLocaleString()
:格式化日期字串,會根據瀏覽器用戶的地區(Locale)自動顯示對應的日期與時間格式
let now = new Date(); |
小技巧:計算兩個日期的天數差
d1 = new Date('2024-08-01');
d2 = new Date('2024-08-10');
let diff = (d2 - d1) / (1000 * 60 * 60 * 24);
console.log(diff); // 9
- 日期運算時,直接相減會得到毫秒差,需轉換為天數
set 指定方法系列
setFullYear(year)
:設定年份setMonth(month)
:設定月份(0~11)setDate(date)
:設定日期(1~31)setHours(hours)
:設定小時(0~23)setMinutes(minutes)
:設定分鐘(0~59)setSeconds(seconds)
:設定秒數(0~59)setTime(timestamp)
:設定時間戳記setMilliseconds(ms)
:設定毫秒(0~999)
let date = new Date(); |
小技巧:使用 set 方法調整日期
// 將日期調整到下個月的同一天 |
- 利用 set 方法可以方便地調整日期和時間
練習
請寫一個 JavaScript 程式,計算「距離今年跨年(12/31 23:59:59)」還有幾天、幾小時、幾分鐘、幾秒,並將結果用 console.log
輸出。
- 提示:可用
Date
物件、時間相減、毫秒轉換等技巧
function getCountdownToNewYear() { |
錯誤處理與除錯
學習如何處理錯誤和除錯程式。
常見錯誤類型
// 語法錯誤(Syntax Error) |
使用 try-catch 處理錯誤
在程式執行過程中,難免會遇到各種錯誤(例如:資料格式錯誤、除以零、存取不存在的屬性等)。如果不加以處理,這些錯誤會導致程式中斷。JavaScript 提供了 try-catch
機制,讓我們可以「攔截」錯誤,並給予適當的處理方式,提升程式的穩定性與使用者體驗。
try
區塊:放入可能發生錯誤的程式碼catch
區塊:當 try 內發生錯誤時,會跳到這裡執行錯誤處理finally
區塊(可選):不論有無錯誤,最後都會執行這裡的程式碼(常用於資源釋放、收尾工作)
這種結構讓我們可以針對錯誤進行友善提示、記錄錯誤資訊,甚至避免整個程式崩潰。
function divide(a, b) { |
補充說明:throw new Error(message) 是什麼?
throw
是 JavaScript 用來「主動拋出」錯誤的關鍵字,會立即中斷目前的程式流程,並將錯誤物件傳遞到最近的 catch 區塊。new Error(message)
會建立一個錯誤物件,訊息內容可自訂。- 結合起來,
throw new Error('除數不能為零')
代表「主動產生一個錯誤,並帶有自訂訊息」,讓我們可以針對特定情境(如除以零)給出明確的錯誤提示。 - 這種寫法常用於資料驗證、API 回傳錯誤、流程控制等場合。
除錯技巧
// 1. 使用 console.log 追蹤程式執行 |
輸入驗證
function validateEmail(email) { |
學習總結
恭喜您完成了 JavaScript 基礎教學!讓我們回顧一下學習的內容:
已學習的概念
- 陣列:儲存和操作多個值的資料結構
- 物件:儲存相關資料的集合
- 函式:撰寫可重複使用的程式碼塊
- 內建函式:使用 JavaScript 提供的數學、字串、日期等功能
- 網頁整合:將 JavaScript 整合到 HTML 網頁中
- 錯誤處理:使用 try-catch 處理錯誤,學習除錯技巧
繼續學習的方向
進階主題建議:
- 非同步程式設計:Promise 和 async/await
- ES6+ 新功能:類別語法
- DOM 操作進階:事件處理、表單驗證
- 前端框架:React、Vue.js 或 Angular
- Node.js:使用 JavaScript 進行後端開發
- 開發工具:Webpack、Babel、ESLint
JavaScript 是一個不斷發展的語言,新的功能和最佳實踐會持續出現。保持學習的熱忱,並將所學應用到實際專案中,您很快就能成為一名優秀的 JavaScript 開發者!
記住:每個專業程式設計師都是從基礎開始的,重要的是持續練習和應用所學的知識。祝您在 JavaScript 的學習之路上順利!
學習資源
- MDN Web Docs - 最權威的 JavaScript 文件
- JavaScript.info - 深入淺出的 JavaScript 教學
- ES6 功能介紹 - 現代 JavaScript 新功能