物件導向的特徵之一就是繼承。大多數物件導向的程式語言都支援兩種繼承方式:介面繼承和實作繼承。介面繼承只繼承方法簽名,而實作繼承則繼承實際的方法。由於在JavaScript中函數沒有簽名,所以無法實作介面繼承。在JavaScript中主要是透過原型鏈來實現繼承。
基於原型鏈實現繼承
基於原型鏈實現繼承的基本思想是利用原型讓一個引用類型繼承另一個引用類型的屬性和方法。在前面我們已經介紹了原型,建構函數和物件實例之間的關係,並詳細的分析了它們的記憶體模型結構。我們透過下面的例子來分析JavaScript基於原型鏈實作繼承的方法。
// 创建父类 function Parent(){ this.parentValue = "Parent"; } // 在父类的原型中添加方法 Parent.prototype.showParentValue = function(){ alert(this.parentValue); } // 创建子类 function Child(){ this.childValue ="Child"; } // 实现继承,让子类Child的原型链指向Parent对象 Child.prototype = new Parent(); // 在子类的原型中添加方法 Child.prototype.showChildValue = function(){ alert(this.childValue); } // 创建子类对象 var c = new Child(); // 子类对象调用继承自父类的方法 c.showParentValue(); // 子类对象调用自己的方法 c.showChildValue();
在上面的程式碼中,我們先建立了一個父類別Parent,並在它的原型中加入了showParentValue方法。
接著我們創建了一個子類別Child,並透過讓子類別的原型鏈指向父類別來實現繼承。
/** 实现继承的关键代码 **/Child.prototype = new Parent();
接著在子類別的原型鏈中加入showChildValue方法。然後創建了一個子類別對象,這時的子類別對象c既可以呼叫自己的方法,也可以呼叫繼承自父類別的方法。
在執行上面的程式碼之後,子類別和父類別的記憶體模型圖如下圖所示:
從上圖我們可以看到,在建立子類別的時候,子類別的prototype屬性是指向子類別的原型物件的。當我們透過Child.prototype = new Parent();語句讓子類別的原型指向父類別物件的時候,實際上是重寫了子類別的原型物件。此時在子類別的原型物件中會有一個_proto_屬性指向父類別的原型物件。而原來的子類別原型物件(圖中紅色區域)其實已經沒有用了。
之後我們在子類別原型中加入的方法會被加入到新的子類別原型中。同時,父類別中的屬性也會被寫入到新的子類別原型中。
當我們建立了子類別物件c之後,透過物件c呼叫了父類別的showParentValue方法。物件c在自己的空間中沒有找到這個方法,就會透過_proto_屬性去子類別的原型中查找,同樣也沒有找到這個方法,接著它有透過原型中的_proto_屬性到父類別的原型中去查找,這時,showParentValue方法被找到,並且被正確的執行。
上面的過程就是基於原型鏈實現繼承的過程。
完整的基於原型鏈實作繼承的記憶體模型
在上面的基於原型鏈實作繼承的記憶體模型中,我們實際上還少描述了一個物件:Object。所有的引用型別都是繼承自Object,這個繼承也是透過原型鏈來實現的。因此,完整的基於原型鏈實作繼承的記憶體模型如下圖所示:
所有函數的預設原型都是Object,所以預設原型都會包含一個內部指標指向Object.prototype。在Object.prototype中會有內建的hasOwnProperty、isPrototypeOf、propertyEmunerable、toLocaleString、toString和valueOf方法,所以我們自訂的類型都會繼承這些方法。
使用原型鏈進行繼承時的注意事項
在使用原型鏈進行繼承的時候要注意以下問題:
1、不能在設定了原型鏈之後再重新為原型鏈賦值。
2、一定要在原型鏈賦值之後才能加入或覆蓋方法。
對於第一點,我們來看下面的範例:
function Parent(){ this.parentValue = "Parent"; } Parent.prototype.showParentValue = function(){ alert(this.parentValue); } function Child(){ this.childValue ="Child"; } //实现继承,让Child的原型链指向Parent对象 Child.prototype = new Parent(); //下面的操作重写了Child的原型 Child.prototype = { showChildValue:function(){ alert(this.value); } }
在上面的代码中,我们分别创建了一个父类和一个子类,并让子类的原型指向父类对象,实现继承。但是在这之后,我们又通过Child.prototype = {...}的方式重写了子类的原型,在原型的重写一文中我们已经讲过,重写原型实际上是使子类的原型重新指向一个新的子类原型,这个新的子类原型和父类之间并没有任何的关联关系,所以子类和父类之间此时不再存在继承关系。
对于第二点也很好理解,也来看一个例子:
function Parent(){ this.parentValue = "Parent"; } Parent.prototype.showParentValue = function(){ alert(this.parentValue); } function Child(){ this.childValue ="Child"; } //在实现继承之前为子类在原型中添加方法 Child.prototype.showChildValue = function(){ alert(this.childValue); } //实现继承,让Child的原型链指向Parent对象 Child.prototype = new Parent();
在上面的代码中,我们分别创建了父类和子类。在创建子类之后马上为子类在原型中添加一个方法,然后才让Child的原型链指向Parent对象,实现继承。
这样做的后果是实现继承之后,子类指向的是新的子类原型,而前面添加的方法是放置在原来的原型中的(内存模型图中的红色区域),所以在实现继承之后,子类对象将不再拥有这个方法,因为原来的原型现在已经没有作用了。
方法的覆盖及原型链继承的缺点
如果我们需要实现子类的方法来覆盖父类的方法,只需要在子类的原型中添加与父类同名的方法即可。
/** 覆盖父类中的showParentValue方法 **/ Child.prototype.showParentValue = function(){ alert("Override Parent method"); }
原型链虽然是否强大,可以实现继承,但是原型链也存在一些缺点。原型链继承的缺点主要有:
1、使用原型链进行继承最大的缺点是无法从子类中调用父类的构造函数,这样就没有办法把子类中的属性赋值到父类中。
2、如果父类中存在引用类型的属性,此时这个引用类型会添加到子类的原型中,当第一个对象修改这个引用之后,其它对象的引用同样会被修改。
原型链和原型在处理引用类型的值的时候存在同样的问题。我们在介绍原型的时候曾经举过一个使用引用类型的例子。在使用原型链时同样会有这个问题。来看下面的例子:
// 创建父类 function Parent(){ this.parentValue = "Parent"; //引用类型的属性 this.friends = ['Leon','Ada']; } // 在父类的原型中添加方法 Parent.prototype.showParentValue = function(){ console.info(this.name+"["+this.friends+"]"); } // 创建子类 function Child(){ this.childValue ="Child"; } // 实现继承,让子类Child的原型链指向Parent对象 Child.prototype = new Parent(); // 在子类的原型中添加方法 Child.prototype.showChildValue = function(){ console.info(this.name+"["+this.friends+"]"); } // 创建子类对象 var person1 = new Child(); person1.name = "Jack"; person1.friends.push('Tom'); var person2 = new Child(); person2.name = "John"; console.info(person2.friends);
在上面的代码中,在父类中有一个引用类型的数组对象属性friends,在子类实现继承之后,子类对象person1为它的friends添加了一个新的朋友,此时,新的朋友是被添加到父类的原型中的,所以在这之后创建的所有新的对象都会有一个新的朋友“Tom”。这就是引用类型属性存在的问题。
以上就是JavaScript面向对象-基于原型链实现继承的内容,更多相关内容请关注PHP中文网(www.php.cn)!