Chương 04 · Objects, Prototypes & Classes

Đối tượng, prototype và lớp

Trong JavaScript, mọi thứ không phải primitive đều là object — array, function, Date, kể cả class. Khác với Java/C#, JS dùng mô hình prototype chain chứ không phải hệ thống lớp cứng. class ES6 chỉ là cú pháp ngọt phủ lên prototype. Hiểu lớp dưới đó = không bị lạc khi gặp this kỳ lạ, __proto__ hay Object.create(null).

Độ dài: ~1000 dòng Bài tập: 5 Quiz: 8 Prerequisites: Chương 1-3
🎯 Mục tiêu chương
  • Master object literal: shorthand property, computed key, method shorthand.
  • Phân biệt dot vs bracket notation và biết khi nào bắt buộc dùng bracket.
  • Hiểu rõ reference vs value semantics — sao chép object thực ra chỉ sao chép địa chỉ.
  • Sử dụng thành thạo Object.keys/values/entries, spread {...obj}, Object.assign, Object.freeze.
  • Xây dựng mental model prototype chain và tra cứu property theo chain.
  • Khai báo class ES6 với constructor, method, static, getter/setter, private field #x.
  • Inheritance qua extends/super và biết khi nào nên thay bằng composition.

1. Object literal — cú pháp tạo object

Object literal là cách tạo object ngắn gọn nhất trong JS — chỉ cần cặp {} với danh sách key: value. Key luôn là string (hoặc Symbol); value có thể là bất kỳ giá trị nào, kể cả function (gọi là method).

Object cơ bản
const user = {
  name: 'Anh',
  age: 22,
  isAdmin: false,
  greet: function() {
    return `Xin chào ${this.name}`;
  }
};

console.log(user.name);     // 'Anh'
console.log(user.greet());  // 'Xin chào Anh'

1.1. Shorthand property — viết gọn khi key ≡ tên biến

Khi tên biến trùng với tên key, ES6+ cho phép bỏ phần lặp lại. { x } tương đương với { x: x }. Cực kỳ phổ biến khi return object từ function.

shorthand.js
const name = 'Anh';
const age = 22;

// Cũ — lặp
const u1 = { name: name, age: age };

// Mới — shorthand
const u2 = { name, age };

// Dùng trong return
function makeUser(name, age) {
  return { name, age, createdAt: Date.now() };
}

1.2. Computed key — key tính động lúc tạo

Dùng [expression] để biến biểu thức thành key. Trước ES6 phải tạo object rồi gán obj[key] = value riêng — giờ làm thẳng trong literal.

computed-key.js
const field = 'email';

const form = {
  [field]: 'anh@example.com',
  [`${field}_verified`]: true,
  [Symbol.for('id')]: 42
};

console.log(form.email);          // 'anh@example.com'
console.log(form.email_verified); // true

1.3. Method shorthand — bỏ function

Khi value là function, có thể viết gọn foo() {} thay vì foo: function() {}. Cú pháp này còn cho phép định nghĩa generator method (*gen() {}) và async method (async fetch() {}).

method-shorthand.js
// Cũ
const api1 = {
  fetch: function() { return fetch('/api'); }
};

// Mới — method shorthand
const api2 = {
  fetch() { return fetch('/api'); },
  async load() { return await fetch('/api'); },
  *ids() { yield 1; yield 2; }
};
💡 Lưu ý — method shorthand vs arrow function

foo() {} tạo function bình thường với this binding lúc gọi. Arrow function foo: () => {} không có this của riêng nó — sẽ lấy this của scope ngoài. Trong object literal, hầu như luôn muốn method shorthand, không phải arrow.

2. Truy cập property — dot, bracket, ?., ??

2.1. Dot notation vs bracket notation

Có hai cú pháp truy cập property: obj.key (dot) và obj['key'] (bracket). Chúng tương đương về kết quả, nhưng khác về khả năng biểu đạt:

Dot obj.keyBracket obj['key']
Key biết trước, tên hợp lệ✅ ngắn gọnOK nhưng dài
Key có dấu cách / dấu gạch❌ syntax errorobj['full name']
Key bắt đầu bằng sốobj['2024']
Key là biến / biểu thứcobj[varName]
Key là Symbolobj[sym]
access.js
const user = { name: 'Anh', 'full name': 'Quách Việt Anh' };

// Dot — key là identifier hợp lệ
user.name;                // 'Anh'

// Bracket — bắt buộc khi key có khoảng trắng
user['full name'];        // 'Quách Việt Anh'

// Bracket — key động (dynamic key)
const field = 'name';
user[field];              // 'Anh'

// Truy cập property không tồn tại → undefined (không phải lỗi)
user.age;                 // undefined

2.2. Optional chaining ?.

Truy cập property của null/undefined sẽ throw: TypeError: Cannot read properties of null. Toán tử ?. (ES2020) cho phép chain an toàn — nếu giá trị bên trái là null/undefined, biểu thức trả về undefined luôn, không throw.

optional-chaining.js
const user = { name: 'Anh', address: null };

// Không có ?. — crash
user.address.city;
// TypeError: Cannot read properties of null

// Có ?. — short-circuit, trả undefined
user.address?.city;       // undefined ✅

// Chain nhiều cấp
user.address?.geo?.lat;   // undefined ✅

// ?. trên method — chỉ gọi nếu tồn tại
user.greet?.();          // undefined (greet không tồn tại)

// ?. trên bracket access
user.address?.['city'];   // undefined

2.3. Nullish coalescing ??

Toán tử ?? trả về vế phải khi vế trái là null hoặc undefined — và chỉ hai giá trị đó. Khác với || (trả vế phải với mọi falsy: 0, '', false, NaN).

nullish.js
const config = { volume: 0, label: '' };

// || coi 0 là falsy → trả default sai
config.volume || 50;        // 50 (sai — 0 là hợp lệ)

// ?? chỉ trả default khi null/undefined
config.volume ?? 50;        // 0 ✅
config.label ?? '(no label)'; // '' ✅
config.missing ?? 'fallback'; // 'fallback'
💡 Kết hợp ?. + ??

Pattern phổ biến: user?.address?.city ?? 'Unknown'. ?. chặn crash khi chain qua null; ?? đặt giá trị mặc định nếu kết quả cuối là null/undefined. Đây là cú pháp hiện đại thay thế cho chuỗi if dài hoặc obj && obj.a && obj.a.b kiểu cũ.

3. Reference vs Value — bản chất gán object

Đây là điểm quan trọng nhất của chương. Primitive (number, string, boolean, null, undefined, symbol, bigint) được sao chép theo giá trị. Object (bao gồm array, function) được sao chép theo tham chiếu — tức là cả hai biến đều trỏ về cùng một vùng nhớ.

primitive-by-value.js
// Primitive — copy giá trị
let a = 5;
let b = a;      // b = 5 (copy độc lập)
b = 10;
console.log(a); // 5 — a không đổi
console.log(b); // 10
object-by-reference.js
// Object — copy tham chiếu (cùng địa chỉ)
const a = { x: 1 };
const b = a;          // b trỏ về cùng object với a

b.x = 99;             // Mutate qua b

console.log(a.x);    // 99 ← a thấy luôn (cùng object)
console.log(a === b); // true (cùng reference)
🧠 Mental model — biến chứa "địa chỉ", không chứa object

Hình dung object nằm ở heap, còn biến chỉ lưu một con trỏ tới object đó. Khi gán b = a, JS copy con trỏ — không copy object. Sửa b.x = sửa object gốc, nên a.x cũng đổi theo.

So sánh === giữa hai object trả true chỉ khi chúng cùng reference — không so sánh nội dung. {a:1} === {a:1}false dù trông giống nhau.

equality.js
{} === {};            // false — 2 object khác nhau
[] === [];            // false

const x = {};
const y = x;
x === y;              // true — cùng reference

// Object truyền vào function cũng là reference
function addAge(u) { u.age = 22; }
const me = { name: 'Anh' };
addAge(me);
console.log(me.age); // 22 — function mutate object gốc
🔥 Gotcha — sao chép object cần làm thủ công

Không có cách "copy mặc định". Phải explicit:

  • Shallow copy: {...obj} hoặc Object.assign({}, obj).
  • Deep copy: structuredClone(obj) (modern, ES2022), hoặc JSON.parse(JSON.stringify(obj)) (rẻ tiền — mất Date, function, undefined).

4. Các method tiện ích của Object

Object.keys/values/entries

Bộ ba này biến object thành array để có thể .map/.filter/.forEach như mảng thông thường.

keys-values-entries.js
const user = { name: 'Anh', age: 22, role: 'admin' };

Object.keys(user);
// ['name', 'age', 'role']

Object.values(user);
// ['Anh', 22, 'admin']

Object.entries(user);
// [['name','Anh'], ['age',22], ['role','admin']]

// Loop qua entries
for (const [k, v] of Object.entries(user)) {
  console.log(`${k}: ${v}`);
}

Object.assign và spread {...obj}

Cả hai dùng để merge object hoặc tạo shallow copy. Spread cú pháp gọn hơn và là cách hiện đại; Object.assign vẫn dùng được nhưng mutate object đầu tiên.

merge.js
const base = { a: 1, b: 2 };
const extra = { b: 99, c: 3 };

// Spread — tạo object mới
const merged1 = { ...base, ...extra };
// { a: 1, b: 99, c: 3 } — extra ghi đè b

// Object.assign — mutate target
const target = {};
Object.assign(target, base, extra);
// target === { a: 1, b: 99, c: 3 }

// Pattern phổ biến: override một field
const user = { name: 'Anh', age: 22 };
const updated = { ...user, age: 23 };
// { name: 'Anh', age: 23 } — immutable update
⚠️ Shallow — không deep!

Spread/Object.assign chỉ copy một lớp. Nested object vẫn share reference. Mutate nested object qua bản copy → bản gốc cũng đổi. Xem mục Object.freeze và bài tập 1.

Object.freezeObject.seal

Object.freeze(obj) biến object thành read-only — mọi mutate sẽ bị bỏ qua (strict mode thì throw). Object.seal(obj) mềm hơn: không thêm/xóa property nhưng vẫn cho phép sửa giá trị property hiện có.

freeze.js
const config = Object.freeze({
  apiUrl: 'https://api.example.com',
  timeout: 5000
});

config.apiUrl = 'hacked';     // Silent fail (hoặc throw ở strict mode)
console.log(config.apiUrl); // 'https://api.example.com' — không đổi

Object.isFrozen(config);    // true
🔥 Gotcha — freeze cũng shallow

Object.freeze({ nested: { x: 1 } }) chỉ freeze layer ngoài. config.nested.x = 99 vẫn chạy bình thường. Muốn deep freeze phải đệ quy hoặc dùng thư viện như immer.

Object.fromEntries

Đảo ngược của Object.entries — nhận array [[key, value], ...] và trả về object. Cực kỳ tiện khi cần biến đổi object qua map/filter trên entries rồi build lại.

fromEntries.js
const user = { name: 'Anh', age: 22, password: 'secret' };

// Loại bỏ password
const safe = Object.fromEntries(
  Object.entries(user).filter(([k]) => k !== 'password')
);
// { name: 'Anh', age: 22 }

// Map → uppercase keys
const upper = Object.fromEntries(
  Object.entries(user).map(([k, v]) => [k.toUpperCase(), v])
);
// { NAME: 'Anh', AGE: 22, PASSWORD: 'secret' }

// Convert Map → object
const m = new Map([['a', 1], ['b', 2]]);
Object.fromEntries(m); // { a: 1, b: 2 }

5. Prototype chain — mô hình kế thừa của JS

Mọi object trong JS đều có một liên kết ẩn ([[Prototype]], truy cập qua __proto__ hoặc Object.getPrototypeOf) trỏ tới một object cha. Khi truy cập property obj.x, JS:

  1. Tìm x ngay trên obj. Có → trả về.
  2. Không có → đi lên obj.__proto__, tìm tiếp.
  3. Không có → đi tiếp lên prototype của prototype.
  4. Hết chain (null) → trả undefined.

Đây gọi là prototype lookup chain — cốt lõi OOP của JS.

🧠 Mental model — Resolution Order

Hình dung như "lookup table có fallback". Mỗi object là một bảng riêng; nếu key không có, hỏi sếp (prototype), sếp không có hỏi sếp của sếp. Đi mãi tới null thì dừng.

CấpObjectProperty có
0adminname
1admin.__proto__ (User.prototype)greet
2User.prototype.__proto__ (Object.prototype)toString, hasOwnProperty
3Object.prototype.__proto__null — hết chain
prototype-lookup.js
const parent = { greet() { return 'hi'; } };
const child  = Object.create(parent);
child.name = 'Anh';

child.name;            // 'Anh' — tìm thấy ngay trên child
child.greet();         // 'hi'  — leo lên parent
child.toString();      // '[object Object]' — leo lên Object.prototype
child.foo;             // undefined — hết chain

Object.getPrototypeOf(child) === parent; // true

Object.create(proto) — tạo object với prototype cụ thể

Object.create(p) tạo object mới có __proto__ chính là p. Đây là cách "low-level" để dựng prototype chain — class ES6 ẩn việc này bên dưới.

object-create.js
const animal = {
  eat() { return `${this.name} đang ăn`; }
};

const dog = Object.create(animal);
dog.name = 'Lulu';
dog.bark = () => 'Woof!';

dog.eat();   // 'Lulu đang ăn' — kế thừa eat từ animal
dog.bark();  // 'Woof!'

// Tạo object KHÔNG có prototype — không kế thừa từ Object.prototype
const dict = Object.create(null);
dict.x = 1;
dict.toString;  // undefined! — không có Object.prototype.toString
💡 Object.create(null) — dictionary "thuần"

Object literal {} kế thừa Object.prototype nên luôntoString, hasOwnProperty... Nếu dùng object như dictionary (map string → value), các key này có thể đụng độ với data của user. Object.create(null) tạo object "trắng tinh", an toàn hơn để làm hash map. Hoặc dùng thẳng Map — chương 6.

6. class ES6 — cú pháp ngọt phủ prototype

ES2015 thêm keyword class giúp khai báo "lớp" trông quen thuộc với người từ Java/Python. Nhưng về bản chất, JS không có lớp thật — class chỉ là syntactic sugar phía trên function constructor + prototype.

class-basic.js
class User {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  greet() {
    return `Xin chào ${this.name}`;
  }
}

const u = new User('Anh', 22);
u.greet();                            // 'Xin chào Anh'

// Behind the scenes:
u.__proto__ === User.prototype;     // true
typeof User;                          // 'function' (!!)
User.prototype.greet;                // [Function: greet]
🧠 Mental model — class là function

Khi viết class User, JS tạo một function tên User (đóng vai trò constructor) và một object User.prototype chứa các method. Khi gọi new User():

  1. Tạo object mới rỗng {}.
  2. Set __proto__ của object đó = User.prototype.
  3. Gọi constructor với this = object mới.
  4. Trả về object đó.
🔀 So sánh — JS class vs Java/Dart class
JavaScriptJava/Dart
Bản chấtFunction + prototype objectLớp thật (class metadata trong runtime)
InheritancePrototype chain (runtime)Class hierarchy (compile-time check)
Tạo instancenew User()new User() hoặc User() (Dart)
FieldGán trong constructor (hoặc class field)Khai báo field tường minh, có type
ModifierChỉ static, #privatepublic/private/protected, final/sealed/base (Dart 3)
Multi-inheritanceKhông (mixin qua composition)Java: không. Dart: mixin

6.1. Class fields — khai báo property ngoài constructor

ES2022 cho phép khai báo field thẳng trong thân class, không cần qua this.x = ... trong constructor. Dùng cho giá trị mặc định hoặc field độc lập với constructor argument.

class-fields.js
class Counter {
  count = 0;                // class field — public, init khi new
  step  = 1;

  increment() {
    this.count += this.step;
  }
}

const c = new Counter();
c.increment();
c.count; // 1

7. static — method/property thuộc về class

Method/field gắn keyword static thuộc về class chứ không thuộc instance. Truy cập qua tên class trực tiếp. Phù hợp cho factory, utility liên quan đến class, hoặc counter chia sẻ giữa các instance.

static.js
class User {
  static count = 0;            // static field

  constructor(name) {
    this.name = name;
    User.count++;
  }

  static makeAdmin(name) {     // factory method
    const u = new User(name);
    u.isAdmin = true;
    return u;
  }
}

const a = new User('Anh');
const b = User.makeAdmin('Bình');

User.count;        // 2
a.count;            // undefined — static không nằm trên instance
a.makeAdmin;        // undefined
💡 Khi nào dùng static
  • Factory method: User.fromJSON(data), Date.now().
  • Utility liên quan: Math.max, Number.isInteger — không cần instance.
  • Counter / cache toàn class: chia sẻ state giữa instance.

Không dùng static khi method cần truy cập this instance — vì this trong static là chính class.

8. Getter / Setter — property có logic

get foo()set foo(v) tạo "property ảo" — code đọc/ghi như field thường, nhưng thực chất chạy hàm. Dùng cho computed property hoặc validation lúc gán.

getter-setter.js
class User {
  constructor(first, last) {
    this.first = first;
    this.last  = last;
  }

  get fullName() {
    return `${this.first} ${this.last}`;
  }

  set fullName(value) {
    if (typeof value !== 'string') {
      throw new TypeError('fullName phải là string');
    }
    [this.first, this.last] = value.split(' ');
  }
}

const u = new User('Anh', 'Quách');
u.fullName;                  // 'Anh Quách' — gọi getter
u.fullName = 'Bình Trần';     // gọi setter
u.first;                     // 'Bình'
🔥 Gotcha — đặt tên trùng field

get name()this.name = ... trong constructor sẽ infinite loop vì setter cũng gán this.name. Convention: dùng backing field tên khác (_name hoặc #name private).

9. Inheritance — extendssuper

class B extends A thiết lập B.prototype.__proto__ = A.prototype — instance của B sẽ có chain qua A trước khi tới Object.

extends.js
class User {
  constructor(name) {
    this.name = name;
  }
  greet() {
    return `Hi ${this.name}`;
  }
}

class Admin extends User {
  constructor(name, level) {
    super(name);            // PHẢI gọi trước khi dùng this
    this.level = level;
  }

  greet() {
    return super.greet() + ` (lv ${this.level})`;
  }
}

const a = new Admin('Anh', 3);
a.greet();              // 'Hi Anh (lv 3)'
a instanceof Admin; // true
a instanceof User;  // true (chain qua User)
⚠️ Quy tắc super()

Khi class con có constructor, bắt buộc gọi super(...) trước khi đụng tới this. Nếu không sẽ throw ReferenceError. Lý do: this của class con chưa được khởi tạo cho tới khi super chạy xong constructor của class cha.

Nếu class con không khai báo constructor, JS tự sinh constructor(...args) { super(...args); } ngầm — nên không cần lo trường hợp này.

9.1. Giới hạn của inheritance

Inheritance nhanh chóng trở thành cứng nhắc khi cây kế thừa sâu, vì:

  • Coupling chặt: thay đổi class cha = nguy cơ vỡ class con.
  • Diamond / multi-inheritance: JS không hỗ trợ đa kế thừa.
  • Không linh hoạt: object chỉ có thể "is-a" một thứ.

Đây là lý do cộng đồng JS từ lâu đề cao composition over inheritance — sẽ trình bày ở mục 12.

10. Private field #x (ES2022)

Trước ES2022, JS không có truly private — convention là dùng dấu gạch dưới _field để báo "đừng đụng vào", nhưng người ngoài vẫn truy cập được. Từ ES2022, prefix # tạo field private thực sự, không thể truy cập từ ngoài class.

private-field.js
class BankAccount {
  #balance = 0;          // private — chỉ truy cập trong class

  deposit(amount) {
    if (amount <= 0) throw new Error('amount > 0');
    this.#balance += amount;
  }

  get balance() {
    return this.#balance;
  }
}

const acc = new BankAccount();
acc.deposit(100);
acc.balance;       // 100 ✅
acc.#balance;      // SyntaxError — không thể truy cập từ ngoài
acc['#balance'];   // undefined — không phải string key thường
🔀 #x vs _x
_field#field
Cấp độConvention (chỉ là quy ước)Language-level (runtime enforce)
Truy cập từ ngoàiVẫn được (chỉ "đừng đụng")Cấm — syntax error
Lộ qua Object.keysKhông
Subclass truy cậpĐượcKhông (truly private to declaring class)
Phiên bảnMọi version JSES2022 trở lên

Lựa chọn modern: dùng # nếu môi trường hỗ trợ; legacy code vẫn dùng _.

11. Composition over Inheritance

Thay vì xếp object vào cây "is-a" cứng, ta build object bằng cách ghép các đoạn hành vi nhỏ ("has-a" / "can-do"). Mỗi đoạn là một object hoặc function, có thể tái sử dụng độc lập, dễ test, dễ tháo lắp.

composition.js
// Các capability riêng lẻ — đều là object trả ra method
const canEat = (state) => ({
  eat(food) { state.energy += 10; return `${state.name} ăn ${food}`; }
});

const canWalk = (state) => ({
  walk() { state.energy -= 5; return `${state.name} đi bộ`; }
});

const canSwim = (state) => ({
  swim() { state.energy -= 15; return `${state.name} bơi`; }
});

// Ghép tùy ý — không cần class hierarchy
function createDuck(name) {
  const state = { name, energy: 100 };
  return { ...state, ...canEat(state), ...canWalk(state), ...canSwim(state) };
}

const d = createDuck('Donald');
d.eat('cá');   // 'Donald ăn cá'
d.swim();      // 'Donald bơi'
🔀 Composition vs Inheritance
Tiêu chíInheritance (extends)Composition (has-a)
Quan hệis-a (cứng)has-a / can-do (mềm)
Tái sử dụngTheo cây — cần đặt đúng vị tríTheo nhu cầu — ghép tự do
CouplingChặt (đổi cha = vỡ con)Lỏng — mỗi piece độc lập
Đa khả năngKhó (single inheritance)Dễ — mix bao nhiêu cũng được
Khi nên dùngCây phân loại rõ ràng, ổn địnhCapability cắt rời, thay đổi nhiều

Quy tắc cộng đồng (Gang of Four): "Favor composition over inheritance." Khi nghi ngờ — chọn composition. Dùng inheritance khi và chỉ khi quan hệ "is-a" thực sự đúng cả về khái niệm lẫn dài hạn.

Bài tập

Bài 1 — deepClone: clone đệ quy

Viết deepClone(obj) clone sâu object — nested object/array đều phải là instance mới, mutate bản clone không ảnh hưởng bản gốc. Test:

  • { a: 1, b: { c: 2 } } — sửa clone.b.c không ảnh hưởng gốc.
  • [1, [2, [3]]] — clone array lồng.
  • Bonus: handle Date, không bị stack overflow với circular reference.
Đáp án + giải thích
function deepClone(obj, seen = new WeakMap()) {
  // Primitive hoặc null → trả thẳng
  if (obj === null || typeof obj !== 'object') return obj;

  // Circular — đã thấy → trả copy đã tạo
  if (seen.has(obj)) return seen.get(obj);

  // Date
  if (obj instanceof Date) return new Date(obj);

  // Array
  if (Array.isArray(obj)) {
    const copy = [];
    seen.set(obj, copy);
    obj.forEach(item => copy.push(deepClone(item, seen)));
    return copy;
  }

  // Object thường
  const copy = {};
  seen.set(obj, copy);
  for (const [k, v] of Object.entries(obj)) {
    copy[k] = deepClone(v, seen);
  }
  return copy;
}

// Hoặc dùng built-in (ES2022)
const clone = structuredClone(obj);

WeakMap dùng để track object đã thấy → tránh đệ quy vô hạn với circular reference. Ở production, ưu tiên structuredClone (built-in, handle Date/Map/Set/circular).

Bài 2 — BankAccount với private field

Viết class BankAccount với:

  • #balance private, mặc định 0.
  • deposit(amount) — cộng vào balance. Throw nếu amount <= 0.
  • withdraw(amount) — trừ. Throw nếu thiếu tiền hoặc amount <= 0.
  • get balance() — đọc số dư.

Kiểm chứng: từ ngoài không truy cập được #balance trực tiếp.

Đáp án
class BankAccount {
  #balance = 0;

  deposit(amount) {
    if (typeof amount !== 'number' || amount <= 0) {
      throw new Error('amount phải là số dương');
    }
    this.#balance += amount;
  }

  withdraw(amount) {
    if (typeof amount !== 'number' || amount <= 0) {
      throw new Error('amount phải là số dương');
    }
    if (amount > this.#balance) {
      throw new Error('Số dư không đủ');
    }
    this.#balance -= amount;
  }

  get balance() {
    return this.#balance;
  }
}

const acc = new BankAccount();
acc.deposit(100);
acc.withdraw(30);
acc.balance;        // 70

acc.#balance;       // SyntaxError ✅ truly private
acc.withdraw(1000);  // throws 'Số dư không đủ'

Bài 3 — Shape hierarchy → refactor sang composition

Bước 1 (inheritance): tạo class Shape với method area() trả 0. Class CircleSquare extends Shape, override area().

Bước 2 (composition): refactor lại — không dùng extends, mỗi shape là object có method area().

Hai cách giải

Cách inheritance:

class Shape {
  area() { return 0; }
}

class Circle extends Shape {
  constructor(r) { super(); this.r = r; }
  area() { return Math.PI * this.r ** 2; }
}

class Square extends Shape {
  constructor(side) { super(); this.side = side; }
  area() { return this.side ** 2; }
}

Cách composition:

const circle = (r) => ({
  kind: 'circle', r,
  area() { return Math.PI * this.r ** 2; }
});

const square = (side) => ({
  kind: 'square', side,
  area() { return this.side ** 2; }
});

const shapes = [circle(5), square(3)];
shapes.map(s => s.area()); // [78.54, 9]

Composition dễ test/serialize hơn, không phụ thuộc this binding qua chain dài. Inheritance vẫn ổn nếu bạn chắc có một "Shape root" chung lâu dài.

Bài 4 — Tự viết pickomit như lodash

Viết hai hàm thuần (không mutate input):

  • pick(obj, keys) — trả object mới chỉ chứa keys.
  • omit(obj, keys) — trả object mới không chứa keys.

Test: pick({a:1, b:2, c:3}, ['a','c']){a:1, c:3}.

Đáp án
function pick(obj, keys) {
  return Object.fromEntries(
    Object.entries(obj).filter(([k]) => keys.includes(k))
  );
}

function omit(obj, keys) {
  return Object.fromEntries(
    Object.entries(obj).filter(([k]) => !keys.includes(k))
  );
}

pick({a:1, b:2, c:3}, ['a', 'c']); // {a:1, c:3}
omit({a:1, b:2, c:3}, ['b']);      // {a:1, c:3}

Phiên bản tối ưu hơn dùng Set cho keys (lookup O(1) thay vì O(n) qua includes) — quan trọng khi keys dài.

Bài 5 — Mixin Serializable

Viết một mixin object Serializable có hai method:

  • toJSON() — trả về JSON string của object.
  • fromJSON(str) (static-like) — parse JSON và assign back vào instance.

Apply mixin vào class User. Mục tiêu: composition thay vì kế thừa.

Đáp án
// Mixin — object chứa method, ghép qua Object.assign
const Serializable = {
  toJSON() {
    return JSON.stringify(this);
  },
  fromJSON(str) {
    Object.assign(this, JSON.parse(str));
    return this;
  }
};

class User {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
}

// Apply mixin lên prototype — tất cả User instance có method
Object.assign(User.prototype, Serializable);

const u = new User('Anh', 22);
const s = u.toJSON();    // '{"name":"Anh","age":22}'

const u2 = new User();
u2.fromJSON(s);
u2.name; // 'Anh'

Mixin = composition cấp prototype. Không tạo cây kế thừa, không khóa class vào một quan hệ "is-a" cụ thể — chỉ "có thêm khả năng".

Quiz

Q1

{}Object.create(null) khác nhau ra sao?

Xem đáp án
✓ Đáp án

{} kế thừa từ Object.prototype nên có sẵn toString, hasOwnProperty, valueOf... Object.create(null) tạo object với __proto__ === null — không kế thừa gì, không có các method trên. Phù hợp làm dictionary "thuần" tránh đụng tên với data của user.

Q2

JS class và Java class khác nhau về bản chất thế nào?

Xem đáp án
✓ Đáp án

JS classsyntactic sugar trên function constructor + prototype object — runtime vẫn là prototype chain. typeof class Foo {} trả 'function'. Java class là một đơn vị type thực sự, có metadata, có compile-time check, không phải hàm.

Q3

#x_x khác gì?

Xem đáp án
✓ Đáp án

#xtruly private: runtime enforce — code ngoài class truy cập sẽ throw SyntaxError. _x chỉ là convention: dấu gạch dưới mang ý nghĩa "đừng đụng vào" nhưng code ngoài vẫn truy cập được bình thường.

Q4

Mối quan hệ giữa instance.__proto__Constructor.prototype?

Xem đáp án
✓ Đáp án

instance.__proto__ === Constructor.prototype. Khi new Constructor() chạy, JS gán __proto__ của instance mới bằng Constructor.prototype. Tất cả method khai báo trong class đều nằm trên prototype này, được chia sẻ giữa các instance.

Q5

Có gọi được static method qua instance không?

Xem đáp án
✓ Đáp án

Không trực tiếp. static gắn vào class chứ không vào prototype — instance không thấy. Phải gọi qua tên class: User.makeAdmin(). Nếu thật sự cần qua instance, có thể dùng this.constructor.makeAdmin() bên trong method (lấy class qua constructor reference).

Q6

class B extends A, nếu B có constructor riêng — có bắt buộc gọi super() không? Trước hay sau khi dùng this?

Xem đáp án
✓ Đáp án

Bắt buộc gọi super(...), và phải gọi trước mọi thao tác với this. Lý do: this chưa được khởi tạo cho tới khi constructor của lớp cha chạy xong. Nếu B không khai báo constructor, JS tự sinh constructor(...args) { super(...args); } ngầm.

Q7

Object.freeze có deep không?

Xem đáp án
✓ Đáp án

Không — shallow. Chỉ freeze layer ngoài cùng. Object lồng bên trong vẫn mutate được. Muốn deep: đệ quy gọi freeze trên từng property, hoặc dùng thư viện như immer / immutable.js.

Q8

Composition vs Inheritance — cái nào "tốt hơn"?

Xem đáp án
✓ Đáp án

Không có "tốt hơn" tuyệt đối — nhưng composition linh hoạt hơn trong đa số trường hợp thực tế. Quy tắc cộng đồng: "Favor composition over inheritance." Inheritance phù hợp khi quan hệ "is-a" rõ ràng và ổn định (vd Admin is-a User). Khi nghi ngờ hoặc khi cần ghép nhiều khả năng (mixin, capability) — chọn composition.

Tổng kết

Sau chương 4, bạn đã master:

  • Object literal modern: shorthand property, computed key, method shorthand.
  • Truy cập property: dot vs bracket, optional chaining ?., nullish coalescing ??.
  • Reference semantics: object copy theo địa chỉ, primitive copy theo giá trị.
  • Object utilities: keys/values/entries, spread, assign, freeze, fromEntries.
  • Prototype chain: tra cứu property leo dần lên cha, kết thúc ở null.
  • Class ES6+: constructor, method, static, getter/setter, private #x.
  • Inheritance: extends + super, quy tắc super() trước this.
  • Composition: pattern ghép capability, ưu tiên hơn inheritance đa số trường hợp.

Kết nối

  • Chương 5 (Arrays & Iteration) — array cũng là object đặc biệt; method chain (map/filter/reduce) dựa trên cùng cơ chế prototype.
  • Chương 6 (Map, Set, WeakMap) — khi nào dùng Map thay vì object thường; WeakMap đã xuất hiện trong bài tập deepClone.
  • Chương 11 (TypeScript Class & Interface) — TS bổ sung type modifier (public/private/protected/readonly), abstract class, interface và type-level OOP.
  • Dart Chương 4 (Class Modifiers) — Dart 3 có hệ thống sealed/base/final/interface class rất giàu, đối lập với mô hình prototype tự do của JS. Đáng đối chiếu để hiểu trade-off: JS — linh hoạt, runtime; Dart — chặt, compile-time.