JavaScript基础之继承
JavaScrip是一个无类、面向对象的语言。因为如此,它采用了原型的继承方式取代了传统的继承方式。这就会使那些接受传统面向对象语言(像C++,Java)教育的程序员们迷惑了。JavaScript的原型继承方式使它比传统的继承方式具有更强的表现力。这点我们马上就要看到了。
Java |
JavaScript |
强类型 |
弱类型 |
静态的 |
动态的 |
传统的继承 |
原型的方式 |
类 |
函数 |
构造器 |
函数 |
方法 |
函数 |
但是首先,为什么我们这么在意继承呢?有两个主要原因。第一,类型转换。语言系统能自动地将实例转换成相似的类型。JavaScript里对象的实例从来就不需要强制转换。第二,代码复用。写出很多重复实现相同方法的对象是非常常见的。而类就可以实现同样的效果而只需定义函数一次。拥有很多类似的对象也是非常常见的,这些对象只是在方法的个数和参数上有些细微的不同。传统的继承方式对解决这个问题非常有用,但是原型继承方式比这更为优雅。为了证明这点,我将用类似于传统面向对象语言的方式写一个"小玩意儿",然后我将展示一些在传统面向对象语言中没有但非常有用的方式。最后,我将解释这个"小玩意儿"。
传统继承
首先,我将写一个名叫Parenizor的类。它有set和get方法去获取类的值,一个toString方法把它的值包裹在一对小括号里面然后返回。
function Parenizor(value)
{
this.setValue(value);
}
Parenizor.method('setValue', function (value)
{
this.value = value;
return this;
});
Parenizor.method('getValue', function ()
{
return this.value;
});
Parenizor.method('toString', function ()
{
return '(' + this.getValue() + ')';
});
这个语法有点不同寻常,但很容易就会发现里面有面向对象语言的痕迹。method方法需要一个方法名和一个函数两个参数,它将传入的方法名称添加为类的一个公共方法。
我们可以这样使用:
myParenizor = new Parenizor(0);
myString = myParenizor.toString();
myString的值为:"(0)",将值0包裹在一对小括号里面。
现在我要写另外一个类,它继承自Parenizor。但它的toString方法和Parenizor类的toString方法不一样。如果值为0或为空,则toString方法就返回"-0-";
function ZParenizor(value)
{
this.setValue(value);
}
ZParenizor.inherits(Parenizor);
ZParenizor.method('toString', function ()
{
if (this.getValue())
{
return this.uber('toString');
}
return "-0-";
});
inherits 类似于Java的extends关键字。uber方法类似于Java的super方法。它使得子类可以去调用父类中的方法。(方法名称必须和保留字、关键字区别开。)
我们可以这样使用:
myZParenizor = new ZParenizor(0);
myString = myZParenizor.toString();
这时,myString返回的是"-0-"。
JavaScript没有类,但是我们能去模拟它。
多重继承
通过控制一个函数的prototype对象,我们能实现多重继承,使一个类继承自多个类。滥用多重继承实现起来会非常困难而且可能会造成名称冲突。我们可以用JavaScript实现杂乱的多重继承。但是在下面的例子中我将会使用一种更规范的方式——Swiss Inheritance。
假如有一个NumberValue类,它有一个setValue方法来验证值是否在一个确定的区间内,可能的话还会抛出一个异常。在ZParenizor类里我只需要setValue方法和setRange方法。我不需要toString方法。因此,我可以这样写:
ZParenizor.swiss(NumberValue, 'setValue', 'setRange');
这样就只向类中添加了需要的方法。
寄生继承
有另外一个方法去重写ZParenizor,而不继承自Parenizor类。我们在这个类里面写一个构造函数去调用Parenizor的构造函数,然后在构造函数中添加私有的方法而不是公有的。代码如下:
function ZParenizor2(value)
{
var that = new Parenizor(value);
that.toString = function ()
{
if (this.getValue())
{
return this.uber('toString');
}
return "-0-"
};
return that;
}
传统的继承是is-a关系,寄生的继承是was-a-but-now's-a关系。构造函数在构造类的时候起到了非常重要的作用。
类扩展
JavaScript的特性使我们可以添加或替换一个已经存在的类的方法。我们可以在任何时候调用method方法,新添加或替换的方法将会出现在类所有的实例中。我们可以在任何时候去扩展一个类。继承会追本溯源。
对象扩展
在面向对象的语言里,如果你需要一个类,而这个类与另一个只有细微的差别。你就不得不去定义一个新的类。在JavaScript里,你可以向一个类里面添加方法而不是去添加新的类。这有非常大的好处,因为你可以少写很多类并且类将变得非常简单。在JavaScript里对象就像是一个哈希表(hashtable),你可以随时随地地向里面添加值。如果值是一个函数,则他会变成一个方法。
因此对于上面的例子,我完全不需要再去定义一个ZParenizor类。我只需要去稍微的修改一个我的实例。
myParenizor = new Parenizor(0);
myParenizor.toString = function ()
{
if (this.getValue())
{
return this.uber('toString');
}
return "-0-";
};
myString = myParenizor.toString();
我向myParenizor实例里面添加了一个toString方法而不用去使用任何形式的继承。我们可以扩展单独的类实例。
小玩意儿
为了使上面的例子能够运行,我写了四个小方法:
method方法。method方法用于向一个类添加方法。
Function.prototype.method = function (name, func)
{
this.prototype[name] = func;
return this;
};
这样就向Function对象的原型里面添加了一个公共的方法,然后所有的函数都会通过类扩展得到这个方法。它需要一个函数名和一个函数,然后将这个函数添加到对象的原型里面。它返回的是this。当我写一个函数但不想让它放回一个值,通常地,我就会让它返回一个this。
下一个就是inherits方法,它表明一个类继承自另外一个。他可以在类被定义了,但类的方法还没有添加的时侯去继承这个类。
Function.method('inherits', function (parent)
{
var d = {}, p = (this.prototype = new parent());
this.method('uber', function uber(name)
{
if (!(name in d))
{
d[name] = 0;
}
var f, r, t = d[name], v = parent.prototype;
if (t)
{
while (t)
{
v = v.constructor.prototype;
t -= 1;
}
f = v[name];
}
else
{
f = p[name];
if (f == this[name])
{
f = v[name];
}
}
d[name] += 1;
r = f.apply(this, Array.prototype.slice.apply(arguments, [1]));
d[name] -= 1;
return r;
});
return this;
});
再一次,我扩展了函数这个对象。我实例化了一个父类,然后用它里面的方法作为一个新的原型。我同样也修改了它的构造函数并向它的原型里面添加了uber函数。
uber方法看起来就像是它自己原型里面的方法一样。这个函数援引了寄生继承或对象扩展。如果我们采用传统的继承,我们就不得不在父类的原型中去寻找函数。return语句用了函数的apply方法去调用函数,通过设置一个函数的参数数组。参数都从这个参数数组里面获取。
最后就是swiss方法。
Function.method('swiss', function (parent)
{
for (var i = 1; i < arguments.length; i += 1)
{
var name = arguments[i];
this.prototype[name] = parent.prototype[name];
}
return this;
});
swiss方法循环了参数数组。它从父类的原型中拷贝了每一个所需的函数到新类的原型中。
小节
JavaScript可以像传统面向对象的语言那样来用,但它有其独有的表达方式。纵观传统继承,Swiss继承,寄生继承,类扩展和对象扩展,这些大量的代码重用方式都是来自于一个被认为是Java缩略版、雕虫小技的JavaScript语言实现的。
正因为JavaScript里的对象是如此的灵活,你就会去想不同的类结构。复杂的类结构是不合适的;简单的类结构就能胜任和具有丰富的表达力。
后记
这篇文章是一篇翻译的文章。昨天发了一篇名为"JavaScript基础之继承(附实例)"的帖子,也是说JavaScript继承的。但写那篇帖子的时候没有看见这篇文章,不然就能将两篇帖子合二为一了。这两篇文章从不同的角度说明了继承的实现,结合起来看比较好。
JavaScript基础写到这里共三篇,就不再写了。一是看到标准里面有那么多好的规范但是各浏览器却在很多方面"开小灶",看着有些难受。二是女朋友现在在外面实习学习asp.net,晚上回去得给她讲asp.net也没有时间研究JavaScript了。只能暂时放放了。