上一篇温习了传统的Javascript实现面向对象编程的实现方式,但这种实现方式有点麻烦,感觉看上去功能上是实现了Object-Oriented了,但形式上却不像,所以现在我们用另一种方法去实现同样的需求,效果一样,只是我们会对类的构造和继承的实现做一定的封装。
首先,在写正文之前,为了参考怎样封装Javascript的面向对象特征,特意找了一些JS框架查看源代码,可惜资历尚浅,还是很难看懂,主要参考了prototype.js,mootools.js和base.js这几个框架对class的实现,基本方式类似,现在就据我所知,说说一种容易理解一点的实现方法吧。
首先,我们定义一个总类Class。
1 //总的父类 2 Class = function () { 3 };
然后我们向Class添加一个extend方法,用于创建一个新的类,extend的参数即我们要添加的属性和方法的一个json对象了。例如我们要定义一个点类Point和一个圆形Circle继承自图形父类Shape,可能写出来是这样子的:
1 //定义图形父类Shape 2 Shape = Class.extend({ 3 type:'', 4 draw:function () { 5 console.log('I am drawing a shape of ' + this.type + '!'); 6 } 7 }); 8 //定义点Point 9 Point = Shape.extend({ 10 x:100, 11 y:100, 12 type:'Point', 13 init:function (x, y) { 14 this.x = x || 100; 15 this.y = y || 100; 16 }, 17 set:function (x, y) { 18 this.x = x || 100; 19 this.y = y || 100; 20 }, 21 draw:function (ctx) { 22 this.super(); 23 var ctx = ctx || document.getElementById('canvas').getContext('2d'); 24 ctx.beginPath(); 25 ctx.arc(this.x, this.y, 5, 0, 2 * Math.PI, true); 26 ctx.closePath(); 27 ctx.fill(); 28 } 29 }); 30 //定义圆Circle 31 Circle = Shape.extend({ 32 type:'Circle', 33 center:new Point(150, 150), 34 radius:10, 35 init:function (r, x, y) { 36 this.radius = r; 37 this.center.set(x, y); 38 }, 39 draw:function () { 40 this.super(); 41 var ctx = ctx || document.getElementById('canvas').getContext('2d'); 42 ctx.beginPath(); 43 ctx.arc(this.center.x, this.center.y, this.radius, 0, 2 * Math.PI, true); 44 ctx.closePath(); 45 ctx.fill(); 46 } 47 });
但怎样实现这种清晰的面向对象编程呢?我们一步一步来。我们预想extend函数大概结构是这样子的,这是第一个版本的初始模样:
1 Class.extend = function (props) { 2 subclass = function () { 3 for (var key in props) { 4 this[key] = props[key]; 5 } 6 if (this.init) { 7 this.init.apply(this, arguments); 8 } 9 } 10 subclass.prototype = new this(); 11 subclass.prototype.constructor = subclass; 12 subclass.extend = arguments.callee; 13 return subclass; 14 }
这如何解释呢(原谅我没有写注释)?传入的参数props是类的属性和方法,然后我们创建了一个子类(第2行),让子类继承父类(第10行),修改子类的构造函数(第11行),让子类像父类一样也拥有extend方法(第12行),最后返回新创建的子类subclass(第13行)。当我们创建一个对象的时候,则会把对象拥有的属性和方法props赋值一次给当前新创建的对象,如果对象拥有init初始化方法,则调用初始化方法。
注意,这是第一个版本,只是为以后的优化搭建一个原型,但明显它是有问题的。首先,这种做法在每次new一个对象的时候才去进行赋值,而且是全部赋值,这样子每一个新创建的对象都拥有相同的所有属性和方法,显然一方面共享了props里面的属性和方法,某个对象改变这些值,都会引起所有对象改变,另一方面函数不应该重复赋值的。另一方面,这种方法直接覆盖掉了父类的方法了,不能再调用父类的方法,例如Java中调用this.super()这样子。于是,我们对此进行改进:
1 //用来创建一个新的子类 2 Class.extend = function (props) { 3 //存储着父类的prototype ; 4 var parent = this.prototype; 5 //标志这时候是在初始化子类的prototype,不要执行父类的属性复制和构造函数; 6 var prototyping = true; 7 //初始化子类的prototype,用于实现继承 8 var prototype = new this(); 9 //初始化完再把标志修改过来; 10 prototyping = false; 11 //对每一个新属性和方法添加到子类的prototype中 12 for (var name in props) { 13 //如果新添加的方法和父类拥有相同名称,则可以通过this.super()临时指向父类的方法,执行完整个子类函数后恢复super方法。 14 if (typeof(props[name]) == "function" && typeof(parent[name]) == "function") { 15 prototype[name] = (function (name, fn) { 16 return function () { 17 var temp = this.super || null; 18 this.super = parent[name]; 19 var ret = fn.apply(this, arguments); 20 if (temp) { 21 this.super = temp; 22 } 23 return ret; 24 }; 25 })(name, props[name]); 26 } 27 //否则只是简单的复制属性,浅复制。 28 else { 29 prototype[name] = props[name]; 30 } 31 } 32 //子类的定义 33 function c() { 34 //如果不是在prototyping 35 if (!prototyping) { 36 //对每一个属性和方法,如果属性是个object,包括可能是json对象,可能是一个类的实例,也可能是数组,因为他们都是prototype的属性,所以必须进行复制,避免影响其它对象 37 for (var p in this) { 38 if (this[p] && typeof(this[p]) === 'object') { 39 this[p] = this[p].clone(); 40 } 41 } 42 //如果类拥有init初始化方法,则调用初始化函数进行初始化 43 if (this.init) { 44 this.init.apply(this, arguments); 45 } 46 } 47 return this; 48 } 49 //定义子类的prototype,保证继承。 50 c.prototype = prototype; 51 //修改构造器,不然子类的构造器默认是Class父类 52 c.prototype.constructor = c; 53 //让子类也跟父类一样拥有extend方法,用于实现继承 54 c.extend = arguments.callee; 55 //最后返回子类,创建成功 56 return c; 57 };
对了,这就是最后的版本了,相关解释可以一点一点参看代码的注释。简单来说,extend函数做了几件事:
1.复制新的属性和方法;
2.当父类存在相同名字的方法时,通过创建一个临时函数super来实现方法的覆盖;
3.每次创建类的实例时,对prototype中的每一个object属性进行深复制,以免影响了其他对象。
下面,我简单说说object的深复制吧。浅复制很容易实现,很多框架的extend方法都可以实现,但深复制需要把object类型的属性都一层一层去复制。Jquery拥有clone方法,但它是用来复制DOM的,不要误会,我们可以使用Jquery的extend方法实现复制,本来是没问题的,但Jquery中处理深度复制的object并不能复制我们自己用Class.extend创建出来的对象,只能复制纯对象,即使用{}或者new Object()创建出来的对象,所以当我们的某个属性例如位置position为一个点Point对象时,就不会进行深度复制,结果某一个对象的position修改了,所有对象position都变成一样的了。为了处理这个麻烦,我们写了一个clone函数,即上一篇提到的clone函数,添加到Object的prototype上,代码如下:
1 Object.prototype.clone = function () { 2 if (!this || this instanceof HTMLElement || this instanceof Function) { 3 return this; 4 } 5 var objClone; 6 if (this.constructor == Object) { 7 objClone = new this.constructor(); 8 } 9 else { 10 objClone = new this.constructor(this.valueOf()); 11 } 12 for (var key in this) { 13 if (objClone[key] != this[key]) { 14 if (this[key] && typeof(this[key]) === 'object') { 15 objClone[key] = this[key].clone(); 16 } else { 17 objClone[key] = this[key]; 18 } 19 } 20 } 21 objClone.toString = this.toString; 22 objClone.valueOf = this.valueOf; 23 return objClone; 24 }
这段代码的解释会留到下次解读valueOf,constructor,toString这些方法属性时再解释。最后我们写一段测试代码,跟上次的一样,结果也一样,但定义类的方式改变了而已。
1 Test = function () { 2 var c1 = new Circle(10); 3 var c2 = c1.clone(); 4 c2.center.set(200, 250); 5 // console.log(c1); 6 // console.log(c2); 7 c1.draw(); 8 c2.draw(); 9 var p1 = new Point(50, 70); 10 var p2 = new Point(150, 200); 11 // console.log(p1); 12 // console.log(p2); 13 p1.draw(); 14 p2.draw(); 15 16 p3 = p1.clone(); 17 p3.set(150, 350); 18 // console.log(p3); 19 p3.draw(); 20 } 21 window.onload = Test;
到此,Javascript面向对象编程的实现基本完结了,大家使用时遇到什么bug可以留言给我。不过下一篇文章会解读Javascript里面的instanceof,typeof,toString,valueOf,constructor这些方法对于不同类型的参数会返回什么值,可以结合克隆函数一起解读。
代码下载地址:https://files.cnblogs.com/avicha/Javascript面向对象编程的实现(2).rar 压缩包包含了几个JS框架的源代码,可以查阅相关的函数实现方式。