物件導向 = 一種思維方式。
class = 實現這種思維的語法工具。
封裝、繼承、多型 = 物件導向的「三大特性」,及核心精神。
例如寫一個物件來描述一隻貓,
js
javascript
const cat = {
name: "小黑",
age: 5,
scratching: function() {
console.log("刮!");
}
};
cat.scratching(); // 輸出:刮!
scratching有抓/刮的意思。
當你今天想要描述五隻貓,會寫五次對吧!
const cat1 = {name: "小紅", age:5,scratching: function() {console.log("刮!"); }};
const cat2 = {name: "小黃", age:1,scratching: function() {console.log("刮!"); }};
const cat3 = {name: "小藍", age:6,scratching: function() {console.log("刮!"); }};
const cat4 = {name: "小綠", age:3,scratching: function() {console.log("刮!"); }};
const cat5 = {name: "小紫", age:9,scratching: function() {console.log("刮!"); }};
當寫完五次會有產生一個想法,就是累! 雖然能把第一個範例來複製貼上,是還是要改const的變數名稱、name屬性、
age屬性。但其實有更快的更簡短的寫法,就是下面要講的。
像是製作物件的設計圖。用 class 可以批量生產相似的物件。
class Cat {
constructor(name, age) {
this.name = name;
this.age = age;
}
scratching() {
console.log(${this.name} 刮!);
}
}
const Cat1 = new Cat("小紅", 5);
const Cat2 = new Cat("小黃", 1);
const Cat3 = new Cat("小藍", 6);
const Cat4 = new Cat("小綠", 3);
const Cat2 = new Cat("小紫", 9);
當你console.log(Cat1);會得到
Cat {name: '小紅', age: 5,scratching(){console.log(${this.name} 刮!);}}
當你console.log(Cat2);會得到
Cat {name: '小黃', age: 1,scratching(){console.log(${this.name} 刮!);}}
當你console.log(Cat2);會得到
Cat {name: '小藍', age: 6,scratching(){console.log(${this.name} 刮!);}}
當你console.log(Cat2);會得到
Cat {name: '小綠', age: 3,scratching(){console.log(${this.name} 刮!);}}
當你console.log(Cat2);會得到
Cat {name: '小紫', age: 9,scratching(){console.log(${this.name} 刮!);}}
雖然看起來好像前面在寫class的時候,更複雜、更久,但是當你要創立描述100隻貓的物件,這時候你應該就會覺得方便了![]()
建構子的特性:
唯一性: 當您定義一個類別時,如果需要初始化新建立的物件實例,只能定義一個constructor的方法。
彈性參數: 不能根據傳入的參數數量或類型不同而定義多個同名函式。
當你使用類別Class創造一個模板(設計圖),只有在 new 新的物件實例時,需要初始化屬性/值,才需要寫 constructor(){}。如果 new 新的物件實例時,並沒有打算初始化屬性/值,就可以不寫constructor(){}。
需要使用constructor的例子:
// 情況 1:創建時需要給屬性/值 → 需要 constructor
class Cat {
constructor(name, age) { // 需要!
this.name = name;
this.age = age;
}
}
const cat1 = new Cat("小黑", 5); // 創建時就給值
console.log(cat1.name); // "小黑"
console.log(cat1.age); // 5
在這例子當中,我要創立一個Cat的模板物件(設計圖),並且當使用這個模板物件new一個
新實例物件的時候,能在創立新物件時,把name、age的屬性給新實例物件,這樣新的cat1物件
就有name、age屬性,值分別為new的時候給的參數"小黑"、 5。
這個例子如果不寫this,會發生什麼事情?
// 情況 1:創建時需要給屬性/值 → 需要 constructor
class Cat {
constructor(name, age) { // 需要!
name = name; // 沒有 this
age = age;
}
}
const cat1 = new Cat("小黑", 5); // 創建時就給值
console.log(cat1); // {} 空物件
console.log(cat1.name); // undefined
console.log(cat1.age); // undefined
這個例子中,你會發現,如果不寫this,cat1是{},你說為什麼?不是在new的時候就把name、age
給cat1了嗎?這時候就要看以下new Cat("小黑", 5)時發生什麼事情。
// 1. 創建一個空物件
const cat1 = {};
// 2. 呼叫 constructor,並傳入參數
// 此時 constructor 裡的 name = "小黑", age = 5(區域變數)
// 3. 執行 constructor 內的程式碼
name = name; // "小黑" = "小黑"(只是把變數指派給自己,沒做任何事)
age = age; // 5 = 5(同上)
// 注意:這裡完全沒有碰到 cat1 這個物件
// 因為沒有 this.name,所以 cat1 還是 {}
當你看到步驟三。name = name; ,帶進去的「參數 name」指派給「參數 name」自己。
age = age;,帶進去的「參數 age」指派給「參數 age」自己。
這樣的話是一件沒有意義的事情。當你用new創一個實例的時候,你也希望創建物件時就有給模板物件內的屬性跟值,
所以必須要在class的Cat模板物件中的constructor函式裡面,使用this.name = name的方式,才能夠透過模板物件把值跟屬性給到cat1物件。
接下來你或許對this不太熟悉,可能會問
constructor(name, age) {
this.name = name;
this.age = age;
}
裡面的this是什麼意思?
this 就是指「你現在正在創造的這個物件」。每次用 new 創造新物件,this 就指向那個新物件,在以上描述五隻貓的範例中,Cat1的this就是Cat1、Cat2的this就是Cat2、Cat3的this就是Cat3、Cat4的this就是Cat4、Cat5的this就是Cat5。
一定要有 this 嗎?
在 constructor 裡如果你要設定物件的屬性,就一定要用 this。以上面用class描述五隻貓的物件作為例子,如果 把constructor方法裡面的this去掉,會變成name = name; age = age;。其他不變的情況下,這時候const Cat1 = new Cat("小紅", 5);新增一個實例Cat1,當你使用console.log(Cat1.name);會出現undefined
為何要加new?
new 是 JavaScript 的關鍵字,作用是:
創造一個新的空物件
執行 class 的 constructor
把新物件回傳給你
如果不寫 new ,
const cat1 = Cat("小紅", 5); 會出錯(TypeError: Class constructor Cat cannot be invoked without 'new'),TypeError:不能在沒有「new」的情況下呼叫類別建構子 Dog。
是將物件的內部狀態(資料/屬性)隱藏起來,只透過物件提供的公共方法(行為)來存取這些狀態。核心目的是保護資料的完整性,並限制外部程式碼隨意修改內部資料。
方式一:使用 # 私有欄位 (現代、嚴格的封裝)
javascript
class SafeBox {
#password = "Secret123"; // 私有屬性
// 類別內部的方法可以存取 #password
checkPassword(input) {
return input === this.#password;
}
}
const myBox = new SafeBox();
// 外部程式碼嘗試存取,這會導致錯誤!
console.log(myBox.#password); // Uncaught SyntaxError: Private field '#password' must be declared in an enclosing class
方式二:沒有 # 私有欄位 (傳統的命名約定/閉包技巧
使用命名約定:
我們使用一個底線 _ 作為屬性名稱開頭,告訴其他工程師:「嘿,這個屬性是私有的,請不要動它。」但 JavaScript
執行環境不會阻止任何人去存取它。本質是開發團隊內部的共識或禮貌。
使用閉包:
利用 JavaScript 的閉包特性來實現真正的私有變數。
當程式碼需要重用的時候,不用重複寫。
// 父類別:所有動物共同的特性
class Animal {
constructor(name) {
this.name = name;
}
eat() {
console.log(`${this.name}在吃東西`);
}
sleep() {
console.log(`${this.name}在睡覺`);
}
}
// 子類別:狗(繼承 Animal)
class Dog extends Animal {
bark() {
console.log(`${this.name}說:汪汪!`);
}
}
// 子類別:貓(繼承 Animal)
class Cat extends Animal {
meow() {
console.log(`${this.name}說:喵喵!`);
}
}
// 子類別:鳥(繼承 Animal)
class Bird extends Animal {
fly() {
console.log(`${this.name}在飛`);
}
}
// 使用
const dog = new Dog("小白");
dog.eat(); // 繼承來的方法
dog.sleep(); // 繼承來的方法
dog.bark(); // 自己的方法
const cat = new Cat("小咪");
cat.eat(); // 繼承來的方法
cat.meow(); // 自己的方法
const bird = new Bird("小鳥");
bird.eat(); // 繼承來的方法
bird.fly(); // 自己的方法
當你有定義多個class模板類別,且全部類別都有一樣的方法時,可以不用每個new的實例物件都寫一次,而是可以先class定義這個有一樣方法的類別,當new一個有相同方法的新實例物件,就可以在new Cat之後寫extends,後面再放裝有重複方法的類別名稱,以上面例子來說就是Animal類別物件,這時候就完成繼承。不必在創建class類別物件時寫重複的方法,就可以使用。
不同類別可以有同名的方法,但做不同的事情。
class Dog {
speak() {
console.log("汪汪!");
}
}
class Cat {
speak() {
console.log("喵喵!");
}
}
class Cow {
speak() {
console.log("哞~");
}
}
// 都叫 speak,但行為不同
const animals = [
new Dog(),
new Cat(),
new Cow()
];
animals.forEach(animal => {
animal.speak(); // 每個動物發出不同的聲音
});
// 輸出:
// 汪汪!
// 喵喵!
// 哞~
雖然每個物件的類別不同,但都有 speak() 方法,就能統一處理,這就是多型的好處。