前面的話
學習如何創建對象是理解面向對象編程的第一步,第二步是理解繼承。開宗明義,繼承是指在原有對象的基礎上,略作修改,得到一個新的對象。javascript主要包括類式繼承、原型繼承和拷貝繼承這三種繼承方式。本文是javascript面向對象系列第三篇——實現繼承的3種形式
類式繼承
大多數面向對象的編程語言都支持類和類繼承的特性,而JS卻不支持這些特性,只能通過其他方法定義并關聯多個相似的對象,如new和instanceof。不過在后來的ES6中新增了一些元素,比如class關鍵字,但這并不意味著javascript中是有類的,class只是構造函數的語法糖而已
類式繼承的主要思路是,通過構造函數實例化對象,通過原型鏈將實例對象關聯起來。下面將對類式繼承進行詳細解釋
【原型鏈繼承】
javascript使用原型鏈作為實現繼承的主要方法,實現的本質是重寫原型對象,代之以一個新類型的實例。下面的代碼中,原來存在于SuperType的實例對象中的屬性和方法,現在也存在于SubType.prototype中了
function Super(){ this.value = true; } Super.prototype.getValue = function(){ return this.value; }; function Sub(){} //Sub繼承了Super Sub.prototype = new Super(); Sub.prototype.constructor = Sub; var instance = new Sub(); console.log(instance.getValue());//true
原型鏈最主要的問題在于包含引用類型值的原型屬性會被所有實例共享,而這也正是為什么要在構造函數中,而不是在原型對象中定義屬性的原因。在通過原型來實現繼承時,原型實際上會變成另一個類型的實例。于是,原先的實例屬性也就順理成章地變成了現在的原型屬性了
function Super(){ this.colors = ['red','blue','green']; } function Sub(){}; //Sub繼承了Super Sub.prototype = new Super(); var instance1 = new Sub(); instance1.colors.push('black'); console.log(instance1.colors);//'red,blue,green,black' var instance2 = new Sub(); console.log(instance2.colors);//'red,blue,green,black'
原型鏈的第二個問題是,在創建子類型的實例時, 不能向超類型的構造函數中傳遞參數。實際上,應該說是沒有辦法在不影響所有對象實例的情況下,給超類型的構造函數傳遞參數。再加上包含引用類型值的原型屬性會被所有實例共享的問題,在實踐中很少會單獨使用原型鏈繼承
【借用構造函數繼承】
借用構造函數(constructor stealing)的技術(有時候也叫做偽類繼承或經典繼承)。基本思想相當簡單,即在子類型構造函數的內部調用超類型構造函數,通過使用apply()和call()方法在新創建的對象上執行構造函數
function Super(){ this.colors = ['red','blue','green']; } function Sub(){ //繼承了Super Super.call(this); } var instance1 = new Sub(); instance1.colors.push('black'); console.log(instance1.colors);// ['red','blue','green','black'] var instance2 = new Sub(); console.log(instance2.colors);// ['red','blue','green']
相對于原型鏈而言,借用構造函數有一個很大的優勢,即可以在子類型構造函數中向超類型構造函數傳遞參數
function Super(name){ this.name = name; } function Sub(){ //繼承了Super,同時還傳遞了參數 Super.call(this,"bai"); //實例屬性 this.age = 29; } var instance = new Sub(); console.log(instance.name);//"bai" console.log(instance.age);//29
但是,如果僅僅是借用構造函數,那么也將無法避免構造函數模式存在的問題——方法都在構造函數中定義,因此函數復用就無從談起了
【組合繼承】
組合繼承(combination inheritance)有時也叫偽經典繼承,指的是將原型鏈和借用構造函數的技術組合到一塊,從而發揮二者之長的一種繼承模式。其背后的思路是使用原型鏈實現對原型屬性和方法的繼承,而通過借用構造函數來實現對實例屬性的繼承。這樣,既通過在原型上定義方法實現了函數復用,又能夠保證每個實例都有它自己的屬性
function Super(name){ this.name = name; this.colors = ['red','blue','green']; } Super.prototype.sayName = function(){ console.log(this.name); }; function Sub(name,age){ //繼承屬性 Super.call(this,name); this.age = age; } //繼承方法 Sub.prototype = new Super(); Sub.prototype.constructor = Sub; Sub.prototype.sayAge = function(){ console.log(this.age); } var instance1 = new Sub("bai",29); instance1.colors.push("black"); console.log(instance1.colors);//['red','blue','green','black'] instance1.sayName();//"bai" instance1.sayAge();//29 var instance2 = new Sub("hu",27); console.log(instance2.colors);//['red','blue','green'] instance2.sayName();//"hu" instance2.sayAge();//27
組合繼承有它自己的問題。那就是無論什么情況下,都會調用兩次父類型構造函數:一次是在創建子類型原型的時候,另一次是在子類型構造函數內部。子類型最終會包含父類型對象的全部實例屬性,但不得不在調用子類型構造函數時重寫這些屬性
function Super(name){ this.name = name; this.colors = ["red","blue","green"]; } Super.prototype.sayName = function(){ return this.name; }; function Sub(name,age){ // 第二次調用Super(),Sub.prototype又得到了name和colors兩個屬性,并對上次得到的屬性值進行了覆蓋 Super.call(this,name); this.age = age; } //第一次調用Super(),Sub.prototype得到了name和colors兩個屬性 Sub.prototype = new Super(); Sub.prototype.constructor = Sub; Sub.prototype.sayAge = function(){ return this.age; };
【寄生組合繼承】
解決兩次調用的方法是使用寄生組合式繼承。寄生組合式繼承與組合繼承相似,都是通過借用構造函數來繼承不可共享的屬性,通過原型鏈的混成形式來繼承方法和可共享的屬性。只不過把原型繼承的形式變成了寄生式繼承。使用寄生組合式繼承可以不必為了指定子類型的原型而調用父類型的構造函數,從而寄生式繼承只繼承了父類型的原型屬性,而父類型的實例屬性是通過借用構造函數的方式來得到的
[注意]下方中會對寄生繼承進行詳細說明
function Super(name){ this.name = name; this.colors = ["red","blue","green"]; } Super.prototype.sayName = function(){ return this.name; }; function Sub(name,age){ Super.call(this,name); this.age = age; } if(!Object.create){ Object.create = function(proto){ function F(){}; F.prototype = proto; return new F; } } Sub.prototype = Object.create(Super.prototype); Sub.prototype.constructor = Sub; var instance1 = new Sub("bai",29); instance1.colors.push("black"); console.log(instance1.colors);//['red','blue','green','black'] instance1.sayName();//"bai" var instance2 = new Sub("hu",27); console.log(instance2.colors);//['red','blue','green'] instance2.sayName();//"hu"
這個例子的高效率體現在它只調用了一次Super構造函數,并且因此避免了在Sub.prototype上面創建不必要的、多余的屬性。與此同時,原型鏈還保持不變
因此,開發人員普遍認為寄生組合式繼承是引用類型最理想的繼承范式,YUI的YAHOO.lang.extend()方法就采用了這種繼承模式
【ES6中的class】
如果使用ES6中的class語法,則上面代碼修改如下
[注意]關于關于ES6中的class語法,詳細情況移步至此
class Super { constructor(name){ this.name = name; this.colors = ["red","blue","green"]; } sayName(){ return this.name; } } class Sub extends Super{ constructor(name,age){ super(name); this.age = age; } } var instance1 = new Sub("bai",29); instance1.colors.push("black"); console.log(instance1.colors);//['red','blue','green','black'] instance1.sayName();//"bai" var instance2 = new Sub("hu",27); console.log(instance2.colors);//['red','blue','green'] instance2.sayName();//"hu"
ES6的class語法糖隱藏了許多技術細節,在實現同樣功能的前提下,代碼卻優雅不少
原型繼承
【原型繼承】
原型繼承,在《你不知道的javascript》中被翻譯為委托繼承
道格拉斯·克羅克福德(Douglas Crockford)在2006年寫了一篇文章,《javascript中的原型式繼承》。在這篇文章中,他介紹了一種實現繼承的方式,這種方式并沒有使用嚴格意義上的構造函數。他的想法是借助原型可以基于已有的對象來創建新對象,同時不必因此創建自定義類型
原型繼承的基礎函數如下所示
function object(o){ function F(){}; F.prototype = o; return new F(); }
在object()函數內部,先創建了一個臨時性的構造函數,然后將傳入的對象作為這個構造函數的原型,最后返回了這個臨時類型的一個新實例。從本質上講,object()對傳入其中的對象執行了一次淺復制
下面是一個例子
var superObj = { init: function(value){ this.value = value; }, getValue: function(){ return this.value; } } var subObj = object(superObj); subObj.init('sub'); console.log(subObj.getValue());//'sub'
ES5通過新增Object.create()方法規范化了原型式繼承
[注意]關于Object.create()方法的詳細內容移步至此
var superObj = { init: function(value){ this.value = value; }, getValue: function(){ return this.value; } } var subObj = Object.create(superObj); subObj.init('sub'); console.log(subObj.getValue());//'sub'
【與原型鏈繼承的關系】
原型繼承雖然只是看上去將原型鏈繼承的一些程序性步驟包裹在函數里而已。但是,它們的一個重要區別是父類型的實例對象不再作為子類型的原型對象
1、使用原型鏈繼承
function Super(){ this.value = 1; } Super.prototype.value = 0; function Sub(){}; //將父類型的實例對象作為子類型的原型對象 Sub.prototype = new Super(); Sub.prototype.constructor = Sub; //創建子類型的實例對象 var instance = new Sub; console.log(instance.value);//1
2、使用原型繼承
function Super(){ this.value = 1; } Super.prototype.value = 0; function Sub(){}; Sub.prototype = Object.create(Super.prototype); Sub.prototype.constructor = Sub; //創建子類型的實例對象 var instance = new Sub; console.log(instance.value);//0
上面的Object.create函數一行代碼Sub.prototype = Object.create(Super.prototype)可以分解為
function F(){}; F.prototype = Super.prototype; Sub.prototype = new F();
由上面代碼看出,子類的原型對象是臨時類F的實例對象,而臨時類F的原型對象又指向父類的原型對象;所以,實際上,子類可以繼承父類的原型上的屬性,但不可以繼承父類的實例上的屬性
原型繼承與原型鏈繼承都存在著子例共享父例引用類型值的問題
var superObj = { colors: ['red','blue','green'] }; var subObj1 = object(superObj); subObj1.colors.push("black"); var subObj2 = object(superObj); subObj2.colors.push("white"); console.log(superObj.colors);//["red", "blue", "green", "black", "white"] console.log(subObj1.colors);//["red", "blue", "green", "black", "white"]
【寄生式繼承】
寄生式繼承(parasitic)是與原型繼承緊密相關的一種思路,并且同樣是由道格拉斯·克羅克福德推而廣之的。寄生式繼承的思路與寄生構造函數和工廠模式類似,即創建一個僅用于封裝繼承過程的函數,該函數內部以某種方式來增強對象,最后再返回對象
function parasite(original){ var clone = Object.create(original);//通過調用函數創建一個新對象 clone.sayHi = function(){ //以某種方式來增強這個對象 console.log("hi"); }; return clone;//返回這個對象 } var superObj = { colors: ['red','blue','green'] }; var subObj1 = parasite(superObj); subObj1.colors.push('black'); var subObj2 = parasite(superObj); subObj2.colors.push('white'); console.log(superObj.colors);//["red", "blue", "green", "black", "white"] console.log(subObj1.colors);//["red", "blue", "green", "black", "white"]
由于原型繼承存在著引用類型的值被共享的問題,所以使用得并不很多,只在一些簡單應用場景下使用。如果需要解決該問題,則需要借用構造函數,與原型繼承的初衷相違背,相當于使用了類式繼承的終極寫法——寄生組合繼承
拷貝繼承
拷貝繼承在《javascript面向對象摘要》中翻譯為混入繼承,jQuery使用的就是拷貝繼承
拷貝繼承不需要改變原型鏈,通過拷貝函數將父例的屬性和方法拷貝到子例即可
[注意]關于對象拷貝的詳細信息移步至此
【拷貝函數】
下面是一個深拷貝的拷貝函數
function extend(obj,cloneObj){ if(typeof obj != 'object'){ return false; } var cloneObj = cloneObj || {}; for(var i in obj){ if(typeof obj[i] === 'object'){ cloneObj[i] = (obj[i] instanceof Array) ? [] : {}; arguments.callee(obj[i],cloneObj[i]); }else{ cloneObj[i] = obj[i]; } } return cloneObj; } var obj1={a:1,b:2,c:[1,2,3]}; var obj2=extend(obj1); console.log(obj1.c); //[1,2,3] console.log(obj2.c); //[1,2,3] obj2.c.push(4); console.log(obj2.c); //[1,2,3,4] console.log(obj1.c); //[1,2,3]
【對象間的拷貝繼承】
由于拷貝繼承解決了引用類型值共享的問題,所以其完全可以脫離構造函數實現對象間的繼承
function extend(obj,cloneObj){ if(typeof obj != 'object'){ return false; } var cloneObj = cloneObj || {}; for(var i in obj){ if(typeof obj[i] === 'object'){ cloneObj[i] = (obj[i] instanceof Array) ? [] : {}; arguments.callee(obj[i],cloneObj[i]); }else{ cloneObj[i] = obj[i]; } } return cloneObj; } var superObj = { arrayValue:[1,2,3], init: function(value){ this.value = value; }, getValue: function(){ return this.value; } } var subObj = extend(superObj); subObj.arrayValue.push(4); console.log(subObj.arrayValue);//[1,2,3,4] console.log(superObj.arrayValue);//[1,2,3]
【使用構造函數的拷貝組合繼承】
如果要使用構造函數,則屬性可以使用借用構造函數的方法,而引用類型屬性和方法使用拷貝繼承。相當于不再通過原型鏈來建立對象之間的聯系,而通過復制來得到對象的屬性和方法
function extend(obj,cloneObj){ if(typeof obj != 'object'){ return false; } var cloneObj = cloneObj || {}; for(var i in obj){ if(typeof obj[i] === 'object'){ cloneObj[i] = (obj[i] instanceof Array) ? [] : {}; arguments.callee(obj[i],cloneObj[i]); }else{ cloneObj[i] = obj[i]; } } return cloneObj; } function Super(name){ this.name = name; this.colors = ["red","blue","green"]; } Super.prototype.sayName = function(){ return this.name; }; function Sub(name,age){ Super.call(this,name); this.age = age; } Sub.prototype = extend(Super.prototype); var instance1 = new Sub("bai",29); instance1.colors.push("black"); console.log(instance1.colors);//['red','blue','green','black'] instance1.sayName();//"bai" var instance2 = new Sub("hu",27); console.log(instance2.colors);//['red','blue','green'] instance2.sayName();//"hu"
總結
本文介紹的類式繼承、原型繼承和拷貝繼承這三種繼承方式中,類式繼承用的最普遍,由于ES6中的class的語法糖,使其代碼復雜度大大降低;原型繼承由于無法處理引用類型值共享的問題,使用較少,但由原型繼承引申出的寄生組合繼承是類式繼承的范式方法;拷貝繼承使用范圍最廣泛,不僅可以實現原型之間的繼承,也可以脫離構造函數,直接實現對象間的繼承
總之,繼承主要就是處理父例和子例之間的兩個問題,即是否使用構造函數,及如何建立聯系
類式繼承的核心就是使用構造函數,通過原型鏈來建立聯系
原型繼承不使用構造函數,通過Object.create()來建立聯系
拷貝繼承使不使用構造函數都可以,通過復制來建立聯系
文章列表