继承是什么?
继承可以将重复的代码逻辑抽取到父类中,子类只需要直接继承过来使用即可。
怎么实现继承?
原型链实现继承(组合继承)
首先回顾一下原型,原型链是什么?
原型
- JavaScript的所有对象中都包含了一个 [_ _ proto _ _ ]内部属性,这个属性所对应的就是该对象的原型
- JavaScript的函数对象,除了原型 [_ _ proto _ _ ] 之外,还预置了 prototype 属性
- 当函数对象作为构造函数创建实例时,该 prototype 属性值将被作为实例对象的原型[_ _ proto _ _]
- 函数的原型上有一个属性constructor,指向函数本身
原型链
- 当一个对象调用的属性/方法在自身不存在时,就会去自己 [_ _ proto_ _ ] 关联的前辈prototype 对象上去找
- 如果没找到,就会去该 prototype 原型对象的 [_ _ proto_ _ ] 关联的前辈 prototype 去找。依次类推,直到找到属性/方法或 undefined 为止,从而形成了所谓的“原型链”
实现继承
- 创建两个构造函数,往里面添加属性和方法,这时创建出来的stu对象并不能使用Person里面的属性和方法
// 父类:公共的属性和方法
function Person(){
this.name = 'bx',
this.friends = []
}
Person.prototype.eating = function(){
console.log(this.name + '正在吃饭')
}
// 子类:特有的属性和方法
function Student(){
this.sno = 111
}
Student.prototype.studying = function(){
console.log(this.name + '正在学习')
}
var stu = new Student()
// 2.创建出来两个stu对象
var stu1 = new Student()
var stu2 = new Student()
- 在往Student原型添加方法之前,让Student的原型prototype指向Person创建出来的那个对象p,这样就形成了原型链,当在stu对象上找不到某个属性或方法时,就沿着原型链去p对象身上找,p对象上找不到就去p的原型prototype上找,这样就实现了继承
// 父类:公共的属性和方法
function Person(){
this.name = 'bx',
this.friends = []
}
Person.prototype.eating = function(){
console.log(this.name + '正在吃饭')
}
// 子类:特有的属性和方法
function Student(){
this.sno = 111
}
var p = new Person()
Student.prototype = p
// 合并为一行代码 Student.prototype = new Person()
Student.prototype.studying = function(){
console.log(this.name + '正在学习')
}
var stu = new Student()
// 2.创建出来两个stu对象
var stu1 = new Student()
var stu2 = new Student()
这时stu1和stu2身上都有了name和friends属性,有了eating方法,相当于继承了父类的属性和方法
但是这种实现继承的方法有几个弊端,开发中不使用这种方法:
- 打印stu对象,继承的属性是看不到的,而且体现不出来自己类的名字,而是父类的名字,后面寄生组合式继承会解释到
打印stu1出来的是 Person { sno: 111 }
- 获取引用,修改引用中的值,会互相影响
如果给stu1的friends添加数据,会影响到stu2的friends,因为它们指向的都是p对象
stu1.friends.push('bx')
console.log(stu1.friends) // ['bx']
console.log(stu2.friends) // ['bx']
如果stu1.friends = []就是给本对象添加新的属性了,不会影响到stu2
- 前面实现类的过程中都没有传递参数
构造函数实现继承
在Person里传入三个参数name,age,friends,在Student构造函数里用call调用Person函数,并把this和name,age,friends参数传进去,解决了以下问题:
- 打印stu对象可以看到继承的属性,因为这些属性都加在自己身上了
- 获取引用,修改引用中的值,不会互相影响
- 在创建stu对象的时候就能传入参数,解决上面原型实现继承的第三个弊端
- 这里是借用构造函数实现继承,调用的是子类函数,但实际上是借用父类构造函数来给子类身上加属性 代码如下:
// 父类:公共的属性和方法
function Person(name,age,friends){
// 现在的情况下this就是Student对象了
this.name = name,
this.age = age,
this.friends = friends
}
Person.prototype.eating = function(){
console.log(this.name + '正在吃饭')
}
// 子类:特有的属性和方法
function Student(name,age,friends,sno){
// 传入this
Person.call(this,name,age,friends)
this.sno = 111
}
var p = new Person()
Student.prototype = p
// 合并为一行代码 Student.prototype = new Person()
Student.prototype.studying = function(){
console.log(this.name + '正在学习')
}
var stu = new Student()
// 2.创建出来两个stu对象
var stu1 = new Student('bx',21,['yjt'],11)
var stu2 = new Student('lucy',22,['wje'],12)
这种方法虽然解决了原型链实现继承的弊端,但是还是有不足之处:
- Person函数至少被调用了两次,创建p对象一次,创建stu对象一次
- stu的原型对象上会多出一些属性,这些属性没有必要存在。p对象上没有必要存在name,age,friends这些属性,因为这些属性在stu对象创建的时候及已经存在在stu身上了,p里面存在这些属性没用
注意:如果为了不给p对象添加额外没用的属性,不可以这样做:Student.prototype = Person.prototype,这样的话,两个构造函数的原型都是Person的原型对象,给Student的prototype加属性和方法也会加到Person的原型对象上,会让Person的原型对象越来越大,不符合面向对象的思想
原型式继承
这里obj使用字面量方式创建,想让obj作为info的原型,创建了createObject函数,Object.setPrototypeOf(newObj,o)方法意思是把o作为newObj的原型,createObject2里面的实现和createObject意思一样
var obj = {
name:'bx',
age:18
}
// 原型式继承函数
function createObject(o){
var newObj = {}
Object.setPrototypeOf(newObj,o)
return newObj
}
function createObject2(o){
function Fn(){}
Fn.prototype = o
var newObj = new Fn()
return newObj
}
var info = createObject(obj)
// var info = createObject2(obj)
console.log(info) //{}
console.log(info.__proto__) //{ name: 'bx', age: 18 }
createObject和createObject2方法可以用一个方法代替:Object.create()
var info = Object.create(obj)
这样也实现了info的原型是obj,就可以继承obj的属性
寄生式继承
思想:寄生式继承是与原型式继承紧密相关的一种思想,思路是结合原型式继承和工厂函数的一种方式,创建一个封装继承过程的函数,在该函数内部以某种方式来增强(扩展)对象,最后再将这个对象返回
var personObj = {
running:function(){
console.log('running')
}
}
// 工厂函数(需要把创建的对象明确的return出去)
function createStudent(name){
var stu = Object.create(personObj)
stu.name = name
stu.studing = function(){
console.log('studying')
}
return stu
}
var stu1 = createStudent('bx')
var stu2 = createStudent('yjt')
var stu3 = createStudent('lhw')
寄生组合式继承
即通过借用构造函数来继承属性,通过原型链继承方法
function Person(name,age,friends){
this.name = name,
this.age = age,
this.friends = friends
}
Person.prototype.eating = function(){
console.log('eating~')
}
function Student(name,age,friends,sno,score){
//借用构造函数来继承属性
Person.call(this,name,age,friends)
this.sno = sno,
this.score = score
}
//通过原型链继承方法
Student.prototype = Object.create(Person.prototype)
Student.prototype.studying = function(){
console.log('studying~')
}
var stu1 = new Student('bx',21,'wyy',123,100)
console.log(stu1)
stu1.eating() //eating~
stu1.studying() //studying~
这里打印出来的stu1是Person { name: 'bx', age: 21, friends: 'wyy', sno: 123, score: 100 }
为什么名字是Person呢?
Student.prototype = Object.create(Person.prototype)这行代码执行时,Object.create()创建出来的新对象取代了Student.prototype一开始指向的对象,这个新创建出来的对象没有constructor属性,但是名字要从constructor的name取,在自身找不到就沿着原型链找,所以就找到了Person.prototype,因为Person.prototype身上有constructor属性,并且指向Person函数,所以这里的名字是Person。
这里也解释了原型链继承的弊端1
这里封装了一个实现继承的函数,包括修改constructor名字
// 封装一个继承方法,SubType子类,SuperType父类
function inheritPrototype(SubType,SuperType){
SubType.prototype = Object.create(SuperType.prototype)
// 修改constructor的名字
Object.defineProperty(SubType.prototype,"constructor",{
enumerable:false, //可枚举
configurable:true, //可配置
writable:true, //可写
value:SubType //名字
})
}
也可以这么写:
function createObject(o){
function Fn(){}
Fn.prototype = o
return new Fn()
}
// 封装一个继承方法,SubType子类,SuperType父类
function inheritPrototype(SubType,SuperType){
// SubType.prototype = Object.create(SuperType.prototype)
SubType.prototype = createObject(SuperType.prototype)
// 修改constructor的名字
Object.defineProperty(SubType.prototype,"constructor",{
enumerable:false,
configurable:true,
writable:true,
value:SubType
})
}
后面在往Student的prototype上添加方法之前调用这个方法inheritPrototype(Student,Person)