面向对象-继承

19 阅读5分钟

继承是获取已经存在的对象已有属性和方法的一种方式,ECMAScript 依靠原型连实现继承。

就近继承原则

function F(name) {
  this.name = name;
  this.toString = function() {
    console.log('我是构造函数上的方法')
  }
}

F.prototype.toString = function() {
  console.log('我是构造函数原型上的方法')
};

var fun = new F()
fun.toString()  // 我是构造函数上的方法

对象的方法去访问属性时,先查找有没有对应的实例属性,有则使用;
若没有,就去该对象的原型对象上查找,有则使用;
若没有,就接着去原型对象的原型对象上查找,有则使用;
若没有,就继续沿原型链查找;
直到搜索到 Object.prototype 为止,如果还是没有找到就返回 undefined 或者报错。
可以通过删除实例属性的方式,打破就近原则,去访问原型链上的属性。

function Box() {}
Box.prototype.name = 'lee'

var box1 = new Box()
box1.name = 'jack'

alert(box1.name) // jack

delete box1.name

alert(box1.name) // lee

继承实现方式

  1. 原型链继承
    核心就是让子类的原型为父类的一个实例对象。

    // 父类
    function Person() {
      this.name = 'lxx'
      this.friends = ['aaa','bbb','ccc']
    }
    Person.prototype.run = function() {
      consolee.log(this.name + ' running~')
    }
    
    // 子类
    function Student() {
      this.sno = 110
    }
    
    // 实现继承
    Student.prototype = new Person()
    
    var stu1 = new Student()
    stu1.sno // 110
    stu1.friends // ['aaa','bbb','ccc']
    stu1.run() // lxx running~
    stu1.friends.push('ddd')
    
    var stu2 = new Student()
    stu2.sno // 110
    stu2.friends // ['aaa','bbb','ccc','ddd']
    stu2.run() // lxx running~
    

    缺点:父类所有引用类型属性被所有子类共享,修改一个子类的引用属性,其他子类也会受影响;子类在实例化时不能给父类构造函数传参;继承的属性打印看不到。

  2. 盗用构造函数继承
    在子类构造函数中调用父类构造函数,使用 call()apply() 方法以新创建的对象为上下文执行构造函数。

    // 父类
    function Person(name,firends) {
      this.name = name;
      this.friends = friends
      this.run = function() {
        console.log(this.name + ' running~')
      }
    }
    Person.prototype.eating = function() {
      console.log(this.name + ' eating~')
    }
    
    // 子类
    function Student(name,friends,sno) {
      Person.call(this, name, friends)
      this.sno = sno
    }
    
    var stu1 = new Student('lxx',['aaa','bbb'],1)
    stu1 // {name: 'lxx', friends: Array(2), sno: 1, run: ƒ}
    stu1.run() // lxx running~
    stu1.friends.push('ddd')
    stu1.friends // ['aaa','bbb','ddd']
    stu1.eating() // stu1.eating is not a function
    
    var stu2 = new Student('xxx',['ccc'],2)
    stu2 // {name: 'xxx', friends: Array(1), sno: 2, run: ƒ}
    stu2.run() // xxx running~
    stu2.friends // ['ccc']
    stu2.eating() // stu2.eating is not a function
    
    stu1.run === stu2.run // false
    

    优点:可以在子类构造函数中向父类构造函数传参;父类的引用类型属性不会共享。 缺点:必须在构造函数中定义方法,且方法不能重用;子类不能访问父类原型上的方法。

  3. 组合式继承
    原型链继承+盗用构造函数继承。基本思路是使用原型链继承方式继承原型上的属性和方法,而通过盗用构造函数方式继承实例属性。

    // 父类
    function Person(name,friends) {
      this.name = name
      this.friends = friends
    }
    
    Person.prototype.run = function (){
      console.log(this.name + ' running~')
    }
    
    // 子类
    function Student(name,friends,sno) {
      // 通过盗用父类构造函数构造属性,this是Student的实例对象
      Person.call(this, name, friends) // 第一次调用父类构造函数
      this.sno = sno
    }
    
    // 通过原型链继承父类方法
    Student.prototype = new Person() // 第二次调用父类构造函数
    // 修复子类 constructor 指向
    Student.prototype.constructor = Student
    
    var stu1 = new Student('lxx',['aaa','bbb','ccc'],1) 
    stu1.name // lxx 
    stu1.friends.push('ddd') 
    stu1.friends // ["aaa", "bbb", "ccc", "ddd"] 
    var stu2 = new Student('xxx',['ddd'],2) 
    stu2.name // xxx 
    stu2.friends // ["ddd"]
    

    💣 盗用父类构造函数构造属性时,要把 call 放置到最前面,因为父类构造函数和子类构造函数如果发生属性重名,父会覆盖子。

    💣 为什么明明是 new Student() 出来的实例,结果 constructor 确指向 Person ?因为 Student() 中盗用了构造函数 Person(),所以要修复指向。

    实例的方法继承自原型对象上的同一个方法,实现了方法的重用。

    stu1.run === stu2.run // true
    

    缺点:调用多次父类构造函数,第一次在子类构造函数内部,第二次在创建子类构造函数原型对象时;子类实例的原型对象上会多出一些没有必要的属性。

  4. 寄生式继承

    var personObj = {
      running() {
        console.log('running')
      }
    }
    
    function createStudent(name) {
      var stu = Object.create(personObj)
      stu.name = name
      stu.studying = function () {
        console.log('studying')
      }
      return stu
    }
    
    var s1 = createStudent('xxx')
    s1.running()
    
    var s2 = createStudent('lxx')
    s2.running()
    

    缺点:创建的对象没有明确的类型;createStudent 中的函数重复创建。

  5. 👍 寄生组合式继承
    业界公认,最有效最完整的继承方式
    红色代码为第三方函数(原型继承函数)创建方式一:直接创建构造函数

// 父类
function Person(name,age) {
  this.name = name
  this.age = age 
}

// 子类
function Student(num, name, age) {
  // 一、盗用父类构造函数构造属性
  Person.call(this, name, age)
  this.num = num
}

// 二、借助原型链继承和寄生式继承来继承方法
// 1、创建一个第三方构造函数
function Temp() {}
// 2、把该构造函数的原型对象指向父构造函数的原型对象
Temp.prototype = Person.prototype
// 3、创建一个实例
var temp = new Temp()
// 4、设置子构造函数的原型对象为一个实例对象
Student.prototype = temp
// 5、修复constructor指向
Student.prototype.constructor = Student

// 这里一定要放在以上步骤后面,放在前面原型对象被修改了,无法访问!
Student.prototype.run = function() {
  console.log('running~')
}

var stu = new Student('001','lxx',18) 
console.log(stu) 
stu.run()

--.jpg

寄生组合式继承,第三方函数(原型继承函数)创建方式二:Object.create()

function Person(name,age,friends) {
  this.name = name
  this.age = age
  this.friends = friends
}

Person.prototype.running = function () {
  console.log('running')
}

function Student(name,age,friends,sno,score) {
  // 盗用父类构造函数构造属性
  Person.call(this,name,age,friends)
  this.sno = sno
  this.score = score
}

// 以Person.prototype 为原型创建一个新对象,并赋值给Student.prototype
Student.prototype = Object.create(Person.prototype)
// 修复constructor指向
Object.defineProperty(Student.prototype,'constructor',{
  value:Student,
  enumerable:false,
  writable:true,
  configurable:true
})

Student.prototype.studying = function () {
  console.log('studying')
}

var s1 = new Student('lxx',25,['aaa','bbb'],100,100)
console.log(s1)

s1.running() // running
s1.studying() // studying

var s2 = new Student('xxx',29,['xxx','yyy'],110,80)

s1.friends.push('ccc')
s2.friends.push('zzz')

👍 寄生组合式继承,封装原型继承函数

function inheritPrototype(subtype,supertype) {
  subtype.prototype = Object.create(supertype.prototype)
  Object.defineProperty(subtype.prototype,'constructor',{
    value:subtype,
    enumerable:false,
    writable:true,
    configurable:true
  })
}

function Person(name,age,friends) {
  this.name = name
  this.age = age
  this.friends = friends
}

Person.prototype.running = function () {
  console.log('running')
}

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
}

// 使用封装的继承函数
inheritPrototype(Student,Person)

Student.prototype.studying = function () {
  console.log('studying')
}

var s1 = new Student('lxx',25,['aaa','bbb'],100,100)
console.log(s1)

s1.eating() // eating
s1.running() // running
s1.studying() // studying

var s2 = new Student('xxx',29,['xxx','yyy'],110,80)

s1.friends.push('ccc')
s2.friends.push('zzz')

原型继承函数除了 Object.create() 方式,还有其他方式:

原型继承函数

将某个对象作为新创建对象的原型。

var obj = {
  name:'lxx',
  age:25
}

// 方式一
function createObject(o) {
  function Foo() {}
  Foo.prototype = o
  var newObj = new Foo()
  return newObj
}

var info = createObject(obj)
console.log(info) // {}
console.log(info.__proto__) // obj

// 方式二
function createObject(o) {
  var newObj = {}
  Object.setPrototypeOf(newObj,o)
  return newObj
}

var info = createObject(obj)
console.log(info) // {}
console.log(info.__proto__) // obj

// 方式三 ES6
var info = Object.create(obj)
console.log(info) // {}
console.log(info.__proto__) // obj