Lập trình hướng đối tượng trong JavaScript

1114

Chào các bạn,

JavaScript vốn không phải là một ngôn ngữ tuân hoàn toàn theo hướng đối tượng hay hướng cấu trúc, mà nó lại thuộc dạng “nửa nạc nửa mỡ”. Đối với lập trình hướng cấu trúc trong JavaScript thì tạm thời mình chưa nói tới, nhưng lập trình hướng đối tượng trong JavaScript thì đúng là “chán”. Bởi mang tiếng là có hỗ trợ hướng đối tượng, nhưng nó lại không hỗ trợ hết các tính chất của OOP, làm cho developer cảm thấy bị lúng túng khi code OOP với JavaScript.

Qua bài viết này, chúng ta sẽ cùng nhau đi tìm hiểu về cách lập trình hướng đối tượng trong JavaScript. Để xem nó hỗ trợ và không hỗ trợ những tính chất nào của OOP, nếu không hỗ trợ thì chúng ta có cách nào để thay thế không nhé.

>> Đọc thêm: Lập trình hướng đối tượng – Hiểu cái ý đồ

1. Khởi tạo class trong JavaScript

Từ phiên bản javascript es6 hay còn được biết đến với cái tên là es2015, JavaScript đã bổ sung thêm khái niệm class, giúp developer dễ dàng triển khai code hướng đối tượng hơn. Về cơ bản, một class trong JavaScript được khai báo và sử dụng như sau:

class Person {
    say () {
        return "Hello world";
    }
}

var me = new Person();
console.log(me.say()); // Hello world

Nhìn có vẻ ổn đấy chứ, đối tượng trong JavaScript được khởi tạo thông qua từ khóa new giống như bao ngôn ngữ lập trình hướng đối tượng khác. Vậy là việc khai báo class và khởi tạo đối tượng trong JavaScript không có gì lạ lẫm cả. Tới đây mọi thứ vẫn quen thuộc.

2. Thuộc tính của đối tượng trong JavaScript

Chúng ta cùng xét ví dụ với một class Person có kèm theo thuộc tính name xem thế nào nhé.

class Person {
    constructor (name) {
        this.name = name
    }

    say () {
        return "My name is " + this.name;
    }
}

var me = new Person("Bình");
me.say (); // My name is Bình

Trông mọi thứ vẫn có vẻ ổn. JavaScript cũng hỗ trợ hàm constructor như các ngôn ngữ lập trình hướng đối tượng khác. Ấy à mà khoan, hãy để ý vào thân hàm constructor xem.

Cái gì đây?

this.name = name;

Đoạn code trên là chúng ta đang gán giá trị cho thuộc tính name với giá trị name được truyền vào thông qua hàm constructor. Vậy câu hỏi đặt ra ở đây là thuộc tính name này là public, private hay protected? (tính đóng gói trong OOP).

Câu trả lời public. Bởi vì JavaScript không hỗ trợ đầy đủ tính đóng gói trong OOP, mọi thuộc tính của đối tượng đều có thể truy xuất được từ bên ngoài nếu biết chính xác tên thuộc tính là gì, điều này giống với public trong các ngôn ngữ lập trình hướng đối tượng khác.

Mọi thứ bắt đầu có vẻ rắc rối rồi, vậy có cách nào để khai báo thuộc tính cho javascript là private hay protected không? Câu trả lời ngắn gọn là KHÔNG, code javascript “nguyên bản” thì không hỗ trợ. Tuy nhiên bạn vẫn có thể mô phỏng các thuộc tính ở dạng private, protected nhưng sẽ phức tạp – khuyên bạn không nên cố.

3. Kế thừa class trong JavaScript

Trong Javascript, một class có thể kế thừa một class khác thông qua từ khóa extends.

Đối với kế thừa, chúng ta sẽ lần lượt đi xét 3 trường hợp:

  • class con chỉ kế thừa class cha
  • class con kế thừa và ghi đè phương constructor ở class cha
  • class con kế thừa và ghi đè các phương thức thường ở class cha

3.1 Kế thừa hoàn toàn class cha

Xét ví dụ sau, class Student chỉ đơn giản là extends từ class Person

class Person {
  constructor (name) {
    this.name = name;
  }

  say () {
    return "I am a person, my name is " + this.name
  }
}

class Student extends Person {
}

var me = new Student("Bình");
console.log(me.name); // Bình
console.log(me.say()); // 'I am a person, my name is Bình'


Việc kế một class con chỉ kế thừa class cha mà không ghi đè bất kỳ phương thức nào có vẻ ổn, không có gì đáng bàn luận.

3.2 Kế thừa và ghi đè phương thức constructor ở class cha

Vẫn là ví dụ tương tự như trên, nhưng giờ ở class Student mình sẽ ghi đè phương thức constructor ở class Person xem thế nào nhé.

class Person {
  constructor (name) {
    this.name = name;
  }

  say () {
    return "I am a person, my name is " + this.name
  }
}

class Student extends Person {
  constructor (name, code) {
    this.name = name;
    this.code = code;
  }
}

Nhìn đoạn code trên có vẻ ổn, nhưng thực chất bạn sẽ nhận một lỗi thế này:

error: Uncaught TypeError: Cannot set property ‘name’ of undefined

Nguyên nhân là đối với JavaScript, nếu bạn muốn ghi đè phương thức constructor của class cha thì ở class con phải gọi hàm super().

super() là hàm tham chiếu tới phương thức ở class cha.

Ví dụ trên cần phải được đổi lại như sau:

class Person {
  constructor (name) {
    this.name = name;
  }
  say () {
    return "I am a person, my name is " + this.name
  }
}

class Student extends Person {
  constructor (name, code) {
    super(name);
    this.code = code;
  }
}

var me = new Student("Bình", "DTC-145");
console.log(me.name); // Bình
console.log(me.say()); // I am a person, my name is Bình
console.log(me.code); // DTC-145

Hai lưu ý quan trọng
– Thứ 1: Nếu trong class bạn không khai báo phương thức constructor thì mặc nó luôn có một class constructor với thân hàm rỗng. Vì vậy trong bất kỳ trường hợp nào, nếu muốn ghi đè phương thức constructor ở class cha (cho dù class cha không được bạn khai báo constructor) thì bạn vẫn phải gọi hàm super().
– Thứ 2: Hàm super() phải được gọi trước khi bạn động tới this trong thân hàm, nếu không sẽ bị lỗi tham chiếu tới class cha. Cách tốt nhất là gọi hàm super() ở ngay dòng đầu tiên của thân hàm constructor().

3.3 Kế thừa và ghi đè phương thức thường của class cha

Vẫn là ví dụ tương tự, giờ ở class Student mình sẽ thử ghi đè phương thức say() xem thế nào.

class Person {
  constructor (name) {
    this.name = name;
  }
  say () {
    return "I am a person, my name is " + this.name
  }
}

class Student extends Person {
  constructor (name, code) {
    super(name);
    this.code = code;
  }

  say () {
    return "I am student, named " + this.name;
  }
}

var me = new Student("Bình", "DTC-145");
console.log(me.name); // Bình
console.log(me.say()); // I am student, named Bình
console.log(me.code); // DTC-145

Đoạn code trên chạy ngon, với phương thức say() đã trả về “I am student...” thay vì “I am a person...” như lúc trước. Có nghĩa là việc ghi đè phương thức thường ở class cha chỉ đơn giản là tạo một phương thức khác ở class con có tên trùng với phương thức muốn ghi đè.

Ấy khoan, thế nếu muốn ghi đè, nhưng vẫn thực hiện logic ở phương thức ở class cha thì làm thế nào? Câu trả lời là chúng ta sẽ sử dụng super.methodName(), bạn lưu ý nhé là super chứ không phải super().

Ví dụ:

class ParentClass {
  say () {
    console.log('Parent say');
  }
}

class ChildClass extends ParentClass {
  say () {
    super.say();
    console.log('Child say');
  }
}

var child = new ChildClass();
child.say();

// Parent say
// Child say

Trải qua 3 trường hợp kế thừa trong JavaScript, thì mình thấy nó cũng ổn, không có điều gì đặc biệt lắm.

4. Sử dụng abstract class trong JavaScript

Thật buồn, hiện tại (2019) thì JavaScript không hỗ trợ khái niệm abstract class. Nhưng may mắn là chúng ta vẫn có thể dễ dàng mô phỏng một abstract class dựa vào các tính chất của nó mà chúng ta vẫn thường hiểu là:

  • Abstract class không thể sử dụng để khởi tạo
  • Các class extends abstract class đều phải triển khai một số phương thức cụ thể và thuộc tính do abstract class quy định.

Các bạn có thể tham khảo class AbstractPerson trong ví dụ dưới đây để biết cách mô phỏng một abstract class trong JavaScript.

class AbstractPerson {
  constructor () {
    // Không cho phép sử dụng class AbstractPerson để khởi tạo đối tượng
    if (new.target == AbstractPerson) {
      throw "AbstractPerson is abstract class.";
    }
    
    // Class kế thừa buộc phải triển khai phương thức getName()
    if (this.getName == undefined) {
      throw "Missing getName method";
    }
    
    // Class kế thừa buộc phải triển khai phương thức getAge()
    if (this.getAge == undefined) {
      throw "Missing getAge method";
    }
  }
}

new.target sẽ trả về tên class được khởi tạo sau từ khóa new. Bạn có thể tham khảo thêm ở đây.

5. Sử dụng Interface trong JavaScript

Đến abstract class mà JavaScript còn không hỗ trợ thì bạn nghĩ sao về interface. Cách tốt nhất là bạn nên quên khái niệm interface đi nếu đang lập trình với JavaScript, bởi đơn giản là nó không hỗ trợ interface và việc mô phỏng một interface (giống như mình mô phóng abstract class ở trên) quá phức tạp.

Bộ sưu tập áo thun cho dân IT, đủ các ngôn ngữ lập trình và hệ điều hành.
Click vào ảnh để xem.
QC Được tài trợ

6. Tổng kết

Trong lập trình hướng đối tượng có 4 tính chất quan trọng là: tính đóng gói, tính trừu tượng, tính đa hình, tính kế thừa. Thì với JavaScript, các tính chất trên được thể hiện ở mức độ như sau:

  • Tính đóng gói: Hỗ trợ kém
  • Tính trừu tượng: Hỗ trợ kèm
  • Tính đa hình: Tốt
  • Tính kế thừa: Tốt

Đánh giá tổng quan: 5/10 điểm. Với số điểm lưng chừng này thì bạn vẫn có thể lập trình hướng đối tượng với JavaScript được, tuy nhiên sẽ gặp khó khăn ở việc mô phỏng tính đóng gói và tính trừu tượng.

Có cách nào cải thiện khả năng hướng đối tượng của JavaScript không?

Câu trả lời là có, bạn có thể sử dụng các ngôn ngữ tiền JavaScript (các ngôn ngữ sẽ biên dịch ra code JavaScript rồi mới chạy) như TypeScript chẳng hạn. TypeScript cung cấp cú pháp hướng đối tượng trong sáng hơn JavaScript, nhưng cuối cùng TypeScript sẽ được biên dịch thành code JavaScript để chạy.

JavaScript vốn sida vậy đó, theo bạn nó có đang để theo học hay không?


Bài viết được viết dựa trên kinh nghiệm cá nhân, rất mong nhận được góp ý của các bạn.

Chúc các bạn học tập hiệu quả.