title: JavaScript學習記錄三
toc: true
date: 2018-09-14 23:51:22html
——《JavaScript高級程序設計(第2版)》學習筆記設計模式
要多查閱MDN Web 文檔數組
工廠模式是軟件工程領域廣爲人知的一種設計模式,這種模式抽象了建立具體對象的過程。瀏覽器
用函數來封裝以特定接口建立對象的細節:閉包
function createPerson(name, age, job) { var o = new Object; o.name = name; o.age = age; o.jpb = job; o.sayName = function() { alert(this.name); }; return o; } var person1 = createPerson("Nicholas", 29, "Software Engineer"); var person2 = createPerson("Greg", 27, "Doctor"); person1.sayName(); // "Nicholas" person2.sayName(); // "Greg"
工廠模式雖然解決了建立多個類似對的問題,卻沒有解決對象識別的問題。app
咱們能夠建立自定義的構造函數,從而定義自定義對象類型的屬性和方法。函數
function Person(name, age, job) { this.name = name; this.age = age; this.jpb = job; this.sayName = function() { alert(this.name); }; } var person1 = new Person("Nicholas", 29, "Software Engineer"); var person2 = new Person("Greg", 27, "Doctor"); person1.sayName(); // "Nicholas" person2.sayName(); // "Greg"
構造函數模式與工廠模式的區別:學習
沒有顯示地建立對象(new Object())this
函數名首字母大寫.net
要建立Person的新實例,必須使用new操做符。以這種方式調用構造函數實際上會經歷4個步驟:
這樣經過構造函數模式建立的兩個對象都有一個constructor(構造函數)屬性,該屬性指向Person:
person1.constructor == Person; // true person1 instanceof Person; // true person1 instanceof Object; // true, 由於全部對象均繼承自Object
建立自定義的構造函數意味着未來能夠將它的實例標識爲一種特定的類型,這正是構造函數模式優於工廠模式的地方。
前邊例子中的Person()函數能夠經過下邊任何一種方式來調用:
// 當作構造函數使用 var person = new Person("Nicholas", 29, "Software Engineer"); person.sayName(); // 做爲普通函數調用 Person("Greg", 27, "Doctor"); // 添加到window window.sayName(); // "Greg" //在另外一個對象的做用域中調用 var o = new Obeject(); Person.call(o, "Kristen", 25, "Nurse"); o.sayName(); // "Kristen"
使用構造函數的主要問題,是每一個方法都要在每一個實例上從新建立一遍,這是沒有必要的,所以Person()能夠像下邊這樣定義:
function Person(name, age, job) { this.name = name; this.age = age; this.jpb = job; this.sayName = sayName; } function sayName() { alert(this.name); }
可是這樣的話,在全局做用域中定義的函數(sayName())只能被 某個對象調用,這讓全局做用域有點名存實亡,並且若是對象須要定義不少方法,那麼就要定義不少個全局函數,這樣咱們自定義的引用類型就毫無封裝性可言。
可是這些問題能夠經過使用原型模式解決。
關於prototype能夠先看這一篇。
而後看下邊這個例子:
function Person() {} Person.prototype.name = "Nicolas"; Person.prototype.age = 29; Person.prototype.job = "Software Engineer"; Person.prototype.sayName = function(){ alert(this.name); }; var person1 = new Person(); person1.sayName(); // "Nicolas" var person2 = new Person(); person2.sayName(); // "Nicolas" person1.sayName == person2.sayName; // true
在原型模式下,對象調用這些屬性和方法時,其實是調用prototype的屬性和方法。
默認狀況下,全部prototype屬性都會自動得到一個constructor(構造函數)屬性,這個屬性包含一個指向prototype所在函數的指針。
若是person1的__proto
指向Person的prototype
,則
Person.prototype.isPrototypeOf(person); // true
當爲對象實例添加一個屬性時,這個屬性就會屏蔽源性對象中保存的同名屬性,但不會修改那個屬性。
若是將爲對象實例添加的這個屬性設爲null,也只會在實例中設置這個屬性,而不會恢復其指向原型的鏈接。
要想從新訪問原型中的屬性,可使用delete操做符徹底刪除實例屬性,
使用hasOwnProperty()能夠檢測一個屬性是否存在於實例中(這個方法是從Object繼承來的),若是是原型屬性則返回false:
function Person() {} Person.prototype.name = "Nicolas"; Person.prototype.age = 29; Person.prototype.job = "Software Engineer"; Person.prototype.sayName = function(){ alert(this.name); }; var person1 = new Person(); var person2 = new Person(); person1.hasOwnProperty("name"); // false person1.name = "Greg"; person1.name; // "Greg"————來自實例 person2.name; // "Nicolas"————來自原型 person1.hasOwnProperty("name"); // true person2.hasOwnProperty("name"); // false delete person1.name; person1.name; // "Nicolas"————來自原型 person1.hasOwnProperty("name"); // false
in操做符會在經過對象可以訪問給定屬性時返回true,不管該屬性存在於實例中仍是原型中。所以對於上面的例子,在person1和person2聲明後,不管什麼時候調用"name" in person1
或"name" in person2
都會獲得true。
所以,在hasOwnPrototype()返回false而使用in操做符返回true時,就說明這個屬性是原型屬性。
in操做符還能夠經過for-in循環使用,返回的是全部能經過對象訪問的、可枚舉的(enumerated)屬性和方法。
原型中不可枚舉的屬性和方法(即設置了[[DontEnum]]標記的屬性和方法)有hasOwnProperty()、propertyIsEnumerable()、toLocalString()、toString()和valueOf(),有的瀏覽器也爲constructor和prototype打上標記,
可是當咱們在實例中添加這些屬性和方法從而屏蔽了原型中的這些屬性和方法時,那麼這些屬性和方法就會被認爲是可枚舉的(IE中除外):
var o = { toString: function() { return "My Object"; } }; for (var prop in o) { if (prop == "toString") { alert("Found toString"); // 在IE中不會顯示,其餘瀏覽器顯示 } }
每添加一個屬性和方法就要敲一遍Person.prototype是沒必要要的,同事也爲了從視覺上更好地封裝原型的功能,更常見的作法是用一個包含全部屬性和方法的對象字面量來重寫整個原型對象:
function Person() {} Person.prototype = { /* 重寫prototype會致使其constructor等於Object, * 若constructor的值很重要,能夠給constructor設置回適當的值 */ constructor: Person, name: "Nicholas", age: 29, job: "Software Engineer", sayName: function(){ alert(this.name); } }; var person = new Person(); person.constructor == Person; // 如果添加了上邊constructor那一句則爲true
因爲在原型中查找值的過程是一次搜索,所以對原型對象的修改都可以當即從實例中反映出來,
可是若是像上邊的例子同樣重寫了原型,在重寫原型以前聲明的實例的__proto__
指向的還是最初的原型:
function Person() {} var person = new Person(); Person.prototype.sayHi = function() { alert("hi"); }; person.sayHi(); // "hi",沒有問題 Person.prototype = { constructor: Person, name: "Nicholas", age: 29, job: "Software Engineer", sayName: function(){ alert(this.name); } }; person.sayHi(); // "hi",沒有問題 person.sayName(); //error
全部原生的引用類型,都是採用原型模式建立的。所以咱們亦能夠對原生引用類型的prototype添加屬性或方法。
以String爲例:
String.prototype.startsWith = function(text) { return this.indexOf(text) == 0; }; var msg = "Hello World!"; msg.startsWith("Hello"); // true
可是不建議在產品化的程序中修改原生對象的原型。
若是一個原型的屬性包含引用類型值時,實例對該屬性進行操做時,實際上修改的就是原型中的屬性(引用類型對象名能夠看作指針),所以當其餘實例訪問該屬性時,獲得的就是這個實例修改後的值:
function Person() {} Person.prototype = { constructor: Person, name: "Nicholas", age: 29, job: "Software Engineer", friends: ["Shelby", "Court"], // 屬性值爲引用類型 sayName: function(){ alert(this.name); } }; var person1 = new Person(); var person2 = new Person(); person1.friends.push("Van"); person1.friends; // ["Shelby", "Court", "Van"] person2.friends; // ["Shelby", "Court", "Van"] person1.friends == person2.friends; // true
使用構造函數模式定義實例屬性,原型模式定義方法和共享的屬性,
這樣每一個實例都會有自已的一份實例屬性的副本,又共享着對方法的引用,最大限度地節省了內存,還能夠向構造函數傳遞參數:
function Person(name, age, job) { this.name = name; this.age = age; this.job = job; this.friends = ["Shelby", "Court"]; } Person.prototype = { constructor: Person, sayName: function(){ alert(this.name); } }; var person1 = new Person("Nicholas", 29, "Software Engineer"); var person2 = new Person("Greg", 27, "Doctor"); person1.friends.push("Van"); person1.friends; // ["Shelby", "Court", "Van"] person2.friends; // ["Shelby", "Court"] person1.friends == person2.friends; // false person1.sayName == person2.sayName; // true
這種混合使用的模式是ECMAScript中使用最普遍、認同度最高的自定義類型的方法。能夠說是一種默認模式。
這種模式把全部信息都封裝在了構造函數中,並在構造函數中經過檢查某個應該存在的方法是否有效,來決定是否須要初始化模型:
function Person(name, age, job) { // 屬性 this.name = name; this.age = age; this.job = job; // 方法 // 只有在sayName()方法不存在時纔將其添加到原型中 // 即只有在初次調用構造函數時纔會執行下面的代碼 // if語句只須要判斷一個方法(例如sayName)是否存在 if (typeof this.sayName != "function") { Person.prototype = { constructor: Person, sayName: function() { alert(this.name); }, sayHi: function() { alert("hi"); } }; } }
寄生構造函數模式和穩妥構造函數模式,寄生構造模式沒有什麼意義這裏就再也不贅述,穩妥構造函數模式至關於爲引用類型添加了private屬性,有興趣能夠自行搜索。
在ECMAScript中沒法實現接口繼承(與函數沒法重載的理由相同,ECMAScript中的函數沒有簽名),
可是能夠利用原型鏈實現實現繼承。
除了這一篇講到的,還應注意:
instanceof
和isPrototypeOf()
又叫僞造繼承或經典繼承。
在子類型構造函數獲得內部利用調用超類型的構造函數,還能夠傳遞參數。
function SuperType(name) { this.name = name; } function SubType() { // 繼承了SuperType,同時還傳遞了參數 SuperType.call(this, "Nicholas"); // 實例屬性 this.age = 29; } var instance = new SubType(); instance.name; // "Nicholas" instance.age; // 29
可是若是方法都在構造函數中定義,函數複用就無從談起了。
combination inheritance,僞經典繼承,組合使用原型鏈和借用構造函數。
使用原型鏈實現原型屬性和方法的繼承,經過借用構造函數實現實例屬性的繼承,
這樣既能夠實現函數複用,又能保證每一個實例都有它本身的屬性。
同時,instanceof
和isPrototypeOf
也能識別基於組合繼承建立的對象。
function SuperType(name) { this.name = name; this.colors = ["red", "green", "blue"]; } SuperType.prototype.sayName = function() { alert(this.name); }; function SubType(name, age) { // 繼承屬性 SuperType.call(this, name); this.age = age; } // 繼承方法 SubType.prototype = new SuperType(); SubType.prototype.sayAge = function(){ alert(this.age); }; var instance1 = new SubType("Nicholas", 29); instance1.colors.push("black"); instance1.colors; // ["red", "green", "blue", "black"] instance1.sayName(); // "Nicholas" instance1.sayAge(); // 29 var instance2 = new SubType("Greg", 27); instance1.colors; // ["red", "green", "blue] instance1.sayName(); // "Greg" instance1.sayAge(); // 27
組合繼承融合了前二者的優勢,所以成爲JavaScript中最經常使用的繼承模式。
主要用於只是想讓一個對象與另外一個對象保持相似,沒有必要興師動衆地建立構造函數。
function object(o) { function F() {} F.prototype = o; return new F(); }
這樣子其實是object()函數對傳入的對象執行了一次淺複製:
var person = { name: "Nicholas", friends: ["Shelby", "Court", "Van"]; }; var anotherPerson = object(person); anotherPerson.name = "Greg"; anotherPerson.friends.push("Rob"); person.friends; // ["Shelby", "Court", "Van", "Rob"] person.name; // "Nicholas" anotherPerson.name; // "Greg"
寄生式,parasitic。
思路與寄生構造函數和工廠模式相似,建立一個僅用於封裝繼承過程的函數,在函數內部以某種方式來加強對象。
可是也會由於作不到函數複用而下降效率。
適用於主要考慮對象而不是自定義類型和構造函數的狀況:
function createAnother(original) { // 經過調用函數建立一個新對象,不必定使用object()函數 var clone = object(original); // 以某種方式加強這個對象 clone.sayHi = function() { alert("hi"); }; // 返回這個對象 return clone; } var person = { name: "Nicholas", friends: ["Shelby", "Court", "Van"]; }; var anotherPerson = createAnother(person); anotherPerson.sayHi(); // "hi"
對於爲何要寄生組合式繼承,看了這篇文章還有知乎上的一些回答,主要的優點是組合繼承兩次調用了構造函數,而寄生只使用了一次。
剛開始不理解的是,爲何在建立超類型原型副本時對超類型原型的實例化就不算調用構造函數呢?
後來仔細想了一下,的確能夠不算調用了構造函數——
object()函數內的臨時類型F的構造函數爲空(function F() {}
),所以能夠忽略不計。
如下是代碼:
function object(o) { // 主要區別就是這裏,構造函數的不一樣 function F() {} F.prototype = o; return new F(); } function inheritPrototype(subType, superType) { var prototype = object(superType.prototype); // 拷貝原型 prototype.constructor = subType; // 彌補因重寫prototype而失去的默認的constructor屬性 subType.prototype = prototype; // 替換子類型原型 } function SuperType(name) { this.name = name; this.colors = ["red", "green", "blue"]; } SuperType.prototype.sayName = function() { alert(this.name); }; function SubType(name, age) { // 繼承屬性 SuperType.call(this, name); this.age = age; } // 寄生組合式繼承 inheritPrototype(SubType, SuperType); SubType.prototype.sayAge = function(){ alert(this.age); };
沒有名字的函數,也成爲拉姆達(lamda)函數。
像
var functionName = function(arg0, arg1, arg2) { // 函數體 }
這樣的函數表達式至關於建立了一個匿名函數,而後將這個匿名函數賦給一個變量。
將函數做爲參數傳入另外一個函數,或者從一個函數中返回另外一個函數時,一般都是用匿名函數。
(雖然不知道爲何這本書要在這裏再講一遍這個,也許可能意思是callee指向的其實是匿名函數,無論怎麼樣複習一下callee吧)
前邊在講到函數內部對象arguments的屬性callee(指向擁有這個arguments的函數)時有提到過遞歸階乘函數這個例子:
function factorial(num) { if (num <= 1) { return 1; } else { return num * arguments.callee(num-1); // 建議 // return num * factorial(num-1); // 不建議 } } var anotherFactorial = factorial; factorial = null; anotherFactorial(4); // 使用callee這裏結果爲24, 函數內使用factorial這裏會出錯
有些人會分不清閉包和匿名函數。
閉包指的是有權訪問另外一個函數做用域的函數。
建立閉包的常見方式是在一個函數內部建立另外一個函數。
首先先回顧一下做用域鏈(scope chain)。
當一個函數第一次被調用時,會建立一個執行環境(execute context)及相應的做用域鏈,並將做用域鏈賦值給一個特殊的內部屬性[[Scope]]。
而後,使用this、arguments和其餘命名參數的值來初始化函數的活動對象(activation object)。
這個活動對象處於做用域鏈的頂端,外部函數的活動對象處於第二位,外部函數的外部函數的活動對象處於第三位,... ... 直到全局執行環境的變量對象處於做用域鏈終點。
通常來講,當函數執行完畢後,局部活動對象就會被銷燬,內存中僅保存全局做用域(全局執行環境的變量對象)。
可是,閉包的狀況又有所不一樣。
在另外一個函數內部定義的函數會將外部函數的活動對象添加到它的做用域鏈中,當外部函數執行完畢後,若是內部的這個函數還未執行,即其做用域鏈還在引用外部函數的活動對象時,這個活動對象就不會被銷燬。
知道內部的這個函數執行完畢,外部函數的活動對象纔會隨之一塊兒銷燬。
因爲閉包會攜帶包含它的函數的做用域,所以回比其它函數佔用更多內存,所以建議只有在必要時再考慮使用閉包。
做用域鏈的這種配置機制有一個反作用:閉包只能取得包含函數的任何變量的最後一個值。
function createFunctions() { var result = new Array(); for (var i = 0; i < 10; i++) { result[i] = function() { return i; }; } return result; } var funcs = createFunctions(); // 每一個函數都輸出10 for (var i = 0; i < funcs.length; i++) { document.write(funcs[i]() + "<br />"); }
由於每一個函數的做用域鏈都保存着createFunctions()的活動對象,所以它們引用的都是同一個變量i,
當createFunctions()函數返回後,變量i的值爲10,
因此每一個函數內部的i都是10。
能夠經過建立另外一個匿名函數強制讓閉包行爲符合預期:
for (var i = 0; i < 10; i++) { result[i] = (function(num) { return function(){ return num; }; })(i); }
在這裏,定義了一個當即執行的匿名函數,並將它的結果賦給數組。
在當即執行時,傳入了變量i,又由於函數參數是按值傳遞的,所以就會將i的當前值賦給num。
而這個函數內部,又建立並返回了一個訪問num的閉包。
這樣,result數組中每一個函數都有一個本身的num變量的副本,就能夠返回不一樣的值了。
在閉包中使用this對象也可能致使一些問題。
this對象是在運行時基於函數的運行環境綁定的:
匿名函數的執行環境具備全局性,若是經過call()或者apply()改變環境執行環境,this會指向其餘環境,但一般this指向window。
arguments也有一樣的問題,
所以若是想訪問做用域中的this和arguments對象,必須將對它們的引用保存到另外一個閉包可以訪問的變量中,而後就可讓閉包訪問該對象了,以this爲例:
var name = "The Window"; var object = { name: "My Object", getNameFunc1: function() { return function() { return this.name; } }, getNameFunc2: function() { var that = this; return function() { return that.name; } } }; object.getNameFunc1(); // "The Window" object.getNameFunc2(); // "My Object"
因爲IE對JScript對象和COM(組件對象模型)對象使用不一樣的垃圾收集例程,所以閉包在IE中可能會致使問題。
若是閉包的做用域鏈中保存着一個HTML元素,那麼就意味着該元素沒法被銷燬:
function assignHansdler() { var element = document.getElementById("someElement"); element.onclick = function() { alert(element.id); }; }
以上代碼建立了一個做爲element元素事件處理程序的閉包,而這個閉包又建立了一個循環引用。
因爲匿名函數保存了一個對assignHandler()的活動對象的引用,所以就會致使沒法減小element的引用數。
只要匿名函數存在,element的引用數至少也是1,所以它佔用的內存永遠都不會被回收。
能夠用以下方式解決:
function assignHansdler() { var element = document.getElementById("someElement"); var id = element.id; element.onclick = function() { alert(id); }; element = null; }
這樣就消除了循環引用。
須要注意的是,即便閉包不直接引用element,包含函數的活動對象中也仍然會保存一個引用。
所以 ,有必要把element設爲null。
JavaScript在遇到屢次聲明一個變量的狀況時,會自動忽略後邊的聲明,可是會執行後邊聲明中的初始化。
JavaScript沒有塊級做用域的概念,
所以塊語句中定義的變量,其實是在包含函數中而不是語句中建立的。
能夠用匿名函數來模仿塊級做用域(私有做用域)來避免這個問題:
(function() { // 塊級做用域 })();
須要注意的是,JavaScript將function當作一個函數聲明的開始,而函數聲明後邊是不能跟括號的。
所以上邊代碼中函數外面包括的括號不能省略。這樣能夠把函數聲明轉換成函數表達式。
不管在什麼地方,只要臨時須要一些變量,就可使用私有做用域。
在匿名函數中的任何變量,都會在執行結束時銷燬。
咱們應該經過創造私有做用域來儘可能少地向全局做用域添加變量和函數,以避免致使命名衝突。
除了前邊提到的穩妥構造函數模式,還能夠:
在構造函數中定義特權方法:
function MyObject() { // 函數的私有變量 var privateVariable = 10; // 函數的私有函數 function privateFunction() { return false; } // 特權方法 this.publicMethod = function() { privateVariable++; return privateFunction(); }; }
在建立MyObject實例後,除了publicMethod沒有任何方法能夠直接訪問privateVariable和privateFunction()。
或者利用私有和特權成員,隱藏那些不該該被直接修改的數據:
function Person(name) { this.getName = function() { return name; }; this.setName = function(value) { name = value; } } var person = new Person("Nicholas"); person.getName(); // "Nicholas" person.setName("Greg"); person.getName(); // "Greg"
私有變量name在每個實例的做用域中都不相同,由於每次調用構造函數都會從新建立這兩個方法。
可是這樣使用構造函數會有構造函數模式的缺陷:沒法方法複用。每次建立實例都會建立一樣一組方法,用靜態私有變量來實現特權方法就能夠解決這個問題。
(function() { var name = ""; // 沒有使用var聲明,所以爲全局變量 Person = function(value) { name = value; } Person.prototype.getName = function() { return name; } Person.prototype.setName = function(value) { name = value; } })(); var person1 = new Person("Nicholas"); person1.getName(); // "NIcholas" person1.setName("Greg"); person1.getName(); // "Greg" var person2 = new Person("MIchael"); person1.getName(); // "MIchael" person2.getName(); // "MIchael"
在這種模式下,name就變成了靜態的、由全部實例共享的屬性。
所以每次改變name改變的是全部實例的name。
這樣創造靜態私有變量會由於使用原型而增進代碼複用,但每一個實例都沒有本身的私有變量。
所以使用哪一個方法還要視具體狀況而定。
多查找做用域鏈的一個層次會必定程度上影響查找速度,這正是閉包和私有變量的一個明顯的不足之處。
對於私有變量,我認爲可使用二者組合的模式,不知道對不對,這裏貼出想法,歡迎指正(zmj原創,轉載需註明出處):
function Person(name) { this.getName = function() { return name; }; this.setName = function(value) { name = value; } } (function() { var teacher = "Nicholas"; // 初始化 Person.prototype.getTeacher = function() { return teacher; } Person.prototype.setTeacher = function(value) { teacher = value; } })();
這樣,就既有實例本身的私有變量,也有靜態私有變量了。
模塊模式(module pattern)是爲單例(singleton)建立私有變量和私有方法。
所謂單例就是隻有一個實例的對象,通常以對象字面量的方式來建立:
var singleton = { name: value, method: function() { // 這裏是方法的代碼 } };
模塊模式經過爲單例添加私有變量和特權方法來使其加強:
var singleton = function() { // 私有變量和私有函數 var privateVariable = 10; function privateFunction() { return false; } // 特權/公有方法和屬性 return { publicProperty: true, publicMethod: function() { privateVariable++; return privateFunction(); } }; }();
這種模式在須要對單例進行某些初始化,同時又須要維護其私有變量時是十分有用的:
function BaseComponent() {} function OtherComponent() {} var application = function() { // 私有變量和函數 var components = new Array(); // 初始化 components.push(new BaseComponent()); // 公共 return { getComponentCount: function() { return components.length; }, registerComponent: function(component) { if (typeof component == "object") { components.push(component); } } }; }(); application.registerComponent(new OtherComponent()); application.getComponentCount(); // 2
在Web應用程序中,常用一個單例來管理應用程序級的信息。
以這種模式建立的單例都是Object的實例。
若是單例必須是某種類型的實例,還必須添加某些屬性和/或方法加以加強,可使用加強的模塊模式:
function BaseComponent() {} var application = function() { // 私有變量和函數 var components = new Array(); // 初始化 components.push(new BaseComponent()); // 創造application的一個局部副本 var app = new BaseComponent(); // 公共接口 app.getComponentCount: function() { return components.length; }; app.registerComponent: function(component) { if (typeof component == "object") { components.push(component); } }; // 返回這個副本 return app; }();