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语言的function
4) 可以用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替换换成parent
4) "."和"[]"重新定义,需要支持在对象内部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 = topObject
2.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!

好像有些什么不妥——

突然,我好像明白了什么...

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值