昨天,我們成功地為程式注入了「判斷力」(if...else
) 與「耐力」(for...of
迴圈),讓它能夠根據不同情況執行任務,並不知疲倦地處理重複性工作。我們已經能寫出一連串指令來完成特定目標 。
然而,當專案變得複雜時,將所有指令都寫在同一個地方,就像把所有工具都扔在一個大麻布袋裡——混亂、難找,而且很容易不小心拿到錯的工具弄傷自己。
今天,我們要從「工匠」晉升為「建築師」。我們不再只是堆砌指令,而是要學習如何設計和建造可重複使用的「功能模組」。這就是今天要學習的核心:函式 (Functions)、作用域 (Scope) 與閉包 (Closures)。這將徹底改變你組織程式碼的方式。
核心概念:函式,就是一個帶有名字、為了完成特定任務而封裝起來的程式碼區塊。你可以隨時透過呼叫它的名字來執行它,還可以傳遞「原料」(稱為參數)給它,它處理完後,可以回傳一個「成品」(稱為回傳值)。
這遵循一個非常重要的軟體工程原則:DRY (Don't Repeat Yourself)。
語法結構:
function 函式名稱(參數1, 參數2) {
// 函式內部的程式碼邏輯
// ...
return 回傳值; // 使用 return 將結果送出
}
實戰演練:讓我們將昨天的 if...else
邏輯,改造成一個更專業、可重複使用的函式。這個函式將接收一個 characteristic
物件,並回傳一個包含它所有可用操作的陣列。
/**
* 模擬一個從藍牙裝置讀取到的特徵 (Characteristic) 物件
*/
const heartRateCharacteristic = {
uuid: '00002a37-0000-1000-8000-00805f9b34fb',
properties: {
read: false,
write: false,
notify: true
}
};
const batteryLevelCharacteristic = {
uuid: '00002a19-0000-1000-8000-00805f9b34fb',
properties: {
read: true,
write: false,
notify: true
}
};
/**
* 分析單一特徵物件,並回傳其所有可用的操作類型陣列
* @param {object} characteristic - 從藍牙裝置讀取到的特徵物件
* @returns {string[]} 一個包含可用操作字串的陣列 (e.g., ['read', 'notify'])
*/
function getAvailableActions(characteristic) {
const actions = []; // 先建立一個空陣列來存放結果
// 檢查 properties 物件中的每個屬性
if (characteristic.properties.read) {
actions.push('read'); // 如果可讀,將 'read' 加入陣列
}
if (characteristic.properties.write) {
actions.push('write'); // 如果可寫,將 'write' 加入陣列
}
if (characteristic.properties.notify) {
actions.push('notify'); // 如果可訂閱,將 'notify' 加入陣列
}
return actions; // 回傳最終的結果陣列
}
// ---- 使用我們建立的函式 ----
const heartRateActions = getAvailableActions(heartRateCharacteristic);
console.log(`心率特徵 (${heartRateCharacteristic.uuid}) 的可用操作:`, heartRateActions);
// 預期輸出: 心率特徵 (...) 的可用操作: [ 'notify' ]
const batteryActions = getAvailableActions(batteryLevelCharacteristic);
console.log(`電池電量特徵 (${batteryLevelCharacteristic.uuid}) 的可用操作:`, batteryActions);
// 預期輸出: 電池電量特徵 (...) 的可用操作: [ 'read', 'notify' ]
看到了嗎?我們現在有了一個乾淨、專注的getAvailableActions
函式。未來無論有多少個特徵,我們只需要呼叫這個函式,就能得到一致的結果,程式碼變得清晰且易於維護 。
/**
: 開頭是斜線加上兩個星號,這是一個特殊的標記,告訴程式碼編輯器(例如 VS Code)或其他工具:「注意!這不是普通註解,這是一份 JSDoc 文件!」
*/
: 結尾和普通的多行註解一樣
@param
: 這是一個「標籤 (Tag)」,專門用來說明函式需要接收的參數(原料)。如果函式有多個參數,就會有多個 @param
標籤。
{object}
: 這是用大括號包起來的資料型態。它告訴我們,傳入的 characteristic
參數應該是一個物件 (object
)。這對程式碼的穩定性至關重要,如果你不小心傳入一個數字或字串,有些編輯器會立刻給你提示,告訴你可能用錯了。
characteristic
: 這是參數的名稱,必須和函式定義中的參數名稱一致。
-
後面的文字: 這是對這個參數的詳細描述。它解釋了這個物件代表什麼——「從藍牙裝置讀取到的特徵物件」。
核心概念:作用域定義了變數的「可見範圍」或「有效範圍」。它決定了你在哪裡可以存取哪個變數。
全域作用域 (Global Scope):在所有函式之外宣告的變數,在哪裡都可以存取。
區域作用域 (Local Scope / Function Scope):在函式內部宣告的變數,只有在該函式內部才能存取。
這就像每個函式都有自己的「工具箱」,放在裡面的工具(區域變數)只有函式自己能用,不會跟別的函式或外面的工具(全域變數)搞混。
程式碼範例:
const tool = '板手'; // 這是一個全域變數
function fixCar() {
const tool = '螺絲起子'; // 這是一個區域變數,只存在於 fixCar 函式中
console.log(`在 fixCar 函式裡,我用的工具是: ${tool}`); // 會印出 '螺絲起子'
}
function fixBike() {
// 這裡沒有宣告自己的 tool 變數
console.log(`在 fixBike 函式裡,我用的工具是: ${tool}`); // 會往外找到全域的 '板手'
}
fixCar();
fixBike();
console.log(`在函式外面,我的工具是: ${tool}`); // 會印出 '板手'
// console.log(tool); // 若試圖在外面存取 fixCar 裡的 tool,會直接報錯!
這個機制非常重要,它能確保我們在不同功能模組中使用的變數不會互相「污染」,是大型專案維持穩定的基石。
核心概念:閉包,是整個 JavaScript 中最強大也最常被誤解的概念之一。簡單來說:一個函式,可以「記住」並存取它被建立時所在的作用域,即使那個作用域已經執行完畢。
這聽起來很抽象,但它解決了我們專案未來一個
最關鍵的問題:當我們用迴圈為數十個藍牙特徵動態生成按鈕時,如何讓每個按鈕「知道」自己對應的是哪一個特徵?
實戰演練:閉包的威力
讓我們直接模擬這個場景。假設我們有一個特徵列表,我們要為每個特徵都創建一個「顯示 UUID」的按鈕。
// 模擬的特徵列表
const characteristics = [
{ uuid: ' UUID-A ', instance: { /* ...藍牙特徵的原始物件A... */ } },
{ uuid: ' UUID-B ', instance: { /* ...藍牙特徵的原始物件B... */ } },
{ uuid: ' UUID-C ', instance: { /* ...藍牙特徵的原始物件C... */ } }
];
// 一個容器,用來放我們動態生成的按鈕 (在HTML中應有一個 <div id="controls"></div>)
const controlsContainer = document.getElementById('controls');
// 遍歷所有特徵
for (const char of characteristics) {
const showUuidButton = document.createElement('button');
showUuidButton.textContent = `顯示特徵 ${char.uuid} 的資訊`;
// !!! 閉包的魔法就在這裡 !!!
// 這個 onclick 箭頭函式,就是一個閉包。
// 它被建立時,外層的 for...of 迴圈正在處理當前的 `char` 物件。
// 它「捕獲」並「記住」了這個 `char` 變數。
showUuidButton.onclick = () => {
// 即使 for 迴圈早已執行完畢,
// 當我們點擊按鈕時,這個函式依然記得它被創建時對應的那個 `char`!
console.log(`按鈕被點擊!對應的特徵是: ${char.uuid}`);
};
controlsContainer.appendChild(showUuidButton);
}
當你點擊「顯示特徵 UUID-B 的資訊」按鈕時,控制台會精準地印出
按鈕被點擊!對應的特徵是: UUID-B
。這就是閉包的力量。它讓事件處理函式與其對應的資料牢牢綁定在一起 。
今天我們完成了從「寫指令」到「建構模組」的思維轉變,我們現在擁有的,不僅僅是零散的工具,而是一個有組織、有紀律的工具箱,並且知道如何製造出帶有「記憶」的智慧工具。
明天,我們將用今天學到的「物件」思維,來設計整個專案的「靈魂」——那個用來儲存所有藍牙裝置資訊的核心資料結構 GATT Profile
物件。這是專案的架構基石,
今天的內容就到這邊,感謝你能看到這裡,在這邊祝你早安、午安、晚安,我們明天見。