继承是面向对象的三大特性之一,在JS
中实现继承就是通过原型与原型链来实现,
原型链
对象在查找属性或方法时,在自己身上找不到时,就会沿着当前对象的原型对象(隐式原型)进行查找,直到查找到最顶层的原型对象为null
的对象,这个由原型串联起来的链就是原型链。
例如创建了一个空对象obj
,这时候如果使用.
操作符获取属性age
时,获取到的就是undefined
const obj = {}
console.log(obj.age) // undefined
因为不管是在obj本身还是原型链上都不能找到
age
通过这种字面量形式创建的对象,实际上是一种语法糖,它等于
new Object()
,根据之前new
关键字所做的操作可以验证(其他的js数据类型也一样)console.log(obj.__proto__ === Object.prototype) // true
这时候如果通过__proto__
给obj
的原型对象添加一个age
属性,再次获取age
属性时,就可以成功拿到
obj.__proto__.age = 111
console.log(obj.age) // 111
需要注意的是,如果是set
操作,他会在自己身上添加该属性,而不会去查找原型链
const obj = {}
// 给原型对象添加一个age
obj.__proto__.age = 111
// obj本身添加一个age
obj.age = 222
console.log(obj.age, obj.__proto__.age) // 222 111
// 可以看见它并没有修改原型对象上的age,而是给自己添加了一个age
接下来瞅瞅如何通过原型链来实现继承
实现继承
创建Teacher
与Student
两个对象
function Teacher(name, age, course) {
this.name = name
this.age = age
this.course = course
}
Teacher.prototype.eating = function () {
console.log("eating")
}
Teacher.prototype.say = function () {
console.log("say")
}
Teacher.prototype.teaching = function () {
console.log("teaching")
}
function Student(name, age, snum) {
this.name = name
this.age = age
this.snum = snum
}
Student.prototype.eating = function () {
console.log("eating")
}
Student.prototype.say = function () {
console.log("say")
}
Student.prototype.study = function () {
console.log("studying")
}
可以看见这两个对象有许多相同的属性与方法,那我们可以创建一个Person
类,然后让上面两个对象继承于Person
,接下来看看怎样来实现继承
继承(方法)
// Person 类
function Person() {}
Person.prototype.eating = function () {
console.log("eating")
}
Person.prototype.say = function () {
console.log("say")
}
错误方式
将父类的显示原型对象赋值给子类
Teacher.prototype = Person.prototype
这样做,子类确实拥有了父类的方法,但是现在这种情况,子类与父类的显式原型都指向了同一个对象,如图所示
所以当给子类添加方法时,该方法也会添加到父类上,这样这种方法就不可取
正确方式
我们知道当我们使用new
关键字实例化对象的时候,会将构造函数的显式原型赋值给新对象的隐式原型,我们可以利用这点以及原型链的查找规则,可以实例化一个父类,然后将该对象赋值给子类的显式原型对象
Teacher.prototype = new Person()
这样子类在继承了父类方法的同时,也不会在为自己添加方法时而影响到父类了,但是目前也只是继承了方法而已,关于属性的继承还无法实现。
继承(属性)
属性的继承,通过借用构造函数来实现形如
function Teacher(name, age, course) {
Person.call(this, name, age)
this.course = course
}
在子类的构造函数中调用父类的构造函数并显式的将this
绑定进去,这样就可以完成属性的继承了
组合式继承
我们将上面继承方法与属性的方式结合起来,就能够实现继承,这种方式叫组合式继承
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.eating = function () {
console.log("eating")
}
Person.prototype.say = function () {
console.log("say")
}
function Teacher(name, age, course) {
// 借用构造函数继承属性
Person.call(this, name, age)
this.course = course
}
// 继承方法
Teacher.prototype = new Person()
通过这种方式,就实现了继承,但是该方法还具有以下缺点
- 会调用两次父类构造函数
- 拥有了两份父类属性
- 本身存在一份
- 原型上有一份
为了解决上面的问题,所以有了寄生组合式继承
寄生组合式继承
上面继承父类方法的方式,其实目的就是创建一个新对象,而这个对象的原型指向父类的显式原型,所以还可以通过以下方法实现
function createObject(obj) {
const newObj = {}
Object.setPrototypeOf(newObj, obj)
// 将 obj 作为 newObj 的隐式原型对象
return newObj
}
我们调用该方法,并传入Person
的原型对象
const obj = createObject(Person.prototype)
console.log(obj.__proto__ === Person.prototype) // true
可以看见现在的obj
对象的原型就指向了Person
的显式原型对象,这样就实现了方法的继承并且没有多余的属性,而属性的继承还是和刚才一样借用构造函数
function Teacher(name, age, course) {
Person.call(this, name, age)
this.course = course
}
const obj = createObject(Person.prototype)
Teacher.prototype = obj
const t = new Teacher("sakurige",22,"math")
console.log(t)
现在除了自己身上有一份属性以外,原型上是没有的了,这样大致继承效果就完成了,不过还有一个constructor
属性需要创建一下,所以最后的继承应该是这样的
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.eating = function () {
console.log("eating")
}
Person.prototype.say = function () {
console.log("say")
}
function Teacher(name, age, course) {
Person.call(this, name, age)
this.course = course
}
function createObject(obj) {
const newObj = {}
Object.setPrototypeOf(newObj, obj)
return newObj
}
function inherit(SubType, SuperType) {
SubType.prototype = createObject(SuperType.prototype) // 子类原型指向父类
Object.defineProperty(SubType.prototype, "constructor", { // 定义 constructor 属性
enumerable: false,
configurable: true,
writable: true,
value: SubType,
})
}
inherit(Teacher, Person)