前端JS如何实现继承
构造函数的应用
当多个构造函数
需要使用一些共同的方法或者属性
的时候
我们需要把这些共同的东西拿出来, 单独书写一个构造函数
让其他的构造函数去继承
自这个公共的构造函数
概念
- 让
B
构造函数的实例能够使用A
构造函数的属性和方法 - 我们管
B
构造函数叫做A
构造函数的子类 - 我们关
A
构造函数叫做B
构造函数的父类
目的 : 让 B
构造函数能够使用 A
构造函数的属性和方法
继承前的准备:准备一个父类
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.sayHi = function () { console.log('hello world') }
那么此时我们new
出来一个Person
实例应该长这样:
Person 的实例 = {
name: 'Jack',
age: 18,
__proto__: { // Person.prototype
constructor: Person,
sayHi: function () { },
__proto__: Object.prototype
}
}
一、原型继承
利用改变
原型链
的方式来达到继承效果 直接把父类的实例当作子类的 prototype
父类在前面已经准备好了,我们现在定义一个子类
// 子类
function Student(gender) {
this.gender = gender
}
需求: 子类继承父类的name 、age 、sayHi
核心代码:
子类.prototype = new 父类
需求实现:Student.prototype = new Person('Jack', 18)
// 子类
function Student(gender) {
this.gender = gender
}
// 直接把父类的实例当作字类的原型对象
Student.prototype = new Person('Jack', 18)
此时Student
类的原型应该长这样
Student.prototype = {
name: 'Jack',
age: 18,
__proto__: { // Person.prototype
constructor: Person,
sayHi: function () { },
__proto__: Object.prototype
}
}
而当我们const s = new Student('男')
时,s应该长这样
s = {
gender: '男',
__proto__: { // Student.prototype 也是 Person 的实例
name: 'Jack',
age: 18,
__proto__: { // Person.prototype
constructor: Person,
sayHi: function () { },
__proto__: Object.prototype
}
}
}
此时的原型链示意图:
原型继承的优缺点
- 优点:
- 构造函数体内和原型上的都可以继承
- 写法方便简洁,容易理解。
- 缺点:
- 一个构造函数的内容, 在两个位置传递参数
- 继承来的属性不再子类实例的身上
二、借用继承
借用构造函数继承 (借用继承 / call继承)
原理:
通过改变 父类 构造函数的 this 指向来达到继承效果
核心代码:在子类构造函数体内,
父类.call(字类的实例)
我们要了解借用继承之前必须要知道的知识:
构造函数的执行:
1. 是一个普通函数, 可以当作函数直接调用
2. 当作普通函数执行的时候, this 指向谁, 就向谁身上添加内容
3. call 方法可以改变函数的 this 指向
代码实现
function Student(gender, name, age) {
this.gender = gender
// Person('Jack', 18)
// 使用 call 方法改变一下 Person 函数内部的 this 指向
// 改变成指向谁, Person 就会向谁的身上添加一个 name 一个 age
// Person.call('Jack', 18)
// 这个位置的 this 指向 Student 的实例, 因为 new Student
// Person 函数内部的 this 指向 Student 的实例 this === s
Person.call(this, name, age)
// 这个函数执行完毕以后, 会像 Student 的实例身上添加一个 name 一个 age
}
借用继承的优缺点
- 优点:
- 继承来的属性是在自己身上
- 我们一个实例化过程在一个位置传递参数
- 缺点:
- 只能继承父类构造函数体内的内容
- 父类原型上的内容不能继承
- 我们只是把父类当成普通函数调用了一下而已,方法是继承不到的
三、最早的组合继承
原理:把
原型继承
和借用构造函数继承
合并在一起使用
代码实现
function Student(gender, name, age) {
this.gender = gender
// 借用继承, 目的: 把属性继承在自己身上
Person.call(this, name, age)
}
// 原型继承, 目的: 继承父类原型上的方法
Student.prototype = new Person()
// 书写属于 Student 自己的方法
Student.prototype.study = function () { console.log('study') }
// 使用 Student 创建实例
const s = new Student('男', 'Jack', 18)
组合继承的优缺点
- 优点:
- 父类构造函数体内和原型上的内容都能继承
- 继承下来的属性放在自己身上
- 在一个位置传递所有参数
- 缺点:
- 当你给字类添加方法的时候, 实际上是添加在了父类的实例身上
四、拷贝继承
原理:利用
for in
循环的特点, 来继承所有的内容
步骤:
- 先实例化一个父类的实例
- 使用 for in 循环来遍历这个实例对象
=>
因为for in
循环不光遍历对象自己, 还会遍历__proto__
- 直接把父类实例身上的所有内容直接复制到字类的
prototype
代码实现
function Student(gender, name, age) {
this.gender = gender
// for in 继承
const p = new Person(name, age)
for (let key in p) {
Student.prototype[key] = p[key]
}
}
Student.prototype.study = function () { console.log('study') }
const s = new Student('男', 'Jack', 18)
console.log(s)
拷贝继承的优缺点
- 优点:
- 父类的构造函数体内的和原型上的都可以继承
- constructor 能正常配套
- 添加自己的方法的时候, 确实是在自己的原型身上
- 一个地方传参
- 缺点:
for in
循环:for in
循环需要一直遍历到Object.prototype
- 对性能的消耗比较大
- 不能继承
不可枚举
的属性 - 继承来的属性不在自己身上
五、寄生继承
1.寄生实例
关键代码:
const instance = new Person(name, age)
return instance
代码实现:
function Student(name, age) {
this.gender= '男'
// 寄生继承
const instance = new Person(name, age)
return instance
}
const s = new Student('Jack', 18)
Student.prototype.study = function () {}
console.log(s)
仔细研究上面的代码我们会发现一些端倪
我们可以看一下控制台的打印结果:
- s 确实是
new Student
来的 - s 就是
Student
的实例, 但是真实的内容是Person
的实例
最重要的是:study
方法竟然也不在原型链上了,这就很难搞了,就有了寄生继承的另一种版本
2.寄生原型
不直接寄生实例, 寄生原型
代码实现:
function Student(gender) {
this.gender = gender
}
// 寄生原型
Student.prototype = Person.prototype
// 该自己的
Student.prototype.stduy = function () {}
const s = new Student('男')
console.log(s)
我们可以看一下控制台的打印结果:
表面上看着很完美,属性和方法都完整继承下来了,而且自己写的方法也没有任何影响,但是它却有着致命的缺陷,嘿嘿,Student.prototype = Person.prototype
这两个东西指向的是一个地址,那么你往Student.prototype
上面挂东西的时候,直接操作了Person.prototype
,别的实例也会受到影响了
寄生继承的优缺点
- 优点:
- 原型和构造函数体内的都能继承下来
- 寄生原型的话, 自己的属性和方法依旧可以添加和使用
- 缺点:
- 寄生实例的时候, 没有自己的任何内容
- 寄生原型的时候, 一旦修改原型上的任何属性方法, 父类的实例也会有这些属性方法
本来写完了,但还是忍不住回来吐槽一下这个设计!我们都知道构造函数不应该写 return 的,因为当你 return 一个基本数据类型的时候,你写也是白写!而当你 return 一个复杂数据类型, 构造函数就没有意义了!当然啦,奈何人家确实也能完成继承,就这样吧
六、寄生式组合继承 (完美继承)
合并了
寄生继承
+原型继承
+独立第三方构造函数
+借用继承
代码实现
function Student(gender, name, age) {
this.gender = gender
// 借用继承: 继承来了父类的属性
Person.call(this, name, age)
}
(function () {
function Abc(name, age) {}
// 让 第三方构造函数 来寄生 父类 的原型
Abc.prototype = Person.prototype
Student.prototype = new Abc()
})()
看一下最终结果:
- 继承来的和自带的属性在自己身上
- 可以将父类的原型上也继承下来了
- 添加自己的方法不影响其他
- 完美
七、ES6的类继承
官方把
寄生式组合继承
用class
关键字封装起来了
前面的理解之后这个就太水了,直接上代码吧
class Student extends Person {
constructor (gender, name, age) {
super(name, age)
this.gender = gender
}
study () {
console.log('study')
}
}
- extends
class 子类类名 extends 父类 { }
- super()
在 constructor 里面书写一个 super()
super( name , age ) 等价于 Person.call ( this , name , age )
注意
super
需要写在constructor
里面- 如果你要写自己的属性, 必须写在
super
后面 ES6
的继承可以继承ES5
的构造函数也可以继承ES6
的类