摘要:與構造函數模式不同的是,新對象的這些屬性和方法是由所有實例共享。當調用構造函數創建一個新實例后,該實例的內部將包含一個指針內部屬性,指向構造函數的原型對象。
本文記錄了我在學習前端上的筆記,方便以后的復習和鞏固。
ECMAScript中沒有類的概念,因此它的對象也與基于類的語言的對象有所不同。
ECMA-262把對象定義為:"無序屬性的組合,其屬性可以包含基本值,對象或者函數。"對象的每個屬性或方法都有一個名字,而每個名字映射到一個值。我們可以把ECMAScript的對象想象成散列表:無非就是一組名值對,其中值可以使數據或函數。
每個對象都是基于一個引用類型創建的,這個引用類型可以使原生類型,也可以是開發人員定義的類型
創建自定義對象最簡單的方式就是創建一個Object的實例,然后再為它添加屬性和方法
var person = new Object(); person.name = "Jason"; person.age = 18; person.job = "Web"; person.sayName = function() { console.log(this.name); };
對象字面量創建:
var person = { name: "Jason", age: 18, job: "Web", sayName = function() { console.log(this.name); } }
這兩個方法的person對象是一樣的,都有相同的屬性和方法,這些屬性在創建的時都帶有一些特征值(characteristic),JavaScript通過這些特征值來定義它們的行為。
1.1 屬性類型ECMAScript中有兩種屬性:數據屬性和訪問器屬性
數據屬性
數據屬性包含一個數據值的位置。在這個位置可以讀取和寫入值。數據屬性有4個描述其行為的特性。
[[Configurable]]: 表示能否通過delete刪除屬性從而重新定義屬性,能否修改屬性的特性,或者能否把屬性修改為訪問器屬性。直接在對象上定義的屬性,它們的這個特性默認值為true。
[[Enumerable]]: 表示能否通過for-in循環返回屬性。直接在對象上定義的屬性,它們的這個特性默認值為true。
[[Writable]]: 表示能否修改屬性的值。直接在對象上定義的屬性,它們的這個特性默認為true。
[[Value]]: 包含這個屬性的數據值。讀取屬性值的時候,從這個位置讀;寫入屬性值的時候。把新值保存在這個位置。這個特性的默認值為undefined。
要修改屬性默認的特性,必須使用ECMAScript5的Object.defineProperty()方法。這個方法接收三個參數: 屬性所在的對象、屬性的名字和一個描述符對象。其中,描述符(descriptor)對象的屬性必須是: configurable、enumerable、writable和value。設置其中的一或多個值,可以修改對應的特征值。
var person = {}; Object.defineProperty(person, "name", { writable: false, //不能修改屬性的值.... configurable: false, //不能通過delete刪除屬性..... value: "Jason" //寫入屬性值 }); console.log(person.name); //Jason person.name = "Cor"; console.log(person.name); //Jason delete person.name; console.log(person.name); //Jason
一旦把屬性定義為不可以配置的,就不能再把它變回可配置的了。此時再調用Object.defineProperty()方法修改除writable之外的特性,都會導致錯誤
var person = {}; Object.defineProperty(person, "name", { configurable: false, value: "Jason" }); //拋出錯誤 Object.defineProperty(person, "name", { comfogirable: true, //這行代碼修改了特性導致報錯 value: "Cor" });
也就是說,可以多次調用Object.defineProperty()方法修改同一屬性,但在吧configurable特性設置為false之后就會有限制了。
注意:在調用Object.defineProperty()方法時,如果不指定,configurable、enumerable和writable特性的默認值都是false。
訪問器屬性
訪問器屬性不包含數據值;它們包含一對兒getter和setter函數(不過,這兩個函數都不是必需的)。在讀取訪問器屬性時,會調用getter函數,這個函數負責返回有效的值;在寫入訪問器屬性時,會調用setter函數并傳入新值,這個函數負責決定如何處理數據。訪問器屬性有如下4個特性。
[[Configurable]]: 表示能否通過delete刪除屬性從而重新定義屬性,能否修改屬性的特性,或者能否把屬性修改為數據屬性。對于直接在對象上定義的屬性,這個特性默認值為true。
[[Enumerable]]: 表示能否通過for-in循環返回屬性。對于直接在對象上定義的屬性,它們的這個特性默認值為true。
[[Get]]: 在讀取屬性時調用的函數。默認值為undefined。
[[Set]]: 在寫入屬性時調用的函數。默認值為undefined。
訪問器屬性不能直接定義,必須使用Object.defineProperty()來定義。
var book = { _year: 2004, edition: 1 }; Object.defineProperty(book, "year", { get: function() { return this._year; }, set: function(newValue) { //接受新值的參數 if(new Value > 2004) { this._year = new Value; this.edition += newValue - 2004; } } }); book.year = 2005; //寫入訪問器,會調用setter并傳入新值 console.log(book.edition); //2
不一定非要同時指定getter和setter。只指定getter意味著屬性是不能寫,嘗試寫入屬性會被忽略。在嚴格模式下,嘗試寫入只指定getter函數的屬性會拋出錯誤。同樣只指定setter函數的屬性也不能讀,否則在非嚴格模式下會返回undefined,在嚴格模式下會拋出錯誤。
1.2 定義多個屬性由于為對象定義多個屬性的可能性很大,ECMAScript5又定義了一個Object.defineProperties()方法。利用這個方法可以通過描述符一次定義多個屬性。這個方法接收兩個對象參數: 第一個對象是要添加和修改其屬性的對象,第二個對象的屬性與第一個對象中要添加或修改的屬性一一對應
var book = {}; Object.defineProperties(book, { _year: { value: 2004 }, edition: { value: 1 }, year: { get: function() { return this._year; }, set: function(newValue) { if(newValue > 2004) { this._year = newValue; this.edition += newValue - 2004; } } } });1.3 讀取屬性的特性
使用ES5的Object.getOwnPropertyDescriptor()方法,可以取得給定屬性的描述符。這個方法接收兩個參數:屬性所在的對象和要讀取其描述符的屬性名稱。返回值是一個對象,如果是訪問器屬性,這個對象的屬性有configurable、enumerate、get和set;如果是數據屬性,這個對象的屬性有configurable、enumerable、writable和value。
var book = {}; Object.defineProperties(book, { _year: { value: 2004 }, edition: { value: 1 }, year: { get: function() { return this._year; }, set: function(newValue) { if(newValue > 2004) { this._year = newValue; this.edition += newValue - 2004; } } } }); var descriptor = Object.getOwnPropertyDescriptor(book, "_year"); console.log(descriptor.value); //2004 console.log(descriptor.configurable); //false console.log(typeof descriptor.get); //"undefined" var descriptor = Object.getOwnPropertyDescriptor(book, "year"); console.log(descriptor.value); //"undefined" console.log(descriptor.enumerable); //false console.log(typeof descriptor.get); //"function"2.創建對象
雖然Object構造函數或對象字面量都可以用來創建單個對象,但這些方式有個明顯得缺點:使用同一個接口創建很多對象,會產生大量的重復的代碼。
2.1 工廠模式這種模式抽象了創建具體對象的過程,考慮到ECMAScript中無法創建類,開發人員就發明了一種函數,用函數來封裝以特定接口創建對象的細節。
function createPerson(name, age, job) { var o = new Object(); o.name = name; o.age = age; o.job = job; o.sayName = function() { console.log(this.name); }; return o; } var person1 = createPerson("Jason", 18, "WEB"); var person2 = createPerson("Cor", 19, "WEB");
函數createPerson()能夠根據接受的參數來構建一個包含所有必要信息的Person對象。可以無數次地調用這個函數,而每次它都會返回一個包含三個屬性的對象。工廠模式雖然解決了創建多個相似對象的問題,但卻沒有解決對象識別的問題(就是怎么用知道一個對象的類型)。
2.2 構造函數模式像Object和Array這樣的原生構造函數,在運行時會自動出現在執行環境中。也可以創建自定義構造函數,從而定義對象類型的屬性和方法。
function Person(name, age, job) { this.name = name; this.age = age; this.job = job; this.sayName = function() { console.log(this.name); } } var person1 = new Person("Jason", 18, "WEB"); var person2 = new Person("Cor", 19, "WEB");
構造函數模式和工廠模式存在以下不同之處:
沒有顯示地創建對象;
直接將屬性和方法賦給了this對象;
沒有return語句
像上面創建的Person構造函數。構造函數使用都應該以一個大寫字母開頭,而非構造函數則應該以一個小寫字母開頭。
要創建Person的新實例,必須使用new操作符。這樣調用構造函數實際上會經歷一下4個步驟:
創建一個新對象
將構造函數的作用域賦給新對象(因此this就指向了這個新對象)
執行構造函數中的代碼(為這個新對象添加屬性)
返回新對象。
在前面例子的最后,person1和person2分別保存著Person的一個不同的實例。這兩個對象都有一個constructor(構造函數)屬性,該屬性指向Person。
console.log(person1.constructor == Person); //true console.log(person2.constructor == Person); //true console.log(person1 instanceof Object); //true console.log(person1 instanceof Person); //true console.log(person2 instanceof Object); //true console.log(person2 instanceof Person); //true
創建對的對象既是Object的實例,同時也是Person的實例,上面通過instanceof驗證。
創建自定義的構造函數意味著將它的實例標識一種特定類型;而這正是構造函數模式勝過工廠模式的地方。person1和person2之所以同時是Object的實例,是因為所有對象均繼承自Object。
2.2.1 把構造函數當函數以這種方式定義的構造函數是定義在Global對象(在瀏覽器就是window對象)中的
//當作構造函數使用 var person = new Person("Jason", 18, "web"); person.sayName(); //"Jason" //作為普通函數調用 Person("Cor", 19, "web"); //添加到window window.sayName(); //"cor" //在另一個對象的作用域中調用 var o = new Object(); Person.call(o, "Kristen", 22, "web"); o.sayName(); //"kriten"
當在全局作用域中調用一個函數時,this對象總是指向Global對象(在瀏覽器中的window對象),最后使用了call() ( 或者apply() )在某個特殊對象的作用域中調用Person()函數。這里是在對象o的作用域調用的,因此調用后o就擁有了所有屬性和方法。
2.2.2 構造函數的問題構造函數的主要問題,就是每個方法都要在每個實例上重新創建一遍。在前面例子中,person1和person2都有一個名為sayName()的方法,但那兩個方法不是同一個Function的實例。ECMAScript中的函數是對象,因此每定義一個函數,也就是實例化了一個對象。從邏輯角度講,此時的構造函數也可以這樣定義
function Person(name, age, job) { this.name = name; this.age = age; this.job = job; this.sayName = new Function(console.log(this.name)); //與聲明函數在邏輯上是等價的 }
以這種方式創建函數,會導致不同的作用域鏈和標識符解析,但創建Function新實例的機制仍然是相同的。因此不同的實例上的同名函數是不相等的
console.log(person1.sayName == person2.sayName); //false
function Person(name, age, job) { this.name = name; this.age = age; this.job = job; this.sayName = sayName; } function sayName() { console.log(this.name); } var person1 = new Person("Jason", 18, "WEB"); var person2 = new Person("Cor", 19, "WEB");
在構造函數內部我們把sayName屬性設置成等于全局的sayName函數。由于構造函數的sayName屬性包含的是一個指向函數的指針,因此person1和person2對象就共享了在全局作用域中定義的同一個sayName()函數。
2.3 原型模式我們創建的每個函數都有一個prototype(原型)屬性,這個屬性是一個指針,指向一個對象。而這個對象的用途是包含可以由特定類型的所有實例共享的屬性和方法。如果按照字面意思來理解,那么prototype就是通過調用構造函數而創建的那個對象實例的原型對象。使用原型對象的好處就是可以讓所有對象實例共享它所包含的方法。
function Person() { } Person.prototype.name = "Jason"; Person.prototype.age = 18; Person.prototype.job = "Web"; Person.prototype.sayName = function() { console.log(this.name); } var person1 = new Person(); person1.sayName(); //"Jason" var person2 = new Person(); person2.sayName(); //"Jason" console.log(person1.sayName == person2.sayName); //true
與構造函數模式不同的是,新對象的這些屬性和方法是由所有實例共享。這樣說吧,person1和person2訪問的都是同一組屬性和同一個sayName()函數。
2.3.1 理解原型對象無論什么時候,只要創建了一個新函數,就會根據一組特定的規則為該函數創建一個prototype屬性,這個屬性指向函數的原型對象。在默認的情況下,所有原型對象都會自動獲得一個constructor(構造函數)屬性,這個屬性包含一個指向prototype屬性所在函數的指針,就拿前面的例子來說,Person.prototype.constructor指向Person。而通過這個構造函數,我們還可以繼續為原型對象添加其他屬性和方法。
創建了自定義構造函數之后,其原型對象默認只會取得constructor屬性;至于其他方法,則都是從Object繼承而來的。當調用構造函數創建一個新實例后,該實例的內部將包含一個指針(內部屬性),指向構造函數的原型對象。ECMA-262第五版中管這個指正交[[Prototype]]。雖然在腳本中沒有標準的方式訪問[[Prototype]],但Firefox、Safari和Chrome在每個對象上都支持一個熟悉 __ proto__;而在其他實現中,這個屬性對腳本則是完全不可見的。不過在明確的真正重要一點就是,這個連接存在于實例與構造函數的原型對象之間,而不是存在于實例與構造函數之間。
前面例子如圖:
在此Person.prototype指向了原型對象,而Person.prototype.constructor又指回了Person。原型對象中除了都包含constructor屬性之外,還包含后來添加的屬性。Person的每個實例person1和person2都包含一個內部屬性,該屬性僅僅指向了Person.prototype;換句話說它們與構造函數沒有直接的關系。此外,雖然這兩個實例都不包含屬性和方法,但我們卻可以調用person1.sayName()。這是通過查找對象屬性的過程來實現的。
雖然在所有的實現中都無法訪問到[[Prototype]]可以通過isPrototypeOf()方法來確定對象之間是否存在這種關系。從本質上講,如果[[Prototype]]指向調用isPrototypeOf()方法的對象(Person.prototype),那么這個方法就會返回true
console.log(Person.prototype.isPrototypeOf(person1)) //true console.log(Person.prototype.isPrototypeOf(person2)) //true
ES5增加了一個新方法,叫Object.getPrototypeOf(),在所有支持的實現中,這個方法返回[[Prototype]]的值,可以方便地獲取一個對象的原型
console.log(Object.getPrototypeOf(person1) == Person.prototype); //true console.log(Object.getPrototypeOf(person1).name); //"Jason"
搜索機制:
每當代碼讀取某個對象的某個屬性時,都會執行一次搜索,目標是具有給定名字的屬性。搜索首先從對象的實例本身開始。如果在實例中找到具有給定名字的屬性,則返回該屬性的值;如果沒有找到。則繼續搜索指針指向的原型對象,在原型對象中查找具有給定名字的屬性。如果在原型對象中找到這個屬性,則返回該屬性的值。簡單的說,就是會一層層搜索搜索到則返回,沒搜索到則繼續下層搜索。
原型最初只包含constructor屬性,該屬性也是共享的,因此可以通過對象實例訪問
雖然可以通過對象實例訪問原型的值,但卻不能通過對象實例重寫原型的值。如果我們為對象實例添加了一個熟悉,并且該屬性名和實例原型中的一個屬性同名,就會在實例中創建該屬性,該屬性就會屏蔽原型中的相同屬性。因為上面講過讀取對象的屬性時,會進行搜索,搜索會先從對象實例先開始搜索,因為對象實例有這個屬性,原型就沒必要搜索了,就會返回對象實例的的屬性值。
function Person() { } Person.prototype.name = "Jason"; Person.prototype.age = 29; Person.prototype.job = "Web"; Person.prototype.sayName = function() { console.log(this.name); } var person1 = new Person(); var person2 = new Person(); person1.name = "Cor"; person1.sayName(); //"Cor" person2.sayName(); //"Jason"
如果繼續能夠重新訪問原型中的屬性可以用delete操作符
delete person1.name; person1.sayName(); //"Jason"
使用hasOwnProperty()方法可以檢測一個屬性是存在實例中,還是存在于原型中。這個方法(不要忘了它是從Object繼承來的)只在給定屬性存在于對象實例中時,才會返回true。
console.log(person1.hasOwnProperty("name")); //false person1.name = "Cor"; console.log(person1.hasOwnProperty("name")); //true;2.3.2 原型與in操作符
有兩種方式使用in操作符:多帶帶使用何在for-in循環中使用。在多帶帶使用時,in操作符會在通過對象能訪問給定屬性時返回true,無論屬性存在于實例還是原型中
console.log("name" in person1); true
同時使用hasOwnProperty()方法和in操作符,就可以確定該屬性到底是存在于對象中,還是存在于原型中
function hasPrototypeProperty(object, name) { return !object.hasOwnProperty(name) && (name in object); }
要取得對象上所有可枚舉的實例屬性,可以使用ES5的Object.key()方法。這個方法接收一個對象作為參數,返回一個包含所有可枚舉屬性的字符串數組。
function Person(){} Person.prototype.name = "Jason"; Person.prototype.age = 18; Person.prototype.job = "Web"; Person.prototype.sayName = function() { console.log(this.name); } var keys = Object.keys(Person.prototype); console.log(keys); //"name,age,job,sayName" var p1 = new Person(); p1.name = "cor"; p1.age = 11; var p1keys = Object.keys(p1); console.log(p1keys); //"name,age"
所有你想得到所有實例屬性,無論它是否可以枚舉,都可以使用Object.getOwnPropertyNames()方法。
var keys = Object.getOwnPropertyNames(Person.prototype); console.log(keys); //"constructor,name,age,job,sayName"2.3.3 更簡單的原型語法
可以用一個包含所有屬性和方法的對象字面量來重寫整個原型對象
function Person(){} Person.prototype = { name : "Jason", age : 29, job : "Web", sayName : function() { console.log(this.name) } };
用對象字面的方法和原來的方法會有區別: constructor屬性不再指向Person了。每創建一個函數,就會同時創建它的prototype對象,這個對象也會自動獲得constructor屬性。而對象字面量的寫法,本質上重寫了默認的prototype對象,因此constructor屬性也就變成了新對象的constructor屬性(指向Object構造函數),不再指向Person函數。次數盡管instanceof操作符漢能返回正確的結果,但通過constructor已經無法確定對象的類型了。
var friend = new Person(); console.log(friend instanceof Object); //true console.log(friend instanceof Person); //true console.log(friend.constructor == Person); //false console.log(friend.constructor == Object); //true
如果constructor的值真的很重要,可以像下面這樣特意將它設置回適當的值。
function Person(){} Person.prototype = { constructor : Person, name : "Jason", age : 29, job : "Web", sayName : function() { console.log(this.name); } };
默認情況下,原生的constructor屬性是不可枚舉的,因此如果你使用兼容ES5的JavaScript引擎,可以試下Object.defineProperty()。
function Person(){} Person.prototype = { name : "Jason", age : 29, job : "Web", sayName : function() { console.log(this.name); } }; //重設構造函數,只適用ES5兼容的瀏覽器 Object.defineProperty(Person.prototype, "constructor", { enumerable: false, value: Person });2.3.4 原型的動態性
由于在原型中查找值的過程是一次搜索,因此我們隊原型對象所做的任何修改都能夠立即從實例上反映出來——即使是先創建了實例后修改原型也一樣。
var friend = new Person(); Person.prototype.sayHi = function() { alert("hi"); }; friend.sayHi(); //"hi"
即使person實例是在添加新方法之前創建的,但它仍然可以訪問這個新方法。其原因可以歸結為實例與原型之間松散連接關系。因為實例與原型之間的連接只不過是一個指針,而非一個副本,因此就可以在原型中找到新的sayHi屬性并返回保存在那的函數。
如果是重寫整個原型對象,情況就和上面不一樣了。調用函數時會為實例添加一個指向最初原型的[[Prototype]]指針,而把原型修改為另外一個對象就等于切斷了構造函數與最初原型之間的聯系。請記住:實例中的指針僅指向原型,而不指向構造函數。
function Person(){} var friend = new Person(); Person.prototype = { constructor : Person, name : "Jason", age : 29, job : "Web", sayName : function() { console.log(this.name); } }; friend.sayName(); //error
我們先創建了一個實例,然后重寫了其原型對象。下圖展示了整個過程
如圖,重寫原型對象切斷了現有原型與任務之前已經存在的對象實例之間的聯系;friend實例引用的仍然是最初的原型
2.3.5 原生對象的原型原型模式的重要性不僅體現在創建自定義類型方面,就連所有原生的引用類型。都采用這種模式創建的。所有原生引用類型(Object、Array、String,等等)都在其構造函數的原型上定義了方法。
console.log(Array.prototype.sort); //function(){...} console.log(String.prototype.substring); //function(){...}
通過原生對象的原型,我們也可以自己定義新方法。
String.prototype.startsWith = function(text) { return this.indexOf(text) == 0; }; var msg = "Hello world!"; console.log(msg.startsWith("Hello")); //true2.3.6 原型對象的問題
原型的所有屬性是被很多實例共享的,這種共享對于函數非常合適。原型的問題就是其共享的本性造成的。
function Person(){} Person.prototype = { constructor : Person, name : "Jason", age : 29, job : "Web", friend : ["Cor", "Sam"], sayName : function() { console.log(this.name); } } var person1 = new Person(); var person2 = new Person(); person1.friends.push("Court"); console.log(person1.friends); //"Cor,Sam,Court" console.log(person2.friends); //"Cor,Sam,Court" console.log(person1.friends === person2.friends); //true
通過原型共享雖然全部實例都可以共享屬性和方法。可是實例一般都是要有屬于自己的全部屬性。
2.4 組合使用構造函數模式和原型模式創建自定義類型的最常用方式,就是組合使用構造函數模式與原型模式。構造函數模式用于定義實例屬性,而原型模式用于定義方法和共享的屬性。這樣每個實例都會有自己的一份實例屬性的副本,但同時共享著對方法的引用,最大限度地節省了內存。這種混成模式還支持向構造函數傳遞參數。結合了兩種模式之長。
function Person(name, age, job) { this.name = name; this.age = age; this.job = job; this.friends = ["Cor", "Sam"]; } Person.prototype = { constructor : Person, sayName : function() { console.log(this.name); } }; var person1 = new Person("Jason", 18, "Web"); var person2 = new Person("cou", 19, "doctor"); person1.friends.push("Van"); console.log(person1.friends); //"Cor,Sam,Van" console.log(person2.friends); //"Cor,Sam" console.log(person1.friends === person2.friends); //false console.log(person1.sayName === person2.sayName); //true
構造函數定義屬性,原型定義共享的方法和屬性。這種構造函數與原型混成的模式,是目前在ECMAScript中使用最廣泛,認同度最高的一種創建自定義類型的方法。可以說,這是用來定義引用類型的一種默認模式。
2.5 動態原型模式動態原型模式把所有信息都封裝在了構造函數中,而通過構造函數中初始化原型(僅在必要的情況下),又保持了同時使用構造函數和原型的優點。簡單說可以通過檢查某個應該存在的方法是否有效, 來決定是否需要初始化原型。
function Person(name, age, job) { this.name = name; this.age = age; this.job = job; //方法 if(typeof this.sayName != "function") { Person.prototype.sayName = function() { console.log(this.name); }; } } var friend = new Person("Jason", 18, "Web"); friend.sayName();
方法那塊,只在sayName()方法不存在的情況下,才會將它添加到原型中。這段代碼只會在初次調用中構造函數時才會執行。此后,原型已經完成初始化嗎,不需要再做什么修改了。不過要記住,這里對原型所做的修改,能夠立即在所有實例中得到反映。因此,這種方法確實可以說是非常完美。其中if語句檢查的可以是初始化之后應該存在的任何屬性和方法——不必用一大堆if語句檢查每個屬性的每個方法;只需要檢查一個即可。對于采用這種模式創建的對象,還可以使用instanceof操作符確定它的類型。
2.6 寄生構造函數模式注意:使用動態原型模式時,不能使用對象字面量重寫原型。前面已經解釋過了,如果在已經創建了實例的情況下重寫原型,那么就會切斷現有實例與新原型之間的練習。
在前面幾種模式都不適用的情況下,可以使用寄生構造函數模式。這種模式的基本思想是創建一個函數,該函數的作用僅僅是封裝創建對象的代碼,然后再返回新創建的對象;但從表面上看,這個函數又很像是典型的構造函數。
function Person(name, age, job) { var o = new Object(); o.name = name; o.age = age; o.job = job; o.sayName = function() { console.log(this.name); }; return o; } var friend = new Person("Jason", 19, "Web"); friend.sayName(); //"Jason"
除了使用new操作符并把使用的包裝函數叫做構造函數之外,這個模式跟工廠模式其實是一模一樣的。構造函數在不返回值的情況下,默認會返回新對象的實例。而通過在構造函數的末尾添加一個return語句,可以重寫調用構造函數時返回的值。
這個模式可以在特殊的情況下用來為對象創建構造函數。假設我們想創建一個具有額外方法的特殊數組。由于不能直接修改Array構造函數,因此可以使用這個模式。
function SpecialArray() { //創建數組 var values = new Array(); //添加值 values.push.apply(values, arguments); //添加方法 values.toPipedString = function() { return this.join("|"); }; //返回數組 return values; } var colors = new SpecialArray("red", "blue", "green"); console.log(colors.toPipedString()); //"red|blue|green"
關于寄生構造函數模式,有一點需要說明: 首先,返回的對象與構造函數或者與構造函數的原型之間沒有關系;也就是說,構造函數返回的對象與在構造函數外部創建對象沒有什么不同。不能依賴instanceof操作符來確定對象類型。
2.7 穩妥構造函數模式所謂穩妥對象,指的是沒有公共屬性,而且其方法也不引用this的對象。穩妥對象最適合在一些安全的環境中(這些環境中會禁止使用this和new)
function Person(name, age, job) { //創建要返回的對象 var o = new Object(); //可以定義私有變量和函數 //添加方法 o.sayName = function() { console.log(name)"" } //返回對象 return o; }
注意,在以這種模式創建的對象中,除了使用sayName()方法之外,沒有其他辦法訪問到name的值。可以像下面使用穩妥的Person構造函數
var friend = Person("Jason", 18, "Web"); friend.sayName(); //"Jason"
這樣,變量friend中保存的是一個穩妥對象,除了調用sayName()方法外,沒有別的方式可以訪問其數據成員。
3.繼承由于函數沒有簽名,在ECMAScript中無法實現接口繼承。ECMAScript只支持實現繼承,而且其實現繼承主要是依靠原型鏈來實現的。
3.1 原型鏈原型鏈是實現繼承的主要方法。其基本思想是利用原型讓一個引用類型繼承另一個引用類型的屬性和方法。
回顧下構造函數、原型和實例的關系:
每個構造函數都有一個原型對象;
原型對象都包含一個指向構造函數的指針;
實例都包含一個指向原型對象的內部指針{{Prototype}}
我們讓原型對象等于另一個類型的實例,原型對象將包含一個指向另一個原型的指針,相應地,另一個原型也包含著一個指向另一個構造函數的指針。假如另一個原型又是另一個類型的實例,那么上訴關系依然成立,如此層層遞進,就構成了實例與原型的鏈條。
實現原型鏈的基本模式
function SuperType(){ this.property = true; } SuperType.prototype.getSuperValue = function(){ return this.property; } function SubType(){ this.subproperty = false; } //繼承了SuperType //SubType的原型對象等于SubperType的實例, //這樣SubType內部就會有一個指向SuperType的指針從而實現繼承 SubType.prototype = new SuperType(); SubType.prototype.getSubValue = function(){ return this.subproperty; } var instance = new SubType(); console.log(instance.getSuperValue()); //true
SubType繼承了superType,而繼承是通過創建SuperType實例,并將該實例賦給SubType.prototype實現的。原來存在于SuperType的實例中的所有屬性和方法,現在也存在于SubType.prototype中了。這個例子中的實例、構造函數和原型之間的關系如圖:
要注意instance.constructor現在指向的是SuperType,是因為SubType的原型指向了另一個對象SuperType的原型,而這個原型對象的constructor屬性指向的是SuperType。
通過實現原型鏈,本質上擴展了前面說的原型搜索機制。在通過原型鏈實現繼承的情況下,搜索過程就得以沿著原型鏈繼續向上。拿上面的例子來說。調用instance.getSuperValue()會經歷三個搜索步驟:
搜索實例;
搜索SubType.prototype;
搜索SuperType.prototype,最后一步才會找到該方法。
在找不到屬性或方法的情況下,搜索過程總是要一環一環地前行到原型鏈末端才會停下來。
1. 別忘記默認的原型事實上,前面的例子中展示的原型鏈還少一環。所有引用類型默認都繼承了Object,而這個繼承也是通過原型鏈實現的。要記住,所有函數的默認原型都是Object的實例,因此默認原型都會包含一個內布指針,指向Object.prototype。這也正是所有自定義類型都會繼承toString()、valueOf()等默認方法的根本原因。所以上面的例子展示的原型鏈中還應該包含另一個繼承層次。
一句話,SubType繼承了SuperType,而SuperType繼承了Object。當調用instance.toString()時,實際上調用的是保存在Object.prototype中的那個方法。
2. 確定原型和實例的關系有兩種方式可以確定。
用instanceof操作符
只要用這個操作符來測試實例與原型鏈中出現過的構造函數,結果就會返回true。
console.log(instance instanceof Object); //true console.log(instance instanceof SuperType); //true console.log(instance instanceof SubType); //true
由于原型鏈的關系,可以說instance是Object、SuperType或SubType中任何一個類型的實例。因此,都返回true;
用isPrototypeOf()方法
同樣,只要是原型鏈中出現過得原型,都可以說是該原型鏈所派生的實例的原型。
console.log(Object.prototype.isPrototypeOf(instance)); //true console.log(SuperType.prototype.isPrototypeOf(instance)); //true console.log(SubType.prototype.isPrototypeOf(instance)); //true3. 謹慎地定義方法
子類型(上例的SubType)有時候需要重寫超類型(上例的SuperType)中的某個方法,或者需要添加超類型中不存在的的某個方法。但不管怎樣,給原型添加方法一定要放在替換原型的語句之后。
function SuperType(){ this.property = true; } SuperType.prototype.getSuperValue = function(){ return this.property; } function SubType(){ this.subproperty = false; } //繼承超類型 SubType.prototype = new SuperType; //添加新方法 SubType.prototype.getSubValue = function(){ return this.subproperty; } //重寫超類型中的方法 SubType.prototype.getSuperValue = function(){ return false; }
當通過SubType的實例調用getSuperValue()時,調用的就是這個重寫的方法,這是因為搜索機制搜索首先從實例中搜索然后到子類型的原型再到超類型的原型,然后子類型重寫了該方法,搜索機制首先在子類型的原型中找到了該方法就不會繼續繼續搜索了。但通過SuperType的實例調用getSuperValue()時,還會繼續調用原來的那個方法。這里要注意的是,必須在用SuperType的實例替換原型之后,再定義著兩個方法。
還有一點需要注意,在通過原型鏈實現繼承時,不能使用對象字面量創建原型方法。這樣做會重寫原型鏈。
function SuperType(){ this.property = true; } SuperType.prototype.getSuperValue = function(){ return this.property; } function SubType(){ this.subproperty = false; } //繼承了SuperType SubTyoe.prototype = new SuperType(); //使用字面量添加新方法,會導致上一行代碼無效 SubType.prototype = { getSubValue : function(){ return this.subproperty; }, someOtherMethod : function(){ return false; } } var instance = new SubType(); console.log(instance.getSuperValue()); //error!
由于現在的原型包含的是一個Object的實例,而非SuperType的實例,因此我們設想中的原型鏈已經被切斷了。SubType和SuperType之間已經沒有關系了。
4. 原型鏈的問題原型鏈最主要的問題來自包含引用類型值的原型。包含應用類型值的原型屬性會被所有實例共享;而這也正是為什么要在構造函數中,而不是在原型對象中定義屬性的原因。在通過原型來實現繼承時,原型實際上會變成另一個類型的實例。于是,原先的實例實際上會變成另一個類型的實例。于是,原生的實例屬性也就順理成章地變成了現在的原型屬性了。
function SuperType(){ this.colors = ["red", "blue", "green"]; } function SubType(){ } SubType.prototype = new SuperType(); var instance1 = new SubType(); instance1.colors.push("black"); console.log(instance1.colors); //"red, blue, green, black" var instance2 = new SubType(); console.log(instance2.colors); //"red, blue, green, black"
這個例子中的SuperType構造函數定義了一個colors屬性,該屬性包含一個數組(引用類型值)。SuperType的每個實例都會有各自包含自己數組的colors屬性。當SubType通過原型鏈繼承了SuperType之后,SubType.prototype就變成了SuperType(),所以它也用用了一個colors屬性。就跟專門創建了一個SubType.prototype.colors屬性一樣。結果SubType得所有實例都會共享這一個colors屬性。
原型鏈的第二個問題是:在創建子類型的實例時,不能向超類型的構造函數傳遞參數。實際上,應該說是沒有辦法在不影響所有對象實例的情況下,給超類型的構造函數傳遞參數。所以在實際運用中很少會多帶帶使用原型鏈。
3.2 借用構造函數在子類型構造函數的內部調用超類型構造函數。函數只不過是在特定環境中執行代碼的對象,因此通過使用apply()和call()方法也可以在(將來)新創建的對象上執行構造函數。
function SuperType(name){ this.name = name; this.colors = ["red", "blue", "green"]; } function SubType(){ //繼承了SuperType,同時還傳遞了參數 SuperType.call(this, "Jason"); //實例屬性 this.age = 18; } var instance1 = new SubType(); instance1.colors. push("black"); console.log(instance1.colors); //"red,blue,green,black" console.log(instance1.name); //"Jason" console.log(instance1.age); //18 var instance2 = new SubType(); console.log(instance2.colors); //"red,blue,green"
為了確保SuperType構造函數不會重寫子類型屬性,可以再調用超類型構造函數后,再添加應該在子類型中定義的屬性。
借用構造函數的問題如果僅僅是借用構造函數那么也將無法避免構造函數模式存在的問題——方法都在構造函數中定義,因此函數服用就無從談起了。而卻,在超類型的原型中定義的方法,對子類型而言也是不可見的,結果所有類型都只能使用構造函數模式。借用構造函數技術也是很少多帶帶使用的。
3.3 組合繼承組合繼承(combination inheritance),有時候也叫做為經典繼承,指的是將原型鏈和借用構造函數的技術組合到一塊,從而發揮二者之長的一種繼承模式。其背后的思路是使用原型鏈實現對原型屬性和方法的繼承,而通過借用構造函數來實現對實例屬性的繼承。這樣,既通過在原型上定義方法實現了函數復用,又能保證每個實例都有它自己的屬性。
function SuperType(name){ this.name = name; this.colors = ["red", "blue", "green"]; } SuperType.prototype.sayName = function(){ console.log(this.name); }; function SubType(name, age){ //繼承屬性 SuperType.call(this, name); this.age = age; } SubType.prototype = new SuperType(); SubType.prototype.constructor = SubType; SubType.prototype.sayAge = function(){ console.log(this.age); }; var instance1 = new SubType("Jason", 18); instance1.colors.push("black"); console.log(instance1.colors); //"red,blue,green,black" instance1.sayName(); //"Jason" instance1.sayAge(); //18 var instance2 = new SubType("Cor", 20); console.log(instance2.colors)l //"red,blue,green" instance2.sayName(); //"Cor" instance2.sayAge(); //20
這樣一來,就可以讓兩個不同的SubType實例即分別用有自己的屬性——包括colors屬性,又可有使用相同的方法了。如圖:
組合繼承避免了原型鏈和借用構造函數的缺陷,融合了它們的優點,成為JavaScript中最常用的繼承模式。而且,instanceof和isPrototypeof()也能夠用于識別基于組合繼承創建的對象。
3.4 原型式繼承基本思想是借助原型可以基于已有的對象創建新對象,同時還不必因此創建自定義類型。
function object(o){ function F(){} F.prototype = o; return new F(); }
在object()函數內部,先創建了一個臨時性的構造函數,然后將傳入的對象作為這個構造函數的原型,最后返回了這個臨時類型的一個新實例。從本質上講,object()對傳入其中的對象執行了一次深淺復制。
var person = { name: "Jason", friends: ["Cor", "Court", "Sam"] }; var anotherPerson = Object.create(person); anotherPerson.name = "Greg"; anotherPerson.friends.push("Rob"); var yetAnotherPerson = Object.create(person); yetAnotherPerson.name = "Linda"; yetAnotherPerson.friends.push("Barbie"); console.log(person.friends); //"Cor,Court,Sam,Rob,Barbie"
這種原型式繼承,要求你必須有一個對象可以作為另一個對象的基礎。如果有這么一個對象的話,可以把它傳遞給object()函數,然后再根據具體需求對得到的對象加以修改即可。在這個例子中,可以作為另一個對象的基礎是person對象,于是我們把它傳入到object()函數中,然后該函數就會返回一個新對象。這個新對象將person作為原型,所以它的原型中就包含一個基本類型值屬性和一個引用類型值屬性。這以為著person.friends不僅屬于person所有,而且也會被anotherPerson以及yetAnotherPerson共享。實際上,這就相當于又創建了person對象的兩個副本。
ECMAScript5通過新增Object.create()方法規范化了原型式繼承。這個方法接收兩個參數:一個用于做新對象原型的對象和(可選的)一個為新對象定義額外屬性的對象。在傳入一個參數的情況下,Object.create()與object()方法的行為相同
var person = { name: "Jason", friends: ["Cor", "Court", "Sam"] }; var anotherPerson = Object.create(person); anotherPerson.friends.push("Rob"); var yetAnotherPerson = Object.create(person, { name: { value: "Greg" } }); yetAnotherPerson.friends.push("Barbie"); console.log(yetAnotherPerson.name); //"Greg" console.log(anotherPerson.name); //"Jason" console.log(person.friends); //"Cor,Court,Sam,Rob,Barbie"
Object.create()方法的第二個參數與Object.defineProperties()方法的第二個參數格式相同:每個屬性都是通過自己的描述符定義的。以這種方式制定的任何屬性會覆蓋原型對象上的同名屬性。
3.5 寄生式模式別忘了,包含引用類型值的屬性始終都會共享相應的值,就像使用原型模式一樣
寄生式(parasitic)繼承是與原型式繼承緊密相關的一種思路。寄生式繼承的思路與寄生構造函數和工廠模式類似,即創建一個僅用于封裝繼承過程的函數,該函數在內部以某種方式來增強對象,最后再像真的是它做了所有工作一樣返回對象。
function createAnother(original){ var clone = object(original); //通過調用函數創建一個新對象 clone.sayHi = function(){ //以某種方式來增強這個對象 alert("Hi"); }; return clone; //返回這個對象 }
可以像下面這樣來使用createAnother()函數:
var person = { name: "Jason", friends: ["Shelby", "Court", "Van"] }; var anotherPerson = createAnother(person); anotherPerson.sayHi(); //"hi"
這個例子中的代碼基于person返回了一個新對象——anotherPerson。新對象不僅具有person的所有屬性和方法,而且還有自己的sayHi()方法。
3.6 寄生組合式繼承組合繼承是JavaScript最常用的繼承模式;不過它也有不足。組合繼承最大的問題就是無論什么情況下,都會調用兩次超類型的構造函數:一次是在創建子類型原型的時候,另一次是在子類型構造函數內部。
function SuperType(name){ this.name = name; this.colors = ["red", "bule", "grenn"]; } SuperType.prototype.sayName = function(){ console.log(this.name); } function SubType(name, age){ SuperType.call(this, name); //第二次調用SuperType this.age = age; } SubType.prototype = new SuperType(); //第一次調用SuperType SubType.prototype.constructor = SubType; SubType.prototype.sayAge = function(){ console.log(this.age); }
有注釋的兩行代碼是調用SuperType構造函數的代碼,第一次調用SuperType構造函數時,SubType.prototype會有SuperType的實例屬性。第二次調用SuperType的構造函數時SubType會在構造函數中添加了SuperType的實例屬性。當創建SubType的實例它的[[Prototype]]和自身上都有相同屬性。根據搜索機制自身的屬性就會屏蔽SubType原型對象上的屬性。等于原型對象上的屬性是多余的了。如圖:
如圖所示,有兩組name和colors屬性:一組在實例上,一組在SubType原型中。這就是調用兩次SuperType構造函數的結果。解決辦法是——寄生組合式繼承
所謂寄生組合式繼承,即通過借用構造函數來繼承屬性,通過原型鏈的混成形式來繼承方法。其背后的基本思路是:不必為了指定子類型的原型而調用超類型的構造函數,我們所需要的無非就是超類型原型的一個副本而已。本質上,就是使用寄生式來繼承超類型的原型,然后再將結果指定給子類型的原型。寄生組合式的基本模式如下:
function inheritPrototype(subType, superType){ var prototype = object(superType.prototype); //創建對象 prototype.constructor = subType; //增強對象 subType.prototype = prototype //指定對象 }
這個實力的inheritPrototype()函數實現了寄生組合式繼承的最簡單形式。這個函數接受兩個參數:子類型構造函數和超類型構造函數。在函數內部,第一步是創建超類型原型的一個副本。第二部是為創建的副本添加constructor屬性,從而彌補因重寫原型而失去的默認的constructor屬性。最后一步,將新創建的對象(即副本)賦值給子類型的原型。
function object(o){ function F(){} //創建個臨時構造函數 F.prototype = o; //superType.prototype return new F(); //返回實例 } function inheritPrototype(subType, superType){ /* 創建對象 傳入超類型的原型,通過臨時函數進行淺復制,F.prototype的指針就指向superType.prototype,在返回new F() */ var prototype = object(superType.prototype); prototype.constructor = subType; //增強對象 /* 指定對象 子類型的原型等于F類型的實例,當調用構造函數創建一個新實例后,該實例會包含一個[[prototype]]的指針指向構造函數的原型對象,所以subType.prototype指向了超類型的原型對象這樣實現了繼承,因為構造函數F沒有屬性和方法這樣就子類型的原型中就不會存在超類型構造函數的屬性和方法了。 */ subType.prototype = prototype //new F(); } function SuperType(name){ this.name = name; this.colors = ["red", "bule", "grenn"]; } SuperType.prototype.sayName = function(){ console.log(this.name); } function SubType(name, age){ SuperType.call(this, name); this.age = age; } inheritPrototype(SubType, SuperType);A SubType.prototype.sayAge = function(){ console.log(this.age); } var ins1 = new SubType("Jason", 18);
下圖是我自己的理解:
最后,如有錯誤和疑惑請指出,多謝各位大哥
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/82614.html
摘要:對象有狀態對象具有狀態,同一對象可能處于不同狀態之下。中對象獨有的特色對象具有高度的動態性,這是因為賦予了使用者在運行時為對象添改狀態和行為的能力。小結由于的對象設計跟目前主流基于類的面向對象差異非常大,導致有不是面向對象這樣的說法。 筆記說明 重學前端是程劭非(winter)【前手機淘寶前端負責人】在極客時間開的一個專欄,每天10分鐘,重構你的前端知識體系,筆者主要整理學習過程的一些...
摘要:對象有狀態對象具有狀態,同一對象可能處于不同狀態之下。中對象獨有的特色對象具有高度的動態性,這是因為賦予了使用者在運行時為對象添改狀態和行為的能力。小結由于的對象設計跟目前主流基于類的面向對象差異非常大,導致有不是面向對象這樣的說法。 筆記說明 重學前端是程劭非(winter)【前手機淘寶前端負責人】在極客時間開的一個專欄,每天10分鐘,重構你的前端知識體系,筆者主要整理學習過程的一些...
摘要:對象有狀態對象具有狀態,同一對象可能處于不同狀態之下。中對象獨有的特色對象具有高度的動態性,這是因為賦予了使用者在運行時為對象添改狀態和行為的能力。小結由于的對象設計跟目前主流基于類的面向對象差異非常大,導致有不是面向對象這樣的說法。 筆記說明 重學前端是程劭非(winter)【前手機淘寶前端負責人】在極客時間開的一個專欄,每天10分鐘,重構你的前端知識體系,筆者主要整理學習過程的一些...
摘要:子類繼承自父類的方法可以重新定義即覆寫,被調用時會使用子類定義的方法什么是多態青蛙是一個對象,金魚也是一個對象,青蛙會跳,金魚會游,定義好對象及其方法后,我們能用青蛙對象調用跳這個方法,也能用金魚對象調用游這個方法。 1、專用術語 面向對象編程程序設計簡稱:OOP,在面向對象編程中常用到的概念有:對象、屬性、方法、類、封裝、聚合、重用與繼承、多態。 2、什么是對象? 面向對象編程的重點...
摘要:即另外,注意到構造函數里的屬性,都沒有經過進行初始化,而是直接使用進行綁定。并且在模式下,構造函數沒有使用進行調用,也會導致報錯。調用構造函數千萬不要忘記寫。 1. 基礎 JavaScript不區分類和實例的概念,而是通過原型來實現面向對象編程。Java是從高級的抽象上設計的類和實例,而JavaScript的設計理念,聽起來就好比Heros里的Peter,可以復制別人的能力。JavaS...
閱讀 2607·2021-09-26 10:17
閱讀 3230·2021-09-22 15:16
閱讀 2142·2021-09-03 10:43
閱讀 3268·2019-08-30 11:23
閱讀 3666·2019-08-29 13:23
閱讀 1310·2019-08-29 11:31
閱讀 3695·2019-08-26 13:52
閱讀 1401·2019-08-26 12:22