这篇文章是Yahoo!的一名资深开发人员写的,对于JavaScript的弱类对象及其继承方式讲得非常透彻,文章写得很好,而自己最近又很有点翻译欲望,于是也一并翻译过来了。另外,MooTools 1.2.1已经发布了,修复了一些bug。
原文地址:JavaScript’s class-less objects
请尊重个人劳动,转载请注明出处:http://fdream.net,译者:Fdream
Java和JavaScript是相差极大的两种语言,尽管他们的名字非常像,而且都有类C的语法风格,很多时候这让人们很迷惑。(Fdream注:曾有人在论坛上问Java和JavaScript是什么关系?我一师兄的回答非常经典:雷锋和雷峰塔的关系。)我们来看看两者最主要的区别——对象是怎样创建的。在Java中,你有类。然后是对象,又叫实例,都是基于那些类建立的。而在JavaScript中,没有类存在,对象更像是一个包含键值对(key-value pair)的哈希表(hash table)。然后继承是什么样的呢?好,我们一步一步来。
JavaScript对象
当你考虑一个JavaScript对象时,想一下hash。它们和对象完全一样——它们都是名值对(name-value pair)集合,值可以是其它任何东西,包括对象和函数。当一个对象的属性是函数的时候,你也可以叫它们方法。
这是一个空对象:
var myobj = {};
现在,你可以开始给这个对象添加一些有意义的功能:
myobj.name = "My precious";
myobj.getName = function() {return this.name};
注意这样一些事情:
- 在方法中,this指向当前对象,和期望一样
- 你可以在任何时候添加、修改、删除属性,不限于创建的时候
另一种创建对象并同时添加属性或者方法的方式是这样的:
var another = {
name: 'My other precious',
getName: function() {
return this.name;
}
};
这种语法就叫做“对象枚举表示法”(object literal notation)——你把所有的东西都包含在花括号 { 和 } 之间,并用逗号在对象内部区分每个属性。键值对(key:value pair)则以冒号分割。这中语法也不是创建对象的唯一方式。
构造函数
另一种创建JavaScript对象的方式就是使用构造函数(constructor function)。这里是一个构造函数示例:
function ShinyObject(name) {
this.name = name;
this.getName = function() {
return this.name;
}
}
现在,我们可以更像Java那样创建一个对象:
var my = new ShinyObject('ring');
var myname = my.getName(); // "ring"
创建一个构造函数的语法和其他函数没有任何区别,唯一的区别是它们的用法不一样。如果你用new关键字来调用一个方法,它会创建并且返回一个对象。通过使用this关键字,你可以在它返回之前修改这个对象。作为约定俗成的习惯,构造函数的命名通常以一个大写字母开头,以区分于其他一般函数和方法。
哪一种方式更好呢?对象枚举还是构造函数?这完全取决于你指定的任务。例如,如果你需要创建许多不同的,但是类似的对象,使用类类(class-like)的构造函数可能才是正确的选择。但是,如果你的对象更接近于一个单例(singleton),对象枚举方式肯定要更简单更简短。
好了,如果没有类,那么哪来继承呢?在我们回答这个问题之前,这里还有一点惊喜——在JavaScript中,函数(function)也是实际对象。
(实际上,在JavaScript中,几乎所有东西都是一个对象,出了一些元数据类型——字符串(string)、布尔值(boolean)、数字(number)和undefined。函数(function)是对象,数组(array)是对象,甚至null也是一个对象。而且,元数据类型也可以转换并作为对象使用,因此”string.length“是有效的。)
函数对象和原型对象
在JavaScript中,函数是对象。他们可以赋值给变量,你可以给它们添加属性和方法等等。这里是一个函数的示例:
var myfunc = function(param) {
alert(param);
};
这和下面的几乎一样:
function myfunc(param) {
alert(param);
}
不管你通过什么方式创建这个函数,它最后都成为了一个myfunc对象,你可以得到它们的属性和方法:
alert(myfunc.length); // 显示 1, 参数个数
alert(myfunc.toString()); // 显示这个函数的源代码
一个有趣的属性是——每个函数对象都有一个prototype属性。一旦你创建一个函数,它就会自动获得一个prototype属性,这个属性指向一个空的对象。当然,你可以修改那个空对象的属性。
alert(typeof myfunc.prototype); // 显示 "object"
myfunc.prototype.test = 1; // 这是完全可以的
问题是:这个原型对象有什么用呢?只有当你把一个函数作为构造函数调用来创建一个对象时有用。当你这么做的时候,这个对象自动地获得一个秘密链接指向原型对象的属性,并可以把这些属性当作自己的属性一样访问。迷惑了?让我们看一个例子:
一个新函数:
function ShinyObject(name) {
this.name = name;
}
给这个函数的原型属性增加一些功能:
ShinyObject.prototype.getName = function() {
return this.name;
};
把这个函数作为构造函数使用,来创建一个对象:
var iphone = new ShinyObject('my precious');
iphone.getName(); // returns "my precious"
正如你所看到的,新的对象自动获得了原型对象的属性。当一些功能可以”免费“地获得的时候,这就有点像是代码重用和继承了。
通过原型继承
现在我们来看看,你如果通过使用原型来实现继承。
这里是一个构造函数,将会作为父类(parent)使用:
function NormalObject() {
this.name = 'normal';
this.getName = function() {
return this.name;
};
}
这里是第二个构造函数:
function PreciousObject(){
this.shiny = true;
this.round = true;
}
现在是继承部分:
PreciousObject.prototype = new NormalObject();
可不是嘛!现在你可以创建一个珍宝(precious)对象,然后它们会得到所有普通物品(normal)对象的功能:
var crystal_ball = new PreciousObject();
crystal_ball.name = 'Ball, Crystal Ball.';
alert(crystal_ball.round); // true
alert(crystal_ball.getName()); // "Ball, Crystal Ball."
注意到我们为什么需要使用new来创建一个对象,然后把它赋值给原型,因为原型仅仅只是一个对象。不像一个构造函数继承于其它的,本质上,我们从一个对象继承。JavaScript没有类从其他类继承,只有对象从其他对象继承。
如果你有几个构造函数都要从NormalObject继承,你也许需要每次都创建一个new NormalObject(),但是这是没有必要的。甚至连整个NormalObject构造函数都不是必须的。另外一种实现方式就是创建一个单例普通对象,然后把它作为其他对象的基类使用。
var normal = {
name: 'normal',
getName: function() {
return this.name;
}
};
然后PreciousObject就可以像这样继承了:
PreciousObject.prototype = normal;
通过复制属性继承
因为继承只是为了代码重用,因此还有一种实现方式就是简单地复制属性。
假设你有这些对象:
var shiny = {
shiny: true,
round: true
};
var normal = {
name: 'name me',
getName: function() {
return this.name;
}
};
怎样让shiny得到normal的属性呢?这里有一个简单的extend()函数,可以循环遍历并赋值属性:
function extend(parent, child) {
for (var i in parent) {
child[i] = parent[i];
}
}
extend(normal, shiny); // inherit
shiny.getName(); // "name me"
现在这个属性赋值看起来像是额外的开销,而且性能也不是很好,但是事实是,在大多数情况下它还是很好的。你也可以看见——这是一种实现混合继承和多重继承的简单方式。
Crockford的beget object
Douglas Crockford,一代JavaScript大师,JSON的创造者,提出了这样一种有趣的begetObject()方式来实现继承:
function begetObject(o) {
function F() {}
F.prototype = o;
return new F();
}
这里你创建了一个临时构造函数,因此你可以使用原型功能,这个目的在于你创建了一个新的对象,不过不是一个全新的对象,而是从其它对象那里继承了一些已经存在的功能。
父对象:
var normal = {
name: 'name me',
getName: function() {
return this.name;
}
};
一个从父对象继承的新对象:
var shiny = begetObject(normal);
给这个新对象增加更多功能:
shiny.round = true;
shiny.preciousness = true;
YUI的extend()
让我们来总结一下另一种方式来实现继承,这可能是最接近Java的,因为在这种方法中,它看起来像一个构造函数继承自其它构造函数,因此她看起来有一点像从一个类继承。
在非常受欢迎的YUI JavaScript库(Yahoo! User Interface)中已经使用了这种方法,这里是一个简单的版本:
function extend(Child, Parent) {
var F = function(){};
F.prototype = Parent.prototype;
Child.prototype = new F();
}
通过这个方法,你传递两个构造函数,第一个(子类)将会通过原型(prototype)属性得到第二个(父类)的所有属性和方法。
总结
让我们很快地总结一下我们刚才所学的有关JavaScript的内容:
- JavaScript中没有类
- 对象从对象继承
- 对象枚举表示法 var o = { };
- 构造函数提供类Java语法 var o = new Object();
- 函数是对象
- 所有的函数对象都有一个prototype属性
- 最后,有很多方式来实现继承,你可以任意挑选,这完全取决于你的手头任务、你个人喜好、团队喜好、你的心情或者当前的月相。
作者及声明
Stoyan Stefanov是一名资深Yahoo!开发者,YSlow工具的领导、开源贡献者,博客作者和技术作者,最近由Packt出版的《Object-Oriented JavaScript》(《面向对象的JavaScript》)的作者。
shiny object的例子灵感来源于Jim Bumgardner的《Theory of the Precious Object 》。