先看看繼承能帶來的好處.設計類的時候,但願能減小重複性的代碼,儘可能弱化對象間的耦合.使用繼承符合前一個原則.能夠在現有類的基礎上進行設計並充分利用他們已經具有的各類方法.
讓一個類繼承另外一個類可能會致使兩者產生強耦合,一個類依賴於另外一個類的內部實現.接下來會講到如何避免.好比用摻元類爲其餘類提供方法...等等.css
經過用函數來聲明類,用關鍵字 new 來建立實例,下面是一個簡單的類聲明:前端
// Class Person function Person(name) { this.name = name; } Person.prototype.getName = function() { return this.name; }
首先要作的事建立構造函數,名字就是類名,首字母大寫.在構造函數中,建立實例屬性要使用關鍵字 this.類的方法則被添加到其 prototype 對象中.要建立該類的實例,只需結合關鍵字 new 調用這個構造函數:程序員
var reader = new Person('John Smith'); reader.getName();
而後你能夠訪問全部的實例屬性,也能夠調用全部的實例方法.數組
建立繼承 Person 的類要複雜一些:瀏覽器
// Class Author function Author(name, books) { Person.call(this, name); // Call the superclass's constructor in the scope of this. this.books = books; // Add an attribute to Author. } Author.prototype = new Person(); // Set up the person chain. Author.prototype.constructor = Author; // Set the constructor attribute to Author. Author.prototype.getBooks = function () { // Add to method to Author. return this.books; }
讓一個類繼承另外一個類須要用到許多行代碼(不像其餘面嚮對象語言只要一個關鍵字 extend 便可),首先要作的是建立一個構造函數,在構造函數中,調用超類的構造函數.並將 name 參數傳給他,在使用 new 運算符時,系統會爲你作一些事,會建立一個空對象,而後調用構造函數,在此過程當中這個空對象處於做用域鏈的最前端.
下一步是設置原型鏈,js 沒有 extend 關鍵字,可是每一個 js 對象中都有一個名爲 prototype 的屬性,要麼指向另外一個對象,要麼 Null.在訪問對象的某個成員時(好比reader.getName),若是這個成員未見於當前對象,那麼 js 會在prototype屬性所指的對象中查找他,沒找到js就會沿着原型鏈向上逐一訪問每一個原型對象,直到找到他(或者已經查找過原型鏈最頂端的 Object.prototype 對象).
因此說爲了讓一個類繼承另外一個類,只需將子類的 prototype 設置爲指向超類的一個實例便可.
爲了讓Author 繼承 Person,必須手動地將 Author 的 prototype 設置爲 Person 的一個實例,最後一步是將 prototype 的 constructor 屬性重設爲 Author(由於把 prototype 屬性設置爲 Person 的實例時,其 constructor 屬性被抹掉了).
儘管本例中爲實現繼承須要額外使用三行代碼,可是建立這個新的子類的的實例與建立 Person 的實例沒有什麼不一樣:閉包
var author = []; author[0] = new Author('Dustin Diaz', ['Javascript Design Patterns']); author[0] = new Author('Ross Harmes', ['Javascript Design Patterns']); author[1].getName(); author[1].getBooks();
因此說,類式繼承的複雜性只侷限於類的聲明,建立新實例的過程仍然很簡單.app
爲了簡化類的聲明,能夠把派生子類的整個過程包裝在一個名爲 extend 的函數中,他的做用和其餘語言的 extend 關鍵字相似,即基於一個給定的類結構建立一個新類:框架
// Extend Function. function extend(subClass, superClass) { var F = function () {}; F.prototype = superClass.prototype; subClass.prototype = new F(); subClass.prototype.constructot = subClass; }
這個 extend 函數和先前咱們作的同樣,它設置了 prototype.而後再從新設置其 constructor 爲其自己.有一項改進,他添加了空函數 F,並將用它建立的一個對象實例插入原型鏈中(這樣能夠避免建立超類的新實例).
使用了 extend 函數後:異步
// Class Person. function Person(name) { this.name = name; } Person.prototype.getName = function () { return this.name; } // Class Author. function Author(name, books) { Person.call(this.name); this.books = books; } extend(Author, Person); Author.prototype.getBooks = getBooks() { return this.books; }
上面的代碼不像以前那樣手動地設置prototype 和 constructor 屬性,而是經過在類聲明以後(在向 prototype 添加任何方法以前)當即調用 extend 函數.惟一的問題是超類(Person)的名稱被固化在 Author 類的聲明之中,更好的作法是下面那樣:模塊化
// Extend function, improved. function extend(subClass, superClass) { var F = function() {}; subClass.prototype = new F(); subClass.superclass = superclass.prototype; if (superClass.prototype.constructor == Object.prototype.constructor) { superClass.prototype.constructor = superClass; } }
說到這個 改進版的extend函數,我想起了之前的一個東東,說要實現一個js類繼承工具方法:
繼承
多態
如今想一想,此處的 extend 函數已經實現了第一步,還差第二步,暫不討論.
該版本要長一點,可是提供了superclass 屬性,這個屬性用來弱化 Author 和 Person 之間的耦合.該函數的最後3行代碼用來確保超類的 constructor 屬性已被正確設置(即時超類就是 Object 類自己),在用這個新的superclass 屬性調用超類的構造函數時這個問題很重要:
// Class Author. function Author(name, books) { Author.superclass.constructor.call(this, name); this.books = books; } extend(Author, Person); Author.prototype.getBooks = function () { return this.books; };
有了 superclass 屬性,就能夠直接調用超類的方法,這在既要重定義超類的某個方法又想訪問其在超類的實現時能夠派上用場.例如,爲了用一個新的 getName 方法重定義 Person 類中的同名方法,你能夠先用Author.superclass.getName 得到做者名字,而後在此基礎上添加其餘信息:
Author.prototype.getName = function () { var name = Author.superclass.getName.call(this); return name + ',Author of' + this.getBooks(0.join(', '); }
它與類式繼承大相徑庭,此刻最好忘掉類和實例的一切知識,只從對象的角度來思考.用基於類的辦法來建立對象包括兩個步驟:首先,用一個類的聲明定義對象結構;第二,實例化該類以建立一個新對象.用這種方式建立的對象都有一套該類的全部實例屬性的副本.每個實例方法都只存在一份,可是每一個對象都有一個指向他的連接.
使用原型式繼承並不須要用類來定義對象的結構,只需直接建立一個對象便可.這個對象隨後能夠被新的對象重用,這個得益於原型鏈查找的工做機制.該對象被稱爲原型對象.取原型式繼承這個名稱是由於他爲其餘對象應有的模式提供了一個原型.
下面咱們使用原型式繼承從新設計 Person 和 Author:
// Person Prototype Object. var Person = { name: 'default name', getName: function () { return this.name; } };
這裏沒有使用一個名爲 Person 的構造函數來定義類的結構,Person如今是一個對象字面量.他是所要建立的其餘各類類 Person 對象的原型對象.其中定義了全部類 Person 對象都要具有的屬性和方法,而且有默認值.方法的默認值通常不會改變,可是屬性與此相反.
var reader = clone(Person); alert(reader.getName()); // This will output 'default name'. reader.name ='John Smith'; alert(reader.getName()); // This will output 'John Smith'.
clone 函數能夠用來建立新的類 Person 對象.他會建立一個空對象,二該對象的原型對象被設置成 Person.也就是說若是在這個新對象中查找不刀某個方法或者屬性時,那麼接下來會在其原型對象中繼續查找.
沒必要爲建立A Author 而定義一個一個 Person 的子類,只需執行一次克隆便可.
// Author Prototype Object. var Author = clone(Person); Author.books = []; // Default value. Author.getBooks = function () { return this.books; }
而後你能夠重定義該克隆中的方法和屬性.能夠修改在 Person 中提供的默認值.也能夠添加新的屬性和方法.這樣一來就建立了一個新的原型對象.能夠將其用於建立新的類 Author 對象:
var author = []; author[0] = clone(Author); author[0].name = 'Dustin Diaz'; author[0].books = ['Javascript Design Patterns']; author[1] = clone(Author); author[1].name = 'Ross Harmes'; author[1].books = ['Javascript Design Patterns']; author[1].getName(); author[1].getBooks();
在類式繼承中,Author 的每個實例都有一份本身的 books 數組副本,能夠用代碼 author[1].books.push('New Book Title')
爲其添加元素.可是對於使用原型式繼承方式建立的類 Author 對象來講,因爲原型鏈的工做方式,這種作法行不通.一個克隆並不是其原型對象的一份徹底獨立的副本,只是一個以那個對象爲其原型對象的空對象而已.克隆剛被建立時,author[1].name 實際上是一個指向最初的Person.name 的連接,對於從原型對象繼承而來的成員,其讀和寫具備內在的不對等性.在讀取 author[1].name 的值時,若是尚未直接爲 author[1]實例定義 name 屬性的話,那麼所獲得的事其原型對象的同名屬性值.而在寫入 author[1].name 的值時,你是在直接爲 author[1]對象定義一個新屬性.下面這個實例顯示了這種不對等性:
var authorClone = clone(Author); console.log(authorClone.name); // Linked to the primative Person.name, which is the string 'default name'. authorClone.name = 'new name';// A new primative is created and added to the authorClone object itself. console.log(authorClone.name); // Now linked to the primative authorClone.name, which is the string 'new name'. authorClone.books.push('new book'); // authorClone.books is linked to the arrayAuthor.books. We just modifiedthe prototype object's default value, and all other objects that link to it will now have a new default value there. authorClone.books = []; // A new array is created andadded to the authorClone object itself. authorClone.books.push('new book'); // We are now modifying that new array.
上面的例子說明了爲何必須經過引用傳遞的數據類型的屬性建立新副本.向 authorClone.books 數組添加新元素其實是把這個元素添加到Author.books 數組中,這樣的話值的修改會同時影響到Author 和全部繼承了 Author 但還未改寫那個屬性的默認值的對象.這種錯誤必須儘可能避免,調試起來會很是費時.在這類場合,可使用 hasOwnProperty 方法來區分對象的實際成員和繼承而來的成員.
有時原型對象本身也含有子對象.若是想覆蓋其子對象中的一個屬性值,不得不從新建立整個子對象.這能夠經過將該子對象設置爲一個空對象字面.而後對其重塑.但這意味着克隆出來的對象必須知道其原型對象的每個子對象的確切結構.和默認值.爲了儘可能弱化對象之間的耦合,任何複雜的子對象都應該使用方法來建立:
var ComponoudObject = { string1: 'default value', childObject: { bool: true, num: 10 } } var CompoundObject = { string1: 'default value', childObject: { bool: true, num: 10 } } var compoundObjectClone = clone(CompoundObject); // Bad! Changes the value of CompoundObject.childObject.num. compoundObjectClone.childObject.num = 5; // Better. Creates a new object, but compoundObject must know the structure of that object, and the defaults. This makes CompoundObject and compoundObjectClone tightly coupled. compoundObjectClone.childObject = { bool: true, num: 5 };
在這個例子中,爲 compoundObjectClone 必須知道 childObject 具備兩個默認值分別爲 true 和10的屬性.這裏有一個更好的辦法: 用工廠辦法來建立 childObject:
// Best approach. Uses a method to create a new object, with the same structure and defaults as the original. var CompoundObject = {}; CompoundObject.string1 = 'default value'; CompoundObject.createChildObject = function () { return { bool: true, num: 10 } }; CompoundObject.childObject = CompoundObject.createChildObject(); var compoundObjectClone = clone(CompoundObject); compoundObjectClone.childObject = CompoundObject.createChildObject(); compoundObjectClone.childObject.num = 5;
以前的例子用來建立克隆對象的 clone 函數到底是什麼樣呢:
// Clone function. function clone(object) { function F() {} F.prototype = object; return new F; }
clone 函數首先建立了一個新的空函數 F,而後將 F 的 prototype 屬性設置做爲參數 object 傳入的原型對象.prototype 屬性就是用來指向原型對象的,經過原型鏈機制,它提供了到全部繼承而來的成員的連接.該函數最後經過把 new 運算符做用於 F 建立出一個新對象.而後把這個新對象做爲返回值返回.函數所返回的這個克隆結果是一個一給定對象爲原型對象的空對象.
類式繼承和原型式繼承是截然不同的兩種繼承範型,他們生成的對象也有不一樣的行爲方式.須要對二者的優缺點和特定使用場合進行了解.
若是你設計的是一個衆人使用的 API,或者可能會有不熟悉原型式繼承的其餘程序員基於你的代碼進行改造.那麼最好使用類式繼承.
原型式繼承更能節約內存.原型鏈讀取成員的方式使得全部克隆出來的對象都共享每一個屬性和方法的惟一一份實例,只有在直接設置了某個克隆出來的對象的屬性和方法時,狀況纔會變化.
類式繼承方式中建立的每個對象在內存中都有本身的一套屬性(和私有方法)德芙笨.因此說原型式繼承更節約內存,並且只使用一個 clone 函數也更爲簡練,不須要像後者那樣須要爲每個想繼承的類寫上好幾行這樣的晦澀代碼:`
SuperClass.call(this, arg)和 SubClass.prototype = new SuperClass...`
不過也能夠寫到 extend 方法裏面去,因此說最後到底使用哪一種繼承方式除了考慮實際狀況以外還取決於你的口味.
如今來談談封裝對繼承的影響.
從現有的類派生出一個子類時,只有公有和特權成員會被繼承下來,可是私有成員沒法繼承下來.
因爲這個緣由,門戶大開型類是最適合派生子類的,它們的全部成員都是公開的,能夠被遺傳給子類,若是某個成員須要稍加隱藏,可使用下劃線規範.
在派生具備真正的私有成員的類時,特權方法是公有的,因此會被遺傳下來.因此能夠在子類中間接訪問父類的私有屬性.可是子類的實例方法都不能直接訪問這些私有屬性.父類的私有成員只能經過這些既有的特權方法訪問到,在子類中添加新特權方法也訪問不到.
這是一種沒有嚴格繼承,重用代碼的方法.是這樣,若是想把一個函數用到多個類中,能夠經過擴充的方式讓這些類共享該函數.
先建立一個包含各類通用方法的類,而後再用它擴充其餘類,這種類叫作摻元類(mixin class),一般不會被實例化或者直接調用,只是向其餘類提供本身的方法(說實話這個 mixin 在各類場合是否是很熟悉呢...各類 js 框架,css 預處理,是否是都跟這個有關呢...):
// Mixin class. var Mixin = function () {}; Mixin.prototype = { serialize: function () { var output = []; for (key in this) { output.push(key + ';' + this[key]); } retyurn output.join(', '); } };
這個 Mixin 類只有一個名爲 serialize 的方法,遍歷 this 對象的全部成員並輸出一個字符串.這種方法可能在許多不一樣類型的類中都會用到,可是沒有必要讓這些類都繼承 Mixin,最好是用一個函數 augment 把這個方法添加到每個須要他的類中:
augment(Author, Mixin); var author = new Author('Ross Harmes', ['Javascript Design Patterns']); var serializaedString = author.serialize();
在此咱們用 Mixin 類中的全部方法擴充了 Author 類,Author 類的實例如今就能夠調用 serialize 方法了,稱爲爲多親繼承 multiple inheritance.儘管在 js 中一個對象只能用有一個原型對象,不容許子類繼承多個超類,可是一個類能夠用多個摻元類擴充,實際上也就實現了多繼承.
augment 函數很簡單,其實是用一個 for...in 循環遍歷 第二個參數(Mixin 類,予類 giving class)的 prototype 中的每個成員,並將其添加到第一個參數(受類 receiving class)的 prototype 中,若是受類中已經存在同名成員,那麼跳過它,這樣受類中的成員就不會被改寫.若是你想達到這麼一個目的: 只複製摻元類當中的一兩個方法,那麼就能夠給 augment 函數加上第三個及更多的可選參數:
// Augment function, improved. function augment(receivingClass, givingClass) { if (arguments[2]) { // Only give certain methods. for (var i = 2, len = arguments.length; i < len; i++) { receivingClass.prototype[arguments[i]] = givingClass.prototype[arguments[i]]; } } else { // Give all methods. for (methodName in givingClass.prototype) { if (!receivingClass.prototype[methodName]) { receivingClass.prototype[methodName] = givingClass.prototype[methodName]; } } } }
如今使用 augment(Author, Mixin, 'serialize');z能夠只爲 Author 類添加一個 serialize 方法的目的了.
從條理性的角度來看,嚴格的繼承方案比擴充方案更加清楚.摻元類很是適合於組織那些彼此迥然不一樣的類所共享的方法.
繼承的好處主要表如今代碼的重用方面.經過創建類或者對象之間的繼承關係,有些方法咱們只需定義一次便可.若是須要修改這些方法或者排查其中錯誤,那麼因爲其定義只出如今一個位置,因此很是節省時間.
各類繼承範型各有優缺點.
原型式繼承工做機制: 先建立一些對象而後再對其進行克隆,從而獲得建立子類和實例的等效效果.用這種辦法建立的對象有很高的內存效率,由於它們會共享那些未被改寫的屬性和方法.
在內存效率重要的場合原型式繼承(clone 函數)是最佳選擇,若是你更容易接受其餘面嚮對象語言中的繼承機制,那麼對於 js 繼承仍是選用類式繼承(extend 函數)比較好.這兩種方法都適合於類間差別較小的類層次體系(hierarchy).
若是類之間的差別較大,那麼用摻元類來擴充這些類會更合理.
------------ 分割線 ----------
也叫單例模式singleton,js 中最基本也最有用.將代碼組織爲一個邏輯單元,能夠經過單一的變量進行訪問.單體對象只存在一份實例,全部代碼使用的都是一樣的全局資源.
單體類在 js 中有許多用途,能夠用來劃分命名空間,減小網頁中全局變量的數量.它們還能夠在一種名爲分支的技術中用來封裝瀏覽器之間的差別(在使用各類經常使用的工具函數時就沒必要再操心瀏覽器嗅探的事).
最重要的是,能夠把代碼組織的更爲一致,可維護性提升.
在網頁上使用全局變量有很大的風險,而用單體對象建立的命名空間是清除這些全局變量的最佳手段之一.
這裏先討論最基本最簡單的類型,一個對象字面量,把一批有必定關聯的方法和屬性組織在一塊兒:
// Basic Singleton. var Singleton = { attribute1: true, attribute2, 10, method1: function () { }, method2: function (arg) { } };
示例中,全部成員均可以經過變量 Singleton 訪問.可使用圓點運算符.
這個單體對象能夠被修改.能夠爲其添加新成員,也能夠用 delete 運算符刪除其現有成員.實際上違背了面向對象設計的一條原則:類能夠被擴展,但不該該被修改(道理有點像 css classes 的增減).區別於其餘面嚮對象語言js 中的全部對象都易變,若是某些變量須要保護,那麼能夠將其定義在閉包之中.
你可能注意到了,剛剛的示例並非單體,由於按照定義,單體是一個只能被實例化一次而且能夠經過一個訪問點訪問的類,而這個例子不是一個可實例化的類.咱們能夠把單體定義地更廣義一些:
單體是一個能夠用來劃分命名空間並將一批相關方法和屬性組織到一塊兒的對象,若是能夠被實例化,那麼它只能被實例化一次.
並不是全部對象字面量都是單體,若是隻是用來模仿關聯數組或者容納數據的話,那就不是單體;可是若是是用來組織一批相關方法和屬性的話就有多是單體.
單體對象有兩部分: 包含着方法和屬性成員的對象自身,還有用於訪問它的變量.這個變量一般是全局性的,這個變量一般是全局性的,一遍在網頁上任何地方都能直接訪問到它所指向的單體對象.
雖然定義單體沒必要是全局性的,可是它應該在各個地方都能被訪問,由於單體對象的全部內部成員都被包裝在這個對象中,因此它們不是全局性的.
因爲這些成員只能經過這個單體對象變量進行訪問,因此能夠說它們被單對對象圈在了一個命名空間中.
// using a namespace. var MyNameSpace = { findProduct: function(id) { ... }, // Other methods can go there as well. } ... // Later in your page, another programmer adds... var resetProduct = $('reset-product-button'); var findProduct = $('reset-product-button'); // NOthing was overwritten.
如今 findProduct 函數是MyNameSpace中的一個辦法,他不會被全局命名空間中聲明的任何新變量改寫.該方法仍然能夠從各個地方訪問,可是如今調用方式不是 findProduct(id),而是 MyNameSpace.findProduct(id).
已經知道如何把單體做爲命名空間使用,如今咱們在介紹單體的一個特殊用途.
有些 js 代碼是一個網站中全部網頁都要用到的,一般被存放在獨立的文件中;有些代碼則是某個網頁專用的,不會被用到其餘地方,能夠把這兩種代碼分別包裝到本身的單體對象中.
以前咱們討論過建立類的私有成員的作法,使用真正私有方法一個缺點在於它們比較耗費內存,由於每一個實例都具備方法的一份新副本,不過因爲單體對象只會被實例化一次,因此定義真正的私有方法時不用考慮內存.不過咱們先談談更簡單的建立僞私有成員的作法.
// DataParser singleton, coverts character delimited strings into arrays. GaintCorp.DAtaParser = { // private methods. _stripWhitespace: function (str) { return str.replace(/\s+/, ''); }, _stringSplit: function(str, delimiter) { return str.splist(delimiter); }, // Public method. stringToArray: function(str, delimiter, stripWS) { if (stripWS) { str = this._stripWhitespace(str); } var outputArray = this._stringSplit(str, delimiter); return outputArray; } };
在單體對象中建立私有成員的第二種辦法須要藉助閉包.與以前建立真正私有成員的作法很是類似.
但也有重要區別.先前的作法是把變量和函數定義在構造函數體內(不使用 this 關鍵字),此外還在構造函數內定義了全部的特權方法並用 this 關鍵字使其可被外界訪問.每生成一個該類的實例時,全部聲明在構造函數內的方法和屬性都會再次建立一份,可能會很是低效.
由於單體只會被實例化一次,因此構造函數內成員個數不是重點.每一個方法和屬性都只會被建立一次,因此能夠把它們都聲明在構造函數內(位於同一個閉包內)
// Singleton as an Object Literal. MyNamespace.Singleton = {}; // Singleton with Private Members, step 1. MyNamespace.Singleton = function () { return {}; }();
上面兩個 MyNamespace.Singleton
徹底相同.對於第二個,並無把一個匿名函數賦給 MyNamespce.Singleton
而是返回一個對象再賦值.函數定義後的大括號是爲了當即執行該函數.還能夠像下面那樣再套上一對圓括號.
如今大概能夠知道,談到單體,有兩個關鍵詞:閉包+大括號
再回顧一下,能夠把公有成員添加到單體所返回的那個對象字面量:
//Singleton with Private Members, step 2. MyNamespace.Singleton = (function () { return { // Public members. publicAttribute0: true, publicAttribute2: 99, publicMethod1: function () { ... } }; })();
使用閉包和使用一個對象字面量的區別在於:
對於前者,任何聲明在匿名函數中(但不是在那個對象字面量中)的變量或者函數都只能被在同一個閉包中聲明的其餘函數訪問.這個閉包在匿名函數結束執行後依然存在,因此在其中聲明的函數和變量總能從匿名函數所返回的對象內部訪問.
單體模式跟js模塊化有必定關聯,因此又稱模塊模式,意指他能夠把一批相關方法和屬性組織爲模塊並起到劃分命名空間.
如今咱們再也不爲每一個私有方法名稱的開頭添加一個下劃線,而是把這些方法定義在閉包中:
// DataParser singleton, converts character delimited strings into arrays. // Now using true private methods. CiantCorp.DataPraser = (function () { // Private attributes. var whitespaceRegex =/\s+/; // Private methods. function stripWhitespace(str) { return str.repalce(whitespaceRegex, ''); } function stringSplit(str, delimiter) { return str.split(delimiter); } // Everything returned in the object literal is public, but can access the members in the closure created above. return { // Public method. stringToArray: function(str, delimiter, stringWS) { if (stringWS) { str = stripWhitespace(str); } var outputArray = stringSplit(str, delimiter); return outputArray; } }; })(); // Invoke the functio nand assign the returned object literal to GiantCorp.DataParser.
如今這些私有方法和屬性能夠直接用其名稱訪問,沒必要在其前面加上 this.
或者GaintCorp.DataParser
,這些前綴只用於訪問單體對象的公有成員.
單體相比於下劃線表示法有幾點優點:
把私有成員放到閉包中能夠確保其不會在單體對象以外被使用.
能夠任意改變對象實現細節,不破壞其餘代碼.
還能夠對數據進行保護和封裝.
使用單體時,能夠享受真正的私有成員帶來的好處,單體類只會被實例化一次,能夠節省內存.這是單體模式成爲受歡迎,應用普遍的模式之一的緣由.
注意事項:公有成員和私有成員的聲明語法不同,前者被聲明在對象字面量內部然後者並非這樣.私有屬性必須用 var 聲明,不然它將成爲全局性的,私有方法是按 `function funcName (args) {...}` 這樣的形式聲明,在最後一個大括號以後不須要使用分號,公有屬性和方法分別按照 attributeName: attributeValue 和 `methodName: function (args) {...}`這樣的形式聲明.若是後面還要聲明別的成員的話,那麼該聲明的後面應該加上一個逗號.
以前的單體模式的各類實現方式有一個共同點:單體對象都是在腳本加載時被加載出來,若是單體資源密集或者配置開銷大,那麼更合理的作法是將其實例化推遲到須要使用它的時候.被稱爲惰性加載,最經常使用於那些必須加載大量數據的單體.那些被用作命名空間,特定網頁專用代碼包裝器,組織相關實用方法的工具的單體最好仍是當即實例化.
惰性加載單體的特別之處在於對他們的訪問必須藉助於一個靜態方法.這樣調用: Singleton.getInstance().methodName()
,而不是這樣調用: Singleton.methodName()
.getInstance()方法薈兼差單體是否已經被實例化,若是尚未,那麼將建立而且返回實例.若是實例化過,那麼它將返回現有實例.下面咱們從前面那個擁有真正私有成員的單體出發將普通單體轉化爲惰性加載單體(轉化工做第一步是把單體的全部代碼移到一個叫作 constructor 的方法中:
// General skeleton for a lazy loading singleton, step 1. MyNamespace.Singleton = (function() { function constructor () { // All of the normal singleton code goes here. // Private members. var privateAttribute1 = false; var privateAttribute2 = [1, 2, 3]; function privateMethod1 () { ... } function privateMethod2 () { ... } return { // Public members. publicAttribute1: true, publicAttribute2: 2, publicMethod1: function () { ... } } } })();
這個方法不能從閉包外部訪問這是件好事,由於咱們想控制調用時機.公有方法 getInstance 就是要這麼作,爲了使其成爲公有方法,只須要將其放到一個對象字面量中而且返回該對象便可:
// General skeleton for a lazy loading singleton, step 2. MyNamespace.Singleton = (fucntion () { function constructor() { // All of the normal singleton code goes here. ... } return { getInstance: function () { // Control code goes here. } } })();
如今討論如何編寫控制實例化時機的代碼.首先,必須知道該類是否被實例化過;其次,若是該類被實例化過,那麼他須要掌握其實例的狀況,以便能返回這個示例;要作到這兩點,須要用到一個私有屬性和已有的私有方法constructor:
// General skeleton for a lazy loading singleton, step 3. MyNamespace.Singleton = (function () { var uniqueInstance; // Private attribute that holds the single instance. function constructor () { // All of the normal singleton code goes here. ... } return { getInstance: function () { if (!uniqueInstance) { // Intantiate only if the instance doesn't exist. uniqueInstance = constructor(); } return uniqueInstance; } } })();
惰性加載單體缺點在於複雜性,用於建立這種類型的單體代碼並不直觀,不易理解.若是你須要建立一個延遲加載實例化的單體,那麼最好爲其寫註釋,以避免別人把其簡化爲普通單體.
一種用來將瀏覽器間的差別封裝到在運行期間進行設置的動態方法中的技術.假設咱們須要建立一個返回 XHR 對象的方法,這個XHR對象在大多數瀏覽器中是 XMLHttpRequest 類的實例,而在 IE 早期版本中則是某種 ActiveX 類的實例.咱們要建立的方法一般會進行某種瀏覽器嗅探或者對象檢測.若是不用分支技術,那麼每次調用時,全部那些瀏覽器嗅探代碼都要再次運行.若是調用頻繁,那麼會很低效.
更有效的作法是隻在腳本加載時一次性地肯定針對特定瀏覽器的代碼,遮掩的話,在初始化後,每種瀏覽器都會只執行鍼對他的 js 實現而設計的代碼.可以在運行時動態肯定函數代碼的能力,是 js 的高度靈活性和強大表現能力的一種體現,提升了調用這些函數的效率.
在以前,單體對象的全部代碼都是在運行時肯定的,這在鄙薄建立私有成員的模式中很容易看出來:
MyNamespace.Singleton = (function () { return {}; })();
這個匿名函數在運行時執行,返回的對象字面量賦值給 MyNamespace.Singleton 變量.
如今咱們要建立一個單體,他有一個用來生成 XHR 對象實例的方法.
首先判斷分支數量,由於全部實例化的對象只有3種不一樣類型,因此須要3個分支,分別按照其返回的XHR 對象類型命名:
// SimpleXhrFactory singleton. var SimpleXhrFactory = (function () { // Three branches. var standard = { createXhrObject: function () { return new XMLHttpRequest(); } }; var activeXNew = { createXhrObject: function () { return new ActiveXObject('Msxml2.XMLHTTP'); } }; var activeXOld = { createXhrObject: function () { return new ActiveXObject('Microsoft.XMLHTTP'); } }; // To assign the branch, try each method, var testObject; try { testObject = standard.createXhrObject(); return standard; // Return this if no error was thrown. } catch(e) { try { testObject = activeXNew.createObject(); return activeNew; } catch(e) { try { testObject = activeXOld.createXhrObject(); return activeXOld; } catch(e) { throw new Error('No XHR object found in the environment.'); } } } })();
上面的示例代碼建立了三個對象字面量,它們有相同一個方法 createXhrObject()
,它是用來返回一個能夠執行異步請求的新對象,很明顯名字雖然同樣,方法內部代碼不同,分支之間做出選擇的判斷條件值是在運行時肯定.這種條件一般是某種能力檢測的結果,目的在於確保運行代碼的 js 環境準確地實現了所須要的條件特性.
本例中,具體的條件判斷步驟是這樣的: 使用 try{...} catch{...}
來逐一嘗試每種 XHR 對象,直到遇到一個當前 js 環境所支持的對象爲止.
使用該 API,只要調用SimpleXhtFacyory.createXhtObject();
就能獲得適合特定的運行時環境的 XHR 對象.用了分支技術,全部那些特性嗅探代碼只會執行一次,不是每生成一個對象就要執行一次.
使用單體,一則,提供命名空間,二則,加強其模塊性.
單體模式幾乎適用於全部大大小小的項目,在簡單快開發的項目中,能夠只把單體用做命名空間,將本身的全部代碼組織在一個全局變量名下;在稍大稍複雜的項目中,把單體用來把相關代碼組織在一塊兒以便往後維護;在大型項目中:那些開銷較大卻不多使用的組件能夠被包裝到惰性加載單體中,而針對特定環境的代碼能夠被包裝到分支型單體中.
幾乎全部項目都會用到某種形式的單體,js 的靈活性使得單體能夠被用於多種不一樣任務,它在 js 當中的重要性大大超過他在其餘語言中的重要性.由於它能夠用來建立命名空間以減小全局變量的數目.因爲全局變量在 js 中很容易被其餘人重寫,因此至關危險,單體模式能夠很好的解決這種問題.
主要好處在於對代碼的組織.
把相關方法和屬性組織在一個不會被屢次實例化的單體中,可使得代碼的調試和維護更輕鬆.單體能夠把你的代碼和第三方庫代碼,廣告代碼哥離開,提升網頁的穩定性.
單體的一些高級變體能夠在開發週期的後期用於對腳本進行優化,提高性能.
惰性實例化,能夠直到須要一個對象的時候才建立它,從而減小哪些不須要他的用戶承受的沒必要要的內存消耗.
分支技術能夠根據運行時條件肯定賦給單體變量的對象字面量,建立出爲特定環境量身定製的方法,不會在每次調用時都一再浪費時間去檢查運行環境.
主要的客觀缺點:
單體提供的是一種單點訪問,因此可能致使模塊間強耦合,不利於單元測試.沒法單獨測試一個調用了來自單體的方法的類,只能把她與那個單體做爲一個單元一塊兒測試.
而對於劃分命名空間,實現分支型方法這些用途,耦合不是什麼問題.
有時候某些其餘更高級的模式比單體高級變體更符合任務須要.
虛擬代理與惰性加載單體,能夠給予你對類實例化方式更多的控制;還能夠是用一個對象工廠來取代分支型單體.
做爲 js 中最基本的模式,它不只能夠單獨使用,還能和大多數其餘模式配合使用.例如,對象工廠能夠被設計爲單體,組合對象的全部子對象也能夠被封裝進一個單體命名空間中.本書講的是如何建立可重用的模塊化代碼,單體對全局變量的減小具備重要做用.