关于JavaScript中“类”的思考
为什么要有类?
众所周知,JavaScript是没有像Java那样的类。Java的对象需要通过类来实例化才能创建,而JavaScript是没有这个必要的,那么JavaScript需要类来做什么呢?一个Java类可以实例化N个一模一样的对象,如果从这个角度来看,JavaScript中,一个可以构造N个一样对象的函数就可以称作是“类”了呢?
JavaScript中类的实现
我本人实战经验不多,只看过基本书,知道的方法有以下几种。
1、两个方向
伪类
JavaScript提供了一个伪类的机制。
var Duck = function (){ this.name = 'duck'; };
通过调用var aDuck = new Duck();即可获得一个拥有name属性的对象。在这个过程中,new函数是先创建了一个空的对象,用this关键字引向了这个对象,然后this.name=’duck’;执行的时候相当于为这个空对象添加了name属性,最后函数自动返回了此对象。这个过程看起来很完美,但是万一使用过程中我们完了new这个关键字呢?这时的this指向的是window全局对象,而这个函数只会为window添加了一个name的全局变量后,返回一个undefined。这样的失误是很麻烦的,污染了全局变量就像埋下了定时炸弹。
所以老道在《JavaScript语言精粹》中提出说,干脆不用new了,也就是函数化。
函数化
var duck = function(){ var that = {}; that.name = ‘duck’; return that; };
这样var aDuck = duck();就可以做出一个对象了,而且全局变量很安全。
这两个方法看起来差别不是很大,只是一个new,一个不new而已。其实有很大的区别的。这两个例子我们都没有考虑类里面定义的方法。假如我们要添加一个getName的方法,应该怎么做呢?
伪类方法
var Duck = function (){ this.name = 'duck'; }; Duck.prototype.getName = function(){ alert(this.name); };
函数化方法
var duck = function(){ var that = {}; that.name = 'duck'; that.getName = function(){alert(this.name);} return that; };
在伪类方法中,这里的getName方法在内存只占用了一份,因为对象的原型都是同一个对象,getName只是一个方法。而函数化方法中,实例化了多少个对象,内存就有多少份getName的拷贝。
2、关于共享函数的方法
做前端的都是完美主义者,“只占一份内存”和“N份内存”之间肯定选择的是前者,即使冒着污染全局变量的危险。但我不甘心,我想让函数化方法也可以共享一个方法。我做了一些尝试。
说起公用,我想当的是原型和闭包。
我能不能让函数方法实例化的每个对象都指向一个原型,那么为该原型添加的方法就可以公用了。
原型失败的例子
var duck = function(){ if(! Arguments.callee.myproto){ //如果没有原型,这定义一个 var proto = arguments.callee.myproto = function(){}; //为原型添加方法 proto.getName = function(){alert(this.name);}; } //让所有的对象接上原型 var that = new aruguments.callee.myproto(); that.name = 'duck'; return that; };
这个方法一写出来我就有点后悔了。不仅仅是因为很复杂,还因为使用了new。并没让函数化的好处体现出来。
另外一个尝试使用闭包。
var duck = function(){ //这个函数藏在闭包内 var getName = function(){alert(this.name);}; return function(){ var that = {}; that.name = 'duck'; that.getName = getName; return that; }; }();//这对括号不是多余的。
懂闭包的人应该很轻松看懂这行代码。getName藏在闭包内只定义了一次,每个对象调用getName的时候都会回到闭包内找到这个函数。
这段代码看起来看起来不错。但是我们还有问题需要考虑。
3、Private属性
无论刚才说的哪种方法,我们都默认属性是public的,假如我们要为duck添加age这个private的属性,这个属性只能通过某个特定的方法访问,且不能被修改。
说到这种问题,脑海里面唯一的答案就是“闭包”。
先给一个错误的例子:
var duck = function(){ //这个函数藏在闭包内 var getName = function(){alert(this.name);}; //这样写是错的 var getAge = function(){ if(this.age > 30) alert('不告诉你'); else alert(this.age); }; return function(duckAge){ var that = {}; var age = duckAge; //不能直接that.age = duckAge! that.name = 'duck'; that.getName = getName; return that; }; }();//这对括号不是多余的。
这样的代码是错的,因为getAge函数中,this指向的是该对象,age属性不是直接加在该对象上的,否则谁都可以this.age访问修改了吧。那么我们在getAge函数中把this去掉呢,这样也是很显然不对的。
我们怎么办?不能怎么办了。这能老老实实地让getName占多份内存了。
又回到了“占n份内存了”,我已经没有办法了。只能说为了private只能牺牲性能了。但需要private属性的机会其实也不多。
另外,采用伪类法添加private变量也没有特别的高招。
var Duck = function (age){ this.name = 'duck'; this.getAge = function(){ if(age > 30) alert('不告诉你'); else alert(age); }; }; Duck.prototype.getName = function(){ alert(this.name); };
接下来是更重要的问题:继承。
4、继承
我们需要代码重用,而重用的一般方法是继承和组合。这里先不讲组合。
考虑下,我们需要一个ColorDuck类,它比duck类多一个color的属性。其实主要采用的方法称为差异化继承”。主要思想为复制一份父类的实例(即对象),然后哪里不一样就修改哪里。
函数化的实现
父类:
var duck = function(){ //这个函数藏在闭包内 var getName = function(){alert(this.name);}; return function(duckAge){ var that = {}; that.name = 'duck'; that.getName = getName; var age = duckAge; that.getAge = function(){ if(age > 30) alert('不告诉你'); else alert(age); }; return that; }; }();//这对括号不是多余的。
子类:
var ColorDuck = function(age,color){ var that = duck(age); that.color = color; return that; };//完了。
var that = duck(age);这行语句让子类对象拥有了父类对象的所有属性和方法,然后根据差异添加属性方法即可。
对于伪类的实现就稍微要多考虑一些东西。
先看一段错误代码。
父类:
var Duck = function (age){ this.name = 'duck'; this.getAge = function(){ if(age > 30) alert('不告诉你'); else alert(age); }; }; Duck.prototype.getName = function(){ alert(this.name); };
子类:
var ColorDuck = function(age,color){ //要调用父类的方法实例化对象 //var that = new duck();这样写是错的,因为此this不是那个this. Duck.call(this,age);//这个this才是那个this。 this.color = color; }; ColorDuck.prototype = Duck.prototype;
这样算是完成了吗?不是的。
问题出在这行。ColorDuck.prototype = Duck.prototype;子类的prototype不应该和父类的prototype为同一个对象,否则我们为子类添加函数的时候,父类也会增加。而这个情况不是我们希望出现的。所以我们要为子类添加一个自己的原型,而同时这个原型又必须有父类原型的方法。那不就是父类的实例嘛。
ColorDuck.prototype = new Duck();
ColorDuck.prototype.constructor = ColorDuck;
这样继承算是OK了。完整代码如下:
父类:
var Duck = function (age){ this.name = 'duck'; this.getAge = function(){ if(age > 30) alert('不告诉你'); else alert(age); }; }; Duck.prototype.getName = function(){ alert(this.name); };
子类:
var ColorDuck = function(age,color){ //要调用父类的方法实例化对象 //var that = new duck();这样写是错的,因为此this不是那个this. Duck.call(this,age);//这个this才是那个this。 this.color = color; }; ColorDuck.prototype = new Duck(); ColorDuck.prototype.constructor = ColorDuck;
总结:
我只是菜鸟,如果上述说得不对,希望指正!此贴不是教学贴,这是把我的思路总结一下而已,谢谢各位耐心看完。
JavaScript里头没有最好的实现,只有最适合的实现。这就是JavaScript的魅力。