[TypeScript] Tìm hiểu về Classes
Bài đăng này đã không được cập nhật trong 3 năm
TypeScript
là một ngôn ngữ mã nguồn mở miễn phí hiện đang được phát triển và bảo trì bởi Microsoft. Nó là superset
của JavaScript, với các bổ sung các tuỳ chọn kiểu tĩnh và lớp trên cơ sở lập trình hướng đối tượng cho ngôn ngữ này. Vì TypeScript là tập cha của JavaScript nên bất kì chương trình JavaScript nào đã có cũng đều là chương trình TypeScript hợp lệ. Trong bài viết này, ta hãy cùng đi tìm hiểu về Classes trong TypeScript để thấy được tính super
của nó so với JavaScript nhé.
1. Giới thiệu
TypeScript
, giống như ES6, hỗ trợ lập trình hướng đối tượng bằng cách sử dụng các lớp. Điều này trái ngược với các phiên bản JavaScript cũ hơn, vốn chỉ hỗ trợ chuỗi kế thừa dựa trên nguyên mẫu (prototype). Hỗ trợ về class trong TypeScript tương tự như trong các ngôn ngữ Java hay C#, mà trong đó, các lớp có thể kế thừa từ các lớp khác, trong khi các đối tượng được khởi tạo dưới dạng thể hiện của lớp.
Lớp trong TypeScript được khai báo thêm các chú thích kiểu dữ liệu vào các thuộc tính và phương thức của lớp. Ví dụ sau đây là khai báo cho class Car
trong TypeScript
class Car {
name: string;
brand: string;
price: number;
constructor(name: string, brand: string, price: number) {
this.name = name;
this.brand = brand;
this.price = price;
}
getCarInfo(): string {
return `${this.name} ${this.brand} cost ${this.price}`;
}
}
Khi bạn chú thích kiểu dữ liệu cho thuộc tính, hàm khởi tạo (constructor) và phương thức, trình biên dịch TypeScript sẽ thực hiện các kiểm tra kiểu dữ liệu tương ứng.
2. Access Modifier
Chỉ thị truy cập (access modifier) thay đổi khả năng truy cập của các thuộc tính và phương thức của một lớp (class). TypeScript cung cấp cho chúng ta 4
từ khóa access modifier
public
private
protected
readonly
Để hiểu rõ hơn, cùng xét một ví dụ sau đây:
class Car {
private name: string;
protected brand: string;
readonly price: number;
constructor(name: string, brand: string, price: number) {
this.name = name;
this.brand = brand;
this.price = price;
}
public getCarInfo(): string {
return `${this.name} ${this.brand} cost ${this.price}`;
}
}
Public
Với public , bạn có thể thoải mái gọi thuộc tính (property) hay phương thức (method) từ bất cứ đâu, trong hay ngoài class đều được.
Nếu một thuộc tính hoặc phương thức
không được modifier
thì mặc định sẽ làpublic
Ở ví dụ trên, phương thức getInfo()
là public nên bạn hoàn toàn có thể truy cập được vào nó từ bên ngoài class
let car = new Car("E450", "Mercedes", 50000);
console.log(car.getInfo());
// E450 Mercedes cost 50000
Private
Ngược lại với public, với private thì bạn chỉ có thể truy cập từ bên trong class đó
Thuộc tính name
của class Car
được modifier là private, nên khi ta truy cập từ bên ngoài sẽ báo lỗi như hình dưới đây:
Protected
Với protected, nó chỉ cho phép truy cập từ bên trong cùng một lớp (giống private) và trong các lớp con kế thừa của nó
Thuộc tính brand
là protected nên chỉ có thể truy cập từ lớp kế thừa của nó. Ta có thể làm như sau:
class MyCar extends Car {
getMyCar(): string {
return this.brand;
}
}
let myCar = new MyCar("I8", "BMW", 45000);
console.log(myCar.getMyCar());
// BMW
Readonly
Ngoài ra, trong TypeScript, bạn còn có thêm một modifier mới là readonly
. Khi một thuộc tính có thêm readonly, có nghĩa là thuộc tính này chỉ có thể lấy được giá trị của chúng mà không thể thay đổi giá trị.
Lưu ý là ta cần phải gán giá trị cho thuộc tính có modifier là readonly thì mới khởi tạo nó hoặc gán nó cho hàm constructor, hoặc không nó sẽ báo lỗi
Trong ví dụ trên, thuộc tính price
được modifier là readonly. Chính vì vậy, ta chỉ có thể lấy giá trị ra mà không thể thay đổi giá trị của nó
3. Getter & Setter
Đối với mỗi thuộc tính:
Phương thức getter
trả về giá trị của thuộc tính. Một getter còn được gọi là một accessorPhương thức setter
cập nhật giá trị của thuộc tính. Một setter còn được gọi là một mutator
Phương thức getter bắt đầu với từ khóa get
và phương thức setter bắt đầu với từ khóa set
Cách triển khai getter và setter:
- Trước hết, cập nhật chỉ thị truy cập (access modifier) cho các thuộc tính (properties) từ
public
thànhprivate
- Sau đó, thay đổi tên của thuộc tính từ dạng
nameProperty
sang thành dạng_nameProperty
- Cuối cùng, tạo phương thức
getter
vàsetter
cho thuộc tính
Để hiểu rõ hơn, hãy cùng mình làm thử một ví dụ dưới đây nhé:
Ta tạo một lớp Person
đơn giản với 3 thuộc tính: age
, firstName
và lastName
class Person {
public age: number;
public firstName: string;
public lastName: string;
Ở đây, thuộc tính age
hoàn toàn có thể được truy cập từ bên ngoài. Giả sử ta muốn kiểm soát tính hợp lệ đầu vào do người dùng nhập để gán cho thuộc tính age
. Ta có thể kiểm tra trước khi gán như sau:
if(inputAge > 0 && inputAge <= 200) {
// Thực hiện gán giá trị cho thuộc tính age
}
Nhưng, nếu sử dụng đoạn mã này để kiểm tra ở khắp mọi nơi thì rất dư thừa và sẽ rất khó đọc, khó quản lý code. Đây chính là lúc sử dụng sự tối ưu của setter và getter. Ta sẽ có đoạn mã sử dụng getter
và setter
cho cả 3 thuộc tính age
, firstName
và lastName
:
class Person {
private _age: number;
private _firstName: string;
private _lastName: string;
public get age() {
return this._age;
}
public set age(theAge: number) {
if (theAge <= 0 || theAge >= 200) {
throw new Error("The age is invalid");
}
this._age = theAge;
}
public get firstName() {
return this._firstName;
}
public set firstName(theFirstName: string) {
if (!theFirstName) {
throw new Error("Invalid first name.");
}
this._firstName = theFirstName;
}
public get lastName() {
return this._lastName;
}
public set lastName(theLastName: string) {
if (!theLastName) {
throw new Error("Invalid last name.");
}
this._lastName = theLastName;
}
public getFullName(): string {
return `${this.firstName} ${this.lastName}`;
}
}
Lúc này, nếu ta muốn gán một giá trị không hợp lệ cho thuộc tính age
thì sẽ lỗi ngay
let person = new Person();
// person.age = 10;
person.age = 0; // Error: The age is invalid
4. Inheritance (Kế thừa) trong TypeScript
Một lớp có thể sử dụng lại các thuộc tính và phương thức của một lớp khác. Đây gọi là kế thừa (Inheritance)
Lớp kế thừa các thuộc tính và phương thức được gọi là lớp con. Và lớp có các thuộc tính và phương thức được kế thừa gọi là lớp cha.
Kế thừa cho phép ta sử dụng lại các thuộc tính và phương thức của một lớp hiện có mà không cần viết lại cho nó.
JavaScript
hỗ trợ prototype inheritance, không phải dạng kế thừa cổ điển như Java hay C#. ES6
cung cấp cú pháp class đơn giản là cú pháp kế thừa của prototype. TypeScript
hỗ trợ kế thừa giống như ES6
.
Để kể thừa một lớp, ta cũng sử dụng từ khóa quen thuộc là extends
class Person {
constructor(private firstName: string, private lastName: string) {
this.firstName = firstName;
this.lastName = lastName;
}
getFullName(): string {
return `${this.firstName} ${this.lastName}`;
}
describe(): string {
return `This is ${this.firstName} ${this.lastName}.`;
}
}
Lớp Employee kế thừa từ lớp Person
class Employee extends Person {
//..
}
let newEmployee = new Employee("Quynh", "Nhu");
console.log(newEmployee.getFullName()); // Quynh Nhu
Trong ví dụ trên, Employee
là lớp con và Person
là lớp cha
5. Constructor
Vì lớp Person
có một phương thức khởi tạo (constructor) có các thuộc tính firstName
và lastName
, ta có thể khởi tạo các thuộc tính này trong phương thức khởi tạo của lớp Employee
bằng cách gọi phương thức khởi tạo lớp cha của nó
Để gọi hàm khởi tạo của lớp cha trong hàm khởi tạo của lớp con, ta sử dụng cú pháp super()
class Employee extends Person {
constructor(firstName: string, lastName: string, private jobTitle: string) {
// call the constructor of the Person class:
super(firstName, lastName);
}
}
Đây là một thể hiện của lớp Employee
let employee = new Employee("Tung", "Hoang", "NodeJSDev");
console.log(employee);
/*
Employee {
firstName: 'Tung',
lastName: 'Hoang',
jobTitle: 'NodeJSDev'
}
*/
console.log(employee.getFullName()); // Tung Hoang
console.log(employee.describe()); // This is Tung Hoang.
6. Method Overriding
Khi ta gọi phương thức employee.describe()
trên đối tượng Employee, phương thức describe()
của lớp Person
sẽ được thực thi và hiển thị ra thông điệp theo cách mà ta đã định nghĩa trong lớp Person
Nhưng nếu ta muốn lớp Employee
có một phiên bản riêng của nó cho phương thức describe()
, ta có thể ghi đè phương thức (method overriding) trong lớp Employee
như sau:
class Employee extends Person {
constructor(firstName: string, lastName: string, private jobTitle: string) {
super(firstName, lastName);
}
describe(): string {
return super.describe() + `I'm a ${this.jobTitle}.`;
}
}
Trong phương thức describe()
, ta sử dụng cú pháp super.methodInParentClass()
Lúc này, khi ta gọi phương thức describe() trên đối tượng employee thì phương thức describe() của riêng lớp Employee được gọi đến:
let employee = new Employee("Tung", "Hoang", "NodeJSDev");
console.log(employee.describe()); // This is Tung Hoang. I'm a NodeJSDev.
7. Phương thức và thuộc tính static trong TypeScript
Thuộc tính static
Không giống như thuộc tính thông thường, thuộc tính static được chia sẻ giữa tất cả các thể hiện của một lớp
Để khai báo thuộc tính tĩnh, ta sử dụng từ khóa static
. Để truy cập thuộc tính tĩnh, ta sử dụng cú pháp className.propertyName
class Employee {
static headcount: number = 0;
constructor(
private firstName: string,
private lastName: string,
private jobTitle: string
) {
Employee.headcount++;
}
}
let tung = new Employee("Tung", "Hoang", "NodeJSDev");
let minh = new Employee("Quynh", "Nhu", "QA");
console.log(Employee.headcount); // 2
}
Trong ví dụ trên, thuộc tính tĩnh headcount
được khởi tạo bằng 0, giá trị của nó sẽ tăng lên 1 bất cứ khi nào một đối tượng mới được tạo
Phương thức static
Tương tự như thuộc tính static
class Employee {
private static headcount: number = 0;
constructor(
private firstName: string,
private lastName: string,
private jobTitle: string
) {
Employee.headcount++;
}
public static getHeadCount() {
return Employee.headcount;
}
}
let tung = new Employee("Tung", "Hoang", "NodeJSDev");
let minh = new Employee("Quynh", "Nhu", "QA");
console.log(Employee.getHeadCount); // 2
Trong ví dụ này, ta đã thay đổi chỉ thị truy cập của thuộc tính tĩnh headcount
từ public thành private để giá trị của nó không thể thay đổi bên ngoài lớp mà không tạo một đối tượng Employee mới. Sau đó, thêm phương thức static getHeadCount()
trả về giá trị của thuộc tính static headcount
8. Abstract Class
Một abstract class
(lớp trừu tượng) thường được sử dụng để định nghĩa hành vi chung cho các lớp dẫn xuất mở rộng. Không giống như một lớp thông thường, một lớp trừu tượng không thể được khởi tạo trực tiếp
Để khai báo một lớp trừu tượng, ta sử dụng từ khóa abstract
Thông thường, một abstract class chứa một hoặc nhiều phương thức trừu tượng (abstract method)
Một abstract method không chứa mã thực thi. Nó chỉ định nghĩa chữ ký của phương thức mà không bao gồm phần thân của phương thức. Một abstract method phải được triển khai trong lớp dẫn xuất
abstract class Employee {
constructor(private firstName: string, private lastName: string) {}
abstract getSalary(): number;
get fullName(): string {
return `${this.firstName} ${this.lastName}`;
}
compensationStatement(): string {
return `${this.fullName} makes ${this.getSalary()} a month.`;
}
}
Ta không thể khởi tạo abstract class
Lớp FullTimeEmployee
và Contractor
kế thừa abstract class Employee
:
class FullTimeEmployee extends Employee {
constructor(firstName: string, lastName: string, private salary: number) {
super(firstName, lastName);
}
getSalary(): number {
return this.salary;
}
}
class Contractor extends Employee {
constructor(
firstName: string,
lastName: string,
private rate: number,
private hours: number
) {
super(firstName, lastName);
}
getSalary(): number {
return this.rate * this.hours;
}
}
Thể hiện của 2 lớp FullTimeEmployee
và Contractor
let tung = new FullTimeEmployee("Tung", "Hoang", 12000);
let minh = new Contractor("Quynh", "Nhu", 100, 200);
console.log(tung.compensationStatement());
// Tung Hoang makes 12000 a month.
console.log(minh.compensationStatement());
// Quynh Nhu makes 20000 a month.
9. Kết luận
Trên đây là những kiến thức cơ bản về Classes trong TypeScript. Hy vọng bài viết này sẽ giúp bạn hiểu và sử dụng được classes trong dự án của mình, và đồng thời, sẽ giúp bạn hiểu được lý do tại sao TypeScript được gọi là superset của JavaScript. Mình cũng chỉ mới tìm hiểu về TypeScript nên có lẽ bài viết sẽ không tránh khỏi sai sót, rất mong nhận được các ý kiến đóng góp từ bạn trong phần comments dưới bài viết để chúng ta cùng học thêm được những kiến thức hay ho nhé.
Cảm ơn bạn đã đọc bài viết của mình. Happy Coding
10. Tham khảo
All rights reserved