1、继承
应注意区分继承和实例化,实例化是生成一个对象,这个对象具有构造函数的属性和方法;继承指的应该是利用父类生成一个新的子类构造函数,通过这个子类构造函数实例化的对象,具有子类的属性和方法,同时也具有父类的属性和方法。
2、原型链继承
2.1、实现方法
实现原型链继承的方法是通过重写子类的原型对象(比如 Student.prototype )的值为父类(比如Person) 的一个实例,由此可以实现继承(Student 继承了 Person ) 。
Son.prototype = new Parent();
代码示例:
//父类:人 function Person() { this.head = "脑袋瓜子"; } //子类:学生,继承了“人”这个类 function Student(studentID) { this.studentID = studentID; } Student.prototype = new Person(); var stu1 = new Student(1001); console.log(stu1.head); //脑袋瓜子 stu1.head = "聪明的脑袋瓜子"; console.log(stu1.head); //聪明的脑袋瓜子 var stu2 = new Student(1002); console.log(stu2.head); //脑袋瓜子
上面代码中,student 继承了 person,所以 stu1 能访问到父类 Person 上定义的 head 属性。
2.2、原型链继承的弊端(误修改原型对象上的引用类型值)
原型链继承的缺点有:
(1)实例可能会误修改原型对象上的引用类型的值,导致其他实例也受到影响
(2)在实现继承时,子类无法向父类传参数
在上面的代码中,所有的 Student 实例都共享着原型对象上的属性。那为什么修改了 stu1 的 head 值以后,重新实例化的对象 stu2 的 head 值仍旧不变呢?
这是因为,当实例对象中存在和原型对象上同名的属性时,会自动屏蔽原型对象上的同名属性。stu1.head = "聪明的脑袋瓜子" 实际上只是给 stu1 添加了一个本地属性 head 并设置了相关值。所以当我们打印 stu1.head 时,访问的是该实例的本地属性,而不是其原型对象上的 head 属性(它因和本地属性名同名已经被屏蔽了)。
原型对象上基本类型的值,都不会被实例所重写/覆盖。在实例上设置与原型对象上同名属性的值,只会在实例上创建一个同名的本地属性。但是,原型对象上引用类型的值可以通过实例进行修改,致使所有实例共享着的该引用类型的值也会随之改变,这正是原型链继承的最大缺点。
//父类:人 function Person () { this.head = '脑袋瓜子'; this.emotion = ['喜']; this.say = function () { console.log('hi'); } } //子类:学生,继承了“人”这个类 function Student(studentID) { this.studentID = studentID; } Student.prototype = new Person(); var stu1 = new Student(1001); console.log(stu1.emotion); //['喜'] stu1.emotion.push('怒'); console.log(stu1.emotion); //["喜", "怒"] var stu2 = new Student(1002); console.log(stu2.emotion); //["喜", "怒"] console.log(stu1.say === stu2.say); //true 证明子类的实例对象共享引用类型的值
我们在 Person 类中添加了一个 emotion 属性,它是一个引用类型的值。可以看到,此时如果一个实例不小心修改了原型对象上引用类型的值,会导致其它实例也跟着受影响。从上面的代码可知,在实现继承时,子类并不能向父类传递参数。
参考:https://www.cnblogs.com/sarahwang/p/6879161.html
3、构造函数实现继承
借用构造函数实现的继承可以避免原型链继承会导致误修改原型对象上引用类型值的缺点。
3.1、实现方法
构造函数实现继承的方法是在子类的构造函数中,通过 apply ( ) 或 call ( )的形式来调用父类的构造函数,以实现继承。
function Son(){ Parent.call(this); }
代码示例:
//父类:人 function Person (headMsg) { this.head = headMsg; this.emotion = ['喜', '怒']; this.say = function () { console.log('hi'); } } //子类:学生,继承了“人”这个类 function Student(studentID) { this.studentID = studentID; Person.call(this, '脑袋瓜子'); //构造函数在实现继承时可以传递参数 } var stu1 = new Student(1001); console.log(stu1.emotion); //['喜', '怒'] stu1.emotion.push('哀'); console.log(stu1.emotion); //["喜", "怒", "哀"] var stu2 = new Student(1002); console.log(stu2.emotion); //["喜", "怒"] console.log(stu1.say === stu2.say); //false 证明与原型链继承不同,引用类型的值并不共享
构造函数继承相对于原型链继承来说只是去掉了之前通过 prototype 继承的方法,而采用了 Person.call (this) 的形式实现继承,通过 call 来指定父类构造函数的作用域。
this 指向解析:
可以简单理解为:谁调用它,它就指向谁。在 stu1 = new Student ( ) 构造函数时,是 stu1 调用 Student 方法,所以其内部 this 的值指向的是 stu1,所以 Person.call ( this ) 就相当于Person.call ( stu1 ),就相当于 stu1.Person( )。最后,stu1 去调用 Person 方法时,Person 内部的 this 指向就指向了 stu1。那么Person 内部this 上的所有属性和方法,都被拷贝到了 stu1 上。stu2 也是同理,所以其实是,每个实例都具有自己的 emotion 属性副本,它们互不影响。
所以,通过构造函数来实现继承,每个示例都会具有属性及方法的副本,互相不影响,由此也避免了原型链继承的弊端。
参考:https://www.cnblogs.com/sarahwang/p/6879161.html
3.2、构造函数继承的弊端(占用内存)
这种形式的继承,每个子类实例都会拷贝一份父类构造函数中的方法,作为实例自己的方法,这样做会有以下的缺点:
(1)每个实例都拷贝一份,占用内存大,尤其是方法过多的时候。(函数复用又无从谈起了,本来我们用 prototype 就是解决复用问题的)
(2)方法都作为了实例自己的方法,当需求改变,要改动其中的一个方法时,之前所有的实例,他们的该方法都不能及时作出更新。只有后面的实例才能访问到新方法。
所以说,无论是单独使用原型链继承还是借用构造函数继承都有很大的缺点,最好的办法是,将两者结合一起使用,这就是下面介绍的组合式继承。
4、组合式继承
4.1、实现方法
组合式继承实现方式是将需要共享的方法写在父类的原型对象上,而需要每个实例都拷贝一份的属性则写在父类的构造函数上,由此可以需要共享的方法能实现实例间共享,需要自己维护的属性能实现每个实例都有具有自己的副本而不会导致误修改,避免了原型链继承和构造函数继承的弊端。
//父类:人 function Person () { this.head = '脑袋瓜子'; this.emotion = ['喜']; //人都有喜怒哀乐 } //将 Person 类中需共享的方法放到 prototype 中,实现复用 Person.prototype.say = function () { console.log('hi'); } //子类:学生,继承了“人”这个类 function Student(studentID) { this.studentID = studentID; Person.call(this); //构造函数继承的方法 } Student.prototype = new Person(); //原型链继承的方法 此时 Student.prototype 中的 constructor 被重写了,会导致 stu1.constructor === Person Student.prototype.constructor = Student; //将 Student 原型对象的 constructor 指针重新指向 Student 本身 var stu1 = new Student(1001); console.log(stu1.emotion); //['喜'] stu1.emotion.push('怒'); console.log(stu1.emotion); //["喜", "怒"] var stu2 = new Student(1002); console.log(stu2.emotion); //["喜"] 需要实例各自维护一份的属性不会被误修改 stu1.say(); //hi console.log(stu1.say === stu2.say) //true 证明函数实现了共享 console.log(stu1.constructor); //Student 实例的构造函数仍然是子类构造函数 Student,而不是父类 Person
将 Person 类中需要复用的方法提取到 Person.prototype 中,然后设置 Student 的原型对象为 Person 类的一个实例,这样 stu1 就能访问到 Person 原型对象上的属性和方法了。其次,为保证 stu1 和 stu2 拥有各自的父类属性副本,我们在 Student 构造函数中,还是使用了 Person.call ( this ) 方法。如此,结合原型链继承和借用构造函数继承,就完美地解决了之前这二者各自表现出来的缺点。
参考:https://www.cnblogs.com/sarahwang/p/9098044.html
5、类实现继承(class、extends)
在 ES6 中,可以通过 class 和 extends 关键字来实现继承。ES6 中类实现继承可以看做是组合式继承的语法糖(简单理解),但两者的继承机制还是不太一样的。
class Animal { constructor(age) { this.age = age; } say() { console.log("hi"); } } // extends 实现继承 class Dog extends Animal { constructor(age) { super(age); //ES6 要求,子类的构造函数必须执行一次 super() 函数。 } } // extends 实现继承 class Cat extends Animal { constructor(age) { super(age); } say() { super.say(); console.log("miao miao!!"); } } var cat = new Cat(11); var dog = new Dog(22); console.log(cat.age, dog.age); // 输出11 22 继承了父类的属性 cat.say(); // 输出 hi dog.say(); // 输出 miao miao!!, hi