Câu 1Sau class Coupon {}, nếu gọi Coupon("SPRING") mà không có new thì sao?
Class cơ bản — định nghĩa class và tạo instance
Dùng class để tạo nhiều object cùng cấu trúc: tạo instance bằng constructor và new, phương thức dùng this, và khi nào chỉ cần object literal.
Hội viên, sản phẩm, coupon: trong một ứng dụng, những dữ liệu có cùng các trường và dùng cùng một phép tính xuất hiện rất nhiều lần. Nếu viết từng cái bằng object literal, bạn sẽ phải chép lại cả các trường lẫn công thức tính cho từng mục.
Bài này nói về class, nơi gom cấu trúc dữ liệu và phần xử lý về một chỗ, và cách dùng new để tạo từng object từ đó.
Định nghĩa cấu trúc dữ liệu ở một chỗ — class và new
Giả sử bạn cần xử lý hai loại coupon giảm giá. Nếu viết { code: "WELCOME", discount: 500 } cho từng coupon và chỉ gõ nhầm thành discont ở một cái, chương trình vẫn chạy bình thường, chỉ có mức giảm của coupon đó thành undefined. Vì tên key được viết lại ở mọi coupon, bạn sẽ phải dò từng cái để tìm ra chỗ gõ nhầm.
Class (cú pháp định nghĩa một loại object được tạo bằng new) được viết dưới dạng class Coupon { ... }; bên trong là constructor (phần mà new gọi tới), viết không cần từ khóa function.
this trong đó là instance mới (object được tạo từ class), và new trả về nó dù không có return.
// Định nghĩa cấu trúc coupon ở một chỗ bằng class
class Coupon {
constructor(code, discount) {
this.code = code; // Bên trái là code của instance, bên phải là đối số nhận vào
this.discount = discount;
}
}
// Tạo từng coupon bằng new
const welcome = new Coupon("WELCOME", 500);
const summer = new Coupon("SUMMER", 1000);
console.log(welcome.code); // WELCOME
console.log(summer.discount); // 1000
console.log(welcome instanceof Coupon); // true
// Gọi mà không có new sẽ báo lỗi
// Coupon("SPRING", 300); // TypeError
this bên trong constructor. Nếu gọi mà không có new, lỗi xảy ra trước cả khi vào constructor.Tên key chỉ được viết ở một chỗ duy nhất là bên trong constructor, nên nếu có gõ nhầm như discont thì cũng chỉ cần sửa một chỗ. Ở cột bên phải, console hiển thị TypeError: Class constructor Coupon cannot be invoked without 'new'.
Tính toán bằng giá trị của instance — phương thức và this
Nếu bạn viết công thức tính số tiền sau giảm giá total - welcome.discount ở cả màn hình giỏ hàng lẫn màn hình xác nhận đơn hàng, mỗi lần quy tắc tính thay đổi bạn sẽ phải sửa hai chỗ. Kể cả khi gom vào một hàm, các trường vẫn nằm trong class còn phép tính lại nằm bên ngoài.
Phương thức (hàm thuộc về một object, được gọi bằng dấu chấm như welcome.apply(3000)) được viết trong cặp ngoặc nhọn của class dưới dạng apply(total) { ... }, cũng không có function. Bên trong phương thức, this trỏ tới instance đứng bên trái dấu chấm, và this.discount đọc giá trị tại thời điểm gọi.
class Coupon {
constructor(code, discount) {
this.code = code;
this.discount = discount;
}
// Trừ mức giảm của coupon này khỏi tổng tiền rồi trả về
apply(total) {
return total - this.discount; // this là instance bên trái dấu chấm
}
}
// Tạo hai loại coupon
const welcome = new Coupon("WELCOME", 500);
const summer = new Coupon("SUMMER", 1000);
// Cùng một apply, nhưng kết quả thay đổi theo instance bên trái dấu chấm
console.log(welcome.apply(3000)); // 2500
console.log(summer.apply(3000)); // 2000
this là biến đứng ngay trước .apply. Cùng một đối số, nhưng this khác thì kết quả cũng khác.- Đối số
code/discount— chỉ dùng được trong cặp ngoặc này this.discount = discount— lưu giá trị vào instance
this.discount— đọc được giá trị đã lưu vào instancetotal - discount— không thấy tên nên báoReferenceError
constructor thì tên discount không còn thấy được. apply đọc giá trị đã lưu vào this.Nếu trong apply bạn quên this. và viết total - discount, JavaScript sẽ tìm tên discount từ cặp ngoặc trong cùng ra dần bên ngoài. Vì vậy kết quả phụ thuộc vào việc bên ngoài class có biến cùng tên hay không.
Quên this. không phải lúc nào cũng báo lỗi
Nếu bên ngoài class có biến cùng tên như const discount = 300;, thì total - discount thiếu this. sẽ không báo ReferenceError mà lặng lẽ dùng con số 300 đó. Khi gọi với coupon nào cũng ra cùng một số tiền, hãy nghi ngờ mình đã quên this..
Mỗi instance giữ giá trị riêng — nhiều instance
Mỗi lần dùng một coupon, số lượt còn lại của coupon đó giảm đi 1. Nếu bạn giữ số lượt còn lại bên ngoài class bằng một object như { WELCOME: 3, SUMMER: 3 }, thì ở mỗi màn hình dùng coupon, bạn đều cần đoạn code tìm số lượt theo code rồi ghi giá trị mới trở lại.
Bên trong phương thức, bạn cũng có thể gán vào property của this, như this.remaining = this.remaining - 1. Vế phải đọc số lượt hiện tại, rồi giá trị đã trừ 1 được ghi lại vào remaining của chính instance đó, nên số lượt giảm dần từ 3 (giá trị truyền cho new) sau mỗi lần gọi.
class Coupon {
constructor(code, remaining) {
this.code = code;
this.remaining = remaining; // Số lượt còn dùng được
}
// Giảm 1 lượt của instance bên trái dấu chấm
use() {
this.remaining = this.remaining - 1;
}
}
// Tạo hai loại với cùng số lượt, rồi dùng số lần khác nhau
const welcome = new Coupon("WELCOME", 3);
const summer = new Coupon("SUMMER", 3);
welcome.use();
welcome.use();
summer.use();
console.log(welcome.remaining); // 1
console.log(summer.remaining); // 2
Instance không chỉ lưu được vào biến mà còn vào phần tử của mảng. Khi lấy ra và gọi như coupons[0].use(), this vẫn trỏ tới phần tử đó. Dù có bao nhiêu coupon, đoạn code giảm số lượt cũng chỉ là một dòng bên trong use().
Chọn theo số lượng cần tạo — so sánh với object literal
Với dữ liệu chỉ dùng một lần, như thiết lập hiển thị của một màn hình, bạn chỉ cần viết { ... } ngay tại chỗ. Nhưng với dữ liệu như coupon, cần tạo nhiều mục có cùng cấu trúc và cùng phép tính, nếu cứ viết theo cách đó thì bạn sẽ phải chép cả phần thân của phép tính cho từng mục.
Object literal cũng có thể có phương thức: chỉ cần viết apply(total) { ... } cạnh các cặp key–giá trị, và this bên trong sẽ trỏ tới chính object đó. Điểm khác với class là nơi viết phần thân của phương thức, và sự khác biệt này hiện ra trong kết quả của Object.keys.
// class: phần thân chỉ viết một lần trong định nghĩa
class Coupon {
constructor(code, discount) {
this.code = code;
this.discount = discount;
}
apply(total) { return total - this.discount; }
}
const welcome = new Coupon("WELCOME", 500);
// Literal: phần thân phải viết lại cho từng mục
const summer = {
code: "SUMMER",
discount: 1000,
apply(total) { return total - this.discount; },
};
// Các key khác nhau
console.log(Object.keys(welcome).join(", ")); // code, discount
console.log(Object.keys(summer).join(", ")); // code, discount, apply
Dù phần thân nằm ở chỗ khác nhau, welcome.apply(3000) vẫn được gọi giống hệt summer.apply(3000), nên code phía gọi không thay đổi. Cơ chế giúp instance gọi được phần thân nằm trong class sẽ được giải thích trong bài về prototype. Bảng dưới đây so sánh hai cách viết.
| Tiêu chí | Object literal | class |
|---|---|---|
| Số object tạo được | Chỉ đúng { } đã viết | Mỗi lần new thêm 1 |
| Thân phương thức | Chép cho từng mục | Viết 1 lần trong class |
| Key trong Object.keys | code, discount, apply | code, discount |
| instanceof Coupon | false | true |
| Phù hợp khi | Cấu hình, dữ liệu dùng 1 lần | Nhiều mục, cùng logic |
Không đặt dấu phẩy sau phương thức trong class
Khi chuyển một literal thành class, nếu để sót dấu phẩy sau phương thức như apply(total) { ... },, bạn sẽ gặp SyntaxError: Unexpected token ',' và không dòng nào được chạy. Trong cặp ngoặc nhọn của class, các phương thức được viết nối tiếp nhau mà không cần dấu phân cách.
Kiểm tra kiến thức
Hãy trả lời từng câu hỏi một.
Câu 2Bên ngoài có biến discount bằng 300. Nếu quên this. thì welcome.apply(3000) trả về gì?
Câu 3Dùng Object.keys cho instance của class có constructor(code, discount) và apply thì nhận được gì?