JavaScript大杂烩4 - 理解JavaScript对象的继承机制
JavaScript是单根的完全面向对象的语言
JavaScript是单根的面向对象语言,它只有单一的根Object,所有的其他对象都是直接或者间接的从Object对象继承。而在JavaScript的众多讨论中,JavaScript的继承机制也是最让人津津乐道的,在了解它的机制之前,先让我们温习一下继承的本质。
继承的本质
继承的本质是重用,就是这么简单,重用是软件编程技术向前发展最原始的动力。从语法上来讲,继承就是"D是B"的描述,其中B是基类,描述共性,D是子类,描述特性。例如"猫是动物",动物就是基类,猫是子类,动物的共性,比如会呼吸,猫也会,但是猫的特性,如打呼噜,别的动物就不一定有了。
JavaScript的原型继承机制
前面我们已经总结过了原型的作用,原型链继承是JavaScript实现继承的主要方式。这种继承的典型实现方式如下:
function Base(name) { // 基对象构造函数实例成员 this.name = name; }; // 基对象原型中的实例成员 Base.prototype.showName = function() { alert(this.name); }; // 子对象的构造函数 function Derived(name, age) { // 关键点1:获取基对象构造函数中的成员 Base.call(this, name); // 定义子对象自己的成员 this.age = age; }; // 关键点2:获取基对象原型上的成员 Derived.prototype = new Base(); // 关键点3:将子对象的原型的构造函数属性设回正确的值 // 这样可以防止new对象的挂接对象出错 Derived.prototype.constructor = Derived; // 扩展子类自己的原型成员 Derived.prototype.showAge = function() { alert(this.age); }; var d = new Derived('Frank', 10); d.showName(); d.showAge(); // 验证继承关系 alert(d instanceof Base);
上面的实现关键点已经标了出来:
关键点1:使用call方式来调用基对象的构造函数,这样子对象就能按照基对象构造函数的逻辑来构造一份基对象构造函数中的成员。
关键点2:使用new方式来创建一个新的Base对象,然后把它挂到Derived对象的原型上,这样从"Derived对象->Derived原型->Base对象->Base原型->Object对象->Object原型"就形成了链条了。
关键点3:new操作符要求构造函数对象的原型的constructor属性要指向构造函数,而在关键点2中把Derived对象的原型完全换成Base对象了,这样有问题了。修复这个问题也很简单,直接把这个属性重新设一下就行了。
上面的实现近乎完美,只有一个问题:Base对象的构造函数被调用了2次,一次在构造函数中,一次在构造原型链中,这样的效率并不高。而根据上面的几点说明我们知道第一次调用是为了复制基对象构造函数中的成员,必不可少;而第二次的调用纯粹是为了把原型链接上,构造函数中的成员并没有使用上,于是这里就存在一个优化的契机:既然构造函数中的成员没有使用到,那我就用一个空对象来辅助创建原型链不就就可以了,看下面的代码:
// 利用空对象来挂接原型链 function extend(Child, Parent) { var F = function(){}; F.prototype = Parent.prototype; Child.prototype = new F(); Child.prototype.constructor = Child; } function Base(name) { this.name = name; }; Base.prototype.showName = function() { alert(this.name); }; function Derived(name, age) { Base.call(this, name); this.age = age; }; // 挂接原型链 extend(Derived, Base); Derived.prototype.showAge = function() { alert(this.age); }; var d = new Derived('Frank', 10); d.showName(); d.showAge(); alert(d instanceof Base);
这个版本的实现据说是YUI的实现继承的方式,个人并没参看其源代码,有这个爱好的同学可以自行研究一下。
复制继承
既然继承的本质是复用,那么最直接的想法应该是复制基对象的所有成员,在JavaScript中确实可以这么做,看下面的代码:
// 浅拷贝实现 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();
在上面的代码中,我们需要注意两个问题:
1. 我们从前面已经了解过,JavaScript有简单的“值类型”和复杂的“引用类型”,这样复制成员的时候就也有所谓的“浅拷贝”和“深拷贝”的说法。上面的代码实现了两种算法,深拷贝本质上就是为引用类型创建新的对象,这样修改的时候就不会误操作,修改了其他对象的成员。
2. 这种对象的扩展方式不能使用instanceof来检查继承关系,例如下面的代码是无效的:
alert(Derived instanceof Base);
运行这段代码会返回异常:Uncaught TypeError: Expecting a function in instanceof check, but got #<Object> 。这是因为instanceof只能用于检查函数实现的那种继承关系。
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);
很多人对于这种使用方式不以为然,但是个人觉得这正是动态语言快捷编程的特点,很多时候还是很方便的。