Giới thiệu
Trước (và thậm chí sau khi) giới thiệu phiên bản ES2015, việc triển khai OOP trong JavaScript dựa vào prototype-based programming. Trong phong cách lập trình này, object tự đóng gói các properties, methods và data của nó.
Bạn có thể thêm các properties mới cho object này bất cứ khi nào bạn muốn. Object này là độc lập/ riêng lẻ, thay vì là một thể hiện (instance) của một class nào đó; với cách này, nếu bạn muốn một object, bạn có thể tạo một object mà không cần tạo class trước.
JavaScript Prototypes
Tất cả các JavaScript objects đều có property prototype: __proto__
. Prototype của mỗi object được gán cho object trong quá trình tạo ra object đó, nhìn ví dụ sau đây:
let vehicle = { wheels : 4 }; let car = { seats : 5 }; let driver = {}
In tất cả objects và property __proto__
của mỗi variable:
console.log(`vehicle:`, vehicle, vehicle.__proto__);
console.log(`car:`, car, car.__proto__);
console.log(`driver:`, driver, driver.__proto__);
Output
vehicle: { wheels: 4 } {}
car: { seats: 5 } {}
driver: {} {}
Property __proto__
của mỗi object có thể được coi là một empty object
vì các properties của prototype object bị ẩn. Nhưng nó có thể được nhìn thấy thông qua một phương tiện khác, chẳng hạn như browser console. Dưới đây ta sẽ xem các properties được gán cho variable vehicle
:
Tất cả các properties này đềucần thiết cho việc phân loại object và triển khai OOP trong JavaScript.
Sử dụng prototypes
let vehicle = { wheels : 4 }; let car = { seats : 5, __proto__ : vehicle
}; console.log(`vehicle:`, vehicle, vehicle.__proto__);
console.log(`car:`, car, car.__proto__);
console.log(`vehicle seat:`,vehicle.seats);
console.log(`car wheels:`, car.wheels);
Output
vehicle: { wheels: 4 } {}
car: { seats: 5 } { wheels: 4 }
vehicle seat: undefined
car wheels: 4
Đoạn mã trên giống như ví dụ trước của chúng ta, object được gán cho variable car
, thêm cái là bây giờ chúng ta gán vehicle
cho property __proto__
của biến car
.
Khi chúng ta in thuộc tính seats
(thuộc tính của object car
) mà truy cập từ biến vehicle
, chúng ta nhận được giá trị không xác định (undefined
).
Tuy nhiên, khi chúng ta in thuộc tính wheels
(thuộc tính của vehicle
) truy cập từ biến car
, chúng ta nhận được 4
.
Điều này là do vehicle
đã được gán cho __proto__
của đối tượng car
.
Việc gán vehicle
cho __proto__
của car
cho phép object car
truy cập các properties của object vehicle
Property __proto__
cho phép truy cập các thuộc tính của một object khác, nếu object đó được gán cho nó.
Vậy nếu object car
có một property tên là wheels
(trùng tên với property của object được gán cho __proto__
của nó) thì sao?
let vehicle = { wheels : 4 }; let car = { seats : 5, __proto__ : vehicle, wheels : 6,
}; console.log(`vehicle:`, vehicle, vehicle.__proto__);
console.log(`car:`, car, car.__proto__);
console.log(`car wheels:`, car.wheels);
Output
vehicle: { wheels: 4 } {}
car: { seats: 5, wheels: 6 } { wheels: 4 }
car wheels: 6
Với đoạn mã trên, chúng ta có thể thấy rằng mặc dù __proto__
được gán cho car
, giá trị car.wheels
là 6
vì object car
có property wheels
của riêng nó. Property __proto__
chỉ được kiểm tra nếu đối tượng car
không có property có tên tương ứng.
Truy cập và thay đổi prototypes
Vậy, chúng ta đã tìm hiểu về property __proto__
và đã thử gán
cũng như truy cập
trực tiếp vào property đó. Các phép gán hoặc truy xuất các objects prototype của một object gọi ngầm hai phương thức sau:
- Object.getPrototypeOf(obj): Lấy giá trị thuộc tính
__proto__
của objectobj
- Object.setPrototypeOf(obj, proto): Gán giá trị của
proto
cho thuộc tính__proto__
của objectobj
Giờ ta thử sử dụng trực tiếp 2 methods này thay vì gán như ở trên:
let vehicle = { wheels : 4 }; let car = { seats : 5 }; Object.setPrototypeOf(car, vehicle); console.log(`vehicle:`, vehicle, Object.getPrototypeOf(vehicle));
console.log(`car:`, car, Object.getPrototypeOf(car));
console.log(`car wheels:`, car.wheels);
Output
vehicle: { wheels: 4 } {}
car: { seats: 5 } { wheels: 4 }
car wheels: 4
Trong đoạn mã trên, sử dụng các methods Object.getPrototypeOf
và Object.setPrototypeOf
cho kết quả tương tự khi ta gán object cho __proto__
của một object khác.
JavaScript Constructor Functions
Chúng ta sẽ sử dụng các functions để xây dựng các objects, còn được gọi là constructor functions. Đây là cách tiếp cận tốt nhất để triển khai OOP dựa trên prototype.
Constructor functions
Các constructor functions hoặc, object constructor functions chứa các bản thiết kế bao gồm các properties và methods cho một loại đối tượng nào đó, và constructor function này dùng để tạo các đối tượng. Tất cả các đối tượng được tạo từ constructor function sẽ có các properties và methods giống nhau nhưng không nhất thiết phải có cùng giá trị.
Cú pháp
function FunctionName(parameter1, parameter2,...){ // các properties của đối tượng được khởi tạo ở đây // các methods được cung cấp cho các đối tượng được định nghĩa ở đây
}
LƯU Ý: Các tham số là tùy chọn.
Ví dụ
// Tên của constructor function: Employee
function Employee(_name, _age, _designation){ // Các properties của đối tượng được tạo được gán bởi các tham số được truyền vào. this.name = _name; this.age = _age; this.designation = _designation; // Cập nhật giá trị của property age this.setAge = newage => { console.log(`setting age from ${this.age} to ${newage}`) this.age = newage; } this.company = 'Amazon';
} var employee1 = new Employee('Mark', 12, 'Manager'); console.log(`employee1 name: ${employee1.name} age: ${employee1.age}`)
console.log(`employee1 company: ${employee1.company}`) employee1.setAge(20); console.log(`employee1 name: ${employee1.name} age: ${employee1.age}`)
Output
employee1 name: Mark age: 12
employee1 company: Amazon
setting age from 12 to 20
employee1 name: Mark age: 20
Ở đoạn code trên, ta đã làm quen với việc tạo ra constructor function có chứa properties, method, sau đó tạo object bằng constructor function và truy cập properties của objects đó.
Vậy thêm property sau khi constructor function đã được tạo ra thì sao?
function Employee(_name, _age, _designation){ this.name = _name; this.age = _age; this.designation = _designation; this.setAge = newage => { console.log(`setting age from ${this.age} to ${newage}`) this.age = newage; } this.company = 'Amazon';
} Employee.planet = 'Earth'; // thử thêm property planet vào constructor function Employee var employee1 = new Employee('Mark', 20, 'Manager'); var employee2 = new Employee('Bob', 30, 'Accountant'); employee1.gender = 'male'; // thêm property gender cho employee1 console.log(`employee names: ${employee1.name}, ${employee2.name}`)
console.log(`employee planet: ${employee1.planet}, ${employee2.planet}`)
console.log(`employee gender: ${employee1.gender}, ${employee2.gender}`)
Output
employee names: Mark, Bob
employee planet: undefined, undefined
employee gender: male, undefined
- Ở dòng
Employee.planet = 'Earth'
, ta đang cố thêm propertyplanet
vào vào constructor function , điều này là không được. - Nhưng với dòng
employee1.gender = 'male';
, ta thêm propertygender
choemployee1
, và được.
Ta thấy, khi truy cập planet
từ 2 object employee1
và employee2
, ta nhận được undefined
, nhưng khi thử truy cập property gender
của employee1
thì lại nhận được male
. JS cho phép chúng ta thêm property cho các object đã tạo mà không làm thay đổi property của các object khác.
Prototype objects với constructor functions
Tất cả các objects được tạo bởi constructor functions chia sẻ prototype object của nó. Lấy ví dụ trước của chúng ta, tất cả các object được tạo bằng constructor function Employee
sẽ chia sẻ cùng một prototype object.
Thử xem đoạn code sau đây:
function Employee(_name, _age, _designation){ this.name = _name; this.age = _age; this.designation = _designation; this.setAge = newage => { console.log(`setting age from ${this.age} to ${newage}`) this.age = newage; } this.company = 'Amazon';
}
Employee.prototype.name = 'Bill';
var employee1 = new Employee('Mark', 20, 'Manager'); var employee2 = new Employee('Bob', 30, 'Accountant'); console.log(employee1.name, employee2.name);
console.log(Employee.prototype);
console.log(`employee protoypes: ${employee1.__proto__}, ${employee2.__proto__}`);
console.log(`protoype equalities: ${employee1.__proto__ === employee2.__proto__}`);
Output
Mark Bob
Employee { name: 'Bill' }
employee protoypes: [object Object], [object Object]
protoype equalities: true
Prototype object của constructor function Employee
được gán cho __proto__
của mỗi đối tượng được tạo ra bởi constructor function Employee
.
Đó là lý do khi ta so sánh __proto__
của employee1
và employee2
, ta nhận được kết quả là chúng giống nhau (true
).
Và prototype object của constructor function Employee
được truy cập như sau:
Employee.prototype
Nên nếu chúng ta so sánh
console.log(`protoype equalities: ${Employee.prototype === employee2.__proto__}`);
Cũng sẽ nhận được kết quả là true
.
protoype equalities: true
Kết bài
Vậy là em/mình đã giới thiệu qua prototypes và constructor functions trong JavaScript. Bài này là bài đầu tiên trong series ngắn: OOP trong JavaScript.
Bài này được viết lúc em/mình đang học JavaScript nên nếu có gì sai sót cũng như hiểu chưa tới, mong mọi người góp ý và chỉnh sửa. Cảm ơn mọi người đã đọc.
Em vừa tốt nghiệp, đang làm việc intern/fresher SWE/Python developer/JS Developer. Mong team anh chị nào cần người có thể cân nhắc cho em 1 cơ hội. Em cảm ơn.