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
关键字掩盖了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) ;
上面例子中rect
从rectangle
中继承了area
方法。同时注意到rectangle
是一个对象字面量。对象字面量是一个简洁的方法用来创建一个Object.prototype
的克隆然后用新的属性来扩展它。它等价于:
var rectangle = Object.create(Object.prototype) ;rectangle.area = function(){ return this.width * this.height ;} ;
上面的例子中我们克隆了rectangle
对象命名为rect
,但是在我们使用rect
的area
方法之前我们需要扩展它的width
和height
属性,像下面这样:
rect.width = 5 ;rect.height = 10 ;alert(rect.area()) ;
然而这种方式来创建一个对象的克隆然后扩展它是一个非常傻缺的方法。我们需要在每个rectangle
对象的克隆上手动定义width
和height
属性。如果有一个方法能够为我们来完成这些工作就很好了。是不是听起来有点熟悉?确实是。我要来说说构造函数。我们把这个函数叫做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()) ;
等等。这看起来很像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
方法中调用rectangle
的create
函数并且返回对象。相反的,使用构造函数的原型继承像下面这样:
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{
新闻热点
疑难解答