JavaScript的面向对象继承设计是个技术活,它没有Java或C++那样的想当然,JavaScript的继承是基于原型和原型链的机制,在你使用JS对象或者继承的时候都需要稍微注意一下原型链的问题,本节主要是集中讨论JS的继承,如果你对原型和原型链了解不多或者理解不够,推荐参考上一节的JavaScript原型和原型链的机制和原理详解,里面也有很清晰的原型图解,最好能根据这些图片得到原型链的正确印象。
在讨论继承之前,我们得首先问一下:为什么要继承?编程要求需要继承就使用继承?当然不是,我们要回到最开始的地方,继承是为了复用代码,什么是复用代码?例如你写了一个普通用户的对象,然后又写了一个超级用户的对象,等下你就会发现,普通用户和超级用户很多属性和方法存在大量相似的地方,这时肯定就感觉这代码冗余了,解决办法就是使用继承,即使讨论JavaScript的继承都不能离开这个主题。
一、最基本的继承模式:原型链继承模式
这是一个最重要的继承模式,其它继承模式都是基于这个模式进行优化的。说回到上一节谈到的关于原型的实质:两个对象A和B,在对象A上添加__proto__属性值为B对象,那么A就可以共享B对象的所有属性和方法了,这就是原型和原型链的最简要的解释。
那么我们要实现代码复用,复用的对象就是B,即使用A对象复用或共享B的属性和方法,答案已经给出了:就是让A.__proto__=B。先看回到实际代码,实际上我们使用或称呼原型对象的方式是Function.prototype(Function为构造函数名,要记得Function.prototype是一个对象),假设C构造函数和D构造函数,那么这时要实现继承的就是这么写:(C.prototype).__proto__ = (D.prototype),不过__proto__属性不是一个正式的属性不推荐用,这时用C构造的对象就能共享D原型对象的属性和方法了。既然不用__proto__,那么这样行不行:(C.prototype)=
(D.prototype),不行!这时会造成C和D构造的对象都共享一个原型了,整个原型链就变得混乱了。那么如何实现?这样吧,在中间接一个对象,(C.prototype) = (new D()),这时候(new D())的__proto__就等于(D.prototype)了,以后new一个C对象就能共享D.prototype的属性和方法了,结合下图理解一下:
这就是最基本的原型继承模式,它的特点就是使用一个已存在的对象在中间接上原型链,这里这个对象刚好是来自D()的一个实例,其它高级的继承都是基于这个模式,这也是对原型链的一个应用,如果你还不清楚原型链强烈推荐你去看上一节的内容,否则有可能不知道自己写的代码是什么意思。
下面我们看一个实际的例子(注意代码的注释有详细解释):
// 父类构造函数,原型Basic.prototype
function Basic(name, age){
this.name = name;
this.age = age;
}
// 子类构造函数,原型User.prototype
function User(name, age){
this.name = name;
this.age = age;
}
// 给父类原型添加login方法
Basic.prototype.login = function(){
console.log(this.name + " " + this.age + " Login...");
}
// 将子类原型替换成父类的一个实例对象,注意,这里是new,它是一个基于父类原型的一个实例对象
User.prototype = new Basic();
User.prototype.constructor = User; // 将子类原型的构造函数改为User
var user = new User("Principle", 90);
user.login();
/**
* user的原型指向为:
* user.__proto__ => {{User.prototype==[new Basic()] =>[new Basic()].__proto__}} => Basic.prototype
* {{}}为中间对象
*/
你可能会注意到父类Basic的构造函数没有被子类使用到,而且又new了一次Basic,里面的属性均为undefined,这就造成了一个不必要的浪费,这是这个模式的主要问题,不过并没有说这个模式不可以用,这是可以用的,不要过于在乎模式的优点缺点,用了你自然就会知道哪里有问题以及如何修改。
二、原型继承链继承模式的升级:借用构造函数调用
也称为组合继承模式,但是名字太多太杂,靠记住名字恐怕不是一个好方法。这个模式的独特之处在于借用构造函数调用,上一个模式的问题之一是子类没有使用父类的构造函数,那么这个模式就是简单地使用借用函数调用修复这个问题,函数借用调用可以使用apply()和call(),对于这个两个函数的区别和使用可以参考 apply()函数和call()函数的区别和作用。如何使用借用构造函数复用父类构造函数呢?很简单,将上面的代码中子类的构造函数改成下面即可:
// 子类构造函数,原型User.prototype
function User(name, age){
Basic.call(this, name, age); // 使用借用调用父类Basic构造函数,即可复用父类的构造函数
}
原型模式还有另一个问题就是,new了一次Basic(),里面的属性没有用到而变得冗余了,如何让子类User的原型对象无冗余地接上父类的原型对象?既然冗余,那不如直接去掉new
Basic()中的属性就好了,但是它是Basic的一个对象啊!那就让它别做Basic的对象,没错!最佳的继承就是这样的,让中间对象仅作为一个桥梁,一个简单的空对象。
三、JavaScript最佳继承模式:寄生组合模式
寄生组合模式,就是原型模式、工厂方法和借用构造函数调用的一个混杂名称,但是不用过于执着这些名字(如果你设计了一个不错的模式,你也可以让别人记的)。
这个最佳继承模式就是上面说的,将中间的那个对象设计成一个空对象(与子类和父类的原型都无关)就行了,照着原型链的图马上就能写出来了,不过首先要拿定设计的原型对象是哪几个,目的是更改User.prototype的这个子类原型对象,父类原型对象是Basic.prototype,中间new一个空对象,让这个对象右边指向父类原型对象,左边更改User的原型对象,看图(设新对象的构造函数为F()):
实现代码就很简单了,将上面代码的设置子类原型对象的地方修改为:
// User.prototype = new Basic();
function F(){} // 空对象的构造函数
F.prototype = Basic.prototype; // 将空对象的原型修改为父类Basic的原型
var obj = new F(); // 创建空对象实例
User.prototype = obj; // 将子类的原型修改为空对象实例
User.prototype.constructor = User; // 将子类原型的构造函数改为User
有人将构造这个空对象的过程封装成一个方法,又称为原型式继承或寄生继承模式,这个方法可以抽象成构建一个实例对象继承自已存在的实例对象,代码如下:
// obj为已存在的实例对象
function createObject(obj){
function F(){}
F.prototype = obj;
return new F(); // new出来的对象继承自obj对象
}
在这个最佳继承模式寄生组合模式中,就是为了要找到一个空对象继承自父类原型对象,只是这个方法的一个特殊情形,使用这个方法传入Basic.prototype即可创建目标的空对象了,JavaScript中已经有一个这样的方法就是Object.create(),你可以使用这个方法更简单实现继承。
另外要记得将子类的原型对象的constructor属性指回自己的构造函数,因为我们是使用自己构造函数来创建对象实例的,不然子类就没有构造函数了。如果在原型链上创建过多的原型对象会造成性能问题,例如对不存在的属性访问会遍历整个原型链(可以使用hasOwnProperty()过滤),因此尽量不要无节制添加原型对象。
到此,JavaScript的继承和原型链就讲完了,最好能结合上节的原型和原型链机制进行理解,在这个问题上得到解决,以后编写代码就不用过于烦恼了,不然的话问题依然存在,遇到问题还是不能理解和迅速解决。