OOP và Prototypes trong JavaScript hiện đại

Bạn đã bao giờ viết đi viết lại cùng một đoạn code chỉ để tạo các object tương tự nhau chưa? Hoặc thắc mắc tại sao this trong JavaScript đôi khi lại trỏ về undefined một cách khó hiểu? Đây là những vấn đề cực kỳ phổ biến khi làm việc với OOP trong JavaScript.

JavaScript không có OOP “truyền thống” như Java hay C#. Thay vào đó, nó dùng prototype chain — một cơ chế thừa kế linh hoạt hơn nhiều. Bài này sẽ đi từ cách đơn giản nhất (gom dữ liệu vào object) đến các tính năng hiện đại như class, private fields, và giải thích rõ tại sao this lại “chạy mất” trong một số trường hợp.

#1. Xây dựng Object trong JavaScript

#1.1. Encapsulation — Gom dữ liệu và hàm lại với nhau

Vấn đề đầu tiên khi code JavaScript là dữ liệu và các hàm xử lý thường nằm rời rạc. Encapsulation (đóng gói) giải quyết điều này bằng cách bundle chúng vào cùng một object.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Cách rời rạc — khó quản lý
const userName = "Alice";
const userAge = 30;

function greetUser(name) {
  return `Hello, ${name}!`;
}

// Encapsulation — gom lại vào một object
const user = {
  name: "Alice",
  age: 30,
  greet() {
    return `Hello, ${this.name}!`;
  },
  isAdult() {
    return this.age >= 18;
  },
};

console.log(user.greet());    // Hello, Alice!
console.log(user.isAdult());  // true

Giải thích:

  • Thay vì để userName, userAge, greetUser nằm lung tung, bạn gom tất cả vào object user
  • Method greet() có thể gọi trực tiếp trên data: user.greet() thay vì greetUser(userName)
  • Trong thực tế, encapsulation giúp code dễ đọc và dễ maintain hơn nhiều

#1.2. DRY Object Creation — Factory Functions

Vấn đề với cách trên: nếu cần tạo 100 user thì sao? Copy-paste 100 lần? Đây là lúc factory function phát huy tác dụng.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function createUser(name, age, role) {
  return {
    name,
    age,
    role,
    greet() {
      return `Hello, I'm ${this.name} (${this.role})`;
    },
    isAdult() {
      return this.age >= 18;
    },
    describe() {
      return `${this.name}, ${this.age} tuổi`;
    },
  };
}

const alice = createUser("Alice", 30, "admin");
const bob = createUser("Bob", 17, "user");

console.log(alice.greet());   // Hello, I'm Alice (admin)
console.log(bob.isAdult());   // false

Giải thích:

  • Factory function là hàm thường (không phải class), nhận tham số và trả về object mới
  • Mỗi lần gọi createUser() sẽ tạo ra một object độc lập
  • Không còn code lặp lại — đúng với nguyên tắc DRY (Don’t Repeat Yourself)

#Lưu ý

Factory function có một nhược điểm lớn: mỗi object tạo ra đều có bản sao riêng của từng method. Nếu tạo 1000 users thì sẽ có 1000 bản sao của greet(), isAdult(), describe() trong bộ nhớ — cực kỳ lãng phí!

#1.3. Prototype Chain và Object.create() (ES5+)

Giải pháp: thay vì mỗi instance giữ một bản sao riêng, hãy tách các method dùng chung ra một object riêng — các instance sẽ trỏ đến object đó và dùng chung method từ đó. Đây chính là cơ chế prototype chain, và Object.create() là cách thiết lập liên kết đó.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Object chứa các method dùng chung
const userMethods = {
  greet() {
    return `Hello, I'm ${this.name}`;
  },
  isAdult() {
    return this.age >= 18;
  },
  describe() {
    return `${this.name}, ${this.age} tuổi`;
  },
};

function createUser(name, age) {
  const user = Object.create(userMethods); // Tạo object có prototype là userMethods
  user.name = name;
  user.age = age;
  return user;
}

const alice = createUser("Alice", 30);
const bob = createUser("Bob", 17);

console.log(alice.greet());   // Hello, I'm Alice
console.log(bob.greet());     // Hello, I'm Bob

// Cả hai cùng dùng một hàm greet duy nhất trong bộ nhớ
console.log(alice.greet === bob.greet); // true ✅

Khi bạn gọi alice.greet(), JavaScript sẽ:

  1. Tìm greet trong chính alice — không có
  2. Leo lên prototype (userMethods) — tìm thấy, chạy hàm đó
View Mermaid diagram code
flowchart LR
    alice["alice<br/>{ name, age }"] -->|"[[Prototype]]"| userMethods["userMethods<br/>{ greet, isAdult, describe }"]
    bob["bob<br/>{ name, age }"] -->|"[[Prototype]]"| userMethods

Chain có thể nhiều tầng — Object.create() cho phép nối tiếp các object lại với nhau:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const userMethods = {
  greet() {
    return `Hello, I'm ${this.name}`;
  },
  isAdult() {
    return this.age >= 18;
  },
  describe() {
    return `${this.name}, ${this.age} tuổi`;
  },
};

const adminMethods = Object.create(userMethods); // adminMethods kế thừa userMethods

adminMethods.ban = function (target) {
  return `${this.name} đã ban ${target}`;
};

function createAdmin(name, age) {
  const admin = Object.create(adminMethods);
  admin.name = name;
  admin.age = age;
  return admin;
}

const aliceAdmin = createAdmin("Alice", 30);

console.log(aliceAdmin.ban("Bob"));   // Alice đã ban Bob       (adminMethods)
console.log(aliceAdmin.greet());      // Hello, I'm Alice       (userMethods)
console.log(aliceAdmin.isAdult());    // true                   (userMethods)

JavaScript leo chain theo thứ tự: aliceAdminadminMethodsuserMethods cho đến khi tìm thấy method hoặc trả về undefined.

View Mermaid diagram code
flowchart LR
    aliceAdmin["aliceAdmin<br/>{ name, age }"] -->|"[[Prototype]]"| adminMethods["adminMethods<br/>{ ban }"]
    adminMethods -->|"[[Prototype]]"| userMethods["userMethods<br/>{ greet, isAdult, describe }"]

#1.4. new Keyword — Tự Động Hóa Quá Trình Tạo Object (ES3+)

Cách dùng Object.create() thủ công hơi dài dòng. Từ khóa new tự động làm những việc này cho bạn khi gọi một constructor function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function User(name, age) {
  // new tự động:
  // 1. Tạo object rỗng {}
  // 2. Gán this = object rỗng đó
  // 3. Link prototype: this.[[Prototype]] = User.prototype
  // 4. Return this

  this.name = name; // Bạn chỉ cần gán data
  this.age = age;
}

// Methods đặt trên User.prototype để dùng chung
User.prototype.greet = function () {
  return `Hello, I'm ${this.name}`;
};

User.prototype.isAdult = function () {
  return this.age >= 18;
};

const alice = new User("Alice", 30);
const bob = new User("Bob", 17);

console.log(alice.greet());              // Hello, I'm Alice
console.log(bob.isAdult());             // false
console.log(alice.greet === bob.greet); // true ✅ — dùng chung

Bốn thứ new làm tự động:

  1. Tạo object rỗng {}
  2. Gán this = object rỗng vừa tạo
  3. Liên kết prototype: object.[[Prototype]] = ConstructorFn.prototype
  4. Return this (ngầm định)

#Lưu ý

Nếu quên new khi gọi constructor function, this sẽ trỏ về globalThis (hoặc undefined trong strict mode) và code sẽ bị lỗi hoặc tạo biến global không mong muốn.

#1.5. class Keyword — Syntactic Sugar Dễ Đọc Hơn (ES2015+)

class trong JavaScript chỉ là cú pháp đẹp hơn cho constructor function + prototype. Bên dưới engine vẫn hoạt động hoàn toàn giống nhau.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class User {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  greet() {
    return `Hello, I'm ${this.name}`;
  }

  isAdult() {
    return this.age >= 18;
  }
}

const alice = new User("Alice", 30);
const bob = new User("Bob", 17);

console.log(alice.greet());              // Hello, I'm Alice
console.log(alice.greet === bob.greet); // true ✅ — vẫn dùng chung prototype

// Proof: class chỉ là syntactic sugar
console.log(typeof User); // "function" — vẫn là function!

So sánh class vs constructor function:

Constructor FunctionClass
Syntaxfunction User() {}class User {}
MethodsUser.prototype.greet = ...Viết trong body class
HoistingĐược hoistingKhông được hoisting
Strict modeTùyLuôn strict mode
ReadabilityTrung bìnhDễ đọc hơn

Trong thực tế, dùng class là tiêu chuẩn hiện đại. Nhưng hiểu prototype chain bên dưới vẫn quan trọng để debug khi gặp vấn đề.

#2. Tính năng Class hiện đại

#2.1. this trong Arrow vs Regular Functions (ES2015+)

Đây là một trong những nguyên nhân gây bug phổ biến nhất trong JavaScript OOP.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Timer {
  constructor() {
    this.seconds = 0;
  }

  // ❌ Vấn đề: regular function mất this
  startBroken() {
    setInterval(function () {
      this.seconds++; // this = undefined (strict mode) hoặc global!
      console.log(this.seconds);
    }, 1000);
  }

  // ✅ Giải pháp: arrow function giữ this từ context bên ngoài
  startFixed() {
    setInterval(() => {
      this.seconds++; // this = Timer instance ✅
      console.log(this.seconds);
    }, 1000);
  }
}

const timer = new Timer();
timer.startFixed(); // 1, 2, 3, 4, ...

Giải thích:

  • Regular function (function() {}) có this riêng, được xác định tại lúc gọi hàm — không phải lúc định nghĩa
  • Arrow function (() => {}) không có this riêng, nó kế thừa this từ scope bên ngoài (lexical binding)
  • Trong callback như setInterval, setTimeout, array methods (map, forEach), hãy dùng arrow function khi cần truy cập this của class
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class ShoppingCart {
  constructor(discount) {
    this.prices = [100, 200, 300];
    this.discount = discount;
  }

  // ✅ Arrow function giữ this trong map
  getDiscounted() {
    return this.prices.map((price) => price * (1 - this.discount));
  }

  // ❌ Regular function mất this
  getDiscountedBroken() {
    return this.prices.map(function (price) {
      return price * (1 - this.discount); // this.discount = undefined!
    });
  }
}

const cart = new ShoppingCart(0.1);
console.log(cart.getDiscounted()); // [90, 180, 270] ✅

#2.2. Public Instance Fields (ES2022+)

Instance fields là thuộc tính được khởi tạo mặc định cho mỗi instance, chạy trước constructor.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class UserProfile {
  // Instance fields — set mặc định trước constructor
  isActive = true;
  loginCount = 0;
  preferences = {
    theme: "light",
    language: "vi",
  };

  constructor(name, email) {
    this.name = name;
    this.email = email;
    // isActive, loginCount, preferences đã có sẵn rồi
  }

  login() {
    this.loginCount++;
    return `${this.name} đã đăng nhập lần thứ ${this.loginCount}`;
  }
}

const alice = new UserProfile("Alice", "alice@example.com");
const bob = new UserProfile("Bob", "bob@example.com");

alice.login();
alice.login();

console.log(alice.loginCount);  // 2
console.log(bob.loginCount);    // 0 — độc lập với alice

// Mỗi instance có preferences object riêng
alice.preferences.theme = "dark";
console.log(bob.preferences.theme); // "light" — không bị ảnh hưởng

Giải thích:

  • Instance fields viết ngoài constructor, trực tiếp trong class body
  • Mỗi instance có bản sao riêng — thay đổi alice.loginCount không ảnh hưởng bob.loginCount
  • Hữu ích để set giá trị mặc định rõ ràng, dễ đọc hơn so với viết trong constructor

#2.3. Public Static Fields (ES2022+)

Static fields là thuộc tính thuộc về class, không phải instance. Tất cả instance đều truy cập cùng một giá trị.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Config {
  static version = "2.1.0";
  static maxUsers = 1000;
  static appName = "MyApp";

  constructor(userRole) {
    this.userRole = userRole;
  }

  getInfo() {
    return `${Config.appName} v${Config.version} — Role: ${this.userRole}`;
  }
}

const admin = new Config("admin");
const guest = new Config("guest");

console.log(Config.version);    // "2.1.0"   — truy cập qua class
console.log(Config.maxUsers);   // 1000
console.log(admin.getInfo());   // MyApp v2.1.0 — Role: admin
console.log(guest.getInfo());   // MyApp v2.1.0 — Role: guest

// Static field không có trên instance
console.log(admin.version);     // undefined ❌

Khi nào dùng static fields:

  • Hằng số dùng chung (version, config, limits)
  • Counter đếm số lượng instance đã tạo
  • Utility methods không cần dữ liệu của instance

#2.4. Private Fields (#) — Ẩn Dữ Liệu Nội Bộ (ES2022+)

Private fields (dùng prefix #) chỉ có thể truy cập bên trong class. Đây là cách enforce encapsulation thực sự trong JavaScript.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class BankAccount {
  #balance = 0;        // Private — bên ngoài không thể đọc/ghi trực tiếp
  #owner;              // Private

  constructor(owner, initialBalance) {
    this.#owner = owner;
    this.#balance = initialBalance;
  }

  deposit(amount) {
    if (amount <= 0) throw new Error("Số tiền phải lớn hơn 0");
    this.#balance += amount;
    return this;  // Cho phép chaining
  }

  withdraw(amount) {
    if (amount > this.#balance) throw new Error("Số dư không đủ");
    this.#balance -= amount;
    return this;
  }

  // Getter — đọc balance một cách có kiểm soát
  get balance() {
    return this.#balance;
  }

  getStatement() {
    return `Tài khoản của ${this.#owner}: ${this.#balance.toLocaleString()}đ`;
  }
}

const account = new BankAccount("Alice", 1000000);

account.deposit(500000).withdraw(200000); // Chaining
console.log(account.balance);            // 1300000
console.log(account.getStatement());     // Tài khoản của Alice: 1.300.000đ

// Cố tình truy cập private field — lỗi ngay!
console.log(account.#balance); // SyntaxError: Private field '#balance' must be declared

Tại sao cần private fields:

  • Bảo vệ dữ liệu quan trọng — balance không thể bị set tùy tiện từ bên ngoài
  • Kiểm soát access — mọi thay đổi phải qua method có validation
  • Khác với _balance (convention cũ) — #balance thực sự không thể truy cập, không chỉ là “thỏa thuận”

#2.5. Private Static Fields — Dữ Liệu Dùng Chung Nhưng Ẩn (ES2022+)

Private static fields kết hợp hai tính chất: dùng chung giữa tất cả instance (static) nhưng chỉ accessible bên trong class (#).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class UserRegistry {
  static #count = 0;             // Đếm số user đã tạo — private và shared
  static #instances = new Map(); // Lưu trữ tất cả instances

  #id;
  name;

  constructor(name) {
    UserRegistry.#count++;
    this.#id = UserRegistry.#count;
    this.name = name;
    UserRegistry.#instances.set(this.#id, this);
  }

  static getTotalUsers() {
    return UserRegistry.#count;
  }

  static findById(id) {
    return UserRegistry.#instances.get(id) || null;
  }

  toString() {
    return `User #${this.#id}: ${this.name}`;
  }
}

const alice = new UserRegistry("Alice");
const bob = new UserRegistry("Bob");
const charlie = new UserRegistry("Charlie");

console.log(UserRegistry.getTotalUsers()); // 3
console.log(UserRegistry.findById(2));     // User object của Bob
console.log(`${UserRegistry.findById(1)}`); // "User #1: Alice"

// Không thể truy cập trực tiếp
console.log(UserRegistry.#count); // SyntaxError ❌

Giải thích:

  • static #count — tất cả instance đều tăng cùng một counter, nhưng code bên ngoài không thể reset nó tùy tiện
  • Hữu ích cho patterns như singleton, registry, hoặc caching
  • Đảm bảo tính nhất quán của dữ liệu nội bộ class

#3. 4 Đặc tính OOP

#3.1. Encapsulation — Đóng gói

Encapsulation là đặc tính đầu tiên và cũng là nền tảng của OOP: gom dữ liệu và method xử lý vào cùng một chỗ, đồng thời kiểm soát quyền truy cập từ bên ngoài.

Bạn đã thấy dạng đơn giản nhất ở phần 1.1 — bundle data + method vào một object. Nhưng encapsulation đầy đủ trong JavaScript hiện đại còn bao gồm private fields (phần 2.4) để ngăn code bên ngoài can thiệp trực tiếp vào trạng thái nội bộ:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Counter {
  #count = 0; // Trạng thái nội bộ — bên ngoài không thể ghi trực tiếp

  increment() { this.#count++; }
  decrement() { if (this.#count > 0) this.#count--; }
  reset()     { this.#count = 0; }

  get value() { return this.#count; }
}

const counter = new Counter();
counter.increment();
counter.increment();
counter.decrement();

console.log(counter.value);   // 1
1
2
// ❌ Cố tình truy cập private field — SyntaxError ngay khi parse, không chạy được
counter.#count = 999;

#3.2. Abstraction — Ẩn Chi Tiết, Chỉ Expose Những Gì Cần Thiết

Abstraction (trừu tượng hóa) có nghĩa là ẩn đi sự phức tạp bên trong, chỉ để lộ ra interface đơn giản mà người dùng cần. Bạn không cần biết động cơ xe hoạt động thế nào — chỉ cần biết đạp ga và phanh.

Trong JavaScript, abstraction được thực hiện qua private fields + getter/setter + public methods:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
class VideoPlayer {
  #currentTime = 0;
  #isPlaying = false;
  #volume = 0.8;
  #src;

  constructor(src) {
    this.#src = src;
    this.#loadMedia(); // Chi tiết load ẩn bên trong
  }

  // Chi tiết kỹ thuật — người dùng không cần biết
  #loadMedia() {
    console.log(`Loading: ${this.#src}...`);
    // decode, buffer, prepare stream, v.v.
  }

  #applyVolume() {
    // normalize, equalizer, DSP processing...
    console.log(`Volume set to ${this.#volume}`);
  }

  // Interface đơn giản để người dùng tương tác
  play() {
    if (this.#isPlaying) return;
    this.#isPlaying = true;
    console.log("Playing...");
  }

  pause() {
    this.#isPlaying = false;
    console.log("Paused.");
  }

  setVolume(level) {
    if (level < 0 || level > 1) throw new Error("Volume phải từ 0 đến 1");
    this.#volume = level;
    this.#applyVolume(); // Gọi logic phức tạp bên trong
  }

  get status() {
    return {
      playing: this.#isPlaying,
      volume: this.#volume,
      time: this.#currentTime,
    };
  }
}

const player = new VideoPlayer("movie.mp4");
player.play();
player.setVolume(0.5);
console.log(player.status); // { playing: true, volume: 0.5, time: 0 }

// Người dùng không thể (và không cần) gọi #loadMedia, #applyVolume

Giải thích:

  • #loadMedia()#applyVolume() là private methods — ẩn hoàn toàn logic kỹ thuật phức tạp
  • Người dùng chỉ cần play(), pause(), setVolume() — interface gọn, dễ hiểu
  • Abstraction và Encapsulation bổ trợ nhau: Encapsulation gom dữ liệu lại, Abstraction ẩn sự phức tạp đi

#3.3. Inheritance — Kế Thừa với extends và super (ES2015+)

Inheritance (kế thừa) cho phép một class con tái sử dụng code từ class cha, chỉ cần thêm hoặc override những gì khác biệt.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
class Animal {
  constructor(name, sound) {
    this.name = name;
    this.sound = sound;
  }

  speak() {
    return `${this.name}: ${this.sound}!`;
  }

  eat(food) {
    return `${this.name} đang ăn ${food}`;
  }

  toString() {
    return `[Animal: ${this.name}]`;
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name, "Gâu gâu"); // Gọi constructor của Animal
    this.breed = breed;
  }

  fetch(item) {
    return `${this.name} đã lấy ${item} về!`;
  }

  // Override method của Animal
  toString() {
    return `[Dog: ${this.name} (${this.breed})]`;
  }
}

class Cat extends Animal {
  constructor(name, isIndoor) {
    super(name, "Meo meo");
    this.isIndoor = isIndoor;
  }

  purr() {
    return `${this.name}: Rrrr...`;
  }
}

const rex = new Dog("Rex", "Labrador");
const mimi = new Cat("Mimi", true);

console.log(rex.speak());        // Rex: Gâu gâu!  (từ Animal)
console.log(rex.fetch("bóng"));  // Rex đã lấy bóng về!  (của Dog)
console.log(rex.eat("xương"));   // Rex đang ăn xương  (từ Animal)
console.log(`${rex}`);           // [Dog: Rex (Labrador)]  (override)

console.log(mimi.speak());       // Mimi: Meo meo!
console.log(mimi.purr());        // Mimi: Rrrr...

// Kiểm tra quan hệ kế thừa
console.log(rex instanceof Dog);    // true
console.log(rex instanceof Animal); // true ✅ — Dog là Animal
View Mermaid diagram code
flowchart TD
    Animal["Animal<br/>- name, sound<br/>+ speak(), eat()"] --> Dog["Dog extends Animal<br/>- breed<br/>+ fetch(), toString()"]
    Animal --> Cat["Cat extends Animal<br/>- isIndoor<br/>+ purr()"]

Quy tắc khi dùng super:

  • Trong constructor: phải gọi super() trước khi dùng this, nếu không sẽ lỗi
  • Trong method: super.methodName() để gọi method cùng tên của class cha
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class User {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  greet() {
    return `Hello, I'm ${this.name}`;
  }

  isAdult() {
    return this.age >= 18;
  }
}

class AdminUser extends User {
  constructor(name, age, permissions) {
    super(name, age); // Phải gọi trước
    this.permissions = permissions;
  }

  greet() {
    const base = super.greet(); // Lấy kết quả từ User.greet()
    return `${base} [ADMIN]`;
  }
}

const admin = new AdminUser("Alice", 30, ["read", "write", "delete"]);
console.log(admin.greet()); // Hello, I'm Alice [ADMIN]

#3.4. Polymorphism — Cùng Tên, Khác Hành Vi

Polymorphism (đa hình) là khả năng các object khác loại có thể phản hồi cùng một method call theo cách riêng của mình. Code gọi không cần biết đang làm việc với loại object nào.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
class Shape {
  constructor(color) {
    this.color = color;
  }

  area() {
    // Class cha định nghĩa interface, subclass phải override
    throw new Error(`${this.constructor.name} phải implement area()`);
  }

  describe() {
    return `${this.constructor.name} màu ${this.color}, diện tích: ${this.area().toFixed(2)}`;
  }
}

class Circle extends Shape {
  constructor(color, radius) {
    super(color);
    this.radius = radius;
  }

  area() {
    return Math.PI * this.radius ** 2; // Override
  }
}

class Rectangle extends Shape {
  constructor(color, width, height) {
    super(color);
    this.width = width;
    this.height = height;
  }

  area() {
    return this.width * this.height; // Override
  }
}

class Triangle extends Shape {
  constructor(color, base, height) {
    super(color);
    this.base = base;
    this.height = height;
  }

  area() {
    return (this.base * this.height) / 2; // Override
  }
}

const shapes = [
  new Circle("đỏ", 5),
  new Rectangle("xanh", 4, 6),
  new Triangle("vàng", 3, 8),
];

// Polymorphism: gọi cùng method area() trên các loại object khác nhau
shapes.forEach((shape) => {
  console.log(shape.describe());
});
// Circle màu đỏ, diện tích: 78.54
// Rectangle màu xanh, diện tích: 24.00
// Triangle màu vàng, diện tích: 12.00

// Hàm tổng diện tích — không cần biết từng shape là loại gì
function totalArea(shapes) {
  return shapes.reduce((sum, shape) => sum + shape.area(), 0);
}

console.log(totalArea(shapes).toFixed(2)); // 114.54

Giải thích:

  • totalArea() gọi shape.area() mà không quan tâm shape là Circle, Rectangle hay Triangle
  • Mỗi subclass tự quyết định cách tính area() — đây chính là polymorphism
  • Khi thêm Pentagon sau này, chỉ cần tạo class mới extends Shape, không cần sửa totalArea()

Tóm tắt 4 đặc tính OOP trong JavaScript:

Đặc tínhCơ chế chínhMục đích
EncapsulationObject, class, # fieldsGom dữ liệu và method, kiểm soát truy cập
AbstractionPrivate fields, getter/setterẨn sự phức tạp, expose interface gọn
Inheritanceextends, superTái sử dụng code từ class cha
PolymorphismMethod overrideCùng method, hành vi khác theo từng class

Vậy là bạn đã có đủ bức tranh OOP trong JavaScript — từ prototype chain đến đủ 4 đặc tính cốt lõi. Điểm khác biệt của JavaScript so với Java hay C# là mọi thứ đều xây trên prototype, class chỉ là lớp cú pháp bên trên. Hiểu cả hai tầng này sẽ giúp bạn vừa viết code hiện đại, sạch đẹp, vừa debug được khi gặp vấn đề kỳ lạ với this hay inheritance.