許多Web開發人員對JavaScript的瞭解僅僅停留在簡單的表單數據操作,以及瀏覽器DOM對象的簡單操作上,以達到一些數據驗證和動態頁面的效果。所以當要實現的功能比較複雜時,寫出的代碼就顯得凌亂並且難以維護,更不用說實現一個基於JavaScript的UI框架了。事實上,JavaScript 提供了完善的機制來實現面向對象的開發思想。
本章假設讀者已經瞭解面向對象思想的基本概念,熟悉對象、類、繼承等基本術語。以此爲基礎,將重點介紹如何在 JavaScript 中使用面向對象的思想,包括實現的原理、機制和技巧。我們將使用JavaScript來實現以下面向對象特性:
¾ 對象、類;
¾ 封裝;
¾ 多態;
¾ 繼承。
1.對象
在JavaScript中創建一個對象非常簡單,我們可以使用內建的Object對象來創建一個對象:
var myObj=new Object; //創建一個名爲myObj的對象
myObj.name='sam'; //給myObj對象添加一個name屬性,其值爲sam
myObj.age=28; //給myObj對象添加一個age屬性,其值爲28
我們也可以使用JSON(JavaScript Object Notation)[1] 來創建一個對象:
var myObj={name:’sam’,age:28}
//創建一個包含name屬性值爲sam,age屬性值爲28的對象
2.類
JavaScript不同於Java、C++、C#等面嚮對象語言,它通過構造函數和原型對象(prototype)來實現類的創建。爲了創建一個類,還需要創建一個構造函數:
function Person(name,age){
this.name=name;
this.age=age;
}
這樣我們就創建了一個構造函數(類),它包含兩個屬性:name和age。
var sam=new Person('sam',28); //創建一個Person對象,name爲sam,age爲28
我們可以通過“.”運算符來訪問它的屬性:
alert(sam.name); //輸出結果爲sam
var bob=new Person('bob',30); //創建一個Person對象,name爲bob,age爲30
alert(bob.age); //輸出結果爲30
細心的讀者可能會發現,到目前爲止,我們通過函數創建的對象只是封裝了數據成員,並沒有封裝相應的方法。下面我們將在Person類中添加一個方法:
function Person(name,age){
this.name=name;
this.age=age;
this.introduceSelf=function(){
alert('I am '+name+' , I am '+age +' years old.');
}
}
var sam=new Person('sam',28);
sam.introduceSelf() //輸出結果爲:I am sam,I am 28 years old
var bob=new Person('bob',30);
bob.introduceSelf() //輸出結果爲:I am bob,I am 30 years old
但是上面這種添加方法的方式不是很好。因爲introduceSelf函數對於所有Person的實例來說,都是一樣的,我們不希望在實例級別加上一個對於所有實例來說都一樣的方法。這裏我們要引入原型對象(prototype)的介紹。
每個JavaScript構造函數都有一個內部引用指向另一個稱爲原型對象(prototype)的對象。對於這個構造函數所產生的對象實例,該構造函數的原型對象所有屬性對於它們來說都是可見的,並且是共享的。所以有時候也稱該原型對象是這些實例的原型對象。對於這些實例,對原型對象的屬性的訪問方式和對自身的屬性的訪問方式是一致的。下面我們舉例說明。
首先定義一個構造函數(類):
function Person(name,age){
this.name=name;
this.age=age;
}
然後,在函數的原型對象中添加一個屬性kind:
Person.prototype.kind='animal';
接着創建兩個Person的實例:
var p1=new Person('sam',28);
var p2=new Person('bob',30);
訪問這兩個實例的kind屬性:
alert(p1.kind); //輸出animal
alert(p2.kind); //輸出animal
通過上面的例子,我們可以看到函數的原型對象對於所有實例來說是共享的,並且屬性的訪問方式和實例本身的屬性的訪問方式完全一致。
如果修改這個屬性,會怎麼樣呢?讓我們接着往下看:
p1.kind='male';
alert(p1.kind); //輸出male
alert(p2.kind); //輸出animal
這是怎麼回事呢?原來對於原型對象的操作,讀寫是不對稱的。通過實例對屬性名的引用並不能修改原型對象屬性的值,它只是在實例本身添加了一個和原型對象屬性名一樣的屬性,並將該值賦給自身的那個屬性。而對屬性的訪問,JavaScript首先會從對象本身開始查找,如果找到則返回該屬性的值;如果沒有找到,纔會在其原型對象中查找。所以對於p1,當執行了p1.kind='male'之後,p1本身就有了一個kind屬性,所以當再次訪問p1的kind屬性時,就會直接返回p1本身kind屬性值“male”,而不是其原型對象裏的值“animal”。
在瞭解了原型對象之後,我們再回到原來的例子。我們需要在構造函數Person的原型對象上添加一個方法,這樣這個方法就會被所有的Person對象共享:
Person.prototype. introduceSelf=function(){
alert('I am '+this.name+' , I am '+this.age +' years old.');
}
var p1=new Person('sam',28);
var p2=new Person('bob',30);
p1.introduceSelf() //輸出結果爲:I am sam,I am 28 years old
p2.introduceSelf() //輸出結果爲:I am bob,I am 30 years old
3.多態
JavaScript允許我們將任意一個函數(function)分配給對象的一個屬性。當使用 obj.function 的語法調用函數時,將把函數原來定義this 的指向當前這個對象obj(就像它在構造函數中的那樣)。所以,我們可以通過定義有相同名字的方法的對象,來簡單地實現多態性(polymorphism)。
//定義一個dogSpeek函數
function dogSpeek(){
alert('I am '+this.name);
}
//定義一個Dog類
function Dog(){
this.name='dog';
this.speek= dogSpeek;//將dogSpeek 函數賦給Dog的speek屬性
}
//定義一個catSpeek函數
function catSpeek(){
alert('I am '+this.name);
}
//定義一個Cat類
function Cat(){
this.name='cat';
this.speek= catSpeek; //將catSpeek ()函數賦給Cat的speek屬性
}
var dog=new Dog;
dog.speek();//輸出“I am dog”
var cat=new Cat;
cat.speek();//輸出“I am cat”
對於同一個方法,不同類的對象就展現出不同的行爲,這樣就實現了多態性。
4.繼承
繼承是面向對象開發的又一個重要概念,在JavaScript中通過原型鏈機制來實現類的繼承,當然也可以通過將一個類的prototype拷貝到另外一個類來實現繼承。
function Base(x) // 定義一個父類
{
this.x = x;
}
Base.prototype.doIt = function() //在父類中添加一個方法
{
this.x += 1;
}
function Sub(x,y) //定義一個子類
{
Base.call(this,x); // 調用父類的構造函數(非必需)
this.y = y;
}
Sub.prototype = new Base; // 將Sub的原型對象修改爲Base的實例,這是實現繼承的關鍵一步
//因爲Sub類的原型對象是由構造函數Base產生的,所以它的constructor屬性是Base,
//我們需要把它改成Sub
Sub.prototype.constructor=Sub;
var obj = new Sub(1,1);
alert(obj.x); //輸出1
alert(obj.y); //輸出1
obj.doIt();
alert(obj.x); //輸出2
從上面的例子我們可以看到,Sub類的實例obj繼承了Base類的屬性x,以及方法doIt。
我們還可以通過拷貝父類中的方法來實現繼承。
function Inherit(superclass, subclass) {
var from = superclass.prototype; // 父類的原型對象
var to = subclass.prototype; // 子類的原型對象
for(m in from) { //搜索原型對象中的所有屬性
if (typeof from[m] != "function") continue; // 忽略非函數
to[m] = from[m]; // 拷貝方法
}
}
下面我們用這個函數來實現繼承。
function Base(x){
this.x=x;
}
Base.prototype.doIt=function(){
this.x+=1;
}
function Sub(x,y){
Base.call(this,x);
this.y=y;
}
Inherit(Base,Sub);
var obj = new Sub(1,1);
alert(obj.x); //輸出1
alert(obj.y); //輸出1
obj.doIt();
alert(obj.x); //輸出2
通過上面的方式,同樣實現了繼承。對於第二種方式,說是繼承並不嚴格,因爲它只是借用Base類中的方法,它們之間沒有真正的繼承關係。我們可以使用instanceof方法來證明這一點。
對於第一種方式:
alert(obj instanceof Base); //輸出true,說明Sub類對象是一個Base類的實例
對於第二種方式:
alert(obj instanceof Base); //輸出false,說明Sub類對象不是一個Base類的實例
總結:JavaScript不同於Java、C++、C#等基於類的面嚮對象語言,它是基於原型對象的面嚮對象語言。它通過原型對象和構造函數可以很好地實現面向對象的開發,這爲使用JavaScript開發大型的、複雜的程序提供了可能。
總結:一、封裝 創建對象就可以
二、 類 創建一個構造函數 通過new可以實例化對象 類的屬性等於一個方法可以定義方法 類的原型屬性定義爲一個方法也可以定義類的方法
三、 多態 用不同函數賦值給多個類的相同的屬性名上作爲方法 就可以用 類的實例.方法名來調用不同的實際方法實現多態.
四、 繼承 方法一、子類的原型賦值爲父類的實例對象 ,並且把構造器指向子類;
方法二、複製原型 通過一個額外方法轉換原型