這種模式抽象了建立具體對象的過程安全
考慮到在 ECMAScript 中沒法建立類,開發人員就發明了一種函數,用函數來封裝以特定接口建立對象的細節函數
function createPerson(name, age, job){ var o = new Object(); o.name = name; o.age = age; o.job = job; o.sayName = function(){ alert(this.name); } return o; } var person1 = createPerson("Nicholas", 29, "Software Engineer"); var person2 = createPerson("Greg", 27, "Doctor");
函數 createPerson() 可以根據接受的參數來構建一個包含全部必要信息的 Person 對象。能夠無數次地調用這個函數,而每次它都會返回一個包含三個屬性一個方法的對象。工廠模式雖然解決了建立多個類似對象的問題,但卻沒有解決對象識別的問題(即怎樣知道一個對象的類型)。this
ECMAScript 中的構造函數可用來建立特定類型的對象,例如Object 和 Array 這樣的原生構造函數spa
function Person(name, age, job){ this.name = name; this.age = age; this.job = job; this.sayName = function(){ alert(this.name); } } var person1 = new Person("Nicholas", 29, "Software Engineer"); var person2 = new Person("Greg", 27, "Doctor");
在這個例子中, Person() 函數取代了 createPerson() 函數。prototype
咱們注意到, Person() 中的代碼除了與 createPerson() 中相同的部分外,還存在如下不一樣之處:指針
沒有顯式地建立對象;code
直接將屬性和方法賦給了 this 對象;對象
沒有 return 語句。繼承
要建立 Person 的新實例,必須使用 new 操做符。以這種方式調用構造函數實際上會經歷如下 4個步驟:接口
(1) 建立一個新對象;
(2) 將構造函數的做用域賦給新對象(所以 this 就指向了這個新對象) ;
(3) 執行構造函數中的代碼(爲這個新對象添加屬性) ;
(4) 返回新對象。
在前面例子的最後, person1 和 person2 分別保存着 Person 的一個不一樣的實例。這兩個對象都有一個 constructor (構造函數)屬性,該屬性指向 Person ,以下所示。
alert(person1.constructor == Person); //true alert(person2.constructor == Person); //true
可是,提到檢測對象類型,仍是 instanceof 操做符要更可靠一些。 咱們在這個例子中建立的全部對象既是 Object 的實例, 同時也是 Person的實例,這一點經過 instanceof 操做符能夠獲得驗證。
alert(person1 instanceof Object); //true alert(person1 instanceof Person); //true alert(person2 instanceof Object); //true alert(person2 instanceof Person); //true
構造函數的問題:使用構造函數的主要問題,就是每一個方法都要在每一個實例上從新建立一遍,person1 和 person2 都有一個名爲 sayName() 的方法,但那兩個方法不是同一個 Function 的實例
function Person(name, age, job){ this.name = name; this.age = age; this.job = job; this.sayName = new Function("alert(this.name)"); // 與聲明函數在邏輯上是等價的 }
如下代碼能夠證實這一點:
alert(person1.sayName == person2.sayName); //false
所以,大可像下面這樣,經過把函數定義轉移到構造函數外部來解決這個問題。
function Person(name, age, job){ this.name = name; this.age = age; this.job = job; this.sayName = sayName; } function sayName(){ alert(this.name); } var person1 = new Person("Nicholas", 29, "Software Engineer"); var person2 = new Person("Greg", 27, "Doctor");
咱們建立的每一個函數都有一個 prototype (原型)屬性,這個屬性是一個指針,指向一個對象,而這個對象的用途是包含能夠由特定類型的全部實例共享的屬性和方法。
若是按照字面意思來理解,那麼 prototype 就是經過調用構造函數而建立的那個對象實例的原型對象。
使用原型對象的好處是可讓全部對象實例共享它所包含的屬性和方法。
換句話說,沒必要在構造函數中定義對象實例的信息,而是能夠將這些信息直接添加到原型對象中
function Person(){ } Person.prototype.name = "Nicholas"; Person.prototype.age = 29; Person.prototype.job = "Software Engineer"; Person.prototype.sayName = function(){ alert(this.name); }; var person1 = new Person(); person1.sayName(); //"Nicholas" var person2 = new Person(); person2.sayName(); //"Nicholas" alert(person1.sayName == person2.sayName); //true
但與構造函數模式不一樣的是,新對象的這些屬性和方法是由全部實例共享的。換句話說,person1 和 person2 訪問的都是同一組屬性和同一個 sayName() 函數。
只要建立了一個新函數,就會根據一組特定的規則爲該函數建立一個 prototype屬性,這個屬性指向函數的原型對象。在默認狀況下,全部原型對象都會自動得到一個 constructor(構造函數)屬性,這個屬性包含一個指向 prototype 屬性所在函數的指針。
就拿前面的例子來講,Person.prototype. constructor 指向 Person 。而經過這個構造函數,咱們還可繼續爲原型對象添加其餘屬性和方法。
當調用構造函數建立一個新實例後,該實例的內部將包含一個指針(內部屬性) ,指向構造函數的原型對象。
ECMA-262 第 5 版中管這個指針叫 [[Prototype]] 。雖然在腳本中沒有標準的方式訪問 [[Prototype]] ,但 Firefox、Safari 和 Chrome 在每一個對象上都支持一個屬性__proto__ ;而在其餘實現中,這個屬性對腳本則是徹底不可見的。
不過,要明確的真正重要的一點就是,這個鏈接存在於實例與構造函數的原型對象之間,而不是存在於實例與構造函數之間。
此外,要格外注意的是,雖然這兩個實例都不包含屬性和方法,但咱們卻能夠調用 person1.sayName() 。
這是經過查找對象屬性的過程來實現的。(屬性->原型->原型...)
雖然在全部實現中都沒法訪問到 [[Prototype]] ,但能夠經過 isPrototypeOf() 方法來肯定對象之間是否存在這種關係。從本質上講,若是 [[Prototype]] 指向調用 isPrototypeOf() 方法的對象( Person.prototype ) ,那麼這個方法就返回 true ,以下所示:
alert(Person.prototype.isPrototypeOf(person1)); //true alert(Person.prototype.isPrototypeOf(person2)); //true
ECMAScript 5 增長了一個新方法,叫 Object.getPrototypeOf() ,在全部支持的實現中,這個方法返回 [[Prototype]] 的值。例如:
alert(Object.getPrototypeOf(person1) == Person.prototype); //true alert(Object.getPrototypeOf(person1).name); //"Nicholas"
當爲對象實例添加一個屬性時,這個屬性就會屏蔽原型對象中保存的同名屬性;換句話說,添加這個屬性只會阻止咱們訪問原型中的那個屬性,但不會修改那個屬性。即便將這個屬性設置爲 null ,也只會在實例中設置這個屬性,而不會恢復其指向原型的鏈接。不過,使用 delete 操做符則能夠徹底刪除實例屬性,從而讓咱們可以從新訪問原型中的屬性
使用 hasOwnProperty() 方法能夠檢測一個屬性是存在於實例中, 仍是存在於原型中。 這個方法 (不要忘了它是從 Object 繼承來的)只在給定屬性存在於對象實例中時,纔會返回 true 。
function Person(){ } Person.prototype.name = "Nicholas"; Person.prototype.age = 29; Person.prototype.job = "Software Engineer"; Person.prototype.sayName = function(){ alert(this.name); }; var person1 = new Person(); var person2 = new Person(); alert(person1.hasOwnProperty("name")); // 屬性中沒有 返回false person1.name = "Greg"; alert(person1.name); // "Greg"——來自實例 alert(person1.hasOwnProperty("name")); //true alert(person2.name); // "Nicholas"——來自原型 alert(person2.hasOwnProperty("name")); //false delete person1.name; alert(person1.name); //"Nicholas"——來自原型 alert(person1.hasOwnProperty("name")); //false
2.原型與 in 操做符
有兩種方式使用 in 操做符:單獨使用和在 for-in 循環中使用。
在單獨使用時, in 操做符會在經過對象可以訪問給定屬性時返回 true ,不管該屬性存在於實例中仍是原型中。
function Person(){ } Person.prototype.name = "Nicholas"; Person.prototype.age = 29; Person.prototype.job = "Software Engineer"; Person.prototype.sayName = function(){ alert(this.name); }; var person1 = new Person(); var person2 = new Person(); alert(person1.hasOwnProperty("name")); //false alert("name" in person1); //true person1.name = "Greg"; alert(person1.name); //"Greg" ——來自實例 alert(person1.hasOwnProperty("name")); //true alert("name" in person1); //true alert(person2.name); //"Nicholas" ——來自原型 alert(person2.hasOwnProperty("name")); //false alert("name" in person2); //true delete person1.name; alert(person1.name); //"Nicholas" ——來自原型 alert(person1.hasOwnProperty("name")); //false alert("name" in person1); //true
結合hasOwnProperty 判斷一個屬性是否存在切存在於原型中
function hasPrototypeProperty(object, name){ return !object.hasOwnProperty(name) && (name in object); }
3.更簡單的原型語法
function Person(){ } Person.prototype = { name : "Nicholas", age : 29, job: "Software Engineer", sayName : function () { alert(this.name); } };
在上面的代碼中,咱們將 Person.prototype 設置爲等於一個以對象字面量形式建立的新對象。
最終結果相同,但有一個例外: constructor 屬性再也不指向 Person 了。
前面曾經介紹過,每建立一個函數,就會同時建立它的 prototype 對象,這個對象也會自動得到 constructor 屬性。
而咱們在這裏使用的語法,本質上徹底重寫了默認的 prototype 對象,所以 constructor 屬性也就變成了新對象的 constructor 屬性 (指向 Object 構造函數) , 再也不指向 Person 函數。
此時, 儘管 instanceof操做符還能返回正確的結果,但經過 constructor 已經沒法肯定對象的類型了,以下所示。
var friend = new Person(); alert(friend instanceof Object); //true alert(friend instanceof Person); //true alert(friend.constructor == Person); //false alert(friend.constructor == Object); //true
解決辦法很簡單,手動指定
function Person(){ } Person.prototype = { constructor : Person, name : "Nicholas", age : 29, job: "Software Engineer", sayName : function () { alert(this.name); } };
注意,以這種方式重設 constructor 屬性會致使它的 [[Enumerable]] 特性被設置爲 true。默認狀況下,原生的 constructor 屬性是不可枚舉的,所以若是你使用兼容 ECMAScript 5 的 JavaScript 引擎,能夠試一試 Object.defineProperty() 。
4.原型的動態性
因爲在原型中查找值的過程是一次搜索, 所以咱們對原型對象所作的任何修改都可以當即從實例上反映出來——即便是先建立了實例後修改原型也照樣如此
var friend = new Person(); Person.prototype.sayHi = function(){ alert("hi"); }; friend.sayHi(); //"hi"(沒有問題!)
可是,若是咱們使用對象字面量的方式修改Person的原型,此時的Person.protptype指向的是一個新對象
咱們知道,調用構造函數時會爲實例添加一個指向最初原型的[[Prototype]] 指針,而把原型修改成另一個對象就等於切斷了構造函數與最初原型之間的聯繫。
function Person(){ } var friend = new Person(); Person.prototype = { constructor: Person, name : "Nicholas", age : 29, job : "Software Engineer", sayName : function () { alert(this.name); } }; friend.sayName(); //error
過程以下圖所示
5.原生對象的原型
原型模式的重要性不只體如今建立自定義類型方面,就連全部原生的引用類型,都是採用這種模式建立的。全部原生引用類型( Object 、 Array 、 String ,等等)都在其構造函數的原型上定義了方法。例如,在 Array.prototype 中能夠找到 sort() 方法,而在 String.prototype 中能夠找到substring() 方法
alert(typeof Array.prototype.sort); //"function" alert(typeof String.prototype.substring); //"function"
經過原生對象的原型,不只能夠取得全部默認方法的引用,並且也能夠定義新方法。能夠像修改自定義對象的原型同樣修改原生對象的原型,所以能夠隨時添加方法。下面的代碼就給基本包裝類型String 添加了一個名爲 startsWith() 的方法。
String.prototype.startsWith = function (text) { return this.indexOf(text) == 0; }; var msg = "Hello world!"; alert(msg.startsWith("Hello")); //true
6. 原型對象的問題
原型模式也不是沒有缺點。首先,它省略了爲構造函數傳遞初始化參數這一環節,結果全部實例在默認狀況下都將取得相同的屬性值。 雖然這會在某種程度上帶來一些不方便, 但還不是原型的最大問題。原型模式的最大問題是由其共享的本性所致使的。
尤爲對於包含引用類型值的屬性來講(基本值還能夠隱藏)
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"); alert(person1.friends); //"Shelby,Court,Van" alert(person2.friends); //"Shelby,Court,Van" alert(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"); alert(person1.friends); //"Shelby,Count,Van" alert(person2.friends); //"Shelby,Count" alert(person1.friends === person2.friends); //false alert(person1.sayName === person2.sayName); //true
是目前在 ECMAScript中使用最普遍、認同度最高的一種建立自定義類型的方法
動態原型模式致力於解決這樣一個問題,它把全部信息都封裝在了構造函數中,而經過在構造函數中初始化原型(僅在必要的狀況下) ,又保持了同時使用構造函數和原型的優勢。換句話說,能夠經過檢查某個應該存在的方法是否有效,來決定是否須要初始化原型。
function Person(name, age, job){ //屬性 this.name = name; this.age = age; this.job = job; // 方法 if (typeof this.sayName != "function"){ Person.prototype.sayName = function(){ alert(this.name); }; } } var friend = new Person("Nicholas", 29, "Software Engineer"); friend.sayName();
注意構造函數代碼中加粗的部分。這裏只在 sayName() 方法不存在的狀況下,纔會將它添加到原型中。
使用動態原型模式時,不能使用對象字面量重寫原型。前面已經解釋過了,若是在已經建立了實例的狀況下重寫原型,那麼就會切斷現有實例與新原型之間的聯繫。
一般,在前述的幾種模式都不適用的狀況下,可使用寄生(parasitic)構造函數模式。這種模式的基本思想是建立一個函數,該函數的做用僅僅是封裝建立對象的代碼,而後再返回新建立的對象;但從表面上看,這個函數又很像是典型的構造函數。
function Person(name, age, job){ var o = new Object(); o.name = name; o.age = age; o.job = job; o.sayName = function(){ alert(this.name); }; return o; } var friend = new Person("Nicholas", 29, "Software Engineer"); friend.sayName(); //"Nicholas"
除了使用 new 操做符並把使用的包裝函數叫作構造函數以外, 這個模式跟工廠模式實際上是如出一轍的。
工廠模式爲
function createPerson(name, age, job){ var o = new Object(); o.name = name; o.age = age; o.job = job; o.sayName = function(){ alert(this.name); }; return o; } var person1 = createPerson("Nicholas", 29, "Software Engineer"); var person2 = createPerson("Greg", 27, "Doctor");
關於寄生構造函數模式,有一點須要說明:首先,返回的對象與構造函數或者與構造函數的原型屬性之間沒有關係;也就是說,構造函數返回的對象與在構造函數外部建立的對象沒有什麼不一樣。爲此,不能依賴 instanceof 操做符來肯定對象類型。 因爲存在上述問題, 咱們建議在可使用其餘模式的狀況下,不要使用這種模式。
七、穩妥構造函數模式
所謂穩妥對象,指的是沒有公共屬性,並且其方法也不引用 this 的對象。
穩妥對象最適合在一些安全的環境中 (這些環境中會禁止使用 this 和 new ) , 或者在防止數據被其餘應用程序 (如 Mashup程序)改動時使用。
穩妥構造函數遵循與寄生構造函數相似的模式,但有兩點不一樣:
一是新建立對象的實例方法不引用 this ;
二是不使用 new 操做符調用構造函數。按照穩妥構造函數的要求,能夠將前面的 Person 構造函數重寫以下。
function Person(name, age, job){ //建立要返回的對象 var o = new Object(); //能夠在這裏定義私有變量和函數 //添加方法 o.sayName = function(){ alert(name); }; //返回對象 return o; } var friend = Person("Nicholas", 29, "Software Engineer"); friend.sayName(); //"Nicholas"
注意, 在以這種模式建立的對象中, 除了使用 sayName() 方法以外, 沒有其餘辦法訪問 name 的值。
與寄生構造函數模式相似, 使用穩妥構造函數模式建立的對象與構造函數之間也沒有什麼關係,所以 instanceof 操做符對這種對象也沒有意義。