DEV Community

Prototype — Prototype Chain

Prototype chain: kế thừa thật sự của JavaScript hoạt động ra sao

JavaScript không có class theo nghĩa của Java hay C++ ở tầng cơ chế — class chỉ là cú pháp đẹp đặt lên trên prototype. Mỗi object có một liên kết ẩn tới một object khác gọi là prototype của nó; khi truy cập một property không có trên object, engine leo theo chuỗi prototype này tới khi tìm thấy hoặc chạm null. Hiểu prototype chain là điều kiện để debug method bị kế thừa sai, để biết vì sao sửa một prototype lại ảnh hưởng tới mọi object đang sống, và để không bao giờ động vào native prototype.

Cơ chế hoạt động

Liên kết ẩn đó là [[Prototype]], đọc/ghi qua Object.getPrototypeOf/Object.setPrototypeOf (hoặc __proto__ cũ). Tách bạch hai thứ hay bị nhầm: __proto__ là liên kết prototype của một instance, còn prototype là property trên hàm constructor, là object sẽ trở thành [[Prototype]] của mọi instance tạo bằng new.

function User(name) {
  this.name = name // own property, riêng mỗi instance
}
User.prototype.greet = function () { // dùng chung mọi instance
  return `hi ${this.name}`
}

const u = new User('an')
u.greet() // không có greet trên u -> leo lên User.prototype -> tìm thấy
Enter fullscreen mode Exit fullscreen mode

name nằm trực tiếp trên u (own property). greet không có trên u, engine leo lên User.prototype và tìm thấy ở đó. Một bản greet duy nhất phục vụ mọi instance — đây là lý do đặt method lên prototype tiết kiệm bộ nhớ hơn gán method trong constructor (mỗi instance một bản).

Chuỗi tra cứu dừng ở Object.prototype rồi null. class ... extends chỉ nối thêm một mắt xích vào chuỗi này, không tạo cơ chế mới.

Vấn đề gặp trong production

Failure mode 1: sửa shared prototype, ảnh hưởng mọi instance. Vì instance chia sẻ một object prototype, sửa prototype lúc runtime đổi hành vi của tất cả object đang sống, kể cả đã tạo trước đó:

User.prototype.role = 'guest'
const a = new User('a')
const b = new User('b')
a.role = 'admin' // tạo own property trên a, che prototype
b.role // vẫn 'guest' — đọc từ prototype
Enter fullscreen mode Exit fullscreen mode

Nhầm lẫn giữa "ghi tạo own property" và "đọc leo prototype" sinh ra bug khó chịu: gán giá trị tưởng dùng chung hóa ra chỉ che cục bộ, hoặc ngược lại sửa prototype tưởng cục bộ lại lan ra toàn bộ. Đặc biệt nguy với property kiểu reference đặt thẳng trên prototype:

function Cart() {}
Cart.prototype.items = [] // BUG: mọi cart chia sẻ chung một mảng
const c1 = new Cart(); const c2 = new Cart()
c1.items.push('x')
c2.items // ['x'] — c2 thấy item của c1
Enter fullscreen mode Exit fullscreen mode

items nằm trên prototype dùng chung, push không tạo own property nên mọi giỏ hàng chung một mảng. State kiểu này phải khởi tạo trong constructor (this.items = []) để mỗi instance có bản riêng. Bug "dữ liệu của user này lẫn sang user khác" trong production thường truy về đúng kiểu chia sẻ reference trên prototype.

Failure mode 2: sửa native prototype (prototype pollution). Thêm/đổi method trên Array.prototype, Object.prototype... ảnh hưởng mọi code trong process, gồm cả thư viện:

Array.prototype.last = function () { return this[this.length - 1] } // đừng
for (const k in someArray) { /* giờ duyệt cả 'last' */ }
Enter fullscreen mode Exit fullscreen mode

Method tự thêm xuất hiện trong for...in, phá vòng lặp của thư viện khác, và xung đột khi hai đoạn code cùng định nghĩa. Nguy hiểm hơn, Object.prototype pollution là một lớp lỗ hổng bảo mật: dữ liệu ngoài (JSON từ client) merge ẩu vào object có thể ghi __proto__ và tiêm property vào mọi object.

Cách debug và monitor

Khi một method "biến mất" hoặc trả về kết quả lạ, dùng Object.getPrototypeOf(obj) để in chuỗi prototype và xác định method thực sự đến từ mắt xích nào. obj.hasOwnProperty('x') phân biệt own property với property kế thừa — công cụ chính để gỡ bug "tưởng riêng hóa ra chung". Với bug dữ liệu lẫn giữa các instance, kiểm tra ngay xem property reference (mảng/object) có bị đặt trên prototype thay vì khởi tạo trong constructor không. Để chặn prototype pollution từ input ngoài, dùng Object.create(null) cho map dữ liệu thuần và Object.freeze(Object.prototype) trong môi trường nhạy cảm.

Tradeoff

Đặt method lên prototype tiết kiệm bộ nhớ: một bản dùng chung cho mọi instance, thay vì mỗi instance một bản như khi gán trong constructor — đáng kể khi tạo hàng nghìn object. Cái giá là sự chia sẻ đó: mọi thứ trên prototype là chung, nên state có thể thay đổi (mảng, object) tuyệt đối không được để trên prototype, chỉ method (vốn không giữ state) mới hợp. Quy tắc: method → prototype (dùng class cho gọn); dữ liệu instance → khởi tạo trong constructor; không bao giờ động vào native prototype.

Câu hỏi phỏng vấn

Prototype chain hoạt động ra sao, và vì sao đặt một mảng trên prototype lại gây bug dữ liệu lẫn giữa các instance?

Mỗi object có liên kết ẩn [[Prototype]] tới một object khác; truy cập property không có trên object thì engine leo theo chuỗi prototype tới khi tìm thấy hoặc chạm null. Instance tạo bằng new[[Prototype]] trỏ tới Constructor.prototype, nên mọi instance chia sẻ các property trên prototype. Đặt một mảng lên prototype nghĩa là mọi instance dùng chung đúng một mảng đó; push lên nó không tạo own property nên thay đổi thấy được ở mọi instance — gây lẫn dữ liệu. Cách đúng là khởi tạo state có thể thay đổi trong constructor (this.items = []) để mỗi instance có bản riêng, chỉ để method (stateless) trên prototype. Điểm cộng: nhắc native prototype không được sửa, và Object.prototype pollution là lỗ hổng bảo mật.

Hands-on

Dựng một class Cart thật với danh sách item, cố tình khởi tạo items trên prototype rồi tạo hai giỏ hàng và thêm sản phẩm vào một giỏ; xác nhận giỏ kia cũng thấy sản phẩm đó. Sửa bằng cách chuyển khởi tạo vào constructor và kiểm tra hai giỏ độc lập. Sau đó viết một hàm merge(target, source) ngây thơ rồi nạp một payload chứa khóa __proto__ để tái tạo prototype pollution (kiểm tra một object trống bỗng có property bị tiêm), và vá lại bằng cách bỏ qua khóa __proto__/constructor/prototype hoặc dùng Object.create(null) cho đích merge.

Top comments (0)