1.封裝類
怎么封裝一個類,也就是創建自定義對象?
- 構造函數式
1 function Car(sColor,iDoors,iMpg) { 2 this.color = sColor; 3 this.doors = iDoors; 4 this.mpg = iMpg; 5 this.showColor = function() { 6 alert(this.color); 7 }; 8 } 9 10 var oCar1 = new Car("red",4,23); 11 var oCar2 = new Car("blue",3,25);
和Java模式最像。缺點是每生成實例都會創建一次showColor。
- 原型方式
1 function Car() {} 2 3 Car.prototype.color = "blue"; 4 Car.prototype.doors = 4; 5 Car.prototype.mpg = 25; 6 Car.prototype.showColor = function() { 7 alert(this.color); 8 }; 9 10 var oCar1 = new Car();
利用了前述prototype。缺點是不靈活,無法傳參數。
通過給this添加屬性的方式創建成員變量都是公有的,在函數體內var的變量都是私有的。類用一個Function來聲明,這個Function就是構造函數,如果變量再構造函數外聲明就等同靜態變量,不需要new就可以訪問。
- 構造函數和原型混合式
這種方式為了避免重復創建函數副本。
1 function Car(sColor,iDoors,iMpg) { 2 this.color = sColor; 3 this.doors = iDoors; 4 this.mpg = iMpg; 5 this.drivers = new Array("Mike","John"); 6 } 7 8 Car.prototype.showColor = function() { 9 alert(this.color); 10 }; 11 12 var oCar1 = new Car("red",4,23); 13 var oCar2 = new Car("blue",3,25);
但第12行如果不小心忘寫了new,Car中的this就是window了,為了避免幾個屬性加載window上,可以在Car內添加this instanceof Car判斷。
2.原型鏈
所有對象都擁有prototype屬性,這個屬性也是一個對象。給obj.prototype添加屬性和方法意味着這些屬性和方法掛在了原型鏈上,原型鏈下游的對象(子類)可以繼承到原型鏈上所有的方法。也就是說,obj的prototype對象中的屬性一般情況下不是給自己用的,用obj.hasOwnProperty(obj.prototype.attr)結果都為false,給自己添加屬性直接obj.attr=XXX。
當一個子類查找一個屬性時,首先在自己的屬性中查找(注意不去找自己的prototype里的屬性),然后查找父類的prototype,一直找到原型鏈頂端Object.prototype。這個原型鏈的鏈接是由prototype.__proto__這個私有屬性實現的,子類的prototype.__proto__指向父類的prototype。所有類的基類Object的prototype.__proto__指向null,所以原型鏈總是有窮的。
對象實例與上面說的子類類似。
3.繼承
先看一下call和prototype方式實現繼承。
1 function BMW(sColor,iDoors,iMpg,len){ 2 Car.call(this,sColor,iDoors,iMpg); //繼承父類Car的屬性,可以是一部分. 詳見aguments對象可實現重載 3 this.len = len; //子類自有屬性 4 }; 5 BMW.prototype = new Car(); //獲得父類Car的所有方法 6 BMW.prototype.showLen = function(){ //子類自有方法 7 alert(this.len); 8 }; 9 var x5 = new BMW("green",4,35,5); 10 x5.showColor(); 11 x5.showLen(); //結果彈出"green","5"
- 上面代碼第2行使用了call繼承父類Car的屬性。
原理:call方法的用法是func.call(obj,args),使obj可以調用本不是自己的方法func,從而改變this,args是參數。與它類似的是apply方法,只是傳參時參數是數組形式。
- 代碼第5行使用了prototype繼承父類所有方法。
原理:對象的任何屬性和方法都被傳遞給那個對象的所有實例。子類的prototype設置成父類的實例,就繼承了父類的所有屬性和方法。但子類的 prototype 屬性被覆蓋了,原來的方法不復存在,所以子類自有屬性需要在prototype設置之后添加。
繼承的順序:
上面的代碼是先使用call繼承屬性,同時添加自有屬性,再使用prototype繼承方法,最后添加自己方法。不要疑惑這個順序會產生覆蓋。如果第5行改為BMW.prototype = new Car('yellow',3,25)才會覆蓋。原因是JS的aguments對象,它的特性就不在這里細說了,可以利用它實現函數重載。其實使用call並沒有繼承到父類的任何屬性,因為父類構造函數的參數undefined,父類實例中並沒有添加上前幾個幾個屬性,drivers屬性有。
call和prototype的區別:
這里將父類實例賦給子類prototype有明顯限制,引用類型成員在子類中以淺拷貝的方式存在,即一個子類修改引用類型成員,其他類都會受到影響。而call方式沒有這個問題,只是每次都產生副本。
顯而易見,call方法可以方便的實現多繼承,而原型鏈方法不能支持多繼承。
另外,call方法繼承可以獲取父類的一部分屬性,而prototype繼承只是全盤接收。
最后一點也相當重要,instanceof可以識別prototype繼承,也就是下面兩行代碼結果都是真。
alert(x5 instanceof BMW); alert(x5 instanceof Car);
另一種安全的繼承:復制繼承
把屬性復制都新對象上,是對象的深拷貝。當然也可以區分父類自有屬性和繼承屬性,有取舍的復制。
1 var foo = {}; 2 var prop; 3 for(prop in props){ 4 if(!foo[prop]){ 5 foo[attr] = obj[attr]; 6 } 7 }
再談原型繼承
上面的
BMW.prototype = new Car();
如果換成
BMW.prototype = Car.prototype;
會有什么不一樣呢?
如果直接把prototype賦值給子類,子類中改變引用類型的屬性會影響到父類。而實例是通過分配空間,構造出來的個體,不會出現這種情況。看下面的例子:
1 function People(){} 2 People.prototype.name = 'human'; 3 People.prototype.hi = function(msg){ 4 console.log(msg); 5 } 6 7 var Student = function(){}; 8 //Student.prototype = new People(); 9 Student.prototype = People.prototype; 10 11 var stu = new Student(); 12 stu.hi && stu.hi('stu'); 13 Student.prototype.hi = {}; 14 People.prototype.hi('people'); //不是一個函數
ES5支持的Object.create()方法。其特點是潔凈繼承,和方式2相比,沒有實例化父類這一步,而是使用一個新對象,把父類的原型復制給新對象的原型。
模擬Object.create():
1 function create(proto){ //要繼承的父類的原型 2 function F(){}; 3 F.prototype = proto; 4 return new F(); 5 } 6 7 var student = create(People.prototype);