原型链继承
function Parent() {
this.users = ['userA', 'userB', 'userC']
}
// 关键点1:person1和person2可以继承构造函数`prototype`上的属性和方法;
Parent.prototype.getUsers = function() {
console.log(this.users)
}
Parent.prototype.sex = '男'
Parent.prototype.list = []
function Child() {
this.age = 18
}
Child.prototype = new Parent()
var person1 = new Child()
// 关键点2:构造函数中以及原型对象上【引用类型】的属性都会被共享(list和users)
person1.users.push('userD')
person1.list.push('userF')
// 关键点3:这里person1实例本身并没有sex属性,这里实际上是给person1添加了sex属性
person1.sex = '女'
var person2 = new Child()
console.log(person1.sex, person2.sex)
console.log(person1, person2)
如图所示,可以很明显的看出原型链继承的优缺点
- 优点: 可以同时继承构造函数中和原型对象
prototype
上的属性和方法; - 缺点: 引用类型的属性被所有实例共享(实际上是所有的属性,只是基本类型的属性无法被实例修改);
构造函数继承(经典继承)
function Parent(user) {
this.users = ['userA', 'userB', 'userC']
this.users.push(user)
}
// 关键点:prototype属性无法被继承
Parent.prototype.sex = '男'
function Child(user) {
// 关键点:可以向父类传参
Parent.call(this, user)
this.age = 18
}
var person1 = new Child('person1')
// 关键点:users属性不会被共享
person1.users.push('userD')
var person2 = new Child('person2')
console.log(person1.age, person2.age)
console.log(person1.sex, person2.sex)
console.log(person1.users, person2.users)
通过这个例子,可以看到,构造函数继承对比于原型链继承方式:
- 优点:
- 避免了引用类型的属性被所有实例共享;
- 可以向父类传参
- 缺点:只能继承构造函数中的属性和方法,无法继续原型对象
prototype
上的属性和方法;
组合继承
function Parent(user) {
this.users = ['userA', 'userB', 'userC']
user && this.users.push(user)
}
Parent.prototype.sex = '男'
function Child(user) {
Parent.call(this, user)
this.age = 18
}
// Parent.call(this, user)和new Parent()重复继承了Parent构造函数中的属性
Child.prototype = new Parent()
var person1 = new Child('person1')
person1.users.push('userD')
var person2 = new Child('person2')
console.log(person1, person2)
组合继承
- 优点:融合了原型链继承和构造函数的优点;
- 缺点:
- 创建了多余的属性(如图所示的users)
- Parent方法被调用了两次(new Parent(),Parent.call(this, user))
组合继承优化1
- 针对上面的问题1,我们先复习下原型链的知识点
// 实例对象的原型`__proto__`指向构造函数的原型对象
new Parent().__proto__ === Parent.prototype // true
// 构造函数原型对象的`constructor`属性指向构造函数本身
Child.prototype.constructor === Child // true
那么我们可以做出优化代码如下:
function Parent(user) {
this.users = ['userA', 'userB', 'userC']
this.users.push(user)
}
Parent.prototype.sex = '男'
function Child(user) {
Parent.call(this, user)
this.age = 18
}
// 由于Parent.call(this, user),已经继承了Parent构造函数中的属性,只需要再继承Parent原型对象上的属性即可
Child.prototype = Parent.prototype
// 期望:Child.prototype.constructor === Child;实际:Child.prototype.constructor === Parent
console.log(Child.prototype.constructor === Child) // false
console.log(Child.prototype.constructor === Parent) // true
var person1 = new Child('person1')
person1.users.push('userD')
var person2 = new Child('person2')
console.log(person1, person2)
对比优化前后端结果,不难看出,创建的多余属性(users)已经没有了,且Parent构造函数只被调用了一次;结合之前的原型链的知识,我们期望:Child.prototype.constructor === Child,然而实际上Child.prototype.constructor === Parent
接下来让我们继续优化下代码
组合继承优化2
function Parent(user) {
this.users = ['userA', 'userB', 'userC']
this.users.push(user)
}
Parent.prototype.sex = '男'
function Child(user) {
Parent.call(this, user)
this.age = 18
}
// 疑问:是否有问题,怎么优化
Child.prototype = Parent.prototype
// 优化代码
Child.prototype.constructor = Child
var person1 = new Child('person1')
var person2 = new Child('person2')
console.log(Child.prototype.constructor === Child) // true
console.log(person1.constructor === Child) // true
代码到这里基本上就已经完成了,但是这里还有个问题,我们知道引用类似的对象直接赋值时是浅拷贝,所以这里修改Child的prototype属性值会覆盖Parent.prototype的同名属性,增加新属性
如下示例,我们希望Child继承Parent的属性,而不会直接去修改Parent本身的属性,不会对Parent的实例造成影响
function Parent(user) {
this.users = ['userA', 'userB', 'userC']
this.users.push(user)
}
Parent.prototype.sex = '男'
function Child(user) {
Parent.call(this, user)
this.age = 18
}
Child.prototype = Parent.prototype
Child.prototype.constructor = Child
// 会覆盖parent中的同名属性
Child.prototype.sex = '女'
Child.prototype.weight = '70kg'
var person = new Child('person')
var parent = new Parent('Parent')
console.log(person.sex, person.weight) // 女,70kg
// 期望:男,undefined; 实际女,70kg
console.log(parent.sex, parent.weight) // 女,70kg
组合继承优化3
上面提到,由于Parent.call(this, user),已经继承了Parent构造函数中的属性,只需要再继承Parent原型对象上的属性即可,结合之前原型链的知识可以优化代码如下
Parent.prototype = new Parent().__proto__
function Parent(user) {
this.users = ['userA', 'userB', 'userC']
this.users.push(user)
}
Parent.prototype.sex = '男'
function Child(user) {
Parent.call(this, user)
this.age = 18
}
Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child
// 会覆盖parent中的同名属性
Child.prototype.sex = '女'
Child.prototype.weight = '70kg'
var person = new Child('person')
var parent = new Parent('Parent')
console.log(person.sex, person.weight) // 女,70kg
console.log(parent.sex, parent.weight) // 男,undefined
补充,关于Object.create(obj)
Object.create(obj)创建的其实是obj对象的原型引用
// 模拟Object.create(obj)
function createObj(obj) {
var prototype = obj
function Fn() {}
Fn.prototype = prototype
// new Fn().__proto__ == Fn.prototype
return new Fn()
}
修改之前的代码,如下所示,我们得到了完全一样的结果
function Parent(user) {
this.users = ['userA', 'userB', 'userC']
this.users.push(user)
}
Parent.prototype.sex = '男'
function Child(user) {
Parent.call(this, user)
this.age = 18
}
function createObj(obj) {
var prototype = obj
function Fn() {}
Fn.prototype = prototype
return new Fn()
}
Child.prototype = createObj(Parent.prototype)
Child.prototype.constructor = Child
// 会覆盖parent中的同名属性
Child.prototype.sex = '女'
Child.prototype.weight = '70kg'
var person = new Child('person')
var parent = new Parent('Parent')
console.log(person.sex, person.weight) // 女,70kg
console.log(parent.sex, parent.weight) // 男,undefined
总结
- 本文讲述了继承的多种方式和优缺点
- 如何去一步步的去实现函数的继承,以及如何去优化
- 本文涉及到原型链、基本数据类型、引用数据类型、Object.create()等相关知识点