Lớp là bản thiết kế cho đối tượng
Một lớp (class) mô tả hình dạng và hành vi của một loại đối tượng. Bạn định nghĩa bản thiết kế một lần, rồi tạo ra bao nhiêu instance tùy ý. Mỗi instance mang dữ liệu riêng nhưng dùng chung các phương thức.
Hình dạng cơ bản:
class User { ... } định nghĩa bản thiết kế. new User(...) tạo một instance. Mỗi instance có name và email riêng, nhưng cả hai dùng chung một phương thức greet duy nhất được định nghĩa trên lớp.
Hai instance, hai bộ dữ liệu, một bộ phương thức. Đó là toàn bộ ý tưởng.
constructor thiết lập từng instance
constructor là một phương thức đặc biệt chạy đúng một lần cho mỗi instance, ngay khi new tạo ra nó. Nhiệm vụ của nó là khởi tạo đối tượng mới, thường bằng cách chép các đối số vào this:
this bên trong constructor trỏ tới instance mới toanh đang được tạo. Gán this.x = x đặt x lên chính đối tượng đó. Mỗi lời gọi new Point(...) có this riêng, nên có x và y riêng.
Nếu bạn không viết constructor, JavaScript cho bạn một constructor rỗng miễn phí. Bạn chỉ cần viết khi thật sự có việc cần thiết lập.
new là thứ khiến cơ chế hoạt động
Gọi một lớp mà không có new sẽ ném lỗi:
const p = Point(3, 4);
// TypeError: Class constructor Point cannot be invoked without 'new'
Đó là có chủ ý. new làm bốn việc theo thứ tự:
- Tạo một đối tượng rỗng mới.
- Liên kết nó với prototype của lớp (nơi chứa các phương thức).
- Chạy
constructorvớithisgắn với đối tượng mới. - Trả về đối tượng mới.
Không có new thì không việc nào trong số đó xảy ra. Lớp bắt buộc quy tắc này để bạn không vô tình quên nó, một cải tiến thật sự so với thời trước khi có class, khi quên new sẽ âm thầm làm hỏng đối tượng toàn cục.
Phương thức dùng chung, trường thuộc riêng từng instance
Các phương thức định nghĩa trong thân lớp nằm trên prototype: chỉ một bản được mọi instance dùng chung. Trường của instance (bất cứ thứ gì bạn gán vào this) thuộc riêng từng instance, mỗi lần là một bản riêng.
Cả hai instance đều có count riêng, nên tăng một cái không ảnh hưởng tới cái kia. Nhưng a.increment và b.increment đúng nghĩa đen là cùng một hàm: lưu một lần trên prototype, được tra cứu mỗi khi bạn gọi nó trên một instance. Đó là lý do lớp mở rộng quy mô rất rẻ: một nghìn instance không tạo ra một nghìn bản sao của mỗi phương thức.
Trường của lớp
Bạn có thể khai báo trường của instance ở đầu thân lớp, bên ngoài constructor:
Các khai báo trường chạy trước thân constructor, như thể chúng được viết ở đầu constructor. Chúng hữu ích khi giá trị ban đầu không phụ thuộc vào đối số của constructor, gọn hơn so với nhồi this.count = 0; this.step = 1; vào constructor.
Khi giá trị có phụ thuộc vào đối số, hãy giữ phép gán trong constructor, nơi các đối số nằm trong phạm vi.
Getter và setter
Getter hoặc setter trông như phương thức nhưng hoạt động như truy cập thuộc tính. Bạn đọc hoặc gán mà không cần dấu ngoặc tròn, và phương thức chạy ở bên dưới:
Để ý chỗ gọi: t.fahrenheit (không có ngoặc) đọc getter, t.fahrenheit = 100 kích hoạt setter. Nhìn từ bên ngoài, fahrenheit trông như một thuộc tính bình thường, nhưng nó được tính ngay lúc đó từ celsius.
Getter rất hợp cho các giá trị suy ra. Setter rất hợp để kiểm tra hoặc chuẩn hóa giá trị khi gán. Đừng lạm dụng chúng: nếu một getter tốn nhiều công tính toán, người đọc sẽ bất ngờ khi một "truy cập thuộc tính" lại làm việc nặng.
Phương thức rút gọn, tên tính toán và this
Định nghĩa phương thức trong lớp dùng cú pháp rút gọn: không có từ khóa function, không có : giữa tên và thân:
Trả về this từ add cho phép nối chuỗi phương thức: mỗi lời gọi trả lại chính instance đó để phương thức tiếp theo chạy trên nó.
Một cái bẫy: phương thức không tự động gắn với instance của nó. Nếu bạn tách một phương thức ra và gọi nó độc lập, this sẽ bị mất:
Gọi g.hello() chạy được vì dấu . cung cấp this. Gọi fn() độc lập thì không. Nếu bạn cần một phương thức tách rời đã được gắn sẵn (trường hợp hay gặp với trình xử lý sự kiện), hãy bind nó trong constructor hoặc dùng một trường của lớp là arrow function: hello = () => \Hi, ${this.name}`;`.
Một ví dụ nhỏ hoàn chỉnh
Ghép các mảnh lại với nhau: trường, constructor, phương thức, một getter:
Đọc lướt là hiểu: lớp mô tả chính xác BankAccount là gì và làm được những gì.
Thật ra lớp là hàm
Một điều đáng ghi nhớ: class phần lớn là cú pháp tiện lợi. Bên dưới, một lớp là một hàm, cụ thể là một hàm khởi tạo, và các phương thức của nó nằm trên ClassName.prototype:
typeof User là "function". greet nằm trên User.prototype. Các instance tra cứu greet qua chuỗi prototype, cùng cơ chế đã có trong ngôn ngữ từ ngày đầu. Lớp chỉ cho bạn một cú pháp gọn gàng để thiết lập nó.
Mô hình tư duy đó sẽ có ích về sau: kế thừa, instanceof và việc gỡ lỗi chuỗi prototype đều dễ hiểu hơn khi bạn nhớ rằng lớp là một hàm với các phương thức gắn trên prototype của nó.
Tiếp theo: kế thừa
Đến giờ mỗi lớp đều đứng riêng lẻ. Phần lớn thiết kế thực tế xây dựng hệ thống phân cấp lớp: một Dog là một loại Animal, một AdminUser là một loại User. Từ khóa extends và super đảm nhận việc đó, và chúng là nội dung tiếp theo.
Câu hỏi thường gặp
Làm sao để tạo một lớp trong JavaScript?
Dùng từ khóa class, theo sau là tên lớp, rồi một khối chứa phương thức constructor và các phương thức khác. Tạo instance bằng new ClassName(...). Ví dụ: class User { constructor(name) { this.name = name; } } rồi new User('Ada').
constructor làm gì trong một lớp JavaScript?
constructor chạy một lần khi bạn gọi new ClassName(...). Nhiệm vụ của nó là thiết lập instance mới, thường là gán các đối số vào this làm thuộc tính của instance. Nếu bạn không viết constructor, JavaScript tự động cho bạn một constructor mặc định rỗng.
Lớp và hàm trong JavaScript khác nhau thế nào?
Bên dưới, một lớp là một hàm, cụ thể là một hàm khởi tạo có các phương thức gắn vào prototype. Từ khóa class phần lớn là cú pháp tiện lợi, nhưng nó bắt buộc gọi bằng new, hỗ trợ extends gọn gàng và khiến các phương thức không bị liệt kê khi duyệt. Với code mới, hãy ưu tiên class thay vì tự viết hàm khởi tạo.