一文完全吃透JavaScript繼承(面試必備良藥)
背景
繼承給我們提供了一種優(yōu)雅可復(fù)用的編碼方式,繼承也是面試中經(jīng)常被問(wèn)到的問(wèn)題,本文全面總結(jié)了JavaScript 中的繼承類(lèi)型、各個(gè)繼承類(lèi)型的優(yōu)缺點(diǎn)及使用場(chǎng)景等,一文吃透 JavaScript 繼承,收藏起來(lái)吧~
原型繼承
原型鏈?zhǔn)菍?shí)現(xiàn)原型繼承的主要方法,基本思想就是利用原型讓一個(gè)引用類(lèi)型繼承另一個(gè)引用類(lèi)型的屬性和方法。
實(shí)現(xiàn)原型鏈的基本模式
- function SuperType(){
- this.property=true;
- }
- SuperType.prototype.getSuperValue=function(){
- returnthis.property;
- }
- function SubType(){
- this.subproperty=false;
- }
- SubType.prototype=new SuperType();
- SubType.prototype.getSubValue=function(){
- returnthis.property;
- };
- var instance=new SubType();
- console.log(instance.getSuperValue()); //true;
例子中的實(shí)例及構(gòu)造函數(shù)和原型之間的關(guān)系圖:
在例子代碼中,定義了兩個(gè)對(duì)象,subType和superType。
兩個(gè)對(duì)象之間實(shí)現(xiàn)了繼承,而這種繼承方式是通過(guò)創(chuàng)建SuperType的實(shí)例并將該實(shí)例賦給subType.prototype實(shí)現(xiàn)的。實(shí)現(xiàn)的本質(zhì)就是重寫(xiě)了原型對(duì)象。
這樣subType.prototype中就會(huì)存在一個(gè)指針指向superType的原型對(duì)象。也就是說(shuō),存在superType的實(shí)例中的屬性和方法現(xiàn)在都存在于subType.prototype中了。這樣繼承了之后,又可以為subType添加新的方法和屬性。
要注意,這個(gè)指針([[prototype]])默認(rèn)情況下是不可以再被外部訪問(wèn)的,估計(jì)是會(huì)被一些內(nèi)部方法使用的,例如用for...in來(lái)遍歷原型鏈上可以被枚舉的屬性的時(shí)候,就需要通過(guò)這個(gè)指針找到當(dāng)前對(duì)象所繼承的對(duì)象。不過(guò),F(xiàn)irefox、Safari和Chrome在每個(gè)對(duì)象上都支持一個(gè)屬性__proto__。
原型繼承需要注意的一些問(wèn)題
1. 別忘記默認(rèn)的類(lèi)型
我們知道,所有的引用類(lèi)型都繼承了Object,而這個(gè)繼承也是通過(guò)原型鏈實(shí)現(xiàn)的。所以所有的對(duì)象都擁有Object具有的一些默認(rèn)的方法。如:hasOwnProperty()、propertyIsEnumerable()、toLocaleString()、toString()和valueOf()
2. 確定原型和實(shí)例的關(guān)系可以通過(guò)兩種方式來(lái)確定原型和實(shí)例之間的關(guān)系。
① 使用instanceof 操作符,只要用這個(gè)操作符來(lái)測(cè)試實(shí)例與原型鏈中出現(xiàn)過(guò)的構(gòu)造函數(shù),結(jié)果就會(huì)返回true。
② 第二種方式是使用isPrototypeOf()方法。同樣,只要是原型鏈中出現(xiàn)過(guò)的原型,都可以說(shuō)是該原型鏈所派生的實(shí)例的原型,因此isPrototypeOf()方法也會(huì)返回true。
例子:
- alert(instance instanceofObject); //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
③ 子類(lèi)要在繼承后定義新方法
因?yàn)?,原型繼承是實(shí)質(zhì)上是重寫(xiě)原型對(duì)象。所以,如果在繼承前就在子類(lèi)的prototype上定義一些方法和屬性。那么繼承的時(shí)候,子類(lèi)的這些屬性和方法將會(huì)被覆蓋。
如圖:
④ 不能使用對(duì)象字面量創(chuàng)建原型方法
這個(gè)的原理跟第三點(diǎn)的實(shí)際上是一樣的。當(dāng)你使用對(duì)象字面量創(chuàng)建原型方法重寫(xiě)原型的時(shí)候,實(shí)質(zhì)上相當(dāng)于重寫(xiě)了原型鏈,所以原來(lái)的原型鏈就被切斷了。如圖:
⑤ 注意父類(lèi)包含引用類(lèi)型的情況
如圖:
這個(gè)例子中的SuperType 構(gòu)造函數(shù)定義了一個(gè)colors 屬性,該屬性包含一個(gè)數(shù)組(引用類(lèi)型值)。SuperType 的每個(gè)實(shí)例都會(huì)有各自包含自己數(shù)組的colors 屬性。當(dāng)SubType 通過(guò)原型鏈繼承了SuperType 之后,SubType.prototype 就變成了SuperType 的一個(gè)實(shí)例,因此它也擁有了一個(gè)它自己的colors 屬性——就跟專(zhuān)門(mén)創(chuàng)建了一個(gè)SubType.prototype.colors 屬性一樣。但結(jié)果是什么呢?結(jié)果是SubType 的所有實(shí)例都會(huì)共享這一個(gè)colors 屬性。而我們對(duì)instance1.colors 的修改能夠通過(guò)instance2.colors 反映出來(lái)。也就是說(shuō),這樣的修改會(huì)影響各個(gè)實(shí)例。
原型繼承的缺點(diǎn)(問(wèn)題)
- 最明顯的就是上述第⑤點(diǎn),有引用類(lèi)型的時(shí)候,各個(gè)實(shí)例對(duì)該引用的操作會(huì)影響其他實(shí)例。
- 沒(méi)有辦法在不影響所有對(duì)象實(shí)例的情況下,給超類(lèi)型的構(gòu)造函數(shù)傳遞參數(shù)。
有鑒于此,實(shí)踐中很少會(huì)單獨(dú)使用原型繼承。
借用構(gòu)造函數(shù)繼承
在解決原型中包含引用類(lèi)型值所帶來(lái)問(wèn)題的過(guò)程中,開(kāi)發(fā)人員開(kāi)始使用一種叫做借用構(gòu)造函數(shù) (constructor stealing)的技術(shù)(有時(shí)候也叫做偽造對(duì)象或經(jīng)典繼承)。這種技術(shù)的基本思想相當(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
- SuperType.call(this);
- }
- 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"
基本思想
借用構(gòu)造函數(shù)的基本思想就是利用call或者apply把父類(lèi)中通過(guò)this指定的屬性和方法復(fù)制(借用)到子類(lèi)創(chuàng)建的實(shí)例中。因?yàn)閠his對(duì)象是在運(yùn)行時(shí)基于函數(shù)的執(zhí)行環(huán)境綁定的。也就是說(shuō),在全局中,this等于window,而當(dāng)函數(shù)被作為某個(gè)對(duì)象的方法調(diào)用時(shí),this等于那個(gè)對(duì)象。call 、apply方法可以用來(lái)代替另一個(gè)對(duì)象調(diào)用一個(gè)方法。call、apply 方法可將一個(gè)函數(shù)的對(duì)象上下文從初始的上下文改變?yōu)橛?thisObj 指定的新對(duì)象。
所以,這個(gè)借用構(gòu)造函數(shù)就是,new對(duì)象的時(shí)候(注意,new操作符與直接調(diào)用是不同的,以函數(shù)的方式直接調(diào)用的時(shí)候,this指向window,new創(chuàng)建的時(shí)候,this指向創(chuàng)建的這個(gè)實(shí)例),創(chuàng)建了一個(gè)新的實(shí)例對(duì)象,并且執(zhí)行SubType里面的代碼,而SubType里面用call調(diào)用了SuperTyep,也就是說(shuō)把this指向改成了指向新的實(shí)例,所以就會(huì)把SuperType里面的this相關(guān)屬性和方法賦值到新的實(shí)例上,而不是賦值到SupType上面。所有實(shí)例中就擁有了父類(lèi)定義的這些this的屬性和方法。
優(yōu)勢(shì)
相對(duì)于原型鏈而言,借用構(gòu)造函數(shù)有一個(gè)很大的優(yōu)勢(shì),即可以在子類(lèi)型構(gòu)造函數(shù)中向超類(lèi)型構(gòu)造函數(shù)傳遞參數(shù)。因?yàn)閷傩允墙壎ǖ絫his上面的,所以調(diào)用的時(shí)候才賦到相應(yīng)的實(shí)例中,各個(gè)實(shí)例的值就不會(huì)互相影響了。
例如:
- function SuperType(name){
- this.name = name;
- }
- function SubType(){
- //繼承了SuperType,同時(shí)還傳遞了參數(shù)
- SuperType.call(this, "Nicholas");
- //實(shí)例屬性
- this.age = 29;
- }
- var instance = new SubType();
- alert(instance.name); //"Nicholas";
- alert(instance.age); //29
劣勢(shì)
如果僅僅是借用構(gòu)造函數(shù),那么也將無(wú)法避免構(gòu)造函數(shù)模式存在的問(wèn)題——方法都在構(gòu)造函數(shù)中定義,因此函數(shù)復(fù)用就無(wú)從談起了。而且,在超類(lèi)型的原型中定義的方法,對(duì)子類(lèi)型而言也是不可見(jiàn)的,結(jié)果所有類(lèi)型都只能使用構(gòu)造函數(shù)模式??紤]到這些問(wèn)題,借用構(gòu)造函數(shù)的技術(shù)也是很少單獨(dú)使用的。
組合繼承
組合繼承(combination inheritance),有時(shí)候也叫做偽經(jīng)典繼承。是將原型鏈和借用構(gòu)造函數(shù)的技術(shù)組合到一塊,從而發(fā)揮二者之長(zhǎng)的一種繼承模式。
基本思想
思路是使用原型鏈實(shí)現(xiàn)對(duì)原型屬性和方法的繼承,而通過(guò)借用構(gòu)造函數(shù)來(lái)實(shí)現(xiàn)對(duì)實(shí)例屬性的繼承。這樣,既通過(guò)在原型上定義方法實(shí)現(xiàn)了函數(shù)復(fù)用,又能夠保證每個(gè)實(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();
- SubTypeSubType.prototype.constructor = 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)勢(shì)
組合繼承避免了原型鏈和借用構(gòu)造函數(shù)的缺陷,融合了它們的優(yōu)點(diǎn),成為JavaScript 中最常用的繼承模式。
劣勢(shì)
組合繼承最大的問(wèn)題就是無(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í)例屬性,但我們不得不在調(diào)用子類(lèi)型構(gòu)造函數(shù)時(shí)重寫(xiě)這些屬性。
寄生類(lèi)繼承
原型式繼承
其原理就是借助原型,可以基于已有的對(duì)象創(chuàng)建新對(duì)象。節(jié)省了創(chuàng)建自定義類(lèi)型這一步(雖然覺(jué)得這樣沒(méi)什么意義)。
模型
- function object(o){
- function W(){
- }
- W.prototype = o;
- returnnew W();
- }
ES5新增了Object.create()方法規(guī)范化了原型式繼承。即調(diào)用方法為:Object.create(o);
適用場(chǎng)景
只想讓一個(gè)對(duì)象跟另一個(gè)對(duì)象建立繼承這種關(guān)系的時(shí)候,可以用Object.create();這個(gè)方法,不兼容的時(shí)候,則手動(dòng)添加該方法來(lái)兼容。
寄生式繼承
寄生式繼承是原型式繼承的加強(qiáng)版。
模型
- function createAnother(origin){
- var clone=object(origin);
- clone.say=function(){
- alert('hi')
- }
- return clone;
- }
即在產(chǎn)生了這個(gè)繼承了父類(lèi)的對(duì)象之后,為這個(gè)對(duì)象添加一些增強(qiáng)方法。
寄生組合式繼承
實(shí)質(zhì)上,寄生組合繼承是寄生式繼承的加強(qiáng)版。這也是為了避免組合繼承中無(wú)可避免地要調(diào)用兩次父類(lèi)構(gòu)造函數(shù)的最佳方案。所以,開(kāi)發(fā)人員普遍認(rèn)為寄生組合式繼承是引用類(lèi)型最理想的繼承范式。
基本模式
- function inheritPrototype(SubType,SuperType){
- var prototype=object(SuperType.prototype);
- prototype.constructor=subType;
- subType.prototype=prototype;
- }
這個(gè)object是自定義的一個(gè)相當(dāng)于ES5中Object.create()方法的函數(shù)。在兼容性方面可以?xún)蓚€(gè)都寫(xiě)。
兼容寫(xiě)法
- function object(o){
- function W(){
- }
- W.prototype=o;
- returnnew W;
- }
- function inheritPrototype(SubType,SuperType){
- var prototype;
- if(typeofObject.create==='function'){
- prototype=Object.create(SuperType.prototype);
- }else{
- prototype=object.create(SuperType.prototype);
- }<br> prototype.constructor=SubType;
- SubType.prototype=prototype;
- }
Class繼承
Class 可以通過(guò)extends關(guān)鍵字實(shí)現(xiàn)繼承。子類(lèi)必須在constructor方法中調(diào)用super方法,否則新建實(shí)例時(shí)會(huì)報(bào)錯(cuò)。這是因?yàn)樽宇?lèi)自己的this對(duì)象,必須先通過(guò)父類(lèi)的構(gòu)造函數(shù)完成塑造,得到與父類(lèi)同樣的實(shí)例屬性和方法,然后再對(duì)其進(jìn)行加工,加上子類(lèi)自己的實(shí)例屬性和方法。如果不調(diào)用super方法,子類(lèi)就得不到this對(duì)象。
注意 :ES5 的繼承,實(shí)質(zhì)是先創(chuàng)造子類(lèi)的實(shí)例對(duì)象this,然后再將父類(lèi)的方法添加到this上面(Parent.apply(this))。ES6 的繼承機(jī)制完全不同,實(shí)質(zhì)是先將父類(lèi)實(shí)例對(duì)象的屬性和方法,加到this上面(所以必須先調(diào)用super方法),然后再用子類(lèi)的構(gòu)造函數(shù)修改this。
- class ColorPoint extends Point {
- constructor(x, y, color) {
- super(x, y); // 調(diào)用父類(lèi)的constructor(x, y)
- this.color = color;
- }
- toString() {
- returnthis.color + ' ' + super.toString(); // 調(diào)用父類(lèi)的toString()
- }
- }
Class的繼承鏈
大多數(shù)瀏覽器的 ES5 實(shí)現(xiàn)之中,每一個(gè)對(duì)象都有__proto__屬性,指向?qū)?yīng)的構(gòu)造函數(shù)的prototype屬性。Class 作為構(gòu)造函數(shù)的語(yǔ)法糖,同時(shí)有prototype屬性和__proto__屬性,因此同時(shí)存在兩條繼承鏈。
(1)子類(lèi)的__proto__屬性,表示構(gòu)造函數(shù)的繼承,總是指向父類(lèi)。
(2)子類(lèi)prototype屬性的__proto__屬性,表示方法的繼承,總是指向父類(lèi)的prototype屬性。
- class A {
- }
- class B extends A {
- }
- B.__proto__ === A // true
- B.prototype.__proto__ === A.prototype // true
上面代碼中,子類(lèi)B的__proto__屬性指向父類(lèi)A,子類(lèi)B的prototype屬性的__proto__屬性指向父類(lèi)A的prototype屬性。