JavaScript và lập trình hướng đối tượng [2]

Trong bài trước chúng ta đã tìm hiểu những cách thức cơ bản để tạo các đối tượng trong JavaScript. Tuy nhiên một vấn đề rắc rối cho những cách thức đó là sự phiền hà khi phải làm việc với một lô các đối tượng tương tự nhau về phương thức cũng như thuộc tính. Tạo đối tượng kiểu như vậy có thể so sánh với công việc của những người thợ làm gốm thủ công, họ nặn ra vô số cái bát, cái đĩa, các con giống nhưng rõ ràng là các sản phẩm đó thật khó có sự đồng nhất về các đặc điểm mặc dù cùng là bát hay đĩa v.v… Để giải quyết vấn đề này những người thợ làm gốm đã phát minh ra cái gọi là khuôn. Sử dụng khuôn có thể tạo ra vô số các đối tượng với những đặc tính giống hệt nhau mà không phải bận tâm để làm chúng có giống nhau hay không bởi tất cả chúng đều sinh ra từ một khuôn. Với lập trình hướng đối tượng, chúng ta có khái niệm Class (Lớp) bao gồm các đặc tả về các đối tượng như thuộc tính và phương thức. Việc mô tả lớp có thể ví như công việc tạo khuôn, sau khi đã có khuôn chi tiết về đối tượng cần tạo ta chỉ việc đưa vào quy trình đề “sản xuất” các đối tượng từ khuôn đó. JavaScript cung cấp một cách thức tạo khuôn khá thú vị đó là Prototype.

Class và prototype 

Xây dựng class trong JavaScript

Như đã nói, trong lập trình hướng đối tượng, chúng ta thường tạo những đối tượng từ các lớp (class), trong đó các object được coi như là thể hiện của một lớp. Cả hai ngôn ngữ Java và JavaScript đều cung cấp cho chúng ta từ khóa new dùng để tạo một thể hiện của một đối tượng đã được định nghĩa trước đó.

Trong Java, mọi thứ đều là object, và được phát triển từ class nguyên thủy là java.lang.Object. Nếu bạn là lập trình viên Java hẳn sẽ thấy câu lệnh sau khá quen thuộc:

——————-
MyObject myObj=new MyObject(arg1,arg2);
——————-

Với JavaScript cũng có cách khai báo rất giống như vậy:

——————-
var myObj= new MyObject(arg1, arg2);
——————-

Nhưng có một sự khác biệt lớn về bản chất đó là chúng ta hoàn toàn không định nghĩa lớp MyObject như trong Java mà thay vào đó là một hàm thật sự có cùng tên:

——————-
function MyObject(){
//do something here
}
——————-

Để minh họa cho điều này, ta xét ví dụ sau khai báo một class Animal với các thuộc tính đơn giản như name, food và phương thức đơn giản là eat:

——————-
function Animal(name, food){
//Thuộc tính
this.name=name;//Tên con vật, hoặc tên loài
this.food=food;//Loại thức ăn có thể ăn

//Phương thức
this.eat=function(something){
if(something===food){//Nếu thức ăn phù hợp
alert(‘This is very delicious’);
}else{
alert(“I don’t like this”);
}
}
}

var myAnimal = new Animal(“Cat”,”Mouse”);
myAnimal.eat(“Mouse”);// This is very delicious
——————-

Trong ví dụ trên, một lần nữa ta sử dụng từ khóa this, với cấu trúc trên thì ta có thể hiểu this là đối tượng được tạo ra sau từ khóa new.

Tất nhiên với cách khai báo như vậy mọi việc đều OK, nhưng nếu đi sâu hơn một chút chúng ta sẽ thấy: đầu tiên, cho mỗi thể hiện của class Animal mà chúng ta tạo ra, chúng ta cũng tạo ra một hàm eat() mới, điều này dẫn đến vấn đề về bộ nhớ và tốc độ xử lý nếu bạn có ý định tạo ra rất nhiều đối tượng trong chương trình từ class Animal (ví dụ tạo một đàn kiến chẳng hạn!!!); thứ nữa, khi làm việc với các DOM (Document Object Model), chúng ta sẽ gặp phải vô số vấn đề rắc rối không mong đợi. Chính vì vậy bây giờ ta hãy thử một cách khác an toàn hơn, và nó được biết đến cái tên khá lạ tai: prototype-base.

Prototype

Prototype là gì vậy? Thực ra đây là một thuộc tính sẵn có trong mọi object của JavaScript (trong JavaScrip thuộc tính cũng có thể là một đối tượng, ta sẽ đề cập chi tiết hơn về điều này sau), đây có thể coi là nét đặc trưng của JavaScript mà các ngôn ngữ hướng đối tượng khác không có. Các hàm cũng như các thuộc tính đều có thể kết hợp được với prototype. Prototype và từ khóa new có thể làm việc cùng nhau, khi một đối tượng được tạo bởi từ khóa new thì tất cả các thuộc tính và phương thức prototype đều được gắn vào đối tượng mới đó. Điều này nghe có vẻ lạ và rắc rối, nhưng khá là hiệu quả:

Ta xây dựng lại class Animal theo phong cách prototype như sau:

——————-
function Animal(name, food){
//Thuộc tính
this.name=name;
//Tên con vật, hoặc tên loài
this.food=food;
//Loại thức ăn có thể ăn
}

Animal.prototype.eat=function(something){
if(something===this.food){//Nếu thức ăn phù hợp
alert(‘This is very delicious’);
}else{
alert(“I don’t like this”);
}
}

var myAnimal = new Animal(“Cat”,”Mouse”);
myAnimal.eat(“Mouse”);// This is very delicious
——————-

Như vậy đầu tiên chúng ta vẫn khai báo class với các thuộc tính bình thường, sau đó đối với các phương thức ta chỉ việc gắn nó vào prototype như trên. Khi chúng ta tạo một thể hiện của class thì phương thức được gắn vào object đó mà không phải tạo mới, và từ khóa this sẽ đảm bảo rằng trường food là của object vừa tạo.

Trong việc thao tác với prototype, có một lưu ý là mọi object được tạo ra sau prototype thì sẽ được gắn các thuộc tính hoặc phương thức được khai báo kiểu prototype trước đó, ví dụ:

——————-
myObject.prototype.color=”red”;

var myObj1=new myObject();

myObject.prototype.color=”blue”;

myObject.prototype.sound=”boom!”;

var myObj2=new myObject();

alert(myObj1.color); //red

alert(myObj2.color); //blue

alert(myObj2.sound); //boom!

alert(myObj1.sound); //error!
——————-

Khi đó myObj1 chỉ có thuộc tính color với giá trị là red, còn myObj2 có thêm thuộc tính sound và giá trị color là blue

* Mở rộng một object trong JavaScript

Trên thực tế, cơ chế prototype còn có thể áp dụng cho các đối tượng xây dựng sẵn trong JavaScript, ta có thể dùng nó để mở rộng các đối tượng này. Một ví dụ rất hữu ích là mở rộng đối tượng Array như sau:

——————-
Array.prototype.indexof=function(obj){
var result=-1;
for(var i=0;i<this.length;i++){
if(this[i]==obj){
result=i;
break;
}
}
return result;
}

var ary=new Array();

ary=[“one”,”two”,”three”];

alert(ary.indexof(“one”))//0
——————-

Ví dụ trên, ta mở rộng Array bằng cách thêm vào một phương thức indexof tính chỉ số của một phần tử trong mảng, nếu đối tượng không có trong mảng thì trả về giá trị là -1;

Cơ chế của prototype

Một câu hỏi sẽ được đặt ra là cơ chế hoạt động của prototype như thế nào? Tại sao với prototype ta có thể giả lập khá nhiều các đặc tính hướng đối tượng? Thật ra prototype lại có một cơ chế hoạt động khá đơn giản: Mỗi khi bạn thực hiện thao tác với thuộc tính và phương thức của một đối tượng nào đó trong JavaScript, thì trình thông dịch sẽ thực hiện các bước tuần tự sau để xác định thuộc tính hay phương thức nào được thực thi:

–        Nếu thuộc tính hay phương thức của đối tượng đang xét có giá trị hoặc đã được gán giá trị thì thuộc tính hay phương thức đó được sử dụng.

–        Nếu không thì kiểm tra giá trị của thuộc tính của prototype trong cấu trúc của object.

–        Cứ tiếp tục như vậy cho đến khi tìm thấy thuộc tính phù hợp (thuộc tính đã được gán giá trị) hoặc giá trị tìm được là kiểu Object.

Chính vì vậy bất cứ khi nào muốn thêm một thuộc tính hay gắn thêm một phương thức mới cho một object bất kỳ ta chỉ việc khai báo nó như là thuộc tính của prototype.

Để thấy rõ hơn điều này ta sẽ cùng nhau tìm hiểu sâu hơn một chút về các thuộc tính và phương thức của các đối tượng cài đặt sẵn trong JavaScript là Object và Function

a) Object:

Thuộc tính:

+constructor

+prototype

Phương thức:

+hasOwnProperty()

+isPrototypeOf()

+toString()

+valueOf()

+toLocaleString()

Ở đây chúng ta chỉ cần lưu ý phương thức khá đặc biệt đó là

+hasOwnProperty();

Với hasOwnProperty() sẽ trả về giá trị true nếu object đang xét có thuộc tính nào đó nhưng không phải là thuộc tính được khai báo kiểu mở rộng prototype, ví dụ:

——————-
var myObj=new Object();

myObj.firstProperty=”xyz”;

myObj.prototype.secondProperty=”abc”;

alert(myObj.hasOwnProperty(“firstProperty”)) //true
alert(myObj.hasOwnProperty(“fdasffsdf”)) //false

alert(myObj.hasOwnProperty(“secondProperty”)) //false
——————-

b) Function

Thuộc tính:

+constructor

+prototype

+arguments

+arity

+caller

+length

Phương thức:

+apply()

+call()

+toString()

+valueOf()

Với đối tượng Function, chúng ta sẽ tìm hiểu kĩ hơn một chút bởi lẽ đây là đối tượng chính dùng trong lập trình OOP với JavaScript.

Thuộc tính constructor: thuộc tính này trả về một constructor mà từ đó object được tạo ra, ví dụ:

——————-
var myFunc=new Function();

alert(myFunc.constructor); // kết quả sẽ là Function
——————-

Lưu ý rằng thuộc tính này chỉ có trong các biến có kiểu object, nên với một biến bất kỳ muốn sử dụng constructor thì ta phải kiểm tra kiểu trước khi sử dụng, ví dụ.

——————-
if(typeof myVar ==”object”){
alert(myVar.constructor);
}
——————-

Thuộc tính arguments: arguments thực chất là một mảng chứa các tham số cho function, khi function được gán cho một đối số thì đối số này sẽ được đẩy vào mảng arguments, ta xét ví dụ sau:

——————-
function test() {
var arg=test.arguments;
for(var i=0;i<arg.length;i++){
alert(arg[i]);
}
}
test(1,”a”);// sẽ đưa ra giá trị 1 và sau đó là “a”
——————-

Để ý rằng hàm test ban đầu không khai báo tham số, nhưng khi chạy ta đã thêm hai tham số vào cho nó do vậy trong arguments sẽ có 2 giá trị.

Phương thức call() và apply(): Đây là hai phương thức của object Function, dùng để thực thi, hoặc gọi một function. Tham số đầu của hai phương thức trên thường là một object, cái mà sẽ được coi là object hiện thời và là tham số cho hàm thực thi phương thức cal () hay apply(). Trong thực tế người ta thường dùng call thay cho apply bởi chức năng không có gì khác nhau, chỉ khác ở tham số truyền vào cho hai phương thức. Để minh họa điều này ta xét ví dụ sau:

——————-
function showInfo(){
alert(this.name);
}

function myObject(name){
this.name=name;
this.show=showInfo;
}

var myObj=new myObject(“AxitDN”);

//Cách thông thường

myObj.show();

//kết quả hiện AxitDN

//Sử dụng call

showInfo.call(myObj); //kết quả hiện AxitDN
——————-

Ví dụ trên cho ta thấy rằng trong JavaScript, các hàm được sử dụng một cách khá tự do, thậm trí được sử dụng với một mục đích hoàn toàn khác so với lúc nó được tạo cho đến khi kết thúc chương trình. Không những thế với thuộc tính đặc biệt prototype càng làm cho chương trình viết bằng JavaScript trở nên vô cùng sinh động.

Như vậy, trong bài viết này các bạn đã được tìm hiểu về cách mà JavaScript mô tả các lớp để từ đó tạo nên những đối tượng thông qua Function và prototype-base. Trong bài viết tiếp theo chúng ta sẽ tìm hiểu cách phát triển các đặc tính hướng đối tượng khác với JavaScript, và hứa hẹn sẽ có nhiều điều thú vị được gợi mở!

1 – 2 – 3

(Còn nữa)

Tài nguyên:

[link] http://www.w3schools.com/js/

[eBook] Head First JavaScript by Michael Morrison

[eBook] JavaScript Bible by Danny Goodman

4 thoughts on “JavaScript và lập trình hướng đối tượng [2]

Trả lời

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *