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