JavaScript贪吃蛇游戏实现:从HTML5 Canvas到面向对象编程详解
本文详细解析基于JavaScript和HTML5 Canvas开发的贪吃蛇小游戏。通过Food、Snake和Game三个核心类的面向对象设计,讲解如何利用绝对定位实现网格移动、使用数组管理蛇身坐标、以及通过定时器与键盘事件控制游戏逻辑。文章涵盖匿名函数自调用、原型方法扩展及this指向处理等关键技术点,提供完整代码思路与实现细节。
本文通过一个完整的贪吃蛇小游戏Demo,详细讲解JavaScript游戏开发的实现思路与代码结构,供读者参考学习。
近期跟随视频教程完成了一个贪吃蛇项目的开发,
在此记录其核心实现逻辑。
项目代码结构与匿名函数自调用
首先展示项目代码结构: 静态页面部分
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>贪吃蛇</title>
</head>
<style>
*{
margin: 0;
padding: 0;
}
.map{
width:800px;
height: 600px;
background-color: #ccc;
position:relative;
}
</style>
<body>
<!-- 画出地图,设置样式 -->
<div class="map">
</div>
</body>
<script src="food.js"></script>
<script src="snake.js"></script>
<script src="game.js"></script>
</html>
food.js
//食物就是一个对象,宽高横纵坐标,先定义构造函数,然后创建对象
(function (){
var elements=[];//用来保存每个小方块食物
function Food(x,y,width,height,color){
//横纵坐标
this.x=x||0;
this.y=y||0;
this.width=width||20;
this.height=height||20;
//背景颜色
this.color=color||"green";
}
//为原型添加初始化的方法(作用:在页面上取显示这个食物)
//因为食物要在地图上显示,所以,需要地图这个参数
Food.prototype.init=function(map){
//先删除这个食物
//外部无法访问的函数
remove()
var div=document.createElement("div");
//把这个div加到map中
map.appendChild(div);
//设置div的样式
div.style.width=this.width+"px";
div.style.height=this.height+"px";
div.style.backgroundColor=this.color;
// div.style.left=this.x+"px";
//先脱离文档流
div.style.position="absolute";
//随机横纵坐标
this.x=parseInt(Math.random()*(map.offsetWidth/this.width))*this.width;
this.y=parseInt(Math.random()*(map.offsetHeight/this.height))*this.height;
div.style.left=this.x+"px";
div.style.top=this.y+"px";
// Food.prototype.init=function(map){
// }
//把div加入到数组elements中
elements.push(div);
}
function remove(){
//elements数组中有这个食物
for(var i=0;i<elements.length;i++){
var ele=elements[i]
//找到这个子元素的父级元素,然后删除这个子元素
ele.parentNode.removeChild(ele);
//再次把elements中的这个子元素也要删除
elements.splice(i,1)
}
}
//把Food暴露给Window,外部可以使用
window.Food=Food;
}());
snake.js
//蛇
(function(){
var elements=[];//存放小蛇的每个身体部分
//蛇的构造函数
function Snake(width,height,direction){
//小蛇的每个部分的宽
this.width=width||20;
this.height=height||20;
//身体
this.body=[
{x:3,y:2,color:"red"},
{x:2,y:2,color:"orange"},
{x:1,y:2,color:"orange"}
];
this.direction=direction||"right";
}
//蛇的初始化
Snake.prototype.init=function(map){
remove()
//循环遍历创建div
for(var i=0;i<this.body.length;i++){
var obj=this.body[i];
//创建div
var div=document.createElement("div");
//把div加入到map地图中
map.appendChild(div);
//设置div的样式;
div.style.position="absolute";
div.style.width=this.width+"px";
div.style.height=this.height+"px";
div.style.left=obj.x*this.width+"px";
div.style.top=obj.y*this.height+"px";
div.style.backgroundColor=obj.color;
//把div加入到elements数组中--目的是删除
elements.push(div)
}
}
//蛇的移动
Snake.prototype.move=function(food,map){
//改变蛇身体位置
var i=this.body.length-1; //2
for(;i>0;i--){
this.body[i].x=this.body[i-1].x;
this.body[i].y=this.body[i-1].y;
}
//判断方向---改变小蛇的头的坐标位置
switch (this.direction){
case "right":
this.body[0].x+=1;
break;
case "left":
this.body[0].x-=1;
break;
case "top":
this.body[0].y-=1;
break;
case "bottom":
this.body[0].y+=1;
break;
}
//判断有没有吃到食物
//小蛇的头的坐标和食物位置
var headX=this.body[0].x*this.width;
var headY=this.body[0].y*this.height;
//食物的横纵坐标
var foodX=food.x;
var foodY=food.y;
if(headX==foodX&&headY==foodY){
//获取蛇的最后尾巴
var last=this.body[this.body.length-1];
//把最后的蛇尾复制一份
this.body.push({
x:last.x,
y:last.y,
color:last.color
})
//重新初始化食物
food.init(map);
}
}
//删除小蛇的私有函数
function remove(){
//获取数组
var i=elements.length-1;
for(;i>=0;i--){
//先从当前的子元素中找到该子元素的父级元素,然后再弄死这个子元素
var ele=elements[i];
//从map地图上删除这个子元素div
ele.parentNode.removeChild(ele);
elements.splice(i,1);
}
}
window.Snake=Snake;
}());
//蛇
(function(){
var elements=[];//存放小蛇的每个身体部分
//蛇的构造函数
function Snake(width,height,direction){
//小蛇的每个部分的宽
this.width=width||20;
this.height=height||20;
//身体
this.body=[
{x:3,y:2,color:"red"},
{x:2,y:2,color:"orange"},
{x:1,y:2,color:"orange"}
];
this.direction=direction||"right";
}
//蛇的初始化
Snake.prototype.init=function(map){
remove()
//循环遍历创建div
for(var i=0;i<this.body.length;i++){
var obj=this.body[i];
//创建div
var div=document.createElement("div");
//把div加入到map地图中
map.appendChild(div);
//设置div的样式;
div.style.position="absolute";
div.style.width=this.width+"px";
div.style.height=this.height+"px";
div.style.left=obj.x*this.width+"px";
div.style.top=obj.y*this.height+"px";
div.style.backgroundColor=obj.color;
//把div加入到elements数组中--目的是删除
elements.push(div)
}
}
//蛇的移动
Snake.prototype.move=function(food,map){
//改变蛇身体位置
var i=this.body.length-1; //2
for(;i>0;i--){
this.body[i].x=this.body[i-1].x;
this.body[i].y=this.body[i-1].y;
}
//判断方向---改变小蛇的头的坐标位置
switch (this.direction){
case "right":
this.body[0].x+=1;
break;
case "left":
this.body[0].x-=1;
break;
case "top":
this.body[0].y-=1;
break;
case "bottom":
this.body[0].y+=1;
break;
}
//判断有没有吃到食物
//小蛇的头的坐标和食物位置
var headX=this.body[0].x*this.width;
var headY=this.body[0].y*this.height;
//食物的横纵坐标
var foodX=food.x;
var foodY=food.y;
if(headX==foodX&&headY==foodY){
//获取蛇的最后尾巴
var last=this.body[this.body.length-1];
//把最后的蛇尾复制一份
this.body.push({
x:last.x,
y:last.y,
color:last.color
})
//重新初始化食物
food.init(map);
}
}
//删除小蛇的私有函数
function remove(){
//获取数组
var i=elements.length-1;
for(;i>=0;i--){
//先从当前的子元素中找到该子元素的父级元素,然后再弄死这个子元素
var ele=elements[i];
//从map地图上删除这个子元素div
ele.parentNode.removeChild(ele);
elements.splice(i,1);
}
}
window.Snake=Snake;
}());
game.js
//游戏
(function(){
var that=null;
//游戏的构造函数
function Game(map){
this.food=new Food();
this.snake=new Snake();
this.map=map;//地图
that=this;
}
Game.prototype.init=function(){
//初始化游戏
//食物初始化
this.food.init(this.map);
this.snake.init(this.map);
this.runSnake(this.food,this.map)
this.bindKey();
}
Game.prototype.runSnake=function(food,map){
//自动的去移动
var timeId=setInterval(function(){
//此时的this是window
//蛇的移动
this.snake.move(food,map);
//初始化蛇
this.snake.init(map);
//横坐标最大值
var maxX=map.offsetWidth/this.snake.width;
//获取纵坐标的最大值
var maxY=map.offsetHeight/this.snake.height;
//蛇头的坐标
var headX=this.snake.body[0].x;
var headY=this.snake.body[0].y;
//判断横坐标
if(headX<0||headX>=maxX){
clearInterval(timeId)
alert("游戏结束")
}
//判断纵坐标
if(headY<0||headY>maxY){
clearInterval(timeId)
alert("游戏结束")
}
console.log(headX)
}.bind(that),150)
}
Game.prototype.bindKey=function(){
//获取用户的按键,改变小蛇的方向
document.addEventListener("keydown",function(e){
//获取案件的值
switch(e.keyCode){
case 37:
this.snake.direction="left";
break;
case 38:
this.snake.direction="top";
break;
case 39:
this.snake.direction="right";
break;
case 40:
this.snake.direction="bottom";
break;
}
}.bind(that),false)
}
window.Game=Game;
}());
//初始化游戏对象
var gm=new Game(document.querySelector(".map"));
gm.init()
此处插入一个关于匿名函数自调用(IIFE)的三种常见写法的小插曲。
第一种写法

第二种写法

第三种写法

特别注意:匿名函数表达式末尾必须加分号(;)。若遗漏,后续代码可能因语法解析错误引发难以排查的异常。
推荐采用第三种写法,因其语义清晰且易于阅读。
代码展示完毕后,接下来分析具体的实现思路。
第一步
创建游戏画布。
将画布宽度设置为800px,高度设置为600px。由于小蛇需要在画布内自由移动,需脱离标准文档流,因此给画布元素添加position:relative;样式,并设置背景色为灰色#ccc。
画布创建完成后,即可开始编写核心逻辑代码。
食物模块(Food.js)实现逻辑
Food.js 代码分析
首先定义贪吃蛇所需的“食物”对象,在food.js中创建一个自定义构造函数。
定义“食物Food”的x坐标、y坐标、宽度、高度及颜色属性。

利用逻辑或(||)运算符设置默认值:若左侧值为假,则取右侧值。因此,实例化对象时若未传参,将自动使用默认值。
在Food原型上定义init初始化方法。
首先创建一个div元素,并将该对象引用保存至div变量中。
将该div添加至地图容器中,并逐步设置其宽度、高度、背景颜色及绝对定位样式。
如何确定食物的具体位置?
将地图视为坐标系,用地图宽度除以食物宽度得到网格份数。x=1代表1个食物宽度的单位长度,x=3代表3个单位长度。
高度同理处理。
此处取随机数并乘以地图被切分的总份数,从而获得随机的 X 和 Y 坐标,再乘以宽度和高度即可得到不超出地图范围的随机坐标。例如:Math.random(0,5) 的结果不包括 5。

由于“食物”会被贪吃蛇吃掉,
必须创建一个方法来“销毁”食物,因此定义了一个remove函数。
同时,在上方创建了一个数组elements用于存放所有生成的“食物”div元素对象,便于后续删除。每次初始化食物时,将对象追加至elements数组。

遍历elements数组,通过每个div对象的父级元素,利用removeChild方法将其自身从DOM中移除
。
由于elements数组保存了引用,删除食物变得便捷。每次初始化前调用remove函数即可实现“消灭”旧食物并生成新食物的逻辑。
由于所有函数均封装在自调用的匿名函数中,内部的Food对象在外部无法直接访问。
如何解决?
通过调用window对象,将配置好的Food对象暴露给全局window,这样在其他模块中即可实例化Food。
小蛇模块(Snake.js)实现逻辑
Snake.js 代码分析
贪吃蛇身体的实现原理与食物类似。同样建立一个elements数组,用于存放小蛇移动时产生的旧“身体”对象,以便删除。由于均为局部变量,尽管两个数组同名,但不会发生冲突。
为小蛇设置宽、高,默认值与食物相同。同时设置方向属性direction,默认值为“right”(向右移动)。
游戏开始时,小蛇需具备初始长度。此处设置一个红色“脑袋”和两节身体,红色便于识别。
若需让小蛇变长,只需在body数组中追加对象即可体现“越吃越长”的效果。

小蛇基本属性配置完毕,下一步是初始化小蛇。
如前所述,elements数组用于存放“小蛇”的旧身体。初始化前需调用remove函数,遍历elements数组,采用与删除食物相同的方法清除旧蛇身。

执行删除操作后,即可专心进行初始化。
蛇身较长,如何确定每一节的具体位置?
利用上方定义的body数组,它存放了小蛇身体的所有部分。
遍历该数组,根据每个对象的属性创建新的div对象,统一设置其宽、高、left、top值。将创建好的对象再次存入elements数组,方便下一次删除。

定义初始化方法后,需考虑小蛇的移动实现。
作为贪吃蛇游戏,需与玩家互动,由玩家操控小蛇走向。
自定义构造函数中设置了direction属性,需利用此属性判断小蛇走向。
至于更改方向的操作,将在后续代码中实现。
定义move函数,传入两个参数:food(食物对象)和map(地图对象)。
为何需要传入这些参数?
虽然本Demo仅有一条小蛇,但此写法保留了同时运行多个游戏实例的可能性。
首先获取body数组长度存入i,然后倒序遍历i。根据i作为索引,从蛇尾巴向蛇脑袋遍历。想象贪吃蛇的移动过程:蛇身总是沿着脑袋走过的路径移动,无论绕直角或正方形,蛇身都会跟随。因此,每次移动只需控制蛇头,让蛇身各节获取前一节的坐标即可。
倒序遍历中,将第i节身体的x和y属性赋值给前一节身体。
蛇身坐标更新完成后,判断蛇头移动方向。由于只有4个固定方向,使用switch语句较为方便。
根据上、下、左、右不同情况,对头部的x和y进行增减操作。
贪吃蛇游戏的核心是吃食物,需实现该过程。
分别计算蛇头和食物的X和Y坐标。
进行判断:
当蛇头的x、y坐标与食物的x、y坐标完全相等时,
延长一节蛇身。此处复制最后一节蛇身体对象并追加至body数组。注意!
由于对象是引用类型,必须拆开赋值以避免引用共享问题。
最后,再次调用食物初始化方法,生成新的食物。

同样,将Snake对象暴露给window,供下方Game.js调用。

游戏控制模块(Game.js)实现逻辑
Game.js 代码分析
在Game.js开头定义变量that。
用于保存this的指向,供后续方法使用。
分别实例化“食物”对象和“贪吃蛇”对象。
传入地图对象并赋值。
、
属性设置完毕。
既然是游戏,需设定游戏规则。
当小蛇到达地图边界时,视为撞墙死亡,游戏结束。
此外,尚未实现小蛇的移动逻辑,接下来继续实现。
定义runSnake函数,传入food和map对象。
首先定义一个计时器,存入timeId变量。
调用蛇的move(移动)和init(初始化)函数。
小蛇成功移动后,判断其是否到达边界。
计算地图宽度最多能被蛇头宽度分为几份,高度同理。
获取蛇头自身的x和y坐标。
判断:若蛇头x<0,说明越过左边界;若超过maxX,说明越过右边界。
y坐标判断同理。
若越过边界,清除定时器,并执行弹窗提示。
注意,在定时器方法后使用了bind并传入了预先定义的that(即保存的this指向)。若不这样做,由于setInterval内部this指向window,会导致代码错误,无法找到相关方法和属性。

接下来实现键盘控制小蛇移动的功能。
根据keycode更改snake对象的direction属性。
此处的this指向不正确,指向的是触发事件的对象,无法直接调用snake对象。因此必须改变指向,在bind中传入(that)。
将Game对象暴露给window。

接着定义初始化游戏的函数。
分别调用food对象的init函数、小蛇的init函数,并调用runSnake函数开启定时器使小蛇运行。
最后绑定keydown事件。

最后一步
实例化一个Game对象。
调用gm的init方法,贪吃蛇小Demo即实现。

效果展示

更多关于JavaScript相关内容感兴趣的读者可查看本站专题:《JavaScript数学运算用法总结》、《JavaScript数据结构与算法技巧总结》、《JavaScript数组操作技巧总结》、《JavaScript排序算法总结》、《JavaScript遍历算法与技巧总结》、《JavaScript查找算法技巧总结》及《JavaScript错误与调试技巧总结》
希望本文所述对大家JavaScript程序设计有所帮助。
AOS.js 动画不触发怎么办?排查 DOM 加载与移动端兼容问题
2026-11-05 17:02:26
sigma.js 怎么实现节点点击、拖拽与悬停提示交互
2026-11-05 15:03:27
Matter.js 与 PixiJS/Three.js 集成:分离物理计算与渲染
2026-11-05 13:43:48
主流JS弹幕库选型指南:Danmaku.js等库的功能与体积对比
2026-11-05 09:28:19
JavaScript点击事件监听与交互效果实例
2026-11-05 09:10:19
