Kỹ thuật mô phỏng lớp và kế thừa trong ECMAScript

1- Lớp trong Javascript

Javascript thực tế là một thế giới của các hàm và các đối tượng, ban đầu nó được thiết kế một cách đơn giản, không có khái niệm về lớp một cách rõ ràng, có lẽ những người tạo ra Javascript cũng không thể nghĩ rằng có một ngày ngôn ngữ này lại được sử dụng một cách rộng rãi đến như vậy.

Đối tượng trong Javascript là một thực thể có nhiều cặp “Khóa/Giá trị”, và bạn có thể truy cập vào các giá trị thông qua đối tượng và khóa.

object-example1.js


var tom =  {
  name: "Tom",
  country: "USA"
};
// Access:
console.log( tom.name ); // Tom
console.log( tom.country ); // USA
console.log( tom["name"] ); // Tom
console.log( tom["country"] ); // USA

Bạn có thể thêm cặp “Khóa/Giá trị” mới vào cho một đối tượng có sẵn hoặc xóa bỏ một cặp “Khóa/Giá trị” nào đó của nó.

object-example2.js


var tom =  {
  name: "Tom",
  country: "USA"
};
// Delete property - country
delete tom["country"]; // Same as: delete tom.country;
// Add property - gender
tom["gender"] = "Male"; // Same as: tom.gender = "Male"; 
// Access:
console.log( tom.name ); // Tom
console.log( tom["name"] ); // Tom

console.log( tom["country"] ); // undefined
console.log( tom.country ); // undefined

console.log( tom["gender"] ); // Male
console.log( tom.gender ); // Male

Lớp là một khái niệm hiện đại có trong các ngôn ngữ như Java, C#,… Lớp là một bản thiết kế, sử dụng bản thiết kế này giúp bạn nhanh chóng tạo ra các đối tượng có cấu trúc giống nhau. Phiên bản đầu tiên của Javascript không có khái niệm này.

Javascript ngày càng quan trọng vì vậy nó cần phải được nâng cấp, những nhà thiết kế Javascript cố gắng mô phỏng khái niệm Lớp dựa trên những khái niệm có sẵn trong Javascript. Cú pháp mô phỏng một lớp được giới thiệu trong ES3, ES5, nhưng phải đến ES6 chúng ta mới có một cú pháp hiện đại, và làm hài lòng tất cả mọi người.

Trước hết, để đơn giản hãy cùng tôi xem một cú pháp hiện đại được giới thiệu trong ECMAScript 6 để tạo ra lớp Rectangle (Hình chữ nhật), với 2 thuộc tính (property) width (chiều rộng) và height (chiều cao). Lớp này có phương thức getArea() trả về diện tích của hình chữ nhật này.


es6-class-example.js


// ECMAScript 6 class:
class Rectangle  {
  constructor (width, height)  {
      this.width = width;
      this.height = height;
  }
  getArea()  {
    return this.width * this.height;
  }
}
// ------------- TEST -------------------
var rect = new Rectangle(10, 5);
var area = rect.getArea();

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

Tạo ra một lớp con của một lớp thực sự rất dễ dàng với ES6:

es6-inheritance-example.js


class Shape  {
    constructor( x, y) {
        this.setLocation(x, y);
    }
    setLocation(x, y) {
        this.x = x;
        this.y = y;
    }
}
// Subclass:
class Circle extends Shape {
    constructor(x, y, radius) {
        // Call Shape's constructor via super
        super(x, y);
        this.radius = radius;
    }
    getArea() {
      return Math.PI * this.radius * this.radius;
    }
}
// ----- TEST ---- 
var circle = new Circle(0, 2, 5);
console.log( circle.getArea() );
 

ES6 đã giới thiệu một cú pháp hiện đại để tạo một lớp, và các lớp con, nhưng kỹ thuật thực sự không thay đổi. Cú pháp của ES6 đã che đi các khái niệm khó hiểu của Javascript khi nó cố gắng mô phỏng một lớp. Trong bài học này tôi thảo luận với bạn cách mà ES3 & ES5 đã làm để mô phỏng một lớp và mô phỏng sự thừa kế.

2- Lớp và thừa kế trong ES3

ES3 sử dụng từ khóa function để định nghĩa một “hàm tạo đối tượng”. Bạn sẽ tạo ra một đối tượng mới khi bạn gọi hàm này với toán tử new:


// ECMAScript 3 class.
function Rectangle(width, height)  {
   this.width = width;
   this.height = height;
}
// Create an Object:
var rect = new Rectangle(10, 5);

Hình dưới đây minh họa hành động được thực hiện bởi bộ máy thực thi Javascript:

  1. Đối tượng ‘rect’ được tạo ra (nó chỉ là một đối tượng thông thường, không có gì đặc biệt)
  2. Thêm thuộc tính (property) __proto__ cho đối tượng ‘rect’, đây là một thuộc tính ẩn.
  3. Biến this sẽ được trỏ tới địa chỉ của đối tượng ‘rect’.
  4. Thêm thuộc tính (property) width cho đối tượng ‘rect’.
  5. Thêm thuộc tính (property) height cho đối tượng ‘rect’.

Khái niệm prototype cũng được giới thiệu trong ES3. Chúng ta hãy xem mục đích của nó là gì?


// ECMAScript 3 class.
function Rectangle(width, height)  {
   this.width = width;
   this.height = height;
}
Rectangle.prototype.bgColor = "red";
Rectangle.prototype.borderColor = "blue";

// Create an Object:
var rect = new Rectangle(10, 5); 

console.log(rect); // Rectangle { width: 10, height: 5 }
console.log(rect.__proto__); // Rectangle { bgColor: 'red',borderColor: 'blue' }
console.log(rect.__proto__.bgColor); // red
console.log(rect.__proto__.borderColor); // blue
// (Read the explanation**)
console.log(rect.bgColor); // red
console.log(rect.borderColor); // blue
 

Điều gì xẩy ra khi bộ máy thực thi Javascript bắt gặp biểu thức myObject.myProperty?

Câu trả lời là: Nó sẽ kiểm tra xem đối tượng myObject có thuộc tính (property) myProperty hay không, nếu có nó truy cập vào thuộc tính này, ngược lại nó truy cập vào myObject.__proto__.myProperty.

es3-proto-example3.js


var rect =  {
   __proto__ : { bgColor : "red", borderColor : "blue" },
   width: 10,
   height: 5
}
console.log(rect.width); // 10
console.log(rect.__proto__.bgColor); // red
console.log(rect.bgColor); // red

new AFunction ( args ) vs AFunction. call ( anObj, args )

Thừa kế

Với ES3 bạn có thể mô phỏng một lớp thừa kế một lớp khác bằng nhiều cách khác nhau, tất nhiên nó không dễ dàng như cách bạn làm trong ES6, và nó khá khó hiểu với nhiều lập trình viên.

Để đơn giản tôi đưa ra một ví dụ về thừa kế, và phân tích nguyên tắc hoạt động của bộ máy thực thi Javascript trong trường hợp này.

  1. Lớp Animal có 2 thuộc tính (property) name & gender.
  2. Lớp Cat thừa kế từ lớp Animal, nó có 1 thuộc tính color.


es3-inheritance-example1.js


function Animal(n, g) {
    this.name = n;
    this.gender = g;
} 
function Cat(n, g, c) {
    Animal.call(this, n, g);
    this.color = c;
}
var tom = new Cat("Male", "Tom", "Black");
// Cat { gender: 'Male', name: 'Tom', color: 'Black' }
console.log(tom);


Thừa kế và vai trò của prototype ( ES3 )
es3-inheritance-example2.js


// Class: Animal
function Animal(n, g) {
    this.name = n;
    this.gender = g;
}
Animal.prototype.sleep = function()  {
    console.log("Animal sleeping..");
}
Animal.prototype.move = function()  {
    console.log("Animal moving..");
}
// Class: Cat
function Cat(n, g, c) {
    Animal.call(this, n, g); // IMPORTANT!!
    this.color = c;
}
// IMPORTANT!!
var TempFunc = function() {}; // Temporary class.
TempFunc.prototype = Animal.prototype;
Cat.prototype = new TempFunc();
// ------------------
Cat.prototype.cry = function()  {
    console.log("Meo meo");
}
// Override 'move' method of Animal.
Cat.prototype.move = function() {
    console.log("Cat moving..");
}
var tom = new Cat("Male", "Tom", "Black");
// Cat { gender: 'Male', name: 'Tom', color: 'Black' }
console.log(tom);
tom.move(); // Cat moving..
tom.sleep(); // Animal sleeping..
tom.cry(); // Meo meo

3- Lớp và thừa kế trong ES5

Object. create ( srcObject )

Phương thức Object.create(srcObject) tạo ra một đối tượng mới newObject, trong đó newObject.__proto__ là bản sao chép của srcObject.

method-object-create-example.js


var john = {
  name: "John",
  gender: "Male"
};
var other = Object.create(john); 
console.log(other.__proto__); // { name: 'John', gender: 'Male' }

Thừa kế ( ES5 + Object. create )

Sử dụng phương thức Object.create(srcObject) của ES5 giúp bạn mô phỏng lớp và sự thừa kế dễ dàng hơn so với ES3.

es5-inheritance-example1.js


// Class: Animal
function Animal(n, g) {
    this.name = n;
    this.gender = g;
}
Animal.prototype.sleep = function()  {
    console.log("Animal sleeping..");
}
Animal.prototype.move = function()  {
    console.log("Animal moving..");
}
// Class: Cat
function Cat(n, g, c) {
    Animal.call(this, n, g); // IMPORTANT!!
    this.color = c;
}
Cat.prototype = Object.create(Animal.prototype); // IMPORTANT!!
Cat.prototype.cry = function()  {
    console.log("Meo meo");
}
// Override 'move' method of Animal.
Cat.prototype.move = function() {
    console.log("Cat moving..");
}
var tom = new Cat("Male", "Tom", "Black");
// Cat { gender: 'Male', name: 'Tom', color: 'Black' }
console.log(tom);

tom.move(); // Cat moving..
tom.sleep(); // Animal sleeping..
tom.cry(); // Meo meo