《JavaScript高级程序设计》中提到过多种继承方法:原型链继承,借用构造函数,组合式继承,原型式继承等等,方式很多,让人傻傻 分不清楚。那么,这篇文章将从宏观角度对这些继承方式做一个梳理。
我个人认为,JS
继承从继承手段上可以分为三类:
- 拷贝式继承
- 修改
prototype
- 修改
__proto__
指针
拷贝式继承
在我的这篇文章中提到过类似的概念,通过将父类的属性和方法拷贝到子类实例上来实现继承,在js
中的实现方式就是借用构造函数。可以看下面的例子
function Father(a, b) {
this.a = a
this.b = b
this.fn = function() {}
}
const f = new Father(1, 2)
function Son(aa, bb) {
Father.call(this, 10, 20)
this.aa = aa
this.bb = bb
}
const s1 = new Son(11, 22)
const s2 = new Son(33, 44)
红宝书中也提到过,这种方法的问题在于每个子类实例上都有父类属性的一份独立拷贝,浪费空间,此外,各个子类上的属性和方法是独立的,不能实现函数的重用
// s1和s2两个实例的方法不是同一个引用
s1.fn === s2.fn // false
这种借用构造函数的继承方式实质上是一种对父类属性的拷贝,可以理解为一种拷贝式继承。
修改prototype
第二种继承方式是修改构造函数的prototype
属性,在js
中,这种方式有两种实现方法
1. Son.prototype = new Father()
这种方式被称为原型链继承
function Father() {
this.a = 'father'
}
Father.prototype.b = 'father prototype'
function Son() {
this.c = 'son'
}
Son.prototype = new Father() // 修改了Son.prototype
const son = new Son()
可以看出,子类构造函数的prototype
属性被重写了,该属性被重写为一个Father
实例,这样就实现了继承。
我们要注意一点,由于用父类的实例重写了子类构造函数的prototype
属性,因此子类构造函数的prototype
是有父类的实例属性的,因此一个子类实例能够访问子类的实例属性,父类的实例属性和父类的原型属性。此外,由于子类构造函数的prototype
被重写,因此contructor
属性会失真。
2. Object.create()
function Father() {
this.a = 'father'
}
Father.prototype.b = 'father prototype'
function Son() {
this.c = 'son'
}
Son.prototype = Object.create(Father.prototype)
const son = new Son()
从上面的代码可以看出,Object.create()
和传统的原型链方式都通过修改prototype
来实现继承,但是区别在于,Object.create(Father.prototype)
创造的对象是一个空对象,没有任何属于自身的属性,这个空对象继承自Father.prototype
,并用这个空对象重写了Son.prototype
。
这种方式与原型链继承的区别在于,子类的prototype
不再是父类的实例,因此子类实例不再能访问父类的实例方法。此外,由于prototype
被重写,因此子类的contructor
也会失真。
如果直接使用Object.create
来创建一个对象,其实也有一个重写prototype
属性的过程,这个方法的pollyfill
如下
if (!Object.create) {
Object.create = function(proto) {
function F(){}
F.prototype = proto // 对中间函数prototype的重写
return new F()
}
}
修改__proto__
ES6
后,我们可以使用Object.setPrototypeOf
来修改__proto__
属性,通过该方法,我们可以直接让子类原型继承自父类原型,不需要重写子类原型对象,因此constructor
也不会失真。
ES6 extends
看完了前面几种基础的继承方式,我们来看看ES6
中的extends
关键字
class Father {
constructor() {
this.name = 'Father'
}
static log() {
console.log('static', this.name)
}
say() {
console.log('method', this.name)
}
}
class Son extends Father {
constructor() {
super()
this.name = 'Son'
}
}
const son = new Son()
一个简单的extends
,里面有三种继承关系
- 实例属性继承:首先是在子类中调用
super()
,这与拷贝式继承对应(不完全相同) - 原型方法的继承:我们在子类实例中能够调用父类中定义的原型方法,是两个
prototype
之间的继承,Son.prototype = Object.create(Father.prototype)
- 静态方法的继承:可以通过子类直接调用父类中的静态方法,是两个函数之间的继承:
Object.setPrototypeOf(Son, Father)
属性屏蔽
其实说到继承,就不得不提一下属性屏蔽的问题,但是很多博客上并没有提到过这部分内容。
我们都知道,在查找一个对象的某个属性时,如果源对象没有目标属性,就会沿着__proto__
链,一直向上查找。在读属性时,属性屏蔽很容易理解:对象上的属性会屏蔽原型上的同名属性,但是在写属性时情况就比较复杂了。先来看一个问题吧:
let proto = {
num: 1
}
let obj = Object.create(proto)
obj.num++
console.log(obj)
console.log(proto)
答案是obj: { num: 2 }, proto: { num: 1 }
,你答对了吗?
如果没有专门了解过这部分的知识的话,这道题很容易出错。很多人可能会认为,读取的是原型上的属性,那么赋值是也就会修改原型上的属性,其实没有这么简单,属性屏蔽符合下面的规则(以下源对象指obj
,原型对象指proto):
- 如果源对象有目标属性,无论该属性是否可写,直接修改该属性
- 如果源对象没有目标属性
- 如果原型上也没有该属性,直接在源对象上新增该属性
- 如果原型上有该属性
- 如果该属性为数据属性(不是
getter
和setter
)- 如果该属性可写,就在源对象上添加该属性
- 如果该属性不可写,赋值语句静默失败
- 如果该属性为访问器属性,直接调用
setter
- 如果该属性为数据属性(不是
在来分析一下上面的题目,源对象obj
上没有num
属性,但是原型对象proto
上有num
属性,并且该属性可写,因此就会在源对象obj
上添加该属性(注意,num++
相当于num = num + 1
),在读取num
时会查找proto.name
,之后加1,因此obj.num
变为2
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!