Mặc dù nhiều người ở đây nói rằng không có cách tốt nhất để tạo đối tượng, nhưng có một lý do là tại sao có rất nhiều cách để tạo đối tượng trong JavaScript, kể từ năm 2019, và điều này phải làm với tiến trình của JavaScript qua các lần lặp khác nhau phát hành EcmaScript có từ năm 1997.
Trước ECMAScript 5, chỉ có hai cách tạo đối tượng: hàm xây dựng hoặc ký hiệu bằng chữ (một cách thay thế tốt hơn cho Object mới ()). Với ký hiệu hàm tạo, bạn tạo một đối tượng có thể được khởi tạo thành nhiều trường hợp (với từ khóa mới), trong khi ký hiệu bằng chữ sẽ cung cấp một đối tượng, giống như một đơn.
// constructor function
function Person() {};
// literal notation
var Person = {};
Bất kể phương thức bạn sử dụng là gì, các đối tượng JavaScript chỉ là các thuộc tính của các cặp giá trị khóa:
// Method 1: dot notation
obj.firstName = 'Bob';
// Method 2: bracket notation. With bracket notation, you can use invalid characters for a javascript identifier.
obj['lastName'] = 'Smith';
// Method 3: Object.defineProperty
Object.defineProperty(obj, 'firstName', {
value: 'Bob',
writable: true,
configurable: true,
enumerable: false
})
// Method 4: Object.defineProperties
Object.defineProperties(obj, {
firstName: {
value: 'Bob',
writable: true
},
lastName: {
value: 'Smith',
writable: false
}
});
Trong các phiên bản đầu tiên của JavaScript, cách thực sự duy nhất để bắt chước kế thừa dựa trên lớp là sử dụng các hàm tạo. hàm tạo là một hàm đặc biệt được gọi với từ khóa 'mới'. Theo quy ước, định danh hàm được viết hoa, albiet nó không bắt buộc. Bên trong hàm tạo, chúng tôi đề cập đến từ khóa 'this' để thêm các thuộc tính cho đối tượng mà hàm constructor đang tạo ngầm. Hàm constructor hoàn toàn trả về đối tượng mới với các thuộc tính được điền trở lại hàm gọi, trừ khi bạn sử dụng rõ ràng từ khóa return và trả về một cái gì đó khác.
function Person(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
this.sayName = function(){
return "My name is " + this.firstName + " " + this.lastName;
}
}
var bob = new Person("Bob", "Smith");
bob instanceOf Person // true
Có một vấn đề với phương thức sayName. Thông thường, trong các ngôn ngữ lập trình dựa trên lớp hướng đối tượng, bạn sử dụng các lớp làm nhà máy để tạo đối tượng. Mỗi đối tượng sẽ có các biến đối tượng riêng, nhưng nó sẽ có một con trỏ tới các phương thức được định nghĩa trong kế hoạch chi tiết của lớp. Thật không may, khi sử dụng hàm xây dựng của JavaScript, mỗi khi được gọi, nó sẽ xác định một thuộc tính sayName mới trên đối tượng mới được tạo. Vì vậy, mỗi đối tượng sẽ có thuộc tính sayName riêng. Điều này sẽ tiêu tốn nhiều tài nguyên bộ nhớ hơn.
Ngoài việc tăng tài nguyên bộ nhớ, việc xác định các phương thức bên trong hàm xây dựng sẽ loại bỏ khả năng kế thừa. Một lần nữa, phương thức sẽ được định nghĩa là một thuộc tính trên đối tượng mới được tạo và không có đối tượng nào khác, vì vậy tính kế thừa không thể hoạt động như thế nào. Do đó, JavaScript cung cấp chuỗi nguyên mẫu như một hình thức thừa kế, biến JavaScript thành ngôn ngữ nguyên mẫu.
Nếu bạn có cha mẹ và cha mẹ chia sẻ nhiều tài sản của một đứa trẻ, thì đứa trẻ nên được thừa hưởng những tài sản đó. Trước ES5, nó đã được thực hiện như sau:
function Parent(eyeColor, hairColor) {
this.eyeColor = eyeColor;
this.hairColor = hairColor;
}
Parent.prototype.getEyeColor = function() {
console.log('has ' + this.eyeColor);
}
Parent.prototype.getHairColor = function() {
console.log('has ' + this.hairColor);
}
function Child(firstName, lastName) {
Parent.call(this, arguments[2], arguments[3]);
this.firstName = firstName;
this.lastName = lastName;
}
Child.prototype = Parent.prototype;
var child = new Child('Bob', 'Smith', 'blue', 'blonde');
child.getEyeColor(); // has blue eyes
child.getHairColor(); // has blonde hair
Cách chúng tôi sử dụng chuỗi nguyên mẫu ở trên có một sự giải quyết. Vì nguyên mẫu là một liên kết trực tiếp, bằng cách thay đổi thuộc tính của một đối tượng trong chuỗi nguyên mẫu, bạn cũng sẽ thay đổi cùng một thuộc tính của đối tượng khác. Rõ ràng, thay đổi phương pháp kế thừa của một đứa trẻ không nên thay đổi phương pháp của cha mẹ. Object.create đã giải quyết vấn đề này bằng cách sử dụng một polyfill. Do đó, với Object.create, bạn có thể sửa đổi một cách an toàn tài sản của trẻ em trong chuỗi nguyên mẫu mà không ảnh hưởng đến tài sản tương tự của cha mẹ trong chuỗi nguyên mẫu.
ECMAScript 5 đã giới thiệu Object.create để giải quyết lỗi đã nói ở trên trong hàm tạo để tạo đối tượng. Phương thức Object.create () TẠO một đối tượng mới, sử dụng một đối tượng hiện có làm nguyên mẫu của đối tượng mới được tạo. Vì một đối tượng mới được tạo, bạn không còn gặp phải vấn đề khi sửa đổi thuộc tính con trong chuỗi nguyên mẫu sẽ sửa đổi tham chiếu của cha mẹ đối với thuộc tính đó trong chuỗi.
var bobSmith = {
firstName: "Bob",
lastName: "Smith",
sayName: function(){
return "My name is " + this.firstName + " " + this.lastName;
}
}
var janeSmith = Object.create(bobSmith, {
firstName : { value: "Jane" }
})
console.log(bobSmith.sayName()); // My name is Bob Smith
console.log(janeSmith.sayName()); // My name is Jane Smith
janeSmith.__proto__ == bobSmith; // true
janeSmith instanceof bobSmith; // Uncaught TypeError: Right-hand side of 'instanceof' is not callable. Error occurs because bobSmith is not a constructor function.
Trước ES6, đây là một mẫu sáng tạo phổ biến để sử dụng các hàm tạo và Object.create:
const View = function(element){
this.element = element;
}
View.prototype = {
getElement: function(){
this.element
}
}
const SubView = function(element){
View.call(this, element);
}
SubView.prototype = Object.create(View.prototype);
Bây giờ Object.create kết hợp với các hàm xây dựng đã được sử dụng rộng rãi để tạo và kế thừa đối tượng trong JavaScript. Tuy nhiên, ES6 đã đưa ra khái niệm về các lớp, chủ yếu là đường cú pháp so với kế thừa dựa trên nguyên mẫu hiện có của JavaScript. Cú pháp lớp không giới thiệu một mô hình kế thừa hướng đối tượng mới cho JavaScript. Do đó, JavaScript vẫn là một ngôn ngữ nguyên mẫu.
Các lớp ES6 làm cho việc thừa kế dễ dàng hơn nhiều. Chúng ta không còn phải sao chép thủ công các hàm nguyên mẫu của lớp cha và đặt lại hàm tạo của lớp con.
// create parent class
class Person {
constructor (name) {
this.name = name;
}
}
// create child class and extend our parent class
class Boy extends Person {
constructor (name, color) {
// invoke our parent constructor function passing in any required parameters
super(name);
this.favoriteColor = color;
}
}
const boy = new Boy('bob', 'blue')
boy.favoriteColor; // blue
Nói chung, 5 chiến lược khác nhau của Tạo đối tượng trong JavaScript đã trùng khớp với sự phát triển của tiêu chuẩn EcmaScript.