賈紅健
摘 要 在很多編程者的心目中,JavaScript作為一種函數式腳本語言長期行走在面向對象語言的邊緣,對于它是否面向對象模棱兩可,本文通過簡單的示例,回歸面向對象本意,從語法角度闡述JavaScript是一種徹底的面向對象語言以及如何應用這種特性。
【關鍵詞】JavaScript 面向對象 封裝 繼承 多態
面向對象程序設計(OOP)是一種程序設計范型,同時也是一種程序開發方法。對象是指類的實例,它將對象作為程序的基本單元,將程序和數據封裝其中,以提高軟件的重用性、靈活性和擴展性。面向對象程序設計推廣了程序的靈活性和可維護性,并且在大型項目設計中廣為應用。那么,JavaScript(以下簡稱JS)是否是面向對象語言?答案是:從語法角度來說,是。但是在實踐中相當多的開發者并不嚴格遵從面向對象。面向對象三個要素:封裝,繼承,多態。通常JS開發中這三個要素并不會被完全遵守,請看以下例子。
//定義個Person類
function Person(id, name)
{
this.id = id; //身份證號
this.name = name;//姓名
}
//實例化一個Person對象
var user = new Person("321321xxxxxx", "Jack");
實現封裝了嗎?實現了,但是不很嚴格。盡管user變量包含了id, name兩個成員,但這兩個成員都可以被任意更改,比如代碼:user.name = “Rose”,沒有Java、c++中類似private的關鍵字來控制訪問權限。繼承呢?JS中沒有顯式關鍵字來表示繼承,像Java中有extends、implements,C++中有”:”。至于多態,是基于繼承的,沒有繼承哪來多態。所以看起來JS對面向對象的支持不好啊,那為什么還要說它是面向對象的語言呢?下面就從面向對象三個要素:封裝,繼承,多態逐條講解JS對它們的支持。
1 封裝
封裝是說,不只是讓你能用簡化的視圖來看復雜的概念,同時還不能讓你看到復雜概念的任何細節,你能看得到的就是你能全部得到的“代碼大全”。將一組變量放到一個對象中并不是完全的封裝,所以前文所說示例中的封裝不嚴格,因為不想暴露的成員變量還是暴露了。一般OO語言中成員變量、函數都至少有三個訪問級別:public所有對象可見;protected自身、子類可見;private自身可見。JS無法支持到如此詳細,僅僅支持public、private。以下示例是在JS類中定義private變量,public方法。
function Person(id, name)
{
varmId; //身份證號
varmName; //姓名
mId = id;
mName = name;
//私有函數,通過身份照Id來取得生日
functiongetBirthday(){}
//讀取姓名
this.getName = function(){returnmName;}
//修改姓名,人是可以改名字的
this.setName = function(name){mName = name;}
//id沒有set方法,身份證號碼是不能改的
this.getId = function(){returnmId;}
this.print = function(){console.log("name:" + mName + " id: " + id);}
}
var user = new Person("P1", "Jack");
private成員變量使用var關鍵字聲明放在Person函數內部,可以防止對象外部的函數直接訪問,而成員函數可以訪問,從而實現了private成員變量。private成員函數getBirthday也只有成員函數才能訪問,類外面是訪問不了的。這個實現方法的原理是使用閉包,篇幅原因不對閉包進行展開講解。實現了private就是完成了封裝了嗎?沒有。
當實例化一個Person對象之后,外部盡管訪問不了private變量,但是卻可以惡意或不小心擴展、篡改這個對象,進而導致軟件缺陷。
比如這樣的代碼,getName函數將無法返回正確的結果:
var user = new Person("P1", "Jack");
user.getName = function() {return "foo";}
盡管這種情況比較少,但是當軟件變得復雜,人員規模變大后,很可能出問題,這是墨菲定律所決定的(墨菲定律:如果有兩種或兩種以上的方式去做某件事情,而其中一種選擇方式將導致災難,則必定有人會做出這種選擇。)。
為了解決這個問題,則要使用函數Object.freeze
var user = new Person("P1", "Jack");
Object.freeze(user); //凍結對象
user.getName = function() {return "foo";}
這個函數調用之后,后面的修改user的代碼將不起作用。不過很可惜,這個函數在IE8,或者更低的IE版本下不支持
2 繼承
繼承是OO設計中支持復用的基石,可以很方便的復用、擴展已有功能。JS中沒有顯式支持繼承的關鍵字,但可把子類的prototye定義為父類的實例來實現。接上面的Person例子,定義一個子類Programmer。
function Programmer(id, name, skill)
{ //id, name 的意義和Person一樣
Person.call(this, id, name); //調用父類構造函數
varmSkill = skill; //數組,表示技能
this.getSkill = function() {returnmSkill;}
this.addSkill = function(s) {mSkill.push(s);}
this.useSkill = function(){console.log(mSkill);}
}
//將子類的prototype指向父類的實例,否則instanceof操作將出錯
Programmer.prototype = newPerson();
//設置constructor,否則子類的constructor將是父類的構造函數
Programmer.prototype.constructor = Programmer;
var nerd = newProgrammer("321321aaaa", "Linus",[ "c++", "JS" ]);
這樣的操作就實現了繼承。但由于無法實現protected權限,導致子類無法訪問父類的private變量。對于父類成員的訪問,子類和其他的類并沒有更多的權限。所以將父類的成員設置為public還是private,要視情況決定了。
3 多態
面向對象中多態即意味著子類的某一功能可以有區別于父類的實現,并且同一父類的不同子類的實現也可以不一樣。多態在JS中實現很簡單,直接在子類中用同名函數重寫父類函數即可。如下所示,在Programmer類重寫print函數,將skill也打印出來:
this.print = function(){console.log("name: " + this.getName() + " id: " + this.getId() + " skill: " + JSON.stringify(this.getSkill()));}
4 其他元素
面向對象中還有一些其他元素,如重載、靜態變量、多繼承/接口繼承以及弱類型語言中的鴨式辯型,此處僅做簡要介紹。
重載可在JS函數內部判斷參數個數、類型來執行不同功能,以此實現重載,代碼如下:
functionfoo(v)
{
if(typeof(v) == "number"){console.log(v + " is a number");}
else if (typeof(v) == "string"){console.log(v + " is a string");}
else if (typeof(v) == "boolean"){console.log(v + " is a boolean");}
}
靜態變量可通過在類的prototype上面定義變量來實現,代碼如下:
Person.prototype.staticVar= "test";
多繼承/接口繼承是指子類有多個父類,兼有多個父類的功能。JS中沒有很好的辦法來實現,但由于JS是弱類型語言,只要在一個對象中添加某一個類型的方法就可以冒充該類型,即鴨式辯型。
5 小結
以上討論了面向對象三要素在JS中的實現。我們討論JS面向對象的目的并非鼓勵大家編寫OO的JS代碼,而是當你認真考慮發現OO更加適合當下的需求之后,用本文提供的方法可以寫出更健壯的JS代碼。
作者單位
中國郵政集團公司南京分公司 江蘇省南京市 210029