对象(一)--对象的继承

聊一聊JavaScript中的对象的继承

前言

在前一篇对象的创建中,文中提到过原型模式,其实呢在JavaScript中对象的继承就是以原型模式作为基础的。
每个构造函数都有一个prototype属性指向的它的原型对象,而构造函数创建的实例对象可以顺势访问到原型对象中的属性

原型继承

原型对象,它也可以是另外一个构造函数的实例对象,因此它就可以访问更高层次的原型对象的属性,这样原型对象与原型对象以某种形式相连就构成了一条原型链, 而继承其实就是依赖于原型链。
打个比方PersonStudent两个类,根据常识Student肯定是Person的一个子集,即Person具有的属性和方法Student按道理都应该有,那么我们在已经有Person的基础上不应该再在Student中编写重复的代码,而是用Student去继承Person来实现代码的复用

function Person(name="name", age="20") {
  this.name = name 
  this.age = age
  this.hobbies = ['readBooks', 'swimming']
}
Person.prototype.sayHello = function() {
  console.log(`Hello, my name is ${this.name}`)
}

function Student(homework) {
  this.homework = homework
}
<!-- 将Student的原型对象去引用Person的实例化对象 -->
Student.prototype = new Person()

Student.prototype.doHomework = function() {
  console.log(`i am doing ${this.homework} homework`)
}

let stu1 = new Student('math')
console.log(!!(stu1.name && stu1.age && stu1.sayHello)) // true

我们可以发现是在Student的实例化对象可以顺着原型链(绿色)找到这几个属性,nameage分布在Student.prototype中, sayHello分布在Person.prototype中,参照如下图
原型链图

另外,构造函数的原型对象都继承自Object.prototype, 这就是为什么所有对象都具有toString 等方法

原型继承通过将原型对象赋值为一个父类的实例对象来实现继承,实质就是原型链的搭建

需要注意的地方是在将原型对象重新赋值后,它的constructor 需要手动指正

另外大家有没有注意到在上面这个例子中name, age 等属性都包含在同一个对象中,也就是说所有的子类对象在访问这些属性的时候实际上都是同一个值,如若它们是引用类型,任何实例对象对其的修改都会在其他对象访问时得到反映

let stu1 = new Student()
console.log(stu1.hobbies) // ['readBooks', 'swimming']
stu1.hobbies.push('running')
let stu2 = new Student()
console.log(stu2.hobbies) // ['readBooks', 'swimming', 'runnning']

并且在创建子类型的实例时不能对父类型的构造函数传参,这也是一个问题

借用构造函数组合继承

为了解决以上两个问题:

  1. 所有子类型实例访问父类型属性时其实都是在访问原型属性
  2. 在创建子类型的实例时不能对父类型的构造函数进行传参
    借用构造函数闪亮登场了
function Student(name, age, homework) {
  // 使用call()(or apply())方法绑定this
  Person.call(this, name, age)
  this.homework = homework
}

// 相当于下面
function Student(name, age, homework) {
  this.name = name
  this.age = age
  this.homework = homework
}

<!-- 将Student的原型对象去引用Person的实例化对象 -->
Student.prototype = new Person()

这样将属性重新定义在子类的实例上,方法还是放置在原型链上,很好地解决了以上两个问题, 自身的nameage遮蔽了原型链上的nameage

还要一个问题就是子类型已经可以创建父类属性了,原型对象还是会再次创建这些属性,在这里Student.prototype对象中nameage依旧会被创建,因为调用了一个Person的构造函数, 但是nameage在原型对象中已经是多余的了

直接继承prototype

之前将父级构造函数的实例赋值给子构造函数的原型对象Student.prototype = new Person(), 会造成属性在原型对象中的冗余
这次我们选择直接继承父构造函数的原型对象

Student.prototype = Person.prototype

避免了不必要的属性定义在原型对象中,并且少了一次构造函数的调用节省了内存
但是又有一个问题暴露出来了,就是此时子构造函数的原型对象和父构造函数的原型对象同时指向一个对象, 其中任何一个添加修改属性都会影响对方
比如上面,当我们想要指正Student.prototypeconstructor时,Person.prototypeconstructor也发生了改变

Student.prototype.constructor = Student
console.log(Perosn.prototype.constructor) // Student

寄生组合式继承

利用空对象作为中介

function object(o) {
  function F() {}
  F.prototype = o
  return new F()
}
// 相当于ES5 Object.create()

function Student(name, age, homework) {
  Person.call(this, name, age)
  this.homework = homework
}

Student.prototype = object(Person.prototype)

寄生组合式继承

既避免的父级构造函数的多余调用,又将父子构造函数的原型对象区分开来,且空对象的占有的内存很少,因此寄生组合式继承棒棒的!

小结

JavaScript主要通过原型链实现继承,原型链的构建是通过将一个类型的实例赋值给另一个构造函数的原型实现。这样,子类型就能够访问父类型的所有属性和方法。原型链的问题是对象实例共享所有继承的属性和方法,因此不适宜单独使用。解决这个问题的技术是借用构造函数,即在子类型构造函数的内部调用超类型的构造函数。这样就可以做到每个实例都有自己的属性。使用最多的继承模式是组合继承,即使用原型链继承共享的属性和方法,而通过借用构造函数继承实例属性

但是,寄生组合式继承是实现基于类型继承的最有效方式

posted @ 2018-07-14 21:40  Guanine  阅读(188)  评论(0编辑  收藏  举报