15分鐘深入了解JS繼承分類、原理與用法
本文全面講述了JS繼承分類、原理與用法。分享給大家供大家參考,具體如下:
許多 OO 語言都支持兩種繼承方式:接口繼承和實現(xiàn)繼承。接口繼承只繼承方法簽名,而實現(xiàn)繼承則繼承實際的方法。由于 ECMAScript 中的函數(shù)沒有簽名,所以在 JS 中無法實現(xiàn)接口繼承。ECMAScript 只支持實現(xiàn)繼承,而且其實現(xiàn)繼承主要是依靠原型鏈來實現(xiàn)的。所以,下面所要說的原型鏈繼承、借用構(gòu)造函數(shù)繼承、組合繼承、原型式繼承、寄生式繼承和寄生組合式繼承都屬于實現(xiàn)繼承。
最后的最后,我會解釋 ES6 中的 extend 語法利用的是寄生組合式繼承。
1. 原型鏈繼承
ECMAScript 中描述了原型鏈的概念,并將原型鏈作為實現(xiàn)繼承的主要方法。其基本思想是利用原型讓一個引用類型繼承另一個引用類型的屬性和方法。實現(xiàn)原型鏈繼承有一種基本模式,其代碼大致如下:
function SuperType(){
this.property = true;
}
SuperType.prototype.getSuperValue = function(){
return this.property;
};
function SubType(){
this.subproperty = false;
}
SubType.prototype = new SuperType(); // 敲黑板!這是重點:繼承了 SuperType
SubType.prototype.getSubValue = function (){
return this.subproperty;
};
var instance = new SubType();
alert(instance.getSuperValue()); // true
原型鏈繼承的一個本質(zhì)是重寫原型對象,代之以一個新類型的實例;給原型添加方法的代碼一定要放在替換原型的語句之后;在通過原型鏈實現(xiàn)繼承時,不能使用對象字面量創(chuàng)建原型方法。
實例屬性在實例化后,會掛載在實例對象下面,因此稱之為實例屬性。上面的代碼中 SubType.prototype = new SuperType(); ,執(zhí)行完這條語句后,原 SuperType 的實例屬性 property 就掛載在了 SubType.prototype 對象下面。這其實是個隱患,具體原因后面會講到。
每次去查找屬性或方法的時候,在找不到屬性或方法的情況下,搜索過程總是要一環(huán)一環(huán)的前行到原型鏈末端才會停下來。
所有引用類型默認都繼承了 Object,而這個繼承也是通過原型鏈實現(xiàn)的。由此可知,所有函數(shù)的默認原型都是 object 的實例,因此函數(shù)的默認原型都會包含一個內(nèi)部指針,指向 Object.prototype 。
缺點:
- 最主要的問題來自包含引用類型值的原型。在通過原型來實現(xiàn)繼承時,原型實際上會變成另一個類型的實例。于是,原先的實例屬性也就順理成章地變成了現(xiàn)在的原型屬性了。
- 在創(chuàng)建子類型的實例時,不能向超類型的構(gòu)造函數(shù)傳遞參數(shù)。
* 題外話:確定原型與實例的關(guān)系的兩種方式
- 第一種方式是使用 instanceOf 操作符,只要用這個操作符來測試實例的原型鏈中是否出現(xiàn)過某構(gòu)造函數(shù)。如果有,則就會返回 true ;如果無,則就會返回 false 。以下為示例代碼:
- 第二種方式是使用 isPrototypeOf() 方法。同樣,只要是原型鏈中出現(xiàn)過的原型,都可以說是該原型鏈所派生出來的實例的原型。以下為示例代碼:
alert(instance instanceof Object); //true alert(instance instanceof SuperType); //true alert(instance instanceof SubType); //true
alert(Object.prototype.isPrototypeOf(instance)); //true alert(SuperType.prototype.isPrototypeOf(instance)); //true alert(SubType.prototype.isPrototypeOf(instance)); //true
2. 借用構(gòu)造函數(shù)繼承
借用構(gòu)造函數(shù)繼承,也叫偽造對象或經(jīng)典繼承。其基本思想相當簡單,即在子類型構(gòu)造函數(shù)的內(nèi)部調(diào)用超類型構(gòu)造函數(shù)。其繼承代碼大致如下:
function SuperType(){
this.colors = [ "red", "blue", "green"];
}
function SubType(){
SuperType.call(this); // 敲黑板!注意了這里繼承了 SuperType
}
var instance1 = new SubType();
instance1.colors.push("black");
alert(instance1.colors); // "red,blue,green,black"
var instance2 = new SubType();
alert(instance2.colors); // "red,blue,green"
通過使用 call() 方法(或 apply() 方法也可以),我們實際上是在(未來將要)新創(chuàng)建的子類的實例環(huán)境下調(diào)用父類構(gòu)造函數(shù)。
為了確保超類構(gòu)造函數(shù)不會重寫子類型的屬性,可以在調(diào)用超類型構(gòu)造函數(shù)后,再添加應(yīng)該在子類型中定義的屬性。
優(yōu)點:可以在子類型構(gòu)造函數(shù)中向超類型構(gòu)造函數(shù)傳遞參數(shù)。
缺點:
- 方法都在構(gòu)造函數(shù)中定義,每次實例化,都是新創(chuàng)建一個方法對象,因此函數(shù)根本做不到復(fù)用;
- 使用這種模式定義自定義類型,超類型的原型中定義的方法,對子類型而言是不可見。
3. 組合繼承
組合繼承(combination inheritance),有時候也叫做偽經(jīng)典繼承,其背后的思路是使用原型鏈實現(xiàn)對原型屬性和方法的繼承,而通過借用構(gòu)造函數(shù)來實現(xiàn)對實例屬性的繼承。其繼承代碼大致如下:
function SuperType(name){
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
alert(this.name);
};
function SubType(name, age){
SuperType.call(this, name); // 繼承屬性
this.age = age; // 先繼承,后定義新的自定義屬性
}
SubType.prototype = new SuperType(); // 繼承方法
Object.defineProperty( SubType.prototype, "constructor", { // 先繼承,后定義新的自定義屬性
enumerable: false, // 申明該數(shù)據(jù)屬性——constructor不可枚舉
value: SubType
});
SubType.prototype.sayAge = function(){ // 先繼承,后定義新的自定義方法
alert(this.age);
};
var instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
alert(instance1.colors); // "red, blue, green, black"
instance1.sayName(); // "Nicholas"
instance1.sayAge(); // 29
var instance2 = new SubType("Greg", 27);
alert(instance2.colors); // "red, blue, green"
instance2.sayName(); // "Greg";
instance2.sayAge(); // 27
優(yōu)點:
- 融合了原型鏈繼承和借用構(gòu)造函數(shù)繼承的優(yōu)點,避免了他們的缺陷;
instanceOf()和isPrototypeOf()也能夠用于識別基于組合繼承創(chuàng)建的對象。
缺點:
在實現(xiàn)繼承的時候,無論什么情況下,都會調(diào)用兩次超類型構(gòu)造函數(shù):一次是在創(chuàng)建子類型原型的時候,另一次是在子類型構(gòu)造函數(shù)內(nèi)部。子類型的原型最終會包含超類型對象的全部實例屬性,但我們不得不在定義子類型構(gòu)造函數(shù)時重寫這些屬性,因為子類型的原型中最好不要有引用類型值。但這在實際中,就造成了內(nèi)存的浪費。
4. 原型式繼承
原型式繼承所秉承的思想是:在不必創(chuàng)建自定義類型的情況下,借助原型鏈,基于已有的對象創(chuàng)建新對象。這其中會用到 Object.create() 方法,讓我們先來看看該方法的原理代碼吧:
function object(o){
function F(){}
F.prototype = o;
return new F();
}
從本質(zhì)上講,object() 對傳入其中的對象執(zhí)行了一次淺復(fù)制。
ECMAScript 5 想通過 Object.create() 方法規(guī)范化原型式繼承。這個方法接受兩個參數(shù):一參是被用來作為新對象原型的一個對象;二參為可選,一個為新對象定義額外屬性的對象,這個參數(shù)的格式與 Object.defineProperties() 的二參格式相同。以下為原型式繼承的示例代碼:
var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = Object.create(person, {
name: {
value: "Greg"
}
});
anotherPerson.friends.push("Rob");
alert(anotherPerson.name); //"Greg"
var yetAnotherPerson = Object.create(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
alert(person.friends); //"Shelby,Court,Van,Rob,Barbie"
缺點:所有實例始終都會共享源對象中的引用類型屬性值。
5. 寄生式繼承
寄生式(parasitic)繼承的思路與寄生構(gòu)造函數(shù)和工廠模式類似,即創(chuàng)建一個僅用于封裝繼承過程的函數(shù),該函數(shù)在內(nèi)部以某種方式來增強對象,最后再像真的是它做了所有工作一樣返回對象。下面來看看,寄生式繼承的示例代碼:
function object(o){
function F(){}
F.prototype = o;
return new F();
}
function createAnother(original){
var clone = object(original); // 通過調(diào)用函數(shù)創(chuàng)建一個新對象
clone.sayHi = function(){ // 以某種方式來增強這個對象
alert("hi");
};
return clone; // 返回這個對象
}
該繼承方式其實就是將原型式繼承放入函數(shù)內(nèi),并在其內(nèi)部增強對象,再返回而已。就相當于原型式繼承寄生于函數(shù)中,故而得名寄生式繼承。
前面示范繼承模式時使用的 object() 函數(shù)不是必需的;任何能夠返回新對象的函數(shù)都適用于此模式。
缺點:不能做到函數(shù)復(fù)用,效率低下。
6. 寄生組合式繼承(推薦)
寄生組合式繼承,即通過借用構(gòu)造函數(shù)來繼承屬性,通過原型鏈的混成形式來繼承方法。其背后的基本思路是:不必為了指定子類型的原型而調(diào)用超類型的構(gòu)造函數(shù),我們所需要的無非就是超類型原型的一個副本而已。本質(zhì)上,就是使用寄生式繼承來繼承超類型的原型,然后再將結(jié)果指定給子類型的原型。以下為寄生組合式繼承的實例代碼:
function object(o){
function F(){}
F.prototype = o;
return new F();
}
function inheritPrototype(subType, superType){
var prototype = object(superType.prototype); //創(chuàng)建對象
prototype.constructor = subType; //增強對象
subType.prototype = prototype; //指定對象
}
function SuperType(name){
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
alert(this.name);
};
function SubType(name, age){
SuperType.call(this, name); // 繼承屬性
this.age = age;
}
inheritPrototype(SubType, SuperType); // 繼承原型方法
SubType.prototype.sayAge = function(){
alert(this.age);
};
優(yōu)點:
- 只調(diào)用一次超類型構(gòu)造函數(shù);
- 避免了在子類原型上創(chuàng)建不必要的、多余的屬性,節(jié)省內(nèi)存空間;
- 原型鏈還能正常保持不變,也就意味著能正常使用 instanceOf 和 isPrototypeOf() 進行對象識別。
寄生組合式繼承是最理想的繼承方式。
7. ES6 中的 extend 繼承
來看看 ES6 中 extend 如何實現(xiàn)繼承的示例代碼:這一塊的內(nèi)容解釋,我閱讀的是這篇文章,欲知原文,請戳這里~
class Child extends Parent{
name ='qinliang';
sex = "male";
static hobby = "pingpong"; //static variable
constructor(location){
super(location);
}
sayHello (name){
super.sayHello(name); //super調(diào)用父類方法
}
}
我們再來看看 babel 編譯過后的代碼中的 _inherit() 方法:
function _inherits(subClass, superClass) {
//SuperClass必須是一個函數(shù),同時非null
if (typeof superClass !== "function" && superClass !== null) {
throw new TypeError("Super expression must either be null or a function, not " + typeof superClass);
}
subClass.prototype = Object.create( // 寄生組合式繼承
superClass && superClass.prototype, //原型上的方法、屬性全部被繼承過來了
{
constructor: { // 并且定義了新屬性,這里是重寫了constructor屬性
value: subClass,
enumerable: false, // 并實現(xiàn)了該屬性的不可枚舉
writable: true,
configurable: true
}
}
);
if (superClass) // 實現(xiàn)類中靜態(tài)變量的繼承
Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
}
從這里我們就可以很明顯的看出 ES6 中的 extend 語法,在內(nèi)部實現(xiàn)繼承時,使用的是寄生組合式繼承。
下面我們來看看編譯過后,除了 _inherit() 方法外的其他編譯結(jié)果代碼:
"use strict";
var _createClass = function () { // 利用原型模式創(chuàng)建自定義類型
function defineProperties(target, props) { // 對屬性進行數(shù)據(jù)特性設(shè)置
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ("value" in descriptor)
descriptor.writable = true;
Object.defineProperty(target, descriptor.key, descriptor);
}
}
return function (Constructor, protoProps, staticProps) {
// 設(shè)置Constructor的原型屬性到prototype中
if (protoProps) defineProperties(Constructor.prototype, protoProps);
// 設(shè)置Constructor的static類型屬性
if (staticProps) defineProperties(Constructor, staticProps);
return Constructor;
};
}();
var _get = function get(object, property, receiver) { // 調(diào)用子類的方法之前會先調(diào)用父類的方法
// 默認從Function.prototype中獲取方法
if (object === null) object = Function.prototype;
// 獲取父類原型鏈中的指定方法
var desc = Object.getOwnPropertyDescriptor(object, property);
if (desc === undefined) {
var parent = Object.getPrototypeOf(object); // 繼續(xù)往上獲取父類原型
if (parent === null) {
return undefined;
} else { // 繼續(xù)獲取父類原型中指定的方法
return get(parent, property, receiver);
}
} else if ("value" in desc) {
return desc.value; // 返回獲取到的值
} else {
var getter = desc.get; // 獲取原型的getter方法
if (getter === undefined) {
return undefined;
}
return getter.call(receiver); // 接著調(diào)用getter方法,并傳入this對象
}
};
function _classCallCheck(instance, Constructor) { // 保證了我們的實例對象是特定的類型
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
// 在子類的構(gòu)造函數(shù)中調(diào)用父類的構(gòu)造函數(shù)
function _possibleConstructorReturn(self, call) { // 一參為子類的this,二參為父類的構(gòu)造函數(shù)
if (!self) {
throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
}
return call && (typeof call === "object" || typeof call === "function") ? call : self;
}
var Child = function (_Parent) {
_inherits(Child, _Parent);
function Child(location) { // static variable
_classCallCheck(this, Child); // 檢測this指向問題
// 調(diào)用父類的構(gòu)造函數(shù),并傳入子類調(diào)用時候的參數(shù),生成父類的this或者子類自己的this
var _this = _possibleConstructorReturn(this, (Child.__proto__ || Object.getPrototypeOf(Child)).call(this, location));
_this.name = 'qinliang';
_this.sex = "male";
return _this;
}
_createClass(Child, [{ //更新Child類型的原型
key: "sayHello",
value: function sayHello(name) {
// super調(diào)用父類方法,將調(diào)用子類的super.sayHello時候傳入的參數(shù)傳到父類中
_get(Child.prototype.__proto__ || Object.getPrototypeOf(Child.prototype), "sayHello", this).call(this, name);
}
}]);
return Child;
}(Parent);
Child.hobby = "pingpong";
從我的注釋中就可以看出 _possibleConstructorReturn() 函數(shù),其實就是寄生組合式繼承中唯一一次調(diào)用超類型構(gòu)造函數(shù),從而對子類型構(gòu)造函數(shù)進行實例化環(huán)境的初始化。從這點,我們可以更加確定的 ES6 中的 extend 使用的是寄生組合式繼承。
更多關(guān)于JavaScript相關(guān)內(nèi)容還可查看本站專題:《javascript面向?qū)ο笕腴T教程》、《JavaScript錯誤與調(diào)試技巧總結(jié)》、《JavaScript數(shù)據(jù)結(jié)構(gòu)與算法技巧總結(jié)》、《JavaScript遍歷算法與技巧總結(jié)》及《JavaScript數(shù)學(xué)運算用法總結(jié)》
希望本文所述對大家JavaScript程序設(shè)計有所幫助。
相關(guān)文章
javascript封裝addLoadEvent實現(xiàn)頁面同時加載執(zhí)行多個函數(shù)的方法
這篇文章主要介紹了javascript封裝addLoadEvent實現(xiàn)頁面同時加載執(zhí)行多個函數(shù)的方法,實例分析了onload事件執(zhí)行的原理與同時執(zhí)行多個函數(shù)功能的實現(xiàn)技巧,需要的朋友可以參考下2016-07-07
JavaScript實現(xiàn)網(wǎng)頁版的五子棋游戲
這篇文章主要為大家詳細介紹了JavaScript實現(xiàn)網(wǎng)頁版的五子棋游戲,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-05-05
JavaScript forEach中return失效問題解決方案
這篇文章主要介紹了JavaScript forEach中return失效問題解決方案,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-06-06
JavaScript 組件之旅(二)編碼實現(xiàn)和算法
話說上期我們討論了隊列管理組件的設(shè)計,并且給它取了個響亮而獨特的名字:Smart Queue. 這次,我們要將之前的設(shè)計成果付諸實踐,用代碼來實現(xiàn)它。2009-10-10

