重点分析四个常用的继承方式,并且由最次到最佳的递进过程,分析每个继承的优缺点。
原型继承
主要利用JavaScript的原型链。
代码演示
function Parent(sex){
this.sex = sex
}
Parent.prototype.setSex = function(){}
function Son(name){
this.name = name
}
Son.prototype = new Parent()
var s1 = new Son("bobo")
console.log(s1)
关键实现
把子类的原型指向父类的实例,完成了子继承父类的私有属性和原型属性。
优点:
- 父类新增的原型属性和方法,子类实例都能访问到
-
缺点:
无法实现多继承,一个子类继承多个父类
- 创建子类实例的时候,无法向父类构造函数传参
存在子类实例共享父类引用属性的问题(子类的原型同时指向父类的一个实例,假如父类的私有属性是一个引用类型,那么任何一个子类操作该引用类型属性的时,会导致其他子类使用的这个属性发生变化)
借用父类构造函数继承
代码实例
function Parent(sex){
this.sex = sex
}
Parent.prototype.setSex = function (){}
function Son(name, age, sex){
Parent.call(this, sex)
this.name = name
this.age = age
}
var son = new Son("bobo",18,'male')
console.log(son)
关键实现:
在子类构造函数中使用call或者apply调用父类的构造函数,继承父类私有属性。
优点:
创建子类实例时,可以向父类传递参数
- 可以实现多继承
解决原型继承中子类实例共享父类引用类型属性的问题。(在创建子类实例时,都会重新调用父类构造函数重新创建一个引用类型属性的数据)
缺点:
每次创建子类实例,都要调用一次父类构造函数,影响性能
- 只继承了父类的私有属性,没有继承父类的原型属性
组合式继承(原型链+借用构造函数)
代码实例: ```javascript function Parent(sex){ this.sex = sex } Parent.prototype.setSex = function(){}
function Son(name, age, sex){ Parent.call(this, sex) this.name = name this.age = age } Son.prototype = Object.create(Parent.prototype)
// 修正Son的构造函数 Son.prototype.constructor = Son var son = new Son(“bobo”, 18, “boy”) console.log(son)
执行结果:<br />![混合继承.png](https://cdn.nlark.com/yuque/0/2020/png/737887/1599752624967-68a9f598-355e-4941-8677-354f507babb8.png#align=left&display=inline&height=468&margin=%5Bobject%20Object%5D&name=%E6%B7%B7%E5%90%88%E7%BB%A7%E6%89%BF.png&originHeight=468&originWidth=484&size=26771&status=done&style=none&width=484)
<a name="AH5Ks"></a>
### 关键实现:
通过调用父类构造函数,继承父类的属性并保留传参的优点,通过Object.create(Parent.prototype)来继承父类原型属性的对象,并把这个对象赋值给子类的原型。<br />既能保证父类构造函数不用执行两次,又能让子类能继承到父类的原型方法。
<a name="9eBZY"></a>
### 优点:
- 创建子类实例时,可以向父类传递参数
- 可以实现多继承
- 解决了原型链继承中子类实例共享父类是引用类型属性的问题。
- 父类构造函数只执行一次。<br />
<a name="4dSvY"></a>
## Es6中class的继承
ES6中引入了**class关键字**,class可以通过extends关键字实现继承,还可以通过**static**关键字定义类的静态方法,这比 ES5 的通过修改原型链实现继承,要**清晰和方便**很多。<br />**注意:ES5 的继承**,实质是**先创造子类的实例对象this**,然后**再将父类的方法添加到this**上面(Parent.apply(this))。<br />**ES6 的继承机制完全不同**,实质是**先将父类实例对象的属性和方法加到this上面(所以必须先调用super方法)**,**然后再用子类的构造函数修改this**。<br />代码实例:
```javascript
class A{
constructor(sex){
this.sex = sex
}
showSex(){
console.log("父类中的方法")
}
}
class B extends A{
constructor(name, age, sex){
super(sex);
this.name = name;
this.age =age;
}
showSex(){
console.log("这里是子类的方法")
}
}
let b = new B("bbb",12,"boy")
console.log(b)
关键实现原理
使用extends关键字继承父类的原型属性,调用super来继承父类的实例属性,并且保留向父类构造函数传参的优点
优点:
简单易用,书写方便。不用自己来修改原型链完成继承
接下来将代码从ES6编译到ES5来看看到底class继承的代码最终会被编译成什么样
从上图分析得到:
- 上述代码示例中的super指的就是父类构造函数
- 子类继承父类的实例属性最终还是通过call或者apply来实现继承的
- 通过extends方法的调用来修改子类和父类的原型链关系
再看经过编译后的extends方法,如下
1、注意Object.setPrototypeOf()方法设置一个指定的对象的原型 ( 即, 内部[[Prototype]]属性)到另一个对象或 null
。
2、(.prototype = b.prototype, new ())表达式的执行执行顺序是先执行前者,再返回后者
从上图可知,extends做了以下几件事:
- 定义了一个function __() {}函数,并把该函数的constructor指向了子类
- 紧接着,把function __() {} 函数的原型指向了父类的原型
- 最后再把function () {} 函数的实例赋给了子类函数,就这样子类的实例就能沿着proto.proto获取到父类的原型属性了,这种继承模式俗称圣杯模式
基于class实现简版Jquery
class Jquery{
constructor(selector){
const selectList = document.querySelectorAll(selector)
let length = selectList.length
for(let i = 0;i< length;i++){
this[i] = selectList[i]
}
this.length = length
}
getIndex(index){
return this[index]
}
each(fn){
for(let i=0;i<this.length; i++){
fn(this[i])
}
}
on(type, fn){
return this.each(elem=>{
elem.addEventListener(type, fn, false)
})
}
// ......
}
// 扩展插件
Jquery.prototype.dialog = function(str){
alert(str)
}
// 基于Jquery扩展处理Xquery
class Xquery extends Jquery{
constructor(selector){
super(selector)
}
// 扩展自己的方法
addClass(){}
getClass(){}
}