Hướng dẫn oop trong javascript

Trước khi đọc bài viết này, bạn nên ôn lại kiến thức về object và prototype trong Javacript. Trước khi phân tích về OOP trong JavaScript, mình sẽ nhắc lại sơ 1 số tính chất trong OOP.

Các tính chất của OOP

Hướng dẫn oop trong javascript

Trong lập trình hướng đối tượng (OOP) có 4 tính chất là tính đóng gói (Encapsulation), tính kế thừa (Inheritance), tính đa hình (Polymorphism) và tính trừu tượng (Abstraction)

1. Encapsulation (Tính đóng gói):

Tính đóng là kỹ thuật giúp bạn che giấu được những thông tin bên trong đối tượng. Mục đích chính của tính đóng gói là giúp hạn chế các lỗi khi phát triển chương trình. Tính chất này không cho phép người sử dụng các đối tượng thay đổi trạng thái nội tại của một đối tượng. Chỉ có các phương thức nội tại của đối tượng cho phép thay đổi trạng thái của nó. Việc cho phép môi trường bên ngoài tác động lên các dữ liệu nội tại của một đối tượng theo cách nào là hoàn toàn tùy thuộc vào người viết mã. Đây là tính chất đảm bảo sự toàn vẹn của đối tượng.

Các lợi ích chính mà tính đóng gói mang lại:

  • Hạn chế được các truy xuất không hợp lệ tới các thuộc tính của đối tượng.
  • Giúp cho trạng thái của đối tượng luôn đúng.
  • Giúp ẩn đi những thông tin không cần thiết về đối tượng.
  • Cho phép bạn thay đổi cấu trúc bên trong lớp mà không ảnh hưởng tới lớp khác. 

2. Inheritance(Tính kế thừa):

Kế thừa trong lập trình hướng đối tượng chính là thừa hưởng lại những thuộc tính và phương thức của một lớp. Có nghĩa là nếu lớp A kế thừa lớp B thì lớp A sẽ có những thuộc tính và phương thức của lớp B. Lớp được thừa hưởng những thuộc tính và phương thức từ lớp khác được gọi là dẫn xuất (Derived Class) hay lớp Con (Subclass) và lớp bị lớp khác kế thừa được gọi là lớp cơ sở (Base Class) hoặc lớp cha (Parent Class). 

Các lợi ích của tính kế thừa:

  • Giúp tái sử dụng lại code.
  • Tăng khả năng mở rộng của chương trình.

3. Polymorphism(Tính đa hình):

Tính đa hình thể hiện thông qua việc gửi các thông điệp (message). Việc gửi các thông điệp này có thể so sánh như việc gọi các hàm bên trong của một đối tượng. Các phương thức dùng trả lời cho một thông điệp sẽ tùy theo đối tượng mà thông điệp đó được gửi tới sẽ có phản ứng khác nhau. Người lập trình có thể định nghĩa một đặc tính (chẳng hạn thông qua tên của các phương thức) cho một loạt các đối tượng gần nhau nhưng khi thi hành thì dùng cùng một tên gọi mà sự thi hành của mỗi đối tượng sẽ tự động xảy ra tương ứng theo đặc tính của từng đối tượng mà không bị nhầm lẫn.

4. Abstraction(Tính trừu tượng):

Tính trừu tượng là một tính chất mà chỉ tập trung vào những tính năng của đối tượng và ẩn đi những thông tin không cần thiết. Tính chất này giúp bạn trọng tâm hơn vào những tính năng thay vì phải quan tâm tới cách mà nó được thực hiện. 

Trong phạm vi bài viết này, chúng ta sẽ bàn về 3 đặc tính của OOP (tính đóng gói, tính kế thừa, tính đa hình), so sánh cách hiện thực chúng trong Java và JavaScript.

OOP trong Java

Như các bạn đã biết, Java là một ngôn ngữ hướng đối tượng, do đó việc hiện thực các đặc tính OOP rất đơn giản và nhanh gọn, dễ hiểu.

1. Tính đóng gói

Tính bao đóng trong Java thể hiện bằng cách để phạm vi truy cập của các thuộc tính là private và truy xuất tới các thuộc tính này thông qua phương thức public (gọi là các setter, getter).

Ví dụ:

class Student {
   private String name;
   private int age;
   private double gpa;

   public String getName() {
      return name;
   }

   public void setName(String name) {
      this.name = name;
   }

   public int getAge() {
      return age;
   }

   public void setAge(int age) {
      this.age = age;
   }

   public double getGpa() {
      return gpa;
   }

   public void setGpa(double gpa) {
      this.gpa = gpa;
   }

}

Với cách làm này thông tin của đối tượng đã được ẩn đi, bạn chỉ có thể giao tiếp với đối tượng thông qua các phương thức. Điều này cũng giống với thực tế. Ví dụ khi bạn gặp một người lạ thì bạn không thể biết được các thuộc tính của người này (số điện thoại, sở thích, ...), kể cả khi bạn hỏi thì người này cũng chưa chắc đã trả lời cho bạn đúng sự thật (giống như phương thức không trả về giá trị thực thuộc tính mà trả về một giá trị khác).

2. Tính kế thừa

Trong Java, để kế thừa một lớp bạn dùng từ khóa extends.

Ví dụ:

class Person {
   private String name;
   private int age;

   public void setName(String name) {
      this.name = name;
   }

   public String getName() {
      return name;
   }

   public void setAge(int age) {
      this.age = age;
   }

   public int getAge() {
      return age;
   }
}

class Student extends Person {
   private double gpa;
   
   public void setGpa(double gpa) {
      this.gpa = gpa;
   }

   public double getAge() {
      return gpa;
   }
}

class Entry {
   public static void main(String[] args) {
      Student s = new Student();
      s.setName("Khoa");
      s.setAge(19);
      s.setGpa(10); 
      System.out.println("Name: " + s.getName());
      System.out.println("Age: " + s.getAge());
      System.out.println("GPA: " + s.getGpa());
   }
}

Kết quả khi chạy chương trình:

Name: Khoa
Age: 19
GPA: 10.0

Có thể thấy rằng lớp Student đã được thừa hưởng những phương thức của lớp Person.

3. Tính đa hình

Tính đa hình trong code dưới 3 hình thức: nạp chồng phương thức, ghi đè phương thức và đa hình thông qua các đối tượng đa hình(polymorphic objects).

Ví dụ về tính đa hình với nạp chồng phương thức: Phương thức cộng sẽ có các "forms" là cộng 2 số nguyên, cộng 2 số thực, cộng 3 số nguyên, v.v.. Có thể thấy cùng là phương thức cộng nhưng lại có nhiểu kiểu khác nhau nên đây chính là biểu hiện của tính đa hình.

package OOP;

class Calculator {
   public int add(int a, int b) {
      return a + b;
   }

   public double add(double a, double b) {
      return a + b;
   }

   public int add(int a, int b, int c) {
      return a + b + c;
   }
}

public class Entry {
   public static void main(String[] args) {
      Calculator s = new Calculator();
      System.out.println(s.add(1, 2));
      System.out.println(s.add(2.1, 2.4));
      System.out.println(s.add(1, 2, 3));
   }
}

Kết quả khi chạy chương trình:

3
4.5
6

OOP trong Javascript

Javascript thì khác, không như Java, chúng ta cần phải áp dụng một vài thủ thuật để thực hiện các đặc tính này.

1. Tính đóng gói

Trong Javascript, để thực hiện tính bao đóng, ta có thể tạo ra 1 Constructor Function, đóng gói toàn bộ các trường và hàm vào 1 object. Thông thường, các bạn hay khai báo như sau:

function Person(firstName, lastName) {
  this.firstName = firstName;
  this.lastName = lastName;
  this.showName = function() {
     console.log(this.firstName + ' ' + this.lastName);
  };
}

var psn1 = new Person('Khoa', 'Nguyen');

// các property khai báo vào biến this có thể bị truy xuất từ bên ngoài
// object không còn bao đóng nữa
psn1.firstName = 'changed';
console.log(ps1.firstName); // changed

Với các khai báo này, tính bao đóng không được đảm bảo. Các property có thể bị truy cập, thay đổi từ bên ngoài. Ở đây, ta phải sử dụng biến cục bộ.

function Person(firstName, lastName) {
  var fstName = firstName;
  var lstName = lastName;
  
  this.setFirstName = function(firstName) { 
      fstName = firstName; 
  };

  this.getFirstName = function() {
      return fstName; 
  };
  
  this.setLastName = function(lastName) { 
      lstName = lastName; 
  };

  this.getLastName = function() { 
      return lstName; 
  };
}

var person1 = new Person('Khoa', 'Nguyen');
console.log(person1.fstName); // Undefined, không thể truy cập được


console.log(person1.getFirstName()); // Khoa

Các biến cục bộ này chỉ có thể truy xuất trong Constructor Function, nó tương đương với các trường private trong Java.

Trong javascript, không có cách nào để tạo ra các trường protected (Chỉ có thể truy cập từ class kế thừa) như Java và C# được.

2. Tính kế thừa

Trong Javascript không có từ khóa extends cũng như class, vậy nên Prototype (and Prototype chains) là sự triển khai tính kế thừa đối tượng của Javascript.

Ví dụ: 

function Person() {
  this.firstName = 'Per';
  this.lastName = 'son';
  this.sayName = function() { return this.firstName + ' ' + this.lastName };
}

// Viết một Constructor Function khác
function SuperMan(firstName, lastName) {
  this.firstName = firstName;
  this.lastName = lastName;
}

// Ta muốn SuperMan sẽ kế thừa các thuộc tính của Person
// Sử dụng prototype để kế thừa
SuperMan.prototype = new Person();

// Tạo một object mới bằng Constructor Function 
var sm = new SuperMan('Khoa', 'Nguyen');
sm.sayName(); // Khoa Nguyen. Hàm này kế thừa từ prototype của Person

Khi trình thông dịch JS kiểm tra thuộc tính đối tượng định nghĩa cho nó, trước tiên nó kiểm kiểm tra object trước. Nếu object không có thuộc tính được định nghĩa, nó sẽ kiểm tra prototype của đối tượng với cùng thuộc tính, nếu nó được tìm thấy, nó sẽ trả về thuộc tính đó. Nó khác với OOP trong Java là prototype object có thể truy cập vào đối tượng tạo ra trước vào sau bất kể khi nào có sự thay đổi nào trên prototype

function Bread() {}; // constructor function
let brownBread = new Bread(); // object of type "Bread"
let sodaBread = new Bread(); // object of type "Bread"
Bread.prototype.toast = function() {
        console.log('I am toasting!');
    }; // set the function on a toast property on the prototype
// inherited prototype is accessible!
brownBread.toast(); // I am toasting!
sodaBread.toast(); // I am toasting!

3. Tính đa hình và trừu tượng

Đối với tính đa hình và tính trừu tượng, việc áp dụng 2 tính chất này trong Javascript là không rõ ràng. Do đó mình sẽ không trình bày trong bài viết này

Việc áp dụng lập trình hướng đối tượng vào JavaScript là tương đối khó. Tuy nhiên, nếu bạn nắm vững những kiến thức cơ bản mà mình đã trình bày trên đây, thì mình tin chắc rằng bạn sẽ dễ dàng tìm hiểu thêm và áp dụng lập trình hướng đối tượng trong JavaScript.

Tạm kết

Như vậy trong bài viết này, chúng ta đã cùng tìm hiểu về OOP trong javascript. Bạn thấy thế nào về JS, hãy đưa ra những ý kiến trong quá trình sử dụng js nhé. Nếu các bạn thấy bài viết hữu ích hãy rate 5* và share cho mọi người tham khảo!

Hãy để lại comment để mình có thể hoàn thiện bản thân hơn trong tương lai. Cám ơn các bạn!