继承
JS继承常用的八种方案
原型链继承
原型链继承,就是修改子构造函数的原型,将其指向父构造函数的实例,这个方法就是通过原型链继承父原型的属性和方法
// 原型链继承
function Parent() {}
Parent.prototype.property = true;
Parent.prototype.color = [];
Parent.prototype.getParentValue = function() {
return this.property;
};
function Child() {}
Child.prototype = new Parent();
const myChild = new Child();
console.log(myChild.color); // []
myChild.color.push(1);
console.log(myChild.color); // [1]
console.log(Parent.prototype.color); // [1]
原型链继承的缺点就是原型上的引用类型的数据,在所有实例下的修改,会造成全部的修改,也就是各个实例对于引用类型的属性,没有独立的继承
借用构造函数继承
这个方法就是在子类的构造函数中,调用父类的函数,就等同于将父类的属性和方法,复制给子类
function Parent(value) {
this.name = value;
this.color = [];
}
function Child(value) {
Parent.call(this, value);
}
缺点
- 只能实现对于父构造函数上的属性和方法的继承,无法实现父构造函数原型上的属性和方法的继承
- 每个实例都有父类函数的实例,浪费性能
组合继承
组合继承就是融合了原型链继承和借用构造函数继承的一个方法,利用原型链继承原型上的属性/方法,利用借用构造函数继承父构造函数上的属性/方法
function Parent(value) {
this.name = value;
this.sayParent = () => {
console.log("this.name", this.name);
};
}
Parent.prototype.getValue = function() {
return this.name;
};
function Child(value) {
Parent.call(this, value);
}
// 修改子构造函数的原型为父构造函数的实例
Child.prototype = new Parent();
// 修改子构造函数的原型的constructor为子构造函数本身
Child.prototype.constructor = Child;
缺点:
会调用两次Parent构造函数,一次是在Child.prototype = new Parent();,将子构造函数的原型修改为父构造函数的实例,会给Child.prototype添加name和sayParent,第二次调用是在Parent.call(this,value);,在声明Child的实例的时候,会给实例同样也添加一遍name和sayParent,实例的属性会覆盖原型的这两个同名属性,就相当于原型上会存在无用的属性和方法
原型式继承
思路:借助原型实现在已有对象的基础上,实现对象到对象的继承,这个思路后来被实现为Object.create()
function object(obj) {
function F() {}
F.prototype = o;
return new F();
}
缺点:
- 同样存在对于引用类型指向相同的问题,某一个实例修改,会导致所有实例的该属性修改
- 无法传参
寄生式继承
核心就是在原型式继承的基础上增强对象
function object(obj) {
function F() {}
F.prototype = o;
return new F();
}
// 寄生式继承
function createOther(obj) {
let clone = object(obj);
clone.testFunc = function() {
console.log(123);
};
return clone;
}
缺点类比原型式继承,是一样的问题
寄生组合式继承
利用借用构造函数实现实例属性的继承,利用原型链实现方法和原型属性的继承,但是是通过直接让子类的原型指向父类的原型,同时修改子类原型的constructor
function Parent(value) {
this.name = value;
}
Parent.prototype.getValue = function() {
return this.name;
};
Parent.sayHai = function() {
console.log("hai");
};
function Child(value) {
Parent.call(this, value);
}
// 设置子类的prototype,通过Object.create的方式创建一个以父类原型作为原型的对象,另外将constructor的value指向子类本身
Child.prototype = Object.create(Parent.prototype, {
constructor: {
value: Child,
enumerable: false,
writeable: true,
configurable: false
}
});
其实ES6的class继承,通过babel转义后,也就是通过类似寄生组合式继承实现的,只是传统的ES5只能实现到这步,还缺少一步,就是子类无法继承父类的静态方法,也就是上面定义的Parent.sayHai方法
其实所谓的静态方法,就是将类这个概念作为对象看待,对标ES5,也就是函数也是对象,所以定义在函数上的属性和方法,在ES5都是没有办法继承的,只能显式的写,例如
Parent.sayHai = function() {
console.log("hai");
};
Child.sayHai = Parent.sayHai;
这样显式的写,显然不符合继承的理念
但是ES6可以通过Object.setPrototypeOf(subClass, superClass)实现,这也是class继承的底层实现原理,class继承就可以继承静态方法,就是通过这个实现的
这里的概念其实有点绕,Object.setPrototypeOf方法,是将第一个参数的对象原型指向第二个参数,等同于subClass.__proto__ = superClass,要注意区分Child.prototype和Child.__proto__是两个不同的概念
Child.prototype是构造函数原型,Child.__proto__是对象原型,而静态属性和静态方法,都是定义在Parent这个函数的对象属性上的,所以可以通过Object.setPrototypeOf来实现对象属性的继承
ES6的class继承,就是通过这个实现的,下面贴上class继承通过babel转义之后的代码
function _inherits (subClass, superClass) {
// ...
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {
value: subClass,
enumerable: false,
writable: true,
configurable: true
}
});
if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
}
混入式继承
混入式继承可以同时继承多个类的构造函数属性和方法,也可以继承多个类的原型属性和方法,原理就是类似寄生组合式继承,通过call继承构造函数属性,通过修改原型继承其他类的原型属性,最后修改constructor指向
// 混入方式
function ChildClass() {
SuperClass.call(this);
OtherSuperClass.call(this);
}
// 继承一个类
ChildClass.prototype = Object.create(SuperClass.prototype);
// 混合其他
Object.assign(ChildClass.prototype,OtherSuperClass.prototype);
// 重新指定constructor
ChildClass.prototype.constructor = ChildClass;
ES6的继承extends
ES6的继承实现的babel转义代码已经在上面贴出来了,其实主要思路也就是寄生组合式继承,只是多了一步,通过Object.setPrototypeOf修改子类作为对象属性的原型,这样就可以继承父类的静态方法
// ES6继承
class MyParent {}
class MyChild extends MyParent {}
总结
ES5继承和ES6继承的主要区别
- ES5的继承是先创建子类的实例对象,然后再将父类的方法添加到this上
- ES6是先创建父类的实例对象this,然后再用子类的构造函数修改this,因为子类没有自己的this对象,所以必须先调用父类的
super()方法,否则新建实例报错
函数声明和类声明的区别:函数声明会有提升,类声明不会,必须先声明类,再访问,否则会抛出错误
ES6的class中,为什么需要使用super()
super()指的是父类的构造函数,js的规范规定了不能在调用super()之前使用this,其核心原因就是因为子类是没有this的,必须通过调用super()创建当前的this,在对当前的this进行修改属性等操作
假设允许在super()之前就调用this,那么很可能出现以下的情况
class Person {
constructor(name) {
this.name = name;
}
}
class PolitePerson extends Person {
constructor(name) {
this.greetColleagues(); // 🔴 这是禁止的,往后见原因
super(name);
}
// 如果在方法中调用this的属性,则会直接报错,因为这个时候this尚且没有
greetColleagues() {
alert('Good morning folks!');
alert('My name is ' + this.name + ', nice to meet you!');
}
}
React中的super()
在React中,及时不传props给super,也能在构造函数之后拿到this.props,这是因为React内部的机制,会在类的实例构造后,将props绑定到实例上
// React 内部
const instance = new YourComponent(props);
instance.props = props;
同时React允许不显式的写super(props),但是并不推荐都用super()替代super(props),因为React的机制是在构造函数之后绑定props到this上,所以如果不传props给super,那么在构造函数中使用this.props则还是会未定义,而如果传了props给super(),那么在super(props)下面使用this.props都是没问题的
// React 內部
class Component {
constructor(props) {
this.props = props;
// ...
}
}
// 你的程式碼內部
class Button extends React.Component {
constructor(props) {
super(); // 😬 我们忘了传入 props
console.log(props); // ✅ {}
console.log(this.props); // 😬 未定义
}
// ...
}
class Button extends React.Component {
constructor(props) {
super(props); // ✅ 传入 props
console.log(props); // ✅ {}
console.log(this.props); // ✅ {}
}
// ...
}