賈紅健
摘 要 在很多編程者的心目中,JavaScript作為一種函數(shù)式腳本語(yǔ)言長(zhǎng)期行走在面向?qū)ο笳Z(yǔ)言的邊緣,對(duì)于它是否面向?qū)ο竽@鈨煽桑疚耐ㄟ^(guò)簡(jiǎn)單的示例,回歸面向?qū)ο蟊疽猓瑥恼Z(yǔ)法角度闡述JavaScript是一種徹底的面向?qū)ο笳Z(yǔ)言以及如何應(yīng)用這種特性。
【關(guān)鍵詞】JavaScript 面向?qū)ο?封裝 繼承 多態(tài)
面向?qū)ο蟪绦蛟O(shè)計(jì)(OOP)是一種程序設(shè)計(jì)范型,同時(shí)也是一種程序開(kāi)發(fā)方法。對(duì)象是指類的實(shí)例,它將對(duì)象作為程序的基本單元,將程序和數(shù)據(jù)封裝其中,以提高軟件的重用性、靈活性和擴(kuò)展性。面向?qū)ο蟪绦蛟O(shè)計(jì)推廣了程序的靈活性和可維護(hù)性,并且在大型項(xiàng)目設(shè)計(jì)中廣為應(yīng)用。那么,JavaScript(以下簡(jiǎn)稱JS)是否是面向?qū)ο笳Z(yǔ)言?答案是:從語(yǔ)法角度來(lái)說(shuō),是。但是在實(shí)踐中相當(dāng)多的開(kāi)發(fā)者并不嚴(yán)格遵從面向?qū)ο?。面向?qū)ο笕齻€(gè)要素:封裝,繼承,多態(tài)。通常JS開(kāi)發(fā)中這三個(gè)要素并不會(huì)被完全遵守,請(qǐng)看以下例子。
//定義個(gè)Person類
function Person(id, name)
{
this.id = id; //身份證號(hào)
this.name = name;//姓名
}
//實(shí)例化一個(gè)Person對(duì)象
var user = new Person("321321xxxxxx", "Jack");
實(shí)現(xiàn)封裝了嗎?實(shí)現(xiàn)了,但是不很嚴(yán)格。盡管user變量包含了id, name兩個(gè)成員,但這兩個(gè)成員都可以被任意更改,比如代碼:user.name = “Rose”,沒(méi)有Java、c++中類似private的關(guān)鍵字來(lái)控制訪問(wèn)權(quán)限。繼承呢?JS中沒(méi)有顯式關(guān)鍵字來(lái)表示繼承,像Java中有extends、implements,C++中有”:”。至于多態(tài),是基于繼承的,沒(méi)有繼承哪來(lái)多態(tài)。所以看起來(lái)JS對(duì)面向?qū)ο蟮闹С植缓冒?,那為什么還要說(shuō)它是面向?qū)ο蟮恼Z(yǔ)言呢?下面就從面向?qū)ο笕齻€(gè)要素:封裝,繼承,多態(tài)逐條講解JS對(duì)它們的支持。
1 封裝
封裝是說(shuō),不只是讓你能用簡(jiǎn)化的視圖來(lái)看復(fù)雜的概念,同時(shí)還不能讓你看到復(fù)雜概念的任何細(xì)節(jié),你能看得到的就是你能全部得到的“代碼大全”。將一組變量放到一個(gè)對(duì)象中并不是完全的封裝,所以前文所說(shuō)示例中的封裝不嚴(yán)格,因?yàn)椴幌氡┞兜某蓡T變量還是暴露了。一般OO語(yǔ)言中成員變量、函數(shù)都至少有三個(gè)訪問(wèn)級(jí)別:public所有對(duì)象可見(jiàn);protected自身、子類可見(jiàn);private自身可見(jiàn)。JS無(wú)法支持到如此詳細(xì),僅僅支持public、private。以下示例是在JS類中定義private變量,public方法。
function Person(id, name)
{
varmId; //身份證號(hào)
varmName; //姓名
mId = id;
mName = name;
//私有函數(shù),通過(guò)身份照Id來(lái)取得生日
functiongetBirthday(){}
//讀取姓名
this.getName = function(){returnmName;}
//修改姓名,人是可以改名字的
this.setName = function(name){mName = name;}
//id沒(méi)有set方法,身份證號(hào)碼是不能改的
this.getId = function(){returnmId;}
this.print = function(){console.log("name:" + mName + " id: " + id);}
}
var user = new Person("P1", "Jack");
private成員變量使用var關(guān)鍵字聲明放在Person函數(shù)內(nèi)部,可以防止對(duì)象外部的函數(shù)直接訪問(wèn),而成員函數(shù)可以訪問(wèn),從而實(shí)現(xiàn)了private成員變量。private成員函數(shù)getBirthday也只有成員函數(shù)才能訪問(wèn),類外面是訪問(wèn)不了的。這個(gè)實(shí)現(xiàn)方法的原理是使用閉包,篇幅原因不對(duì)閉包進(jìn)行展開(kāi)講解。實(shí)現(xiàn)了private就是完成了封裝了嗎?沒(méi)有。
當(dāng)實(shí)例化一個(gè)Person對(duì)象之后,外部盡管訪問(wèn)不了private變量,但是卻可以惡意或不小心擴(kuò)展、篡改這個(gè)對(duì)象,進(jìn)而導(dǎo)致軟件缺陷。
比如這樣的代碼,getName函數(shù)將無(wú)法返回正確的結(jié)果:
var user = new Person("P1", "Jack");
user.getName = function() {return "foo";}
盡管這種情況比較少,但是當(dāng)軟件變得復(fù)雜,人員規(guī)模變大后,很可能出問(wèn)題,這是墨菲定律所決定的(墨菲定律:如果有兩種或兩種以上的方式去做某件事情,而其中一種選擇方式將導(dǎo)致災(zāi)難,則必定有人會(huì)做出這種選擇。)。
為了解決這個(gè)問(wèn)題,則要使用函數(shù)Object.freeze
var user = new Person("P1", "Jack");
Object.freeze(user); //凍結(jié)對(duì)象
user.getName = function() {return "foo";}
這個(gè)函數(shù)調(diào)用之后,后面的修改user的代碼將不起作用。不過(guò)很可惜,這個(gè)函數(shù)在IE8,或者更低的IE版本下不支持
2 繼承
繼承是OO設(shè)計(jì)中支持復(fù)用的基石,可以很方便的復(fù)用、擴(kuò)展已有功能。JS中沒(méi)有顯式支持繼承的關(guān)鍵字,但可把子類的prototye定義為父類的實(shí)例來(lái)實(shí)現(xiàn)。接上面的Person例子,定義一個(gè)子類Programmer。
function Programmer(id, name, skill)
{ //id, name 的意義和Person一樣
Person.call(this, id, name); //調(diào)用父類構(gòu)造函數(shù)
varmSkill = skill; //數(shù)組,表示技能
this.getSkill = function() {returnmSkill;}
this.addSkill = function(s) {mSkill.push(s);}
this.useSkill = function(){console.log(mSkill);}
}
//將子類的prototype指向父類的實(shí)例,否則instanceof操作將出錯(cuò)
Programmer.prototype = newPerson();
//設(shè)置constructor,否則子類的constructor將是父類的構(gòu)造函數(shù)
Programmer.prototype.constructor = Programmer;
var nerd = newProgrammer("321321aaaa", "Linus",[ "c++", "JS" ]);
這樣的操作就實(shí)現(xiàn)了繼承。但由于無(wú)法實(shí)現(xiàn)protected權(quán)限,導(dǎo)致子類無(wú)法訪問(wèn)父類的private變量。對(duì)于父類成員的訪問(wèn),子類和其他的類并沒(méi)有更多的權(quán)限。所以將父類的成員設(shè)置為public還是private,要視情況決定了。
3 多態(tài)
面向?qū)ο笾卸鄳B(tài)即意味著子類的某一功能可以有區(qū)別于父類的實(shí)現(xiàn),并且同一父類的不同子類的實(shí)現(xiàn)也可以不一樣。多態(tài)在JS中實(shí)現(xiàn)很簡(jiǎn)單,直接在子類中用同名函數(shù)重寫父類函數(shù)即可。如下所示,在Programmer類重寫print函數(shù),將skill也打印出來(lái):
this.print = function(){console.log("name: " + this.getName() + " id: " + this.getId() + " skill: " + JSON.stringify(this.getSkill()));}
4 其他元素
面向?qū)ο笾羞€有一些其他元素,如重載、靜態(tài)變量、多繼承/接口繼承以及弱類型語(yǔ)言中的鴨式辯型,此處僅做簡(jiǎn)要介紹。
重載可在JS函數(shù)內(nèi)部判斷參數(shù)個(gè)數(shù)、類型來(lái)執(zhí)行不同功能,以此實(shí)現(xiàn)重載,代碼如下:
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");}
}
靜態(tài)變量可通過(guò)在類的prototype上面定義變量來(lái)實(shí)現(xiàn),代碼如下:
Person.prototype.staticVar= "test";
多繼承/接口繼承是指子類有多個(gè)父類,兼有多個(gè)父類的功能。JS中沒(méi)有很好的辦法來(lái)實(shí)現(xiàn),但由于JS是弱類型語(yǔ)言,只要在一個(gè)對(duì)象中添加某一個(gè)類型的方法就可以冒充該類型,即鴨式辯型。
5 小結(jié)
以上討論了面向?qū)ο笕卦贘S中的實(shí)現(xiàn)。我們討論JS面向?qū)ο蟮哪康牟⒎枪膭?lì)大家編寫OO的JS代碼,而是當(dāng)你認(rèn)真考慮發(fā)現(xiàn)OO更加適合當(dāng)下的需求之后,用本文提供的方法可以寫出更健壯的JS代碼。
作者單位
中國(guó)郵政集團(tuán)公司南京分公司 江蘇省南京市 210029