前端要給力之:分解對象構造過程new()
本文討論JavaScript中的對象創建運算new。需要說明的是,本文所討論的“將new()過程分解為多個步驟”,并非一般js開發中的所須技巧,而是在js來構建OOP系統的必要技術。
一、JavaScript構造器與構造過程的特點
JavaScript中通過以下方式聲明和使用構造器:
this.xxx = 1;
}
MyObject.prototype.yyy = 2;
obj1 = new MyObject();
obj2 = new MyObject();
其中xxx與yyy的不同在于:對于obj1和obj2來說,yyy是相同的屬性的不同引用,不同對象實例的初始值總是相同的;而xxx則是各自不同的引用,每個對象實例都不同。上例中使用1、2這樣的值類型數據,并不足以體現二者的區別。當我們而使用引用類型(例如數組),就很容易看出二者的區別來了。例如:
this.xxx = new Array();
}
MyObject.prototype.yyy = new Array();
obj1 = new MyObject();
obj2 = new MyObject();
obj1.xxx.push('abc');
obj1.yyy.push('abc');
// 顯示1,0,表明obj2.xxx并沒有變化
alert([obj1.xxx.length, obj2.xxx.length]);
// 顯示1,1,表明obj2.yyy同時變化,與obj1.yyy是同一個數組
alert([obj1.yyy.length, obj2.yyy.length]);
使用原型的另一個必要在于繼承樹的建立。這一過程要求new運算符的參與,簡單說來,就是"子類.prototype"必須賦值為"new 父類()"所產生的一個實例。例如:
}
MyObjectEx.prototype = new MyObject();
MyObjectEx.prototype.constructor = MyObjectEx;
在這個過程中,最后面的是一行修正代碼。這行修正的必要性在于:new MyObject()所產生的實例(例如x)的屬性x.constructor總是指向MyObject,而在子類MyObjectEx()中,它應該是指向MyObjectEx的。
OK。這就是全部的基礎知識,下面我們來分解這一過程。
二、new()過程中的原型鏈維護
new總是因為建立原型繼承樹而存在的,如果沒有new過程參與,則當obj = new MyObjecEx()時,我們無法通過instanceof運算: obj instanceof MyObject
來了解obj在繼承樹上的關系。
但是許多人并不知道,事實上這一過程并不需要MyObject的參與。因為instanceof只檢查prototype鏈,并不檢查函數本身。舉例來說:
X = function() {}
Y = function() {}
X.prototype = P;
Y.prototype = P;
obj = new Y();
alert(obj instanceof X); // 顯示true
在這個示例中,obj創建自Y(),但只因為X.prototype(或者它的原型鏈中)指向原型P,所以最后顯示obj也是X的一個實例。可見 instanceof的檢測與obj是否創建自函數X是無關的,并不檢查函數自身。
換而言之,new()在上述過程中,只有“維護原型鏈”的作用。那么,我們事實上也可以手工來維護這個原型鏈。這一點,事實上也就是ECMA Script 5th中的Object.create()出現的原因。
Object.create(O[, Properties]) returns an object with a prototype of O and properties according to Properties (as if called through Object.defineProperties).
不考慮Properties的部分,那么Object.create可以用以下過程來描述:
function F() {};
F.prototype = O;
return new F();
}
通過這個create方法,我們可以創建任意的、足夠長的原型鏈,然后把它“賦值給”某個構造器函數,使“原型鏈創建過程”與“構造器中的初始化過程”二者分解開來。例如:
B = Object.create(A);
C = Object.create(B);
A.x = 1;
B.y = 2;
C.z = 3;
function MyObject() {
}
function MyObjectEx() {
}
MyObject.prototype = B;
MyObjectEx.prototype = C;
這樣一來,我們沒有顯式地聲明MyObject()與MyObjectEx()之間的繼承關系,但B與C的原型關系維護了它們之間的類屬關系。因此:
obj2 = new MyObjectEx();
// 顯示true, obj2是MyObject()的子類的實例
alert(obj2 instanceof MyObject);
// 顯示false, z屬性不會出現在MyObject()的實例中
alert('z' in obj1);
三、new()過程中的構造器調用
對于構造器MyObject()來說,在new()過程中會被調用一次。例如此前提到的:
this.xxx = 1;
}
new()過程將以剛剛創建的實例為this來調用MyObject(),這使得我們有機會為這個實例(this)做一些初始化操作。這個行為其實來自于最最早期的JavaScript設計。在1995年底發布的Netscapes Navigator 2.0 beta以及其后的NN2.0正式版中,這個最原始版本的JavaScript都還沒有“原型繼承”的設計,但已經有了new這個運算。這時所謂新對象的創建,就是不斷地為this賦值而已,只不過new會為產生的對象維護<obj>.constructor屬性。
這件事是很容易做到的。在高版本中的Function已經提供了Function.call()和Function.apply()方法,能很方便的重現這一過程。因此:
就可以被分解為如下兩個步驟(fixed at 2010.12.30. note: 接受book_LoveLiness的意見,將constructor的改寫過程放在原型階段):
constructor: MyObject
});
MyObject.call(obj, x, y, z);
對于上述{constructor: MyObject},內部隱含了如下兩個過程:
o.constructor = MyObject;
當用戶使用自己的構造器來創建MyObject的原型鏈——例如MyObject()的父類是ExtObject(),而不是Object()的時候,就會用到這樣的分解了。
即使對于最早期沒有call()/apply()方法,也沒有原型繼承的JavaScript,上述過程也可以分解為:
obj.constructor = MyObject;
obj.constructor(a, b, c);
當然,這一過程也就與原型繼承沒有了關系——我們假定這就是JavaScript 1.0的時代吧。
四、構造過程分解的意義
因為原型繼承(包括原型鏈維護)與構造器調用是可以分開的,所以我們事實上也可以只使用二者之一來創建任何復雜的過程。對于大型OOP框架來說, “是否需要維護原型鏈”是一個深入的話題。例如一些早期的OOP框架就不管不顧,只考慮子類對象對父類的“相似性”,而無視instanceof運算的效果,這樣的時代大概可以追溯到2004年以及之前。后來OOP框架意識JavaScript的原型系統的重要性,因此又回到正途上來,
- 通過類似于Object.create()的方案,來保證原型鏈的有效性;又
- 通過獨立的類創建或對象創建過程,來保證系統的可擴展性。
這一切,就是我們現在的JS OOP的來源了。
在一些具體的方法中,有許多變形的實現。在QoBean中的Unique()函數綜合地重現了整個過程(fixed at 2010.12.30,考慮到無args參數的情況):
function F() {}
F.prototype = obj;
return func ? func.apply(new F, args||[]) : new F;
}
對于任何一個對象A來說,可以用它為原型創建一個新的實例:
或在創建之后,使用函數MyObject()來初始化:
或在上述初始化中使用特定的參數: