大多面向對象語言都支持兩種繼承方式:接口繼承和實現繼承 ,而ECMAScript只支持實現繼承,且實現繼承主要是依靠原型鏈來實現。
先定義一個父類
<code>// 定義一個動物類
function Animal (name) {
// 屬性
this.name = name || 'Animal';
this.colors = ["black","red"]; //引用類型的屬性
// 實例方法
this.sleep = function(){
console.log(this.name + '正在睡覺!');
}
}
// 原型方法
Animal.prototype.eat = function(food) {
console.log(this.name + '正在吃:' + food);
};/<code>
原型鏈繼承
基本思想:利用原型對象讓一個引用類型繼承另外一個引用類型的屬性和方法。核心就是將父類的實例作為子類的原型對象。
構造函數,原型,實例之間的關係:每個構造函數都有一個原型對象prototype,原型對象prototype包含一個指向構造函數的指針constructor,而實例都包含一個指向原型對象的內部指針__proto__。
原型鏈實現繼承例子:
<code>function Cat(){
}
Cat.prototype = new Animal();
Cat.prototype.name = 'cat';
// Test Code
var cat = new Cat();
cat.name; // "cat"
cat.eat('fish'); // cat正在吃:fish
cat.sleep(); // cat正在睡覺!
cat instanceof Animal; //true
cat instanceof Cat; //true
cat.colors.push("orange");
cat.colors; // ["black","red","orange"]
//引用類型的屬性共享問題
var cat2 = new Cat();
cat2.colors: // ["black","red","orange"]/<code>
優點:
- 非常純粹的繼承關係,實例是子類的實例,也是父類的實例
- 父類新增原型方法/原型屬性,子類都能訪問到
- 簡單,易於實現
缺點:
- 要想給子類新增屬性和方法,必須在子類的原型對象上新增,不能放到子類構造函數中
- 無法實現多繼承
- 來自父類的引用類型屬性被所有子類的實例共享
- 創建子類實例時,無法向父類構造函數傳參
借用構造函數
基本思想:在子類構造函數的內部調用父類構造函數,通過使用call()和apply()方法可以在新創建的對象上執行構造函數。核心就是使用父類的構造函數來增強子類實例,等於是複製父類的實例屬性給子類(沒用到原型)。
<code>function Cat(name){
Animal.call(this,name);
}
// Test Code
var cat = new Cat("Kevin");
cat.name; //"Kevin"
cat.sleep(); //Kevin正在睡覺!
cat instanceof Animal; // false
cat instanceof Cat; // true
cat.colors.push("orange");
cat.colors; // ["black","red","orange"]
//不存在引用類型的屬性共享問題
var cat2 = new Cat();
cat2.colors; // ["black", "red"]
//不能繼承父類的原型屬性/方法
console.log(cat.eat); //undefined/<code>
優點:
- 解決了原型鏈繼承中,子類的實例共享父類引用類型屬性的問題
- 創建子類實例時,可以向父類傳遞參數
- 可以實現多繼承(call多個父類對象)
缺點:
- 實例並不是父類的實例,只是子類的實例
- 只能繼承父類的實例屬性和方法,不能繼承父類的原型屬性/方法
- 無法實現函數複用,每個子類都有父類實例函數的副本,影響性能
組合繼承
基本思想:將原型鏈和借用構造函數的技術組合在一塊,從而發揮兩者之長的一種繼承模式。核心就是通過調用父類構造函數,繼承父類的屬性並保留傳參的優點,然後通過將父類實例作為子類原型對象,實現函數複用。
<code>function Cat(name){
Animal.call(this, name); //繼承屬性 第二次調用Animal()
}
//繼承方法
Cat.prototype = new Animal(); //第一次調用Animal()
Cat.prototype.constructor = Cat; //修復構造函數指向
// Test Code
var cat = new Cat("Kevin");
cat.name; //"Kevin"
cat.sleep(); //Kevin正在睡覺!
cat instanceof Animal; // true
cat instanceof Cat; // true
cat.colors.push("orange");
cat.colors; // ["black","red","orange"]
cat.eat('fish'); // Kevin正在吃:fish
//不存在引用類型的屬性共享問題
var cat2 = new Cat();
cat2.colors; // ["black", "red"]/<code>
優點:
- 彌補了借用構造函數的缺陷,可以繼承實例屬性/方法,也可以繼承原型屬性/方法
- 既是子類的實例,也是父類的實例
- 不存在引用屬性共享問題
- 可傳參
- 函數可複用
缺點:
- 調用了兩次父類構造函數,生成了兩份實例(子類實例將子類原型上的那份屏蔽了)
原型式繼承
基本想法:藉助原型可以基於已有的對象創建新對象,同時還不必須因此創建自定義的類型。
原型式繼承的思想可用以下函數來說明:
<code>function object(o) {
function F(){}
F.prototype = o;
return new F();
}/<code>
在object()函數內部,先創建了要一個臨時性的構造函數,然後將傳入的對象作為這個構造函數的原型,最後返回了這個臨時類型的一個新實例。從本質上將,object()對傳入其中的對象執行力一次淺複製。
《JavaScript高級程序設計(第三版)》
<code>var person = {
name:"EvanChen",
friends:["Shelby","Court","Van"];
};
var anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
var yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
person.friends;//"Shelby","Court","Van","Rob","Barbie"/<code>
ECMAScript5通過新增Object.create()方法規範化了原型式繼承,這個方法接收兩個參數:一個用作新對象原型的對象和一個作為新對象定義額外屬性的對象。
《JavaScript高級程序設計(第三版)》
<code>var person = {
name:"EvanChen",
friends:["Shelby","Court","Van"];
};
var anotherPerson = Object.create(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
var yetAnotherPerson = Object.create(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
console.log(person.friends);//"Shelby","Court","Van","Rob","Barbie"/<code>
優點:
- 不需要創建構造函數
缺點:
- 存在引用類型的屬性共享問題
- 只適用一個對象繼承另一個對象
寄生式繼承
基本思想:創建一個僅用於封裝繼承過程的函數,該函數在內部以某種方式來增強對象,最後再像真正是它做了所有工作一樣返回對象。
<code>function object(o) {
function F(){}
F.prototype = o;
return new F();
}
//需要在原型式繼承的基礎上實現
function createAnother(original) {
var clone = object(original);
clone.sayHi = function () {
alert("hi");
};
return clone;
}
var person = {
name:"EvanChen",
friends:["Shelby","Court","Van"];
};
var anotherPerson = createAnother(person);
anotherPerson.sayHi();//"hi"/<code>
優點:
- 不需要創建構造函數
缺點:
- 存在引用類型的屬性共享問題
- 只適用一個對象繼承另一個對象
- 無法實現函數複用,影響性能
寄生組合繼承
基本思想:通過借用構造函數來繼承屬性,通過原型鏈的混成形式來繼承方法。
基本模型:
<code>function inheritProperty(subType, superType) {
var prototype = object(superType.prototype);//創建對象,使用原型式繼承的object方法
prototype.constructor = subType;//增強對象
subType.prototype = prototype;//指定對象
}/<code>
例子:
<code>function Cat(name){
Animal.call(this,name);//繼承屬性
}
inheritProperty(Cat, Animal);//繼承方法
// Test Code
var cat = new Cat("Kevin");
cat.name; //"Kevin"
cat.sleep(); //Kevin正在睡覺!
cat instanceof Animal; // true
cat instanceof Cat; //true/<code>
優點:
- 只調用了一次父類的構造函數,效率高,解決了組合繼承的缺點,堪稱完美
缺點:
- 實現較為複雜
ES6的類繼承
<code>class Parent {
constructor(x,y){
this.x = x;
this.y = y;
}
toString(){
}
}
class Child extends Parent{
constructor(x,y,color){
super(x,y);
this.color = color;
}
toString(){
return super.toString();
}
}/<code>
通過extends關鍵字繼承了Parent類所有的屬性和方法。子類的constructor方法 和 toString方法中都出現了super關鍵字,在這裡表示父類的構造函數,用來新建父類的this對象。子類必須在constructor方法中調用super方法,因為子類的this對象必須通過父類的構造函數完成創建, 並且得到與父類同樣的實例屬性和方法。如果不調用,子類就得不到this對象,就不能進行二次加工。
要注意的一個地方是,在子類的構造函數中,只有調用super才可以使用this關鍵字, 所以this關鍵字要放到super關鍵字之後, 是因為子類實例的構建,基於父類實例,只有super方法能調用父類實例。
優點:
- 使用方便,容易理解
缺點:
- 不兼容低端瀏覽器
閱讀更多 大前端學堂 的文章