标题的灵感来源于鲁迅的小说《孔乙己》中孔乙己和小伙计的一段对话:“茴香豆的茴字,怎样写的?……回字有四样写法,你知道么?”
这里我们并不探讨封建制度下穷苦潦倒的读书人的迂腐,回字的几种写法留给汉语言的同学去研究吧,今天我们讨论JavaScript
继承的几种写法,由浅入深,一层层剥开她的面纱,最后给出一个最佳实践。
一、通过构造函数实现继承
function Parent() {
this.name = 'name';
}
Parent.prototype.say = function () {
console.log('say');
}
function Child() {
Parent.call(this);
this.type = 'child';
}
const child = new Child();
这种方式通过在子类的构造函数中调用父构造函数实现继承,但是有个致命的缺点:如下图,子类实例并不能继承父类中原型对象上的属性或者方法。
二、通过原型对象实现继承
function Parent() {
this.name = 'name';
this.favor = ['blue', 'red'];
}
Parent.prototype.say = function () {
console.log('say');
}
function Child() {
this.type = 'child';
}
Child.prototype = new Parent();
const child = new Child();
这种方式通过给子类构造函数的prototype
对象赋值实现继承,如下图,无论是name
属性还是say
方法都能被子类实例访问到。
但是这种方式也有缺点,如下图,当父类的属性是引用类型时,子类实例继承的是同一份属性,任一实例改变该属性都会引起全局变化,无法互相隔离。
三、组合方式
function Parent() {
this.name = 'name';
this.favor = ['blue', 'red'];
}
Parent.prototype.say = function () {
console.log('say');
}
function Child() {
Parent.call(this);
this.type = 'child';
}
Child.prototype = new Parent();
const child = new Child();
结合了上两种方式,避免了上面的缺点。但是这种方式在每次实例化子类实例时,都会调用两次父类构造函数,需要优化。
四、组合优化①
function Parent() {
this.name = 'name';
this.favor = ['blue', 'red'];
}
Parent.prototype.say = function () {
console.log('say');
}
function Child() {
Parent.call(this);
this.type = 'child';
}
Child.prototype = Parent.prototype;
const child = new Child();
在给子类prototype
赋值时不要采用实例化父类的方式,直接赋值父类的prototype
。
其实,这种方式也有缺点:如下图,子类实例的constructor
属性直接指向了父类构造函数,导致无法判断当前对象实例化自哪个构造函数。
五、组合优化②
function Parent() {
this.name = 'name';
this.favor = ['blue', 'red'];
}
Parent.prototype.say = function () {
console.log('say');
}
function Child() {
Parent.call(this);
this.type = 'child';
}
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
const child = new Child();
终极改造完成:通过Object.create()
方法指定了子类实例的__proto__
属性,同时显式声明子类的constructor
。