syuko

Just For Fun。生存,社会秩序,娱乐
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

JavaScript基础之继承(二)

Posted on 2008-03-19 13:28  syuko  阅读(3885)  评论(5编辑  收藏  举报


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了。只能暂时放放了。

相关文章:JavaScript基础之对象
         JavaScript基础之继承(附实例)