原作者:Steven Riche
发布时间:2014年2月14日
原文链接:http://code.tutsplus.com/tutorials/javascript-animation-that-works-part-3-of-4--net-35248
翻译:子毅 --------- 将JavaScript进行到底
在本系列的第一篇文章中,我们介绍了精灵,以及它是怎样用于在web上创建简单而有效的跨浏览器动画的。在第二篇文章中, 我们创建了一些简单的跑跳动画,尽管它们有相当数量的bug,并且代码还没有准备好用于实际的用途。
今天,我们将解决所有的bug,并整理代码,使用一种叫封装的技术来避免代码冲突,最后我们可以毫无恐惧地将代码发布到页面上。
变量作用域链
为了解释在最后一步中,我们的代码哪里有错误,为什么封装很重要,我们首先需要解释变量作用域链。
想象你正在使用下面的代码。在do_this()函数中,你有一个很有用的变量,并且你想在另一个函数do_that()中使用这个变量,但你会遇到一个小问题
function do_this() {
var ver_helpful_variable = 20;
...
//This shows '20' just like you expect
alert(ver_helpful_variable);
}
function do_that() {
alert(ver_helpful_variable); ; // But this shows 'undefined'!
}
在变量声明的函数中,你的变量工作的很好,但是在这个函数外面,它好像不存在一样!这是因为do_that()没有在变量ver_helpful_variable的作用域链中
变量只会在声明它的块级代码中可用,这是它们的作用域链。一旦块级代码结束运行,其变量将会被删除。
请看下面的例子:
var w = 1;
function a() {
var x = 2;
function b() {
var y = 3;
alert(w); // works
alert(x); // works
alert(y); // works
alert(z); // undefined
}
alert(w); // works
alert(x); // works
alert(y); // undefined
alert(z); // undefined
}
function c() {
var z = 4;
alert(w); // works
alert(x); // undefined
alert(y); // undefined
alert(z); // workds;
b(); // undefined
}
alert(w); // works
alert(x); // undefined
alert(y); // undefined
alert(z); // undefined
首先,我们有变量w,它在所有函数之外定义。它是一个 全局变量,可以在任何地方工作,因为它的作用域链是整个文档
接下来是变量x,因为它是在函数a()中声明的,所以它仅工作在这个函数中。并且,它也可以工作在函数b()中,因为b()也是在a()内部。
但是,在b()中定义的变量(比如y)不能在外面的的函数工作,因为那些函数不在它的作用域链内
你可能也注意到我们尝试在函数c()中调用函数b(),但是失败了。函数名和变量一样遵守同样的规则。
JavaScript有一个怪癖,假如我们在一个函数内使用没有用关键字"var"声明的变量,浏览器将会假设这个变量是全局的。因此,假如你不确定你总是使用var声明了一个变量,最终你不会意识到有多个全局变量的存在。
所以,结论是,任何时候我们声明一个变量,我们可以在块级代码或在块级代码的嵌套块级代码中使用它。假如在作用域链之外使用它,将会得到undefined
这就是为什么在上一篇文章中,我们在所有函数之外声明变量timer,因为我们需要在函数结束后仍然可以抓住这个变量。
var timer; //这是一个全局变量
function run_right(stage, left) {
...
timer = setTimeout(function() { run_right(2, left);}, 200);
...
}
function stop_running() {
document.getElementById('j').style.backgroundPosition = '0px 0px';
//假如'timer'没有设置为全局变量,我们将不能在这里停止它
clearTimeout(timer);
}
为了清除timer,我们需要stop_running()在timer作用域链范围内。因此,我们设置timer为一个全局变量,这样它就可以在任何地方都可用,那这样做有什么问题吗?
全局变量的问题
在任何给定的作用域链中,不可能同时有两个项目被同时调用。假如你尝试拥有两个具有相同名称的不同变量,浏览器只会读取其中一个。因此,假如我们有一个变量被命名为timer,在相同作用域链中,我们还有一个单独的变量也被命名为timer,其中一个将删除并占有另一个,最终我们的代码将会被破坏。假如我们有一个全局变量被命名为timer,那么它将会干扰本页面中,存在于任何其他地方的timer变量——包括任何相关的JavaScript库及外部文件。
这是一个巨大麻烦源,你刚刚看到一个非常整洁的JavaScript插件,而你下载到你的站点,突然其他所有的插件 崩溃……其中一个插件草率地有一个全局变量,碰巧和其他一些东西共享同样的名字,你的浏览器绊倒自身,使得整个页面戛然而止。
更糟糕的是,你第一次测试代码的时候你根本就没有注意到这个问题。就像我们上一篇文章中的动画代码,它自己运行的时候没有任何问题。但是,你添加的模块越多,命名冲突发生的几率就越大,并且你被陷入多个JavaScript中,试图找出哪两个发生了冲突。
现在你可能会问你自己,”全局变量是如此的方便,假如我非常小心地检查代码,并保证没有任何冲突呢?“在理想世界中它可能工作得很好,但是现实中很可能是几个同工作在同一个页面上,或者你不得不在几年后去更新你自己的不同模块的代码,抑或者还有来自第三方超出你控制范围的代码(比如付费广告)
因此,简而言之,你宁愿裸露的电线延着你房子的墙壁或者机械在你的车子里暴露出来,也不希望页面中存在全局变量,错误的出现只是时间问题而已。幸运的是,这里有更好的方法来避免这些陷阱。
封装
使用一种叫做封装技术,我们就可以没有任何问题地使用全局变量。就像在你的代码外面建了一层围墙并且它只有几扇特殊的门。除非你特别地允许,否则没有任何东西可以从这里出入。
JavaScript有一种变量叫做对象。对象是自定义数据的集合,它包括信息和函数(分别指属性和方法)。我们将编写一个函数,它可以创建拥有我们需要“拷贝”进去的所有功能的特殊对象,它甚至允许我们创建多个机器人而不需要重复我们的代码!
现在开始定义一个有变量名的新函数 ,我们需要给函数传递几个参数,我将传递我们将使之成为动画的HTML元素,加上一些可以改变跑动速度和跳跃高度的独特的值,这样我们改变这些值从而得到不同状态的机器人。
var RobotMaker = function(robot, run_speed, jump_height) {
//这里将放置我们所有的变量和函数
//这在我们“实现”的墙内,因此这里的代码不会
//和任何其他地方的代码发生冲突
return {
//这里放置我们所有的“门”
//只有通过这些“门”才能从这段代码拿走或放入东西
//并且,这儿和上面的变量和方法都在RobtMaker的作用域内
//所以我们可以使用上面的任何变量方法
}
};
因为我们将把所有的函数放置到我们的新“墙”内,现在将是一个好时机来重新审视我们起始代码的bug(你可以在 这里看到它们表现出的行为)
你可能会注意到,,假如我们点击两个跑动按钮(或者一个跑动一个跳跃按钮),而在两次点击之间没有点击
停止按钮,J将会同时执行两个动作。第二个问题是,无论J正朝向哪个方向,当我们点击
跳跃或
停止按钮,它每次都会朝向右边。最后,如果在J在第一跳落下时再次点击跳跃按钮,它将延着页面无线地往下落。
为了解决那些问题,我们需要更具体地了解我们想要每个函数执行怎样的功能。
当我们点击向右跑
- 假如J正在跳跃,不做任何事情,让它继续跳跃
- 假如J正在向左跑,将它停止下来
- 让它以合适的帧向右跑动
- 假如J跑到stage的边沿,停下来,面向右边
当我们点击向左跑
- 假如J正在跳跃,不做任何事情,让它继续跳跃
- 假如J正在向右跑,将它停止下来
- 让它以合适的帧向左跑动
- 假如J跑到stage的边沿,停下来,面向左边
当我们点击向停止跑动
- 假如J正在跳跃,不做任何事,J继续跳跃(我们不想让它停止在半空中!)
- 假如向左或向右跑动,则停止跑动
- 假如面向右边,停止时也面向右边,反之,面向左边
当我们点击跳跃
- 假如J正在跳跃,不做任何事,J继续跳跃(我们不想它在半空中再跳一次!)
- 假如J在跑动,则停止它
- 开始跳跃,J之前面向右边,跳跃时也面向右边,反之,面向左边。
- 到达地面时,和跳跃时面向同一个方向
首先,我们要增加几个新变量,因为跑动和跳跃需要不同的计时器,我们将需要两个单独的计时器,需要一个boolean(true/false)来跟踪面向左还是右,并且我们还将创建一个stage变量,使得我们不必键入一个完整的元素名
//在RobotMaker函数内
var stage = document.getElementById('stage');
var run_timer, jump_timer;
var face_right = true;
// 在RobotMaker函数内部 ...
function run_r(phase, left){
face_right = true;
if ((left + (15 * run_speed)) < (stage.offsetWidth - robot.offsetWidth)){
left = left + (15 * run_speed);
robot.style.left = left+"px";
switch (phase){
case 1:
robot.style.backgroundPosition = "-40px 0px";
run_timer = setTimeout(function(){run_r(2, left);}, 200);
break;
case 2:
robot.style.backgroundPosition = "-80px 0px";
run_timer = setTimeout(function(){run_r(3, left);}, 200);
break;
case 3:
robot.style.backgroundPosition = "-120px 0px";
run_timer = setTimeout(function(){run_r(4, left);}, 200);
break;
case 4:
robot.style.backgroundPosition = "-80px 0px";
run_timer = setTimeout(function(){run_r(1, left);}, 200);
break;
}
} else {
robot.style.backgroundPosition = "0px 0px";
}
}
function run_l(phase, left){
face_right = false;
if (0 < robot.offsetLeft - (15 * run_speed)){
left = left - (15 * run_speed);
robot.style.left = left+"px";
switch (phase){
case 1:
robot.style.backgroundPosition = "-40px -50px";
run_timer = setTimeout(function(){run_l(2, left);}, 200);
break;
case 2:
robot.style.backgroundPosition = "-80px -50px";
run_timer = setTimeout(function(){run_l(3, left);}, 200);
break;
case 3:
robot.style.backgroundPosition = "-120px -50px";
run_timer = setTimeout(function(){run_l(4, left);}, 200);
break;
case 4:
robot.style.backgroundPosition = "-80px -50px";
run_timer = setTimeout(function(){run_l(1, left);}, 200);
break;
}
} else {
robot.style.backgroundPosition = "0px -50px";
}
}
function jmp(up, top){
if (face_right){
robot.style.backgroundPosition = "-160px 0px";
} else {
robot.style.backgroundPosition = "-160px -50px";
}
if (up && (robot.offsetTop > (20 * (1 / jump_height)))){
top = top - (top * .1);
robot.style.top = top+"px";
jump_timer = setTimeout(function(){jmp(up, top);}, 60);
} else if (up) {
up = false;
jump_timer = setTimeout(function(){jmp(up, top);}, 60);
} else if (!up && (robot.offsetTop < 115)){
top = top + (top * .1);
robot.style.top = top+"px";
jump_timer = setTimeout(function(){jmp(up, top);}, 60);
} else {
robot.style.top = "120px";
if (face_right){
robot.style.backgroundPosition = "0px 0px";
} else {
robot.style.backgroundPosition = "0px -50px";
}
jump_timer = false;
}
}
现在,所有的变量和函数都在我们的”墙“内,因此现在需要做”门“来只访问我们所需要的。这四个”门“是都是一个对象的方法,它们和我们之前的四个函数一样,并且指向上面受保护的函数。同时,我们也将通过检测jump_timer是否在运行来修复bug,并保证取消run_timer。记住,两个计时器都在RobotMaker()函数的作用域链内,因此在这里可以使用它们。然而,由于它们不是全局变量,我们不会因为使用它们而在其他地方遇到任何麻烦
// 在RobotMaker函数内 ...
return {
run_right : function(){
if (!jump_timer || jump_timer == undefined){
clearTimeout(run_timer);
run_r(1, robot.offsetLeft);
}
},
run_left : function(){
if (!jump_timer || jump_timer == undefined){
clearTimeout(run_timer);
run_l(1, robot.offsetLeft);
}
},
stop_running : function(){
if (!jump_timer || jump_timer == undefined){
clearTimeout(run_timer);
if (face_right){
robot.style.backgroundPosition = "0px 0px";
} else {
robot.style.backgroundPosition = "0px -50px";
}
}
},
jump : function(){
if (!jump_timer || jump_timer == undefined){
clearTimeout(run_timer);
jmp(true, robot.offsetTop);
}
}
}
现在已经写了一个可以创建对象的函数,我们可以多次使用它来创建拥有动画属性的对象。在我们页面的底部,我们将创建两个RobotMaker对象,并给他们传递我们想使它成为动画的元素,以及跑动速度和跳跃高度。
var j = RobotMaker(document.getElementById('j'), 1, 1);
var j2 = RobotMaker(document.getElementById('j2'), .8, 5);
现在我们可以没有任何危险地使用RobotMaker()函数,不用担心它泄漏或干扰我们的代码,同时,仍然可以通过“门”来取得我们想使用的函数。下面是安装方式:
注意这里是怎样使得一个函数和另一个之间不再彼此干扰,并且,你可以单独地操纵每个机器人也使得相互之前没有影响。封装是一项非常重要的技术,如果你想做任何web交互设计,你真的应该熟悉它。
假如你喜欢,你可以在
这里查看所有的包含全部注释的代码,同时你可以
在这里取得第一张精灵表,
在这里取得第二张精灵表。请注意,为了使精灵都可以在相同代码下工作,我需要做另一张和第一张有相同格式和尺寸的精灵表
总结
于是,我们就完成了第三篇精灵动画。在我们的下一篇也是最后一篇文章里,我将替换掉那些按钮而使用鼠标在屏幕上来操纵机器人,并向你展示怎样建立
事件监听,支持跨浏览器以及触摸设备