虽然ES6的Class继承确实很方便,但是ES5的继承还是要好好了解一下:
参考视频:详解JS继承(超级详细且附实例)
预备知识
构造函数的属性
function A(name){
this.name = name; //实例基本属性(该属性,强调私有,不共享)
this.arr = [1]; //实例引用属性(该属性,强调私用,不共享)
this.say = function(){ //实例引用属性(该属性,强调复用,需要共享)
console.log('hello');
}
}
注意:数组和方法都属于’实例引用属性’,但是数组强调私有不共享,方法需要复用共享。在构造函数中,很少有数组形式的引用属性,大部分情况都是:基本属性+方法。
在构造函数中,为了属性(实例基本属性)的私有性、方法(实例引用属性)的复用共享,提倡:将属性封装在构造函数中,将方法定义在原型对象上。
修正constructor指向的意义:任何一个prototype对象都有一个constructor属性,指向它的构造函数(它本身),更重要的是,每一个实例也有一个constructor属性,默认调用prototype对象的constructor属性。
在new之后,constructor会指向父类构造函数,如果我们要生成子类构造函数的实例,这些实例的constructor属性会指向父类构造函数,然而它们是靠子类构造函数生成的,constructor属性应该指向子类构造函数。因此,不修改constructor指向的话,会导致继承链的紊乱。
(以上来自阮一峰博客,我目前不清楚继承链紊乱会引起什么后果,最起码在我看来,即便不修改constructor指向,好像也没什么影响?)
文档的原作者说:要修复constructor指向,原因是:不能判断子类实例的直接构造函数,到底是子类构造函数还是父类构造函数
JS继承方式
原型链继承
- 核心:将父类实例作为子类原型
- 优点:方法复用
方法定义在父类的原型上,可以复用父类构造函数的方法,比如say方法。 - 缺点:
- 创建子类实例时,无法传父类参数
- 子类实例共享
- 继承单一,无法实现多继承
function Parent(name){
this.name = name || '父亲'; 实例基本属性(该属性,强调私有,不共享)
this.arr = [1]; //实例引用属性(该属性,强调私用,不共享)
}
Parent.prototype.say = function(){ //将需要复用、共享的方法定义在父类原型上
console.log('hello');
}
function Child(like){
this.like = like;
}
Child.prototype = new Parent(); //核心,但此时Child.prototype.constructor == Parent;
Child.prototype.constructor = Child; //修正constructor指向
let boy1 = new Child();
let boy2 = new Child();
//优点:共享父类构造函数的say方法
console.log(boy1.say(),boy2.say(),boy1.say === boy2.say); //hello,hello,true
//缺点1:不能传入父类的参数(比如name),只能传子类有的参数like
console.log(boy1.name,boy2.name,boy1.name === boy); //父亲,父亲,true
//缺点2:子类实例共享了父类构造函数的引用属性,比如arr属性
boy1.arr.push(2);
console.log(boy2.arr);//[1,2];
//修改了boy1的arr属性,boy2的arr属性也会变化,
//因为两个实例的原型上(Child.prototype)有了父类构造函数的实例属性arr,所以只要修改了boy1.arr,boy2.arr也变化
借用构造函数
- 核心:借用父类构造函数来增强子类实例,等于是复制父类的实例属性给子类
- 优点:实例之间独立
- 创建子类实例,可以向父类构造函数传参
- 子类实例不共享父类构造函数的引用属性,如arr
- 可实现多继承(通过多个call或apply继承多个父类)
- 缺点:
- 父类方法不能复用
由于方法在父构造函数中定义,导致方法不能复用(每次创建子类实例都要创建一遍方法) - 子类实例继承不了父类原型上的属性,因为没有用到原型
- 父类方法不能复用
function Parent(name){
this.name = name; //实例基本属性(该属性,强调私有,不共享)
this.arr = [1]; (该属性,强调私有)
this.say = function(){ //实例引用属性(该属性,强调复用,需要共享)
console.log('hello);
}
}
function Child(name,like){
Parent.call(this,name); //核心,拷贝了父类的实例属性和方法
this.like = like;
}
let boy1 = new Child('小刚','apple');
let boy2 = new Child('小明','orange');
//优点1:可向父类构造函数传参
console.log(boy1.name,boy2.name); //小刚,小明
//优点2:不共享父类构造函数的引用属性
boy1.arr.push(2);
console.log(boy1.arr,boy2.arr); //[1,2],[1]
//缺点1:方法不能复用
console.log(boy1.say === boy2.say); //false (说明boy1和boy2的say方法独立,不是共享的)
//缺点2:不能继承父类原型上的方法
Parent.prototype.walk = function(){
console.log('我会走路');
}
boy1.walk; //undefined(说明实例不能获得父类原型上的方法)
组合继承
- 核心:通过调用父类构造函数,继承父类属性并保留传参的优点;然后通过将父类实例作为子类原型,实现函数复用。
- 优点:
- 保留方法1的优点:父类的方法定义在原型对象上,可以实现方法复用
- 保留方法2的优点:创建子类实例,可以向父类构造函数传参;并且不共享父类的引用属性,如arr
- 缺点:由于调用了2次父类的构造方法,会存在一份多余的父类实例属性
原因:第一次Parent.call(this)从父类拷贝一份父类实例属性,作为子类的实例属性,第二次Child.prototype = new Parent()创建父类实例作为子类原型,(Child.prototype中的父类属性和方法会被第一次拷贝来的实例属性屏蔽掉,所以多余←这句话没理解)
我的理解是,第二次new Parent的时候也执行了Parent构造函数,但是因为没有传参,导致子类实例对象的_ proto 的 proto _中,一部分属性为undefined
![](https://i-blog.csdnimg.cn/blog_migrate/214e41b033faca4dc61d68b3fb9d0d81.jpeg)
注意name:undefined
function Parent(name){
this.name = name; //实例基本属性(该属性,强调私有,不共享)
this.arr = [1]; //实例引用属性(该属性,强调私用,不共享)
}
Parent.prototype.say = function(){ //将需要复用、共享的方法定义在父类原型上
console.log('hello');
}
function Child(name,like){
Parent.call(this,name); //核心,第二次
this.like = like;
}
Child.prototype = new Parent(); //核心,第一次
Child.prototype.constructor = Child; //修正constructor指向
let boy1 = new Child('小刚','apple');
let boy2 = new Child('小明','orange');
//优点1:可以复用父类原型上的方法
console.log(boy1.say === boy2.say); true
//优点2:可以向父类构造函数传参数,且不共享父类引用属性
console.log(boy1.name,boy1.like); //小刚,apple
boy1.arr.push(2);
console.log(boy1.arr,boy2.arr); //[1,2],[1]
//缺点:由于调用了2次父类的构造方法,会存在一份多余的父类实例属性
组合继承优化
-
核心:通过这种方式,砍掉父类的实例属性,这样在调用父类的构造函数的时候,就不会初始化两次实例,避免组合继承的缺点
-
优点:
- 只调用一次父类构造函数
- 保留组合继承的优点
-
缺点:修正构造函数的指向之后,父类实例的构造函数指向,同时也发生变化(这是我们不希望的)
具体原因:因为是通过原型来实现继承的,Child.prototype上面没有constructor属性,就会往上找,这样就找到了Parent.prototype上面的constructor属性;当修改了子类实例的constructor属性,所有的constructor的指向都会发生变化。(我觉得这个原因说得不对,constructor属性指向自身,Child上有constructor属性,真正原因可能是因为constructor是引用数据类型,所以修改一方才会影响另一方)
![](https://i-blog.csdnimg.cn/blog_migrate/b26320ec56a55b62e42c6683a9e924d8.jpeg)
之前的name:undefined 消失了,改进成功
function Parent(name){
this.name = name; //实例基本属性(该属性,强调私有,不共享)
this.arr = [1]; //实例引用属性(该属性,强调私用,不共享)
}
Parent.prototype.say = function(){ //将需要复用、共享的方法定义在父类原型上
console.log('hello');
}
function Child(name,like){
Parent.call(this,name); //核心
this.like = like;
}
Child.prototype = Parent.prototype //核心,子类原型和父类原型,实际上是同一个
Child.prototype.constructor = Child;//修复代码
let boy1 = new Child('小刚','apple');
let boy2 = new Child('小明','orange');
let p1 = new Parent('小爸爸');
//优点不演示
//缺点1:当修复子类构造函数的指向后,父类实例的构造函数指向也会跟着变了
console.log(boy1.constructor);//没修复之前:Parent
console.log(boy1.constructor,p1.constructor); //修复之后:Child,Child 这就是问题所在
寄生组合继承
完美的继承方案
function Parent(name){
this.name = name; //实例基本属性(该属性,强调私有,不共享)
this.arr = [1]; //实例引用属性(该属性,强调私用,不共享)
}
Parent.prototype.say = function(){ //将需要复用、共享的方法定义在父类原型上
console.log('hello');
}
function Child(name,like){
Parent.call(this,name); //核心
this.like = like;
}
//核心 通过创建中间对象,子类原型和父类原型就会隔离开,不再是同一个,有效避免了方式4的缺点
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;//修复代码
let boy1 = new Child('小刚','apple');
let boy2 = new Child('小明','orange');
let p1 = new Parent('小爸爸');
console.log(boy1.constructor,p1.constructor); //修复之后:Child,Parent
其中,Object.create()函数等价为:
function object(o) {
function F(){}
F.prototype = o;
return new F();
}
于是中间那段核心代码可改为:
function object(o) {
function F(){}
F.prototype = o;
return new F();
}
Child.prototype = object(Parent);
Child.prototype.constructor = Child;