古之立大事者,不惟有超世之才,亦必有坚忍不拔之志。——苏轼
写在前面
之前我们通过 一篇搞懂原型和原型链 这篇博客介绍了原型链和原型链的两种继承方式,今天就通过这一篇博客来介绍一下 JavaScript 中剩下的常用的继承方式
原型式继承
所谓原型式继承,就是定义一个函数,该函数中创建一个临时性的 构造函数,将作为参数传入的对象作为这个构造函数的原型,最后返回这个构造函数的实例对象。
实例代码如下所示:
// 定义一个函数
function fun(obj) {
// 临时构造函数
function Fun() {
this.name = '涂山苏苏'
}
// 将传入的参数对象作为构造函数的原型
Fun.prototype = obj
// 返回一个用临时构造函数创建的新对象
return new Fun()
}
// 创建一个基类对象
var obj = {
obj: '最强红线仙'
}
// 调用函数
var result = fun(obj);
console.log(result.name); // 涂山苏苏
console.log(result.obj); // 最强红线仙
上述代码中的缺点很明显,如果我们想重新创建一个不一样的子类对象就需要重新定义我们的函数体。
为了解决这个问题,我们通过参数传入一个对象,然后通过遍历对象的方式为临时的构造函数新增属性和方法,示例代码如下所示:
// 定义一个函数
function fun(parent, child) {
// 临时构造函数
function Parent() {
// 遍历传入的对象 获取其每个属性
for (attrName in child) {
// 通过获取到的属性为其为每个属性赋值
this[attrName] = child[attrName]
}
}
// 将传入的参数对象作为构造函数的原型
Parent.prototype = parent
// 返回一个用临时构造函数创建的新对象
return new Parent()
}
// 创建一个对象
var parent = {
parent: '最强红线仙',
friends: []
}
// 调用函数
var yuechu = fun(parent, {
name: '白月初',
print: function () {
console.log(this.name + '是' + this.parent);
}
});
console.log(yuechu.name); // 白月初
yuechu.print(); // 白月初是最强红线仙
// 在创建一个 susu
var susu = fun(parent, {
name: '涂山苏苏',
print: function () {
console.log(this.name + '是' + this.parent);
}
})
console.log(susu.name); // 涂山苏苏
susu.print(); // 涂山苏苏是最强红线仙
// 每个继承于 parent 对象的 隐式原型都指向 parent 对象
console.log(susu.__proto__ === parent && yuechu.__proto__ === parent); // true
// 借助原型继承存在的问题
// 为 susu 的 friend 数组增加一个数据
yuechu.friends.push('涂山雅雅')
// 为 yuechu 的 friend 数组增加一个数据
yuechu.friends.push('王富贵')
// 访问这两个数据
console.log(yuechu.friends); // [ '王富贵', '涂山雅雅' ]
console.log(susu.friends); // [ '王富贵', '涂山雅雅' ]
// 存在的问题:会共享原型中的属性
此段代码的原型链图如下所示:
不过使用原型式继承这种方式 JavaScript 中提供了专门的方法,那就是 Object.create()
方法,我们知道这个方法可以创建对象,但是他也可以完成继承功能,该方法的语法结构如下所示
Object.create(proto,[propertiesObject])
参数说明:
-
proto
: 新创建对象的原型对象 -
propertiesObject
: 可选。需要传入一个对象,传入对象的语法结构如下所示{ name: { // 属性 value: 'is_sweet', enumerable:true, // 是否可枚举 configurable:true, // 是否可配置 writable:true // 是否可写 }, print:{ // 方法 value:function(){ console.log('这是一个函数'); }, enumerable:false, configurable:true, writable:false } }
返回值:一个新对象,带着指定的原型对象和属性。
这个方法非常像我们刚才创建的那个函数,这个方法比我们自己那个函数更加严谨,示例代码如下所示
// 创建一个对象作为原型
var obj = {
obj: '最强红线仙'
}
/*
* Object.create(proto,[propertiesObject])
* 作用:用于创建一个新对象
* 参数1:proto: 新创建对象的原型对象。
* 参数2:propertiesObject: 传入一个对象
* 返回值:一个新对象
*/
var result = Object.create(obj, {
name: {
value: '涂山苏苏',
enumerable: true, // 是否可枚举
configurable: true, // 是否可配置
writable: true // 是否可写
},
print: {
value: function () {
console.log('这是一个函数');
},
enumerable: false,
configurable: true,
writable: false
}
})
console.log(result.name); // 涂山苏苏
result.print() // 这是一个函数
关于借助原型完成继承的,都会有一个致命的缺点:所有实例化后的对象都共享原型的方法和属性,如果有一个更改则都会进行更改。
借助构造函数继承
所谓的借助构造函数继承(有些资料也称为伪造对象或经典继承),就是通过子对象借助 Function.call()
或者 Function.apply()
方法调用父类构造函数完成继承,示例代码如下所示:
// 父级对象
function Parent() {
this.parent = 'parent'
}
// 为 Parent 父级对象的原型增加属性
Parent.prototype.name = '贰货道士'
// 子级对象
function Child() {
this.child = 'child'
// 使用 call() 或者 apply() 方法调用父级构造函数 实现继承。
Parent.call(this)
}
const child = new Child(); // Child { child: 'child', parent: 'parent' }
console.log(child);
// 不会继承父类的原型
console.log(child.name); // undefined
代码执行流程如下图所示
使用这种方式的优点是避免了引用类型的实例被所有对象共享,缺点是因为所有的方法都定义在了构造函数中,是不会继承原型对象,而且每实例化一个对象之后都会重新创建一遍这些方法,占用内存空间,更别说函数复用了。
组合式继承
之前掌握的两种继承方式都是存在缺点的,基于原型继承的继承方式,所有实例化后的对象都共享原型的方法和属性,如果有一个更改则都会进行更改。而借助构造函数继承的方式又无法继承原型属性。所以就出现了结合式继承,就是将基于原型继承方式和借助构造函数的继承方式结合起来,取其精华去其糟粕的一种继承方式。
实现组合式继承的基本思路如下
- 使用原型链或原型式继承实现对原型的属性和方法的继承。
- 通过结构构造函数实现对实例对象的属性的继承。
这样,既通过在原型上定义方法实现了函数的复用,又可以保证每个对象都有自己的专有属性。
示例代码如下所示:
// 父级对象
function Parent() {
this.parent = 'parent'
}
// 为 Parent 父级对象的原型增加属性
Parent.prototype.name = '贰货道士'
// 子级对象
function Child() {
this.child = 'child'
// 使用 call() 或者 apply() 方法调用父级构造函数 实现继承。
Parent.call(this)
}
// 解决不会继承构造函数的原型对象的问题
Child.prototype = Parent.prototype
const child = new Child();
console.log(child.name); // 贰货道士
这种继承方式基本可以规避掉前面的继承方式所存在的问题
写在最后
继承是 JavaScript 中比较难理解的东西,究其原因是因为 JavaScript 中不存在类的概念,所以继承一般是通过对象与对象之间的继承,在一定意义上讲并不是真正的继承。