OverviewCâu hỏi kinh điển khi phỏng vấn web developer:
Thực sự mà nói thì đây không phải là câu hỏi dễ, tất nhiên đáp án là "Đúng" nhưng mọi người vẫn hay có sự nhầm lẫn giữa OOP trong JS với các ngôn ngữ OOP Class-based như C++, Java, PHP,.. Đặc biệt với sự xuất hiện của các bản specification ES 6, tool Babel, rồi Complier Typescript, các từ khóa class, constructor đều đã có trong JS nhưng có phải nó giống như những gì bạn nghĩ không? Tìm hiểu sâu hơn 1 chút càng thấy mọi thứ mông lung như 1 trò đùa =)) Vậy nên bài này mình sẽ đi sâu vào cách JS vận hành hệ thống đối tượng của mình để mọi người có hiểu rõ hơn, OOP trong JS là gì ? Object & PrototypeTạm thời nếu đã lỡ học ES6, Typescript thì hãy xóa ký ức đi, chúng ta sẽ đi vào những gì thuộc về bản chất và sơ khai của JS ObjectJavaScript được coi là 1 ngôn ngữ hướng đối tượng dạng prototype-base phân biệt với class-base như các ngôn ngữ OOP thông dụng. JS có những kiểu primitives( hay còn gọi là built-in type như: number, string , boolean..) nhưng thậm chí cả những kiểu này cũng có thể chuyển sang dạng Object. Nói cách khác Object là cốt lõi của JS, mọi thứ đều có thể trở thành Object. Mỗi Object là 1 tập hợp các thuộc tính (property) và trong số đó có duy nhất 1 thuộc tính được gọi là [[Prototype]]. Thuộc tính [[Prototype]] có thể có giá trị là tham chiếu đến 1 Object hoặc null.
1 ví dụ đơn giản như sau:
Tuy nhiên khi gõ foo lên trình duyệt, bạn không thể tìm thấy thuộc tính [[Prototype]] ở đâu thay vào đó lại là thuộc tính có tên __proto__ khá lạ lẫm. Thuộc tính __proto__ thực ra là 1 getter function, getter function này được định nghĩa trên native built-in Object của JavaScript (cũng chính là tổ tiên của tất cả đối tượng trong gia phả nhà JS - hãy coi nó như 1 đối tượng root). Vì vậy, đây có thể coi như 1 cách trình duyệt public ra để bạn truy cập vào [[Prototype]] của Object. Diagram bên dưới cho thấy sự liên hệ giữa __proto__ và [[Prototype]] A Prototype ChainĐối tượng mà [[Prototype]] tham chiếu tới cũng chỉ là những Object bình thường và bản thân [[Prototype]] cũng có những thuộc tính của riêng mình. Nếu 1 [[Prototype]] tham chiếu đến 1 [[Prototype]] khác ta gọi đó là 1 chuỗi [[Prototype]] hay [[Prototype]] chain. Theo định nghĩa của Mozilla thì [[Prototype]] chain là 1 chuỗi hữu hạn các Objects - các Object này được sử dụng để triển khai "kế thừa" và "chia sẻ thuộc tính".
Cách triển khai "Kế Thừa" trong JS cũng khác biệt với các ngôn ngữ OOP class-base truyền thống, "Kế Thừa" trong JS được gọi là Delegation Based Inheritance hay gần gũi hơn Prototype Base Inheritance Ta cùng xem ví dụ sau:
Nguyên tắc kế thừa rất đơn giản, nêu 1 thuộc tính/ phương thức được gọi không có trong bản thân Object, JS sẽ tìm thuộc tính/ phương thức đó trong chuỗi [[Prototype]]. Có thể thấy sự tương quan khi các ngôn ngữ Class-based dò tìm từ dưới lên trên trong Class Chain. Nếu 1 thuộc tính/ phương thức không được tìm thấy trong chuỗi [[Prototype]], thì giá trị undefined sẽ được trả về. Khi tạo ra 1 Object nếu [[Prototype]] không được chỉ định cụ thể tham chiếu tới Object nào thì mặc định nó sẽ tham chiếu tới Object.prototype . Tại sao lại là Object.prototype mà không phải là Object.[[Prototype]], tôi sẽ giải thích kỹ hơn ở phần sau nhưng tạm thời bạn hãy nhớ Object.[[Prototype]] và Object.prototype là 2 khái niệm khác nhau. Object.prototype cũng có [[Prototype]] của mình và đó cũng chính là là kết thúc của mọi [[Prototype]] chain với giá trị là null.
ConstructorThông thường sử dụng OOP giúp ta tạo ra các đối tượng giống nhau về mặt cấu trúc properties, nhưng lại có sự khác biệt về mặt trạng thái state. Để đạt được điều này trong JS ta cần đến Constructor Function Constructor function được sử dụng để tạo đối tượng mới. Sau khi khai báo 1 function ta có 1 đối tượng prototype lưu trữ trong ConstructorFunction.prototype. Và khi khởi tạo đối tượng bằng từ khóa new thì constructor function sẽ tự động thiết lập [[Prototype]] cho đối tượng vừa được khởi tạo. Và tất nhiên đó chính là đối tượng được lưu trữ trong ConstructorFunction.prototype. Ví dụ như sau:
Dưới đây là 1 biểu đồ cho thấy tất cả mọi đối tượng trong JS đều có [[Prototype]]. Constructor Function cũng có [[Prototype]] (tham chiếu tới Function.prototype và theo [[Prototype]] chain nó lại tham chiếu tới Object.prototype).
How to exactly inherit in JSSự thực là ngày nay ít người còn sử dụng cách này bởi có quá nhiều lớp interface bên trên Vanilla JS - JS thuần (như ES 6, Babel, Typescript ...) (nguyên gốc bản dịch sugar on the top - ngụ ý thêm đường vào cho đỡ chua), nhưng chính xác đây là cách JS thực sự làm việc ở tầng bên dưới. Chúng ta sẽ xem qua 1 ví dụ và sau đó sẽ phân tích nó.
Những thuộc tính có khả năng nắm giữ state của đối tượng ta cần định nghĩa nó trên bản thân constructor, điều này đảm bảo tính đóng gói cho mỗi đối tượng. Mặt khác, các phương thức được định nghĩa trên FunctionConstructor.prototype đảm bảo tất cả các instance được tạo ra đều tham chiếu đến 1 phương thức duy nhất. Điểm này mang lại hai ích lợi :
Quay trở lại ví dụ của chúng ta, Teacher lúc này có đầy đủ thuộc tính theo như cấu trúc nhưng phương thức thì chưa. Những gì bạn trông chờ tới giờ là Teacher.prototype phải tham chiếu đến Person.prototype. Hãy sử dụng hàm Object.create(), hàm này tạo ra 1 đối tượng dựa trên đối số truyền vào.
Tuy nhiên lúc này Teacher.prototype.constructor trở thành Person, đó là do ta đã tham chiếu prototype của Teacher tới 1 đối tượng khởi tạo bằng Person. Hãy sửa lại bằng cách:
Bây giờ thử nghiệm 1 chút
ConclusionQua bài này, tôi mong các bạn nắm rõ hơn về bản chất của JS, những gì thực sự đang chạy bên dưới đoạn mã Typescript hay ES6 của bạn. Tất nhiên đã là năm 2017, nên chắc sẽ không ai còn code kiểu này, nhưng hiểu thì vẫn tốt hơn là cứ coi nó như 1 "phép màu" phải không =)) Bài viết có tham khảo từ: You Dont Know Javascript: this & Object Prototypes - tác giả Getify - Chapter 5: Prototypes ECMA-262-3 in detail. Chapter 7.1 - Dmitry Soshinikov https://developer.mozilla.org/en-US/docs/Learn/JavaScript/ và 1 vài kiến thức cóp nhặt từ StackOverFlow |