這篇文章主要介紹JavaScript實現繼承的方式:javascript
一、類式繼承java
簡單的類式繼承:數組
// 聲明父類function Animal() { this.name = 'animal'; this.type = ['pig', 'cat'];}
// 爲父類添加共有方法Animal.prototype.greet = function(sound) { console.log(sound);}
// 聲明子類function Dog() { this.name = 'dog';}
// 繼承父類Dog.prototype = new Animal();
var dog = new Dog();dog.greet('汪汪'); // "汪汪"console.log(dog.type); // ["pig", "cat"]複製代碼
在上面的代碼中,咱們建立了兩個類Animal和Dog,並且給Animal.prototype
原型上添加了一個greet共有方法,而後經過new
命令實例化一個Animal,而且賦值給Dog.prototype
原型。bash
原理說明:在實例化一個類時,新建立的對象複製了父類的構造函數內的屬性與方法而且將原型__proto__
指向了父類的原型對象,這樣就擁有了父類的原型對象上的屬性與方法。app
不過,經過類式繼承方式,有兩個缺點。函數
第一個是引用缺陷:ui
dog.type.push('dog');var dog2 = new Dog();console.log(dog2.type); // ["dog", "cat", "dog"]複製代碼
經過上面的執行結果,咱們看到當經過dog實例對象修改繼承自Animal中的數組type(引用類型)時,另一個新建立的實例dog2也會受到影響。this
第二個是咱們沒法爲不一樣的實例初始化繼承來的屬性,咱們能夠修改一下上面的例子:spa
function Animal(color) { this.color = color;}...Dog.prototype = new Animal('白色');...console.log(dog.color); // "白色"console.log(do2.color); // "白色"複製代碼
經過上面的代碼能夠看到,咱們沒法爲不一樣dog賦值不一樣的顏色,全部dog只能同一種顏色。prototype
二、構造函數繼承
構造函數繼承方式能夠避免類式繼承的缺陷:
// 聲明父類function Animal(color) { this.name = 'animal'; this.type = ['pig','cat']; this.color = color;}
// 添加共有方法Animal.prototype.greet = function(sound) { console.log(sound);}
// 聲明子類function Dog(color) { Animal.apply(this, arguments);}
var dog = new Dog('白色');var dog2 = new Dog('黑色');
dog.type.push('dog');console.log(dog.color); // "白色"console.log(dog.type); // ["pig", "cat", "dog"]
console.log(dog2.type); // ["pig", "cat"]console.log(dog2.color); // "黑色"複製代碼
首先要知道apply
方法的運用,它是能夠更改函數的做用域,因此在上面的例子中,咱們在Dog子類中調用這個方法也就是將Dog子類的變量在父類中執行一遍,這樣子類就擁有了父類中的共有屬性和方法。
可是,構造函數繼承也是有缺陷的,那就是咱們沒法獲取到父類的共有方法,也就是經過原型prototype
綁定的方法:
dog.greet(); // Uncaught TypeError: dog.greet is not a function複製代碼
三、組合繼承
組合繼承其實就是將類式繼承和構造函數繼承組合在一塊兒:
// 聲明父類 function Animal(color) { this.name = 'animal'; this.type = ['pig','cat']; this.color = color; }
// 添加共有方法 Animal.prototype.greet = function(sound) { console.log(sound); }
// 聲明子類 function Dog(color) { // 構造函數繼承 Animal.apply(this, arguments); } // 類式繼承Dog.prototype = new Animal();
var dog = new Dog('白色'); var dog2 = new Dog('黑色');
dog.type.push('dog'); console.log(dog.color); // "白色"console.log(dog.type); // ["pig", "cat", "dog"]
console.log(dog2.type); // ["pig", "cat"]console.log(dog2.color); // "黑色"dog.greet('汪汪'); // "汪汪"
複製代碼
在上面的例子中,咱們在子類構造函數中執行父類構造函數,在子類原型上實例化父類,這就是組合繼承了,能夠看到它綜合了類式繼承和構造函數繼承的優勢,同時去除了缺陷。
可能你會奇怪爲何組合式繼承能夠去除類式繼承中的引用缺陷?其實這是因爲原型鏈
來決定的,因爲JavaScript引擎在訪問對象的屬性時,會先在對象自己中查找,若是沒有找到,纔會去原型鏈中查找,若是找到,則返回值,若是整個原型鏈中都沒有找到這個屬性,則返回undefined。
也就是說,咱們訪問到的引用類型(好比上面的type)實際上是經過apply
複製到子類中的,因此不會發生共享。
這種組合繼承也是有點小缺陷的,那就是它調用了兩次父類的構造函數。
五、寄生組合式繼承
寄生組合式繼承強化的部分就是在組合繼承的基礎上減小一次多餘的調用父類的構造函數:
function Animal(color) { this.color = color; this.name = 'animal'; this.type = ['pig', 'cat'];}
Animal.prototype.greet = function(sound) { console.log(sound);}
function Dog(color) { Animal.apply(this, arguments); this.name = 'dog';}
/* 注意下面兩行 */Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.getName = function() { console.log(this.name);}
var dog = new Dog('白色'); var dog2 = new Dog('黑色');
dog.type.push('dog'); console.log(dog.color); // "白色"console.log(dog.type); // ["pig", "cat", "dog"]
console.log(dog2.type); // ["pig", "cat"]console.log(dog2.color); // "黑色"dog.greet('汪汪'); // "汪汪"複製代碼
在上面的例子中,咱們並不像構造函數繼承同樣直接將父類Animal的一個實例賦值給Dog.prototype,而是使用Object.create()
進行一次淺拷貝,將父類原型上的方法拷貝後賦給Dog.prototype,這樣子類上就能擁有了父類的共有方法,並且少了一次調用父類的構造函數。
Object.create()的淺拷貝的做用類式下面的函數:
function create(obj) { function F() {}; F.prototype = obj; return new F();}複製代碼
這裏還需注意一點,因爲對Animal的原型進行了拷貝後賦給Dog.prototype,所以Dog.prototype上的constructor
屬性也被重寫了,因此咱們要修復這一個問題:
Dog.prototype.constructor = Dog;複製代碼
六、extends繼承
Class和extends
是在ES6中新增的,Class
用來建立一個類,extends
用來實現繼承:
class Animal { constructor(color) { this.color = color; } greet(sound) { console.log(sound); } }
class Dog extends Animal { constructor(color) { super(color); this.color = color; } }
let dog = new Dog('黑色'); dog.greet('汪汪'); // "汪汪"console.log(dog.color); // "黑色"
複製代碼
在上面的代碼中,建立了父類Animal,而後Dog子類繼承父類,兩個類中都有一個constructor構造方法,實質就是構造函數Animal和Dog。
不知道你有沒有注意到一點,我在子類的構造方法中調用了super方法,它表示父類的構造函數,用來新建父類的this對象。
注意:
子類必須在constructor
方法中先調用super
方法纔可使用this ,不然新建實例時會報錯。這是由於子類沒有本身的this對象,而是繼承父類的this對象,而後對其進行加工。若是不調用super方法,子類就得不到this對象。
ES5的繼承,實質是先創造子類的實例對象this,而後再將父類的方法添加到this上面(Parent.apply(this))。ES6的繼承機制徹底不一樣,實質是先創造父類的實例對象this(因此必須先調用super方法),而後再用子類的構造函數修改this。
在子類的構造函數中,只有調用super以後,纔可使用this關鍵字,不然會報錯。這是由於子類實例的構建,是基於對父類實例加工,只有super方法才能返回父類實例。