工廠模式
function createPerson(name, age){
var o = new Object(); // 創建一個對象
o.name = name;
o.age = age;
o.sayName = function(){
console.log(this.name)
}
return o; // 返回這個對象
}
var person1 = createPerson('ccc', 18)
var person2 = createPerson('www', 18)
工廠函數的問題:
工廠模式雖然解決了創建多個相似對象的問題,但是沒有解決對象識別問題(即怎樣知道一個對象的類型)。如下
person1 instanceof createPerson // --> false
person1 instanceof Object // --> true
構造函數模式
function Person(name , age){
this.name = name;
this.age = age;
this.sayName = function(){
console.log(this.name)
}
}
var person1 = new Person('ccc', 18)
var person2 = new Person('www', 18)
person1.sayName() // --> 'ccc'
person1 和person2 分別保存着Person的一個不同的實例。這兩個對象都有一個constructor(構造函數)屬性指向Person。這正是構造函數模式勝過工廠模式的地方。如下:
console.log(person1 instanceof Person) // --> true
console.log(person1 instanceof Object) // --> true
console.log(person2 instanceof Person) // --> true
console.log(person2 instanceof Object) // --> true
構造函數模式與工廠模式的區別:
- 沒有顯式的創建對象
- 直接將屬性和方法賦給了this對象
- 沒有return 語句
要創建Person的新實例,必須使用new操作符。以這種方式調用構造函數實際上會經歷一下4個步驟:
- 創建一個新對象
- 將構造函數的作用域賦給新對象(因此this就指向了這個新對象)
- 執行構造函數中的代碼(為這個新對象添加屬性)
- 返回新對象
構造函數的問題:
使用構造函數的重要問題,就是每個方法都要在每個實例上重新創建一遍。person1和person2中都有一個名為sayName()的方法,但那兩個方法不是同一個Function實例。因為在ECMAscript中函數就是對象,因此每定義一個函數,也就是實例化了一個對象。從邏輯角度上講,此時的構造函數也可以你這樣定義:
function Person(name , age){
this.name = name;
this.age = age;
this.sayName = new Function('console.log(this.name)') // eslint: The Function constructor is eval. (no-new-func)
}
這會導致,創建的不同的實例上的同名函數是不相等的,比如:console.log(person1.sayName() === person2.sayName()) // -->false
,然而創建兩個完全相同的任務的Function實例是沒有必要的。可以通過把函數定義轉移到構造函數外部來解決這個問題。
function Person(name , age){
this.name = name;
this.age = age;
this.sayName = sayName
}
function sayName(){
console.log(this.name)
}
var person1 = new Person('ccc', 18)
var person2 = new Person('www', 18)
這樣,由於sayName包含的是一個指向函數的指針,因此person1和person2對象就共享了在全局作用域中定義的同一個sayName()函數。這樣做確實解決了兩個函數做同一件事的問題,可是新問題又來了:在全局作用域中定義的函數實際上只能被某個對象調用,這讓全局作用域有點名不副實。
帶來的新問題:
如果對象需要定義很多方法,那么就要定義很多個全局函數,於是我們這個自定義的引用類型就絲毫沒有封裝性可言。
原型模式
關於原型,原型鏈內容不在此描述,只討論原型設計模式
我們創建的每一個函數都有一個prototype(原型)屬性,這個屬性是一個指針,指向一個對象,而這個對象的用途是包含可以由特定類型的所有實例共享的屬性和方法。即不必在構造函數中定義對象實例的信息,而是將這些信息直接添加到原型對象中。
function Person(){
}
Person.prototype.name = 'ccc'
Person.prototype.age = 18
Person.prototype.sayName = function(){
console.log(this.name)
}
var person1 = new Person()
person1.sayName() // --> ccc
var person2 = new Person()
person2.sayName() // --> ccc
console.log(person1.sayName === person2.sayName) // --> true
原型模式的問題:
它省略了為構造函數傳遞參數初始化參數的環節,結果所有的實例在默認情況下都將取得相同的屬性值。另外,原型模式的最大問題是由其共享的本性所導致的。看如下問題:
function Person(){
}
Person.prototype = {
constructor: Person,
name: 'ccc',
age: 18,
friends:['www', 'aaa'],
sayName: function () {
console.log(this.name)
}
}
var person1 = new Person()
var person2 = new Person()
person1.friends.push('bbb')
console.log(person1.friends) // --> ["www", "aaa", "bbb"]
console.log(person2.friends) // --> ["www", "aaa", "bbb"]
console.log(person1.friends === person2.friends) // --> true
帶來的新問題:
如果我們的初衷就是這樣,所有的實例共用一個數組,那么這個結果就是想要的。可是,實例一般都是要有屬於自己的全部屬性的,這個問題正是我們很少看到有人單獨使用原型模式的原因所在。
組合使用構造函數模式和原型模式
創建自定義類型的最常見方式,就是組合使用構造函數模式與原型模式。構造函數模式用於定義實例屬性,而原型模式用於定義方法和共享的屬性。這種方式還支持向構造函數傳遞參數。
function Person(name, age){
this.name = name;
this.age = age;
this.friends = ['aaa', 'bbb']
}
Person.prototype = {
constructor: Person,
sayName: function(){
console.log(this.name)
}
}
var person1 = new Person('ccc', 18)
var person2 = new Person('www', 18)
person1.friends.push('ddd')
console.log(person1.friends) // --> ["aaa", "bbb", "ddd"]
console.log(person2.friends) // --> ["aaa", "bbb"]
console.log(person1.friends === person2.friends) // --> false
console.log(person1.sayName === person2.sayName) // --> true
這種構造函數與原型混成的模式,是目前ECMAscript中使用最廣泛、認同度最高的一種創建自定義類型的方法。可以說,這是用來定義引用類型的一種默認方式。
動態原型模式
動態原型模式就是可以通過檢查某個應該存在的方法是否有效,來決定是否需要初始化原型。
function Person(name, age){
// 屬性
this.name = name
this.age = age
// 方法
if(typeof this.sayName !== 'function'){
Person.prototype.sayName = function(){
console.log(this.name)
}
}
}
var person1 = new Person('ccc', 18)
person1.sayName() // --> ccc
這里只有在sayName()方法不存在的情況下,才會將它添加到原型中。這段代碼只會在初次調用構造函數時才會執行。
注意:
- 在這里對原型所做的修改,能夠立即在所有實例中得到反映。
- 使用動態原型模式時,不能使用對象字面量重寫原型。如果在已經創建了實例的情況下重寫原型,那么就會切斷現有實例與新原型之間的聯系。(參考原型與原型鏈中的內容)
其它模式
還有寄生構造函數模式和穩妥構造函數模式(不常用)。
關於原型的相關知識,可以查看我的這篇文章。