这是我第一次写技术类的博客文章,之前一直想写,但都一拖再拖,但接触的东西越来越多了,于是觉得越有需要把这些经验记录下来,况且平时随便百度出来同样的文章太多了,基本都是转帖,所以还是希望自己亲手写一些原创的文章。由于本人js接触时间不长,文章中难免错漏百出,第一次写文章条理可能也有些乱,欢迎大家指出纠正。废话不多说了,第一篇文章内容是Javascript中面向对象编程的实现。
大家都知道Javascript是一种面向对象的脚本语言,所谓面向对象,即具备封装,继承,多态这些特征,Javascript可以通过多种方法实现这些特征,但Javascript跟Java不同,Javascript中是通过原型链实现继承的。Javascript中实现这种类的定义,类之间的继承等方法有很多,定义一个类,具体方法可以参看这个网址,http://www.w3school.com.cn/js/pro_js_object_defining.asp。目前定义一个类最常用的是构造函数和原型混合使用这种方法,用构造函数来定义类的属性,并进行初始化,用原型来定义类的方法。实现继承的方法也有多种,具体方法也可以参看这个网址介绍:http://www.w3school.com.cn/js/pro_js_inheritance_implementing.asp,目前最常用的也是使用call或者apply方法实现属性的初始化,通过原型链实现方法的继承,WebGL的框架Three.js使用的就是这种传统的类定义和继承方式编写的。
这篇文章内容主要是通过两种方法来实现Javascript中的面向对象特征,令它看起来尽量跟Java相似,第一种方法我会先使用传统的Javascript的prototype实现一个简单的面向对象继承的例子,第二种方法我会把针对传统方法的不足之处,作出改进,把类的定义和继承实现封装起来,令它使用起来更加方便,更加接近java的风格。
下面先看看使用传统的方法来实现如下需求:例如现在我打算做一个简单的图形库,图形有点Point和圆形Circle这两种,它们分别继承自图形Shape,点这个类拥有x,y两个参数,和一个draw方法描绘出这个点,圆形这个类拥有圆心和半径这两个参数,其中圆心是一个点对象,也拥有一个draw方法描绘出这个圆形。
首先我们先来定义父类Shape。父类定义了一个属性type,表明图形的类型,一个draw函数向控制台输出一句语句,我正在画什么图形。提示一点,如果你的父类是个抽象类,抽象类的方法不能够被直接调用的话,可以通过throw抛出异常,提示让子类去实现这个方法(下面注释的地方):
1 Shape = function () { 2 this.type = ''; 3 } 4 Shape.prototype.draw = function () { 5 console.log('I am drawing a shape of ' + this.type + '!'); 6 //throw "需要子类实现draw方法"; 7 }
接下来定义子类Point。首先Point是通过构造函数方式定义了点的坐标x,y值,下一步通过把Point的prototype指向一个父类实例化对象(这时候Shape被实例化了一次,所以Shape初始化的代码会被执行的),这样子就完成了继承自Shape类,同时Point的实例化对象p instanceof Shape会为true。然后还要修改子类的构造器constructor,因为默认子类的constructor是继承父类的,也就是Shape,之后可以在子类的prototype属性再添加其它子类自己的函数。
1 //定义一个点 2 Point = function (x, y) { 3 this.type = 'Point'; 4 this.x = x || 100; 5 this.y = y || 100; 6 } 7 Point.prototype = new Shape(); 8 Point.prototype.constructor = Point; 9 Point.prototype.set = function (x, y) { 10 this.x = x; 11 this.y = y; 12 }; 13 Point.prototype.draw = function (ctx) { 14 Shape.prototype.draw.apply(this, arguments); 15 var ctx = ctx || document.getElementById('canvas').getContext('2d'); 16 ctx.beginPath(); 17 ctx.arc(this.x, this.y, 5, 0, 2 * Math.PI, true); 18 ctx.closePath(); 19 ctx.fill(); 20 }
这里注意两点:
1.定义prototype的顺序,我们必须先继承父类,再定义子类自己的方法,不然Point.prototype=new Shape()会覆盖了之前定义的set和draw方法,同时虽然我们可以通过
Point.prototype={
constructor:Point,
set:function(){...},
draw:function(){...}
}
这样子来定义prototype,但这样子也是相当于prototype重新赋值为一个object,所以也是会覆盖掉Point.prototype=new Shape()的,但Point.prototype.set则只是在原来prototype对象上添加属性,不会影响之前的prototype。
2.使用这种原型链来实现继承,其中一个问题就是方法的覆盖,例如我希望在执行子类的draw方法时,先执行父类的draw,但Point的draw方法已经覆盖了父类Shape的draw方法,所以这时候若在Point的draw方法中调用this.draw()这样子的方法是调用不了父类的方法的(我就试过这样子写)。这时候可以调用
Shape.prototype.draw.apply(this, arguments),这句语句意思就是调用Shape的draw方法,并把Shape的this关键字指向Point的当前this,并把参数传递过去。
使用同样的方法定义圆形类Circle,代码如下:
1 //定义一个圆 2 Circle = function (r, x, y) { 3 this.type = 'Circle'; 4 this.center = new Point(x, y); 5 this.radius = r || 1; 6 } 7 8 Circle.prototype = new Shape(); 9 Circle.prototype.constructor = Circle; 10 Circle.prototype.draw = function (ctx) { 11 Shape.prototype.draw.apply(this, arguments); 12 var ctx = ctx || document.getElementById('canvas').getContext('2d'); 13 ctx.beginPath(); 14 ctx.arc(this.center.x, this.center.y, this.radius, 0, 2 * Math.PI, true); 15 ctx.closePath(); 16 ctx.fill(); 17 };
接下来我们写一个测试函数测试一下结果,draw函数可以自己改写,因为我这里是使用html5的canvas标签进行简单的描绘点和圆形,同时我写了一个深度克隆的函数clone,用于深度复制一个创建的对象,主要测试一下这样子定义类在复制对象时会不会出错,例如克隆体改变时改变了源对象。clone代码如下,代码解释会在第二种实现方式时再次使用再讲解:
1 Object.prototype.clone = function () { 2 if (!this || this instanceof HTMLElement) { 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 }
测试代码如下:如果你想加深对上面代码的认识,可以在控制台输出每个对象,查看它的属性和prototype属性,这样子可以更加清楚上面代码是怎样实现继承的。如果没有出错,测试结果将会见到画布上画着两个圆形和三个点,可以查看控制台查看输出消息。
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;
这次先写到这里,主要先做个铺垫,复习一下传统的继承机制实现方法,下一次会结合prototype这个框架说说另一种封装过后的面向对象编程的实现。当然没有说那种方法好,那种方法不好,如果是简单的写几个类,使用传统的方法也就可以了,但如果是编写框架,例如我之前就是编写游戏框架,需要定义大量的类,且存在继承关系,这时候最好还是封装一下使用,像物理引擎Box2d的js版本就是使用了prototype来实现继承。测试代码下载如下: