继承是 OO 语言中一个最为津津乐道的概念,许多 OO 语言都支持两种继承方式:接口继承和实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法。由于函数没有签名,在 ECMAScript 中无法实现接口继承。ECMAScript 只支持实现继承而且实现继承主要是依靠原型链来实现的。
关于原型链,我之前的文章里面有介绍,如果有些忘记了,可以看这篇文章。
下面我将详细的介绍前端前辈在开发过程中不断摸索创造的几种继承方式。看完面试的时候千万不要简单的回答 call 跟 apply 了。
为了说起来省事,虽然 js 没有严格意义的类,我还是以父类和子类来做区分继承关系。
1. prototype模式继承
既然子类想要继承父类的全部方法,而且我们知道父类的实例拥有父类所有的方法,那么接下类就好办了,我将子类的 prototype 指向父类的实例,子类就拥有了父类的全部方法了
// 定义父类
function Parent (name, age) {
this.name = name;
this.age = age;
}
Parent.prototype.sayName = function () {
alert(this.name);
}
// 定义子类
function Child (sex) {
this.sex = sex;
}
// 实现继承
var p = new Parent('leizore', 25);
Child.prototype = p;
var child = new Child('男');
child.sayName(); // leizore
那么对应的关系图如下:
这种方式 Child 继承了 Person 的全部方法,但是也是有缺点的。
- 创建子类实例时,无法向父类构造函数传参。指定 prototype 时,实例化 Person 传的参数,会出现在所有子类上,不灵活。
- 由图可以看到,p 的 contructor 指向 Person, 所以 Child.prototype.constructor 也指向 Person,显然会导致继承链的紊乱。
2.借用构造函数继承
针对上面的继承方法的缺点1,开发人员使用一种叫做借用构造函数的技术,也就是我们平时说的 call 跟 apply 继承。
// 定义父类
function Parent (name, age) {
this.name = name;
this.age = age;
}
Parent.prototype.sayName = function () {
alert(this.name);
}
// 定义子类
function Child (name, age, sex) {
// 继承,同时传递了参数
Parent.call(this, name, age)
this.sex = sex;
}
这里简单讲一下 call(apply)是如何实现的,其实就是将 call(apply) 前面的函数立即执行一遍,并且执行时将作用域 this 指向 call(apply) 函数的第一个参数,比如这里的 call 就是将 Parent 实例一遍,将 name 跟 age 当成参数传过去
这种继承方式解决了继承过程中的传参问题,但是缺点是并没有继承到父类的原型,为了解决这个问题,我们很容易想到将上面两个方法结合起来不久好了。于是另一种继承方式出现了
3.组合继承
没错,就是两种方式并用,从而发挥两者之长的一种继承模式,代码如下
// 定义父类
function Parent (name, age) {
this.name = name;
this.age = age;
}
Parent.prototype.sayName = function () {
alert(this.name);
}
// 定义子类
function Child (name, age, sex) {
// 继承,同时传递了参数
Parent.call(this, name, age)
this.sex = sex;
}
Child.prototype = new Parent('leizore', 25);
嗯,这种方式基本上解决了开发过程中继承的痛点,成为好多人常用的继承模式之一。但是缺点也是有的
- 重复定义了属性,可以看到将 Child 的 prototype指向 Perent 的实例时,继承了name 跟 age 属性,实例 Child 的时候,调用 call 函数,又继承了一次,虽然使用 call 调用这次的属性是在实例属性上,当获取name时优先返回实例属性,然后在 prototype 上,所以并不会出大问题。
- 第一种继承方式方式的缺点二也完美的继承过来了,Child.prototype.constructor 还是指向 parent
那么肯定有人会说,既然Child.prototype.constructor 不指向自己,那么直接让他指向自己不就好了?
Child.prototype.constructor = Child;
答案是不行的。因为 Child.prototype 是 Parent 的实例,这样操作会将 Parent.prototype.constructor 也指向 Child,显然也是不合理的。
4.原型式继承
为了解决上面 Child 与 Parent 继承之后纠缠不清的问题,道格拉斯在2006年提出一种继承方法,它的想法是借助原型可以给予已有的对象创建新对象,同时还不必因此创建自定义类型。函数如下
function object (o) {
function F() {}
F.prototype = o;
return new F();
}
这个模式相当与创建一个新的对象,对象继承了o所有属性,当然这里也只是实现了浅拷贝。
5.组合寄生式继承
嗯,想必大家也想到了,上面这种继承方式可以解决 Child 与 Parent 继承后的纠缠不清的关系。可以由 object 方法创建一个临时对象,从而斩断跟 Parent 的联系。就可以放心的对 Child 原型的constructor 随便指了,当然了为了继承链的不紊乱,还是指向自己比较好
// 定义父类
function Parent (name, age) {
this.name = name;
this.age = age;
}
Parent.prototype.sayName = function () {
alert(this.name);
}
// 定义子类
function Child (name, age, sex) {
// 继承,同时传递了参数
Parent.call(this, name, age)
this.sex = sex;
}
function object (o) {
function F() {}
F.prototype = o;
return new F();
}
var prototype = object(Parent.prototype);
prototype.constructor = Child;
Child.prototype = prototype;
var c = new Child('leizore', 11, 'men');
c.sayName() // leizore
c.constructor === Child // true
到此,基本上解决了上面所说的所有缺点。当然了,也是有一点问题的,就是方法四的实现其实是浅拷贝,如果 Parent.prototype 里又引用类型比如数组,对象,改变Parent.prototype,Child 也会跟着变,解决方式也很简单,使用深拷贝就行了,同时又可以写很多继承方式。当然了,按照我上面顺下来的思想,也可以写出自己的继承方式
比如下面改变object函数:
// 定义父类
function Parent (name, age) {
this.name = name;
this.age = age;
}
Parent.prototype.sayName = function () {
alert(this.name);
}
// 定义子类
function Child (name, age, sex) {
// 继承,同时传递了参数
Parent.call(this, name, age)
this.sex = sex;
}
function object (o) {
var c = {};
for (var i in o) {
c[i] = o[i];
}
return c
}
var prototype = object(Parent.prototype);
prototype.constructor = Child;
Child.prototype = prototype;
var c = new Child('leizore', 11, 'men');
c.sayName() // leizore
c.constructor === Child // true
当然了,es6 中,可以通过extends关键字实现继承,这里就不多说了
参考
- javascript 高级程序设计
- Javascript面向对象编程(二):构造函数的继承
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。