發(fā)布于:2021-02-12 00:00:44
0
82
0
本文是由兩部分組成的系列文章中的第二篇。為了快速起步,您可以去我的博客里找到第一部分。
JavaScript是面向?qū)ο蟮?/strong>
JS是一種面向?qū)ο蟮恼Z言。這不同于面向類。如果您具有C ++或Java之類的工業(yè)語言背景,您可能會(huì)認(rèn)為JS缺少某些東西。我也這么想了很久。
實(shí)際上,諸如C ++或Java之類的流行語言都沒有強(qiáng)大的OOP概念實(shí)現(xiàn),它們是從強(qiáng)大的Smalltalk繼承而來的。OOP的發(fā)展甚至沒有停止Smalltalk。有許多語言使Smalltalk中的概念更進(jìn)一步。這些概念僅在學(xué)術(shù)界廣泛使用。
一種改進(jìn)是擺脫類。諸如Cecil,Self或最近的IO之類的語言都嘗試了一種原型方法,該方法也被引入JS。
您可能仍然認(rèn)為,與諸如Self之類的語言相比,JS沒有最佳的原型實(shí)現(xiàn)方式和丑陋的語法。但是,原型繼承概念本身是一個(gè)很大的改進(jìn)。
舊的方式
創(chuàng)建對象的舊方法是提供構(gòu)造函數(shù)。這是針對Java和C ++開發(fā)人員的,旨在為他們提供他們習(xí)慣的編碼樣式。但是構(gòu)造函數(shù)也有缺點(diǎn)。因此ECMAScript5引入了一種更好的創(chuàng)建對象的方法。
在ECMAScript5介入之前,它看起來如何?假設(shè)您已經(jīng)建立了一家商店,現(xiàn)在您需要開始在線銷售各種產(chǎn)品。在過去,您可能會(huì)這樣寫:
var Product = function(name) { this.name = name; }; Product.prototype.showName = function() {alert(this.name);}; var myProduct = new Product("Super Fancy TV"); myProduct.showName();<span> </span>
構(gòu)造函數(shù)(javascript_refresher / product_in_ecma3.js)
實(shí)際上,這還不錯(cuò)。您有一個(gè)構(gòu)造函數(shù)來創(chuàng)建對象并將方法附加到原型。所有單個(gè)產(chǎn)品都有這些方法,但是有幾個(gè)缺點(diǎn)需要考慮。
JS語言中沒有私有屬性 。名稱的值可以隨時(shí)從外部更改。
該 方法 被分散。 即使將它們?nèi)糠旁谝粋€(gè)地方,也沒有單一的結(jié)構(gòu) 來定義對象概念–就像許多其他 面向?qū)ο笳Z言中的類一樣。
您 很容易 忘記 使用“ new”關(guān)鍵字。 這不會(huì)引發(fā)錯(cuò)誤,只會(huì)導(dǎo)致完全不同的 行為,并且可能會(huì)導(dǎo)致一些很難發(fā)現(xiàn)的非常討厭的錯(cuò)誤。
繼承 會(huì) 帶來更多問題。在 JS社區(qū)中,沒有一種正確執(zhí)行方法的共識(shí)方法。
由于這些問題,許多開發(fā)人員創(chuàng)建了提供所有類型的對象創(chuàng)建和實(shí)例化邏輯的庫,框架和工具。他們中的許多人介紹了類(例如Prototype5或Coffeescript6)。
您可以根據(jù)需要選擇此路徑,但我不建議這樣做。也許難以置信,但是原型繼承實(shí)際上比基于類的繼承容易得多,甚至還提供了其他好處。只需看看其他原型語言,例如IO或Self。正是舊的JS語法使原型難以使用。
道格拉斯·克羅克福德(Douglas Crockford)率先提出了一種更好的方法。他編寫了一個(gè)簡短的Object.create方法,該方法以稍微擴(kuò)展的形式進(jìn)入了ES5標(biāo)準(zhǔn)。
對象創(chuàng)建
好消息是,您不需要具有ES5實(shí)現(xiàn)的現(xiàn)代瀏覽器。Mozilla開發(fā)人員網(wǎng)絡(luò)(MDN)提供了一個(gè)polyfill。polyfill是一種使較新的瀏覽器可以使用現(xiàn)代功能的技術(shù)。您可以在Modernizr-Wiki上找到其他polyfills。
MDN的Object.create-polyfill允許您使用在舊版瀏覽器(例如IE8)中創(chuàng)建對象的新方法。當(dāng)您認(rèn)為IE7 / IE8的綜合市場份額仍約為7%(2013年1月)時(shí),這一點(diǎn)很重要。如果您僅針對IE9 +,F(xiàn)irefox,Chrome,Safari或Opera用戶,這不是問題。如有疑問,請查看兼容性表。
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(); }; }<span style="font-family: Verdana, Arial, Helvetica, sans-serif;font-size: 12px"> </span>
只需包含清單1.6中的polyfill,以確保您具有Object.create函數(shù)。片段的第一行檢查Object.create是否已經(jīng)存在。這樣可以確保如果當(dāng)前瀏覽器提供的方法,polyfill將不會(huì)覆蓋本機(jī)實(shí)現(xiàn)。如果您需要用于其他ES5功能的附加填充料,則可以使用Kris Kowal的或David de Rosier的es5-shim。
獨(dú)奏對象
讓我們再看看shop項(xiàng)目。如果您只想創(chuàng)建一種產(chǎn)品,則不需要Object.create。只需直接創(chuàng)建對象:
var myProduct = { price: 99.50, name: 'Grindle 3' };
但是,這不是最佳選擇。您可以從外部輕松地操作對象的屬性,但是沒有辦法檢查或轉(zhuǎn)換分配的值??匆幌逻@個(gè)作業(yè):
MyProduct.price = -20;
像這樣的錯(cuò)誤將意味著您的公司快速銷售許多產(chǎn)品,并擁有非常滿意的客戶-每次購買可賺取20美元的客戶。您的公司將無法長期這樣做!
多年的面向?qū)ο缶幊毯驮O(shè)計(jì)經(jīng)驗(yàn)教會(huì)我們將對象的內(nèi)部狀態(tài)與它的外部接口分開。您通常希望將屬性設(shè)為私有,并提供一些getter和setter方法。
Getters and setters
ES5提供了一個(gè)使用getter和setter訪問屬性的絕佳概念。不幸的消息是,沒有辦法讓它們在較舊的瀏覽器中運(yùn)行–您不能僅使用polyfill。因此,在接下來的兩年左右的時(shí)間里,我們大多數(shù)人都沒有奢侈地使用它。同時(shí),您可以使用多種方法(請參閱[Stefanov 2010])。它們都有各自獨(dú)特的優(yōu)點(diǎn)和缺點(diǎn)。沒有單一的解決方案,因此歸結(jié)為一個(gè)問題。這是我通常的工作:
帶下劃線的前綴屬性,例如_price。這只是將屬性標(biāo)記為私有的約定。您永遠(yuǎn)不要從對象外部調(diào)用它們。通常容易發(fā)現(xiàn)違反此規(guī)則的情況。還有一些更復(fù)雜的方法,使用基于閉包的模式來歸檔實(shí)際隱私(例如,在[Stefanov 2010]中)。我的意見是,大多數(shù)時(shí)候他們不值得付出努力。
提供以“ set”開頭的setter方法,例如setPrice(value)。在較舊的瀏覽器中,無法覆蓋賦值運(yùn)算符=。所以這是下一件好事。Java和C ++程序員已經(jīng)習(xí)慣了。
提供具有原始屬性名稱的getter方法,例如price()。許多程序員更喜歡在方法的前面加上“ get”。我認(rèn)為這在JS中不是必需的,只會(huì)使代碼的可讀性降低–您的工作量可能會(huì)有所不同。 有時(shí),一個(gè)屬性可能只供內(nèi)部使用,或者您希望它僅準(zhǔn)備就緒。在這種情況下,只需忽略適當(dāng)?shù)姆椒纯伞R虼?,清?.8顯示了更好的實(shí)現(xiàn)。
var myProduct = { _price: 99.50, _name: 'Grindle 3', price: function() {return this._price;}, name: function() {return this._name;}, setPrice: function(p) {this._price = p;}, };<span style="font-family: Verdana, Arial, Helvetica, sans-serif;font-size: 12px"> </span>
具有g(shù)etter和setter的獨(dú)奏對象(javascript_refresher / product_with_getters_and_setters.js)。 如果要在設(shè)置之前檢查價(jià)格,現(xiàn)在可以輕松執(zhí)行以下操作:
setPrice: function(p) { if (p <= 0) { throw new Error("Price must be positive"); } this._price = p; }<span style="font-family: Verdana, Arial, Helvetica, sans-serif;font-size: 12px"> </span>
帶有檢查的setPrice方法(javascript_refresher / product_with_price_check.js)。
真正的ECMAScript5實(shí)現(xiàn)更好(清單1.10)。它具有隱式調(diào)用getter和setter的附加好處,例如myProduct.price = 85.99。最后,JS支持統(tǒng)一訪問原則!但是,在本教程中,我們將堅(jiān)持所提到的第一個(gè)解決方法,因?yàn)槟荒芟蚝笠浦泊苏Z言功能。
var myProduct = { ... get price() {return this._price;}, set price(p) { if (p <= 0) { throw new Error("Price must be positive"); } this._price = p; }, ...<span style="font-family: Verdana, Arial, Helvetica, sans-serif;font-size: 12px"> </span>
樣機(jī)
一個(gè)真正的商店將有很多產(chǎn)品,您不想從頭開始創(chuàng)建所有產(chǎn)品。您將需要一個(gè)可以放置通用結(jié)構(gòu)和行為的地方。JS中通常的模式是為此創(chuàng)建一個(gè)父對象-產(chǎn)品的原型,所有其他產(chǎn)品均來自該產(chǎn)品。使用Object.create可以從中構(gòu)建新產(chǎn)品。
var Product = { _price: 0, _name: '', price: function() {return this._price;}, name: function() {return this._name;}, setPrice: function(p) {this._price = p;}, setName: function(n) {this._name = n;} }; var product1 = Object.create(Product); product1.setName('Grindle 3'); product1.setPrice(99.50); var product2 = Object.create(Product); product2.setName('yPhone 7'); product2.setPrice(599.99);
也有慣例以此類首字母開頭這樣的原型,例如更傳統(tǒng)的語言中的類(即var Product而不是var product)。
初始化器
為了將新產(chǎn)品對象的所有屬性設(shè)置為正確的值,您需要調(diào)用其所有setter方法-這是一個(gè)很大的麻煩。您應(yīng)該改為構(gòu)建一個(gè)初始化方法。可以將這種方法與其他語言中的構(gòu)造方法進(jìn)行比較。
var Product = { ... init: function(name, price) { this._name = name; this._price = price; return this; }, ... }; var aProduct = Object.create(Product).init('Grindle 3', 99.50);
更好的是,覆蓋產(chǎn)品的create -Method來封裝創(chuàng)建和初始化。
var Product = { ... create: function(name, price) { return Object.create(this).init(name, price); }, ... }; var aProduct = Product.create('Grindle 3', 99.50);
原型方法的一大優(yōu)勢是實(shí)例化和繼承的統(tǒng)一。您不需要任何特殊的操作,也可以只使用Object.create進(jìn)行繼承。
var Product = { ... }; var Book = Object.create(Product); Book._author = null; Book._numPages = null; Book.setAuthor = function(author) {this._author = author;}; Book.setNumPages = function(num_pages) {this._numPages = num_pages;}; Book.author = function() {return this.author();}; Book.numPages = function() {return this.numPages();};
Book是從Product派生的新對象。它無需設(shè)置特定的值(如 名稱和價(jià)格),而是通過getter和setter來獲得其他結(jié)構(gòu)和行為(作者頁面和num Pages)。
缺點(diǎn)是 多次調(diào)用Book是多余的,整個(gè)語法與定義基礎(chǔ)對象完全不同。因此,您通常會(huì)構(gòu)建一個(gè)小的 擴(kuò)展函數(shù),使繼承更加方便(清單1.15)。同樣,您在真正的ES5中將不需要此功能。真正的Object.create允許包含擴(kuò)展名的第二個(gè)參數(shù)。
提示:jQuery(http://api.jquery.com/jQuery.extend)和Underscore.js(http://underscorejs.org/#extend)中提供了Object.extend的替代實(shí)現(xiàn) 。如果您的項(xiàng)目中已經(jīng)有這些庫之一,則可以改用它們。
Object.prototype.extend = function(props) { for (var prop in props) { this[prop] = prop; } return this; };
現(xiàn)在,您可以使用新的extend方法重構(gòu)代碼。
var Product = { _price: 0, _name: '', price: function() {return this._price;}, name: function() {return this._name;}, setPrice: function(p) {this._price = p;}, setName: function(n) {this._name = n;} }; var Book = Object.create(Product).extend({ _author: null, _numPages: null, setAuthor: function(author) {this._author = author;}, setNumPages: function(num_pages) {this._numPages = num_pages;}, author: function() {return this.author();}, numPages: function() {return this.numPages();} }); console.log(Product); console.log(Book);
內(nèi)部原型繼承
與基于靜態(tài)類的方法相比,對象和原型概念具有許多優(yōu)點(diǎn),例如:
繼承和實(shí)例的統(tǒng)一。您可以使用相同的機(jī)制(Object.create)從原型繼承或從原型構(gòu)建實(shí)例(類似于類用法)。實(shí)際上–這是同一回事。
價(jià)值的繼承。您可以從原型繼承值;不需要在構(gòu)造函數(shù)中設(shè)置默認(rèn)值。
原型的運(yùn)行時(shí)修改。在JS中,運(yùn)行時(shí)和編譯時(shí)沒有區(qū)別。您可以在程序執(zhí)行期間修改原型,而更多的靜態(tài)語言僅允許在編譯器運(yùn)行之前更改類。這不是原型繼承的直接優(yōu)勢,而是JS的動(dòng)態(tài)特性。甚至有基于類的語言可供使用,這些類允許對類進(jìn)行運(yùn)行時(shí)修改-Ruby或Smalltalk可以滿足要求。但是JavaScript的僅對象方法使這一過程變得簡單得多。如果您想進(jìn)行任何元編程,這將是一大收獲。
“僅對象”方法的最大優(yōu)點(diǎn)是其簡單性。讓我們看一下內(nèi)部工作原理:首先,您不需要對方法進(jìn)行任何特殊處理。方法只是碰巧包含函數(shù)的對象屬性。因此,查找簡單數(shù)字或調(diào)用方法都沒關(guān)系?,F(xiàn)在看一下JS如何確定要調(diào)用的方法。清單1.17演示了該原理。
var Product = { init: function(name) { this._name = name; return this; }, _name: '', name: function() { return this._name; }, setName: function(n) { this._name = n; } }; var Book = Object.create(Product).extend({ init: function(name, author) { Product.init(name); this._author = author; return this; }, author: null, setAuthor: function(author) { this._author = author; }, author: function() { return this.author(); } }); var myBook = Object.create(Book).init('Lords of the Rings', 'J.R.R. Tolkien'); myBook.mostImportantHobbit = "Frodo";<span style="font-family: Verdana, Arial, Helvetica, sans-serif;font-size: 12px"> </span>
如果您嘗試獲取myBook.mostImortantHobbit的值 ,則JavaScript引擎僅查看myBook對象并返回該值。
查找myBook.name()需要更多步驟。JS引擎在myBook對象上找不到名稱-屬性 ,因此需要在其原型Book中進(jìn)行查找 。它也不存在,因此它會(huì)跟蹤原型鏈,直到找到它。該屬性名稱 實(shí)際上在Book的原型 產(chǎn)品中可用。因此,JS解釋了括號(hào)并調(diào)用了name中包含的函數(shù)。該函數(shù)在myBook的上下文中執(zhí)行。因此,this._name 指的是“指環(huán)王””。即使JS確實(shí)需要執(zhí)行幾個(gè)步驟,它們也很容易理解。始終遵循原型鏈。
實(shí)例化和繼承不需要區(qū)別對待。
其他要考慮的事情
在實(shí)際的項(xiàng)目中,還需要考慮許多其他事項(xiàng)。您通常希望將代碼保留在名稱空間中。要管理名稱空間和文件依賴性,您可能需要使用提供AMD(異步模塊定義)的工具。 RequireJS 或 curl 是流行的。
您甚至可能希望使用更大的基礎(chǔ)框架之一,例如 Ember, Backbone 或 AngularJS。在這里我不會(huì)深入研究這些東西,因?yàn)樗鼈儗τ诶斫庑袨轵?qū)動(dòng)的開發(fā)不是必需的。我敦促您真正研究構(gòu)建代碼庫的更好方法。它可以帶來很大的不同。
替代樣式
JS是一種非常靈活的語言,支持各種編程范例(至少是功能和面向?qū)ο蟮模?。這為軟件設(shè)計(jì)和開發(fā)提供了許多不同的方法。您可能會(huì)喜歡純函數(shù)式方法,或者對原型進(jìn)行OOP,或者將庫用于基于類的面向?qū)ο蟆D赡軙?huì)考慮使用混合/特征或其他高級(jí)構(gòu)造。也許您會(huì)考慮使用像Harmonizr這樣的預(yù)處理器/編譯器來編寫ECMAScript6 / Harmony代碼,甚至嘗試使用CoffeeScript。就本教程而言,沒關(guān)系。行為驅(qū)動(dòng)方法應(yīng)使用這些特征或混合中的任何一種起作用。
因此,在這里加點(diǎn)鹽就可以成為我的JS風(fēng)格;與實(shí)際項(xiàng)目相比,我將其簡化了一些。可以將它帶入行為驅(qū)動(dòng)開發(fā)的美好世界。
作者介紹