2026年11月11日 星期三

JavaScript 入門教學(十四):物件與 class(屬性、方法、this 與類別)

前面幾篇已經大量使用物件(例如 document、Math)。這一篇要學習如何自己建立物件,並用 class 把「資料」與「功能」包裝在一起,讓程式更有條理。

一、物件:用屬性描述一個東西

const student = {
  name: '小明',
  age: 20,
  scores: [78, 92, 65],
  greet() {
    return '大家好,我是' + this.name;
  }
};

console.log(student.name);          // 小明(點記法)
console.log(student['age']);        // 20(中括號記法,屬性名稱可用變數)
console.log(student.greet());       // 大家好,我是小明

student.age = 21;                   // 修改
student.email = 'ming@example.com'; // 新增(範例信箱)
delete student.email;               // 刪除

物件裡的函式稱為方法。

二、this:指的是「呼叫它的物件」

方法裡的 this 通常指向呼叫該方法的那個物件。上例中 student.greet() 的 this 就是 student。

常見陷阱:箭頭函式沒有自己的 this

const obj = {
  name: '小明',
  good() { return this.name; },          // 正確
  bad: () => { return this.name; }       // 箭頭函式的 this 不是 obj,拿不到 name
};

console.log(obj.good());            // 小明
console.log(obj.bad());             // 拿不到「小明」(結果依執行環境而定,可能是空字串或 undefined)

所以「物件裡的方法」請用 greet() { ... } 的寫法,不要用箭頭函式。另一個陷阱是把方法單獨取出來呼叫(例如 const f = student.greet; f();),這時 this 就不是 student 了。

三、走訪與複製物件

const price = { apple: 30, banana: 20, cherry: 90 };

console.log(Object.keys(price));      // ['apple', 'banana', 'cherry']
console.log(Object.values(price));    // [30, 20, 90]

for (const [name, p] of Object.entries(price)) {
  console.log(name + ':' + p + ' 元');
}

// 淺層複製
const copy = { ...price, banana: 25 };    // 複製並修改 banana
// 深層複製(含巢狀物件)
const deep = structuredClone(student);    // student 裡的陣列也會一併複製

直接寫 const b = a; 並不是複製,而是兩個名稱指向同一個物件,改其中一個,另一個也會跟著變。展開運算子 { ...a } 只複製第一層,裡面的陣列或物件仍然是共用的,需要完全獨立的副本時用 structuredClone。

四、class:物件的設計藍圖

如果要建立很多相似的物件(例如多位學生、多個購物車),一個一個手寫太麻煩,這時用 class 定義藍圖,再用 new 產生物件。

class Student {
  constructor(name, scores) {     // 建立物件時自動執行
    this.name = name;
    this.scores = scores;
  }

  average() {
    const total = this.scores.reduce((t, n) => t + n, 0);
    return total / this.scores.length;
  }
}

const a = new Student('小明', [78, 92, 65]);
const b = new Student('小華', [88, 95, 90]);

console.log(a.average().toFixed(1));   // 78.3
console.log(b.average().toFixed(1));   // 91.0

檢查:78 + 92 + 65 = 235,235 ÷ 3 約 78.3;88 + 95 + 90 = 273,273 ÷ 3 = 91.0。

五、繼承:extends

class GradStudent extends Student {
  constructor(name, scores, thesis) {
    super(name, scores);            // 先呼叫父類別的建構函式
    this.thesis = thesis;
  }

  info() {
    return this.name + '(研究主題:' + this.thesis + ')';
  }
}

const g = new GradStudent('小美', [90, 85], '網頁效能');
console.log(g.info());              // 小美(研究主題:網頁效能)
console.log(g.average());           // 87.5,繼承自 Student

繼承適合「是一種」的關係(研究生「是一種」學生)。不要為了少寫幾行程式就亂用繼承,關係不明確時,直接把物件放進屬性裡(組合)通常更簡單。

六、私有欄位與 getter

在欄位名稱前加上 #,表示它只能在 class 內部使用,外面無法直接讀寫,藉此保護資料不被亂改。

class Wallet {
  #balance = 0;                      // 私有欄位

  deposit(n) {
    if (n <= 0) throw new Error('金額必須大於 0');
    this.#balance += n;
  }

  get balance() {                    // getter:像屬性一樣讀取
    return this.#balance;
  }
}

const w = new Wallet();
w.deposit(500);
console.log(w.balance);              // 500
// console.log(w.#balance);          // SyntaxError:外部不能直接讀取

七、綜合範例:購物車

class Cart {
  #items = [];

  add(name, price, qty = 1) {
    const found = this.#items.find(i => i.name === name);
    if (found) {
      found.qty += qty;              // 已有同名商品就累加數量
    } else {
      this.#items.push({ name, price, qty });
    }
  }

  get total() {
    return this.#items.reduce((t, i) => t + i.price * i.qty, 0);
  }

  get count() {
    return this.#items.reduce((t, i) => t + i.qty, 0);
  }
}

const cart = new Cart();
cart.add('筆記本', 60, 2);           // 示意商品與價格
cart.add('原子筆', 15, 3);
cart.add('筆記本', 60);              // 再買一本

console.log(cart.count);             // 6
console.log(cart.total);             // 60*3 + 15*3 = 225

八、練習題

  1. 建立一個 Rectangle 類別,有寬與高,並提供 area() 與 perimeter() 方法。
  2. 替 Wallet 加上 withdraw(n),餘額不足時丟出錯誤,並用 try...catch 測試(可複習第十三篇)。
  3. 替 Cart 加上 remove(name) 方法。

結語

物件讓資料有名字,class 讓程式有結構。當程式愈寫愈大,這種「把相關的資料與功能放在一起」的習慣,會讓維護輕鬆很多。下一篇要回到網頁畫面,學習用 JavaScript 控制樣式與動畫。

沒有留言:

張貼留言