金华婺城建设局网站,手机网站 普通网站,做电影网站收入,扁平式风格网站阐述JavaScript中常见的七大继承方案#x1f4d6;序言#x1f4d4;文章内容抢先看#x1f4dd;一、基础知识预备1. 继承的定义2. 继承的方式#x1f4da;二、6大常见继承方式1. 原型链继承 #x1f4a1;#xff08;1#xff09;构造函数、原型和实例的关系#xff08;2… 阐述JavaScript中常见的七大继承方案序言文章内容抢先看一、基础知识预备1. 继承的定义2. 继承的方式二、6大常见继承方式1. 原型链继承 1构造函数、原型和实例的关系2基本思想3实现原型链继承4图例阐述5破坏原型链6优缺点2. 盗用构造函数继承1基本思想2使用方式3实现原型链继承4优缺点3. 组合继承1基本思想2实现组合继承3图例4优缺点4. 原型式继承1基本思想2实现原型式继承3优缺点5. 寄生式继承1基本思想2实现寄生式继承3优缺点6. 寄生式组合继承1基本思想2实现寄生式组合继承3图例4优缺点️三、Class的继承1. 基本概念2. Object.getPrototypeOf()3. super关键字1作为函数使用2作为对象使用1在普通方法中2在静态方法中4. 类的prototype属性和 __ proto __ 属性1class的继承链2特殊情况继承3实例的 __ proto __ 属性四、结束语彩蛋 One More Thing参考资料番外篇序言
在前端的面试中继承是一道很常考的题目面试官总会变着法来问你。比如说你知道哪几种继承方式这几种继承方式有什么区别这几种继承方式的优缺点是啥又各有什么特点呢
一招致命就像周一第一次遇到这道题的时候只记得有那么几种继承方式但是我也说不上个所以然知识还是太浮动于表面了。
因此写下这篇文章来总结各种继承方式的知识点。一起来学习叭~✨
文章内容抢先看
在真正进入本文的讲解之前我们先用一张思维导图来了解本文所涉及到的内容。详情见下图 下面开始进入本文的讲解~
一、基础知识预备
1. 继承的定义
引用 Javascript 高级语言程序设计中的一句话
继承是面向对象编程 (即 object-oriented language 下面简称OO语言) 中讨论的最多的一个话题。很多 OO语言 都支持两种继承接口继承和实现继承。前者只能继承方法签名而后者可以继承实际的方法。
然后接口继承在 ECMAScript 中是不太可能存在的原因在于函数没有签名。
因此呢实现继承是 ECMAScript 唯一支持的继承方式且这主要通过原型链来实现。
2. 继承的方式
了解完定义我们来看一下继承有多少种方法。详情见下图 二、6大常见继承方式
1. 原型链继承
1构造函数、原型和实例的关系
对于构造函数、原型和实例三者之间有以下关系每个构造函数都有一个原型对象原型上有一个属性指回构造函数而实例又有一个内部指针指向原型。
2基本思想
原型链继承的基本思想是通过原型来继承多个引用类型的属性和方法其核心在于将父类的实例作为子类的原型。
3实现原型链继承
上面的内容可能看起来有点抽象我们先用一个例子来体验一下原型链继承。具体代码如下
// 父类函数
function SuperType() {// 父类定义属性this.property true;
}// 父类定义方法
SuperType.prototype.getSuperValue function() {return this.property;
}// 子类函数
function SubType() {this.subproperty false;
}/*** 关键点* 通过创建父类SuperType的实例* 并将该实例赋值给子类SubType.prototype*/
SubType.prototype new SuperType();
// 子类定义新方法
SubType.prototype.getSubValue function() {return this.subproperty;
}// 创建一个子类的实例
let instance new SubType();
// 子类调用父类的方法
console.log(instance.getSuperValue()); // true4图例阐述
依据以上代码我们来用一张图表示这段代码的继承关系。如下图所示 大家定位到图片的最右上角区域同时我们也将由右上到左下进行分析。
首先我们会创建构造函数 SuperType 的实例并将其作为子类的原型也就是 SuperType.prototype 。之后实例创建完了该实例上有一个内容指针指向父类的原型 SuperType.prototype 。完成之后来到了第三步。该原型上有一个属性将指回构造函数 SuperType 。第四步对于构造函数来说每个构造函数又有它自己的原型所以它又会指回 SuperType.prototype 。
依据上面这段描述大家再看下1和2的关系和基本思想是不是就清晰许多了呢。
同样地下面的⑤~⑧步也是和上面一样的步骤大家可以自行再理解对应这里不再细述。
5破坏原型链
还有一个要理解的重点是以对象字面量的方式去创建原型方法回破坏之前的原型链因为这相当于重写了原型链。如下例子所示
function SuperType() {this.property true;
}SuperType.prototype.getSuperValue function() {return this.property;
};function SubType() {this.subproperty false;
}// 继承SuperType
SubType.prototpe new SuperType();
// 通过对象字面量添加新方法这将导致上一行无效
SubType.prototype {getSubValue() {return this.subproperty;},someOtherMoethod(){return false;}
}let instance new SubType();
console.log(instance.getSuperValue());
// TypeError: instance.getSuperValue is not a function在上面这段代码中子类 SubType 的原型在被赋值为 SuperType 的实例后又被一个对象字面量覆盖了。而覆盖后的原型其实已经变成了一个 Object 的实例而不再是 SuperType 的实例因此在赋值为对象字面量之后原型链就被断掉了。这个时候 SubType 和 SuperType 也就不再有任何关系。
6优缺点
1优点
父类的方法可以复用。
2缺点
父类的所有引用类型数据将会被所有子类共享也就是说一旦有一个子类的引用类型数据更改了那么其他子类也会受到影响。子类型实例不能给父类型构造函数传参。原因在于一旦传参的话由于第一个问题的因素会导致当传相同的参数时会覆盖之前的。
2. 盗用构造函数继承
1基本思想
对于以上原型链继承存在的两个问题所以导致原型链基本不会被单独使用。因此为了解决原型包含引用值导致的问题我们引入一种新的继承方式为 “盗用构造函数” 。这种技术也被称为对象伪装或者经典继承。
其基本思路为 使用父类的构造函数来增强子类实例等同于赋值父类的实例给子类不使用原型。
2使用方式
函数是在特定上下文中执行代码的简单对象因此可以使用 apply() 和 call() 方法以新创建的对象为上下文来执行构造函数。
3实现原型链继承
我们用一个例子来实现原型链继承。具体代码如下
function SuperType(name) {this.name name;this.colors [red, blue, green];
}function SubType(name, age) {// 继承SuperType并传参这里是核心SuperType.call(this, monday);// 实例属性this.age age;
}let instance1 new SubType(monday, 18);
instance1.colors.push(gray);
console.log(instance1.name); // monday
console.log(instance1.age); // 18
console.log(instance1.colors); // [ red, blue, green, gray ]let instance2 new SubType();
console.log(instance2.colors); // [ red, blue, green ]大家可以看到在上面的这个例子中 通过使用 call() 方法借用了 SuperType 构造函数这样在创建 SubType 的实例时都会将 SuperType 中的属性复制一份出来。
同时相对于原型链来说盗用构造函数的一个优点就是可以在子类构造函数中向父类构造函数传递参数。大家可以看到上面代码我们在使用 SuperType.call() 时就可以将参数直接传递给父类 SuperType 。
4优缺点
1优点
子类的构造函数可以向父类的构造函数传递参数。
2缺点
只能继承父类的实例属性和方法不能继承父类的原型属性和方法。无法实现复用每新创建一个子类都会产生父类实例函数的副本非常影响性能。
3. 组合继承
1基本思想
由于原型链和盗用构造函数继承的方式都存在一定的缺陷所以它们俩基本上不能单独使用。为此呢我们引入了一种新的继承方式组合继承。组合继承也叫做伪经典继承它综合了原型链和盗用构造函数将两者的优点结合了起来。
其实现的基本思路为使用原型链来继承原型上的属性和方法然后呢通过盗用构造函数来继承实例上的属性。
这样通过在原型上定义方法实现了函数的复用又能保证每个实例都有它自己的属性。
2实现组合继承
下面我们用一个例子来实现组合继承。具体代码如下
function SuperType(name) {this.name name;this.colors [red, blue, green];
}SuperType.prototype.sayName function() {console.log(this.name);
}function SubType(name, age) {// 继承属性→借用构造函数继承实例上的属性SuperType.call(this, name);this.age age;
}// 继承方法→通过原型的方式继承原型的属性和方法
SubType.prototype new SuperType();
SubType.prototype.constructor SubType;
SubType.prototype.sayAge function(){console.log(this.age);
}let instance1 new SubType(Monday, 18);
instance1.colors.push(gray);
console.log(instance1.colors); // [ red, blue, green, gray ]
instance1.sayName(); // Monday
instance1.sayAge(); // 18let instance2 new SubType(Tuesday, 24);
console.log(instance2.colors); // [ red, blue, green ]
instance2.sayName(); // Tuesday
instance2.sayAge(); // 24大家可以看到在以上的代码中通过盗用构造函数继承了父类 SuperType 实例上的属性。同时通过原型的方式也成功继承了父类 SuperType 原型上的属性和方法。
3图例
我们来用一张图展示一下上述的结果。具体如下 4优缺点
1优点
避开原型链和盗用构造函数继承的缺陷实现对实例和原型的继承。组合继承是 javascript 常用的一种继承方式且保留了 instanceof 操作符和 isPrototypeOf() 方法识别合成对象的能力。
2缺点
父类中的实例属性和方法既存在于子类的实例中又存在于子类的原型中这将会占用更多的内存。所以在使用子类创建实例对象时其原型中会存在两份相同的属性和方法。
4. 原型式继承
1基本思想
原型式继承实现的基本思路是将某个对象直接赋值给构造函数的原型。如下代码所示
function object(obj) {function F(){}F.prototype obj;return new F();
}以上这段代码是 Douglas Crockford 在2006年写的一篇文章中所谈及。这篇文章介绍了一种不涉及严格意义上构造函数的继承方法且他的出发点是即使不自定义类型也可以通过原型来实现对象之间的信息共享。
在上述代码中 object() 对传入其中的对象执行了一次并将 F 的原型直接指向传入的对象。
2实现原型式继承
下面我们用一个例子来实现原型式继承。具体代码如下
function object(obj){function F(){}F.prototype obj;return new F();}let person {name: Monday,friends: [January, February, March]
};let otherPerson object(person);
otherPerson.name Friday;
otherPerson.friends.push(April);let anotherPerson object(person);
anotherPerson.name Sunday;
anotherPerson.friends.push(May);console.log(person.friends); // [ January, February, March, April, May ]大家可以看到最终所有的 friend 都拷贝到了 person 对象上。但是这种方法其实用的比较少有点类似于原型链继承的模式所以一般也不会单独使用。
3优缺点
1优点
适用于不需要单独创建构造函数但仍然需要在对象间共享信息的场合。
2缺点
当继承多个实例的引用类型数据时指向是相同的所以存在篡改的可能。无法传递参数。ES5 中已经存在 Object.create() 的方法能够代替上面的 object 方法。
5. 寄生式继承
1基本思想
寄生式继承的基本思想是在上面原型式继承的基础上以某种方式来增强对象并返回这个对象。
2实现寄生式继承
下面我们用一个例子来实现寄生式继承具体代码如下
// object函数
function object(obj){function F(){}F.prototype obj;return new F();
}// 函数的主要作用是为构造函数新增属性和方法以增强函数
function createAnother(original) {// 通过调用函数来创建一个新对象let clone object(original);// 以某种方式增强这个对象clone.sayHello function() {console.log(hello);}// 返回对象return clone;
}let person {name: Monday,friends: [January, February, March]
};let anotherPerson createAnother(person);
anotherPerson.sayHello(); // hello
大家可以看到通过创建一个新的构造函数 createAnother 来增强对象的内容。并在之后对这个对象进行返回供给我们使用。
3优缺点
1优点
适合只关注对象本身而不在乎数据类型和构造函数的场景。
2缺点
给对象添加函数会导致函数难以重用与上面第2中的盗用构造函数继承类似。当继承多个实例的引用类型数据时指向是相同的所以存在篡改的可能。无法传递参数。
6. 寄生式组合继承
1基本思想
寄生式组合继承的基本思想是结合盗用构造函数和寄生式模式来实现继承。
2实现寄生式组合继承
我们用一个例子来展示寄生式组合继承。具体代码如下
function inheritPrototype(subType, superType) {// 创建对象let prototype Object.create(superType.prototype);// 增强对象prototype.constructor subType;// 指定对象subType.prototype prototype;
}// 父类对实例属性和原型属性进行初始化
function SuperType(name) {this.name name;this.friends [January, February, March];
}
SuperType.prototype.sayName function() {console.log(this.name);
}// 借用构造函数对子类实例属性进行传参支持传参和避免篡改
function SubType(name, age) {SuperType.call(this, name);this.age age;
}// 运用寄生式继承的特点将父类的原型指向子类
inheritPrototype(SubType, SuperType);// 新增子类的原型属性
SubType.prototype.sayAge function() {console.log(this.age);
}let instance1 new SubType(Ida, 18);
let instance2 new SubType(Hunter, 23);instance1.friends.push(April);
console.log(instance1.friends); // [ January, February, March, April ]
instance1.friends.push(May);
console.log(instance2.friends); // [ January, February, March ]大家可以看到对于寄生式组合继承来说它借用了构造函数来对子类的实例属性进行传参同时通过运用寄生式继承的特点使用 inheritPrototype 构造函数来将父类的原型指向子类这样子类就继承了父类的原型 。
同时子类还可以在自己的原型上新增自己想要的原型属性达到继承自己的原型方法的效果。
3图例
我们再来用一张图展示一下上述的结果。具体如下 4优缺点
1优点
寄生式组合继承可以算是引用类型继承的最佳方式。几乎避免了上述所有继承方式中所存在的缺陷也是执行效率最高且应用面最广的。
2缺点
实现的过程相对较繁琐要捋好父子之间的关系避免跳入原型的漩涡中。这其实也不算啥缺点因为它值得
️三、Class的继承
1. 基本概念
在上述中我们讲到了各种类型的继承方式但这似乎都逃不开原型链的闭圈中。到了 2015 年ES6 的出现解决了这个问题。 ES6 的 class 可以通过 extends 关键字来实现继承这间接地比 ES5 通过修改原型链实现继承的方式更加清晰和方便了许多。
我们用一个例子来先看一下 class 是如何实现继承的。具体代码如下
class Point{constructor(x, y) {this.x x;this.y y;}
}class ColorPoint extends Point {constructor(x, y, color) {// 调用父类的 constructor(x, y)// 只有super方法才能返回父类的实例super(x, y); this.color color;}toString() {return this.color super.toString() {// 调用父类的toString()}}
}大家可以看到通过 extends 关键字实现了对父类的属性和方法进行继承这似乎看起来就比上面的寄生式组合继承要方便的多了。
接下来我们继续看其他用法。
2. Object.getPrototypeOf()
在 ES6 中 Object.getPrototypeof 方法可以用来从子类上获取父类。比如
Object.getPrototypeof(ColorPoint) Point
// true所以可以使用这一方法来判断一个类是否继承了另外一个类。
3. super关键字
上面我们有看到 super 这个关键字它主要是用来返回父类的实例。那在实际的应用中 super 可以当作函数使用也可以当作对象使用。接下来我们来谈谈这两种情况。
1作为函数使用
第一种情况当 super 作为函数调用时代表父类的构造函数。 ES6 要求子类的构造函数必须执行一次 super 函数。如下例子
class A {}class B extends A {constructor() {super();}
}上述代码中的 super() 代表调用父类 A 的构造函数但是返回的是子类 B 的实例即 super 内部的 this 指的是 B 。因此这里的 super() 相当于 A.prototype.constructor.call(this) 。 值得注意的是 作为函数时 super() 只能用在子类的构造函数之中用在其他地方会报错。比如
class A {}class B extends A {m() {super(); // 报错}
}大家可以看到像上面这种情况 super() 用在 B 类的 m 方法之中就会造成句法错误。
2作为对象使用
第二种情况当 super 作为对象时在普通方法中指向父类的原型对象在静态方法中指向父类。我们来一一分析这两种情况。
1在普通方法中
先来看一段代码具体如下
class A {p() {return 2;}
}class B extends A {constructor() {super();console.log(super.p()); // 2}
}let b new B();在上面的代码中子类 B 中的 super.p() 就是将 super 当作一个对象来使用。依据上面所描述的 super 在普通方法中指向父类的原型对象因此这里的 super.p() 就等于 A.prototype.p() 。所以最终输出为 2 。 继续由于 super 指向父类的原型对象所以定义在父类实例上的方法或属性时无法通过 super 进行调用的。比如
class A {constructor() {this.p 2;}
}class B extends A {get m() {return super.p; // 定义在普通方法上所以super指向父类的原型对象}
}let b new B();
b.m // undefined在上述代码中 super 在普通方法中调用所以也就意味着 super 指向父类的原型对象。而 b.m 却想要直接指向父类 A 实例的属性上自然就是搜索不到它的。 我们可以对代码进行修改将属性定义在父类的原型对象上让 super 可以找到具体的属性。具体如下
class A {}
A.prototype.x 2;class B extends A {constructor() {super();console.log(super.x); // 2}
}let b new B();大家可以看到现在属性 x 是定义在 A.prototype 上面的所以 super.x 就可以顺利的取到它的值了。
2在静态方法中
上面我们谈到了 super 作为对象时在普通方法中的调用现在我们来看一下在静态方法中的的调用是什么样的。
首先我们谈到的一个点就是 super 在静态方法中被调用时指向的是其父类。我们来看一段代码
class Parent {static myMethod(msg) {console.log(static, msg);}myMethod(msg) {console.log(instance, msg);}
}class Child extends Parent {static myMethod(msg) {super.myMethod(msg);}myMethods(msg) {super.myMethods(msg);}
}Child.myMethod(1); // static 1
let child new Child();
child.myMethod(2); // instance 2大家可以看到当直接使用 Child.myMethod(1) 来进行调用时表明直接调用静态方法。而当 super 在静态方法中被调用时指向的就是其父类中的静态方法所以打印 static 1 。
继续下面的 child 通过 new 的方式实例化了一个对象之后对实例进行调用。所以此时调用的是普通方法所以最终打印出 instance 1 。 在使用 super 的时候必须要注意的一个点是必须显式地指定是作为函数还是作为对象使用否则就会出现报错的情况。比如
class A {}
class B extends A {constructor() {super();console.log(super); // 报错}
}大家可以看到如果像上面这样引用的话根本无法看出是作为函数还是作为对象来引用此时 js 引擎解析代码的时候就会报错。
那么我们就得通过清晰的表明 super 的数据类型来判断我们的结果。比如
class A {}
class B extends A {constructor() {super();// object.valueOf() 表示返回对象的原始值console.log(super.valueOf() instanceof B); // true}
}上述代码中我们通过 object.valueOf() 来指明 super 就是一个对象之后 js 引擎识别到了最后也就成功打印出来了。
4. 类的prototype属性和 __ proto __ 属性
1class的继承链
在大多数浏览器的 ES5 实现之中每一个对象都有 __proto__ 属性指向对应的构造函数的 prototype 属性。
而 class 作为构造函数的语法糖同时有 prototype 属性和 __proto__ 属性因此同时存在两条继承链。分别是
子类的 __proto__ 属性表示构造函数的继承总是指向父类。子类 prototype 属性的 __proto__ 属性表示方法的继承总是指向父类的 prototype 属性。
我们用一段代码来演示一下
class A {}class B extends A {}B.__proto__ A // true
B.prototype.__proto__ A.prototype // true在上面的代码中子类 B 的 __proto__ 属性总是指向父类 A 且子类 B 的 prototype 属性的 __proto__ 属性指向父类 A 的 prototype 属性。
这两条原型链可以理解为
当作为一个对象时子类 B 的原型 __proto__ 属性 是父类 A 当作为一个构造函数时子类 B 的原型 prototype 属性 是父类的实例。
2特殊情况继承
下面讨论三种特殊的继承情况。具体如下
第一种情况 子类继承 Object 类。先来看一段代码
class A extends Object {}A.__proto__ Object // true
A.prototype.__proto__ Object.prototype // true在这种情况下 A 其实就是构造函数 Object 的复制 A 的实例就是 Object 的实例。
第二种情况 不存在任何继承。先来看一段代码
class A {}A.__proto__ Function.prototype // true
B.prototype.__proto__ Object.prototype // true在以上这种情况下 A 作为一个基类它不存在任何继承关系。且 class 是构造函数的语法糖所以这个时候可以说 A 就是一个普通函数。所以 A 直接继承 Function.prototype 。
值得注意的是 A 调用后返回的是一个空对象即 Object 实例所以 A.prototype.__proto__ 指向构造函数 Object 的 prototype 属性。
第三种情况 子类继承 null 。先来看一段代码
class A extends null {}A.__proto__ Function.prototype // true
A.prototype.__proto__ undefined // true以上这种情况与第二种情况非常相似。 A 也是一个普通函数所以直接继承 Function.prototype 。
值得注意的是 A 调用后返回的对象不继承任何方法所以它的 __proto__ 指向 Function.prototype 即实际上是执行了下面这段代码
class C extends null {constructor() {return Object.create(null);}
}3实例的 __ proto __ 属性
对于子类来说其实例的 __proto__ 属性的 __proto__ 属性总是指向父类实例的 __proto__ 属性。也就是说子类的原型的原型是父类的原型。
我们来看一段代码具体如下
let p1 new Point(2, 3);
let p2 new ColorPoint(2, 3, green);p2.__proto__ p1.__proto__ // false
p2.__proto__.__proto__ p1.__proto__ // true在上面的代码中 子类 ColorPoint 继承了 Point 所以导致了前者原型的原型是后者的原型。
因此我们可以通过子类实例的 __proto__.__proto__ 属性来修改父类实例的行为。具体如下
p2.__proto__.__proto__.printName function() {console.log(Monday);
};p1.printName(); // Monday大家可以看到通过在 ColorPoint 的实例 p2 上向 Point 类中添加方法从而影响到了 Point 实例 p1 。
四、结束语
上文我们讲到了6大继承方式以及 class 的继承在现如今的开发场景中基本上也是寄生式组合继承和 class 的继承用的比较多。相信通过上文的了解大家对 javascript 的继承又有了一个新的认识。
到这里关于 js 的继承讲解就结束啦希望对大家有帮助
如文章有误或有不理解的地方欢迎小伙伴们评论区留言~
彩蛋 One More Thing
参考资料
书籍ES6书籍《ES6标准入门》
书籍红宝书《JavaScript高级程序设计》第四版
同梦奇缘《javascript高级程序设计》笔记继承
番外篇 关注公众号星期一研究室第一时间关注优质文章更多精选专栏待你解锁~如果这篇文章对你有用记得留个脚印jio再走哦~以上就是本文的全部内容我们下期见