Các khía cạnh lập trình hướng đối tượng trong JavaScript

Cập nhật ngày 13/01/2022

Trong bài viết về lập trình hướng đối tượng với JavaScript, mình đã so sánh ngôn ngữ lập trình dựa trên đối tượng với ngôn ngữ lập trình hướng đối tượng.

Và trong bài viết này, mình sẽ cùng tìm hiểu sâu hơn về các khía cạnh liên quan đến lập trình hướng đối tượng của ngôn ngữ lập trình JavaScript.

Lịch sử object

Có một câu nói mang đầy tính triết học như sau:

Một vấn đề phức tạp có thể được quản lý bằng cách chia nó thành những phần nhỏ độc lập với nhau.

Mỗi thành phần nhỏ ở đây chính là object. Object cung cấp những phương thức để gọi mà không cần biết nội dung bên trong phương thức như thế nào.

Sau khi xây dựng được những object hoạt động theo đúng yêu cầu, bạn có thể sử dụng chúng để giải quyết những bài toán lớn hơn một cách đơn giản.

Đây cũng chính là tư tưởng chia để trị.

Method (phương thức)

Method là một thành phần vô cùng quan trọng trong object. Đối với C++ hay Java, bạn chỉ có thể sử dụng được những method dạng public. Những method này dùng để lấy giá trị hay thay đổi thông tin các thuộc tính của object.

Trong JavaScript, method chính là một thuộc tính mà giá trị là function, ví dụ một phương thức đơn giản:

const rabbit = {};
rabbit.speak = function (line) {
  console.log("The rabbit says '" + line + "'");
};

rabbit.speak("I'm alive.");
// The rabbit says 'I'm alive.'

Thông thường, một method sẽ làm một vài thứ với object gọi nó. Để tham chiếu đến object đã gọi method, JavaScript cung cấp từ khoá this để bạn sử dụng bên trong method.

Ví dụ trên có thể thay đổi như sau:

function speak(line) {
  console.log("The " + this.type + " rabbit says '" + line + "'");
}

const whiteRabbit = { type: "white", speak: speak };
const fatRabbit = { type: "fat", speak: speak };

whiteRabbit.speak("I'm alive.");
// The white rabbit says 'I'm alive.'

fatRabbit.speak("I'm alive.");
// The fat rabbit says 'I'm alive.'

Tư tưởng sử dụng từ khoá this như này cũng được áp dụng trong C++ hay Java.

Sự tương quan với bind, call, apply

Nếu bạn chưa biết bind, call hay apply là gì thì bạn có thể tham khảo bài viết phân biệt call, apply và bind trong JavaScript.

Khi gọi 3 hàm này, tham số đầu tiên chính là giá trị của con trỏ this.

function speak(line) {
  console.log("The " + this.type + " rabbit says '" + line + "'");
}

const whiteRabbit = { type: "white" };
const fatRabbit = { type: "fat" };
const sexyRabbit = { type: "sexy" };

speak.apply(whiteRabbit, ["I'm alive."]);
// The white rabbit says 'I'm alive.'

speak.call(fatRabbit, "I'm alive.");
// The fat rabbit says 'I'm alive.'

const sexyRabbitSpeak = speak.bind(sexyRabbit, "I'm alive.");
sexyRabbitSpeak();
// The sexy rabbit says 'I'm alive.'

Prototypes

Prototype là khái niệm rất riêng của JavaScript.

Không giống như C++ hay Java, một object trong JavaScript luôn có ít nhất một thuộc tính là prototype. Và prototype cũng chính là một object.

Khi bạn truy cập vào một thuộc tính không có trong object thì JavaScript sẽ tự động tìm kiếm trong prototype.

Hãy xem ví dụ sau:

const empty = {};
console.log(empty.toString);
// function toString() { [native code] }

console.log(empty.toString());
// [object Object]

Trong ví dụ trên, mình chỉ khai báo empty là một object mà không định nghĩa thêm thuộc tính nào. Tuy nhiên, ví dụ trên chỉ ra rằng thuộc tính toString tồn tại trong object empty.

Đó là vì toString là một thuộc tính của prototype mà một object thì luôn chứa thuộc tính prototype.

const empty = {};

console.log(Object.getPrototypeOf(empty) == Object.prototype);
// true

console.log(Object.getPrototypeOf(Object.prototype));
// null

Constructors (hàm khởi tạo)

Nếu bạn đã biết về lập trình hướng đối tượng thì chắc sẽ không còn lạ gì khái niệm constructor. Trong JavaScript, hàm khởi tạo constructor sẽ chứa từ khoá this để tham chiếu tới object được tạo ra từ hàm.

Thông thường, constructor sẽ bắt đầu bằng chữ cái viết hoa - dùng để phân biệt hàm khởi tạo với các function khác.

Đối với hàm khởi tạo, bạn phải sử dụng từ khoá new đứng trước tên function để tạo ra một đối tượng mới từ hàm constructor này.

Sau đây là một ví dụ đơn giản về constructor:

function Rabbit(type) {
  this.type = type;
  this.greeting = function () {
    console.log(this.type + " rabbit" + " say Hello!");
  };
}

const blackRabbit = new Rabbit("black");
console.log(blackRabbit.type);
// black
blackRabbit.greeting();
// black rabbit say Hello!

const killerRabbit = new Rabbit("killer");
console.log(killerRabbit.type);
// killer
killerRabbit.greeting();
// killer rabbit say Hello!

Trong ví dụ trên, mỗi đối tượng được tạo ra từ hàm khởi tạo Rabbit đều có hai thuộc tính typegreeting. Tuy nhiên, bạn có thể tạo thêm thuộc tính cho object thông qua Object.prototype như sau:

function Rabbit(type) {
  this.type = type;
  this.greeting = function () {
    console.log(this.type + " rabbit" + " say Hello!");
  };
}

Rabbit.prototype.sayBye = function () {
  console.log(this.type + " rabbit" + " say GoodBye!");
};

const blackRabbit = new Rabbit("black");
blackRabbit.sayBye();
// black rabbit say GoodBye!

Ghi đè thuộc tính

Trong ví dụ trên, Rabbit chứa thuộc tính type.

Đối với mỗi object được tạo ra từ constructor Rabbit, bạn có thể thay đổi giá trị thuộc tính mà không làm ảnh hưởng tới các object khác.

function Rabbit(type) {
  this.type = type;
  this.greeting = function () {
    console.log(this.type + " rabbit" + " say Hello!");
  };
}

Rabbit.prototype.teeth = "small";

const blackRabbit = new Rabbit("black");
const killerRabbit = new Rabbit("killer");

console.log(blackRabbit.teeth); // small
console.log(killerRabbit.teeth); // small

killerRabbit.teeth = "long";
console.log(blackRabbit.teeth); // small - không thay đổi
console.log(killerRabbit.teeth); // long

Tính chất đặc trưng của lập trình hướng đối tượng

Tính đóng gói (Encapsulation)

Tính đóng gói: che giấu dữ liệu, không cho phép truy cập dữ liệu trực tiếp từ bên ngoài, mà phải thông qua các method được cung cấp.

function Person(_name) {
  let name = _name;
  this.setName = function (_name) {
    name = _name;
  };
  this.getName = function () {
    return name;
  };
}

const person = new Person("Alex");
console.log(person.name); // undefined
console.log(person.getName()); // Alex

person.setName("LP Devs");
console.log(person.getName()); // LP Devs

Tính kế thừa (inheritance)

Tính kế thừa: đối tượng con sẽ kế thừa những thuộc tính của đối tượng cha mà không cần phải định nghĩa lại.

Mặc dù JavaScript không hỗ trợ trực tiếp tính kế thừa. Tuy nhiên, bạn vẫn có thể tuỳ biến để áp dụng tính chất này trong JavaScript.

function Person(_name) {
  let name = _name;
  this.setName = function (_name) {
    name = _name;
  };
  this.getName = function () {
    return name;
  };
}

function Student(_name, _school) {
  let school = _school;
  Person.call(this, _name);
  this.setSchool = function (_school) {
    school = _school;
  };
  this.getSchool = function () {
    return school;
  };
}

const student = new Student("Alex", "HUST");
console.log(student.getName()); // Alex
console.log(student.getSchool()); // HUST

student.setSchool("NEU");
student.setName("Ronaldo");

console.log(student.getName()); // Anana
console.log(student.getSchool()); // NEU

Ngoài ra, còn hai tính chất của lập trình hướng đối tượng là: tính đa hình và tính trừu tượng.

Tuy nhiên, việc áp dụng hai tính chất này trong JavaScript là không rõ ràng. Do đó, mình sẽ không trình bày về chúng nữa.

Kết luận

Trên đây là những khía cạnh cơ bản của lập trình hướng đối tượng được áp dụng trong JavaScript. Mình có thể tóm tắt ngắn gọn lại như sau:

  • Method: sử dụng để lấy giá trị và thay đổi giá trị thuộc tính trong object.
  • Prototype: mọi object đều chứa thuộc tính prototype. Bạn có thể thay đổi, thêm thuộc tính của object dựa vào prototype.
  • Constructor: có thể tạo mới một object từ hàm khởi tạo constructor sử dụng từ khoá new.
  • Tính đóng gói: che giấu dữ liệu - không cho phép truy cập dữ liệu trực tiếp từ bên ngoài, mà phải thông qua các method được cung cấp.
  • Tính kế thừa: đối tượng con kế thừa những thuộc tính của đối tượng cha mà không cần phải định nghĩa lại.

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 thì chắc chắn 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.

Thực hành

Vector

Xây dựng constructor Vector biểu diễn một vector trong không gian hai chiều với hai tham số đầu vào là xy.

Xây dựng 2 methods cho Vector thông qua prototype là plusminus, ví dụ như sau:

console.log(new Vector(1, 2).plus(new Vector(2, 3)));
// Vector{x: 3, y: 5}

console.log(new Vector(1, 2).minus(new Vector(2, 3)));
// Vector{x: -1, y: -1}

Tham khảo tại đây.

Interface

Định nghĩa hàm logFive với đầu vào là một object. Thực hiện ghi ra log 5 phần tử đầu tiên hoặc ít hơn (nếu số phần tử thoả mãn ít hơn 5).

Triển khai object kiểu ArraySeg với đầu vào là một mảng và một object khác kiểu RangeSeq với đầu vào là 2 số nguyên biểu diễn khoảng.

// Triển khai code

logFive(new ArraySeq([1, 2]));
/*
 * 1
 * 2
 */

logFive(new RangeSeq(100, 1000));
/*
 * 100
 * 101
 * 102
 * 103
 * 104
 */

Tham khảo code tại đây.

Tham khảo

★ Nếu bạn thấy bài viết này hay thì hãy theo dõi mình trên Facebook để nhận được thông báo khi có bài viết mới nhất nhé:

Lập trình hướng đối tượng với JavaScript?
Các cách kế thừa cơ bản trong JavaScript
Chia sẻ:

Bình luận