摘要:繼承在中繼承較為復雜比其它語言的繼承要復雜在大多數面向對象語言中繼承一個類只要使用一個關鍵字即可而要傳承公有成員的話需要使用靈活微妙的原型繼承或者標準的類繼承本文第一部分將討論中創建子類的各種技術以及它們的使用場合為什么需要繼承先看看繼承能
繼承
先看看繼承能帶來的好處.設計類的時候,希望能減少重復性的代碼,盡量弱化對象間的耦合.使用繼承符合前一個原則.可以在現有類的基礎上進行設計并充分利用他們已經具備的各種方法.
讓一個類繼承另一個類可能會導致二者產生強耦合,一個類依賴于另一個類的內部實現.接下來會講到如何避免.比如用摻元類為其他類提供方法...等等.
通過用函數來聲明類,用關鍵字 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();
所以說,類式繼承的復雜性只局限于類的聲明,創建新實例的過程仍然很簡單.
extend 函數為了簡化類的聲明,可以把派生子類的整個過程包裝在一個名為 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 函數究竟是什么樣呢:
// 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 對象現在我們要創建一個單體,他有一個用來生成 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 中最基本的模式,它不僅可以多帶帶使用,還能和大多數其他模式配合使用.
例如,對象工廠可以被設計為單體,組合對象的所有子對象也可以被封裝進一個單體命名空間中.
本書講的是如何創建可重用的模塊化代碼,單體對全局變量的減少具有重要作用.
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/90948.html
摘要:鏈式調用精髓在于重用一個初始操作可以把方法的鏈式調用技術寫到自己所寫的整個庫中把自己喜歡的方法串起來調用兩個部分一個創建代表元素的對象的工廠還有一批對這個元素執行某些操作的方法每一個這種方法都可以在方法名前附加一個圓點后加入調用鏈中方法的鏈 鏈式調用 精髓在于重用一個初始操作. 可以把方法的鏈式調用技術寫到自己所寫的整個 js 庫中,把自己喜歡的方法串起來調用.兩個部分:一個創建代表 ...
摘要:針對這樣的客戶,靈雀云除了提供容器云,還會基于容器云提供工具鏈和咨詢服務。第三階段,是上云原生。靈雀云建議,先做邊緣應用系統的微服務化,或者單體直接應用上云。靈雀云會幫助客戶成立專家組,實踐敏捷活動和工具鏈一整套的解決方案。 今天很榮幸能在這里跟大家一起分享下靈雀云在金融行業的云原生解決方案。 CNCF的云原生核心理念是快速交付業務價值,而云原生時代,主要由三駕馬車驅動:容器、DevO...
摘要:使用異步編程,有一個事件循環。它作為面向對象編程的替代方案,其中應用狀態通常與對象中的方法搭配并共享。在用面向對象編程時遇到不同的組件競爭相同的資源的時候,更是如此。 翻譯:瘋狂的技術宅原文:https://www.indeed.com/hire/i... 本文首發微信公眾號:jingchengyideng歡迎關注,每天都給你推送新鮮的前端技術文章 不管你是面試官還是求職者,里面...
摘要:上次的訪談,介紹了下可愛的依云醬,回憶傳送門。這里簡單地介紹下龍女仆,全名小林家的龍女仆,為什么介紹這部劇呢因為設計獅顏值同學也安利了這部。劇情簡介在獨身又勞累的小林劃重點一名程序員身邊突然出現的穿著女仆服裝的美少女托爾。 showImg(https://segmentfault.com/img/bVR6p5?w=900&h=385); 上次的訪談,介紹了下可愛的依云醬,回憶傳送門。不...
摘要:上次的訪談,介紹了下可愛的依云醬,回憶傳送門。這里簡單地介紹下龍女仆,全名小林家的龍女仆,為什么介紹這部劇呢因為設計獅顏值同學也安利了這部。劇情簡介在獨身又勞累的小林劃重點一名程序員身邊突然出現的穿著女仆服裝的美少女托爾。 showImg(https://segmentfault.com/img/bVR6p5?w=900&h=385); 上次的訪談,介紹了下可愛的依云醬,回憶傳送門。不...
閱讀 1892·2021-11-23 09:51
閱讀 1535·2021-11-19 09:40
閱讀 3208·2021-11-11 11:01
閱讀 1105·2021-09-27 13:34
閱讀 1835·2021-09-22 15:56
閱讀 2122·2019-08-30 15:52
閱讀 1061·2019-08-30 14:13
閱讀 3473·2019-08-30 14:10