基于原型繼承,動(dòng)態(tài)對(duì)象擴(kuò)展,閉包,JavaScript已經(jīng)成為當(dāng)今世界上最靈活和富有表現(xiàn)力的編程語(yǔ)言之一。
這里有一個(gè)很重要的概念需要特別指出:在JavaScript中,包括所有的函數(shù),數(shù)組,鍵值對(duì)和數(shù)據(jù)結(jié)構(gòu)都是對(duì)象。 舉個(gè)簡(jiǎn)單的例子:
var testFunc = function testFunc() {};testFunc.customP = "James";console.log(testFunc.customP);
上邊的代碼中,testFunc可以添加customP這個(gè)屬性,說(shuō)明testFunc本身就是一個(gè)對(duì)象。在JavaScript中,函數(shù)名是一個(gè)指向函數(shù)對(duì)象的指針,我們看下邊的代碼:
var testFunc = function testFunc() { console.log("Hello world");};var anotherTestFunc = testFunc;testFunc = null;anotherTestFunc();
即使把testFunc置為空,上邊的程序仍然打印了Hello world
。通過(guò)上邊的例子,我們演示了函數(shù)作為對(duì)象的證據(jù),然而JavaScript中的基本數(shù)據(jù)類型,在特定的情況下也會(huì)體現(xiàn)出對(duì)象的特性:
'tonya@example.com'.split('@')[1]; // => example.com
當(dāng)我們可以使用屬性獲取符.
來(lái)操作基本類型的時(shí)候,它就表現(xiàn)的很像對(duì)象,但我們不能給它賦值。原因是:基本類型會(huì)被臨時(shí)包裝成object,之后會(huì)立刻拋棄這個(gè)包裝,表面上看像是賦值成功了,但下次是無(wú)法訪問(wèn)之前的賦值的。
接下來(lái),我們會(huì)探討JavaScript中Object的一些問(wèn)題,這和其他面向?qū)ο蟮恼Z(yǔ)言有很大不同,我們會(huì)解釋為什么JavaScripts不是面向?qū)ο箢愋偷恼Z(yǔ)言,而是原型語(yǔ)言。
類型繼承是否應(yīng)該被淘汰
在Design Patterns: Elements of Reusable Object Oriented Software
這本書(shū)中有兩個(gè)關(guān)于面向?qū)ο笤O(shè)計(jì)程序的原則:
Program to an interface, not an implementation
面向接口編程Favor object composition over class inheritance
優(yōu)先使用組合,而非繼承
在某種意義上,上邊的第二個(gè)原則同樣遵循了第一個(gè)原則,因?yàn)槔^承把父類暴露給了子類,這樣的話,子類就被設(shè)計(jì)成了一個(gè)實(shí)現(xiàn),而不是接口了。因此我們得出一個(gè)結(jié)論,類型繼承破壞了封裝原則,并且把子類同它的祖先緊密聯(lián)系在一起。
舉一個(gè)更生動(dòng)的例子,我們可以把類型繼承比作是家具,我們用很多設(shè)計(jì)好的零部件來(lái)拼裝一個(gè)家具,如果這些零部件都符合設(shè)計(jì),那么我們有很高的機(jī)會(huì)組裝成功,如果有一些不符合要求,那么就會(huì)組裝失敗。
在程序設(shè)計(jì)中,組合就像樂(lè)高積木,大部分的零件被設(shè)計(jì)成能夠和其他零件拼接到一起,這樣,我們就能夠很靈活的進(jìn)行組裝了。
如何按照組件化的思想來(lái)設(shè)計(jì)程序,不是本篇文章的內(nèi)容,現(xiàn)在我們來(lái)看看反對(duì)繼承的理由是什么:
高耦合
繼承在面向?qū)ο蟮脑O(shè)計(jì)中的耦合性最高,子類與其祖先類緊密相連層次劃分不靈活
在真實(shí)的開(kāi)發(fā)中,往往不會(huì)出現(xiàn)單層的繼承,如果使用了多層次的繼承,那么很可能有很多繼承過(guò)來(lái)的屬性是不需要的,這樣就造成了代碼的過(guò)度重復(fù)多繼承難以理解
有時(shí)候很有必要會(huì)繼承多個(gè)父類,對(duì)于這種情況的處理跟單一繼承是不一樣的,會(huì)更復(fù)雜,需要處理沖突和不一致的情況,并且代碼也變得難以閱讀和理解脆弱的架構(gòu)
高耦合的程序,很難對(duì)某個(gè)類進(jìn)行代碼重構(gòu),這就體現(xiàn)了架構(gòu)的脆弱性大猩猩/香蕉問(wèn)題
父類中的某些屬性可能不是我們需要的,子類可以重寫(xiě)父類的屬性,但不能選擇繼承那些屬性,就像,我只需要香蕉,繼承卻給了我一個(gè)拿著香蕉的大猩猩和整片叢林
JavaScript的繼承和其他面向?qū)ο笳Z(yǔ)言的繼承有很大不同,只有我們了解了繼承的缺點(diǎn),才能更好的使用好這些特性。
Prototypes
prototype
是JavaScript中很重要的一個(gè)概念,他可以說(shuō)是對(duì)其他對(duì)象的一個(gè)模仿。又很像是一個(gè)類,你可以用他來(lái)構(gòu)建很多實(shí)例對(duì)象。但他的本質(zhì)就是一個(gè)對(duì)象。 我們可以通過(guò)prototype
做兩件事情:
訪問(wèn)一個(gè)共享的原型對(duì)象,也叫代理原型
克隆一個(gè)原型
代理原型
我們先把原型編程這一概念弄明白,在大多數(shù)的面向?qū)ο蟮恼Z(yǔ)言中,對(duì)象就跟模具一樣,我們根據(jù)模具來(lái)制造對(duì)象,在JavaScript中,不是這樣的,通過(guò)prototype
給object賦值一個(gè)原型或?qū)ο?,然后在新產(chǎn)生的對(duì)象身上做修改,也就是說(shuō)新對(duì)象獲取了原型的數(shù)據(jù)。這就是原型編程思想。
在JavaScript中,對(duì)象內(nèi)部都有一個(gè)原型對(duì)象,當(dāng)一個(gè)對(duì)象查詢某個(gè)屬性或方法的時(shí)候,JavaScript引擎首先會(huì)搜索對(duì)象本身是否答案,如果沒(méi)有,就會(huì)去它的原型對(duì)象中繼續(xù)搜索,如果沒(méi)有,再去它的原型的原型中去找,這就形成了一個(gè)原型鏈。直到Object的prototype為止。
我們看一段代碼:
if (!Object.create) { Object.create = function (o) { if (arguments.length > 1) { throw new Error('Object.create implementation' + ' only accepts the first parameter.'); } function F() {} F.prototype = o; return new F(); }; }
Object.create可用于創(chuàng)建一個(gè)對(duì)象,它的函數(shù)原型中接受兩個(gè)參數(shù),第一個(gè)參數(shù)是原型對(duì)象,表示新建的對(duì)象的原型,必填,第二個(gè)參數(shù)是屬性數(shù)組,表示新建對(duì)象的屬性。它在ES5中被引入,因此上邊的代碼是考慮到兼容問(wèn)題的。
通過(guò)上邊的代碼可以看出來(lái),其內(nèi)部創(chuàng)建了一個(gè)F構(gòu)造器,然后把原型參數(shù)通過(guò)F.prototype = o
進(jìn)行賦值,最后使用構(gòu)造器生成一個(gè)對(duì)象。因此我們得出下邊幾個(gè)結(jié)論:
使用
F.prototype = o
這樣的方法給對(duì)象的原型賦值new F()是怎么的過(guò)程?
var a = {}; a.__proto__ = F.prototype;F.call(a);
有一個(gè)很容易讓人迷惑的地方,我們先看代碼:
var a = new Object();console.log(a.prototype); // => undefinedconsole.log(a.__proto__); // => {}
在上邊我們不是解釋過(guò)了嗎?一個(gè)對(duì)象內(nèi)部默認(rèn)會(huì)指向一個(gè)原型,但是為什么上邊第二行代碼打印的結(jié)果是undefined呢?
這就引出了__proto__的概念,我覺(jué)得這篇文章寫(xiě)的不錯(cuò)。上邊的代碼輸出為undefined,但是__proto__卻有值,說(shuō)明每個(gè)對(duì)象內(nèi)部真正創(chuàng)建的原型是__proto__,而prototype只起到了輔助的作用,完全可以把他當(dāng)做一個(gè)普通的屬性來(lái)看待。當(dāng)使用Object.create方法創(chuàng)建對(duì)象的時(shí)候,參數(shù)中的原型會(huì)賦值給新對(duì)象的__proto__而不是而prototype。
再來(lái)看段代碼:
var switchProto = { isOn: function isOn() { return this.state; }, toggle: function toggle() { this.state = !this.state; return this; }, meta: { name: "James" }, state: false}, switch1 = Object.create(switchProto), switch2 = Object.create(switchProto);var state1 = switch1.toggle().isOn();console.log(state1);var state2 = switch2.isOn();console.log(state2);console.log(switchProto.isOn());/*當(dāng)改變一個(gè)對(duì)象或者數(shù)組中的屬性時(shí),會(huì)有影響,這說(shuō)明了這兩個(gè)賦值的方式不是copy。*/switch1.meta.name = "Bond";console.log(switch2.meta.name);switch2.meta = { name: "zhangsan"};console.log(switch1.meta.name);
上邊代碼中的switch1和switch2都指向了同一個(gè)原型,當(dāng)執(zhí)行完var state1 = switch1.toggle().isOn();
后,state1的結(jié)果為true,而state2為false,這也正好驗(yàn)證了上邊解釋的JavaScript尋找屬性或方法的原理。查找會(huì)使用原型鏈,賦值則不一樣,如果沒(méi)有該屬性,直接在對(duì)象內(nèi)部創(chuàng)建該屬性,這時(shí)候跟原型沒(méi)關(guān)系。
如果修改原型中的對(duì)象或數(shù)組時(shí),需要特別注意,會(huì)對(duì)原型產(chǎn)生副作用,但是對(duì)對(duì)象或數(shù)組直接賦值不會(huì)產(chǎn)生影響。因此在原型中使用對(duì)象或數(shù)組時(shí),要十分小心。
原型克隆
原型編程也是有缺點(diǎn)的,如果兩個(gè)對(duì)象共享了同一原型,那么更改原型內(nèi)容的話會(huì)影響到其他的對(duì)象,我們看一個(gè)例子:
var testPrototype = { name: "James", obj: { objName: "objName" }};var b = Object.create(testPrototype);var c = Object.create(testPrototype);b.obj.objName = "Test";console.log(c.obj.objName); // => Test
上邊的代碼演示了共享數(shù)據(jù)造成的問(wèn)題,只有理解了如何觸發(fā)這些問(wèn)題,才能更好的避免錯(cuò)誤的發(fā)生。我們解決上邊出現(xiàn)的問(wèn)題的思路就是采用值拷貝,復(fù)制一份數(shù)據(jù)。
. extend()
方法在jQuery和Underscore中都存在,它的作用就是實(shí)現(xiàn)原型的拷貝。我們看看Underscore內(nèi)部的實(shí)現(xiàn)方法:
_.extend = function(obj) { each(slice.call(arguments, 1), function(source) { for (var prop in source) { obj[prop] = source[prop]; } }); return obj; };
each函數(shù)會(huì)取出對(duì)象中的每一個(gè)屬性,然后賦值給source,最終把所有的屬性賦值給了obj。我們就不做其他演示了,這種通過(guò)遍歷屬性,然后賦值的方法原理是直接屬性賦值,因此我們說(shuō)這種方式?jīng)]有使用原型繼承。
這種deepCopy的思想在不同語(yǔ)言中還是比較重要的一個(gè)思想,在JavaScript中,我們應(yīng)該使用代理原型來(lái)共享那些公共的屬性,使用原型拷貝來(lái)操縱獨(dú)享的數(shù)據(jù),這是一條基本的編程原則。
The Flyweight Pattern(享元模式)
假如有一組屬性和方法是被很多實(shí)例對(duì)象共享的,把他們?cè)O(shè)計(jì)成可復(fù)用的模式就是享元模式,相對(duì)于給每一個(gè)實(shí)例對(duì)象大量相同的屬性和方法,享元模式大大提高了內(nèi)存性能。
而JavaScript的原型非常完美的契合享元模式。假如我們要開(kāi)發(fā)一款游戲,游戲中的每一個(gè)敵人都有一些共有的屬性,比如名字,位置,血量值,還有一些共有的方法,比如攻擊,防御等等。如果我們每創(chuàng)建出一個(gè)敵人對(duì)象都要把這些屬性進(jìn)行賦值,無(wú)疑會(huì)造成大量的性能問(wèn)題。我們看看下邊這段程序:
var enemyPrototype = { name: "James", position: { x: 0, y: 0 }, setPosition: function setPosition(x, y) { this.position = { x: x, y: y }; return this; }, health: 20, bite: function bite() { }, evade: function evade() { }}, spawnEnemy = function () { return Object.create(enemyPrototype); };var james1 = spawnEnemy();var james2 = spawnEnemy();james1.health = 5;console.log(james2.__proto__);console.log(james2.health);console.log(james1.__proto__.health);james1.setPosition(10, 10);console.log(james1.position);console.log(james1.__proto__.position);console.log(james2.position);
james1和james2這兩個(gè)敵人共享了一個(gè)enemyPrototype,這也算是一個(gè)默認(rèn)配置。修改了一個(gè)的屬性,不會(huì)影響其他對(duì)象的屬性。
值得注意的一點(diǎn)是,在上邊我們也提到了修改原型中的Object或數(shù)組一定要小心,那么在這個(gè)例子中,我們通過(guò)setPosition
這個(gè)函數(shù)解決了這個(gè)問(wèn)題,核心就是這個(gè)函數(shù)中的this關(guān)鍵字。
Object Creation(創(chuàng)建對(duì)象)
關(guān)于JavaScript中對(duì)象的創(chuàng)建,我在這里談兩點(diǎn):一種是使用構(gòu)造器,另一種是使用字面量。
我們之前也提到過(guò),使用構(gòu)造器初始化對(duì)象是很面向?qū)ο?/code>的編程思想,這在JavaScript中并不推薦,原因是它不能很好的利用原型這一利器。我們看個(gè)例子:
function Car(color, direction, mph) { var isParkingBrakeOn = false; this.color = color || "black"; this.direction = direction || 0; this.mph = mph || 0; this.gas = function gas(amount) { amount = amount || 10; this.mph += amount; return this; }; this.brake = function brake(amount) { amount = amount || 10; this.mph = (this.mph - amount) < 0 ? 0 : this.mph - amount; return this; }; this.toggleParkingBrake = function toggleParkingBrake() { isParkingBrakeOn = !isParkingBrakeOn; return this; }; this.isParked = function isParked() { return isParkingBrakeOn; }}var car = new Car();console.log(car.color); // => blackconsole.log(car.gas(30).mph); // => 30console.log(car.brake(20).mph); // => 10console.log(car.toggleParkingBrake().isParked()); // => true
仔細(xì)觀察上邊的代碼,可以總結(jié)出下邊幾點(diǎn):
在設(shè)計(jì)構(gòu)造器函數(shù)的時(shí)候,函數(shù)名第一個(gè)字母要大寫(xiě),創(chuàng)建對(duì)象時(shí)要使用new關(guān)鍵字
函數(shù)內(nèi)部對(duì)外暴露的屬性,方法使用this關(guān)鍵字
函數(shù)內(nèi)部可以添加私有變量
最重要的是理解new Object()這一過(guò)程的原理,再次強(qiáng)調(diào)一下:
var a = {};a.__proto__ = F.prototype;F.call(a);
另一種創(chuàng)建方式是使用字面量:
var myCar = { isParkingBrakeOn: false, color: "black", direction: 0, mph: 0, gas: function gas(amount) { amount = amount || 10; this.mph += amount; return this; }, brake: function brake(amount) { amount = amount || 10; this.mph = (this.mph - amount) < 0 ? 0 : this.mph - amount; return this; }, toggleParkingBrake: function toggleParkingBrake() { this.isParkingBrakeOn = !this.isParkingBrakeOn; return this; }, isParked: function isParked() { return this.isParkingBrakeOn; }}console.log(myCar.color); // => blackconsole.log(myCar.gas(30).mph); // => 30console.log(myCar.brake(20).mph); // => 10console.log(myCar.toggleParkingBrake().isParked()); // => true
代碼稍微做了改變,實(shí)現(xiàn)的效果一模一樣。有一個(gè)缺點(diǎn)是,不能使用私有變量,如果我要生成多個(gè)Car對(duì)象,需要反復(fù)的寫(xiě)上邊的代碼。那么我們應(yīng)該如何批量的生產(chǎn)對(duì)象呢?答案就在下一個(gè)小結(jié)。
Factories (工廠方法)
我們本篇討論的主要內(nèi)容就是Object,上邊我們已經(jīng)提到了使用字面量的方式創(chuàng)建對(duì)象有一個(gè)最大的缺點(diǎn)就是無(wú)法使用私有變量,我們可以使用工廠方法完美解決這個(gè)問(wèn)題。
工廠方法本質(zhì)上就是一個(gè)函數(shù),函數(shù)的返回值就是我們想要?jiǎng)?chuàng)建的對(duì)象。這個(gè)函數(shù)就像工廠一個(gè)樣能夠批量生產(chǎn)出規(guī)格一樣的的產(chǎn)品。
var car = function car(color, direction, mph) { var isParkingBrakeOn = false; return { color: "black", direction: 0, mph: 0, gas: function gas(amount) { amount = amount || 10; this.mph += amount; return this; }, brake: function brake(amount) { amount = amount || 10; this.mph = (this.mph - amount) < 0 ? 0 : this.mph - amount; return this; }, toggleParkingBrake: function toggleParkingBrake() { this.isParkingBrakeOn = !this.isParkingBrakeOn; return this; }, isParked: function isParked() { return this.isParkingBrakeOn; } }}var myCar = car();console.log(myCar.color); // => blackconsole.log(myCar.gas(30).mph); // => 30console.log(myCar.brake(20).mph); // => 10console.log(myCar.toggleParkingBrake().isParked()); // => true
我們把之前的代碼稍作修改,就成了一個(gè)工廠方法,每當(dāng)調(diào)用car()就會(huì)產(chǎn)生一個(gè)對(duì)象,這就是工廠方法,他相比于構(gòu)造器的優(yōu)勢(shì)就在于不需要使用new關(guān)鍵字。
到目前為止,我們已經(jīng)可以使用3中方式創(chuàng)建對(duì)象了:
構(gòu)造器
字面量
工廠方法
還有一個(gè)好玩的事情就是在工廠方法中使用原型,一定要記住的一點(diǎn)是,新建的對(duì)象會(huì)繼承原型中的所有屬性和方法。
var carPrototype = { gas: function gas(amount) { amount = amount || 10; this.mph += amount; return this; }, brake: function brake(amount) { amount = amount || 10; this.mph = ((this.mph - amount) < 0)? 0 : this.mph - amount; return this; }, color: 'pink', direction: 0, mph: 0 }, car = function car(options) { return extend(Object.create(carPrototype), options); }, myCar = car({ color: 'red' });console.log(myCar.color);
上邊這種方式最大的優(yōu)點(diǎn)就是在創(chuàng)建對(duì)象時(shí),可以為新建對(duì)象自由擴(kuò)展屬性和方法,這主要得益于extend函數(shù)的作用。
JavaScript是一門(mén)動(dòng)態(tài)語(yǔ)言,我們可以使用下邊的方法給carPrototype動(dòng)態(tài)的擴(kuò)展屬性和方法:
http://www.cnblogs.com/machao/p/6964155.html