什么是继承
- 继承是面向对象语言的三大特性之一:封装,继承,多态;
- 关于继承MDN的解释是:
JavaScript对象是动态的属性“包”(指其自己的属性)。JavaScript对象有一个指向一个原型对象的链。当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。 (发现看不大懂,我刚接触js的时候,看这个解释也是一脸懵逼)
- 继承是子类可以用父类的方法和属性的一个属性,帮助我们将重复的代码和逻辑抽取到父类中,子类只需要直接继承过来就好,这个属性基于原型链,那什么是原型链呢?
什么是原型及原型链
浏览器给每个对象都赋予了一个属性__proto__,这个属性指向的东西就是原型(也叫原型对象),原型对象也有原型对象,依次不断向上,就构成了原型链
函数也是一个对象,作为对象,他有__proto__属性,指向一个constructor属性是Function函数的对象,作为一个函数,他有一个显式原型prototype指向一个constructor属性是本身的对象,除了Function函数以外的函数,Student.propotype === Student.__proto__为false(这里的student为一个构造函数)
Function函数是个例外,即:Function.propotype === Function.__proto__为true;
function Person() {
}
console.log('1', Person.prototype.__proto__) // 顶层原型对象
console.log('2', Object.getOwnPropertyDescriptors(Person.__proto__))
console.log('3', Object.getOwnPropertyDescriptors(Person.prototype))
console.log('4', Object.propotype === Object.__proto__)
console.log('5', Function.prototype === Function.__proto__); // true 特殊情况
结果如下:
1 [Object: null prototype] {}
2 {
length: { value: 0, writable: false, enumerable: false, configurable: true },
name: { value: '', writable: false, enumerable: false, configurable: true },
arguments: {
get: [Function (anonymous)],
set: [Function (anonymous)],
enumerable: false,
configurable: true
},
caller: {
get: [Function (anonymous)],
set: [Function (anonymous)],
enumerable: false,
configurable: true
},
constructor: {
value: [Function: Function],
writable: true,
enumerable: false,
configurable: true
},
apply: {
value: [Function: apply],
writable: true,
enumerable: false,
configurable: true
},
bind: {
value: [Function: bind],
writable: true,
enumerable: false,
configurable: true
},
call: {
value: [Function: call],
writable: true,
enumerable: false,
configurable: true
},
toString: {
value: [Function: toString],
writable: true,
enumerable: false,
configurable: true
},
[Symbol(Symbol.hasInstance)]: {
value: [Function: [Symbol.hasInstance]],
writable: false,
enumerable: false,
configurable: false
}
}
3 {
constructor: {
value: [Function: Person],
writable: true,
enumerable: false,
configurable: true
}
}
4 false
5 true
原型链和原型对象存在的意义
举个例子,我们要创建对象: student, 包含名字,年龄,性别等属性,跑步,唱歌等方法(你可以先思考一下创建对象的几个方法,先思考,再往看哦) 有以下3个方法:
- 字面量创建(最常见)
student = {
name: '李四',
age: 18,
gender: '男',
singing: function() {
console.log(`${this.name}在唱歌`)
}
running: function() {
console.log(`${this.name}在跑步`)
}
}
student = {
name: '王二',
age: 17,
gender: '男',
singing: function() {
console.log(`${this.name}在唱歌`)
}
running: function() {
console.log(`${this.name}在跑步`)
}
}
// 但是发现,当学生数据量多了,就会有很多重复的代码
- 工厂模式创建
function creatStudent(name, age, gender) {
let p = {};
p.name = name;
p.age = age;
p.gender = gender;
p.singing = function() {
console.log(`${name}在唱歌`);
};
p.running = function() {
console.log(`${name}在跑步`);
};
return p;
}
const student1 = creatStudent('李四', 18, '男');
const student2 = creatStudent('王二', 17, '男');
// console.log(Object.getOwnPropertyDescriptors(student1.__proto__)); // 这个时候student的构造器就是Object
// 这个时候可以批量创造对象了,对对象进行了封装,但是没有解决对象标识问题,就是创建的对象是什么类型,创造的对象应该都有一个共同的来源,但是这里没有显示出来
- 构造函数创建
function Student(name, age, gender) {
this.name = name;
this.age = age;
this.gender = gender;
this.singing = function() {
console.log(`${name}在唱歌`);
};
this.running = function() {
console.log(`${name}在跑步`);
};
}
const student1 = new Student('李四', 18, '男'); // new关键字的作用是创建一个对象,将该对象的__proto__赋值给Student的一个显式对象prototype,这个就是原型对象;
const student2 = new Student('王二', 17, '男');
// console.log(Object.getOwnPropertyDescriptors(p1.__proto__)); // 这个时候student1的构造器是Student, 我们知道他的来源就是Student,父类就是Student;
// 但是这样创造对象也有缺点,就是每创造一个对象,都会创建一个单独的函数方法,不太好,这时候,我们就可以把以上代码改为一下:
function Student(name, age, gender) {
this.name = name;
this.age = age;
this.gender = gender;
}
Student.prototype.running = function() {
console.log(this.name + " running~")
}
Student.prototype.singing = function() {
console.log(this.name + " running~")
}
const student1 = new Student('李四', 18, '男');
const student2 = new Student('王二', 17, '男');
// 这个时候,不会每次创建对象,都重复创建一次相同的方法,造成内存浪费
为什么需要继承
以上利用在构造函数原型上增加方法来减少内存浪费,就是为什么需要‘继承’的原因,可以复用相同的方法和属性,student1和student2从Student的原型上继承了running和singing这两个方法;
当我们调用student1.singing()方法时,在student1上没有这个方法,就顺着student1的__proto__属性来找这个方法,直到找不到为止,这个就是‘原型链’;
常见继承方法
- 原型链继承 当我们想在一个对象student上使用另一个对象person的方法时,可以将这个对象person放到该对象student的原型链上
// 父类: 公共属性和方法,抽离复用的方法和属性,不涉及业务处理
function Person() {
this.name = "why"
this.friends = []
}
Person.prototype.eating = function() {
console.log(this.name + " eating~")
}
// 子类: 特有属性和方法,在子类中进行业务处理
function Student() {
this.sno = 111
}
var p = new Person()
Student.prototype = p
Student.prototype.studying = function() {
console.log(this.name + " studying~")
}
var stu = new Student()
// 原型链实现继承的弊端:
// 1.第一个弊端: 打印stu对象, 继承的属性是看不到的
// console.log(stu.name)
// 2.第二个弊端: 创建出来两个stu的对象
var stu1 = new Student()
var stu2 = new Student()
// 直接修改对象上的属性, 是给本对象添加了一个新属性
stu1.name = "kobe"
console.log(stu2.name)
// 获取引用, 修改引用中的值, 会相互影响
stu1.friends.push("kobe")
console.log(stu1.friends)
console.log(stu2.friends)
// 3.第三个弊端: 在前面实现类的过程中,不能向父类传递参数
var stu3 = new Student("lilei", 112)
- 借用(盗用)构造函数继承
function Person(name, friends) {
this.name = name
this.friends = friends
}
Person.prototype.eating = function() {
console.log(this.name + " eating~")
}
function Student(name, friends, sno) {
Person.call(this, name, friends) // 使用Person的属性
this.sno = sno
}
const stu1 = new Student('lisi', ['ming', 'zhangsan'], 111);
const stu2 = new Student('wanger', ['ming', 'zhangsan'], 222);
stu1.eating() // 不能调用
stu1.friends.push('hanmeimei')
console.log('stu1>>>>>', stu1, stu2);
// 解决了继承的属性看不到问题,原型共享导致的引用更改的问题,以及可以向父类函数中传递参数的问题
// 但是也有弊端
// 1.第一个弊端: Person函数至少被调用了两次
// 2.第二个弊端: stu的原型对象上会多出一些属性, 但是这些属性是没有存在的必要: stu.__proto__
// 3.不能调用父类原型上的方法
- 组合式继承
- 父类对象赋值给子类原型
function Person(name, friends) {
this.name = name
this.friends = friends
}
Person.prototype.eating = function() {
console.log(this.name + " eating~")
}
function Student(name, friends, sno) {
Person.call(this, name, friends) // 使用Person的属性
this.sno = sno
}
const stu1 = new Student('lisi', ['ming', 'zhangsan'], 111);
const stu2 = new Student('wanger', ['ming', 'zhangsan'], 222);
stu1.prototype = new Person() // 继承Person的方法
stu2.prototype = new Person() // 继承Person的方法
stu1.friends.push('hanmeimei')
console.log('stu1>>>>>', stu1, stu2);
// 1.第一个弊端: Person函数至少被调用了两次
// 2.第二个弊端: stu的原型对象上会多出一些属性, 但是这些属性是没有存在的必要: stu.__proto__
- 父类原型赋值给子类原型 在上一个的基础上完善
function Person(name, friends) {
this.name = name
this.friends = friends
}
Person.prototype.eating = function() {
console.log(this.name + " eating~")
}
function Student(name, friends, sno) {
Person.call(this, name, friends) // 使用Person的属性
this.sno = sno
}
const stu1 = new Student('lisi', ['ming', 'zhangsan'], 111);
const stu2 = new Student('wanger', ['ming', 'zhangsan'], 222);
const p = new Person()
stu1.prototype = p.__proto__ // 继承Person的方法
stu2.prototype = p.__proto__ // 继承Person的方法
stu1.friends.push('hanmeimei')
console.log('stu1>>>>>', stu1, stu2);
// 1.第一个弊端: Person函数至少被调用了两次
// 第二个弊端被解决了
// 严格意义上说,这个也属于借用(盗用)构造函数继承
- 原型式继承(可以关注一下
Object.create()的实现)
// 继承的目的:复用另一个对象的属性和方法
const person = {
friends: ['zhangshan', 'lihua'],
singing: function() {
console.log(`${this.name}在唱歌`)
}
}
const student1 = Object.create(person, {
name: {
value: 'zhangshan',
},
age: {
value: 18
},
}) // 第二个参数可以定义student1中的属性
const student2 = Object.create(person, {
name: {
value: 'buzhi',
},
age: {
value: 19
},
})
student1.friends.push('hanmeimei')
console.log('student>>>>>', student1, student2);
student1.singing()
// 缺陷一:student1中的friends改变,student2中的也会改变,Object.create()是一个浅拷贝的方法
// 缺陷二:当student中有重复的属性时,比如这里的age,name还是造成了代码冗余
- 寄生式继承(使用原型式继承对目标函数进行浅拷贝)
var person = {
singing: function() {
console.log(`${this.name}在singing`)
},
friends: ['zhangshan', 'lihua'],
}
function createStudent(name) {
var stu = Object.create(person)
stu.name = name
stu.studying = function() {
console.log(`${this.name}在studying~`)
}
return stu
}
var stu1 = createStudent("why") // 每调用一次这个函数,studing方法都会被创建一次
var stu2 = createStudent("kobe")
var stu3 = createStudent("james")
stu1.friends.push('lihua')
console.log('stu1>>>>>', stu1, stu2, stu3); // friends改变还是会造成其他friends改变
stu1.singing()
// 缺陷1: 每调用一次这个函数,studing方法都会被创建一次
// 缺陷2: 父类的引用属性是共享的
- 寄生组合式继承(寄生式继承和借用构造函数继承的组合)
function createObject(o) {
function Fn() {}
Fn.prototype = o
return new Fn()
}
function inheritPrototype(SubType, SuperType) {
// SubType.prototype = Object.create(SuperType.prototype)
SubType.prototype = createObject(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~")
}
var stu = new Student("why", 18, ["kobe"], 111, 100)
console.log(stu)
stu.studying() // studying~
stu.running() // running~
stu.eating() // eating~
console.log(stu.constructor.name) // Student
不断对继承方法优化,最终得到现在的继承方法,优点如下:(当然这里没说class,class继承其实就是寄生组合式的语法糖)
- 只调用一次父类构造函数
- 子类构造器可以向父类构造器传参
- 父类方法可以复用
- 父类的引用属性不会被共享