构造函数 + 原型链继承 + 临摹面向对象模式的canvas动画框架

感谢谢帅shawn分享的canvas动画框架,我再来分一次

//动画框架

http://neekey.net/blog/2011/05/11/canvas-%E7%AE%80%E5%8D%95%E5%8A%A8%E7%94%BB%E5%AE%9E%E7%8E%B0%E6%80%9D%E8%B7%AF/

//使用JavaScript和Canvas开发游戏

http://www.cn-cuckoo.com/2011/08/10/game-development-with-javascript-and-the-canvas-element-2554.html

 

     之前学过的OC是纯粹的面向对象语言,所以我一直很想知道JS里的难道没有“类”的概念吗?直到小新大神讲到了构造函数和原型链的时候,我明白了——在JS中,没有传说中的“类”,而是利用“构造函数+原型链”来模拟其他语言中“类”和“继承”的思路。

     初看第一个链接中的动画框架,本以为有之前的OC基础,可以看懂,结果完败。无奈之下只能翻书,把JS红皮书第六章《面向对象的程序设计》通读了一遍,练看带琢磨,2天时间,看完直接低血糖了。。。在此要再次重点感谢小新大神之前的讲解,要不2个月也看不明白。

     本章的知识结构是递进完善式的,所以建议想要去看书的同学,不要像我一样,卡在某个你认为不太合理的概念上苦苦纠结,因为你总是会发现,后面一节的概念,是用来完善或代替前一节的概念的(TMD!)。下面的总结,也将采用递进完善式,具体代码请看书P144。

 

构建对象的方式:

    工厂模式:很基础的创建方式,问题是,不能识别对象的爹是谁,总是window。

    构造函数模式:完善了工厂模式,可以识别对象是属于哪类。使用new操作符,来确保this的指向,不用new也可以创建,但是this还是window。问题是,方法应当被共享,而不是被每个新实例重复的创建。如果提到外面去声明方法,就失去了封装性。

    原型模式:每个构造函数被声明时,会产生一个他的原型对象prototype,可以将需要共享的属性和方法放在这个对象里,就不会被重复创建了。问题是,对于基本数据类型的属性,不能改。对于引用类型的属性,牵一发而动全身。

    组合使用构造函数和原型:将需要改的,放在构造函数中,不需要改的即可以被共享的,放在原型中。

    动态原型模式:基于上面的组合式,再次优化,将原型的初始化放在构造函数中,只不过要加个if判断,当this.xxx != 'function'时,将this.xxx初始化到 prototype中。这样可以防止原型中的xxx被修改后,牵一发而动全身。

   (寄生构造函数模式:这个方式一般不用!原理是把工厂模式封装在构造函数模式里。没看出有毛好处。)

   (稳妥构造函数模式:安全模式,不用this不用new,除了用显式声明的方法来访问属性之外,别无他法。)

层层递进有木有!!

 

  继承:存在一个“类”(父类),我要构造一个新的类,让他除了有自己的特有的属性和方法之外,还具有老类的一切。其实就是你想扩展一个类的功能,但是又不想直接去改他的原始代码,因为他可能很重要,别人还要用。那么就继承一个子类吧!

  原型链:真是个相当逻辑的东东,在挠墙的同时不得不佩服这种设计思路,谁想的啊这是。。。

  书上的概念,简而言之就是另一个指向另一个,而另一个又指向另一个。。。

  我的理解就是:每个“类”(即构造函数)都会伴随着一个原型对象,构造函数和原型中都可以放属性和方法。只不过,构造函数中放的是未来很有可能被修改的属性(私家厕所),而原型里放的都是共享的(公共厕所)。上面的大段中提到,创建对象的合理方法是组合式,也就是说,构造函数+原型,构成了“类”的一个整体。当我想继承一个“新类”的时候,我就在新类的构造函数中声明新加入的属性,然后让新类的原型直接等于一个老类的new实例。此时,新类的构造函数+原型构建完成了,也就成为了一个新的整体。这样一来,新类不但具有自己的新属性(在构造函数中),还包含老类的所有属性和方法(在原型中)。当然,你依旧可以在新类的原型中,继续加入那些适合被共享的新方法。

    原型链的问题:牵一发而动全身。还有,不敢给超类的构造函数传参,主要是因为,超类的构造函数现在是子类的原型,你传参了之后怕是会影响子类的所有实例。

  借用构造函数:又是完善。。。这里第三次感谢小新大神,因为他讲了this,call和apply。利用call可以改变作用域来控制this指向,所以在子类的构造函数中,可以用superClass.call(this,x,y,z)来调用父类的构造函数,从而解决了子类new时传参的问题。问题是,这样继承,完全没用上prototype共享的特性,还是会出现重复声明方法的现象。

    组合继承:继续完善。。。用call的方式在子类的构造函数中继承,再将子类的原型等于父类的new实例。这样就是在借用构造函数的基础上,用上了prototype的共享特性。问题是,子类的构造函数中继承了一次父类的构造函数,在子类的原型中又继承了一次,相当于父类的构造函数部分被继承了俩次,也就是父类构造函数中包含的属性在子类的构造函数中有一份,在子类的原型中还有一份,而调用属性的时候,JS引擎会先找构造函数再找原型,所以原型中的那一份相当于被屏蔽掉了,浪费了。看到这的时候,我卡了,因为书上没有提到重复调用的问题,是我自己瞎琢磨的,卡了很久我才决定还是先往下看吧。而且我还在想另一个问题,为啥不能让子类的原型=父类的原型呢?这样不是一样继承了吗?后来明白了,如果这样,你给子类的原型添加新方法的时候,父类的原型也会被添加新方法,那也就是跟直接改父类源代码没区别了,失去了继承的意义。而等于new实例的话,你给子类原型添加新方法的时候,相当于是给这个new实例添加方法,并不会影响到父类的原型。

    原型式继承:可以这么理解,把一个老对象,包装成一个新对象,把老对象作为新对象的原型。这是一种浅复制的过程。ECMAScript5标准自带这种继承的函数。

var anotherPerson = Object.create(person,自定义描述符); 这个继承方法是个伏笔,是为了后面的终极方法做铺垫的,过!

    寄生式继承:就是把原型式继承的语句,和给新对象添加新方法语句,封装在一个新的函数中,依旧是蛋疼的伏笔!

    寄生组合式继承(终极境界):

    看到这的时候,确实低血糖了。让我比较欣慰的是,这个终极方法,正是解决之前我卡住的那个问题的——“俩次调用问题”。回想一下,刚才我们说的,不希望拥有俩份父类的构造函数中的属性,那么,我们就需要减少一次调用。而在子类中用call调用父类的构造函数这个是不能被删掉的,因为他解决了传参的问题。所以,只能拿subClass.prototype = new superClass()这个开刀了。其实我们就是想用这个语句,让子类的原型=一个新的实例,这个实例并不需要是父类的new实例,而只要包含父类的原型就够了,因为我们已经有一份父类的构造函数中的属性了。童鞋,有没有一个疑问——那让子类的原型=父类的原型不是正好吗?呵呵,这不就是我刚才卡的时候的第二个问题么。。。请翻上去再看一下解释吧。换言之,如果我们能够构造一个对象obj,让他只拥有父类的原型,而不包含父类的构造函数,再让subClass.prototype = obj; 这样就解决问题了。这就用到了原型式继承的浅复制原理,以及寄生式继承的封装方式增强对象(就是给对象添加新东西)。实现这个终极目标,我们需要创造一个函数,引用书上的函数名

  function inheritPrototype(子类,父类){

    var obj = object(父类.prototype) ;//这是刚才说到的ECMAScript5支持的原型式继承方法,与object.create()类似。这一步就是复制一个父类的原型。

    obj.constructor = 子类;      //由于下一句代码属于重写子类原型,将丢失原型的constructor的默认指向,所以先修复一下。

    子类.prototype = obj;      //把这个只拥有父类原型,而不包含父类构造函数的新对象,赋给子类的原型,实现继承。

  }

  有了这个函数后,当我们继承时,需要写 subClass.prototype = new superClass()的时候,就调用 inheritPrototype(subClass,superClass)来代替。            

  看完了这些知识,我又去看那个游戏框架了,并且顺着写一遍,因为是凭感觉临摹,所以其中有我自己的修改。写完了,调试调了1个小时,各种大小写错误,语法小错误,其中还有俩处都用到了闭包来解决this指向问题,问题原因是当一个函数作为参数传入setInterval()时,this会变window。

setInterval((function(self){

      return function(){

          alert(self.xxx);

          }

      })(this),30);

总之,由于临摹欲望而触发的学习过程,涉及到的知识还是非常全面的,关于canvas的相关操作,在这篇文章里就不写了。

Ps:JS代码写很多之后,找错误真是个麻烦事。由于我这编辑器不会报错,我就得一行一行的用alert('1')去测,看到底是哪行之后不弹出了。

 

奇多圈

下面附上我的临摹框架代码:

<body style="margin:0;padding:0;">
<canvas id='canvas' width="800" height="800"></canvas>
<script>
//寄生组合式继承方式(复制函数+寄生继承函数)
function copy(o){
var Temp = function(){};
Temp.prototype = o;
return new Temp();
}
//构建独立继承父类原型的函数
function inheritPrototype(subclass,superclass){
var proto = copy(superclass.prototype);
proto.constructor = subclass;
subclass.prototype = proto;
}

//精灵(动画元素的抽象类)
var Sprite = function(){
this.speed = {
x : 1,
y : 1
};
}

Sprite.prototype = {
move : function(){
setInterval((function(self){
return function(){
self.x += self.speed.x;
self.y += self.speed.y;
}
})(this),30);
},
draw : function(){}
};

//帧(控制画布刷新的类)
var Fps = function(){
//所有需要重绘的精灵数组
this.sprites = [];

//重绘所需的定时器
this.render = function(){
setInterval((function(self){
//清空画布
return function(){
self.ctx.clearRect(0, 0, 800, 800);
//重绘所有
for(var i in self.sprites){
self.sprites[i].draw();
}
}
})(this),30);//1000/30 = 33帧/秒
}
}

//动画实体
var Circle = function(ctx,x,y,radius){
Sprite.call(this);
this.ctx = ctx;
this.x = x;
this.y = y;
this.radius = radius;
this.strokeStyle = 'rgba('+Math.floor(Math.random()*225)+','+Math.floor(Math.random()*225)+','+Math.floor(Math.random()*225)+','+Math.floor(Math.random()*5+5)/10+')';
this.fillStyle = 'rgba('+Math.floor(Math.random()*225)+','+Math.floor(Math.random()*225)+','+Math.floor(Math.random()*225)+','+Math.floor(Math.random()*5+5)/10+')';
this.lineWidth = Math.floor(Math.random()*10);
}

inheritPrototype(Circle,Sprite);

Circle.prototype.draw = function(){
this.ctx.beginPath();
this.ctx.lineWidth = this.lineWidth;
this.ctx.arc(this.x,this.y,this.radius,0,Math.PI*2,true);
this.ctx.strokeStyle = this.strokeStyle;
this.ctx.fillStyle = this.fillStyle;
this.ctx.stroke();
this.ctx.fill();
};

var ctx = document.getElementById('canvas').getContext('2d');
//创建画布刷新类Fps
var fps = new Fps();
fps.ctx = ctx;
fps.render();//启动

for (var i=0; i<50; i++){
var circle = new Circle(ctx,400,600,Math.random()*60+10);
circle.speed = {x:Math.random()*10-5,y:Math.random()*10-10};
circle.move();
fps.sprites.push(circle);
}
</script>

</body>

    

posted @ 2012-06-25 03:04  丛子  阅读(679)  评论(3编辑  收藏  举报