- 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/supervà 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).
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.
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.
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() {}).
// 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; }
};
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.key | Bracket obj['key'] |
|---|---|---|
| Key biết trước, tên hợp lệ | ✅ ngắn gọn | OK nhưng dài |
| Key có dấu cách / dấu gạch | ❌ syntax error | ✅ obj['full name'] |
| Key bắt đầu bằng số | ❌ | ✅ obj['2024'] |
| Key là biến / biểu thức | ❌ | ✅ obj[varName] |
| Key là Symbol | ❌ | ✅ obj[sym] |
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.
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).
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'
?. + ??
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 — 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 — 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)
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} là false dù trông giống nhau.
{} === {}; // 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
Không có cách "copy mặc định". Phải explicit:
- Shallow copy:
{...obj}hoặcObject.assign({}, obj). - Deep copy:
structuredClone(obj)(modern, ES2022), hoặcJSON.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.
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.
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
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.freeze và Object.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ó.
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
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.
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:
- Tìm
xngay trênobj. Có → trả về. - Không có → đi lên
obj.__proto__, tìm tiếp. - Không có → đi tiếp lên prototype của prototype.
- Hết chain (
null) → trảundefined.
Đây gọi là prototype lookup chain — cốt lõi OOP của JS.
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ấp | Object | Property có |
|---|---|---|
| 0 | admin | name |
| 1 | admin.__proto__ (User.prototype) | greet |
| 2 | User.prototype.__proto__ (Object.prototype) | toString, hasOwnProperty |
| 3 | Object.prototype.__proto__ | null — hết chain |
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.
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ôn có
toString, 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 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]
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():
- Tạo object mới rỗng
{}. - Set
__proto__của object đó =User.prototype. - Gọi constructor với
this= object mới. - Trả về object đó.
| — | JavaScript | Java/Dart |
|---|---|---|
| Bản chất | Function + prototype object | Lớp thật (class metadata trong runtime) |
| Inheritance | Prototype chain (runtime) | Class hierarchy (compile-time check) |
| Tạo instance | new User() | new User() hoặc User() (Dart) |
| Field | Gán trong constructor (hoặc class field) | Khai báo field tường minh, có type |
| Modifier | Chỉ static, #private | public/private/protected, final/sealed/base (Dart 3) |
| Multi-inheritance | Khô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 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.
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
- 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() và 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.
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'
get name() và 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 — extends và super
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.
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)
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.
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ài | Vẫn được (chỉ "đừng đụng") | Cấm — syntax error |
Lộ qua Object.keys | Có | Không |
| Subclass truy cập | Được | Không (truly private to declaring class) |
| Phiên bản | Mọi version JS | ES2022 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.
// 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'
| Tiêu chí | Inheritance (extends) | Composition (has-a) |
|---|---|---|
| Quan hệ | is-a (cứng) | has-a / can-do (mềm) |
| Tái sử dụng | Theo cây — cần đặt đúng vị trí | Theo nhu cầu — ghép tự do |
| Coupling | Chặt (đổi cha = vỡ con) | Lỏng — mỗi piece độc lập |
| Đa khả năng | Khó (single inheritance) | Dễ — mix bao nhiêu cũng được |
| Khi nên dùng | Cây phân loại rõ ràng, ổn định | Capability 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ửaclone.b.ckhô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:
#balanceprivate, mặc định 0.deposit(amount)— cộng vào balance. Throw nếuamount <= 0.withdraw(amount)— trừ. Throw nếu thiếu tiền hoặcamount <= 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
Circle và Square 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 pick và omit như lodash
Viết hai hàm thuần (không mutate input):
pick(obj, keys)— trả object mới chỉ chứakeys.omit(obj, keys)— trả object mới không chứakeys.
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
{} và Object.create(null) khác nhau ra sao?
Xem đá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.
JS class và Java class khác nhau về bản chất thế nào?
Xem đáp án
JS class là syntactic 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.
#x và _x khác gì?
Xem đáp án
#x là truly 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.
Mối quan hệ giữa instance.__proto__ và Constructor.prototype?
Xem đá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.
Có gọi được static method qua instance không?
Xem đá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).
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
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.
Object.freeze có deep không?
Xem đá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.
Composition vs Inheritance — cái nào "tốt hơn"?
Xem đá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ắcsuper()trướcthis. - 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
Mapthay 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,interfacevà type-level OOP. - Dart Chương 4 (Class Modifiers) — Dart 3 có hệ thống
sealed/base/final/interface classrấ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.