javascript面向物件系列第二篇——建立物件的5種模式
前面的話
如何建立物件,或者說如何更優雅的建立物件,一直是一個津津樂道的話題。本文將從最簡單的建立物件的方式入手,逐步介紹5種建立物件的模式
物件字面量
一般地,我們建立一個物件會使用物件字面量的形式
[注意]有三種方式來建立物件,包括new建構函式、物件直接量和Object.create()函式,詳細情況移步至此
var person1 = { name: "bai", age : 29, job: "Software Engineer", sayName: function(){ alert(this.name); } };
如果我們要建立大量的物件,則如下所示:
var person1 = { name: "bai", age : 29, job: "Software Engineer", sayName: function(){ alert(this.name); } }; var person2 = { name: "hu", age : 25, job: "Software Engineer", sayName: function(){ alert(this.name); } };/* var person3 ... */
雖然物件字面量可以用來建立單個物件,但如果要建立多個物件,會產生大量的重複程式碼
工廠模式
為了解決上述問題,人們開始使用工廠模式。該模式抽象了建立具體物件的過程,用函式來封裝以特定介面建立物件的細節
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('bai',29,'software Engineer'); var person2 = createPerson('hu',25,'software Engineer');
工廠模式雖然解決了建立多個相似物件的問題,但沒有解決物件識別的問題,因為使用該模式並沒有給出物件的型別
建構函式模式
可以通過建立自定義的建構函式,來定義自定義物件型別的屬性和方法。建立自定義的建構函式意味著可以將它的例項標識為一種特定的型別,而這正是建構函式模式勝過工廠模式的地方。該模式沒有顯式地建立物件,直接將屬性和方法賦給了this物件,且沒有return語句
function Person(name,age,job){ this.name = name; this.age = age; this.jog = job; this.sayName = function(){ alert(this.name); }; } var person1 = new Person("bai",29,"software Engineer"); var person2 = new Person("hu",25,"software Engineer");
使用建構函式的主要問題是每個方法都要在每個例項上重新建立一遍,建立多個完成相同任務的方法完全沒有必要,浪費記憶體空間
function Person(name,age,job){ this.name = name; this.age = age; this.jog = job; this.sayName = function(){ alert(this.name); }; } var person1 = new Person("bai",29,"software Engineer"); var person2 = new Person("hu",25,"software Engineer"); //具有相同作用的sayName()方法在person1和person2這兩個例項中卻佔用了不同的記憶體空間 console.log(person1.sayName === person2.sayName);//false
建構函式拓展模式
在建構函式模式的基礎上,把方法定義轉移到建構函式外部,可以解決方法被重複建立的問題
function Person(name,age,job){ this.name = name; this.age = age; this.jog = job; this.sayName = sayName; } function sayName(){ alert(this.name); }; var person1 = new Person("bai",29,"software Engineer"); var person2 = new Person("hu",25,"software Engineer"); console.log(person1.sayName === person2.sayName);//true
現在,新問題又來了。在全域性作用域中定義的函式實際上只能被某個物件呼叫,這讓全域性作用域有點名不副實。而且,如果物件需要定義很多方法,就要定義很多全域性函式,嚴重汙染全域性空間,這個自定義的引用型別沒有封裝性可言了
寄生建構函式模式
該模式的基本思想是建立一個函式,該函式的作用僅僅是封裝建立物件的程式碼,然後再返回新建立的物件。該模式是工廠模式和建構函式模式的結合
寄生建構函式模式與建構函式模式有相同的問題,每個方法都要在每個例項上重新建立一遍,建立多個完成相同任務的方法完全沒有必要,浪費記憶體空間
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 person1 = new Person("bai",29,"software Engineer"); var person2 = new Person("hu",25,"software Engineer"); //具有相同作用的sayName()方法在person1和person2這兩個例項中卻佔用了不同的記憶體空間 console.log(person1.sayName === person2.sayName);//false
還有一個問題是,使用該模式返回的物件與建構函式之間沒有關係。因此,使用instanceof運算子和prototype屬性都沒有意義。所以,該模式要儘量避免使用
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 person1 = new Person("bai",29,"software Engineer"); console.log(person1 instanceof Person);//false console.log(person1.__proto__ === Person.prototype);//false
穩妥建構函式模式
所謂穩妥物件指沒有公共屬性,而且其方法也不引用this的物件。穩妥物件最適合在一些安全環境中(這些環境會禁止使用this和new)或者在防止資料被其他應用程式改動時使用
穩妥建構函式與寄生建構函式模式相似,但有兩點不同:一是新建立物件的例項方法不引用this;二是不使用new操作符呼叫建構函式
function Person(name,age,job){ //建立要返回的物件 var o = new Object(); //可以在這裡定義私有變數和函式 //新增方法 o.sayName = function(){ console.log(name); }; //返回物件 return o; } //在穩妥模式建立的物件中,除了使用sayName()方法之外,沒有其他方法訪問name的值 var friend = Person("bai",29,"Software Engineer"); friend.sayName();//"bai"
與寄生建構函式模式相似,使用穩妥建構函式模式建立的物件與建構函式之間也沒有什麼關係,因此instanceof操作符對這種物件也沒有什麼意義
原型模式
使用原型物件,可以讓所有例項共享它的屬性和方法。換句話說,不必在建構函式中定義物件例項的資訊,而是可以將這些資訊直接新增到原型物件中
function Person(){ Person.prototype.name = "bai"; Person.prototype.age = 29; Person.prototype.job = "software Engineer"; Person.prototype.sayName = function(){ console.log(this.name); } } var person1 = new Person(); person1.sayName();//"bai" var person2 = new Person(); person2.sayName();//"bai" alert(person1.sayName == person2.sayName);//true
更簡單的原型模式
為了減少不必要的輸入,也為了從視覺上更好地封裝原型的功能,用一個包含所有屬性和方法的物件字面量來重寫整個原型物件
但是,經過物件字面量的改寫後,constructor不再指向Person了。因為此方法完全重寫了預設的prototype物件,使得Person.prototype的自有屬性constructor屬性不存在,只有從原型鏈中找到Object.prototype中的constructor屬性
function Person(){}; Person.prototype = { name: "bai", age: 29, job: "software Engineer", sayName : function(){ console.log(this.name); } }; var person1 = new Person(); person1.sayName();//"bai" console.log(person1.constructor === Person);//false console.log(person1.constructor === Object);//true
可以顯式地設定原型物件的constructor屬性
function Person(){}; Person.prototype = { constructor:Person, name: "bai", age: 29, job: "software Engineer", sayName : function(){ console.log(this.name); } }; var person1 = new Person(); person1.sayName();//"bai" console.log(person1.constructor === Person);//true console.log(person1.constructor === Object);//false
由於預設情況下,原生的constructor屬性是不可列舉的,更妥善的解決方法是使用Object.defineProperty()方法,改變其屬性描述符中的列舉性enumerable
function Person(){}; Person.prototype = { name: "bai", age: 29, job: "software Engineer", sayName : function(){ console.log(this.name); } }; Object.defineProperty(Person.prototype,'constructor',{ enumerable: false, value: Person }); var person1 = new Person(); person1.sayName();//"bai" console.log(person1.constructor === Person);//true console.log(person1.constructor === Object);//false
原型模式問題在於引用型別值屬性會被所有的例項物件共享並修改,這也是很少有人單獨使用原型模式的原因
function Person(){} Person.prototype = { constructor: Person, name: "bai", age: 29, job: "Software Engineer", friend : ["shelby","Court"], sayName: function(){ console.log(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(){ console.log(this.name); } } var person1 = new Person("bai",29,"Software Engineer"); var person2 = new Person("hu",25,"Software Engineer"); person1.friends.push("Van"); alert(person1.friends);// ["shelby","Court","Van"]; alert(person2.friends);// ["shelby","Court"]; alert(person1.friends === person2.friends);//false alert(person1.sayName === person2.sayName);//true
動態原型模式
動態原型模式將組合模式中分開使用的建構函式和原型物件都封裝到了建構函式中,然後通過檢查方法是否被建立,來決定是否初始化原型物件
使用這種方法將分開的建構函式和原型物件合併到了一起,使得程式碼更加整齊,也減少了全域性空間的汙染
[注意]如果原型物件中包含多個語句,只需要檢測其中一個語句即可
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("bai",29,"Software Engineer"); friend.sayName();//'bai'
最後
本文從使用物件字面量形式建立一個物件開始說起,建立多個物件會造成程式碼冗餘;使用工廠模式可以解決該問題,但存在物件識別的問題;接著介紹了建構函式模式,該模式解決了物件識別的問題,但存在關於方法的重複建立問題;接著介紹了原型模式,該模式的特點就在於共享,但引出了引用型別值屬性會被所有的例項物件共享並修改的問題;最後,提出了建構函式和原型組合模式,建構函式模式用於定義例項屬性,而原型模式用於定義方法和共享的屬性,這種組合模式還支援向建構函式傳遞引數,該模式是目前使用最廣泛的一種模式
此外,一些模式下面還有一些解決特殊需求的拓展模式
以上