都退后,我要繼續講故事了。

北山愚公者,年且九十,面山而居。
var person = {
name : '愚公',
age: 90,
address: '北山腳下',
whereToLive: function () {
alert(this.address)
}
};
......北山愚公曰:“雖我之死,有子存焉;子又生孫,孫又生子;子又有子,子又有孫;子子孫孫無窮匱也”。
看到這兒,問題來了,愚公的子子孫孫那么多,顯然使用對象字面量去創建是不合理的。我們介紹第一種創建方式。
工廠模式
function createPerson (name, age, address){
var o = new Object();
o.name = name;
o.age = age;
o.address = address;
o.whereToLive = function () {
alert(this.address)
};
return o;
}
var son = createPerson('愚小公', 30, '北山');
var grandSon = createPerson('愚小小公', 5, '北山');
工廠模式比較明顯的一個缺點就是由于生成并返回了一個中間對象,所以不能判斷對象的類型。
構造函數模式
function Person(name, age, address) {
this.name = name;
this.age = age;
this.address = address;
this.whereToLive = function(){
alert(this.address);
};
}
var son = new Person('愚小公', 30, '北山');
var grandSon = new Person('愚小小公', 5, '北山');
構造函數與普通函數沒有異處,沒有語法上的任何差別,只是在調用的時候使用了new關鍵字。所以我們有必要說一下new到底干了什么:
-
創建一個新的中間對象
-
將構造函數的作用于賦給這個中間對象
-
執行構造函數中的代碼
-
返回中間對象
以這里的代碼為例,實際上第二步和第三步的操作可以總結為Person.apply(newObject,arguments),這里順便說一句bind與call/apply的一個區別,bind返回的是一個函數,call/apply是順帶把這個函數給執行了,返回的是執行后的結果。
那么,構造函數模式有什么問題呢,其實也是顯而易見的,如果愚公有一千個子子孫孫,那么每個子孫都會自帶一個whereToLive的方法,顯然這種做法不文藝范兒
原型模式
function Person () {
}
Person.prototype.name = '愚公';
Person.prototype.age = 90;
Person.prototype.address = '北山';
Person.prototype.whereToLive = function () {
alert(this.address);
};
var son = new Person();
var grandSon = new Person();
son.name = '愚小公';
son.address = '山的那邊';
son.whereToLive(); // '山的那邊'
grandSon.whereToLive(); // '北山'
我們在son對象上試圖修改address屬性,并且似乎看起來也修改成功了,但是沒有影響到grandSon的屬性。所以其實這兩個address其實并不一樣。為什么呢?我們在做如下操作:
delete son.address;
son.whereToLive(); // '北山'
我們刪掉了son的address屬性,這時候son的address又成了原型中定義的值。所以我們在修改address屬性的時候并沒有動到原型中的值,而是在這個對象上新建了一個屬性。并且在試圖獲取這個屬性的時候會優先返回對象上的屬性值。我們管這個現象叫屬性屏蔽。
另外多提一點,就是在讀取對象屬性的時候,首先會查看該對象本身有沒有,沒有的話會順著原型鏈一直向上查找,如果達到原型鏈頂層都沒有找到,則返回undefined。這里再穿插一個知識點。很多剛入門的開發者會犯這樣的錯誤:
var a = {};
console.log(a.b.c)
在沒有校驗b屬性是否存在便去試圖獲取c屬性。如果到了原型鏈的頂端都沒有找到b,a.b的值則為undefined,所以獲取undefined的c屬性一定會報錯。正確的做法是在不確定是否存在對應屬性的時候,應當先做判斷。
但是在寫入基本類型屬性的時候有所不同,在當前對象沒有找到要寫入的屬性時,不會向上查找,而是在當前對象里新建一個屬性,這么做的原因是防止污染其他對象的屬性值。細心的你可能發現了我在開頭的時候強調了基本類型屬性。如果是引用類型會怎么樣呢?
function Person () {
}
Person.prototype.name = '愚公';
Person.prototype.age = 90;
Person.prototype.address = ['北山'];
Person.prototype.whereToLive = function () {
alert(this.address);
};
var son = new Person();
var grandSon = new Person();
son.address.push('山的那邊');
grandSon.whereToLive(); // '北山','山的那邊'
這里又有一個小知識點,引用類型是存在堆內存中的,不同地方的應用其實指向的是同一塊堆內存。所以如果試圖修改原型對象中的應用類型,會造成全局污染,這也就是原型模式的一個致命缺點。
組合使用構造函數模式和原型模式
坐穩,我又要穿插新的知識點了。我們可以采用簡寫的方式避免原型模式賦予原型對象方法時啰嗦的問題。
function Person(name, age, address) {
this.name = name;
this.age = age;
this.address = address;
}
Person.prototype = {
constructor : Person, // 手動修改構造函數指向
whereToLive : function () {
alert(this.address);
},
howOld : function () {
alert(this.age);
}
}
組合使用構造函數模式和原型模式的寫法是不是同時規避掉了構造函數模式和原型模式的問題呢?既可以共享公用的函數,又可以讓每個對象獨享自己的屬性。
需要注意的是,我們在重寫Person.prototype的時候,實際上使得constructor指向了Object,所以我這里進行了手動修正。
寄生構造函數模式
function PersonList (name, age, address){
var o = new Array();
o.push.apply(o, arguments);
o.consoleString = function () {
return this.join(",");
};
return o;
}
var list = new PersonList('愚小公', '愚小小公');
alert(list.consoleString());
是不是很眼熟,跟工廠模式一模一樣,只不過是在調用的時候使用了new關鍵字。利用這種模式,我們可以為對象添加額外的能力。本例中,就是給數組添加一個自定義的方法,使其可以擁有我們賦予的新能力。
結語
實際開發中還是得根據實際場景靈活運用,總有適合你的那一款。今天就聊到這,歡迎大家補充和指正。