Kế thừa Prototype ngang và dọc
https://js.edu.vn/7-ke-thua-prototype-ngang-va-doc.html
Last updated
https://js.edu.vn/7-ke-thua-prototype-ngang-va-doc.html
Last updated
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:
1234567
//Cách 1:
__proto__
//Cách 2:
Object.getPrototypeOf(x)
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.
0102030405060708091011121314151617181920
var
o = {a: 1};
// Object o sẽ có prototype là Object.prototype. Người ta hay dùng [[Prototype]] để gọi tên.// Bản thân o không có thuộc tính 'hasOwnProperty'// hasOwnProperty là một thuộc tính của Object.prototype.// do đó o sẽ kế thừa hasOwnProperty từ Object.prototype// Object.prototype có null là prototype.// Vậy cấp bậc kế thừa là o | Object.prototype | null
var
b = ['hello',
'js.edu.vn',
'Vượng'];
// Arrays kế thừa từ Array.prototype gồm các phương thức như indexOf, forEach, vân vân...// Chuỗi prototype là b (cấp 1) | Array.prototype (cấp 2) | Object.prototype (cấp 3) | null
function
f() {return
2;}
// Hàm kế thừa từ Function.prototype với một số phương thức như call, bind...// Chuỗi prototype là f | Function.prototype | Object.prototype | null
với Constructor
0102030405060708091011121314
function
Graph() {this.vertices = [];this.edges = [];}
Graph.prototype = {addVertex:
function(v) {this.vertices.push(v);}};
var
g =
new
Graph();// g là một object với thuộc tính 'vertices' và 'edges'.// g.[[Prototype]] là giá trị của Graph.prototype khi new Graph() được gọi.
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).
0102030405060708091011121314
var
a = {a: 1};// a | Object.prototype | null
var
b = Object.create(a);// b | a | Object.prototype | nullconsole.log(b.a);
// 1 (inherited)
var
c = Object.create(b);// c | b | a | Object.prototype | null
var
d = Object.create(null);// d | nullconsole.log(d.hasOwnProperty);// undefined, because d doesn't inherit from Object.prototype
Với class. Sẽ bàn thêm về kế thừa class sau.
0102030405060708091011121314151617181920212223
'use strict';
class
Polygon {constructor(height, width) {this.height = height;this.width = width;}}
class
Square
extends
Polygon {constructor(sideLength) {super(sideLength, sideLength);}get area() {return
this.height *
this.width;}set sideLength(newLength) {this.height = newLength;this.width = newLength;}}
var
square =
new
Square(2);
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ụ:
010203040506070809101112131415161718192021222324
function
Animal(age) {this.age = age;}
Animal.prototype.showAge =
function() {console.log(
this.age );};
//Tạo ra 1 hàm khởi tạo con (sẽ dùng để kế từ hàm cơ sở)function
Cat(color) {this.color = color;}
//Do prototype của 1 function là 1 object, nên ta có thể dùng cách khởi tạo 1 object trong JS để gán giá trị cho nóCat.prototype =
new
Animal();Cat.prototype.showColor =
function(){console.log(
this.color );};
//Kiểm tra sự kế thừavar
kitty =
new
Cat('pink');kitty.age = 5;kitty.showAge();
// Mặc dù kitty không có hàm showAge(), nhưng giống như ví dụ ở trên, JS sẽ tìm kiếm showAge trong prototype của nókitty.showColor();
// Tương tự với hàm showAge
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.
01020304050607080910111213141516171819202122232425262728293031323334353637
// Initialize constructor functionsfunction
Hero(name, level) {this.name = name;this.level = level;}
function
Warrior(name, level, weapon) {Hero.call(this, name, level);
this.weapon = weapon;}
function
Healer(name, level, spell) {Hero.call(this, name, level);
this.spell = spell;}
// Link prototypes and add prototype methodsWarrior.prototype = Object.create(Hero.prototype);Healer.prototype = Object.create(Hero.prototype);
Hero.prototype.greet =
function
() {return
`${this.name} says hello.`;}
Warrior.prototype.attack =
function
() {return
`${this.name} attacks
with
the ${this.weapon}.`;}
Healer.prototype.heal =
function
() {return
`${this.name} casts ${this.spell}.`;}
// Initialize individual character instancesconst hero1 =
new
Warrior('Bjorn', 1,
'axe');const hero2 =
new
Healer('Kanin', 1,
'cure');
Kế thừa ngang giữa 2 object
Có 2 cách: __proto__ và Object.create. Quan sát ví dụ:
01020304050607080910111213141516
Object.O1='';Object.prototype.Op1='';
Function.F1 =
'';Function.prototype.Fp1 =
'';
Cat =
function(){};Cat.C1 =
'';Cat.prototype.Cp1 =
'';
mycat =
new
Cat();o = {};
// EDITED: using console.dir now instead of console.logconsole.dir(mycat);console.dir(o);
010203040506070809101112
function
Gadget(name, color){ this.name = name; this.color = color;}
Gadget.prototype.rating = 3
var
newtoy =
new
Gadget("webcam",
"black")
newtoy.constructor.prototype.constructor.prototype.constructor.prototype
//trả về 3newtoy.__proto__.__proto__.__proto__// trả về null
1
The most surprising thing for me was discovering that Object.__proto__ points to Function.prototype, instead of Object.prototype.
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:
1
vuong.__proto__ == JS.prototype;
// true
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:
123
JS.prototype.x = something;
// thêm thuộc tính cho cả Constructor JS và tất cả các instance tạo từ JS
JS.prototype.y =
function(){}
// thêm phương thức cho cả Constructor JS và tất cả các instance tạo từ JS
Cách 2:
Giả sử ta có let vuong = new JS().
123
vuong.__proto__.x = something;
// thêm thuộc tính cho cả Constructor JS và tất cả các instance tạo từ JS
vuong.__proto__.y =
function(){}
// thêm phương thức cho cả Constructor JS và tất cả các instance tạo từ 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:
0102030405060708091011
x
instanceof
js//true
y
instanceof
js//true
x
instanceof
Function//false
y
instanceof
Function//false
x
instanceof
Object//true
y
instanceof
Object//true
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:
123
function
Vuong (name) {this.name = name;}
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à
12
constructor: Vuong
// constructor là chính function "a" definition__proto__: Object
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”.
1
let
vidu =
new
Vuong ('JavaScript');
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:
Kế thừa giữa các function
Kế thừa giữa các class.
Các vấn đề trọng tâm trong bài trên:
Kế thừa dọc giữa Function và object
Kế thừa ngang giữa 2 function
Kế thừa ngang giữa 2 object (mà không phải là instance tạo từ với “new” Functionname/Classname