写在前面
面向对象有三大特征:封装,继承,多态,封装我们在上篇文章中将属性和方法封装到一个类里面就可以称之为是一个封装的过程,继承是面向对象中非常重要的,不仅仅可以减少重复代码的数量,也是多态的前提(纯面向对象中),多态指的是不同的对象在执行时候表现出不同的形态,这篇文章我们主要是使用 ES5 的语言通过原型链来实现继承的功能,如果你对原型和原型链不了解,请先看下JavaScript
系列中的原型和原型链相关文章好了 🫨,老规矩,废话不多说,让我们开始吧!
我们在对 ES5 语法实现继承的学习中将会按照如上思维导图的顺序来讲解,分别包括原型链实现的继承
借用构造函数继承
寄生组合实现继承
对象的方法补充
四个部分内容。
一.利用原型链实现继承
对于继承而言其实我们主要关注两方面的继承,一个是属性的继承,另外一个是方法的继承,我们使用原型链来实现一下。
方式一:父类原型直接赋值给子类的原型
js
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.running = function () {
// 通过函数的原型来实现函数
console.log('running~')
}
Person.prototype.eating = function () {
console.log('eating~')
}
// 定义学生类
function Student(name, age, sno, score) {
this.name = name
this.age = age
this.sno = sno
this.score = score
}
Student.prototype = Person.prototype
Student.prototype.studying = function () {
console.log('studying~')
}
// 创建学生对象
let stu1 = new Student('芒果', 12, '12345', 98)
stu1.running()
当我们想要实现继承,我们可以利用原型直接将父类的原型赋值给子类的原型,然后子类就拥有了父类的原型对象,进而可以使用父类的属性和方法,在一定程度上就实现了继承,但是这种继承的方式是有缺点的,直接将父类的原型对象赋值给了子类的原型对象这样会造成,父类和子类的原型对象是共用的更改一方另外一方也会修改。
方式二 :创建一个父类的实例对象(new Person
)用这个实例对象来作为子类的原型对象。
js
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.running = function () {
console.log('running~')
}
Person.prototype.eating = function () {
console.log('eating~')
}
// 定义学生类
function Student(name, age, sno, score) {
this.name = name
this.age = age
this.sno = sno
this.score = score
}
Student.prototype = new Person() // 将父类的实例化对象作为子类的原型对象。
Student.prototype.studying = function () {
console.log('studying~')
}
// 创建学生对象
let stu1 = new Student('芒果', 12, '12345', 98)
stu1.running()
这个方式就可以解决方式一中的共用原型对象的问题了,但是不足之处在于我们并没有将父类的属性继承过来,必须使用子类的属性,需要注意的是必须再将父类的实例化对象赋值给子类原型对象后再进行子类自己属性方法的实现,他的内存图如下:
二.原型链继承的弊端
虽然我们在上面的代码中实现了原型链的继承,但是这样其实有很大的弊端,某些属性其实是保存在 P 对象上的
第一 :我们直接打印对象是看不到name
和age
属性的,因为加入我么将上述的代码更改为下面的代码其实当我们去通过[[get]]
进行获取的时候其实是可以获取到对应的属性的,但是我们直接打印实例化对象的时候是看不到对应的属性的,这个非常好理解,因为对于属性的查找其实name
和age
是在原型链上查找的。
第二 :属性name
和age
会被多个对象共享,如果这个对象是一个引用类型,那么就会造成问题,和上述的问题一样当我们将this.name
和this.age
进行删除的时候我们在实例化对象中是无法查询到对应的属性的,这样就没有自己的属性了,但是如果我们在子类中增加对应的属性的话其实这些属性是重复的。
第三:不能给 Person 传递参数(让每个 stu 有自己的属性)因为这个对象是一次性创建的(没办法定制)
三.借用构造函数继承
其实我们主要面临的问题是属性继承的问题,我么使用原型链的继承可以来解决方法的继承问题,但是属性的继承总会有问题,直接使用父类的属性,我们在子类打印子类实例化对象的时候看不到使用父类的属性,如果不使用父类的属性的话,在子类自己编写又会造成重复的问题,在 JavaScript 社区中经过了长期的总结,提出了借用构造函数的继承方式,也叫做组合继承,他的代码实现如下:
js
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.running = function () {
console.log('running~')
}
Person.prototype.eating = function () {
console.log('eating~')
}
// 定义学生类
function Student(name, age, sno, score) {
Person.call(this, name, age) // 借用父类的构造函数
this.sno = sno
this.score = score
}
Student.prototype = new Person()
Student.prototype.studying = function () {
console.log('studying~')
}
// 创建学生对象
let stu1 = new Student('芒果', 12, '12345', 98)
console.log(stu1) // Person { name: '芒果', age: 12, sno: '12345', score: 98 }
console.log(stu1.name, stu1.age) // 芒果 12
当然其实借用构造函数的继承方式依然存在一点问题,如果你理解到这里,点到为止,那么组合实现继承只能说问题不大,但是它依然不是特别的完美,但是基本已经没有问题了,那么不完美的地方是什么哪?其实我们可以看到上述的父类被实例化了两次,所有的子类实例实际上会拥有两份父类属性,一份在原型对象上面,一份在类自身。
四.寄生组合继承
原型式继承的渊源,这种模式要从道格拉斯.克罗克福德
(著名的前端大师,JSON 的创立者)在 2006 年写的一篇文章说起Prototypal Inheritance in JavaScript
(在 JavaScript 中使用原型式继承),在这篇文章中它介绍了一种继承方法,而且这种继承方法不是通过构造函数来实现的,从上述的内存图我们可以看出来实现继承我们需要创建一个p
对象,那么既然这样我们就来总结一下这个对象的特点:
- 必须创建出来一个对象
- 这个对象的隐式原型必须指向父类的显式原型
- 将这个对象赋值给子类的显式原型
js
function object(o) {
function F() {}
F.prototype = 0
return new F()
}
function inheritPrototype(subType, superType) {
subType.prototype = object(superType.prototype)
subType.prototype.constructor = subType
}
inheritPrototype(Student, Person)
上述的代码是一种兼容性写法,如果担心代码可能出现兼容性问题的时候可以使用这种方式,如果不担心兼容性问题的时候其实可以直接这样写
js
function inherit(SubType, SuperType) {
SubType.prototype = Object.create(SuperType.prototype)
}
一般情况下我们会将这个代码进行封装起来在使用的时候直接进行调用,那么我们通过 ES5 实现继承的最终写法就是如下的代码。
js
function object(o) {
function F() {}
F.prototype = 0
return new F()
}
function inheritPrototype(subType, superType) {
subType.prototype = object(superType.prototype)
subType.prototype.constructor = subType
}
js
import inherit from './inherit.js'
function Person(name, age, height) {
this.name = name
this.age = age
this.height = height
}
Person.prototype.running = function () {
console.log('running~')
}
Person.prototype.eating = function () {
console.log('eating~')
}
function Student(name, age, height, sno, score) {
Person.call(this, name, age, height)
this.sno = sno
this.score = score
}
inherit(Student, Person)
Student.prototype.studying = function () {
console.log('studying~')
}
// 创建实例对象
let stu1 = new Student('芒果', 12, 123, 4444, 56)
五.对象方法的补充
hasOwnProperty :对象是否有某一个属于自己的属性(不是在原型上的属性)
js
let obj = {
name: '芒果',
age: 12,
}
obj.__proto__ = {
message: 'msg~',
}
console.log(obj.hasOwnProperty('name')) // true
console.log(obj.hasOwnProperty('message')) // false
in/for in 操作符:判断某个属性是否在某个对象或者对象的原型上
js
let obj = {
name: '芒果',
age: 12,
}
obj.__proto__ = {
message: 'msg~',
}
console.log('name' in obj) // true
console.log('message' in obj) // true
instanceof :用于检测构造函数(Person、Student 类)的 pototype,是否出现在某个实例对象的原型链上,本质上这个方法的比较是通过判断p.__proto__.constructor
指向的是否是后边的这个构造函数,如果是就返回true
否则就返回false
js
function Person(name, age) {
this.name = name
this.age = age
}
let p = new Person('招财', 23)
console.log(p instanceof Person) // true
isPrototypeOf :用于检测某个对象,是否出现在某个实例对象的原型链上,主要用来判断前边的是否在后边的原型链上,功能比较强大,不仅仅可以用来判断父类,还可以判断对象的上下级关系,但是平时开发使用的比较少。
js
function Person(name, age) {
this.name = name
this.age = age
}
let p = new Person('招财', 23)
console.log(Person.prototype.isPrototypeOf(p)) // true
然后我们再创建一个对象,来使用下这个方法,你会发现这个方法用来判断对象的上下级关系也是可以的。
js
let p = {
name: 'aaa',
age: 12,
}
function object(o) {
function F() {}
F.prototype = 0
return new F()
}
let p2 = object(p)
console.log(p.__proto__.isPrototypeOf(p2)) // true
六.总结与扩展
这篇文章我们到这里就结束了,这篇文章我们介绍了晦涩难懂的 ES5 继承的内容,我们首先通过原型链进行继承,并且展示了通过原型链继承的缺陷有哪些,之后我们通过了寄生组合继承来解决了这些问题,其实在 ES6 中我们直接使用extends
关键字来实现继承通过babel
转换之后的核心代码也是类似的,当然我们目前在平时的开发中一般不会直接写这些代码,但是 ES5 的继承是需要我们去理解的。
扩展知识: 其实道格拉斯.克罗克福德
最初使用这个function object
这个方法主要是来创建上下级对象的,我们在上述的内容中也进行了使用,但是其实这个方法也不完美,因为如果上级的对象中有引用数据类型,比如方法,就会造成方法共享的问题。