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

Instance member và static — giá trị và phương thức thuộc về class

static đặt giá trị và phương thức lên chính class: bộ đếm bằng static field, factory method từ JSON, hằng số dùng chung và instance member.

Để đánh số vận đơn theo thứ tự tạo, bạn cần lưu ở đâu đó số lượng vận đơn đã tạo. Người nhận thì mỗi vận đơn một khác, nhưng số lượng đã tạo hay giới hạn trọng lượng chỉ cần có một giá trị dùng chung cho mọi vận đơn.

Bài này nói về static, cách đặt giá trị và phương thức lên chính class thay vì lên instance.

Đếm số lượng vận đơn — static field

Giả sử bạn muốn đánh số vận đơn theo thứ tự được tạo. Nếu bạn viết class field count = 0;, tăng nó bằng this.count += 1 trong constructor rồi dùng làm số vận đơn, thì vận đơn nào cũng mang số 1, vì field được khởi tạo lại bằng 0 ở mỗi lần new.

Static field (property được viết trong class với static ở phía trước, chỉ có một bản duy nhất nằm trên chính class) được đọc và ghi qua tên class, như Parcel.count.

Ngược lại, những property mà mỗi instance giữ riêng như this.number, cùng các phương thức viết không có static, được gọi là instance member.

class Parcel {
  // Số lượng vận đơn đã tạo đến giờ, chỉ có một giá trị trên class
  static count = 0;

  constructor(recipient) {
    Parcel.count += 1;               // Tăng count của class thêm 1
    this.number = Parcel.count;      // Dùng số lượng tại thời điểm này làm số vận đơn
    this.recipient = recipient;
  }
}

// Tạo 2 vận đơn
const first = new Parcel("Minh");
const second = new Parcel("Linh");

console.log(first.number, second.number);   // 1 2
console.log(Parcel.count);                  // 2
console.log(first.count);                   // undefined
count và số vận đơn nằm ở đâu
Parcel (chính class)
  • static count — thành 1 sau new đầu tiên, 2 sau new thứ hai
  • Đọc và ghi bằng Parcel.count
Các instance tạo bằng new
first
  • number — 1 (count lúc được tạo)
  • recipient — "Minh"
  • Không có countfirst.countundefined
second
  • number — 2 (count lúc được tạo)
  • recipient — "Linh"
count chỉ có một, nằm trong khung của class, không có trong khung của instance. Đọc nó từ instance sẽ ra undefined.

Cả hai lần new đều tăng cùng một count trong khung của class thêm 1, rồi chép giá trị lúc đó vào number của riêng instance mới. Dù sau đó count tăng lên 2, first.number vẫn là 1, giá trị nó nhận lúc được tạo.

Hãy đánh số các câu trả lời khảo sát theo thứ tự nhận được. answerTexts đã được khai báo sẵn.

① Hãy định nghĩa SurveyAnswer nhận nội dung câu trả lời. Mỗi lần tạo, hãy tăng bộ đếm dùng chung cho mọi câu trả lời thêm 1 và dùng giá trị đó làm số thứ tự của câu trả lời.

② Hãy tạo SurveyAnswer cho 3 nội dung và đưa vào answers.

③ Hãy hiển thị mọi câu trả lời theo dạng "A-001 nội dung".

④ Hãy hiển thị "Số câu trả lời: X" và "Số thứ tự của câu đầu tiên: X".

(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ạo vận đơn từ JSON — static method

Khi tải lại một vận đơn đã lưu, bạn sẽ nhận được một chuỗi JSON chứa người nhận và trọng lượng. Nếu màn hình nào tải vận đơn cũng tự viết JSON.parsenew Parcel(...), thì mỗi khi tên key trong JSON thay đổi, bạn phải sửa tất cả các màn hình đó.

Lúc tạo vận đơn từ JSON thì chưa có instance nào để gọi phương thức. Static method (phương thức được định nghĩa với static, gọi qua tên class mà không cần tạo instance) có thể gọi dưới dạng Parcel.fromJSON(text). Static method tạo và trả về một instance như fromJSON được gọi là factory method.

class Parcel {
  constructor(recipient, weightKg) {
    this.recipient = recipient;
    this.weightKg = weightKg;
  }

  label() {
    return `${this.recipient} (${this.weightKg} kg)`;
  }

  // Nhận chuỗi JSON, tạo instance của Parcel rồi trả về
  static fromJSON(text) {
    const data = JSON.parse(text);
    return new Parcel(data.recipient, data.weightKg);
  }
}

const text = '{"recipient":"Minh","weightKg":2}';
const parcel = Parcel.fromJSON(text);        // Gọi qua tên class
console.log(parcel.label());                 // Minh (2 kg)
Từ chuỗi JSON thành Parcel
Parcel.fromJSON(text) được gọiĐọc bằngJSON.parse(text)Tạo và trả vềnew Parcel(…)text chỉ làmột chuỗiCó property nhưngkhông có label()Instance gọiđược label()
Ở bước JSON.parse, object có property nhưng chưa có phương thức. Chỉ sau khi đi qua new, nó mới thành instance gọi được label().

Dòng cuối của fromJSONnew Parcel(...), nên phần thân constructor vẫn chạy như bình thường. Nếu bạn đặt các phép kiểm tra bằng throw trong constructor, vận đơn tải lại từ JSON cũng đi qua đúng những phép kiểm tra như vận đơn nhập trên màn hình.

Không gọi được fromJSON từ instance

Nếu gọi từ instance như parcel.fromJSON(text), bạn sẽ gặp lỗi TypeError: parcel.fromJSON is not a function. Khác với label(), phương thức có static chỉ gọi được qua tên class. Cơ chế phía sau sẽ được giải thích trong bài về prototype.

Hãy khôi phục các sản phẩm đã lưu trong giỏ hàng từ chuỗi JSON. CartItem và savedTexts đã được khai báo sẵn.

① Hãy thêm phương thức fromJSON tạo và trả về instance của CartItem từ một chuỗi JSON.

② Hãy dùng ① để chuyển 2 mục trong savedTexts thành CartItem.

③ Hãy hiển thị "Tên x số lượng = thành tiền yên" cho từng sản phẩm.

④ Hãy hiển thị tổng thành tiền theo dạng "Tổng: X yên".

JavaScript / TypeScript Editor

Chạy code để xem đầu ra

Giá trị không đổi cho mọi vận đơn — hằng số static

Giới hạn trọng lượng 30 kg được dùng cả trong phần hướng dẫn của form nhập liệu (hiển thị trước khi tạo vận đơn) lẫn trong phép tính trọng lượng còn lại của từng vận đơn. Nếu đặt giới hạn này thành field của instance, bạn sẽ phải tạo một vận đơn trống, chưa có người nhận và trọng lượng, chỉ để hiển thị hướng dẫn trên form.

Hằng số static (static field giữ một giá trị không thay đổi trong suốt chương trình) được đặt tên toàn chữ in hoa, các từ nối bằng _, như static MAX_WEIGHT_KG = 30;. Dù ở ngoài class hay trong phương thức, bạn đều đọc nó qua tên class: Parcel.MAX_WEIGHT_KG.

class Parcel {
  // Giới hạn dùng chung cho mọi vận đơn, đặt một lần trên class bằng static
  static MAX_WEIGHT_KG = 30;

  constructor(weightKg) {
    this.weightKg = weightKg;
  }

  // Còn cho thêm được bao nhiêu kg trước khi chạm giới hạn
  remainingKg() {
    return Parcel.MAX_WEIGHT_KG - this.weightKg;
    // Nếu viết this.MAX_WEIGHT_KG - this.weightKg thì sẽ trả về NaN
  }
}

// Đọc được ngay cả khi chưa có instance nào
console.log(`Tối đa ${Parcel.MAX_WEIGHT_KG} kg mỗi kiện`);   // Tối đa 30 kg mỗi kiện
const parcel = new Parcel(12);
console.log(parcel.remainingKg());                           // 18
Kết quả phụ thuộc vào cách đọc giới hạn
static MAX_WEIGHT_KG= 30Hướng dẫn form:Parcel.MAX_WEIGHT_KGTrong remainingKg:Parcel.MAX_WEIGHT_KGTrong remainingKg:this.MAX_WEIGHT_KGĐọc được 30 khichưa có instanceTính30 - 12Instance không cónên là undefinedHiển thị "Tối đa30 kg mỗi kiện"Trả về 18undefined - 12trả về NaN
Cột trái và cột giữa đọc cùng một giá trị 30, dù có instance hay không. Chỉ cột phải, nơi đọc từ this, mới ra NaN.

Cột phải ra NaNthis bên trong phương thức trỏ tới instance, mà hằng số static thì không nằm trên instance. undefined - 12 không báo lỗi, nên bạn chỉ phát hiện ra khi thấy trọng lượng còn lại hiển thị sai.

Tên viết hoa vẫn không ngăn được phép gán

Tên viết hoa chỉ là một quy ước, mang ý nghĩa "giá trị không thay đổi"; nếu bạn gán Parcel.MAX_WEIGHT_KG = 50; thì giá trị vẫn bị thay đổi. Nếu chỉ viết getter như static get MAX_WEIGHT_KG() { return 30; }, phép gán sẽ báo lỗi TypeError (getter được giới thiệu ở bài tiếp theo).

Hãy kiểm tra tên người dùng theo quy tắc về số ký tự. registeredNames đã được khai báo sẵn.

① Hãy định nghĩa Account với giới hạn số ký tự tối thiểu 3 và tối đa 12, dùng chung cho mọi tài khoản.

② Hãy thêm phương thức isValidName cho biết độ dài tên có nằm trong khoảng cho phép hay không.

③ Hãy tạo Account cho mọi tên và hiển thị "tên: hợp lệ" hoặc "tên: không hợp lệ".

④ Giả sử ở một chỗ khác trong code có một phép gán lỡ đặt số ký tự tối đa thành 20. Hãy thêm phép gán đó, rồi hiển thị lại theo đúng dạng ở ③.

JavaScript / TypeScript Editor

Chạy code để xem đầu ra

Quyết định nơi đặt giá trị — static hay instance

Khi đã biết static, bạn có thể thấy tiện nếu đặt cả giá trị như người nhận lên class để đọc ở một chỗ duy nhất qua Parcel.recipient. Khi thử nhanh với chỉ một vận đơn, dù đặt ở static hay ở this thì người nhận hiển thị vẫn như nhau, nên bạn sẽ không nhận ra sự khác biệt.

Hãy quyết định nơi đặt giá trị dựa vào việc nó có khác nhau ở từng mục hay không. Giá trị đặt vào this tồn tại riêng trong từng instance được tạo bằng new, còn giá trị static chỉ có một trên class, dù bạn tạo bao nhiêu vận đơn.

// Đặt người nhận ở static
class SharedParcel {
  static recipient = "";
  constructor(recipient) { SharedParcel.recipient = recipient; }
  label() { return `Người nhận: ${SharedParcel.recipient}`; }
}

// Đặt người nhận ở instance
class Parcel {
  constructor(recipient) { this.recipient = recipient; }
  label() { return `Người nhận: ${this.recipient}`; }
}

const sharedFirst = new SharedParcel("Minh");
new SharedParcel("Linh");
console.log(sharedFirst.label());   // Người nhận: Linh

const first = new Parcel("Minh");
new Parcel("Linh");
console.log(first.label());         // Người nhận: Minh
new lần hai có đổi người nhận của vận đơn đầu không?
Người nhận vàostatic recipientnew lần haigán Linhrecipient duy nhấtbị ghi đèsharedFirst.label()→ Người nhận: LinhNgười nhận vàothis.recipientnew lần hai làinstance khácrecipient vận đơnđầu vẫn là Minhfirst.label()→ Người nhận: Minh
Ở hàng trên, người nhận của vận đơn thứ hai ghi đè lên giá trị duy nhất trên class. Giá trị khác nhau ở từng mục thì đặt vào this để mỗi instance giữ riêng.

sharedFirst không có người nhận của riêng nó, và label() đọc SharedParcel.recipient tại thời điểm gọi, nên kết quả là "Linh", giá trị được gán sau cùng. Bảng dưới đây tóm tắt cách viết và cách dùng các member của vận đơn trong bài này.

Cần lưu gìCách viết và cách dùngVí dụ trong bài
Giá trị riêng mỗi mụcĐặt vào this, đọc từ instancenumber / recipient / weightKg
Phương thức đọc giá trị riêngKhông có static, gọi từ instancelabel() / remainingKg()
Một giá trị chungCó static, đọc qua tên classcount / MAX_WEIGHT_KG
Xử lý không cần instanceCó static, gọi qua tên classfromJSON(text)
QUIZ

Kiểm tra kiến thức

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

Câu 1Parcelstatic count = 0;. Đọc first.count trên instance first thì nhận được gì?

Câu 2Nếu gọi static method từ instance parcel như parcel.fromJSON(text) thì sao?

Câu 3Class đặt người nhận ở static. Nếu tạo vận đơn cho "Minh" rồi cho "Linh", label() của vận đơn đầu hiển thị người nhận nào?