学习 JavaScript:第 1 部分 - 创建星空
在这篇文章中,我们将使用 JavaScript 创建一个星空。我们将了解核心语言功能如何工作、如何创建类以及如何使用 HTML5 Canvas。
引言
查看我们将制作的示例:dwmkerr.com/experiments/starfield
JavaScript 的普及度正在爆炸式增长。在本系列文章中,我们将通过实践来学习 JavaScript。我不会深入探讨语法和设计模式的理论讨论——我将制作项目,我们将在实践中学习理论。
本系列文章的理念是您可以随时加入。查看下面的目录,找到一篇您感兴趣的文章。在每篇文章中,我们都会实际创建一些东西,并在此过程中学习一些技巧。
- 学习 JavaScript 第一部分 - 创建一个星空
- 学习 JavaScript 第 2 部分 - 太空侵略者
- 学习 JavaScript 第 3 部分 - AngularJS 和 Langton's Ant
第 1 步 - 创建一个网页
我们需要一个超简单的网页来运行星空。让我们创建最精简的 HTML 模板
<!DOCTYPE html>
<html>
<head>
<title>Starfield</title>
</head>
<body>
<div id="container"></div>
</body>
</html>
我们创建了一个轻量级的 HTML 页面,其中包含一个 div
,这个 div
将承载我们的星空。我们首先要确保它填满屏幕,可以通过在 head
中添加一些 CSS 样式来实现
<head>
<title>Starfield</title>
<style>
#container {
width: 100%;
height: 100%;
position: absolute;
left: 0px;
top: 0px;
}
</style>
</head>
现在让我们创建一个新的 JavaScript 文件,我们称之为 starfield.js。将其放在与 HTML 文件(我们可以称之为 index.html)相同的目录中。让我们确保在 HTML 中包含 starfield
<body>
<!-- snip -->
<script src="starfield.js"></script>
</body>
第 2 步 - 创建 Starfield 类
如果您不熟悉 JavaScript,那么这里的事情将变得有趣。我们将创建一个类来表示 Starfield
。在 starfield.js 文件中,添加以下代码
// Define the starfield class.
function Starfield() {
this.fps = 30;
this.canvas = null;
this.width = 0;
this.height = 0;
this.minVelocity = 15;
this.maxVelocity = 30;
this.stars = 100;
this.intervalId = 0;
}
如果您之前在 JavaScript 中创建过类,可以跳过这部分解释,否则……
JavaScript 类
我们刚刚写的内容对于 C++ 或 C# 开发者来说,看起来不像一个类。它看起来像一个函数。嗯,它就是。在 JavaScript 中,没有类(尽管在 ECMAScript 6 中我们会得到它们)。但这实际上并不能阻止我们创建类,或者至少是类似类的对象。函数是对象——我们可以创建它们的实例,并且可以设置它们的属性。我们将像这样创建一个 Starfield
// Create a starfield
var starfield = new Starfield();
这非常重要。我们创建了该函数的一个新实例并调用了它。通过调用它,我们已经在 function
对象上设置了一些属性。我们只在创建的 function
对象上设置了它们——而不是每个实例。
现在就像创建属性一样,我们也可以创建 function
,例如
// Define the starfield class
function Starfield() {
/* snip */
this.start = function() { /* do something*/ };
}
我们可以像这样调用这个 function
// Create a starfield.
var starfield = new Starfield();
starfield.start();
但我们实际上可以做得更好一点。如果我们的函数很复杂,那么为每个实例重新创建它实际上效率不高,我们真正想要做的是只创建一次 function
,让每个实例自动获取它。这就是原型发挥作用的地方。
当您在由 function
创建的类型实例后面加上点号时,引擎将尝试在该类型上查找属性。如果找不到,它会查看该类型的“prototype
”。当我们使用函数创建类型实例时,它每次都会继承相同的原型。我的意思是这样
// Define the starfield class.
function Starfield() {
/* snip */
}
// Add a function to the starfield class
Starfield.prototype.start = function() {
/* here's the function */
};
我们修改了 Starfield
函数的 prototype
,这意味着每次创建 Starfield
时,它都会获得 start
函数,即使我们只声明了一次。
还是不清楚?想想这个
var starfield = new Starfield();
starfield.stop = function() { /* do something */ };
我们已经在 starfield
的一个实例上创建了一个 stop
函数——所以我们只能在那个 starfield
上调用 stop
。那么这个呢?
var starfield = new Starfield();
Starfield.prototype.pause = function() { /* do something */ };
现在我们在 Starfield
的 prototype
上创建了一个名为 'pause
' 的 function
。这意味着我们可以从任何 starfield
实例调用它。为什么?因为引擎首先在实例上查找 'pause
',但没有找到。然后它在实例的 prototype
上查找。prototype
是所有 Starfield
共享的,所以它找到了!
这在开始时确实很难适应,我能推荐的最有用的事情是多玩玩它。创建一些类,阅读关于 prototype
的资料并尝试一下。
第 3 步 - 初始化星空
我们有点跑题了,但这就是文章的目的。我们创建了带有一些属性的 Starfield
类,现在让我们实际做点什么。
我们的目标是实现两个函数——第一个将初始化 starfield
,使其准备好使用;第二个将启动它,实际运行动画。让我们先编写 initialise
。
还记得 prototype
吗?我们将在那里放置 initialise
函数——因为每个 starfield
都需要它。
// The initialise function initialises a starfield object so that
// it's ready to be started. We must provide a container div, that's
// what the starfield will live in.
Starfield.prototype.initialise = function(div) {
var self = this;
// Store the div
this.containerDiv = div;
self.width = window.innerWidth;
self.height = window.innerHeight;
window.addEventListener('resize', function resize(event) {
self.width = window.innerWidth;
self.height = window.innerHeight;
self.canvas.width = self.width;
self.canvas.height = self.height;
self.draw();
});
// Create the canvas
var canvas = document.createElement('canvas');
div.appendChild(canvas);
this.canvas = canvas;
this.canvas.width = this.width;
this.canvas.height = this.height;
};
这是一篇学习 JavaScript 的文章,所以我将逐一讲解。如果你觉得它很容易理解,可以跳过。
Starfield.prototype.initialise = function(div) {
我们正在向 starfield prototype
添加一个 function
,这意味着每个 starfield
都能够使用它。该 function
接受一个参数,它是一个 div
。它没有类型,因为 JavaScript 没有类型(尽管如果你想要类型,可以查看 TypeScript)。
var self = this;
// Store the div.
this.containerDiv = div;
self.width = window.innerWidth;
self.height = window.innerHeight;
我们正在将“this
”变量的副本存储在一个局部变量中。原因很快就会变得明显……接下来,我们存储对提供给我们的 div
的引用(注意到我们没有在构造函数中创建“containerDiv
”吗?没关系,您可以根据需要创建属性。我通常在构造函数中创建它们,以便我可以快速查看应该有哪些属性。)
我们还存储了浏览器窗口的客户端区域。“window
”对象由浏览器提供,它允许您对浏览器执行许多操作。
window.addEventListener('resize', function resize(event) {
self.width = window.innerWidth;
self.height = window.innerHeight;
self.canvas.width = self.width;
self.canvas.height = self.height;
self.draw();
});
现在我们将处理窗口的“resize
”事件。有两种方法可以做到这一点。第一种是设置“onresize
”函数,第二种是使用“addEventListener
”。
通常,我们应该使用“addEventListener
”函数,因为这不会阻止任何已经添加的其他事件的工作。如果我们直接设置“onresize
”,我们会替换之前可能存在的任何内容。因此,通过使用“addEventListener
”,我们确保不会干扰其他库。当函数被调用时,我们将更新我们的 width
和 height
,更新 canvas width
和 height
(我们稍后将创建 canvas
),并调用“draw
”函数,我们也将很快创建它。
为什么我们使用“self
”而不是 this
?
好的,我们在 'initialise
' 函数中编写这段代码,在 initialise
函数的上下文中,'this
' 是 Starfield
。但是当窗口为我们调用 'resize
' 函数时,当我们在那个 function
中时,'this
' 实际上是窗口。所以为了编辑 starfield
实例,我们使用我们之前声明的 'self
' 变量,它是对 starfield
的引用。
这实际上是相当高级的——function
被调用了,我们却以某种方式使用了在函数外部创建的变量。这被称为闭包,它让我们的生活轻松多了!闭包允许我们从另一个上下文访问状态。在编写回调函数等等时,这是一件非常有用的事情。
// create the canvas
var canvas = document.createElement('canvas');
div.appendChild(canvas);
this.canvas = canvas;
this.canvas.width = this.width;
this.canvas.height = this.height;
};
这是 initialise
函数的最后一部分。我们使用“document
”对象创建一个新的 HTML 元素。
document
对象在基于 Web 的 JavaScript 开发中极其重要——为什么呢?它代表了“DOM”(文档对象模型)。这实际上是 HTML 页面的树形结构——节点、元素、属性等等。我们在客户端 JavaScript 中所做的大量工作都与 DOM 相关,我们改变元素的样式,添加新项目等等。在这种情况下,我们使用 document 创建一个新的 HTML Canvas
,然后将其添加到我们的 container div
中——然后我们设置它的 width
和 height
。
这是基础——我们刚刚通过编程创建了 HTML,这是我们将在 JavaScript 中经常做的事情之一。
这就是初始化!
我们来回顾一下
- 存储
div
。 - 存储有用的属性,
width
和height
。 - 监听窗口大小调整,当它发生时,更新
width
和height
并重新绘制。 - 创建一个
canvas
用于绘制,并使其成为container div
的子元素。
第 4 步 - 启动星空
这是有趣的部分,我们现在可以实际创建主要的 starfield
逻辑了。让我们创建 starfield
的 start
函数,它将启动 starfield
运行。
Starfield.prototype.start = function() {
// Create the stars
var stars = [];
for(var i=0; i<this.stars; i++) {
stars[i] = new Star(Math.random()*this.width, Math.random()*this.height,
Math.random()*3+1,
(Math.random()*(this.maxVelocity - this.minVelocity))+this.minVelocity);
}
this.stars = stars;
我们正在添加一个 function
,就像之前通过使用 prototype
一样。我们做的第一件事是创建一个数组——就是以 'var stars
' 开头的那一行。通过将其设置为 '[]
',我们将其设置为一个数组。数组在 JavaScript 中功能非常强大,我们也可以将其用作队列或列表。现在我们遍历星星的数量(我们在构造函数中设置了它),每次都在数组中创建一个 Star
——等等,什么是 star
?将这段代码放在文件的末尾——而不是我们正在编写的函数中!
function Star(x, y, size, velocity) {
this.x = x;
this.y = y;
this.size = size;
this.velocity = velocity;
}
Function
作为类是不是很奇怪?但它们也相当容易使用!我想表示 star
对象,所以我有一个函数可以设置一些属性。在函数上调用 'new
' 会根据它实例化一个类型,并带上我提供的属性。这就是我将 'star
' 对象添加到数组的方式。我使用 Math.random
,这是一个标准的 JavaScript 函数,它返回一个介于 0
和 1
之间的值,用于随机化星星的初始位置、大小和速度。
现在回到函数——我们创建了数组,现在我们将其存储在 'this
' 中——即 Starfield
对象。接下来,我们将使用 JavaScript 的 setInterval
函数。该函数设置一个回调,每当一个间隔过去时就会调用该回调。我们的间隔由 fps(每秒帧数)指定。每次我们到达该函数时,我们都会调用 'update
' 和 'draw
'。我们使用 'self
' 闭包来确保我们在 starfield
对象上调用它们!
var self = this;
// Start the timer.
this.intervalId = setInterval(function() {
self.update();
self.draw();
}, 1000 / this.fps);
};
我们还存储了由 'setInterval
' 返回的 id——我们可以用它来稍后停止它。
现在我们将创建 update
函数,它将 update
starfield
的状态。
Starfield.prototype.update = function() {
var dt = 1 / this.fps;
for(var i=0; i<this.stars.length; i++) {
var star = this.stars[i];
star.y += dt * star.velocity;
// If the star has moved from the bottom of the screen, spawn it at the top.
if(star.y > this.height) {
this.stars[i] = new Star(Math.random()*this.width, 0, Math.random()*3+1,
(Math.random()*(this.maxVelocity - this.minVelocity))+this.minVelocity);
}
}
};
这是移动 star
的核心逻辑——我们计算已经过去的时间(dt
是 delta t
)。然后我们遍历每个 star
,并根据其速度和已过去的时间更新其位置。
最后,如果 star
已经移到屏幕底部之外,我们会在顶部创建一个新的 star
!
接下来是 draw
函数
Starfield.prototype.draw = function() {
// Get the drawing context
var ctx = this.canvas.getContext("2d");
// Draw the background
ctx.fillStyle = '#000000';
ctx.fillRect(0, 0, this.width, this.height);
// Draw stars
ctx.fillStyle = '#ffffff';
for(var i=0; i<this.stars.length;i++) {
var star = this.stars[i];
ctx.fillRect(star.x, star.y, star.size, star.size);
}
};
信不信由你,这是一个新的 HTML5 特性——Canvas
。Canvas
是一个对象,您可以使用它在 JavaScript 中进行基于位图的绘制。您可以绘制线条、多边形等等。事实上,您几乎可以用它绘制任何东西。
我们所需要做的就是用黑色填充背景,将填充颜色设置为白色,并为每个 star
绘制一个小的矩形。
第 5 步 - 试一试!
我们做到了!将代码添加到您的 HTML 页面中
<body>
<!-- snip -->
<script>
var container = document.getElementById('container');
var starfield = new Starfield();
starfield.initialise(container);
starfield.start();
</script>
</body>
运行页面,您就看到了。
我们学到了什么?
以下是我们学到知识的总结
- 在 JavaScript 中,类使用“
constructor
”函数创建。 - 调用“
constructor
”函数时使用“new
”来创建该类型的新实例。 - “
constructor
”函数有一个名为“prototype
”的属性,并且在所有类型实例之间共享。 - 通常,类成员函数在
prototype
上定义。 - JavaScript '
window
' 对象由浏览器提供,代表代码运行的环境。 - JavaScript '
document
' 对象由引擎提供,表示 HTML 文档。 - 在 JavaScript 中可以使用 '
setInterval
' 创建定时器。 - JavaScript 中的 '
this
' 关键字应谨慎使用——在回调函数中,'this
' 可能不是您所期望的。 - 你可以使用
var array = [];
在 JavaScript 中创建一个数组。 - 您可以在回调函数中使用在回调函数外部定义的变量,这是一个闭包。
告诉我你的想法
如果您觉得这篇文章有用,请告诉我——如果有什么我可以做得更清楚的地方,也请告诉我。
下一篇文章将是“用 JavaScript 创建太空侵略者”。
历史
- 2014年1月31日:初始版本