面向对象之继承
JavaScript是单根的面向对象语言,它只有单一的根Object,所有的其他对象都是直接或者间接的从Object对象继承(没有指定父类的对象,都被认为是从Object继承的)。
在前面我们讨论了面向对象的封装性,在最后的地方也谈到了JavaScript的继承是通过原型和原型链实现的,下面我们就详细的展开这个问题:JavaScript到底是如何实现继承的?
继承的本质
继承的本质是重用,从语法上来讲,继承就是"D是B"的描述,其中B是基类,描述共性,D是子类,描述特性。例如"猫是动物",动物就是基类,猫是子类,动物的共性,比如会呼吸,猫也会,但是猫的特性,如打呼噜,别的动物就不一定有了。
JavaScript对象的继承方式
JavaScript对象的继承就是实现对象重用的过程,总的来说有两种方式:原型继承与复制继承。
原型继承
继承是重用,那么关键就是获取基类的成员,包括对象自身定义的成员,以及对象的原型对象上定义的成员。于是经典的做法便是:
// 父类的构造函数 function Base(name) { // 定义在对象内的实例成员 this.name = name; this.showName = function() { alert(this.name); }; }; // 定义在原型上的实例成员 Base.prototype.setName = function(name) { this.name = name; }; // 子类的构造函数 function Derived(name, age) { // 继承关键1: 获取父类对象中定义的成员 Base.call(this, name); // 添加子类自己的成员 this.age = age; }; // 继承关键2:获取父类原型中定义的成员 Derived.prototype = new Base(); // 继承关键3:把原型的constructor属性值改为正确的Derived. // 这么做的原因是原型会把它的constructor属性设置为构造函数, // 如果手动修改了原型的话,当然需要把这个值再设回去。 Derived.prototype.constructor = Derived; // 添加子类的成员到原型上去 Derived.prototype.showAge = function() { alert(this.age); }; var d1 = new Derived(‘Frank‘, 30); d1.showName(); d1.setName(‘Dong‘); d1.showName(); d1.showAge();
上面的做法很直接,就是使用call/apply调用获取父类构造函数定义的成员,然后通过new操作拿到父类原型上的成员,唯一需要注意的就是由于把子类整个的原型都设置为了父类的原型,为了保证new子类对象的时候能正确的找到其构造函数,需要把原型对象的constructor属性设置成子类的构造函数。
这个方案几乎完美,不过却有一个小问题,那就是Base的构造函数调用了两次,特别是第二次调用使用了new方式,多创建了一个Base对象,这是一种浪费。
针对这个问题,有很多方式去改进,比如为了继承父类原型上的成员,有的人是这样写的:
Derived.prototype = Base.prototype;
其它的代码不变,这么做提高了内存使用量,不过缺点是Derived与Base的原型对象指向同一个对象,这样一旦想修改Derived的原型的时候,就修改了Base的原型了,不符合面向对象的规范,所以一般没人这么写。
那么如何解决这个这个问题呢?通常来讲,解决这个方法的手段是使用空对象来代替多new出来的那个Base对象:
function extend(Child, Parent) { // 使用了空对象来减少内存使用量 var F = function(){}; F.prototype = Parent.prototype; Child.prototype = new F(); Child.prototype.constructor = Child; }; function Derived(name, age) { // 获取父类对象上的成员
// 其实直接写上this.name = name;的话就能省略call方法了 Base.call(this, name); this.age = age; }; // 获取父类原型上的成员 extend(Derived, Base); Derived.prototype.showAge = function() { alert(this.age); };
这个据说是YUI的继承方式,不过本人没分析过,不做说明。不过通常来说,我们只会在构造函数中放一些非函数的数据(函数为了共享,一般放到原型中),所以一般只需要把这些数据赋值的语句参照父类直接写一下,这样基本上就不用使用call方法了,这样只使用一个extend方法就足够了,还算通用。
上面父类的方法调用成功了,说明继承成功。上面我们说了,继承是一种是的关系,在JavaScript中,验证这种关系可以使用instanceof操作符:
var d1 = new Derived(‘Frank‘, 30); alert(d1 instanceof Base);
上面返回true说明语义上上面的实现是满足继承关系的。
复制继承
其实既然继承是重用,那么最直接的方式其实是复制,就是直接把父类的成员和父类原型的成员全部拷贝到子类和子类的原型中不就可以了。这个方案确实是可行的,而且特别适用于没有构造函数的情况,看下面的例子:
// 浅拷贝实现 function extendCopy(p) { var c = {}; for (var i in p) { c[i] = p[i]; } return c; } // 深拷贝实现 function deepCopy(p, c) { var c = c || {}; for (var i in p) { if (typeof p[i] === ‘object‘) { c[i] = (p[i].constructor === Array) ? [] : {}; deepCopy(p[i], c[i]); } else { c[i] = p[i]; } } return c; } // 基类的对象表示 var Base = { name: ‘Frank‘, showName: function() { alert(this.name); } } // 子类先复制基类成员 var Derived = extendCopy(Base); // 子类添加自己的成员 Derived.age = 10; Derived.showAge = function() { alert(this.age); }; // 使用基类的方法 Derived.showName();
上面的代码有很关键的一点需要说明的:由于JavaScript也存在类似于值类型和引用类型的结构,所以也就有相似的浅拷贝和深拷贝的区别,当遇到对象的时候,深拷贝可以把所有的成员全部复制一遍,上面的例子中把两种方式的实现都给出了,而且使用了简单的浅拷贝实现了继承关系,实际使用中,使用深拷贝会更加安全。
JavaScript继承的实现方式是多种多样的,实际的项目中可以灵活使用。
继承是为了重用,同时也是为了在此基础上让子类去表征变化,这就是对象的多态性。
面向对象之多态
JavaScript语言中多态与别的语言并没多大的不同,继承来的对象可以*修改继承的方法,达到多态的效果。
但是值得一提的是,面向对象中我们总是提倡针对接口编程,这里的接口就是一种约束关系。在JavaScript中,由于是动态语言,静态的约束被压缩到极致,这使得我们不再需要去预先定义接口(广义的接口包括狭义的语法上的接口,抽象类或者基类等等),然后针对接口编程了,看一个简单的例子:
var tank = { run : function () { alert(‘tank run‘); } }; var person = { run : function () { alert(‘person run‘); } }; // 针对接口(run方法)的对象编程 function trigger(target) { target.run(); } trigger(tank); trigger(person);
对象的继承机制目前就是这么多了。