繼承是面向對象語言的一個重要的特性和概念。許多的面向對象語言中都支持兩種繼承方式:接口繼承和實現繼承。
javaScript 僅支持實現繼承,主要是靠原型鏈來實現。
原型: Javascript 的所有函授都有一個PRototype屬性,這個屬性引用了一個對象,即原型對象,也簡稱原型。 注:在JavaScript中對象可以分為函數對象和普通對象。只用函數對象包含prototype屬性。凡是通過 new Function() 創建的對象都是函數對象,其他的都是普通對象。
原型鏈:由于原型對象本身也是對象,它也有自己的原型。而它自己的原型對象又可以有自己的原型,這樣就組成了一條鏈,這個就是原型鏈。 JavaScritp引擎在訪問對象的屬性時,如果在對象本身中沒有找到,則會去原型鏈中查找。如果找到,直接返回值;如果整個鏈都遍歷且沒有找到屬性,則返回undefined。原型鏈一般實現為一個鏈表,這樣就可以按照一定的順序來查找。
構造函數:當任意一個普通函數用于創建一類對象時,它就被稱作構造函數,或構造器。構造函數必須滿子以下幾點: 1、在函數內部對新對象(this)的屬性進行設置,通常是添加屬性和方法。 2、構造函數可以包含返回語句(不推薦),但返回值必須是this,或者其它非對象類型的值。
關系: 1、構造函數通過 new 來創建實例對象;構造函數包含一個原型對象(prototype); 2、原型對象包含一個指向構造函數的指針(constructor); 3、實例對象包含一個指向原型對象的內部指針(_ _ proto _ _); 4、圖一: 5、圖二:
通過圖二來驗證圖一: 1、構造函數 A,通過 new 創建了實例對象 B。A.prototype 為 Object{}(原型對象); 2、Object{}(原型對象)的 constructor 指向構造函數 function A(); 3、B. _ _ proto _ _指向Object{}(原型對象)
圖三: 注:每個函數都是Function函數創建的對象,所以每個函數也有一個 _ _ proto _ _ 屬性指向Function函數的原型。這里需要指出的是,真正形成原型鏈的是每個對象的 _ _ proto _ _ 屬性,而不是函數的prototype屬性,這是很重要的。
圖四: 我們通過圖四來驗證圖三的正確性:
綜圖所述
所有的對象都有 _ _ proto _ _ 屬性,該屬性對應該對象的原型.所有的函數對象都有prototype屬性,該屬性的值會被賦值給該函數創建的對象的 _ _ proto _ _ 屬性.所有的原型對象都有constructor屬性,該屬性對應創建所有指向該原型的實例的構造函數.函數對象和原型對象通過prototype和constructor屬性進行相互關聯.1、基本模式:
var Parent = function() { this.name = 'parent';};Parent.prototype.getName = function() { return this.name;};Parent.prototype.obj = { a: 1};var Child = function() { this.name = 'child';};Child.prototype = new Parent();var parent = new Parent();var child = new Child();console.log(parent.getName()); //parentconsole.log(child.getName()); //child這種是最簡單實現原型繼承的方法,直接把父類的對象賦值給子類構造函數的原型,這樣子類的對象就可以訪問到父類以及父類構造函數的prototype中的屬性。
圖五: 如圖五可知:child -> Parent -> Function -> Object -> null
注:這種方法的優點很明顯,實現十分簡單,不需要任何特殊的操作;同時缺點也很明顯,如果子類需要做跟父類構造函數中相同的初始化動作,那么就得在子類構造函數中再重復一遍父類中的操作。如果初始化工作不斷增加,這種方式是很不方便的。
2、使用構造函數
var Parent = function(name) { this.name = name || 'parent';};Parent.prototype.getName = function() { return this.name;};Parent.prototype.obj = { a: 1};var Child = function(name) { // 在子類構造函數中通過apply調用父類的構造函數來進行相同的初始化工作。 // 這樣不管父類中做了多少初始化工作,子類也可以執行同樣的初始化工作 Parent.apply(this, arguments);};// Child.prototype = new Parent() ;// 使用這種方式父類構造函數被執行了兩次,一次是在子類構造函數中,一次在賦值子類原型時,這是很多余的。// 所以我們還需要做一個改進Child.prototype = Parent.prototype;var parent = new Parent('myParent');var child = new Child('myChild');console.log(parent.getName()); //myParentconsole.log(child.getName()); //myChild圖六: 根據圖六得知,此時的原型鏈為:child/parent -> Object -> null
注:上面借用構造函數模式最后改進的版本還是存在問題,它把父類的原型直接賦值給子類的原型,這就會造成一個問題,就是如果對子類的原型做了修改,那么這個修改同時也會影響到父類的原型,進而影響父類對象,這個肯定不是大家所希望看到的。
3、臨時構造函數模式(圣杯模式)
var Parent = function(name) { this.name = name || 'parent';};Parent.prototype.getName = function() { return this.name;};Parent.prototype.obj = { a: 1};var Child = function(name) { Parent.apply(this, arguments);};var F = function() {};F.prototype = Parent.prototype;Child.prototype = new F();var parent = new Parent('myParent');var child = new Child('myChild');console.log(parent.getName()); //myParentconsole.log(child.getName()); //myChild圖七: 如上圖:此時的原型鏈為:child -> parent -> Object -> null
4、再次改進
console.log(child.obj.a) ; //1console.log(parent.obj.a) ; //1child.obj.a = 2 ;console.log(child.obj.a) ; //2console.log(parent.obj.a) ; //2我們在圣杯模式下添加上述代碼。查看結果會發現,當我們改變child.obj.a的值的時候,parent對應的也會改變。 出現這個情況是因為當訪問child.obj.a的時候,我們會沿著原型鏈一直找到父類的prototype中,然后找到了obj屬性,然后對obj.a進行修改。 改進方式:對父類原型進行拷貝然后再賦值給子類原型,這樣當子類修改原型中的屬性時就只是修改父類原型的一個拷貝,并不會影響到父類原型。
var deepClone = function(source, target) { source = source || {}; target = target || {}; var toStr = Object.prototype.toString, arrStr = '[object array]'; for (var i in source) { if (source.hasOwnProperty(i)) { var item = source[i]; if (typeof item === 'object') { target[i] = (toStr.apply(item).toLowerCase() === arrStr) ? [] : {}; deepClone(item, target[i]); } else { target[i] = item; } } } return target;};var Parent = function(name) { this.name = name || 'parent';};Parent.prototype.getName = function() { return this.name;};Parent.prototype.obj = { a: 1};var Child = function(name) { Parent.apply(this, arguments);};Child.prototype = deepClone(Parent.prototype);var parent = new Parent('myParent');var child = new Child('myChild');console.log(child.obj.a); //1console.log(parent.obj.a); //1child.obj.a = 2;console.log(child.obj.a); //2console.log(parent.obj.a); //1說了這么多,其實Javascript中實現繼承是十分靈活多樣的,并沒有一種最好的方法,需要根據不同的需求實現不同方式的繼承,最重要的是要理解Javascript中實現繼承的原理,也就是原型和原型鏈的問題,只要理解了這些,自己實現繼承就可以游刃有余。
參考來源: 再談Javascript原型繼承:https://segmentfault.com/a/1190000000766541
新聞熱點
疑難解答