JS真的错了吗——Object-Oriented JavaScript(Part 2)
?
一、js是世界上最容易被误解的语言javascript本质上是基于原型的语言,但是却引入了基于类的语言的new关键字和constructor模式,导致javascript饱受争议。
javascript的作者Brendan Eich 1994年研发这门语言的时候,C++语言是最流行的语言,java1.0即将发布,面向对象编程势不可挡,于是他认为,引入new关键字可以使习惯C++/java程序员更容易接受和使用javascript。
实际上,事实证明引入new是个错误的决定。
C++/java程序员看到new一个 function的时候,会认为js通过function创建对象.他们认为function相当于类,接着他们会尝试在js挖掘类似java/C++面向类的编程特性,结果他们发现function没有extends,反而有个很奇怪的prototype对象,于是他们开始咒骂,js的面向对象太糟糕了。确实,new的引入让他们以为js的面向对象与java/C++类似,实际上并不是,如果不是以原型本质去理解js的面向对象,注定要遭受挫折,new,prototype,__proto__都是javascript实现原型的具体手段。
另一方面,理解原型的程序员,抱怨没有最基本的通过对象创建对象的函数。他们很不高兴,因为居然要使用new function的语法来间接实现原型继承,三行代码才做到最基本的原型继承,下面是实现对象newObject继承对象oldObject的代码,
function F(){};F.prototype = oldObject;var newObject = new F();这太繁琐了。基于原型语言理论上应该存在一个函数create(prototypeObject),功能是基于原型对象产生新对象,例如,var newObject = create(oldObject);看到这样的代码,人们就会自然很清晰地联想到,newObject是以oldObject模板构造出来的。js是世界上最容易被误解的语言,原因主要有两个:
1) 作为基于原型的语言中,却连最基本的一个通过原型产生对象的函数create(prototypeObject)也没有,让人不知道js根本上是以对象创建对象。应该添加该函数,现在Chrome和IE9的Object对象就有这个create函数。
2) 使用new func形式创建对象,让人误会js是以类似java类的构造函数创建对象,实际上,构造函数根本上在创建对象上起到次要的作用,甚至不需要,重要的只有函数的属性prototype引用的原型对象,新对象以此为模板生成,生成之后才调用函数做初始化的操作,而初始化操作不是必要的。应该把废弃new 操作符,把new func分解为两步操作,var newObject = create(func.prototype);?func.call(newObject);这样程序员才好理解。如果想把这两个步骤合二为一,应该使用new以外的关键字。
到这里,我们务必要牢牢印入脑海的是,js的面向对象是基于原型的面向对象,对象创建的方式根本上只有一种,就是以原型对象为模板创建对象,newObject = create(oldObject)。new function不是通过函数创建对象,只是刻意模仿java的表象。
js在面向对象上遭遇的争议,完全是因为商业因素导致作者失去了自己的立场。就像现在什么产品都加个云一样,如果那时候不加个new关键字来标榜自己面向对象,产生"js其实类似c++/java"的烟幕,可能根本没有人去关注javascript。更令人啼笑皆非的是,原本称作LiveScript的javascript,因为 后期和SUN合作,并且为了沾上当时被SUN炒得火热的Java的光,发布的时候居然改名成Javascript。
二、让我们一起来研发JavaScript语言
既然js遭受那么多批评,那么我们就搞一个大家都满意的JS吧!假想我们是当时研发javascript的Brendan Eich,我们会怎么设计js的面向对象呢?现在javascript开发到这样的阶段1) 拥有基本类型,分支和循环,基本的数学运算,2) 所有数据都是对象3) 拥有类似C语言的function4) 可以用var obj = {}语句生成一个空对象,然后使用obj.xxx或obj[xxx]设置对象属性5) 没有继承,没有this关键字,没有new我们任务是,实现javascript的面向对象,最好能达到类似java的创建对象和继承效果。更具体一点,我们要扩充js语言,实现类似下面的java代码。
class Empolyee{String name;public Employee(String name){this.name = name;}public getName(){return this.name;}}class Coder extends Employee {String language;public Coder(name,language){super(name);this.language = language;}public getLanguage(){return this.language;}}? 1 实现创建对象现有的对象都是基本类型,怎么创建用户自定义的对象呢?(解释:var i = 1;这里的i是解释器帮忙封装的Number对象,虽然看起来跟C的int没区别,但实际上可以i.toString()。)java使用构造函数来产生对象,我们尝试把java的Empolyee的构造函数代码拷贝下来,看看可不可以模仿function Empolyee(name){this.name = name;}?我们只要生成一个空对象obj,再把函数里面的this换成obj,执行函数,就可以生成自定义对象啦!我们把Employee这样用来创建对象的函数称作构造函数。1) 首先我们用原生的方式为function添加方法call和apply,实现把把函数里面的this替换成obj。call,apply在Lisp语言中已经有实现,很好参考和实现。2) 然后实现生成实例?function Empolyee(name){this.name = name;}var employee = {};Employee.call(employee,'Jack');??? ?3) 到这里,以类似java方式产生对象基本完成了,但是这个employee对象没有方法我们的function是第一类对象,可以运行时创建,可以当做变量赋值,所以没有问题。function Empolyee(name){this.name = name;this.getName = function(){return this.name};}很好,我们团队顺利向前走了一步,今晚大家不用加班了!? 2 实现继承创建对象成功了,接着考虑实现继承。现在我们所有数据都是对象,没有类,有两种方案摆在我们的面前a.类继承b.原型继承2.a 实现类继承a方案是首选方案,因为跟java相似的话,JS更容易被接受先粘贴Java构造函数的代码
function Coder extends Employee(name,language){super(name);this.language = language;}?1) 把extends后面的函数自动记录下来,放到function对象的parentFunc变量2) 如果第一行是super(),替换成var parent = newInstance(Coder.parentFunc,XXX),这样内部保留一个名为parent父对象;3) 把this替换为obj,super替换换成parent4) "."和"[]"重新定义,需要支持在对象内部parent对象查找属性。这四步都属于比较大的改动,只要认真想一想都觉得不是太容易。更重要的是,即使把这4步实现了,不但语言变得太复杂了,而且产生的对象根本享受不了继承带来的好处——内存中的代码复用,因为这样产生的每个对象都有"父类(函数)"的代码而不是仅有一份。这时候该注意到java中使用类的意义了,java类的代码在内存只有一份,然后每个对象执行方法都是引用类的代码,所有子类对象调用父类方法的时候,执行的代码都是同一份父类的方法代码。但是JS没有类,属性和方法都是存在对象之中,根本没有办法做到java那样通过类把代码共享给所有对象!a方案宣告失败? ? ? ? ?2.b 实现原型继承看b方案。我们现在的js语言,一切都是对象,显然非常适合使用基于原型的继承方式,就看具体如何实现了。我们新建一个topObject来代表顶层对象,那么创建employee对象的时候,应该在employee对象内部设置一个属性引用topObject;同理,创建coder对象的时候,应该在coder对象内部设置一个属性引用employee对象,我们把这个引用原型对象的属性命名约定为"__proto__"。更进一步,为了构建一个对象的过程更自然,构建时候应该先在新对象中设置引用原型对象的属性,以表示先用模板制作出一个和模板一致的对象,然后再才执行构造函数初始化这个新对象自身的属性,以添加个性化的东西。具体实现代码如下:var topObject = {__version__ : 1.0;};function Empolyee(name){this.name = name;this.getName = function(){return this.name};}var employee = {};employee.__proto__ = topObject;Employee.call(employee,'Jack');function Coder(name,language){this.name = name;this.language = this.language;this.getLanguage = function(){return this.language};}var coder = {};coder.__proto__ = employee;Coder.call(coder,'Coder Jack','Java');?当然我们还要做的工作就是在javascript解释器中增加对__proto__的支持,当一个对象访问一个自身没有的属性的时候,就通过__proto__属性查找原型链上是否存在该属性。? ? ? ? ? 2.c 优化实现
? ? ? ? ? ? ? ?优化1:函数封装这一切看起来并不是那么美好,我们创建一个employee对象需要3行代码,我们需要这么一个函数封装这3行代码function newInstance(prototype,constructor,arg1,arg2,....);//第一个参数是原型对象,第二个是构造函数,后面的是构造函数的参数可以这么实现
function sliceArguments(argumentsObj,n){var args = [];for(var i=0;i<argumentsObj.length;i++){if(i>=n){args.push(argumentsObj[i]);}}return args;}function newInstance(prototype,constructor){var obj = {};obj.__proto__ = prototype;constructor.apply(obj,sliceArguments(arguments,2));}var employee = newInstance(topObject,Employee,'Jack');var coder = newInstance(employee,Coder,'Coder Jack','Java');? ? ? ? ? ? ? ?优化2:缩减参数仔细一看,function newInstance的参数可以更少,我们可以把原型对象prototype作为属性放在constructor,那样我们的函数就可以只有一个参数了。属性名就约定为prototype吧。2.1 我们修改解释器,把topObject写入语言作为原生的顶级对象;再修改function的源代码,让每一个新建的function都默认具有属性prototype = topObject2.2 优化后的代码如下function newInstance(constructor){var obj = {};obj.__proto__ = constructor.prototype;constructor.apply(obj,sliceArguments(arguments,1));return obj;}function Employee(name){this.name = name;this.getName = function(){return this.name};}var employee = newInstance(Empolyee,'Jack');var employee2 = newInstance(Empolyee,'Jack2');var employee3 = newInstance(Empolyee,'Jack3');function Coder(name,language){this.name = name;this.language = language;this.getLanguage = function(){return this.language};}Coder.prototype = newInstance(Empolyee,'');var coder = newInstance(Coder,'Coder Jack','Java');var coder2 = newInstance(Coder,'Coder Lee','C#');var coder3 = newInstance(Coder,'Coder Liu','C++');var coder4 = newInstance(Coder,'Coder Liu','JavaScript');?至此,我们利用已有的设施,简单有效地开发出一个面向对象的javascript版本!Congratulations!好像有些什么不妥——
突然,我好像明白了什么...
1 楼 fearthenight 2012-09-11 不错哦,解释的很清楚。 2 楼 iame 2012-10-10 不错,第三篇呢? 3 楼 lazy_ 2012-10-11 iame 写道不错,第三篇呢?
不好意思,最近工作比较忙,所以暂时是没有足够的时间和精力去写了。可以关注我。
推荐一些我认为是最优秀的JS文章,让你过把瘾吧。
crockford大师
http://javascript.crockford.com/javascript.html
http://javascript.crockford.com/prototypal.html
http://javascript.crockford.com/inheritance.html
http://www.crockford.com/javascript/private.html
MDN
https://developer.mozilla.org/en-US/docs/JavaScript/Guide/Details_of_the_Object_Model
微软杂志
http://msdn.microsoft.com/zh-cn/magazine/cc163419.aspx#S4
MSDN
http://msdn.microsoft.com/en-us/library/dd282900(v=vs.85).aspx
http://msdn.microsoft.com/en-us/library/dd229916(v=vs.85)
阮一峰
http://www.ruanyifeng.com/blog/2010/05/object-oriented_javascript_encapsulation.html
http://www.ruanyifeng.com/blog/2012/07/three_ways_to_define_a_javascript_class.html?20120830102326#comment-last
http://www.ruanyifeng.com/blog/2010/05/object-oriented_javascript_inheritance.html
http://www.ruanyifeng.com/blog/2010/05/object-oriented_javascript_inheritance_continued.html
others
http://bonsaiden.github.com/JavaScript-Garden/zh/ 4 楼 iame 2012-11-08 能推荐一个比较成熟的的基于原型的无依赖仿OO库吗?
支持extends,parent,static,init等特性。 5 楼 lazy_ 2012-11-09 iame 写道能推荐一个比较成熟的的基于原型的无依赖仿OO库吗?
支持extends,parent,static,init等特性。
我所知道的有prototype,模仿了Class, extends等。应该大多数JS库都会有类似的功能的,YUI,mootools,ext等等。
我更建议的你读完JS权威的这几篇文章后再考虑怎么使用和选择这写封装。目前为止,我只使用过prototype的Class.create。其他情况还是自己搞定,例如利用函数返回一个对象,或者利用原始的new去搞定(参见我博客大技术文f分类下的JAVASCRIPT文章下的后面的章节)。
http://javascript.crockford.com/javascript.html
http://javascript.crockford.com/prototypal.html
http://javascript.crockford.com/inheritance.html
http://www.crockford.com/javascript/private.html
还有。static在面向原型的JS中不存在这个概念。JAVA所谓静态成员就是类共享给所有的对象的成员,每个对象共享同一份静态成员,实际上,JS的原型继承天生就实现了这一点,每个对象都继承父亲对象属性,修改父对象的成员,子对象也马上做出改变,除非子对象重写了该成员。