面向对象的特性——继承
面向对象有三大特性:封装、继承、多态
- 封装:我们前面将属性和方法封装到一个类中,可以称之为封装的过程;
eg:编写构造函数的过程可以称之为是一个封装的过程
function Person(name, age) {
this.name = name
this.age = age
this.eating = function() {
console.log(this.name + "在吃东西~")
}
}
继承
继承可以帮助我们将重复的代码和逻辑抽取到父类中,子类只需要直接继承过来使用即可。
原型链继承(在实践中不会使用,但后续优化的方法是基于它实现的)
// 父类: 公共属性和方法
function Person() {
this.name = "a"
}
Person.prototype.eating = function() {
console.log(this.name + " eating~")
}
// 子类: 特有属性和方法
function Student() {
this.sno = 111
}
Student.prototype = new Person()
Student.prototype.studying = function() {
console.log(this.name + " studying~")
}
var stu = new Student()
console.log(stu.name)
stu.eating()
当还没有继承前(上面代码去除Student.prototype = new Person()),父类和子类还是两个独立的函数:
当加上Student.prototype = new Person()后,两个函数在内存中发生的变化:
重写了Student的原型
- 让子类原型不再指向子类构造函数
- 子类原型上的属性被继承的属性所覆盖
(也是因为是重写了Student的原型,所以Student在原型上的方法不能写在Student.prototype = new Person()
之后,不然它会被继承的属性所覆盖)
- 其原型指针__proto__指向了父类的原型对象,这样子类就可以沿着原型链访问到父类的方法eating。
- 子类原型是父类实例,通过父类构造函数,子类原型继承了父类的属性,最终,子类继承了父类的方法和属性,所以子类原型对象中有属性name = ‘a’
原型链继承的弊端:
- 打印stu对象时,继承的属性看不到(因为继承的属性在原型上,而原型上的属性不可枚举)
父类的实例属性会被子类所有实例共享
- 如果该属性是基本类型值时则没有问题
- 如果这个对象是一个引用类型(比如数组),那么就会造成问题[ 修改实例1的该属性(比如向数组push一个新值),实例2也会跟着改变。]
- 不好将参数传给父类
2.借用构造函数继承
做法:在子类型构造函数的内部调用父类型构造函数,创建子类实例会执行子类的构造函数(含父类的构造函数),也就完成了继承
// 父类: 公共属性和方法
function Person(name, age, friends) {
// this指代的是stu
this.name = name
this.age = age
this.friends = friends
}
// 子类: 特有属性和方法
function Student(name, age, friends, sno) {
Person.call(this, name, age, friends) // 将上面的函数当成普通函数进行调用
this.sno = 111
}
var stu = new Student("a", 18, ["kobe"], 111)
console.log(stu)
解决原型链继承的弊端:
- 解决继承的属性打印不出来问题:因为借用构造函数进行继承在子类构造函数中调用父类的构造函数完成继承,与原型对象无关,所以打印出来的属性也是可枚举的
- 解决传参的问题 :通过apply()和call()方法给父类传参
- 解决共享的问题(实例与实例之间不会相互影响):把
this
指向改成了指向新的实例,所以就会把 Person里面的this
相关属性和方法赋值到新的实例上,而不是赋值到 Student 原型上面
虽然借用函数继承能解决原型链继承的问题,但它也带来了新的问题:
- 子类实例不能访问父类原型对象中的属性和方法,因为借用构造函数进行继承在子类构造函数中调用父类的构造函数完成继承,与原型对象无关,所以它是继承父类构造函数中的属性,而没有继承父类原型上的属性 (有人提出可以通过用
Student.prototype = Person.prototype
来解决,但是这样在子类原型上添加的方法也会加到父类的原型里面去,违背了面向对象的初衷) - 无法实现函数复用,由于 call 有多个父类实例的副本,性能损耗。
原型式继承
原型式继承是针对对象的一种继承
简单实现一下原型式继承(实现的是对象的继承)
// 目的:让info的原型指向obj对象
var obj = {
name: "a",
age: 18
}
var info = Object.create(obj)
console.log(info.__proto__) // {name: "a", age: 18}
// 上面代码的本质如下:
var obj = {
name: "a",
age: 18
}
// 原型式继承函数
function createObject(o) {
var newObj = {}
Object.setPrototypeOf(newObj, o) // 将传入的o作为newObj的原型
return newObj
}
var info = createObject(obj)
console.log(info.__proto__) // {name: "a", age: 18}
寄生式继承(存在工厂函数一样的弊端,了解即可)
原型式继承 + 工厂函数
var personObj = {
running: function() {
console.log("running")
}
}
// 工厂函数
function createStudent(name) {
var stu = Object.create(personObj) // 原型式继承
stu.name = name
stu.studying = function() {
console.log("studying~")
}
return stu
}
var stuObj = createStudent("why")
var stuObj1 = createStudent("kobe")
var stuObj2 = createStudent("james")
寄生组合式继承(最终方案)
我们再来回顾一下我们的目的:子类要继承父类的原型且往子类添加属性或方法不影响到父类
// 工具函数: 实现封装(原型式继承 + 指定子类的constructor为自身)
function inheritPrototype(SubType, SuperType) {
SubType.prototype = Object.create(SuperType.prototype)
Object.defineProperty(SubType.prototype, "constructor", {
enumerable: false,
configurable: true,
writable: true,
value: SubType
})
}
// 要继承的父类
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~")
}
参考:
https://juejin.cn/post/6934498361475072014
https://www.jianshu.com/p/0045cd01e0be