首页 > 网站 > WEB开发 > 正文

[译] 为什么原型继承很重要

2024-04-27 14:09:44
字体:
来源:转载
供稿:网友

[译] 为什么原型继承很重要

javascript是一个多样化的编程语言。它拥有面向对象和函数式的编程特点,你可以使用任何一种风格来编写代码。然而这两个编程风格并不能很好的融合。例如,你不无法同时使用new(典型的面向对象的特点)和apply(函数式编程的特点).原型继承一直都作为连接这两种风格的桥梁。

基于类继承的问题

大部分Javascript程序员会告诉你基于类的继承不好。然而它们中只有很少一部分知道其中的原因。事实实际上是基于类的基础并没有什么不好。 Python是基于类继承的,并且它是一门很好的编程语言。但是,基于类的继承并不适合用于Javascript。Python正确的使用了类,它们只有 简单的工厂方法不能当成构造函数使用。而在Javascript中任何函数都可以被当成构造函数使用。

Javascript中的问题是由于每个函数都可以被当成构造函数使用,所以我们需要区分普通的函数调用和构造函数调用;我们一般使用new关键字来进行区别。然而,这样就破坏了Javascript中的函数式特点,因为new是一个关键字而不是函数。因而函数式的特点无法和对象实例化一起使用。

function Person(firstname,lastname){    this.firstname = firstname ;    this.lastname = lastname ;}

考虑上面这段程序。你可以通过new关键字来调用Person方法来创建一个函数Person的实例:

var author = new Person('Aadit','Shah') ;

然而,没有任何办法来使用apply方法来为构造函数指定参数列表:

var author = new Person.apply(null,['Aadit','Shah']);//error

但是,如果new是一个方法那么上面的需求就可以通过下面这种方式实现了:

var author = Person.new.apply(Person,['Aadit','Shah']) ;

幸运的是,因为Javascript有原型继承,所以我们可以实现一个new的函数:

Function.PRototype.new = function () {    function functor() { return constructor.apply(this, args); }    var args = Array.prototype.slice.call(arguments);    functor.prototype = this.prototype;    var constructor = this;    return new functor;};

在像Java这样对象只能通过new关键字来实例化的语言中,上面这种方式是不可能实现的。

下面这张表列出了原型继承相比于基于类的基础的优点:

基于类的继承原型继承
类是不可变的。在运行时,你无法修改或者添加新的方法原型是灵活的。它们可以是不可变的也可以是可变的
类可能会不支持多重继承对象可以继承多个原型对象
基于类的继承比较复杂。你需要使用抽象类,接口和final类等等原型继承比较简洁。你只有对象,你只需要对对象进行扩展就可以了

不要再使用关键词new了

到现在你应该知道为什么我觉得new关键字是不会的了吧---你不能把它和函数式特点混合使用。然后,这并不代表你应该停止使用它。new关键字有合理的用处。但是我仍然建议你不要再使用它了。new关键字掩盖了Javascript中真正的原型继承,使得它更像是基于类的继承。就像Raynos说的:

new是Javascript在为了获得流行度而加入与Java类似的语法时期留下来的一个残留物

Javascript是一个源于Self的基于原型的语言。然而,为了市场需求,Brendan Eich把它当成Java的小兄弟推出:

并且我们当时把Javascript当成Java的一个小兄弟,就像在微软语言家庭中Visual Basic相对于C++一样。

这个设计决策导致了new的问题。当人们看到Javascript中的new关键字,他们就想到类,然后当他们使用继承时就遇到了傻了。就像Douglas Crockford说的:

这个间接的行为是为了使传统的程序员对这门语言更熟悉,但是却失败了,就像我们看到的很少Java程序员选择了Javascript。 Javascript的构造模式并没有吸引传统的人群。它也掩盖了Javascript基于原型的本质。结果就是,很少的程序员知道如何高效的使用这门语 言

因此我建议停止使用new关键字。Javascript在传统面向对象假象下面有着更加强大的原型系统。然大部分程序员并没有看见这些还处于黑暗中。

理解原型继承

原型继承很简单。在基于原型的语言中你只有对象。没有类。有两种方式来创建一个新对象---“无中生有”对象创建法或者通过现有对象创建。在Javascript中Object.create方法用来创建新的对象。新的对象之后会通过新的属性进行扩展。

“无中生有”对象创建法

Javascript中的Object.create方法用来从0开始创建一个对象,像下面这样:

var object = Object.create(null) ;

上面例子中新创建的object没有任何属性。

克隆一个现有的对象

Object.create方法也可以克隆一个现有的对象,像下面这样:

var rectangle = {    area : function(){        return this.width * this.height ;    }} ;var rect = Object.create(rectangle) ;

上面例子中rectrectangle中继承了area方法。同时注意到rectangle是一个对象字面量。对象字面量是一个简洁的方法用来创建一个Object.prototype的克隆然后用新的属性来扩展它。它等价于:

var rectangle = Object.create(Object.prototype) ;rectangle.area = function(){    return this.width * this.height ;} ;

扩展一个新创建的对象

上面的例子中我们克隆了rectangle对象命名为rect,但是在我们使用rectarea方法之前我们需要扩展它的widthheight属性,像下面这样:

rect.width = 5 ;rect.height = 10 ;alert(rect.area()) ;

然而这种方式来创建一个对象的克隆然后扩展它是一个非常傻缺的方法。我们需要在每个rectangle对象的克隆上手动定义widthheight属性。如果有一个方法能够为我们来完成这些工作就很好了。是不是听起来有点熟悉?确实是。我要来说说构造函数。我们把这个函数叫做create然后在rectangle对象上定义它:

var rectangle = {    create : function(width,height){        var self = Object.create(this) ;        self.height = height ;        self.width = width ;        return self ;    } ,    area : function(){        return this.width * this.height ;    }} ;var rect = rectangle.create(5,10) ;alert(rect.area()) ;

构造函数 VS 原型

等等。这看起来很像Javascript中的正常构造模式:

function Rectangle(width, height) {    this.height = height;    this.width = width;} ;Rectangle.prototype.area = function () {    return this.width * this.height;};var rect = new Rectangle(5, 10);alert(rect.area());

是的,确实很像。为了使得Javascript看起来更像Java原型模式被迫屈服于构造模式。因此每个Javascript中的函数都有一个prototype对象然后可以用来作为构造器(这里构造器的意思应该是说新的对象是在prototype对象的基础上进行构造的)。new关键字允许我们把函数当做构造函数使用。它会克隆构造函数的prototype属性然后把它绑定到this对象中,如果没有显式返回对象则会返回this

原型模式和构造模式都是平等的。因此你也许会怀疑为什么有人会困扰于是否应该使用原型模式而不是构造模式。毕竟构造模式比原型模式更加简洁。但是原型模式相比构造模式有许多优势。具体如下:

构造模式原型模式
函数式特点无法与new关键字一起使用函数式特点可以与create结合使用
忘记使用new会导致无法预期的bug并且会污染全局变量由于create是一个函数,所以程序总是会按照预期工作
使用构造函数的原型继承比较复杂并且混乱使用原型的原型继承简洁易懂

最后一点可能需要解释一下。使用构造函数的原型继承相比使用原型的原型继承更加复杂,我们先看看使用原型的原型继承:

var square = Object.create(rectangle);square.create = function (side) {    return rectangle.create.call(this, side, side);} ;var sq = square.create(5) ;alert(sq.area()) ;

上面的代码很容易理解。首先我们创建一个rectangle的克隆然后命名为square。接着我们用新的create方法重写square对象的create方法。最终我们从新的create方法中调用rectanglecreate函数并且返回对象。相反的,使用构造函数的原型继承像下面这样:

function Square(){    Rectangle.call(this,side,side) ;} ;Square.prototype = Object.create(Rectangle.prototype) ;Square.prototype.constructor = Square ;var sq = new Square(5) ;alert(sq.area()) ;

当然,构造函数的方式更简单。然后这样的话,向一个不了解情况的人解释原型继承就变得非常困难。如果想一个了解类继承的人解释则会更加困难。

当使用原型模式时一个对象继承自另一个对象就变得很明显。当使用方法构造模式时就没有这么明显,因为你需要根据其他构造函数来考虑构造继承。

对象创建和扩展相结合

在上面的例子中我们创建一个rectangle的克隆然后命名为square。然后我们利用新的create属性扩展它,重写继承自rectangle对象的create方法。如果把这两个操作合并成一个就很好了,就像对象字面量是用来创建Object.prototype的克隆然后用新的属性扩展它。这个操作叫做extend,可以像下面这样实现:

Object.prototype.extend = function(extension){    var hasOwnProperty = Object.hasOwnProperty ;    var object = Object.create(this) ;    for(var property in extension){        if(hasOwnProperty.call(extension,property) ||            typeof obejct[property] === 'undefined')            //这段代码有问题,按照文章意思,这里应该使用深复制,而不是简单的浅复制,deepClone(extension[property],object[property]),deepClone的实现可以看我之前关于继承的博客            object[properyty] = extension[property] ;    }    return object ;} ;

译者注:我觉得博主这里的实现有点不符合逻辑,正常extend的实现应该是可以配置当被扩展对象和用来扩展的对象属性重复时是否覆盖原有属性,而博主的实现就只是简单的覆盖。同时博主的实现在if判断中的做法个人觉得是值得学习的,首先判断extension属性是否是对象自身的,如果是就直接复制到object上,否则再判断object上是否有这个属性,如果没有那么也会把属性复制到object上,这种实现的结果就使得被扩展的对象不仅仅只扩展了extension中的属性,还包括了extension原型中的属性。不难理解,extension原型中的属性会在extension中表现出来,所以它们也应该作为extension所具有的特性而被用来扩展object。所以我对这个方法进行了改写:

    Object.prototype.extend = function(extension,override){    var hasOwnProperty = Object.hasOwnProperty ;    var object = Object.create(this) ;    for(var property in extension){        if(hasOwnProperty.call(extension,property) ||             typeof object[property] === 'undefined'){            if(object[property] !== 'undefined'){                if(override){                    deepClone(extension[property],object[property]) ;                }            }else{
发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表