Day 11:JavaScript - 箭頭函式、toThrow、DOM/BOM、while loop

補充概念:箭頭函式 Arrow function

目前練習到的內容,在兩個地方大量用到箭頭函式的形式呼叫方法:

  1. TDD 使用 Vitest 的 test, describe, it => expect 等方法

  2. 陣列 map 方法

// 陣列方法的 callback
const numbers = [1, 2, 3];
const doubled = numbers.map((n) => n * 2);

// Vitest 基本語法
test('adds 1 + 2 to equal 3', () => {
  expect(sum(1, 2)).toBe(3)
})

// 測試裡包一層給 toThrow 用
expect(() => divideBy3Times(3.5)).toThrow('n must be a positive integer');

什麼是「語法糖」

  • 語法糖(syntactic sugar)指的是:語言提供的「更好寫、更好讀」的語法,沒有新增新的能力,只是讓原本就做得到的事情更順手

  • 如果拿掉語法糖,還是可以用比較囉嗦的原始語法寫出一樣的邏輯。


箭頭函式當語法糖時的使用情境

沒有用到 this / arguments / new 的情況下,箭頭函式可以直接當成「短版匿名函式」來用,例如:

// 傳統匿名函式
const add = function (a, b) {
return a + b;
};

// 箭頭函式(語法更精簡)
const add = (a, b) => {
return a + b;
};

// 進一步縮寫
const add = (a, b) => a + b;

這些情境裡,箭頭函式的角色:

  • 不會用到 this

  • 只是讓「宣告一個小小的匿名函式」變得比較短、比較可讀。

  • 可以安全地把它看成「function () { ... } 的縮寫版」。


註記:等學到 this 再回來看行為差異

箭頭函式在 thisargumentsnew 等行為上,和傳統函式其實有實質差異,這部分會在之後學到 this、物件方法、建構函式時再深入處理。

目前階段先把箭頭函式當成「短版匿名函式」使用即可;
等學到 this 時,再回來補「箭頭函式與一般函式在行為上的差異」。

Vitest:為什麼 expect 搭配 toThrow 要用箭頭函式包一層?

在 Vitest / Jest 這類測試框架中,要驗證「呼叫某個函式會丟出錯誤」時,常看到這種寫法:

const call = () => divideBy3Times(3.5);
expect(call).toThrow('n must be a positive integer');

初學時覺得這個寫法很不直覺,練習 TDD 時跟 happy path 一樣直接代入參數呼叫函式又讓測試因為報錯直接中斷,所以有兩個疑問:

  • 為什麼不能直接寫 expect(divideBy3Times(3.5)).toThrow()

  • 箭頭函式 () => ... 在這裡到底扮演什麼角色?


直接呼叫 vs 包成函式

寫法一:直接呼叫(錯誤示範)

expect(divideBy3Times(3.5)).toThrow(); // ❌

執行順序:

  1. JavaScript 會先評估 expect(...) 的參數,這裡是 divideBy3Times(3.5)

  2. divideBy3Times(3.5) 會立刻被執行。

  3. 如果函式內有 throw new Error(...),錯誤會直接往外丟出,整個測試函式在進入 expect 之前就已經中斷。

  4. toThrow() 根本沒機會「幫你檢查」這個錯誤是不是預期中的錯誤。

結果:

  • 測試會因為「未被捕捉的例外」而失敗,而不是因為 toThrow 的判斷失敗。


寫法二:包成箭頭函式(正確示範)

const call = () => divideBy3Times(3.5);
expect(call).toThrow('n must be a positive integer'); // ✅

執行順序:

  1. call 是一個函式本身,還沒被呼叫。

  2. expect(call) 把「函式本體」當作參數傳進去,而不是傳「呼叫結果」。

  3. toThrow() 的實作會在內部呼叫 call(),並在這個呼叫過程中監聽有沒有錯誤被丟出來。

  4. 如果有錯誤丟出來,toThrow 會比對錯誤類型、訊息內容等,決定這個測試要標記為通過或失敗。

關鍵差異:

  • 直接呼叫:錯誤在 expect 外就爆掉了。

  • 包成函式:錯誤被「延後」到 toThrow 內部才被觸發,讓測試框架有機會檢查這個錯誤是不是你預期的。


箭頭函式在這裡的真正角色

就語言層面來說,箭頭函式只是其中一種函式表達式(function expression):

// 一般函式表達式
function call () {
  return divideBy3Times(3.5);
}

// 箭頭函式寫法
const call = () => divideBy3Times(3.5);

在這個場景下,兩種寫法的「語意角色」是一樣的:

  • 把「呼叫 divideBy3Times(3.5)」這件事包裝成一個可以稍後再執行的函式

  • 並把這個函式「交給 expect().toThrow()」去呼叫與檢查。

換句話說:
這裡箭頭函式的重點不是「this 綁定」或「語法糖」,而是用來做延遲執行(lazy execution)

  • 不馬上執行 divideBy3Times(3.5)

  • 先把「要做這件事」改寫成一個函式,讓 toThrow 接管呼叫時機。


連回 TDD 思維

  • 想驗證「值」:expect(函式呼叫結果).toBe(...)

  • 想驗證「會不會丟錯」:expect(包起來的函式).toThrow(...)

寫測試時區分:「這次測試要檢查的是回傳值,還是行為(丟錯這件事本身)」,而箭頭函式在這裡就是拿來幫你把「行為」包裝成一個可以交給 expect 的第一級函式。

資料結構

《圖解資料結構 - 使用 JavaScript》

第一章:

  • 程式設計、演算法和資料結構的關係

  • JavaScript 屬於物件導向語言 (OOP),用函式定義屬性和行為

第二章:

  • 用陣列包陣列表示矩陣,以及用一維矩陣壓縮稀疏矩陣的方法

  • 用一維陣列表達多項式的冪次和係數

瀏覽器的物件模型:DOM 和 BOM

《008天重新認識 JavaScript》第三天

瀏覽器上的 JavaScript 包含:

  • JavaScript 核心 (ECMAScript 標準)

  • BOM (Browser Object Model)

  • DOM (Document Object Model)

BOM 控制「瀏覽器本身」:視窗、網址列、導覽相關資訊。 DOM 控制「網頁內容」:節點、文字、屬性、事件等等。

post image

圖片來源

BOM

  • window 是 JavaScript 跟瀏覽器溝通的窗口

DOM

DOM tree from w3schools

post image
  • JavaScript 可透過呼叫文件的屬性、標籤、名稱等方法控制網頁內容

練習題:while loop

找每次循環除法的商小數點後的特定位數的數值

「在不知道要跑幾次之前,使用 while (true) 這種無限迴圈,配合每一輪的條件判斷決定什麼時候 break;在每一輪裡,把 number 用 toFixed 轉成固定小數位數的字串,再用 indexOf 找出小數點位置,透過字串索引來抽出想要的那一位數字。」

toFixed 方法

  • toFixed(2)Number 的方法,回傳「固定小數點後 2 位」的字串。

  • 會做四捨五入,並補零到指定位數。

  • 範例:

    • 10 .toFixed(2)"10.00"

    • (3.3333).toFixed(2)"3.33"

    • (0.004).toFixed(2)"0.00"

indexOf 方法

str.indexOf() 是 String 的方法,,會回傳「指定子字串第一次出現的索引」,如果找不到則回傳 -1

參考文件來源Number.prototype.toFixed() - JavaScript | MDN

用 try...catch...finally 流程控制顯示使用者友善訊息

在終端機互動時顯示自訂訊息

import { divideBy3Times } from './q9.js';
import readline from 'node:readline/promises';
import { stdin as input, stdout as output } from 'node:process';

const rl = readline.createInterface({ input, output });

async function main () {
  const num = await rl.question('請輸入一個正整數:');
  const n = parseInt(num, 10);

  try {
    const times = divideBy3Times(n);
    console.log(`${n} 至少要除以 3 連續 ${times} 次,小數點後第二位四捨五入後是零`);
  } catch (error) {
    console.log('輸入不合法,請輸入一個大於 0 的整數');
  } finally {
    rl.close();
  }
}

main();

  • parseInt(num, 10) 會把使用者輸入的字串轉成整數或 NaN

  • divideBy3Times 內部用 Number.isInteger(n) || n <= 0 檢查輸入是否合法,不合法就 throw new Error(...)

  • try...catch 捕捉這個錯誤,用 CLI 顯示友善訊息,而不是讓未捕捉例外把 Node 行程炸掉。

  • finally 保證 rl.close() 一定會被呼叫,避免 readline 卡住程式。