继承
1.继承的 6 种方式
原型链继承
-
父子类型定义
【父类型 Person】 function Person(){ this.sleep = function(){ console.log("Person Sleep") } } Person.prototype.getName = function(){ console.log("Person Name") } 【子类型 Student】 function Student(){ } -
继承写法(覆盖原型对象,并生成新原型对象)
关键是,子类型的原型为父类型的一个实例对象,即 私有 变为 公有
Student.prototype = new Person(); -
再写子类型自己的原型方法:
可覆盖同名的父类型的实例对象上的实例方法
Student.prototype.getScore = function () { console.log("Student Score"); }; var s1 = new Student(); -
这样子操作
-
不仅有自己的原型方法
s1.getScore() -
也能向上一层,继承到父类型的实例对象上的实例方法
s1.sleep() -
也能向上两层,继承到父类型的原型方法,
s1.getName()
-
-
相当于沿着原型链一路往上找方法
-
通过
s1.__proto__可以访问到原型,即 Person 的实例对象,调用到实例方法 -
通过
s1.__proto__.__proto__可以访问到原型的原型,即 Person 的原型,调用到原型方法 -
注意:
- 父与子中,引用类型会联动变化
- 在子类中添加新方法或重写父类方法时,一定要在“继承写法”之后去写
-
-
我们
- 称
s1.__proto__为隐式原型,每个实例都有,是私有属性 - 称
Student.prototype为显式原型,每个类都有 - 二者是===全等的,指向对应的是同一个
- 称
-
所以,
- 当实例调用属性或方法时:
- 先在自身里面寻找,
- 找不到再从隐式原型
__proto__中寻找,也就是对应类的显式原型中寻找
- 当实例调用属性或方法时:
-
缺点:
- 多份子类型实例会共享同一份父类型中的实例属性(致命缺陷)
- 创建子类型实例时,无法向父类构造函数传参(只能在改变原型时传一次)
- 无法实现多继承
-
相当于
- 夺取了父类构造函数中定义的实例方法到其原型中
- 夺取了父类的原型方法到其原型的原型中
借用构造函数继承
-
父子类型定义同上
父类型 Person 子类型 Student -
继承写法
关键是,子类型构造函数中,通过 call 调用父类型构造函数
function Student() { Person.call(this, 给父类型填的参数); } var s1 = new Student(); -
优势: 解决了原型链继承的三大痛点
- 不用共享一份父实例属性了
- 可以向父类构造函数传参数了
- 可以实现多继承了(call 多个父类对象)
-
出现新的缺点:
- 只能继承父类中的实例方法和属性(即私有的),而原型方法和属性(即公有的)无法得到
- 每次 new 子类型都要创建一个父类型实例函数的副本,无法复用函数,有些浪费
-
相当于
- 夺取了父类构造函数中的实例方法到子类中
- 无法夺取原型方法
上面两种方式组合继承
-
关键是三点
- 模仿原型链继承,new 父类型()
- 模仿构造函数继承,父类型.call()
- 还要修复构造函数指向(修复语句如下)
Student.prototype.constructor = Student; -
优势: 解决了构造函数继承的两大痛点
- 两者结合,能得到实例和原型的所有属性方法
- 函数可复用,只要写成原型函数即可
-
又出现新的缺点:
- 会调用两次父类型构造函数
- 一次是 new 父类型()时
- 一次是父类型.call()时,生成两份实例
- 会调用两次父类型构造函数
组合继承的优化 1-简单版
-
关键是:父类型.call()的基础上,不用实例化(new)父类型构造函数,直接把 new 父类型()换成父类型原型
- 指向语句
Student.prototype = Person.prototype; - 修复语句
Student.prototype.constructor = Student;
- 指向语句
-
优势: 解决了生成两份实例的痛点
-
又出现新的缺点
- instanceof 没法辨别实例到底是父类还是子类创造的,因为原型链上两者都有
-
相当于
- 夺取了父类构造函数中的实例方法到子类中
- 夺取了父类的原型方法到原型中
组合继承的优化 2-正式版,寄生组合继承(较完美的方法)
-
关键是:父类型.call()的基础上,不用实例化(new)父类型构造函数,直接把 new 父类型()换成“借用对象创建语句得到的原型实例”,再加上修复构造函数指向语句
这样就避免了 new 在调用构造函数时产生的多一份副本
- 对象创建语句得到原型实例
Student.prototype = Object.create(Person.prototype); - 修复语句
Student.prototype.constructor = Student;
- 对象创建语句得到原型实例
-
注意
- 跟方法 1 的差别是,Person 上的原型方法,不再跟 Student 上的原型方法混合在同一层级,而是 Person 更加往上一级(详情可以看下面测试图最后的输出)
- 同样无法辨别实例是父类还是子类创造的
以上是 ES5 继承
实质是先创造子类的实例对象 this,然后再将父类的实例方法添加到 this 上面,再把原型方法加入到子类的原型链上
2. ES6 继承
更先进,利用 class 和 extends 关键字
实质是先将父类实例对象的方法属性加到 this 上面(通过 super 方法),然后再用子类构造函数继承和修改 this
class Student extends Person{
constructor(name,age,score){
//先调用父类的构造方法
super(name,age)
//再补充自己的属性
this.score = score
}
}
非常好,简单易懂操作方便,但不是所有浏览器都支持 ES6
注意,class 只是语法规范,由 ECMA 委员会发布,并不规定具体实现,上面所说的都是 V8 引擎的实现方式,当然这是主流的