JavaScript創建對象的常用模式
對象
面向對象語言有一個標誌,那就是它們都有類的概念,通過類可以創建任意多個具有相同屬性和方法的對象。
ECMAScript沒有類的概念,它的對象也與基於類的語言中的對象有所不同。ECMAScript把對象定義為:
無序屬性的集合,其屬性可以包含基本值、對象或函數。
每個對象實例都是基於一個引用類型創建的,這個引用類型可以是ECMAScript原生類型,也可以是開發者定義的類型。
工廠模式
我們可以通過Object構造函數或對象字面量來創建單個對象,但這些方式有個明顯的缺點:使用同一個接口創建很多對象,會產生大量的重復代碼。
為解決上述問題,可以使用工廠模式創建對象。工廠模式抽象了創建具體對象的過程。
由於ECMAScript沒有類,可以定義一種函數,用函數來封裝以特定接口創建對象的細節。例如:
function createStudent(name,age) { var obj = new Object(); obj.name = name; obj.age = age; obj.sayName = function(){ alert(obj.name); }; return obj; } var Bob = createStudent("Bob", 24); var Tom = createStudent("Tom", 28);
工廠模式雖然解決了創建多個相似對象的問題,但卻沒有解決對象識別的問題(即怎樣知道一個對象的類型)。上面代碼,我們的本意是創建一個Student類,Bob和Tom是Student類型,但實際上根本不存在Student類型,而Bob和Tom是Object類型。
構造函數模式
構造函數可用來創建特定類型的對象,這意味著可以將構造函數的實例標識為一種特定的類型。
構造函數與與工廠模式的不同之處在於:
- 沒有顯示地創建對象
- 直接將屬性和方法賦給this對象
- 沒有return語句
- 按照慣例,構造函數始終都應該以一個大寫字母開頭
使用new操作符調用構造函數時,會經歷以下4個步驟:
- 創建一個新對象
- 經構造函數的作用域賦給新對象(this指向這個新對象)
- 執行構造函數中的代碼
- 返回新對象
function Student(name,age) {
this.name = name;
this.age = age;
this.sayName = function(){
alert(this.name);
};
}
var Bob = new Student("Bob", 24);
var Tom = new Student("Tom", 28);
alert(Bob instanceof Student); // true
alert(Tom instanceof Student); // true
alert(Bob.sayName == Tom.sayName); // false
由於ECMAScript中的函數是對象,因此只要定義一個函數,就會實例化一個對象。這會導致使用構造函數模式創建對象時,構造函數中的每個方法都要在每個實例上重新創建一遍。這樣做浪費內存,降低執行效率。
我們可以把方法定義從構造函數內部移到外部,如:
function Student(name,age) {
this.name = name;
this.age = age;
this.sayName = sayName;
}
function sayName(){
alert(this.name);
}
var Bob = new Student("Bob", 24);
var Tom = new Student("Tom", 28);
alert(Bob instanceof Student); // true
alert(Tom instanceof Student); // true
alert(Bob.sayName == Tom.sayName); // true
這樣會導致新的問題:sayName本應是Student的私有方法,現在卻可以被任意調用,這破壞類的封裝特性。
原型模式
當我們創建一個函數時,就會同時創建它的prototype對象,這個prototype對象也會自動獲得constructor屬性,這個屬性指向構造函數對象。
當我們通過構造函數實例化一個對象時,實例對象內部將包含一個指針(內部屬性),它指向構造函數的原型對象。這個內部屬性稱為[[Prototype]]
,在腳本中沒有標準的方式訪問該內部屬性。
原型中方法和屬性被其全部實例所共享。
function Student() {
}
Student.prototype.name = "xiaohong";
Student.prototype.age = 24;
Student.prototype.sayName = function() {
alert(this.name);
};
var Bob = new Student();
var Tom = new Student();
alert(Bob instanceof Student); // true
alert(Tom instanceof Student); // true
alert(Bob.sayName == Tom.sayName); // true
重寫原型
前面的代碼中每添加一個屬性和方法就要敲一遍Student.prototype
,為了減少不必要的輸入,也為了從視覺上更好地封裝原型的功能,可以用對象字面量重寫整個原型。
function Student() {
}
Student.prototype = {
name = "xiaohong",
age = 24,
sayName = function() {
alert(this.name);
}
};
var Bob = new Student();
var Tom = new Student();
alert(Bob instanceof Student); // true
alert(Tom instanceof Student); // true
alert(Bob.sayName == Tom.sayName); // true
alert(Student.prototype.constructor == Student); // false
alert(Student.prototype.constructor == Object); // true
重寫原型後,現有原型的constructor屬性不再指向構造函數對象,而是指向對象字面量的構造函數Object。
原型的動態性
由於在原型中查找值的過程是一次搜索,因此我們對原型對象所做的任何修改都能夠立即從實例上反映出來——即使是先創建了實例後修改原型也照樣如此。
但是,如果創建實例後重寫原型,實例會由於無法查找到屬性或方法報錯。這是由於實例對象會通過內部屬性[[Prototype]]
連接到原型,在原型中查找屬性,而重寫原型切斷了實例對象與原型對象之間的聯系。
缺點
- 省略了為構造函數傳遞參數的環節,所有實例在默認情況下都將取得相同的屬性值
- 原型中的屬性是共享的,原型屬性數據變化,所有實例對象都會獲得這一變化
組合使用構造函數模式和原型模式
構造函數模式用於定義實例屬性,而原型模式用於定義方法和共享屬性。
這種組合方式的優點是:
- 每個實例都會擁有自己的一份實例屬性,同時共享著對方法的引用,最大限度地節省了內存
- 這種組合模式還支持向構造函數傳遞參數
function Student(name,age) {
this.name = name;
this.age = age;
}
Student.prototype = {
sayName = function() {
alert(this.name);
}
};
var Bob = new Student("Bob",24);
var Tom = new Student("Tom",28);
alert(Bob instanceof Student); // true
alert(Tom instanceof Student); // true
alert(Bob.sayName == Tom.sayName); // true
動態原型模式
原型模式中構造函數和原型是分離,為了把所有信息都封裝在構造函數中,可以使用動態原型模式。
構造函數模式的主要缺點是同個方法要在不同實例對象中重復創建,浪費內存,所以引入了原型模式。動態原型模式通過檢查某個應該存在的方法是否有效,如果無效則在構造函數中初始化原型,這樣就解決方法對象重復創建的問題,而封裝性更好。
function Student(name,age) {
this.name = name;
this.age = age;
// sayName不存在,則初始化原型
if (typeof this.sayName != "function") {
this.sayName = function() {
alert(this.name);
};
}
}
var Bob = new Student("Bob",24);
var Tom = new Student("Tom",28);
alert(Bob instanceof Student); // true
alert(Tom instanceof Student); // true
alert(Bob.sayName == Tom.sayName); // true
寄生構造函數模式
寄生構造函數模式就是用new操作符調用工廠模式。由於重寫了返回值,返回對象和構造函數及其原型沒有任何聯系,無法對象類型識別。
function Student(name,age) {
var obj = new Object();
obj.name = name;
obj.age = age;
obj.sayName = function(){
alert(this.name);
};
return obj;
}
var Bob = new Student("Bob", 24);
var Tom = new Student("Tom", 28);
穩妥構造函數模式
穩妥對象指沒有公共屬性,而且其方法也不引用this的對象。穩妥對象最適合在一些安全的環境中或者防止數據被其他應用程序改動時使用
穩妥構造函數與寄生構造函數類似,但有兩點不同:
- 新創建對象的實例方法不引用this
- 不使用new操作符調用構造函數
function Student(name,age) {
var obj = new Object();
obj.sayName = function(){
alert(name);
};
return obj;
}
var Bob = Student("Bob", 24);
Bob.sayName();
JavaScript創建對象的常用模式