我们知道面向对象的编程语言中都会有继承,而且在各种语言中充当着至关重要的角色,但是继承是什么?又有多少种继承的方式?常见的又有哪些呢?对于js继承有更深层次的理解,能够在开发中应对自如。
所谓继承,继承是面向对象的,使用这种方式能够更好的对代码的复用,能够缩短开发周期、提升开发效率。
那么我们带着两个问题阅读文章,在文章解决这些疑惑:
JS继承到底有多少种继承方式?
ES6中的extends关键字是使用哪种继承方式实现的?
我们知道一个人继承祖业,可以将父辈所有的物质基础继承过来,但是自己作为主体又有自己的其它能力和特性。同样的汽车作为一个大类,生产轿车、跑车、面包车等,都具有汽车四个轮子加发动机的特性,但各自具有自己独特的特性,比如五菱宏光可以在秋名山飙车成为车神。
因此,继承可以使得子类具有父类的各种方法和属性。
常见的继承方式有:
原型链继承
构造函数继承
组合继承
原型链继承
寄生式继承
寄生组合式继承
原型链继承是比较常见的继承方式之一,其中涉及的构造函数、原型和实例:
每个构造函数都有一个原型对象
原型对象又包含一个指向构造函数的指针
实例则包含一个原型对象的指针
复制
function Person(){ this.name = "person"; this.abilities = ["吃饭","睡觉","打豆豆"]; } function Student(){ this.study = ["语文","数学","英语"]; } Student.prototype = new Person(); console.log(new Student());
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
我们可以看到Student类已经继承了Person类的所有特性:
但是,我们注意到:当使用同一个对象创建实例的时候,内存空间是共享的,当一个发生变化的时候,另外一个也会随之变化。
复制
function Person(){ this.name = "person"; this.abilities = ["吃饭","睡觉","打豆豆"]; } function Student(){ this.study = ["语文","数学","英语"]; } Student.prototype = new Person(); const stu1 = new Student(); const stu2 = new Student(); stu1.abilities.push("走路"); console.log(stu1.abilities,stu2.abilities);
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
我们看到stu1和stu2都是由Student对象进行创建的两个实例,当改变stu1的值,stu2的值也随之改变了。图片
构造函数继承可以很好的解决原型链继承的共享内存的弊端,但是父类原型对象中存在父类之前自己定义的方法,那么子类将无法继承这些方法,此时去使用父类方法就会报错。
构造函数继承只能继承父类实例、属性和方法,不能继承原型属性和方法。
复制
function Person(){ this.name = "person"; } Person.prototype.getName = function(){ return this.name; } function Student(){ Person.call(this); this.study = ["语文","数学","英语"]; } const stu = new Student(); console.log(stu); console.log(stu.getName())
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
我们可以看到,打印的stu实例包含了Student对象以及父类Person所有的属性,但是要使用父类原型的方法就会报错。getName是父类Person的引用方法,不会共享内存。
复制
function Person(){ this.name = "person"; this.abilities = ["吃饭","睡觉","打豆豆"]; } //第一次调用Person() Person.prototype.getName = function(){ return this.name; } function Student(){ //第二次调用Person() Person.call(this); this.study = ["语文","数学","英语"]; } Student.prototype = new Person(); //手动挂载构造器,指向自己的构造函数 Student.prototype.constructor = Student; const stu1 = new Student(); const stu2 = new Student(); stu1.abilities.push("走路"); console.log(stu1.abilities,stu2.abilities);//不会互相影响 console.log(stu1.getName());//正常输出"person" console.log(stu2.getName());//正常输出"person"
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
运行得到:我们看到在stu1实例的abilities数组中追加元素,并不会影响到stu2实例的值。图片我们发现使用组合式继承时,可以有效解决原型链继承和构造函数继承的缺点,但是,调用两次Person(),这就造成了性能开销,那么我们还有没有优化空间呢?
我们可以利用es5中的Object.create()方法进行原型式继承,从而实现对组合式继承的优化。Object.create()接收两个参数:
用作新对象原型的对象
为新对象定义额外属性的对象(可选参数)
复制
const person = { name:"person", abilities:["吃饭","睡觉","打豆豆"], getName(){ return this.name; } } const person1 = Object.create(person); person1.name = "human"; person1.abilities.push("走路"); const person2 = Object.create(person); person2.abilities.push("跑步"); console.log(person1.name); console.log(person1.name === person1.getName()); console.log(person2.name); console.log(person1.abilities); console.log(person2.abilities);
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
我们可以看到使用Object.create()可以实现普通对象继承,不仅可以继承属性,还能继承方法。但是也有缺点:包含引用类型的属性值始终都会共享相应的值,这点跟原型链继承一样。
修改person1.name的值,person2.name的值并未发生改变,并不是因为person1和person2有独立的 name 值,而是因为person1.name = 'human',给person1添加了 name 值,并非修改了原型上的 name 值。
寄生式继承:首先使用原型式继承可以获得一份目标对象的浅拷贝,然后利用这个浅拷贝的能力再进行增强,添加一些方法。
寄生式继承相比于原型式继承,还是在父类基础上添加了更多方法。
复制
function clone(original){ const clone = Object.create(original); clone.getAbilities = function(){ return this.abilities; } return clone; } const person = { name:"person", abilities:["吃饭","睡觉","打豆豆"], getName(){ return this.name; } } const person1 = clone(person); console.log(person1.getName()); console.log(person1.getAbilities());
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
运行得到:
前面分析了五种常见的继承方法,现在综合所有方式的优缺点,可以进行优化改造得到寄生组合式的继承方式,这也是所有继承方式中相对最优的。
复制
function clone(parent,child){ //这里使用Object.create()可以减少组合继承中多进行一次构造函数的过程 child.prototype = Object.create(parent.prototype); child.prototype.constructor = child; } function Parent(){ this.name = "parent"; this.abilities = ["吃饭","睡觉","打豆豆"]; } Parent.prototype.getName = function(){ return this.name; } function Child(){ Parent.call(this); this.study = ["语文","数学","英语"]; } clone(Parent,Child); Child.prototype.getStudy =function(){ return this.study; } const child = new Child(); console.log(child); console.log(child.getName()); console.log(child.getStudy());
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
运行得到:
extends关键字主要用于类声明或者类表达式中,以创建一个类,该类是另一个类的子类。其中constructor表示构造函数,一个类中只能有一个构造函数,有多个会报出SyntaxError错误,如果没有显式指定构造方法,则会添加默认的 constructor方法,使用例子如下。
复制
class Rectangle { // constructor constructor(height, width) { this.height = height; this.width = width; } // Getter get area() { return this.calcArea() } // Method calcArea() { return this.height * this.width; } } const rectangle = new Rectangle(10, 20); console.log(rectangle.area); // 输出 200 ----------------------------华丽的分割线------------------------------------- // 继承 class Square extends Rectangle { constructor(length) { super(length, length); // 如果子类中存在构造函数,则需要在使用“this”之前首先调用 super()。 this.name = 'Square'; } get area() { return this.height * this.width; } } const square = new Square(10); console.log(square.area); // 输出 100
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
extends继承的核心代码如下,其实现和上述的寄生组合式继承方式一样。
复制
function _inherits(subType, superType) { // 创建对象,创建父类原型的一个副本 // 增强对象,弥补因重写原型而失去的默认的constructor 属性 // 指定对象,将新创建的对象赋值给子类的原型 subType.prototype = Object.create(superType && superType.prototype, { constructor: { value: subType, enumerable: false, writable: true, configurable: true } }); if (superType) { Object.setPrototypeOf ? Object.setPrototypeOf(subType, superType) : subType.__proto__ = superType; } }
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
《JavaScript常用八种继承方案》
《深入JavaScript继承原理》
继承的方法很多,每个实现的方法都比较零散,需要对常见的继承方法进行一个深入系统的分析总结。图片