继承
概念
继承是面向对象软件技术当中的一个概念,与多态,封装共同为面向对象的三个基本特征。
继承可以使子类具有父类的属性和方法或者重新定义,追加属性和方法等。
方式
原型的继承
分析
由于原型链的存在,实例对象可以通过原型链向上查找属性和方法,就可以使用到原型对象的属性和方法。
实例对象和原型对象是相对的,原型对象也有它自己的原型对象。
因此要通过原型链继承,只需要将子类对象,加入到父类对象的原型链中即可。
怎么加入? 只需要将子类的原型对象 等于 父类对象的实例即可。
关键
子类的原型为父类的一个实例对象(根据原型链的定义,子类可以访问到就可以访问到子类的所有方法和属性)
代码
// 声明父类构造函数
function Person(name,age){
this.name = name
this.age = age
}
// 在构造函数的原型对象上添加公有方法
// 为什么要添加在Person的原型对象上?
// 当然也是可以直接添加在构造函数中,但是这样做,会导致每次实例化对象都会重新创建方法和属性
// 所以一般将公有属性和方法 添加到构造函数的原型上,节省 内存
// 具体参照构造函数的知识
Person.prototype.getName = function(){
return this.name
}
// 声明子类构造函数
function Student(price){
this.price = price
}
// 实现原型链继承
// 子类对象的原型(子类构造函数的prototype属性) 等于父类的一个实例即可
Student.prototype = new Person()
// 实例化子类对象
var s1 = new Student(1500)
console.log(s1)
特点
缺点
- 从打印s1,即可知道,s1的name,age都是undefined,即使用原型链的继承缺点在于 创建实例时无法向父类构造函数传递参数。
- 要想为子类对象新增属性和方法,必须等到Student.prototype = new Person() 之后,不能放到构造函数中。
构造函数的继承
分析
为了解决原型链继承的问题,我们考虑使用构造函数的方式。
我们要使用父类的方法和属性,我们可以借用父类的构造函数,在子类的构造函数中实现一遍,就可以解决传递参数的问题。
关键
在子类构造函数中,使用call借用父类的构造函数,实现一遍父类的构造函数。
代码
// 声明一个父类构造函数
function Person(name,age){
this.name = name
this.age = age
}
//添加公有方法
Person.prototype.getName = function(){
return this.name
}
// 声明子类构造函数
// 在子类构造函数中借用父类构造函数
function Student(price,name,age){
// 子类自己的属性
this.price = price
// 借用父类构造函数
Person.call(this,name,age)
}
// 实例化子类对象
var s1 = new Student(100,'Tom',12)
特点
缺点
- 这种方式只是部分的继承,如果父类的原型上有属性和方法,子类是拿不到这些方法和属性的。
- 只能继承实例方法,不能继承原型方法
- 实例化出来的对象,只是子类的实例,并不是父类的实例
- 无法实现函数复用,每个实例中都有父类中的函数的副本,影响性能
构造函数和原型的组合继承
分析
既然原型链继承和构造函数继承都各有优缺点,那能不能结合起来。
既通过call借用父类方法,继承父类的属性并保留传参的优点,然后通过父类实例作为子类原型,实现函数复用。
关键
代码
// 声明一个父类构造函数
function Person(name,age){
this.name = name
this.age = age
}
//添加公有方法
Person.prototype.getName = function(){
return this.name
}
// 声明子类构造函数
function Student(price,name,age){
// 借用父类构造函数方法
Person.call(this,name,age)
// 修改子类原型为父类实例
Student.prototype = new Person()
// 组合继承 需要修复构造函数指向
Student.prototype.constructor = Student
/*
为什么要修复 constructor指向
1. 补充constructor知识
任何一个对象都有一个constructor属性,指向它的构造函数。
任何一个原型对象都有一个constructor属性,指向它的构造函数(属性值为构造函数的引用)
Person.prototype.constructor = Person
任何一个实例对象也有一个constructor属性,默认调用的是它的原型的constructor的值。
2. Student.prototype = new Person() 执行这一步的时候,就已经替换掉 子类的原型对象,
可以这么理解:
上面的代码可以替换成这两句 var p1 = new Person() Student.prototype = p1
当实例化子类对象 s1 的时候 var s1 = new Student() 假如这个时候,我们要去找 s1的
constructor属性,由上面的知识可得,s1.constructor === s1.__proto.constuctor
即s1.constructor === Student.prototype.constructor, 因为我们修改了子类构造函数为
父类的实例,所以 s1.constructor === p1.constructor,因为p1是父类构造函数的实例对象,
所以p1.constructor指向Person() ,即s1.constructor === Person()。
这与我们上面的知识是违背的( 任何一个对象都有一个constructor属性,指向它的构造函数。)
所以我们要修复 子类构造函数的指向,即Student.prototype.constructor = Student。
*/
// 添加子类原型方法
Student.prototype.sayHello = function(){}
}
// 实例化子类对象
var s1 = new Student(100,'Tom',11)
console.log(s1)
特点
缺点
无论在什么情况下,都要调用两次父类构造函数,一次是创建子类原型的时候,一次是在子类构造函数的内部。生成了两份实例。1. Person.call(this,name,age) 这是第一次 2. Student.prototype = new Person() 这是第二次
组合继承优化1
分析
为了解决生成两份父类实例的缺点,我们可以尝试让子类的原型指向父类的原型,这样就能避免生成两份实例。
代码
// 声明父类构造函数
function Person(name, age) {
this.name = name
this.age = age
}
//添加原型方法
Person.prototype.setAge = function () {
console.log("111")
}
// 声明子类构造函数
function Student(name, age, price) {
Person.call(this, name, age)
this.price = price
this.setScore = function () { }
}
// 将子类的原型指向父类的原型
Student.prototype = Person.prototype
// 修复constructor指向
Student.prototype.constructor = Student
Student.prototype.sayHello = function () { }
// 实例化子类对象
var s1 = new Student('Tom', 20, 15000)
console.log(s1)
缺点
父类构造函数的原型对象会受影响
Student.prototype.constructor = Student
// 我们在做这一步的时候,这样写是有问题的,因为这样写,实际上把Person.prototype.constructor的值
// 也修改成Student了
// 因为在 Student.prototype = Person.prototype这一步的时候,Student.prototype和Person.prototype
// 指向的就是同一个对象了,任何对这个对象的修改,都会同步反应
// 所以修改之后
Person.prototype.constructor = Student
组合继承优化2
分析
由于直接继承父类的prototype存在缺点,所以,利用一个空对象作为中介。
空对象几乎不占内存。
代码
// 声明父类构造函数
function Person(name, age) {
this.name = name
this.age = age
}
//添加原型方法
Person.prototype.setAge = function () {
console.log("111")
}
// 声明子类构造函数
function Student(name, age, price) {
Person.call(this, name, age)
this.price = price
this.setScore = function () { }
}
// 这是新的构造函数
var F = function(){}
// 将构造函数F的原型对象指向父类构造函数的原型
F.prototype = Person.prototype
// 将子类构造函数的原型指向 F的一个实例
Student.prototype = new F()
// 修复constructor指向
Student.prototype.constructor = Student
// F是空对象几乎不占内存,这时候修改Student的原型对象就不会影响到父类的原型对象,
// 也不会重复实例化父类
Object.create()继承
分析
组合继承优化2 中的方式,其实可以使用Object.create()方法代替。
关键
var b = Object.create( A ),以对象A为原型,生成了b对象。b对象继承A对象的所有属性和方法。
代码
// 声明父类构造函数
function Person(name, age) {
this.name = name
this.age = age
}
//添加原型方法
Person.prototype.setAge = function () {
console.log("111")
}
// 声明子类构造函数
function Student(name, age, price) {
Person.call(this, name, age)
this.price = price
this.setScore = function () { }
}
// 核心代码
// 以父类的原型为父对象,生成一个子对象,子对象继承父对象的所有方法和属性
// 又将子对象的原型设置为 这个对象
Student.prototype = Object.create(Person.prototype)
// 修复 constructor指向
Student.prototype.constructor = Student
特点
class 继承
分析
ES6 引入了类的概念,并提供extends关键字 实现继承。还可以通过static关键字定义静态方法。
这比ES5 通过修改原型链 实现继承 更加方便。
对比: ES5继承和ES6继承对比(this先后)(需要再理解)
- ES5的继承实际上是先创建子类的实例对象this,然后再通过父类的构造方法添加到this上(call),即:先有子类的this,再调用父类构造方法修改this。(先有子类的this)
- ES6的继承机制则完全不同,实质上,是先将父类的属性和方法添加到this上,所以必须先调用super(this),再用子类的构造函数修改this,即先拿到父类的this,再去添加子类的属性和方法。(先有父类的this)
代码
class Point(){}
class ColorPoint extends Point{
// constructor属性 指向构造函数
constructor(x,y){
super(x,y) // 调用父类的构造函数
this.color = color
// 重写子类的toString
toString(){
return this.color+ '' + super.toString() // 调用父类的toString
}
}
}
// super 表示父类的构造函数,用来新建父类的this对象
注意
- 如果子类没有定义constructor方法,这个方法会默认添加。任何子类都有一个constructor方法,在子类的构造函数中,只有调用super() 方法,才能使用this关键字,否则会报错。
- 这是因为子类实例的创建,依赖于父类的this,只有先调用super方法。