前面幾篇已經大量使用物件(例如 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
八、練習題
- 建立一個
Rectangle類別,有寬與高,並提供area()與perimeter()方法。 - 替
Wallet加上withdraw(n),餘額不足時丟出錯誤,並用try...catch測試(可複習第十三篇)。 - 替
Cart加上remove(name)方法。
結語
物件讓資料有名字,class 讓程式有結構。當程式愈寫愈大,這種「把相關的資料與功能放在一起」的習慣,會讓維護輕鬆很多。下一篇要回到網頁畫面,學習用 JavaScript 控制樣式與動畫。
沒有留言:
張貼留言