在ES5繼承的實現非常有趣的,由於沒有傳統面向對象類的概念,Javascript利用原型鏈的特性來實現繼承,這其中有很多的屬性指向和需要注意的地方。
原型鏈的特點和實現已經在之前的一篇整理說過了,就是通過將子類構造函數的原型作為父類構造函數的實例,這樣就連通了子類-子類原型-父類,原型鏈的特點就是逐層查找,從子類開始一直往上直到所有對象的原型Object.prototype,找到屬性方法之后就會停止查找,所以下層的屬性方法會覆蓋上層。
一個基本的基於原型鏈的繼承過程大概是這樣的:
//先來個父類,帶些屬性 function Super(){ this.flag = true; } //為了提高復用性,方法綁定在父類原型屬性上 Super.prototype.getFlag = function(){ return this.flag; } //來個子類 function Sub(){ this.subFlag = false; } //實現繼承 Sub.prototype = new Super; //給子類添加子類特有的方法,注意順序要在繼承之后 Sub.prototype.getSubFlag = function(){ return this.subFlag; } //構造實例 var es5 = new Sub;
原型鏈實現的繼承主要有幾個問題:
1、本來我們為了構造函數屬性的封裝私有性,方法的復用性,提倡將屬性聲明在構造函數內,而將方法綁定在原型對象上,但是現在子類的原型是父類的一個實例,自然父類的屬性就變成子類原型的屬性了;
這就會帶來一個問題,我們知道構造函數的原型屬性在所有構造的實例中是共享的,所以原型中屬性的改變會反應到所有的實例上,這就違背了我們想要屬性私有化的初衷;
2、創建子類的實例時,不能向父類的構造函數傳遞參數
function Super(){ this.flag = true; } function Sub(){ this.subFlag = false; } Sub.prototype = new Super; var obj = new Sub(); obj.flag = flase; //修改之后,由於是原型上的屬性,之后創建的所有實例都會受到影響 var obj_2 = new Sub(); console.log(obj.flag) //false;
為了解決以上兩個問題,有一個叫借用構造函數的方法
只需要在子類構造函數內部使用apply或者call來調用父類的函數即可在實現屬性繼承的同時,又能傳遞參數,又能讓實例不互相影響
function Super(){ this.flag = true; } function Sub(){ Super.call(this) //如果父類可以需要接收參數,這里也可以直接傳遞 } var obj = new Sub(); obj.flag = flase; var obj_2 = new Sub(); console.log(obj.flag) //依然是true,不會相互影響
結合借用構造函數和原型鏈的方法,可以實現比較完美的繼承方法,可以稱為組合繼承:
function Super(){ this.flag = true; } Super.prototype.getFlag = function(){ return this.flag; //繼承方法 } function Sub(){ this.subFlag = flase Super.call(this) //繼承屬性 } Sub.prototype = new Super; var obj = new Sub(); Super.prototype.getSubFlag = function(){ return this.flag; }
這里還有個小問題,Sub.prototype = new Super; 會導致Sub.prototype的constructor指向Super;
然而constructor的定義是要指向原型屬性對應的構造函數的,Sub.prototype是Sub構造函數的原型,所以應該添加一句糾正:
Sub.prototype.constructor = Sub;
看完ES5的實現,再來看看ES6的繼承實現方法,其內部其實也是ES5組合繼承的方式,通過call借用構造函數,在A類構造函數中調用相關屬性,再用原型鏈的連接實現方法的繼承
class B extends A { constructor() { return A.call(this); //繼承屬性 } } A.prototype = new B; //繼承方法
ES6封裝了class,extends關鍵字來實現繼承,內部的實現原理其實依然是基於上面所講的原型鏈,不過進過一層封裝后,Javascript的繼承得以更加簡潔優雅地實現
class ColorPoint extends Point { constructor(x, y, color) { super(x, y); // 等同於parent.constructor(x, y) this.color = color; } toString() { return this.color + ' ' + super.toString(); // 等同於parent.toString() } }
通過constructor來定義構造函數,用super調用父類的屬性方法
ES6中Class充當了ES5中,構造函數在繼承實現過程中的作用
同樣有原型屬性prototype,以及在ES5中用來指向構造函數原型的__proto__
屬性,這個屬性在ES6中的指向有一些主動的修改。
一個繼承語句同時存在兩條繼承鏈:一條實現屬性繼承,一條實現方法繼承。
class A extends B {} A.__proto__ === B; //繼承屬性 A.prototype.__proto__ === B.prototype; //繼承方法
ES6的子類的__proto__
是父類,子類的原型的__proto__
是父類的原型
第二條繼承鏈理解起來沒有什么問題,對應到ES5中的A.prototype = new B;A.prototype作為B構造的實例,指向構造函數B的原型B.prototype,
但是在ES5中A.__proto__
是指向Function.prototype的,因為每一個構造函數其實都是Function這個對象構造的,ES6中子類的__proto__
指向父類可以實現屬性的繼承,在ES5中在沒有用借用繼承的時候由於父類屬性被子類原型繼承,所有的子類實例實際上都是同一個屬性引用。
在ES6中實現了子類繼承父類屬性,在構造實例的時候會直接拿到子類的屬性,不需要查找到原型屬性上面去,ES6新的靜態方法和靜態屬性(只能在構造函數上訪問)也是通過這樣類的直接繼承來實現,至於普通復用方法還是放到原型鏈上,道理和實現和ES5是一樣的。
此外我認為這里修改A.__proto__
的指向是有意區分ES6中繼承和實例化,同時建立子類和父類直接的關系,ES5的子類的構造函數通過子類的原型與父類的構造函數連接,不存在直接的關系;
可以這么說,在ES5繼承和構造實例,ES6構造實例的時候可以理解__proto__
原型指針是用來指向構造函數的原型的,但是在ES6繼承中,__proto__
指繼承自哪個類或原型,在A繼承B之后,構造一個實例 var obj = new A; 會發現它所有的屬性指向都是和ES5一致的。
有個有趣的地方:ES6繼承是在父類創建this對象,在子類constructor中來修飾父類的this,ES5是在子類創建this,將父類的屬性方法綁定到子類,由於原生的構造函數(Function,Array等)沒有this,子類無法通過call/apply(this)獲得其內部屬性,所以在ES5無法繼承,ES6實現后可以為原生構造函數封裝一些有趣的接口,比方說阮一峰老師的這個給Array實例封裝一個版本記錄和回滾的方法:
class VersionedArray extends Array { constructor() { super(); this.history = [[]]; } commit() { this.history.push(this.slice()); } revert() { this.splice(0, this.length, ...this.history[this.history.length - 1]); } } var x = new VersionedArray(); x.push(1); x.push(2); x // [1, 2] x.history // [[]] x.commit(); x.history // [[], [1, 2]] x.push(3); x // [1, 2, 3] x.revert(); x // [1, 2]
最后做一個ES5和ES6的繼承小結:
ES5最經典的繼承方法是用組合繼承的方式,原型鏈繼承方法,借用函數繼承屬性,ES6也是基於這樣的方式,但是封裝了更優雅簡潔的api,讓Javascript越來越強大,修改了一些屬性指向,規范了繼承的操作,區分開了繼承實現和實例構造,此外ES6繼承還能實現更多的繼承需求和場景。
原文鏈接:http://www.jianshu.com/p/342966fdf816