深入理解面向对象-继承
前言
面向对象有三大特性:封装、继承、多态
- 封装:上一文深入理解面向对象将属性和方法封装到一个函数(类)中,可以称之为封装的过程;
- 继承:继承面向对象中非常重要的,不仅可以减少重复代码的数量,也是多态的前提(纯面向对象中)
- 多态:不同的对象在执行任务时的不同表现 在真正实现继承之前,我们先来理解一个非常重要的概念:原型链
JavaScript的原型链
我们知道,从一个对象上获取属性,如果在当前对象中没有获取到就会去他的原型上面获取:
那么什么地方时原型的尽头的?比如我们第三个对象是否也有原型__proto__的属性呢?
我们会发现它打印的是 [Object:null prototype]{} ,事实上这个原型就是我们最顶层的原型了,从Object直接创建出来的对象的原型都是 [Object:null prototype]{} 。
那么我们可能会问:[Object:null prototype]{} 原型有什么特殊?
- 特殊一:该对象有原型属性,但是他的原型属性已经指向null,也就是顶层原型
- 特殊二:该对象上有很多默认的方法和属性
原型链关系内存图
从上面给的Object的原型我们可以得出一个结论:原型链最顶层的原型对象就是Object的原型对象null
如果我们现在需要实现继承,那么就可以利用原型链来是实现了
通过原型链实现继承
// 1、定义父类构造函数
function Person (){
this.name = 'why'
this.friend = []
}
//2、父类原型上添加方法
Person.prototype.running = function(){
console.log(this.name + "running")
}
//3、定义子类构造函数
function Student(){
this.ano = "111"
}
//4、通过原型链实现继承:创建父类实例,并且作为子类的protptype原型对象
let p = new Person()
Student.prototype = p
//5、在子类原型上添加新的内容
Student.prototpye.studying = function(){
console.log(this.name + "studying")
}
目前student的原型是p对象,而p对象的原型是Person默认的原型,里面包含running等函数,注:步骤4和步骤5不可以调整顺序,否则会有问题
原型链继承的弊端
使用原型链继承的弊端:某些属性其实是保存在p对象上的;
- 第一:我们通过直接打印对象看不到这个属性的;例如
student.name - 第二:这个属性会被多个对象共享,如果这个对象是一个引用类型,会造成问题
let stu1 = new Student() let stu2 = new Student() //直接修改stu1上的属性,是给本对象添加属性 stu1.name = "kobe" console.log(stu1.name) // "kobe" console.log(stu2.name) // "why" //获取引用, 修改引用中的值, 会相互影响 stu1.friends.push("kobe") console.log(stu1.friends) // ["kobe"] console.log(stu2.friends) // ["kobe"] - 第三:不能给Person传递参数,因为这个对象是一次性创建的
借用构造函数继承
为了解决原型链中存在的问题,开发人员提供了一种新的技术:constructor stealing(借用构造函数)。 借用构造函数的做法很简单:在子类构造函数内部调用父类构造函数
- 函数可以在任意时刻被调用
- 通过apply()和call()方法也可以在新创建的对象上执行构造函数
// 父类: 公共属性和方法
function Person(name, age, friends) {
this.name = name
this.age = age
this.friends = friends
}
Person.prototype.eating = function() {
console.log(this.name + " eating~")
}
// 子类: 特有属性和方法
function Student(name, age, friends, sno) {
Person.call(this, name, age, friends) // this = stu
// this.name = name
// this.age = age
// this.friends = friends
this.sno = 111
}
var p = new Person()
Student.prototype = p
Student.prototype.studying = function() {
console.log(this.name + " studying~")
}
//创建stu 实例
var stu = new Student("why", 18, ["kobe"], 111)
var stu1 = new Student("why", 18, ["lilei"], 111)
var stu2 = new Student("kobe", 30, ["james"], 112)
组合借用继承的问题
- 组合借用构造函数继承最大的问题就是无论在什么情况下,都需要调用两次父类构造函数
- 一次是在创建子类原型的时候
- 另一侧是在子类构造函数内部(也就是每次创建子类实例的时候)
- 另外,如果仔细按照流程走每个步骤,你就会发现,每个子类实例事实上会有两份父类的属性
- 一份在当前的实例自己里面(也就是Person本身的),另一份在子类对应的原型对象中(也就是preson.__proto__里面)
- 当然,这两份属性我们无需担心访问出现问题,因为默认一定是访问实例本身这一部分的;
原型式继承函数
- 原型式继承的渊源
- 这种模式要从道格拉斯·可罗克福德在2006年写的一篇文章说起:Prototypal Inheritance in JavaScript(在JS中使用原型式继承)
- 在这篇文章中,它介绍了一种继承方式,而且这种继承方式不是通过构造函数来实现的
- 为了理解这种方式,我们先再次回顾一下JavaScript想实现继承的目的:重复利用另外一个对象的属性和方法
- 最终的目的:student对象的原型指向了person对象
寄生式继承函数
- 寄生式继承是与原型链继承紧密相关的一种思想
- 寄生式继承的思路是结合原型链继承和工厂模式的一种方式
- 即创建一个封装继承的函数,该函数内部以某种方法来增强对象,最后返回这个对象
寄生组合式继承
我们来回顾一下之前提出的比较理想的组合继承,组合继承是比较理想的继承方式,但是存在两个问题
- 构造函数被调用两次:一次在创建子类原型对象的时候,另一次是在创建子类实例对象的时候
- 父类中的属性会有两份:一份在原型对象上,另一份在子类型中 事实上,我们可以利用寄生式继承将这两个问题解决掉
- 首先明确一点:当在子类构造函数中调用父类.call(this,参数)这个函数的时候,就会在将父类的属性赋值一份到子类中,所以父类本身里面的内容,我们不再需要
- 这个时候,我们还需要获取到一份父类的原型对象中的属性和方法 能不能直接让子类型的原型对象=夫类型的原型对象呢?
- 首先明确表示我们不应该这么做,因为这么做就意味着修改了子类原型对象的某个引用类型的时候,父类原生对象的引用也会改变
- 所以我们考虑创建一个中间量,使用前面的寄生式思想就可以了
//定义object函数
function object(o){
function F(){}
F.prototype = o
return new F
}
//定义寄生式核心函数
function inheritProtptype(subType,superType){
let newObj = object(superType.prototpye)
subType.prototype = newObj
subType.prototype.constructor = subType
}
function inheritProtptype(Student,Person)
小结
综上所述,ES5继承的最优解如下代码所示:
//1、定义父类的属性
function Person (name,age,friends){
this.name =name
this.age = age
this.friends = friends
}
// 2、定义父类的方法
Person.prototype.running = function(){
console.log(this.name + " is running")
}
//3、定义子类
//3.1子类属性继承
function Student(name,age,friends,height,score){
//组合式继承属性
Person.call(this,name,age,friends)
this.height = height
this.score = score
}
//3.2子类方法继承
//第一种:利用上面封装的寄生式函数
function inheritProtptype(Student,Person)
//第二种:利用Object.create()方法和Object.defineProperty
Student.prototype = Object.create(Person.prototype)
Object.defineProperty(Student.prototype,"constructor",{
configurable:true,
enumable:false,
writable:true,
value:Student
})
Student.prototype.studying = function() {
console.log("studying~")
}
var stu = new Student("why", 18, ["kobe"], 111, 100)
console.log(stu)
// Student { name: 'why', age: 18, friends: [ 'kobe' ],height:1.88, score: 100}
stu.studying() // studying~
stu.running() //why is running