Học bằng cách đọc theo thứ tự

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
Có và không có new cho kết quả khác nhau
Định nghĩaclass Couponnew Coupon("WELCOME", 500)new Coupon("SUMMER", 1000)Coupon("SPRING", 300)Gán "WELCOME"cho this.codeGán "SUMMER"cho this.codeThiếu new, khôngvào constructorwelcome nhậninstancesummer nhậninstance khácBáo lỗiTypeErrortruyền vàotruyền vào
Các đối số được ghi vào 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'.

Hãy tạo dữ liệu sản phẩm bằng class. productName và productPrice đã được khai báo sẵn.

① Hãy định nghĩa Product để mỗi instance giữ tên sản phẩm và giá.

② Hãy truyền hai biến đó vào để tạo mouse.

③ Hãy hiển thị mouse theo dạng "Tên sản phẩm: giá yên".

④ Hãy đặt dòng gọi Product không có new vào trong try, rồi hiển thị name của lỗi.

(Nếu chạy đúng, phần giải thích sẽ xuất hiện.)

JavaScript / TypeScript Editor

Chạy code để xem đầu ra

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
Bên trái dấu chấm trở thành this
welcome.apply(3000)this làwelcomethis.discountlà 500Trả về 2500summer.apply(3000)this làsummerthis.discountlà 1000Trả về 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.
Phạm vi của đối số constructor
Ngoặc nhọn của class Coupon
Ngoặc của constructor(code, discount)
  • Đố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
Ngoặc của apply(total)
  • this.discount — đọc được giá trị đã lưu vào instance
  • total - discount — không thấy tên nên báo ReferenceError
Ra khỏi cặp ngoặc nhọn của 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..

Hãy tính giá đã gồm thuế của bàn phím bằng một phương thức của sản phẩm. Product và keyboard đã được khai báo sẵn.

① Hãy thêm vào Product phương thức taxIncluded trả về giá đã gồm thuế (thuế 10%, làm tròn xuống).

② Hãy hiển thị tên và giá gồm thuế của keyboard theo dạng "Tên: X yên (gồm thuế)".

③ Hãy đổi giá của keyboard thành 9800.

④ Hãy hiển thị lại theo đúng dạng ở ②.

JavaScript / TypeScript Editor

Chạy code để xem đầu ra

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
use() chỉ giảm lượt của instance bên trái dấu chấm
Gọi welcome.use()2 lầnthis làwelcomeremaining:3 → 2 → 1Không đụngtới summerGọi summer.use()1 lầnthis làsummerremaining:3 → 2Không đụngtới welcome
use() của welcome và của summer không đụng tới số lượt của nhau. Chỉ instance viết bên trái use() bị giảm lượt.

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().

Hãy quản lý tồn kho của 3 sản phẩm trong kho hàng, mỗi sản phẩm là một instance. Product với phương thức xuất kho ship đã được khai báo sẵn.

① Hãy tạo instance cho giá đỡ màn hình 12 cái, hub USB 30 cái, giá đỡ laptop 8 cái, rồi gom chúng vào một mảng.

② Hãy xuất kho 5 cái, chỉ cho sản phẩm đầu tiên.

③ Hãy hiển thị "Tên sản phẩm: tồn kho X" cho mọi sản phẩm, mỗi sản phẩm một dòng.

④ Hãy hiển thị tổng tồn kho theo dạng "Tổng: X".

JavaScript / TypeScript Editor

Chạy code để xem đầu ra

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 literalclass
Số object tạo đượcChỉ đúng { } đã viếtMỗi lần new thêm 1
Thân phương thứcChép cho từng mụcViết 1 lần trong class
Key trong Object.keyscode, discount, applycode, discount
instanceof Couponfalsetrue
Phù hợp khiCấu hình, dữ liệu dùng 1 lầnNhiề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.

QUIZ

Kiểm tra kiến thức

Hãy trả lời từng câu hỏi một.

Câu 1Sau class Coupon {}, nếu gọi Coupon("SPRING") mà không có new thì sao?

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)apply thì nhận được gì?