雖然Object構造函數(var obj = new Object() )和對象字面量(var obj = {})均可以用來建立單個對象,可是這種方式存在明顯的缺點:使用同一個接口建立不少對象,會產生大量的重複代碼。瀏覽器
爲了解決這個問題,開發者開始使用工廠模式。函數
這種模式抽象了建立具體對象的過程this
因爲JavaScript中沒法建立類,開發人員就發明了一種函數,用函數來封裝以特定接口建立對象的細節spa
// 工廠模式 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('zhangsan', 18, 'student'); var person2 = createPerson('lisi', 27, 'Soft Engineer');
函數可以接受參數來建立一個包含全部必要信息的Person對象。prototype
能夠無數次的調用這個函數,而每次都會返回一個包含三個屬性和一個方法的對象。指針
優勢:解決了建立多個類似對象的問題code
缺點:沒有解決對象識別問題(即怎樣知道一個對象的類型)對象
隨着JavaScript的發展,有一個新的模式出現了---構造函數模式blog
// 構造函數 function Person(name, age, job) { this.name = name; this.age = age; this.job = job; this.sayName = function() { alert(this.name); }; } var person1 = new Person('zhangsan', 18, 'student'); var person2 = new Person('lisi', 27, 'Soft Engineer');
與工廠函數的不一樣之處:繼承
要建立Person的新實例,必須使用new操做符,以這種方式調用函數實際上會經歷如下4個步驟:
person1和person2分別保存着Person對象的不一樣實例,每一個實例對象都有一個constructor(構造函數)屬性,該屬性指向Person
該屬性(constructor)最初是用來標識對象類型
// 對象的constructor屬性最初用來標識對象類型 console.log(person1.constructor == Person); //true console.log(person2.constructor == Person); //true
可是檢測對象類型,仍是建議使用 instanceof 操做符
person1 instanceof Person; //true person1 instanceof Object; //true person2 instanceof Person; //true person2 instanceof Object; //true
優勢:建立自定義構造函數,能夠將它的實例對象標識爲一直可以特定類型,能夠經過constructor屬性來判斷對象類型
缺點:構造函數裏的方法,在每一個實例對象上都要從新建立,也就是person1和person2實例上的方法是來自不一樣Function實例
// 構造函數 function Person(name, age, job) { this.name = name; this.age = age; this.job = job; // this.sayName = function() { // alert(this.name); // }; 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('zhangsan', 18, 'student'); var person2 = new Person('lisi', 27, 'Soft Engineer');
可是這樣又會引起新的問題:
因而,又有了一種新的模式來解決上述問題-----原型模式
咱們建立的每一個函數都有一個prototype (原型)屬性,這個屬性是一個指針,指向一個原型對象。
即原型對象,就是調用構造函數而建立的實例對象的原型。
原型對象的用途:包含由特定類型的全部實例對象共享的屬性和方法
// 原型模式 function Person() {} // 構造函數 Person.prototype.name = 'Ami'; //構造函數的原型對象 Person.prototype
Person.prototype.age = 18;
Person.prototype.job = 'student';
Person.prototype.sayName = function() {
console.log(this.name);
};
var person1 = new Person(); // 實例對象
person1.sayName(); //Ami
var person2 = new Person();
person2.sayName(); //Ami
怎麼理解原型對象?
任什麼時候候,只要建立了一個新函數, 就會根據一組特定的規則爲該函數建立一個prototype屬性,該屬性指向新函數的原型對象。
在默認狀況下,全部原型對象都會自動獲取一個constructor屬性,該屬性是一個指向prototype屬性的指針。
建立了自定義的構造函數以後,構造函數的原型對象會取得一個constructor屬性,其餘方法,則從Object繼承而來。
當調用構造函數去建立一個實例對象後,該實例對象內部包含一個指針([[prototype]]),指向構造函數的原型對象,這就造成了一個閉環。
根據上面例子闡釋:
Person(構造函數)--------->prototype屬性--------->Person.prototype(Person的原型對象)--------->constructor屬性-------> Person(構造函數)------>new Person()---->person1(實例對象)---->__proto__----Person.prototype(構造函數的原型對象)
|
|
Person(構造函數)------>new Person()---->person1(實例對象)---->__proto__----Person.prototype(構造函數的原型對象)
每一個函數上都有prototype屬性,指向構造函數的原型對象
每一個對象上都有constructor屬性,指向構造函數
每一個實例對象上都有一個[[prototype]](__proto__)屬性,指向構造函數的原型對象
var person1 = new Person(); // 實例對象 person1.sayName(); //Ami var person2 = new Person(); person2.sayName(); //Ami
//每一個實例對象都有一個隱藏的[[prototype]]屬性(__proto__),指向原型對象,現代瀏覽器在每一個對象上都支持一個屬性__proto__,這個
//__proto__屬性是存在於實例對象和構造函數的原型對象之間的一種鏈接
原型對象存在的問題:
爲了解決問題2,又提出了一種更簡單的原型語法,就是重寫原型對象
function Person() {} Person.prototype = { name: 'Ami', age: 18, job: 'student', sayName: function() { console.log(this.name); } };
可是,這樣又帶來了新的問題:那就是將Person.prototype設置爲等於一個以對象字面量形式建立的新對象,致使了constructor屬性不在指向構造函數Person了。
解析:由於每當新建一個對象,新對象就會自動獲取一個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 == Object); //true console.log(friend.constructor == Person); //false
解決方法:
將constructor屬性強制指向構造函數Person
function Person() {} Person.prototype = { constructor: Person, name: 'Ami', age: 18, job: 'student', sayName: function() { console.log(this.name); } }; var friend = new Person(); console.log(friend instanceof Object); //true console.log(friend instanceof Person); //true console.log(friend.constructor == Object); //false console.log(friend.constructor == Person); //true
可是,這樣又出問題了,這種方式致使原來不可枚舉的屬性constructor ,變成了可枚舉屬性了,心中萬馬奔騰~~~
如何解決呢?
若是你的瀏覽器兼容ES5,能夠用Object.defineProperty()
function Person() {} Person.prototype = { // constructor: Person, name: 'Ami', age: 18, job: 'student', sayName: function() { console.log(this.name); } }; Object.defineProperty(Person.prototype, 'constructor', { enumerable: false, value: Person }); var friend = new Person(); console.log(friend instanceof Object); //true console.log(friend instanceof Person); //true console.log(friend.constructor == Object); //false console.log(friend.constructor == Person); //true
Object.defineProperty()
方法會直接在一個對象上定義一個新屬性,或者修改一個對象的現有屬性, 並返回這個對象。
Object.defineProperty(obj, prop, descriptor)
obj
要在其上定義屬性的對象。prop
要定義或修改的屬性的名稱。descriptor
將被定義或修改的屬性描述符解決了上述問題,原型模式還存在什麼問題呢?
還有一個問題:那就是原型的動態性帶來的問題
首先來講一下在原型鏈中查找值和設置值的區別:
(這個會在下一篇文章作一個詳細的解釋)
查找:當在原型鏈中查找某一屬性時,首先看實例對象上有沒有該屬性,若是有,則返回;若是沒有,就去該實例對象的原型對象上去查找,若是也沒有,最後查找頂層對象(Object)的原型對象,若是尚未,則返回undefined。
設置:首先看實例對象上有沒有該屬性,若是有,則修改這個屬性,若是沒有,則新建一個屬性,不會再往原型對象上去查找了。
因爲原型鏈的查找是一次搜索過程,所以咱們對原型對象所作的任何修改都能當即從實例對象上反映出來----即便是先建立實例對象,在修改原型對象,也是如此。
舉個栗子:
function Person() {}
var friend = new Person();
Person.prototype.sayHi = function() { console.log('Hi'); }; friend.sayHi(); // Hi
解析:
雖然咱們是先建立實例對象。可是當咱們調用friend.sayHi()時,會先從實例對象上查找有沒有這個方法,若是沒有回繼續去原型對象上查找,最終在原型對象上找到了這個方法,因此,即便是friend實例是在原型對象上添加新方法以前建立的,可是,實例對象仍然能夠訪問這個方法。
緣由是:實例對象和原型之間有鬆散的鏈接關係。實力與原型之間的關係是一個指針,而非一個副本,所以能夠在原型中找到新的sayHi屬性,並返回保存在裏面的函數。
所以,能夠隨時隨地給原型添加屬性或者方法,均可以當即在全部實例對象中反映出來。
可是,若是重寫了整個原型對象,那麼又會出現問題了
咱們知道,當調用構造函數新建實例對象時,會爲實例對象添加一個指向最初構造函數原型對象的[[prototype]]指針(也能夠成爲__proto__),可是,若是重寫真個原型對象,就至關於把原型修改成另外一個對象,就等於切斷了構造函數和最初原型對象之間的聯繫。
function Person() {} var friend = new Person(); //指向最初的Person 實例對象的指針指向的是構造函數的原型對象,而不是構造函數 Person.prototype = { constructor: Person, // 指向構造函數Person name: 'Ami', age: 18, job: 'student', sayName: function() { console.log(this.name); } }; friend.sayName(); //Uncaught TypeError: friend.sayName is not a function
正確的寫法應該是:
function Person() {} Person.prototype = { constructor: Person, name: 'Ami', age: 18, job: 'student', sayName: function() { console.log(this.name); } }; var friend = new Person(); friend.sayName(); // Ami
原型模式最大的問題:就是由共享的本性所致使的
function Person() {} Person.prototype = { constructor: Person, name: 'Ami', age: 18, job: 'student', friends: ['zhangsan', 'lisi'], sayName: function() { console.log(this.name); } }; var person1 = new Person(); var person2 = new Person(); person1.friends.push('wanger'); console.log(person1.friends); // ["zhangsan", "lisi", "wanger"] console.log(person2.friends); // ["zhangsan", "lisi", "wanger"] console.log(person1.friends === person2.friends); // true
因爲共享性,致使其中一個實例修改了原型上的屬性或者方法,其餘全部實例都會跟着改變,因此不多有人會單獨使用原型模式,因而就有了下面的組合模式
構造函數模式:用於定義實例屬性
原型模式:用於定義方法和共享屬性
//構造函數模式 function Person(name, age, job) { this.name = name; this.age = age; this.job = job; this.friends = ['zhangsan', 'lisi']; } // 原型模式 Person.prototype = { constructor: Person, sayName: function() { console.log(this.name); } }; var person1 = new Person('Jack', 28, 'Doctor'); var person2 = new Person('Tom', 34, 'Teacher'); person1.friends.push('wanger'); console.log(person1.friends); // ["zhangsan", "lisi", "wanger"] console.log(person2.friends); // ["zhangsan", "lisi"] console.log(person1.friends === person2.friends); // false console.log(person1.sayName === person2.sayName); // true
下面三種做爲了解