JavaScript的弱类对象及继承实现方式

这篇文章是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 》

参考《Douglas Crockford’s beget object》

 

发表评论

您的电子邮箱地址不会被公开。

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据