iT邦幫忙

2025 iThome 鐵人賽

DAY 6
0
Modern Web

Web Bluetooth API 實戰:30 天打造通用 BLE 偵錯工具系列 第 6

DAY6 :程式碼的模組化:函式、作用域與閉包

  • 分享至 

  • xImage
  •  

昨天,我們成功地為程式注入了「判斷力」(if...else) 與「耐力」(for...of 迴圈),讓它能夠根據不同情況執行任務,並不知疲倦地處理重複性工作。我們已經能寫出一連串指令來完成特定目標 。

然而,當專案變得複雜時,將所有指令都寫在同一個地方,就像把所有工具都扔在一個大麻布袋裡——混亂、難找,而且很容易不小心拿到錯的工具弄傷自己。

今天,我們要從「工匠」晉升為「建築師」。我們不再只是堆砌指令,而是要學習如何設計和建造可重複使用的「功能模組」。這就是今天要學習的核心:函式 (Functions)、作用域 (Scope) 與閉包 (Closures)。這將徹底改變你組織程式碼的方式。


1. 程式碼的積木:函式 (Functions)

核心概念:函式,就是一個帶有名字、為了完成特定任務而封裝起來的程式碼區塊。你可以隨時透過呼叫它的名字來執行它,還可以傳遞「原料」(稱為參數)給它,它處理完後,可以回傳一個「成品」(稱為回傳值)。

這遵循一個非常重要的軟體工程原則: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: 這是參數的名稱,必須和函式定義中的參數名稱一致。

  • - 後面的文字: 這是對這個參數的詳細描述。它解釋了這個物件代表什麼——「從藍牙裝置讀取到的特徵物件」。

2. 變數的家:作用域 (Scope)

核心概念:作用域定義了變數的「可見範圍」或「有效範圍」。它決定了你在哪裡可以存取哪個變數。

  • 全域作用域 (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,會直接報錯!

這個機制非常重要,它能確保我們在不同功能模組中使用的變數不會互相「污染」,是大型專案維持穩定的基石。

3. 擁有記憶的超能力:閉包 (Closures)

核心概念:閉包,是整個 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 物件。這是專案的架構基石,
今天的內容就到這邊,感謝你能看到這裡,在這邊祝你早安、午安、晚安,我們明天見。


上一篇
DAY5:程式的邏輯:運算子、流程控制和迴圈
下一篇
Day7:萬物皆物件:設計儲存 GATT Profile 的資料結構
系列文
Web Bluetooth API 實戰:30 天打造通用 BLE 偵錯工具8
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言