这是我第一次写技术类的博客文章,之前一直想写,但都一拖再拖,但接触的东西越来越多了,于是觉得越有需要把这些经验记录下来,况且平时随便百度出来同样的文章太多了,基本都是转帖,所以还是希望自己亲手写一些原创的文章。由于本人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来实现继承。测试代码下载如下:

https://files.cnblogs.com/avicha/Javascript面向对象编程的实现(1).rar