对象(一)--对象的继承
聊一聊JavaScript中的对象的继承
前言
在前一篇对象的创建中,文中提到过原型模式,其实呢在JavaScript中对象的继承就是以原型模式作为基础的。
每个构造函数都有一个prototype
属性指向的它的原型对象,而构造函数创建的实例对象可以顺势访问到原型对象中的属性
原型继承
原型对象,它也可以是另外一个构造函数的实例对象,因此它就可以访问更高层次的原型对象的属性,这样原型对象与原型对象以某种形式相连就构成了一条原型链
, 而继承其实就是依赖于原型链。
打个比方Person
和Student
两个类,根据常识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
的实例化对象可以顺着原型链(绿色)找到这几个属性,name
和age
分布在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']
并且在创建子类型的实例时不能对父类型的构造函数传参,这也是一个问题
借用构造函数组合继承
为了解决以上两个问题:
- 所有子类型实例访问父类型属性时其实都是在访问原型属性
- 在创建子类型的实例时不能对父类型的构造函数进行传参
借用构造函数闪亮登场了
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()
这样将属性重新定义在子类的实例上,方法还是放置在原型链上,很好地解决了以上两个问题, 自身的name
和age
遮蔽了原型链上的name
和age
还要一个问题就是子类型已经可以创建父类属性了,原型对象还是会再次创建这些属性,在这里Student.prototype
对象中name
和age
依旧会被创建,因为调用了一个Person
的构造函数, 但是name
和age
在原型对象中已经是多余的了
直接继承prototype
之前将父级构造函数的实例赋值给子构造函数的原型对象Student.prototype = new Person()
, 会造成属性在原型对象中的冗余
这次我们选择直接继承父构造函数的原型对象
Student.prototype = Person.prototype
避免了不必要的属性定义在原型对象中,并且少了一次构造函数的调用节省了内存
但是又有一个问题暴露出来了,就是此时子构造函数的原型对象和父构造函数的原型对象同时指向一个对象, 其中任何一个添加修改属性都会影响对方
比如上面,当我们想要指正Student.prototype
的constructor
时,Person.prototype
的constructor
也发生了改变
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
主要通过原型链实现继承,原型链的构建是通过将一个类型的实例赋值给另一个构造函数的原型实现。这样,子类型就能够访问父类型的所有属性和方法。原型链的问题是对象实例共享所有继承的属性和方法,因此不适宜单独使用。解决这个问题的技术是借用构造函数,即在子类型构造函数的内部调用超类型的构造函数。这样就可以做到每个实例都有自己的属性。使用最多的继承模式是组合继承,即使用原型链继承共享的属性和方法,而通过借用构造函数继承实例属性
但是,寄生组合式继承是实现基于类型继承的最有效方式