Kế thừa Prototype ngang và dọc

https://js.edu.vn/7-ke-thua-prototype-ngang-va-doc.html

Prototype trong JS được coi là khái niệm trọng tâm. Cái thằng JS nó khác người nên nếp áp dụng tư duy của các ngôn ngữ hướng đối tượng class-based như PHP hay Java thì hỏng ngay.

Tiêu đề Prototype ngang và dọc do mình lựa chọn để mô hình hóa cách JS xử lý các hình thức kế thừa. Nếu làm ăn với ES6 thì JS có class, nhưng vấn đề là thằng JS này để lại quá nhiều “di sản” hổ lốn.

Cảm nhận của mình là cách mà JS thiết lập cơ chế kế thừa thực sự mạnh mẽ và linh hoạt, cho dù khó nắm bắt. Bài hơi dài, 1700 chữ.

Kế thừa dọc

Kế thừa dọc hiểu là kế thừa theo cấp độ cao thấp từ trên xuống dưới. Hầu như tất cả mọi object trong JavaScript đều là một instance của Object. Object đứng đầu chuỗi prototype chain. Đầu tiên, ta thử kiểm tra thứ tự prototype theo chiều dọc:

Chú ý: ở cách 2 trên, thực ra nên viết là Object.getPrototypeOf(Object.getPrototypeOf(Object.getPrototypeOf(y))) nếu như truy xuất 3 cấp prototype của y.

Trên đây là cách kiểm tra prototype cấp trên của mỗi object. Trong thực tế, chúng ta sẽ khai báo như sau. Chú ý đoạn Vuong.prototype.b = 10000;

Đây là một ví dụ khác:

Chú ý, nó kế thừa TỪ THẤP LÊN CAO NHƯNG RETURN NGAY KHI GIÁ TRỊ LÀ KHÔNG NULL. Do đó, các method hoặc property ở cấp cao hơn không được dùng gọi là “Property Shadowing” Vì method trong object cũng là property, nên nó cũng được kế thừa theo kiểu này, gọi là “Method Overriding”.

Cùng xem hình minh họa sau về kế thừa prototype dọc trong Js.

Chú ý 1: Nếu object tạo bằng let x = new Function; (chú ý Function có “F” viết hoa), x vẫn có thể là một constructor.

Chú ý 2: khi khao báo object dạng let x = {} hay let y = new Object() thì cả hai object dạng này không phải là Constructor, và do đó chúng không thể sinh ra object con được như khi khao báo Function hay Class. Xem minh họa:

Như vậy, khi khai báo let x = {} hoặc let y = new Object thì constructor, tức prototype cha trong chuỗi kế thừa prototype dọc, theo thứ bậc của x chính là Object.prototype luôn. Trong trường hợp này, object nào muốn kế thừa các object này thì sẽ kế thừa ngang thông qua Object.create() hoặc __proto__ sẽ bàn ở dưới.

Các trường hợp kế thừa dọc

Ta sẽ thử với prototype chain của object tạo bằng {}, array hay hàm.

với Constructor

Với Object.create (Chú ý, đây được xem là kế thừa ngang giữa 2 object trong bài viết này, nó cũng có thể coi là dọc thì giữa các object cũng phân cấp).

Với class. Sẽ bàn thêm về kế thừa class sau.

Trong Js, bản thân 1 hàm (1 function) cũng được coi là 1 object, và function có một thuộc tính (property) gọi là thuộc tính prototype, bản thân thuộc tính prototype này mang giá trị là 1 object. Object này chứa các property, method mà ta định nghĩa cho prototye của function đó.

Khi sử dụng function đó như là 1 constructor cho object, thì prototype của object đó sẽ là 1 property có tên là proto, proto là 1 con trỏ, trỏ đến object prototye của function constructor.

Kế thừa ngang giữa 2 function

Trường hợp 1: Thuộc tính giữa 2 function

Dùng call()

Trường hợp 2: Phương thức giữa 2 function.

Cách 1:

F2.prototype = Object.create(F1.prototype);

Cách 2:

F2.prototype = new F1;

Quan sát ví dụ:

Ví dụ về kế thừa prototype ngang theo function constructor. Liên quan đến hàm call(), sẽ có bài chi tiết sau.

Kế thừa ngang giữa 2 object

Có 2 cách: __proto__ và Object.create. Quan sát ví dụ:

Tình cờ tìm được minh họa sau từ trang mollypages:

Còn đây là một minh họa khác trên StackOverFlow, minh hoạt cho code này
 
Một minh họa khác cho code sau:
Code trên có sự khác nhau giữa __proto__ và constructor.prototype
Kết luận đáng chú ý:

Về sự khác biệt giữa __proto__ và prototype

prototype chỉ có trên constructor, ví dụ Object, Function, Class…

__proto__ chỉ có trên object không phải là constructor.

chú ý quan trọng trong code trên:

Tóm tắt mối quan hệ giữa prototype và __proto__:

Một: Thuộc tính prototype được tạo mỗi khi một function được tạo ra. Ví dụ hàm JS trên sẽ có một thuộc tính sinh ra khi tạo là JS.prototype.

Bản thân thuộc tính prototype này lại là một object nên có thể khai báo thêm method hoặc property mới cho hàm JS. Tất cả các instance object tạo từ JS thông qua từ khóa “new” đều mang theo các prototype khai báo từ JS.prototype

Hai: Tất cả các instance object tạo từ new JS() đều có một thuộc tính __proto__, thuộc tính này trỏ tới JS.prototype. Ta có thể khai báo method hoặc property mới bằng 2 cách:

Cách 1:

Cách 2:

Giả sử ta có let vuong = new JS().

Ba: Bản thân mỗi hàm lại là một object nên chúng cũng có __proto__, tức JS.__proto__, trỏ tới cấp cao hơn là Function.prototype. Function.__proto__ lại trỏ tới Object.prototype và Object.__proto__ trỏ tới null. Null không có prototype. Xem ảnh:

Có một điều đáng chú ý là tuy js.__proto__ === Function.prototype //true và js.constructor là Function() nhưng:

Xem ảnh:

Ví dụ về Function constructor và object instance

Để giải thích kĩ hơn, ta đọc ví dụ sau:

Ta tạo hàm sau:

Khi chạy, ta thêm một thuộc tính có tên “prototype” vào a, thuộc tính này cũng là object, và có 2 properties: constructor và __proto__

Khi chúng ta chạy code a.prototype. Kết quả là

Trong trường hợp này constructor chính là function và __proto__ trỏ tới Object. Giờ ta sẽ xem chuyện gì xảy ra khi một instance được khởi tạo với từ khóa “new”.

Khi JavaScript chạy, có 4 thứ xảy ra:

Bước 1) Nó tạo một object mới, một object rỗng // {} Bước 2) Nó tạo một __proto__ trên vidu và chỉ tới Vuong.prototype, do đó vidu.__proto__ === Vuong.prototype //true Bước 3) Thực thi Vuong.prototype.constructor (định nghĩa của hàm Vuong), với một object tạo ở bước 1 và trong ngữ cảnh này (this), thuộc tính sẽ được pass sang thành “JavaScript”. Bước 4) Nó trả lại object mới tạo ở bước một và do đó vidu sẽ được gán với object mới.

Giờ nếu ta gán Vuong.prototype.test= “Học JS tại js.edu.vn” và gọi vidu.test, kết quả sẽ là “Học JS tại js.edu.vn”. JavaScript sẽ tìm thuộc tính test trên vidu, nếu nó không tìm thấy, nó sẽ tìm trên vidu.__proto__ (cái này lại trỏ tới Vuong.prototype ở bước 2) và trả về “Học JS tại js.edu.vn”; Xem minh họa:

Tất cả đều từ Function

Có một điều lạ là, trong khi: Object.__proto__ === Function.protype//true, nhưng khi viết Function.prototype.x/y/z thì các object kế thừa lại không được mà phải viết Object.prototype.x/y/z.

Quan sát ảnh:

Còn phía dưới là câu chuyện của Object và Function!!!

Các vấn đề cần bàn thêm ở các bài sau:

  1. Kế thừa giữa các function

  2. Kế thừa giữa các class.

Các vấn đề trọng tâm trong bài trên:

  1. Kế thừa dọc giữa Function và object

  2. Kế thừa ngang giữa 2 function

  3. Kế thừa ngang giữa 2 object (mà không phải là instance tạo từ với “new” Functionname/Classname

Last updated

Navigation

Lionel

@Copyright 2023