Lớp và đối tượng trong ECMAScript

Xem thêm các chuyên mục:

1- Class đầu tiên của bạn

ECMAScript 5 không có khái niệm về lớp (Class) một cách tường minh, thay vào đó nó mô phỏng một lớp dựa trên 2 khái niệm là function & prototype. Bởi vì ES5 được giới thiệu năm 2011, cho đến nay cú pháp của nó rất phổ biến, tuy nhiên chúng ta không thể phủ nhận rằng tạo ra một lớp theo cách như vậy là khó hiểu đối với các lập trình viên. Trong khi đó, các ngôn ngữ như Java, C#,.. có cách hiện đại hơn để tạo ra một lớp. Phiên bản ES6 ra đời năm 2015 đã kịp thời vá vấn đề này, nó đưa ra cú pháp hiện đại để định nghĩa một lớp.
Ở dưới đây tôi có một hình chữ nhật ( Rectangle), nó có 2 property quan trọng đó là width (chiều rộng) và height (chiều cao). Chúng ta sẽ định nghĩa một lớp có tên Rectangle để mô phỏng nó với cú pháp ES6.
Tạo một file nguồn rectangle.js:
rectangle.js
// Define a class.
class Rectangle  {

    // Một Constructor có 2 tham số.
    // (Được sử dụng để tạo ra đối tượng)
    // this.width trỏ tới property (thuộc tính) width của lớp.
    constructor (width = 5 , height = 10)  {
        this.width = width;
        this.height = height;
    }

    // Phương thức dùng để tính diện tích hình chữ nhật.
    getArea() {
        var area = this.width * this.height
        return area
    }

}

// Tạo 1 đối tượng của lớp Rectangle thông qua Constructor.
var rect = new Rectangle(3, 5);

console.log("Height "+ rect.height);
console.log("Width "+ rect.width);

// Gọi phương thức
let area = rect.getArea();
console.log("Area "+ area );
Chạy ví dụ:
Điều gì xẩy ra khi bạn tạo một đối tượng từ constructor của lớp?
Khi bạn gọi Constructor của lớp, một đối tượng mới sẽ được tạo ra, và các property của đối tượng sẽ được gán giá trị từ các tham số.
Trong ECMAScript mỗi lớp chỉ có nhiều nhất một constructor. Cũng giống như hàm, các tham số của constructor cũng có thể có giá trị mặc định. Vì vậy bạn có thể tạo đối tượng theo nhiều cách khác nhau.
// width = 3, height = 5
let rect = new Rectangle(3, 5);

// width = 15, height = 10 (Default)
let rect2 = new Rectangle(15);

// width = 5 (Default), height = 50
let rect3 = new Rectangle(undefined, 50);


// width = 5 (Default), height = 10 (Default)
let rect4 = new Rectangle();
 

2- Getter & Setter

Trước khi đưa ra khái niệm về Getter & Setter, chúng ta hãy phân tích một tình huống:
Giả sử rằng chúng ta có lớp Person, lớp này có một propertyname.
Class Person
class Person  {

    constructor(name) {
      this.name = name;
    }
}
Và bạn có thể truy cập vào các property của đối tượng, hoặc gán giá trị mới cho nó mà không gặp bất cứ một vấn đề gì.
// Create an object
let person = new Person("Tom");

// Access to property name.
console.log( person.name); // Tom

// Assign new value to property name.
person.name = "Jerry"; // !!!

console.log( person.name ); // Jerry
Việc truy cập tự do vào một property và thay đổi giá trị của nó từ bên ngoài lớp là thực sự nguy hiểm. Đôi khi bạn muốn có một property mà bên ngoài lớp không thể truy cập được vào nó, hoặc bên ngoài lớp không thể gán giá trị mới cho nó. Getter/Setter cho phép bạn tạo ra một property như vậy.
Từ khóa get đặt trước một phương thức không có tham số của lớp giúp tạo ra một property với tên là tên của phương thức. Phương thức này sẽ được gọi mỗi khi khi chương trình truy cập vào property này.
getter-example1.js
class Person  {

    constructor (name)  {
       // property: __name
       this.__name = name;
    }

    // Getter of property name
    get name()  {
       console.log("Call getter of property 'name'!!");
       return this.__name;
    }

}

// ------------ TEST -----------------

let person = new Person("Tom");

// Access to property 'name' ==> Call getter
console.log( person.name); // Tom

// Assign new value to property name.
person.name = "Jerry"; // Not Working!!!!

// Access to property 'name' ==> Call getter
console.log( person.name); // Tom
 
Từ khóa set đặt trước một phương thức có 1 tham số của lớp giúp tạo ra một property với tên là tên của phương thức. Phương thức này sẽ được gọi mỗi khi chương trình gán giá trị mới cho property này.
setter-example1.js
class Person  {

    constructor (name)  {
       // property: __name
       this.__name = name;
    }

    // Setter of property name
    set name(newName)  {
       console.log("Call setter of property 'name'!!");
       this.__name = newName;
    }

    // A method
    showInfo()  {
       console.log("Person: " + this.__name);
    }
}

// ------------ TEST -----------------

let person = new Person("Tom");

// Can not access to property 'name'
console.log( person.name); // undefined

// Set new value to property 'name' ==> Call setter
person.name = "Jerry";

person.showInfo(); // Person: Tom
Ví dụ một property với cả hai Getter & Setter:
getter-setter-example.js
class Rectangle  {

    constructor (width = 5 , height = 10)  {
        this.__width = width;
        this.height = height;
    }

    // Getter of property 'width'
    get width()  {
      return this.__width;
    }

    // Setter of property 'width'
    set width(newWidth)  {
      if(newWidth > 0) {
          this.__width = newWidth;
      } else {
          console.log("Invalid width " + newWidth);
      }
    }

}

// ------------ TEST ------------------


var rect = new Rectangle(3, 5);

console.log("Height "+ rect.height); // Height: 5
console.log("Width "+ rect.width); // Width: 3

rect.width = -100;

console.log("Height "+ rect.height); // Height: 5
console.log("Width "+ rect.width); // Width: 3


rect.width = 100;

console.log("Height "+ rect.height); // Height: 5
console.log("Width "+ rect.width); // Width: 100
Các property với tiếp đầu ngữ là 2 dấu gạch dưới ( __ ) thường được các lập trình viên quy ước với nhau rằng đừng sử dụng nó ở bên ngoài lớp. Tuy nhiên quy ước đó có thể bị ai đó phá vỡ. Và vì vậy sử dụng các property như vậy là nguy hiểm.
Nếu bạn muốn có một property thực sự riêng tư (Private) nó lên được đặt tên với tiếp đầu ngữ là dấu thăng (hashtag) ( # ). Tuy nhiên code này chỉ có thể chạy được với sự trợ giúp của Babel 7 hoặc mới hơn.
Private property
class Something {

  constructor(){

    this.#someProperty = "test";
  }

}

const instance = new Something();

console.log(instance.#someProperty); // undefined
getter-example2.js
class Person  {

    constructor (name)  {
       // Private property: #name
       this.#name = name;
    }

    // Getter of property name
    get name()  {
       console.log("Call getter of property 'name'!!");
       return this.#name;
    }

}

3- Trường tĩnh (Static Field)

Từ khóa static xuất hiện trong khai báo Getter hoặc Setter  giúp bạn định nghĩa một trường tĩnh ( static field). Bạn có thể truy cập vào các static field thông qua tên lớp.
Các trường tĩnh ( static field) có giá trị cố định (Không thể thay đổi) được gọi là các trường hằng số tĩnh ( constant static field).
static-field-example1.js
class Employee {

   constructor (fullName, age)  {
     this.fullName = fullName;
     if(age < Employee.MIN_AGE || age > Employee.MAX_AGE)  {
        throw "Invalid Age " + age;
     }
     this.age = age;
   }

   // A static field: MIN_AGE
   static get MIN_AGE() {
      return 18;
   }

   // A static field: MAX_AGE
   static get MAX_AGE() {
     if(!Employee.__MAXA)  {
        Employee.__MAXA = 60;
     }
     return Employee.__MAXA;
   }

   static set MAX_AGE(newMaxAge)  {
      Employee.__MAXA = newMaxAge;
   }

}

// ---- TEST ---------

console.log("Mininum Age Allowed: " + Employee.MIN_AGE);
console.log("Maximum Age Allowed: " + Employee.MAX_AGE);

// Set new Maximum Age:
Employee.MAX_AGE = 65;

console.log("Maximum Age Allowed: " + Employee.MAX_AGE);

let baby = new Employee("Some Baby", 1); // Error!!
 
Bạn có một cách khác để khai báo một trường tĩnh cho lớp, tuy nhiên các trường tĩnh được tạo ra bởi cách này sẽ không là một hằng số (Constant) vì giá trị của nó có thể thay đổi. Dưới đây là một ví dụ:
static-field-example2.js
class Employee {

   constructor (fullName, age)  {
     this.fullName = fullName;
     if(age < Employee.MIN_AGE || age > Employee.MAX_AGE)  {
        throw "Invalid Age " + age;
     }
     this.age = age;
   }
}

Employee.MIN_AGE = 18;
Employee.MAX_AGE = 60;

// ---- TEST ---------

console.log("Mininum Age Allowed: " + Employee.MIN_AGE);
console.log("Maximum Age Allowed: " + Employee.MAX_AGE);

// Set new Maximum Age:
Employee.MAX_AGE = 65;

console.log("Maximum Age Allowed: " + Employee.MAX_AGE);

let baby = new Employee("Some Baby", 1); // Error!!

4- Phương thức tĩnh

Từ khóa static xuất hiện trong khai báo một phương thức của lớp giúp bạn định nghĩa một phương thức tĩnh ( static method). Bạn có thể gọi phương thức tĩnh thông qua tên của lớp. Phương thức tĩnh không thể gọi thông qua đối tượng của lớp. Phương thức tĩnh thường được sử dụng như là một hàm tiện ích của ứng dụng.
point.js
class Point {

  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  // Tính khoảng cách giữa 2 điểm
  static distance( point1, point2) {
    const dx = point1.x - point2.x;
    const dy = point1.y - point2.y;

    return Math.hypot(dx, dy);
  }
}

// --- TEST ---
let point1 = new Point( 5, 10);
let point2 = new Point( 15, 20);

// Distance
let d = Point.distance(point1, point2);

console.log("Distance between 2 points: " + d);

5- Toán tử so sánh đối tượng

Trong ECMAScript, khi bạn tạo một đối tượng thông qua constructor sẽ có một thực thể thực sự được tạo ra nằm trên bộ nhớ, nó có một địa chỉ xác định.

Một phép toán gán đối tượng AA bởi một đối tượng BB không tạo ra thêm thực thể trên bộ nhớ, nó chỉ là trỏ địa chỉ của AA tới địa chỉ của BB.
Toán tử === dùng để so sánh địa chỉ 2 đối tượng trỏ đến, nó trả về true nếu cả 2 đối tượng cùng trỏ tới cùng một địa chỉ trên bộ nhớ. Toán tử !== cũng sử dụng để so sánh 2 địa chỉ của 2 đối tượng trỏ đến, nó trả về true nếu 2 đối tượng trỏ tới 2 địa chỉ khác nhau.
identify-operator-example.js
// Define a class.
class Rectangle  {
    constructor (width = 5 , height = 10)  {
        this.width = width;
        this.height = height;
    }
    getArea() {
        var area = this.width * this.height;
        return area;
    }
}

// Tạo đối tượng: r1
let r1 = new Rectangle( 20,  10);

// Tạo đối tượng: r2
let r2 = new Rectangle( 20, 10);

let r3 = r1;


let  b12  =   r1 === r2; // false
let  b13  =   r1 === r3; // true

console.log("r1 === r2 ? " + b12); // false
console.log("r1 === r3 ? " + b13); // true


var bb12  = r1 !== r2; // true
var bb13  = r1 !== r3; // false


console.log("r1 !== r2 ? " + bb12); // true
console.log("r1 !== r3 ? " + bb13); // false
Chạy ví dụ:

6- Thừa kế và đa hình

Xem thêm các chuyên mục: