JS 里面有很多没那么好懂的内容,像是闭包,继承,this。我打算将这些内容揉碎了分别输出文档。
这篇讲继承,查看完整代码请戳:继承
为什么要有继承
从 JS 创始人 Brendan Eich 讲起
JS 也是面向对象语言,一切都是对象,必须要有一种机制,将所有对象联系起来。但 Brendan Eich 的创始人又不想在 JS 中引入”类”,因为一旦引入“类”, JS 就会变成一种完整的面向对象编程语言,这显示太正式,不是创建 JS 语言的初衷。受 C++ 和 JAVA 里 new 命令的影响,他就把 new 命令引入了JS,用来从构造函数中产生一个实例对象。具体详见 JS 面向对象之封装
prototype 的引入
由于直接 new 出来的实例对象,都有自己的属性和方法的副本,无法做到数据共享,也浪费了内存。考虑到这点,Brendan Eich 决定为构造函数设置一个 prototype 属性。将所有需要共享的属性和方法,都放在这个 prototype 对象里面;那些独有的属性和方法,就放在构造函数里面。
这样实例对象创建好之后,将会有两类属性和方法,一个是自己的,一个是通过原型链(沿着 prototype )查找到的。
总结
继承就是子类可以使用父类的所有功能,并且对这些功能进行扩展。
比如构造函数 B 想要使用构造函数 A 里的属性和函数,一种是直接复写 A 里的代码,复制粘贴一下。还有一种就是使用继承,让 B 继承 A,这样 B 就可以使用 A 里的功能
那么都有哪几种继承方式呢?分别有哪些优缺点呢?且听我慢慢道来。。。
原型链继承
语法
将子类的原型对象指向父类的实例。
Child.prototype = new Parent()
复制代码
示例
function Parent(name,gender) {
this.name = name;
this.gender = gender;
}
Parent.prototype.getInfo = function() {
console.info('my name is ' + this.name + '; I am a ' + this.gender )
}
function Child(age) {
this.age = age
}
Child.prototype = new Parent("parent","boy")
var child1 = new Child(30)
console.info(child1) // Child {age:30}
child1.getInfo() // my name is parent; I am a boy
console.info(child1.name) // parent
复制代码
分析:
- 默认 Child 实例的原型对象是指向 Child.prototype 的,经过
Child.prototype = new Parent("parent","boy")
复制代码
设置, Child 的原型对象就指向了 Parent 的实例,具体原型链图如下:
依图可得,child 实例 proto 属性指向 parent 实例, parent 实例下的 proto 属性指向 Parent.prototype。所以 child1 既可以访问到 parent 实例的属性和方法(即 Parent 构造函数中定义的),也可以访问到 Parent.prototype 上定义的属性和方法
- 初次看到原型链继承,单看这名字,以为是用子类的原型对象指向父类的原型对象,即:
Child.prototype = Parent.prototype
复制代码
代码修改为:
function Parent(name,gender) {
this.name = name;
this.gender = gender;
}
Parent.prototype.getInfo = function() {
console.info('my name is ' + this.name + '; I am a ' + this.gender )
}
function Child(age) {
this.age = age
}
Child.prototype = Parent.prototype
var child1 = new Child(30)
console.info(child1) // Child {age:30}
child1.getInfo() // my name is parent; I am a boy
console.info(child1.name) // undefined
复制代码
原型链图就会变为:
很明显,此时 Child 的实例就访问不到 Parent 构造函数里定义的属性和方法,只能访问到 Parent 原型对象上定义的属性和方法,这明显不是我们要的真正继承
原型链继承总结
function Parent(name,gender) {
this.name = name;
this.gender = gender;
this.colors= ["blue","pink"] // 新增引用类型属性 colors
}
Parent.prototype.getInfo = function() {
console.info('my name is ' + this.name + '; I am a ' + this.gender )
}
function Child(age) {
this.age = age
this.sports = ["basketball"] // 新增引用类型属性 sports
}
var parent = new Parent("parent","boy")
Child.prototype = parent
var child1 = new Child(30,"child1 name") // 想传入child1 name 作为该实例独有的名称
child1.gender = "girl"
child1.colors.push("green")
child1.sports.push("football")
var child2 = new Child(29,"child2 name") // 想传入child2 name 作为该实例独有的名称
console.info(child1) // Child {age: 30, sports: ["basketball", "football"], gender: "girl"}
console.info(child2) // Child {age: 29, sports: ["basketball"]}
console.log(child1.name) // parent
console.info(child2.colors) // ["blue", "pink", "green"]
复制代码
分析:
- 在 Parent 构造函数里新增引用类型 colors 属性,在 Child 构造函数里新增引用类型 sports 属性
- 创建了两个 Child 实例(child1 与 child2)。
- child1 创建好之后,设置了 gender,并且给 colors 和 sports 都 push 了内容。
- child2 创建好之后,没做任何处理
- child1.gender = “girl” 相当于在 child1 实例上新增了 gender 属性,这样通过 child1 获取 gender 属性时,就会获取它自己的 gender 值,而不会往原型上查找
- child1.colors.push(“green”) 注意这里是先获取到 colors 值,才接着 push,而要获取到 colors 只能在 parent 实例上查找到,所以相当于是往 parent 实例里的 colors 属性添加内容,这样就会影响到 parent 以及实例化出来的所有 Child 实例
- 而 sports 是 Child 上自带的属性,所以在 child1 上 push 之后不会影响到别的实例(比如 child2 )
- 因此 child1 打印出了 age 、sports 和新增的 gender 属性。而 child2 只打印出了 age 和 sports
- child1.name 打印出了’parent’,这是因为 child1.name 访问的实际上是原型对象 parent 上的 name。虽然 new Child 时传递了 ‘child1 name’ 但很明显是没效果的,因为在 Child 构造函数里并没有接收 name 属性
- child2.colors 实际上访问的也是原型对象 parent 上的 colors,而 colors 已经被 child1 给改成了 [“blue”, “pink”, “green”]
优点:
- 继承了父类构造函数里的属性和方法,又继承了父类原型对象上的属性和方法
缺点:
- 无法实现多继承(因为指定了原型对象)
- 来自原型对象的所有属性都被共享了,当更改个引用类型的属性时,所有的实例对象都会受到影响(这点从child2.colors可以看出来)
- 无法传递参数给父对象,比如我要设置个独有的 name 值,就只能在自己的构造函数里重新定义 name 属性,用来屏蔽父对象的 name(这点从child1.name可以看出来)
构造函数继承
语法
在子类构造函数内部使用 call 或 apply 来调用父类构造函数
function Child() {
Parent.call(this,...arguments)
}
复制代码
来达到增强子类的目的,等于复制父类的实例属性给子类
示例
function Parent(name,gender) {
this.name = name;
this.gender = gender;
}
function Child(age,name,gender) {
this.age = age
Parent.call(this,name,gender) // 等价于Parent.apply(this,[name,gender])
}
var child1 = new Child(30,"child1 name","girl")
console.info(child1) // Child {age: 30, name: "child1 name", gender: "girl"}
console.info(child1.name) // child1 name
console.info(child1 instanceof Child) // true
console.info(child1 instanceof Parent) // false
复制代码
分析:
- 相当于在子类构造函数里,直接执行了父类构造函数。执行完后就会在子类的构造函数新增了父类所有的属性。
- 这个继承是借用了 call 或 apply 函数的能力。
- Child 的实例跟 Parent之间并无联系(这点从 intanceof Parent 可以看出来)
构造函数继承总结
function Parent(name) {
this.name = name;
this.gender = gender;
this.colors= ["blue","pink"]
}
Parent.prototype.getInfo = function() {
console.info('my name is ' + this.name + '; I am a ' + this.gender )
}
function Child(age,name,gender) {
this.age = age
this.sports = ["basketball"]
Parent.call(this,name,gender)
}
var child1 = new Child(30,"child1 name","girl")
child1.gender = "girl"
child1.colors.push("green")
child1.sports.push("football")
var child2 = new Child(29,"child2 name")
console.info(child1) // Child {age: 30, sports: ["basketball", "football"], gender: "girl", name: "child1 name", colors: ["blue", "pink", "green"]}
console.info(child2) // Child {age: 29, sports: ["basketball"], gender: undefined, name: "child2 name", colors: ["blue", "pink"]}
console.log(child1.name) // child1 name
console.info(child2.colors) // ["blue", "pink"]
child1.getInfo() //VM1106:1 Uncaught TypeError: child1.getInfo is not a function
复制代码
分析:
- 由于是在子类构造函数里立即执行父类构造函数,所以 colors、name、gender 等属性都是子类自己的,进行任何操作都不会影响别的实例,所以执行 child1.colors.push() 并不会影响别的实例
- getInfo 是父类原型对象上的方法,而 call 函数只是将父类构造函数的属性和方法执行了一遍,并不会复制父类原型对象上的属性和方法,所以会报错
优点:
解决了原型链继承的缺点
- 可以有多继承(在子类构造函数里分别调用父类的构造函数即可)
- 解决了子类实例共享父类构造函数里引用类型属性的问题(这点从 child.colors 可以看出来)
- 可以向父类传递参数(这点从 child1.name 可以看出来)
缺点:
解决了原型链继承的缺点,但同时将原型链的优点给整没了。
- 不能继承父类原型对象上的属性和方法
- 子类实例跟父类之间并无联系(这点从 child1 instanceof Parent 为 false 可以看出来)
- 无法实现函数复用,对于父类里的函数属性,每个子类都会复制一份,影响性能
组合继承
既然原型链继承跟构造函数继承分别有优缺点,那能否将这两者结合起来?
语法
// 原型链继承
Child.prototype = new Parent()
// 构造函数继承
function Child() {
Parent.call(this,...arguments)
}
复制代码
- 使用原型链继承来保证子类能够继承父类原型对象上的属性和方法
- 使用构造函数继承来保证子类能够继承到父类构造函数上的属性和方法
基操:
- 通过call/apply在子类构造函数内部调用父类构造函数
- 将子类构造函数的原型对象指向父类构造函数创建的一个匿名实例
- 修正子类构造函数原型对象的constructor属性,将它指向子类构造函数
示例
function Parent(name){
this.name = name;
}
Parent.prototype.getInfo = function() {
console.info('my name is ' + this.name)
}
function Child(name) {
Parent.call(this,name) // 构造函数继承
}
Child.prototype = new Parent() // 原型链继承,传入空参数即可,因为传入任何参数都会被屏蔽
Child.prototype.constructor = Child // 修复 child1 constructor 指向 Parent 问题
var child1 = new Child("child1 name")
console.info(child1) // Child {name: "child1 name"}
child1.getInfo() // my name is child1 name
console.info(child1.constructor) //ƒ Child(name) { Parent.call(this,name) // 构造函数继承 }
复制代码
分析:
- 通过原型链继承,来达到可以复用父类原型对象上属性和方法的效果。只要传空参数即可,因为传入任何参数都会被屏蔽
- 通过构造函数继承,来继承父类构造函数上属性和方法,同时也屏蔽了子类实例__proto__(即父类实例)上的属性和方法
- 通过Child.prototype.constructor = Child 实际上只是个标记,标识某个实例是由哪个构造函数产生而已。出于编程习惯,最好将它改为正确的构造函数
原型链图修改为如下(比原型链继承多出两条线,一条是 Child里 使用 call/apply 调用 Parent;还有一条是 Child.prototype(即 Parent 的实例) 的 constructor 指向 Child)
组合继承总结
function Parent(name, colors) {
this.name = name
this.colors = colors
}
Parent.prototype.sports = ['basketball']
function Child(name, colors) {
Parent.call(this, name, colors)
}
Child.prototype = new Parent()
Child.prototype.constructor = Child
var child1 = new Child('child1', ['pink'])
child1.colors.push('blue')
child1.sports.push('football')
var child2 = new Child('child2', ['black'])
console.info(child1) // Child {name: "child1", colors: ["pink", "blue"]}
console.info(child2) // Child {name: "child2", colors: ["black"]}
console.info(child2.sports) // ["basketball", "football"]
console.info(Child.prototype) // Parent {name: undefined, colors: undefined, constructor: ƒ Child(name, colors){}}
console.info(child1 instanceof Child) // true
console.info(child1 instanceof Parent) // true
复制代码
分析:
- 两个实例上的 colors 是通过构造函数继承于父类,是复制出来的属性,每个实例之间不会互相影响
- sports 是位于父类原型对象上的属性,是共享的,所以更改了 child1.sports 会影响到 其他 Child 实例
- Child.prototype 是使用 new Parent 生成的,并且生成的时候没有传递参数,所以 name 和 colors 都是 undefined 。而且又将 constructor 指回 Child
- 由于 child1 可以沿着它的原型链查找到 Child.prototype 和 Parent.prototype 。所以后面两个都为 true
优点:
相当于融合了原型链继承跟构造函数继承的优点:
- 可以继承父类实例属性和方法,也可以继承父类原型对象上的属性和方法
- 跟构造函数继承一样,解决了父类构造函数里引用类型属性共享问题
- 跟构造函数继承一样,可以向父级传递参数
那这个组合继承是不是完美的呢?答案是否定的,接下来一探究竟
function Parent(name) {
console.info(name)
this.name = name
}
function Child(name) {
Parent.call(this, name)
}
Child.prototype = new Parent()
Child.prototype.constructor = Child
var child1 = new Child('child1')
console.info(child1)
console.info(Child.prototype)
复制代码
结果为:
// undefined
// child1
// Child {name:'child1'}
// Parent {name:'undefined',constructor:f Child(name){}}
复制代码
分析:
- 由于 new Parent 时会调用一次 Parent 函数,此时 name 为 undefined; 而在 Child 构造函数里也会调用次 Parent 函数,并传入 child1 作为 name 参数
- Child.prototype 是指向 Parent 实例对象,这个实例的 name 为 undefined, constructor 重新设置为 Child 了
缺点:
- 调用了两次父类的构造函数
- 子类实例中的属性和方法会屏蔽父类实例上的属性和方法,增加了不必要的内存
原型式继承
语法
function create(o) {
function F() {}
F.prototype = o
return new F()
}
复制代码
- 在 create 函数内部会创建个临时构造函数,然后将传入的对象作为这个构造函数的原型对象,并返回这个临时构造函数的新实例。
- 本质上,create() 是对传入的对象执行了一次浅复制。
- 可以对返回的对象进行适当修改
示例
function create(o) {
function F() {}
F.prototype = o
return new F()
}
var person = {
name: '张三',
colors: ['pink', 'blue']
}
var student = create(person)
student.name = '小明'
student.colors.push('yellow')
var teacher = create(person)
teacher.name = '夏老师'
teacher.colors.push('green')
console.info(student) // F {name: "小明"}
console.info(teacher) // F {name: "夏老师"}
console.info(teacher.colors) // ["pink", "blue", "yellow", "green"]
复制代码
分析:
- 将 person 作为原型对象,此时实例化出 student 跟 teacher, student 和 teacher 实例的__proto__ 属性就都指向了 person
- student.name 相当于新建了 name 属性,不会影响 person 里的 name 属性
- student.colors.push 首先是要先查找到 colors ,此时是在原型对象,即 person 上查找到,所以进行 push 添加内容了,就会影响到 person 以及所有的实例对象(比如 teacher)
** ES5 之后的原型式继承**
ES5 新增 Object.create 方法将原型式继承规范化了。
var person = {
name: '张三',
colors: ['pink', 'blue']
}
var student = Object.create(person)
student.name = '小明'
student.colors.push('yellow')
var teacher = Object.create(person)
teacher.name = '夏老师'
teacher.colors.push('green')
console.info(student) // F {name: "小明"}
console.info(teacher) // F {name: "夏老师"}
console.info(teacher.colors) // ["pink", "blue", "yellow", "green"]
复制代码
原型式继承总结
优点:
- 比起原型链继承,代码量较少,不用创建构造函数
缺点:
与原型链继承类似
- 实例之间共享继承实例引用类型属性(这点从 teacher.colors 可以看出)
- 无法给父级构造函数传递参数,只能覆盖,覆盖之后就会出现父子存在一样的属性问题,造成内存浪费
寄生式继承
语法
function createAnother(original){
let clone = Object.create(original); // 通过调用函数创建一个新对象
clone.fn = function() {}; // 以某种方式增强这个对象
return clone; // 返回这个对象
}
复制代码
就是在原型式继承的基础上再封装一层,来增强对象,之后将这个对象返回
示例
function createAnother(original){
let clone = Object.create(original); // 通过调用函数创建一个新对象
clone.sayHi = function() { // 以某种方式增强这个对象
console.log("hi");
};
return clone; // 返回这个对象
}
var person = {
name: '张三',
colors: ['pink', 'blue']
}
var student = createAnother(person)
student.sayHi() // hi
复制代码
寄生式继承总结
跟原生式继承基本一样
优点:
- 比起原型链继承,代码量较少,不用创建构造函数
缺点:
与原型链继承类似
- 实例之间共享继承实例引用类型属性(这点从 teacher.colors 可以看出)
- 无法给父级构造函数传递参数,只能覆盖,覆盖之后就会出现父子存在一样的属性问题,造成内存浪费
- 给对象添加函数会导致函数难以重用,造成内存浪费
寄生式组合继承
语法
为了修复组合继承的缺点:
- 父类构造函数会被调用两次
- 生成了两个实例,在父类实例上产生了无用废弃的属性
,所以引入了寄生式组合继承。
实际上通过构造函数继承已经可以实现继承父类构造函数里的属性和方法,所以只要继承父类原型对象上的属性和方法即可。
function Child() {
Parent.call(this,...arguments)
}
Child.prototype = Object.create(Parent.prototype)
复制代码
示例
function Parent(name) {
this.name = name
}
Parent.prototype.getInfo = function () {
console.info(this.name)
}
function Child(name) {
this.sex = 'boy'
Parent.call(this, name)
}
// 与组合继承的差别
Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child
var child1 = new Child('child1')
console.info(child1) // Child {sex: "boy", name: "child1"}
child1.getInfo() // child1
console.info(child1.__proto__) // Parent {}
复制代码
分析:
- child1 与 Parent 之间就没有关系了,仅仅是在构造函数里用 Parent.call 复制了 Parent 里的属性和方法,所以只会调用一次 Parent
- child1 的原型对象是 Parent {} 不存在 name 属性,就不会造成内存浪费
寄生组合继承总结
function Parent(name, colors) {
this.name = name
this.colors = colors
}
Parent.prototype.sports = ['basketball']
function Child(name, colors) {
Parent.call(this, name, colors)
}
Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child
var child1 = new Child('child1', ['pink'])
child1.colors.push('blue')
child1.sports.push('football')
var child2 = new Child('child2', ['black'])
child2.sports = ['badminton']
console.info(child1) // Child {name: "child1", colors: ["pink", "blue"]}
console.info(child2) // Child {name: "child2", colors: ["black"], sports: ["badminton"]}
复制代码
分析:
- name 与 colors 属性都是通过构造函数复制过来的,所以改变 child1.colors 对其它实例没有影响的
- child1.sports 是给原型链上的 sports 添加内容,此时会影响到 parent 与 child2。即 child2.sports 获取到的是 push 后的值。但执行了 child2.sports 此时相当于给 child2 对象新增了 sports 属性,会覆盖原型对象上的 sports 属性,所以此时 child2.sports 取的就是自身的属性
优点:
- 只调用了一次父类构造函数,只创建了一份父类属性
- 子类可以用到父类原型链上的属性和方法