在 JavaScript 中,物件導向(OOP,Object-Oriented Programming)是一種強大的編程方式,透過物件(objects)和類別(classes)的概念來組織和管理程式碼。JavaScript 本身不是純粹的物件導向語言,但透過 class
和 prototype
等特性,我們能夠實現物件導向的編程風格。
在 ES6 之前,JavaScript 是基於 prototype
繼承的,但 ES6 引入了 class
語法,使得物件導向的寫法更加簡潔直觀。
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
return `Hello, my name is ${this.name} and I am ${this.age} years old.`;
}
}
const alice = new Person('Alice', 30);
console.log(alice.greet()); // Hello, my name is Alice and I am 30 years old.
constructor
是用來初始化物件的函式,會在每次使用 new
關鍵字建立物件時自動執行。
繼承允許一個類別(子類,subclass
)從另一個類別(父類,superclass
)繼承屬性和方法,使得程式碼更加可重用和可擴展。
class Animal {
constructor(name) {
this.name = name;
}
speak() {
return `${this.name} makes a sound.`;
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // 呼叫父類的 constructor
this.breed = breed;
}
speak() {
return `${this.name} the ${this.breed} barks.`;
}
}
const dog = new Dog('Rex', 'Labrador');
console.log(dog.speak()); // Rex the Labrador barks.
這裡 Dog
類別繼承了 Animal
,並且使用 super()
來呼叫父類的 constructor
。speak()
方法被覆蓋(重寫),實現了不同的行為。
多型允許子類別在不改變接口的前提下,覆寫父類別的方法,讓子類根據需要實現不同的行為。這提升了程式的靈活性。
class Bird extends Animal {
speak() {
return `${this.name} chirps.`;
}
}
const bird = new Bird('Tweety');
console.log(bird.speak()); // Tweety chirps.
雖然 Bird
和 Dog
都繼承自 Animal
,但它們的 speak()
方法行為不同,這就是多型的體現。
封裝是指將資料與操作這些資料的方法封裝在一起,並隱藏內部的實現細節。JavaScript 並不具有傳統的私有屬性,但可以透過一些技巧來模擬私有性。
class Person {
constructor(name) {
this._name = name; // 以 "_" 開頭表示私有
}
getName() {
return this._name;
}
setName(name) {
this._name = name;
}
}
const person = new Person('Alice');
console.log(person.getName()); // Alice
person.setName('Bob');
console.log(person.getName()); // Bob
#
符號實現真正的私有屬性:class Person {
#age = 0; // 私有屬性
constructor(name, age) {
this.name = name;
this.#age = age;
}
getAge() {
return this.#age;
}
setAge(age) {
if (age > 0) {
this.#age = age;
}
}
}
const person = new Person('Alice', 30);
console.log(person.getAge()); // 30
person.setAge(35);
console.log(person.getAge()); // 35
// console.log(person.#age); // Error: 私有屬性不能被外部訪問
在這裡,使用 #
符號可以定義真正的私有屬性,無法從外部直接訪問或修改。
靜態方法和屬性是屬於類別本身而非實例的,這意味著它們只能被類別直接呼叫,而不是通過類的實例來呼叫。
class MathUtil {
static add(a, b) {
return a + b;
}
static PI = 3.14159;
}
console.log(MathUtil.add(2, 3)); // 5
console.log(MathUtil.PI); // 3.14159
靜態方法 add()
和靜態屬性 PI
只能透過 MathUtil
類別直接訪問,而無法透過實例使用。
JavaScript 本身不直接支持抽象類別與介面,但我們可以透過模式來實現類似的功能。抽象類別是不能被實例化的類,它僅作為其他類別的基礎;介面則是用來定義必須被實現的方法。
class Shape {
constructor() {
if (new.target === Shape) {
throw new Error("Cannot instantiate abstract class Shape");
}
}
area() {
throw new Error("Method 'area()' must be implemented.");
}
}
class Circle extends Shape {
constructor(radius) {
super();
this.radius = radius;
}
area() {
return Math.PI * this.radius ** 2;
}
}
const circle = new Circle(5);
console.log(circle.area()); // 78.53981633974483
// const shape = new Shape(); // Error: Cannot instantiate abstract class Shape
這裡 Shape
是一個模擬的抽象類別,不能直接實例化,並且要求子類實現 area()
方法。
this
關鍵字與上下文綁定在 JavaScript 中,this
的值根據函式的呼叫方式而變動。了解 this
綁定的方式(如 bind()
、call()
和 apply()
)是 OOP 中的重要技巧。
this
的基本綁定方式:class Person {
constructor(name) {
this.name = name;
}
greet() {
console.log(`Hello, I am ${this.name}`);
}
}
const alice = new Person('Alice');
alice.greet(); // Hello, I am Alice
const greetFunction = alice.greet;
greetFunction(); // Error: 'this' is undefined or doesn't refer to 'Person'
// 使用 bind() 明確綁定 this
const boundGreet = alice.greet.bind(alice);
boundGreet(); // Hello, I am Alice
bind()
可以將 this
明確綁定到特定物件上,避免因為上下文變化導致 this
指向錯誤。
JavaScript 的物件繼承是基於原型的,每個物件都有一個隱式的 __proto__
屬性指向它的原型。理解原型鏈有助於掌握物件屬性和方法的查找機制。
function Person(name) {
this.name = name;
}
Person.prototype.greet = function() {
return `Hello, I am ${this.name}`;
};
const bob = new Person('Bob');
console.log(bob.greet()); // Hello, I am Bob
console.log(bob.__proto__ === Person.prototype); // true
在這裡,bob
繼承了 Person.prototype
上的 greet
方法。當呼叫 bob.greet()
時,JavaScript 會沿著原型鏈查找 greet
方法。