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

JavaScript là ngôn ngữ lập trình hướng đối tượng không hoàn toàn, tuy không mạnh bằng Java nhưng cũng đủ để đáp ứng nhiều yêu cầu lập trình phức tạp. Trong bài viết này, các bạn sẽ thấy là có thể dùng JavaScript để tạo đối tượng và thực hiện tính kế thừa như trong Java.

Kiểu dữ liệu trong JavaScript

Trước tiên, tôi muốn nói về các kiểu dữ liệu trong JavaScript. Tuy trong JavaScript, ta không thể khai báo kiểu của biến nhưng thực tế, mỗi biến thuộc 1 trong 5 kiểu dữ liệu sơ cấp, phi đối tượng là undefined, null, boolean, number, string hay 1 trong 2 kiểu dữ liệu đối tượng là object và function. Để biết kiểu của một biến, hãy dùng toán tử typeof. Hàm sau đây cho bạn biết kiểu của từng tham số truyền đến nó:

function reportDataType(){

var s = ;

for (var i=0; i < arguments.length; i++){

s += Argument + i + is + arguments + . ;

s += Its data type is + (typeof arguments) + .n;

}

alert(s);

}

Để kiểm tra hàm trên, hãy dùng đoạn chương trình sau:

var s;

var bool = false;

var obj = new Object();

var funcObj = new reportDataType();

var func = reportDataType;

reportDataType(s, bool, 123, 123.4, “string”, null, obj, funcObj, func, reportDataType);

/* Kết quả hiển thị lần lượt là undefined, boolean, number, number, string, null, object, object, function, function. */

Hàm tự tạo trong JavaScript

Tiếp theo, chúng ta tìm hiểu về đặc điểm của hàm tự tạo trong JavaScript. Trong JavaScript, ngoại trừ 5 kiểu dữ liệu sơ cấp đã nói, tất cả đều là đối tượng, kể cả hàm. Mỗi khi bạn tạo một hàm thì cũng đồng thời tạo ra một đối tượng Function và một lớp mang tên hàm đó. Để tạo ra một biến đối tượng thuộc “lớp” đó, bạn cũng dùng từ khóa new như bình thường. Ví dụ:

var func = new reportDataType();
Hàm bạn tạo là một đối tượng nên nó có thể có thuộc tính (property), hàm chức năng (method). Hàm chức năng đầu tiên của nó là hàm tạo (constructor), chính là hàm mà bạn tạo ra. Ví dụ khi tôi định nghĩa hàm reportDataType() trên thì tôi cũng đã định nghĩa một lớp tên reportDataType với hàm tạo chính là reportDataType().

Hàm tạo dùng để khởi tạo đối tượng. Thông thường, ta sẽ khai báo các thuộc tính (và giá trị mặc định của chúng) và hàm chức năng của lớp bên trong hàm này. Tuy nhiên, để tiện lợi cho việc tạo kế thừa, ta nên thêm hàm chức năng thông qua thuộc tính prototype.

Bên trong hàm tạo và hàm chức năng của một lớp, bạn dùng biến this để chỉ biến đối tượng (instance) hiện tại của lớp. Ví dụ: Giả sử ta định nghĩa 1 lớp ClassA có 1 thuộc tính tên m1 và tạo ra 2 biến a1, a2 của lớp này.

function ClassA(){

this.m1 = 5;

}

var a1 = new ClassA();// (1)

var a2 = new ClassA();// (2)

Với câu lệnh (1), this được hiểu là đối tượng a1 trong khi với câu lệnh (2), this là a2.

Giống như mọi đối tượng khác của JavaScript, đối tượng Function cũng có thuộc tính prototype. Thông qua thuộc tính (cũng là 1 đối tượng) prototype, ta có thể thêm thuộc tính và hàm chuyên cấp lớp cho một lớp đã định nghĩa. Chính thuộc tính này giúp ta có thể lập trình hướng đối tượng, cụ thể là thực hiện sự kế thừa, trong JavaScript.

Khi bạn muốn gọi 1 thuộc tính (hay hàm chức năng) của 1 đối tượng, trình thông dịch sẽ tìm xem trong đối tượng ấy có tên thuộc tính đó không. Nếu không có, trình thông dịch sẽ tìm tiếp trong thuộc tính prototype của đối tượng. Nếu không có, trình thông dịch lại tiếp tục tìm trong thuộc tính prototype của thuộc tính prototype. Quá trình tìm kiếm kết thúc khi tìm ra thuộc tính cần gọi hay khi không còn thuộc tính prototype nào để tìm nữa.

Ngoài prototype, mọi đối tượng còn có một thuộc tính khác là constructor. Thuộc tính này trả về đối tượng Function định nghĩa lớp của đối tượng.

Trong mỗi hàm (chính xác hơn là biến đối tượng hàm) đều có một biến cục bộ (private) tên arguments được tạo ra tự động. Biến này là một mảng chứa tất cả các tham số truyền đến hàm. Nhờ có nó mà một hàm JavaScript có thể có một số lượng tham số tùy ý, không biết trước; đồng thời, cho dù bạn khai báo hàm có nhiều tham số, bạn vẫn có thể gọi hàm với số tham số bất kì.

Mọi hàm JavaScript còn có 2 hàm chức năng đặc biệt là call và apply. Chúng đều giúp ta có thể gọi 1 hàm chức năng của đối tượng này như thể nó là hàm chức năng của một đối tượng khác. Tham số đầu tiên của 2 hàm đều là đối tượng mà bạn muốn gán hàm chức năng. Tuy nhiên, nếu như đối với call, các tham số từ tham số thứ hai trở đi sẽ được truyền đến hàm chức năng cần gọi làm tham số thì với apply, tham số thứ 2 phải là 1 mảng các tham số cần truyền. Như vậy, khi số tham số cần truyền là xác định thì ta có thể dùng call, còn khi số tham số cần truyền không xác định (chẳng hạn như khi ta cần truyền toàn bộ mảng arguments) thì ta nên dùng apply.
Cuối cùng là một số ghi chú về cú pháp liên quan đến biến hàm:

Câu lệnh

Kiểu của biến f

f = new Function();

function

f = Function();

function

f = ClassA;

function

f = ClassA();

kiểu của giá trị trả về bởi câu lệnh return trong hàm ClassA()

f = new ClassA();

object

f = new function(){}

object

f = function(){}

function

 

Khác biệt cơ bản giữa biến object f và biến function f: nếu f là function thì ta có thể gọi f().

Tạo lớp, đối tượng và thực hiện kế thừa

Tạo lớp có nghĩa là định nghĩa các thành viên của lớp: hàm tạo, hàm chức năng, thuộc tính và hàm thuộc tính, hàm sự kiện. Tạo đối tượng là tạo ra biến có kiểu lớp. JavaScript phân biệt 2 loại thành viên của lớp: thành viên cấp lớp (thuộc mọi đối tượng cùng lớp), thành viên cấp đối tượng (thuộc 1 đối tượng nào đó). Trong các cú pháp sau, cú pháp định nghĩa thành viên cấp lớp sẽ bắt đầu là this hay Tên_Lớp, còn cú pháp định nghĩa thành viên cấp đối tượng bắt đầu bằng Tên_Đối_Tượng.

1- Tạo hàm tạo (constructor)

Tạo lớp bắt đầu từ tạo hàm tạo. Cú pháp như sau:

function Tên_Lớp(tham số 1, tham số 2, tham số 3, …){

//Nội dung của lớp

}

2- Tạo hàm chức năng (method)

Nếu tạo ngay trong hàm tạo thì cú pháp như sau:

this.Tên_Hàm_Chuyên = Định_Nghĩa_Hàm
Nếu tạo bên ngoài hàm tạo thì cú pháp như sau:

Tên_Lớp.prototype.Tên_Hàm_Chuyên = Định_Nghĩa_Hàm
hay

Tên_Đối_Tượng.Tên_Hàm_Chuyên = Định_Nghĩa_Hàm

Cú pháp của phần Định_Nghĩa_Hàm như sau:

Tên_Hàm
hay

function(tham số 1, tham số 2,…){

//Nội dung hàm

}

hay

Function(“tham số 1”, “tham số 2”, “tham số 3”,…,”Nội dung hàm”)

3- Tạo thuộc tính và hàm thuộc tính

– Đối với thuộc tính:

Nếu tạo ngay trong hàm tạo thì cú pháp như sau:

this.Tên_Thuộc_Tính = Giá_Trị_Mặc_Định
Nếu tạo bên ngoài hàm tạo thì cú pháp như sau:

Tên_Lớp.prototype.Tên_Thuộc_Tính = Giá_Trị_Mặc_Định
hay

Tên_Đối_Tượng.Tên_Thuộc_Tính = Giá_Trị_Mặc_Định
– Đối với hàm thuộc tính: Trong JavaScript không hỗ trợ tạo hàm thuộc tính như ở các ngôn ngữ hướng đối tượng khác nhưng ta có thể dùng mẹo sau để giả lập:

Nếu tạo ngay trong hàm tạo thì cú pháp như sau:

this.Tên_Thuộc_Tính = Định_Nghĩa_Hàm
Nếu tạo bên ngoài hàm tạo thì cú pháp như sau:

Tên_Lớp.prototype.Tên_Thuộc_Tính = Định_Nghĩa_Hàm
hay

Tên_Đối_Tượng.Tên_Thuộc_Tính = Định_Nghĩa_Hàm
Phần Định_Nghĩa_Hàm cũng giống như nêu ở mục 2. Tuy nhiên, phần Nội dung hàm thì có dạng:

if (arguments.length == 0) {

// Giả lập hàm thuộc tính loại trả về giá trị

return Giá_Trị

}

// Giả lập hàm thuộc tính loại đặt giá trị

Ngoài ra, cách thức truy cập hàm thuộc tính cũng khác cách truy cập thuộc tính bình thường. Cụ thể như sau:

.khi cần lấy giá trị:

Tên_Đối_Tượng.Tên_Thuộc_Tính()
.khi cần đặt giá trị:

Tên_Đối_Tượng.Tên_Thuộc_Tính(tham số 1,…)
4- Tạo hàm sự kiện

Việc này phức tạp hơn một chút. Đầu tiên, chúng ta cần khai báo hàm sự kiện:

Nếu tạo ngay trong hàm tạo thì cú pháp như sau:

this.Tên_Hàm_SK = new Function();
Nếu tạo bên ngoài hàm tạo thì cú pháp như sau:

Tên_Lớp.prototype.Tên_Hàm_SK = new Function();
hay

Tên_Đối_Tượng.Tên_Hàm_SK = new Function();
Kế tiếp, chúng ta định nghĩa 1 hàm chức năng sẽ gọi hàm sự kiện. Việc này giống phần 2, chỉ có điều phần Nội dung hàm sẽ có thêm lệnh gọi hàm sự kiện như sau:

if (Điều_Kiện == true) this.Tên_Hàm_SK();
Cuối cùng, chúng ta tạo một đối tượng và gán 1 hàm xử lí sự kiện vào hàm sự kiện rỗng của đối tượng này. Cú pháp như sau:

var Tên_Đối_Tượng = new Tên_Lớp();

Tên_Đối_Tượng.Tên_Hàm_SK = Định_Nghĩa_Hàm;

Trong trường hợp bạn muốn dùng cùng một hàm xử lí sự kiện cho mọi đối tượng cùng lớp thì chỉ cần dùng 1 trong 2 lệnh sau:

this.Tên_Hàm_SK = Định_Nghĩa_Hàm;

Tên_Lớp.prototype.Tên_Hàm_SK = Định_Nghĩa_Hàm;

5- Tạo đối tượng

Sau khi đã định nghĩa 1 lớp, chúng ta có thể tạo ra các biến đối tượng từ lớp đó bằng cách dùng từ khoá new:

var Tên_Đối_Tượng = new Tên_Lớp();
Tên_Lớp ở đây có thể là tên hàm tự tạo hay tên đối tượng có sẵn của JavaScript như String, Number, Date, Array,…

6- Tạo kế thừa

Để tạo lớp B kế thừa lớp A, ta dùng cú pháp sau:

B.prototype = new A();
Bây giờ thì lớp B đã là con của lớp A. Giả sử lớp A có định nghĩa một hàm h1 và lớp B muốn sửa đổi hàm này cho phù hợp nhưng vẫn giữ nguyên tên (giống như tính năng override hàm trong Java). Nếu sự sửa đổi này không cần đến kết quả trả về từ hàm h1 của lớp A thì ta chỉ việc định nghĩa 1 hàm tên h1 trong lớp B như bình thường. Trong trường hợp ngược lại, vấn đề nảy sinh là làm thế nào để gọi hàm h1 của lớp A như thể nó là hàm chức năng của lớp B? Ta làm như sau:

Khai báo một biến parent

B.parent = A.prototype;

Trong phần nội dung hàm h1 của lớp B, khi cần gọi hàm h1 của lớp A, sử dụng lệnh sau:

B.parent.Tên_Hàm_Chuyên.call(this, tham số 1,…)
hay

B.parent.Tên_Hàm_Chuyên.apply(this, mảng tham số)
Ví dụ minh họa

Cuối cùng, chúng ta xét một ví dụ đơn giản minh họa cho những phần đã đề cập ở trên. Mời các bạn tải mã nguồn (tập tin oop.html) tại đây.