【译】不用构造函数(Constructor)也能实现JavaScript的面向对象
JavaScript中的对象模型(object model)并不广为人知。我曾写过一篇关于他们的博客。之所以不被人所熟知,原因之一就是JavaScript是这些被人广泛使用的语言中唯一一个通过原型(prototype)来实现继承的。但是,我认为另一个原因就是这种对象模型非常复杂,难于解释。它为什么这么复杂并且又令人困惑呢?那是因为JavaScript试图去隐藏它传统的面向对象的特性——最终导致了它的双重人格(译者注:作者意思是JavaScript既有面向过程的特征,又有面向对象的特征)。
我认为正是由于JavaScript对象模型的难以理解和使用,才出现了一些像CoffeeScript,Dart和TypeScript这些通过编译可以生成JS代码的语言。
JavaScript的前辈们和那些顽固派相信JavaScript有更好的对象模型,并且为其将被大家所遗忘感到惋惜。即使JavaScript的专家Nicholas Zakas也欢迎在ECMAScript 6中加入的新的class语法——只不过是对原型风格(prototypal style)的语法做了一些修饰。换句话说,传统的OOP赢了。
一个大胆的设想
但是,让我们以玩笑的方式做一个设想:我们假想穿越到过去,那时候传统的面向对象的程序设计还没有像现在这样被大家广泛的接受,相反的,基于原型的继承模型得到了大家的广泛接受。这样的话会发生什么?我们最终又会得到些什么样的设计模式呢?
我们再设想:假设JavaScript没有构造函数或者没有new关键字会怎样?事情又会变成什么样的呢?让我们推到以前的重来。:)
首先,第一件事情,在JavaScript中,我们可以使用对象字面量的来创建一个新对象。如下所示:
1 var felix = { 2 name: 'Felix', 3 greet: function(){ 4 console.log('Hello, I am ' + this.name + '.'); 5 } 6 };
接下来,假设我们想要将greet函数一般化,将其提取出来,放到一个一般的位置,这样一来,我们就可以创建多个对象来共享同一个greet方法。该怎么实现呢?
我们有好几种选择,先以mixin开始吧。
1、混入(对象扩张)Mixin(Augmentation)
在JavaScript语言中,混入属性非常简单。你只需要将混入对象的属性复制到要混入的对象中去即可。我们将使用一个“augment”函数来实现它,看代码就明白了:
1 var Dude = { 2 greet: function(){ 3 console.log('Hello, I am ' + this.name + '.') 4 } 5 }; 6 var felix = { name: 'Felix' }; 7 augment(felix, Dude);//将Dude中的属性复制一份到felix中,即混入(mixin)
在上面的代码中,augment函数将Dude对象的属性混入到了felix当中。在很多的JS库中,augment函数被叫做extend。我不喜欢用extend,因为一些语言用extend表示继承,以至于是我很困惑。我更喜欢用“augment”表示,因为实际上这种做法并不是继承,并且语法augment(felix, Dude)已经很清楚的表明你是用Dude中的属性对felix进行了扩充,而不是继承。
也许你早就猜到了augment的代码实现了,没错,非常简单。如下所示:
1 function augment(obj, properties){ 2 for (var key in properties){ 3 obj[key] = properties[key]; 4 } 5 }
2、对象克隆(Cloning)
mixin的一个替代的办法就是先克隆Dude对象,然后再给克隆的对象设置name属性。如下所示:
1 var Dude = { 2 greet: function(){ 3 console.log('Hello, I am ' + this.name + '.'); 4 } 5 } 6 var felix = clone(Dude);//克隆Dude对象 7 felix.name = 'Felix';
这两种方法之间的唯一不同就是添加属性的顺序。如果你想覆写克隆的对象中的某些方法,你可以考虑使用这种手法。
1 var felix = clone(Dude); 2 felix.name = 'Felix'; 3 felix.greet = function(){ 4 console.log('Yo dawg!'); 5 };//覆写greet方法
如果想要调用父类的方法也很简单——使用apply函数即可,如下所示
1 felix.greet = function(){ 2 Dude.greet.apply(this); 3 this.greetingCount++; 4 }
这比原型风格的代码要好很多,因为你不必去使用构造函数的.prototype属性——我们不会使用任何构造函数。
以下是clone函数的实现:
1 function clone(obj){ 2 var retval = {};//创建一个空对象 3 augment(retval, obj);//复制属性 4 return retval; 5 }
3、继承(Inheritance)
最后,就是继承了。在我看来,继承被高估了,但是继承在“实例对象”之间共享属性方面确实要比对象扩张有一些优势。让我们编写一个inherit函数,这个函数接收一个对象作为参数,并且返回一个继承自该对象的新对象。
1 var felix = inherit(Dude); 2 felix.name = 'Felix';
使用继承,你可以创建多个继承自同一个对象的子对象,这些子对象可以实时的继承父对象的属性。如下面的代码所示,
1 var garfield = inherit(Dude);//garfield继承自Dude 2 Dude.walk = function(){//给Dude添加新的方法walk 3 console.log('Step, step'); 4 }; 5 garfield.walk(); // prints "Step, step" 6 felix.walk(); // also prints "Step, step"
在inherit函数中使用了基于原型对象的继承
1 function inherit(proto){ 2 if (Object.create){ 3 // 使用ES5中的Object.create方法 4 return Object.create(proto); 5 }else if({}.__proto__){ 6 //使用非标准属性__proto__ 7 var ret = {}; 8 ret.__proto__ = proto; 9 return ret; 10 }else{ 11 //如果两种都不支持,使用构造函数继承 12 var f = function(){}; 13 f.prototype = proto; 14 return new f(); 15 } 16 }
上面的代码看起来不怎么好,那是因为我们使用了特性监测来判断到底使用3种方式中的哪一种。
但是,怎么使用构造方法呢(也就是,初始化方法)?你该怎么在实例对象之间共享初始化代码呢?在一些情况下,如果你只需要为对象设置一些属性,这时候,初始化函数不是必须的,就像我们上面的例子中那样。但是如果你有更多的初始化代码呢,你也许会制定一个约定,例如:使用一个叫initialize的初始化方法。我们假设在Dude中定义了一个叫initialize的方法,如下
1 var Dude = { 2 initialize: function(){ 3 this.greetingCount = 0; 4 }, 5 greet: function(){ 6 console.log('Hello, I am ' + this.name + '.'); 7 this.greetingCount++; 8 } 9 }
然后,你可以这样来初始化对象
1 var felix = clone(Dude); 2 felix.name = 'Felix'; 3 felix.initialize();
或者也可以
1 var felix = { name: 'Felix' }; 2 felix.name = 'Felix'; 3 augment(felix, Dude); 4 felix.initialize();
还可以
1 var felix = inherit(Dude); 2 felix.name = 'Felix'; 3 felix.initialize();
结语
我表示通过上面定义的三个函数——augment,clone和inherit,你可以对JavaScript中的对象做任何你想做的事,而不必使用构造函数和new关键字。我认为这三个函数所体现的语义更简单并且更接近于JavaScript底层的对象系统。
(完)^_^