五天以前我写了一个关于ES6标准中Class的文章。在里面我介绍了如何用现有的Javascript来模拟类而且介绍了ES6中类的用法,其实它只是一个语法糖。感谢Om Shakar以及Javascript Room中的各位,个人编程风格从那时候开始发生了改变;就像Dougla Crockford2006年作的同样,我也学习了不少来彻底理解基于原型的编程方式。javascript
Javascript是一个多样化的编程语言。它拥有面向对象和函数式的编程特色,你可使用任何一种风格来编写代码。然而这两个编程风格并不能很好的融合。例如,你不没法同时使用new
(典型的面向对象的特色)和apply
(函数式编程的特色).原型继承一直都做为链接这两种风格的桥梁。html
大部分Javascript程序员会告诉你基于类的继承很差。然而它们中只有不多一部分知道其中的缘由。事实其实是基于类的基础并无什么很差。Python是基于类继承的,而且它是一门很好的编程语言。可是,基于类的继承并不适合用于Javascript。Python正确的使用了类,它们只有简单的工厂方法不能当成构造函数使用。而在Javascript中任何函数均可以被当成构造函数使用。java
Javascript中的问题是因为每一个函数均可以被当成构造函数使用,因此咱们须要区分普通的函数调用和构造函数调用;咱们通常使用new
关键字来进行区别。然而,这样就破坏了Javascript中的函数式特色,由于new
是一个关键字而不是函数。于是函数式的特色没法和对象实例化一块儿使用。git
function Person(firstname,lastname){ this.firstname = firstname ; this.lastname = lastname ; }
考虑上面这段程序。你能够经过new
关键字来调用Person
方法来建立一个函数Person
的实例:程序员
var author = new Person('Aadit','Shah') ;
然而,没有任何办法来使用apply
方法来为构造函数指定参数列表:github
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{ deepClone(extension[property],object[property]) ; } } } };
利用上面的extend
方法,咱们能够重写square
的代码:
var square = rectangle.extend({ create : function(side){ return rectangle.create.call(this,side,side) ; } }) ; var sq = square.create(5) ; alert(sq.area()) ;
extend
方法是原型继承中惟一须要的操做。它是Object.create
函数的超集,所以它能够用在对象的建立和扩展上。所以咱们能够用extend
来重写rectangle
,使得create
函数更加结构化看起来就像模块模式。
var rectangle = { create : function(width,height){ return this.extend({ height : height , width : width }) ; } } ; var rect = rectangle.create(5,10) ; alert(rect.area()) ;
一些人可能已经注意到extend
函数返回的对象其实是继承了两个对象的属性,一个是被扩展的对象,另外一个是用来扩展的对象。另外从两个对象继承属性的方式也不同。第一种状况下是经过委派来继承属性(也就是使用Object.create()
来继承属性),第二种状况下使用合并属性的方式来继承属性。
不少Javascript程序员对于差异继承比较熟悉。维基百科是这么解释的:
大部分对象是从其余更通常的对象中获得的,只是在一些很小的地方进行了修改。每一个对象一般在内部维护一个指向其余对象的引用列表,这些对象就是该对象自己进行差别化继承的对象。
Javascript中的原型继承是基于差别化继承的。每一个对象都有个内部指针叫作[[proto]] (在大部分浏览器中能够经过__proto__属性访问),这个指针指向对象的原型。多个对象之间经过内部[[proto]]属性连接起来造成了原型链,链的最后指向null
。
当你试图获取一个对象的属性时Javascript引擎会首先查找对象自身的属性。若是在对象上没找到该属性,那么它就会去对象的原型中去查找。以此类推,它会沿着原型链一直查找知道找到或者到原型链的末尾。
function get(object,property){ if(!Object.hasOwnProperty.call(object,property)){ var prototype = Object.getPrototypeOf(object) ; if(prototype) return get(prototype,property) ; }else{ return object[property] ; } } ;
Javascript中属性查找的过程就像上面的程序那样。
大多数Javascript程序员会以为复制一个对象的属性到另外一个对象上并非一个正确的继承的方式,由于任何对原始对象的修改都不会反映在克隆的对象上。五天前我会赞成这个观点。然而如今我相信合并式继承是原型继承的一种正确方式。对于原始对象的修改能够发送到它的副原本实现真正的原型继承。
合并式继承和代理有他们的优势和缺点。下表列出了它们的优缺点:
代理 | 合并 |
---|---|
任何对于原型的修改都会反映在全部副本上 | 任何对于原型的修改都须要手动更新到副本中 |
属性查找效率较低由于须要进行原型链查找 | 属性查找更搞笑由于继承的属性是经过复制的方式附加在对象自己的 |
使用Object.create() 方法只能继承单一对象 |
对象能够从任意数量的对象中经过复制继承属性 |
上表中最后一点告诉咱们对象能够经过合并的方式从多个原型中继承属性。这是一个重要的特色由于这证实原型继承比Java中的类继承更强大而且与C++中的类继承同样强大。为了实现多重继承,你只须要修改extend
方法来从多个原型中复制属性。
Object.prototype.extend = function(){ var hasOwnProperty = Object.hasOwnProperty ; var object = Object.create(this) ; var length = arguments.length ; var index = length ; while(index){ var extension = arguments[length - (index--)] ; for(var property in extension){ if(hasOwnProperty.call(extension,property)|| typeof object[property] === 'undefined'){ //这里一样应该使用深复制 object[property] = extension[property] ; } } } return object; } ;
多重继承是很是有用的由于它提升了代码的可重用性和模块化。对象经过委派继承一个原型对象而后经过合并继承其余属性。好比说你有一个事件发射器的原型,像下面这样:
var eventEmitter = { on : function(event,listener){ if(typeof this[event] !== 'undefined') this[event].push(listener) ; else this[event] = [listener] ; } , emit : function(event){ if(typeof this[event] !== 'undefined'){ var listeners = this[event] ; var length = listeners.length,index = length ; var args = Array.prototype.slice.call(arguments,1) ; while(index){ var listener = listeners[length - (index--)] ; listener.apply(this,args) ; } } } } ;
如今你但愿square
表现得像一个事件发射器。由于square
已经经过委派的方式继承了rectangle
,因此它必须经过合并的方式继承eventEmitter
。这个修改能够很容易地经过使用extend
方法实现:
var square = rectangle.extend(eventEmitter,{ create : function(side){ return rectangle.create.call(this,side,side) ; } , resize : function(newSize){ var oldSize = this.width ; this.width = this.height = newSize ; this.emit('resize',oldSize,newSize) ; } }) ; var sq = square.create(5) ; sq.on('resize',function(oldSize,newSize){ alert('sq resized from ' + oldSize + 'to' + newSize + '.') ; }) ; sq.resize(10) ; alert(sq.area()) ;
在Java中是不可能实现上面的程序的,由于它不支持多重继承。相应的你必须另外再建立一个EventEmitter
类或者使用一个EventEmitter
接口而且在每一个实现该接口的类中分别实现on
和emit
方法。固然你在C++中不须要面对这个问题。咱们都知道Java sucks(呵呵呵)。
在上面的例子中你确定注意到eventEmitter
原型并无一个create
方法。这是由于你不该该直接建立一个eventEmitter
对象。相反eventEmitter
是用来做为其余原型的原型。这类原型称为mixin。它们等价于抽象类。mixin用来经过提供一系列可重用的方法来扩展对象的功能。
然而有时候mixin须要私有的状态。例如eventEmitter
若是可以把它的事件监听者列表放在私有变量中而不是放在this
对象上会安全得多。可是mixin没有create
方法来封装私有状态。所以咱们须要为mixin建立一个蓝图(blueprint)来建立闭包。蓝图(blueprint)看起来会像是构造函数可是它们并不用像构造函数那样使用。例如:
function eventEmitter(){ var evnets = Object.create(null) ; this.on = function(event,listener){ if(typeof events[event] !== 'undefined') events[event].push(listener) ; else events[event] = [listener] ; } ; this.emit = function(event){ if(typeof events[event] !== 'undefined'){ var listeners = events[event] ; var length = listeners.length ,index = length ; var args = Array.prototype.slice.call(arguments,1) ; } } ; } ;
一个蓝图用来在一个对象建立以后经过合并来扩展它(我以为有点像装饰者模式)。Eric Elliot把它们叫作闭包原型。咱们可使用蓝图版本的eventEmitter
来重写square
的代码,以下:
var square = rectangle.extend({ create : function(side){ var self = rectangle.create.call(this,side,side) ; eventEmitter.call(self) ; return self ; } , resize : function(newSize){ var oldSize = this.width ; this.width = this.height = newSize ; this.emit('resize',oldSize,newSize) ; } }) ; var sq = square.create(5) ; sq.on('resize',function(oldSize,newSize){ alert('sq resized from ' + oldSize + 'to' + newSize + '.') ; }) ; sq.resize(10) ; alert(sq.area()) ;
蓝图在Javascript中是独一无二的。它是一个很强大的特性。然而它们也有本身的缺点。下表列出了mixin和蓝图的优缺点:
Mixin | 蓝图 |
---|---|
它们用来扩展对象的原型。所以对象共享同一个原型 | 它们用来扩展新建立的对象。所以每一个对象都是在本身对象自己进行修改 |
由于缺乏封装方法因此不存在私有状态 | 它们是函数,因此能够封装私有状态 |
它们是静态原型而且不能被自定义 | 它们能够传递参数来自定义对象,能够向蓝图函数传递一些用来自定义的参数 |
许多Javascript程序员会以为使用原型模式来继承违背了语言的精髓。他们更偏向于构造模式由于他们以为经过构造函数建立的对象才是真正的实例,由于instanceof
操做会返回true
。然而,这个争论是没有意义的,由于instanceof
操做能够像下面这样实现:
Object.prototype.instanceof = function(prototype){ var object = this ; do{ if(object === prototype) return true ; var object = Object.getPrototypeOf(object) ; }while(object) ; return false ; }
这个instanceof
方法如今能够被用来测试一个对象是不是经过委派从一个原型继承的。例如:
sq.instanceof(square) ;
然而仍是没有办法判断一个对象是不是经过合并的方式从一个原型继承的,由于实例的关联信息丢失了。为了解决这个问题咱们将一个原型的全部克隆的引用保存在原型自身中,而后使用这个信息来判断一个对象是不是一个原型的实例。这个能够经过修改extend
方法来实现:
Object.prototype.extend = function(){ var hasOwnProperty = Object.hasOwnProperty ; var object = Object.create(this) ; var length = arguments.lenght ; var index = length ; while(index){ var extension = arguments[length - (index--)] ; for(var property in extension){ if(property !== 'clones' && hasOwnProperty.call(extension,property) || typeof object[property] === 'undefined') object[property] = extension[property] ; if(hasOwnProperty.call(extension,'clones')}) extension.clones.unshift(object) ; else extension.clones = [object] ; } } return object; } ;
经过合并继承自原型的对象造成了一个克隆树,这些树从根对象开始而后向下一直到叶子对象。一个克隆链是一个从根对象到叶子对象的单一路径,这跟遍历原型链很类似。咱们可使用这个信息来判断一个对象是不是经过合并继承自一个原型。
Object.prototype.instanceof = function(prototype){ if (Object.hasOwnProperty.call(prototype, "clones")) var clones = prototype.clones; var object = this; do { if (object === prototype || clones && clones.indexOf(object) >= 0) return true; var object = Object.getPrototypeOf(o bject); } while (object); return false; } ;
这个instanceof
方法如今能够用来判断一个对象是不是经过合并继承自一个原型。例如:
sq.instanceof(eventEmitter);
在上面的程序中instanceof
会返回true
若是我妈使用mixin版本的eventEmitter
。然而若是咱们使用蓝图版本的eventEmitter
它会返回false
。为了解决这个问题我建立了一个蓝图函数,这个函数接收一个蓝图做为参数,向它添加一个clones
属性而后返回一个记录了它的克隆的新蓝图:
function blueprint(f){ var g = function(){ f.apply(this,arguments) ; g.clones.unshift(this) ; } ; g.clones = [] ; return g ; } ; var eventEmitter = blueprint(function(){ var events = Object.create(null); this.on = function (event, listener) { if (typeof events[event] !== "undefined") events[event].push(listener); else events[event] = [listener]; }; this.emit = function (event) { if (typeof events[event] !== "undefined") { var listeners = events[event]; var length = listeners.length, index = length; var args = Array.prototype.slice.call(arguments, 1); while (index) { var listener = listeners[length - (index--)]; listener.apply(this, args); } } }; }) ;
上面例子中的clones
属性有双重做用。它能够用来判断一个对象是不是经过合并继承自一个原型的,而后他能够用来发送原型改变给全部它的克隆。原型继承相比类继承最大的优点就是你能够修改一个原型在它建立以后。为了使克隆能够继承对于原型的修改,咱们建立了一个叫作define
的函数:
Object.prototype.define = function (property, value) { this[property] = value; if (Object.hasOwnProperty.call(this, "clones")) { var clones = this.clones; var length = clones.length; while (length) { var clone = clones[--length]; if (typeof clone[property] === "undefined") clone.define(property, value); } } };
如今咱们能够修改原型而后这个修改会反映在全部的克隆上。例如咱们能够建立建立一个别名addEventListener
针对eventEmitter
上的on
方法:
var square = rectangle.extend(eventEmitter, { create: function (side) { return rectangle.create.call(this, side, side); }, resize: function (newSize) { var oldSize = this.width; this.width = this.height = newSize; this.emit("resize", oldSize, newSize); } }); var sq = square.create(5); eventEmitter.define("addEventListener", eventEmitter.on); sq.addEventListener("resize", function (oldSize, newSize) { alert("sq resized from " + oldSize + " to " + newSize + "."); }); sq.resize(10); alert(sq.area());
蓝图须要特别注意。尽管对于蓝图的修改会被发送到它的克隆,可是蓝图的新的克隆并不会反映这些修改。幸运的是这个问题的解决方法很简单。咱们只须要对blueprint
方法进行小小的修改,而后任何对于蓝图的修改就会反映在克隆上了。
function blueprint(f) { var g = function () { f.apply(this, arguments); g.clones.unshift(this); var hasOwnProperty = Object.hasOwnProperty; for (var property in g) if (property !== "clones" && hasOwnProperty.call(g, property)) this[property] = g[property]; }; g.clones = []; return g; };
恭喜你。若是你读完了整篇文章而且理解了我所说的东西,你如今就了解了 原型继承而且为何它很重要。很感谢大家看完了这篇文章。我但愿这个博客能帮到大家。原型继承是强大的而且值得更多的信任。而后大部分人历来不明白这个由于Javascript中的原型继承被构造模式所掩盖了。
这篇文章针对几种继承方式进行了对比。文章中说到的几种扩展的方法我以为是比较有用的。蓝图(blueprint,这个实在不知道该怎么翻译)的扩展方式比较像设计模式中的装饰者模式,经过函数对对象进行扩展,这个是一种比较好玩的扩展方式,能够跟原型继承配合使用。另外文中提到了new
关键字的弊端,我的以为主要的缘由仍是new
关键字的出现掩盖了Javascript自己原型继承的特色,人们天然而然就会想到传统的类继承,这样就没法发挥原型继承的最大威力。最后说到的属性修改传播的问题也挺有意思的,应该会有相应的应用场景。总之,我以为原型继承相比于传统的类继承提供了更大的灵活性,能够给咱们开发者提供很大的发挥空间,不过无论怎样,到最后仍是要涉及到基本的原型继承的原理上,因此掌握了原型继承的原理就能够根据不一样的应用场景使用各类各样的扩展方式。
原文地址:http://aaditmshah.github.io/why-prototypal-inheritance-matters/