原生JavaScript面向对象编程-贪吃蛇游戏案例

468 阅读18分钟


游戏截图

目标

游戏的目的是用来体会JavaScript高级语法的使用,暂时不需要具备抽象对象的能力。 使用面向对象的方式分析问题,需要一个漫长的积累过程。

开发工具

VS Code

项目文件搭建

下面我们正式开始贪吃蛇游戏制作。

搭建项目文件目录

搭建页面

放一个容器盛放游戏场景div#map,并设置样式。在index.html中代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <link rel="stylesheet" href="css/index.css">
</head>
<body>
  <div class="map" id="map"></div>
</body>
</html>

在css文件夹中新建index.css文件并编辑,写入如下代码:

* {
  margin: 0;
  padding: 0;
}
.map {
  position: relative;
  width: 800px;
  height: 600px;
  background-color: lightgray;
}

在index.html中使用Alt + B快捷键用浏览器打开, 生成游戏背景图如下:

分析游戏需要创建的对象

看到的对象:

  • 食物(food)
  • 蛇(snake) 看不到的对象:
  • 游戏逻辑(Game)等

创建食物对象

添加工具方法对象。在js文件夹中添加tools.js并写入如下代码:

(function () {
  // 制作一个工具对象,内部添加多种工具的方法
  var Tools = {
    // 获取一个范围内部的随机整数
    getRandom: function (min, max) {
      min = Math.ceil(min);
      max = Math.floor(max);
      return Math.floor(Math.random() * (max - min + 1)) + min; //含最大值,含最小值
    },
    // 获取随机颜色的方法
    getColor: function () {
      // rgb(r,g,b)  三个色值的颜色可以随机获取 0-255 之间的数字
      // 获取三个色值
      var r = this.getRandom(0, 255);
      var g = this.getRandom(0, 255);
      var b = this.getRandom(0, 255);
      // 返回一个 颜色值
      return "rgb(" + r + "," + g + "," + b + ")";
    }
  };
  window.Tools = Tools;
})();

在js文件夹中添加foods.js文件并打开编辑,写入如下代码:
创建食物对象

function Food(option) {
  // 避免传入的参数数据类型不对,或者没有传参
  option = option instanceof Object ? option : {};
  // 传入的数据可能是类似数组等对象,所以需要进一步判断
  this.width = option.width || 20;
  this.height = option.height || 20;
  this.x = option.x || 0;
  this.y = option.y || 0;
  this.color = option.color || "green";
  // 增加一个属性,存储将来这个对象渲染出来的所有 div 元素
  this.elements = [];
}

在页面进行渲染

  // 渲染一个元素到页面之上,需要添加到原型对象的方法中
  Food.prototype.render = function (map) {
  // 创建一个新的 div 元素
  var ele = document.createElement("div");
  // 添加对应的样式
  ele.style.width = this.width + "px";
  ele.style.height = this.height + "px";
  ele.style.left = this.x + "px";
  ele.style.top = this.y + "px";
  ele.style.backgroundColor = this.color;
  // 让新元素添加到指定的父级中
  map.appendChild(ele);
  // 将新元素添加的 数组中,方便后期调用删除
  this.elements.push(ele);
  };

测试, 还是在foods.js中写入如下代码。

// 获取map元素
var map = document.getElementById("map");
// 测试
var food = new Food();
food.render(map);

然后在index.html中引入所有js文件,代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <link rel="stylesheet" href="css/index.css">
  
</head>
<body>
  <div class="map" id="map"></div>
  <script src="js/tools.js"></script>
  <script src="js/foods.js"></script>
</body>
</html>

Alt+B快捷键用默认浏览器打开,一个食物对象就渲染出来了。

实现食物对象位置随机变化

现在食物位置固定,想让它位置随机变动,需定义元素为绝对定位。在foods.js中定义全局变量var ps = "absolute";。 食物对象位置水平随机分析:使用随机函数,其left值在(0,n-1)* w进行随机,n是一行能放下的食物个数,w是食物对象的宽度。高度随机同理,相关代码如下:

  this.x = Tools.getRandom(0, map.clientWidth / this.width - 1) * this.width;
  this.y = Tools.getRandom(0, map.clientHeight / this.height - 1) * this.height;

于是,foods.js内容修改如下:

// 全局的变量
var ps = "absolute";
function Food(option) {
  // 避免传入的参数数据类型不对,或者没有传参
  option = option instanceof Object ? option : {};
  // 传入的数据可能是类似数组等对象,所以需要进一步判断
  this.width = option.width || 20;
  this.height = option.height || 20;
  this.x = option.x || 0;
  this.y = option.y || 0;
  this.color = option.color || "green";
  // 增加一个属性,存储将来这个对象渲染出来的所有 div 元素
  this.elements = [];
}
// 渲染一个元素到页面之上,需要添加到原型对象的方法中
Food.prototype.render = function (map) {
  // 创建一个新的 div 元素
  var ele = document.createElement("div");
  // 每次设置样式之前,都随机获取一个 x 和 y 的值
  this.x = Tools.getRandom(0, map.clientWidth / this.width - 1) * this.width;
  this.y = Tools.getRandom(0, map.clientHeight / this.height - 1) * this.height;
  // 添加对应的样式
  ele.style.width = this.width + "px";
  ele.style.height = this.height + "px";
  ele.style.left = this.x + "px";
  ele.style.top = this.y + "px";
  ele.style.backgroundColor = this.color;
  ele.style.position = ps;
  // 让新元素添加到指定的父级中
  map.appendChild(ele);
  // 将新元素添加的 数组中,方便后期调用删除
  this.elements.push(ele);
};
// 获取map元素
var map = document.getElementById("map");
// 测试
var food = new Food();
food.render(map);

这样就实现了食物每次位置随机的功能。

食物删除方法

食物被蛇吃掉会消失,并在另一个地方重新生成。在foods.js中加入以下代码:

  // 删除一个食物 div 元素
  Food.prototype.remove = function (map, i) {
    // 可以通过一些方法获取要被删除的食物的下标
    // 将元素 从 html结构中删除
    map.removeChild(this.elements[i]);
    // 将元素 从 数组中删除
    this.elements.splice(i, 1);
  };
  // 测试,两秒后删除食物
  setTimeout(function () {
    food.remove(map, 0)
  },2000)


重新生成食物只需要再次调用food.render(map);即可。

自调用函数关住作用域

现在Food对象、tools对象基本创建完成,但是它们包括其方法都是全局变量,这容易造成变量污染,不容易管理。所以我们需要用自调用函数方法来对它们进行再一次封装,也就是(function (){Food})();和(function (){tools})();使它们作用域从全局变成局部。同时设置全局变量来指向被封装起来的Food对象和tools对象。于是foods.js内容如下:

// 需要去缩小定义 构造函数的作用
// 匿名函数,自调用函数,IIFE,关住作用域
(function () {
  // 全局的变量
  var ps = "absolute";
  // 创建 食物 的构造函数
  function Food(option) {
    // 避免传入的参数数据类型不对,或者没有传参
    option = option instanceof Object ? option : {};
    // 传入的数据可能是类似数组等对象,所以需要进一步判断
    this.width = option.width || 20;
    this.height = option.height || 20;
    this.x = option.x || 0;
    this.y = option.y || 0;
    this.color = option.color || "green";
    // 增加一个属性,存储将来这个对象渲染出来的所有 div 元素
    this.elements = [];
  }
  // 渲染一个元素到页面之上,需要添加到原型对象的方法中
  Food.prototype.render = function (map) {
    // 创建一个新的 div 元素
    var ele = document.createElement("div");
    // 每次设置样式之前,都随机获取一个 x 和 y 的值
    this.x = Tools.getRandom(0, map.clientWidth / this.width - 1) * this.width;
    this.y = Tools.getRandom(0, map.clientHeight / this.height - 1) * this.height;
    // 添加对应的样式
    ele.style.width = this.width + "px";
    ele.style.height = this.height + "px";
    ele.style.left = this.x + "px";
    ele.style.top = this.y + "px";
    ele.style.backgroundColor = this.color;
    ele.style.position = ps;
    // 让新元素添加到指定的父级中
    map.appendChild(ele);
    // 将新元素添加的 数组中,方便后期调用删除
    this.elements.push(ele);
  };
  // 删除一个食物 div 元素
  Food.prototype.remove = function (map, i) {
    // 可以通过一些方法获取要被删除的食物的下标
    // 将元素 从 html结构中删除
    map.removeChild(this.elements[i]);
    // 将元素 从 数组中删除
    this.elements.splice(i, 1);
  };
  // 利用 window 对象暴露 Food 函数可以给外部使用
  window.Food = Food;
})();
// 需要想办法在外面调用到这个 Food 函数

tools.js内容如下:

(function () {
  // 制作一个工具对象,内部添加多种工具的方法
  var Tools = {
    // 获取一个范围内部的随机整数
    getRandom: function (min, max) {
      min = Math.ceil(min);
      max = Math.floor(max);
      return Math.floor(Math.random() * (max - min + 1)) + min; //含最大值,含最小值
    },
    // 获取随机颜色的方法
    getColor: function () {
      // rgb(r,g,b)  三个色值的颜色可以随机获取 0-255 之间的数字
      // 获取三个色值
      var r = this.getRandom(0, 255);
      var g = this.getRandom(0, 255);
      var b = this.getRandom(0, 255);
      // 返回一个 颜色值
      return "rgb(" + r + "," + g + "," + b + ")";
    }
  };
  window.Tools = Tools;
})();

创建蛇对象


分析需求:蛇出生一共有三节蛇节,两节身子和一节蛇头,出生时位置固定(地图左上角)。有运动方向,定时运动步长等属性。渲染方法,运动方法,吃食物方法等。吃完食物,蛇身增加需相应长度。
开始创建:
创建Snake的构造函数,并设置属性
width 蛇节的宽度默认20
height 蛇节的高度默认20
body 数组,蛇的头部和身体,第一个位置是蛇头
direction蛇运动的方向默认right可以是left/top/bottom
通过原型设置方法
render 随机创建一个蛇对象,并输出到map上
通过自调用函数,进行封装:通过window暴露Snake对象
在js文件夹下新建snake.js文件,内容如下。

// 使用自调用函数关住作用域
(function () {
  // 全局变量
  var ps = "absolute";
  // 创建 蛇 的构造函数
  function Snake(option) {
    // 避免传入的参数数据类型不对,或者没有传参
    option = option instanceof Object ? option : {};
    // 给对象添加属性
    // 设置蛇节的宽度和高度属性
    this.width = option.width || 20;
    this.height = option.height || 20;
    // 设置蛇身的数据
    this.body = [
      {x: 3,y: 2,color: "red"},
      {x: 2,y: 2,color: "blue"},
      {x: 1,y: 2,color: "blue"}
    ];
    // 设置蛇移动的方向,还可以设置为 left 、top 、bottom
    this.direction = "right";
    // 添加一个元素的数组,存储所有渲染的 div
    this.elements = [];
  }
  // 添加一个将元素渲染到页面上的方法
  Snake.prototype.render = function (map) {
    // 生成对应个数的 div 元素
    // 遍历数组
    for (var i = 0,len = this.body.length ; i < len ; i++) {
      // 根据数组的每一项的数据生成一个新的 div 元素
      var piece = this.body[i];
      // 创建新元素
      var ele = document.createElement("div");
      // 添加样式
      ele.style.width = this.width + "px";
      ele.style.height = this.height + "px";
      ele.style.left = piece.x * this.width + "px";
      ele.style.top = piece.y * this.height + "px";
      ele.style.position = ps;
      ele.style.backgroundColor = piece.color;
      // 渲染到指定的父级内部
      map.appendChild(ele);
      // 将添加的新元素存在数组里
      this.elements.push(ele);
    }    
  };
  // 通过 window 暴露构造函数
  window.Snake = Snake;
})();

//测试 
var map = document.getElementById("map");
var snake = new Snake();
snake.render(map);

然后在index.html中进行引用添加:<script src="js/snake.js"></script>运行得到渲染的蛇对象。

创建游戏对象

创建Game的构造函数,并设置属性
food
snake
map
通过原型设置方法
start 开始游戏(绘制所有游戏对象,渲染食物对象和蛇对象)
通过自调用函数,进行封装,通过window暴露Game对象
在js文件夹中新建文件game.js,并在index.html中进行引用添加:<script src="js/game.js"></script>
注释掉原来测试的代码。game.js内容如下:

// 自调用函数封闭作用域
(function () {
  // 定义一个全局变量,存储 this 
  var that;
  // 创建一个 游戏 的构造函数
  function Game(map) {
    // 设置三个属性,存储 食物、 蛇、地图
    this.food = new Food();
    this.snake = new Snake();
    this.map = map;
    that = this;
  }
  // 添加一个游戏开始的方法,方法内初始化蛇和食物
  Game.prototype.start = function () {
    // 1.添加蛇和食物到 地图上
    this.food.render(this.map);
    this.food.render(this.map);
    this.food.render(this.map);
    this.snake.render(this.map);
  }
  // 将构造函数通过 window 暴露
  window.Game = Game;
})();
//测试
var map = document.getElementById("map");
game = new Game();
game.start();

在index.html中Alt+B快捷键使用默认浏览器运行。

给蛇添加运动方法

分析:蛇运动一格的时候,蛇身每节都会运动到上一个部分的位置。所以我们可以先从后往前处理蛇身的运动,然后再处理蛇头的运动。
在snake.js的自调用函数中添加如下运动方法:

  // 添加 蛇 运动的方法
  Snake.prototype.move = function () {
    // 蛇身的每一节都要变成上一节的位置
    // 循环需要从最后一项开始,为了避免前面的数据提前发生变化
    for (var i = this.body.length - 1 ; i > 0 ; i--) {
      this.body[i].x = this.body[i-1].x;
      this.body[i].y = this.body[i-1].y;
    }
    // 存储一下蛇头的数据
    var head = this.body[0];
    // 蛇头要根据方向发生位置变化
    switch (this.direction) {
      case "right":
        head.x += 1;
        break;
      case "left":
        head.x -= 1;
        break;
      case "top":
        head.y -= 1;
        break;
      case "bottom":
        head.y += 1;
    }

使用时调用snake.move()再进行渲染snake.render(),即可实现运动。但是这样,我们渲染了两次蛇对象,相当于第二条蛇压盖在了第一条蛇的身上,上一次渲染的结果并没有消失,蛇看上去多了节尾巴,所以我们要删除上一次的渲染。 在snake.js中添加如下删除方法:

// 删除上一次渲染的蛇的所有div元素
  Snake.prototype.remove = function (map) {
    // 遍历数组删除所有元素
    // 将元素从html结构中删掉
    for (var i = this.elements.length - 1 ; i >= 0  ; i--) {
      map.removeChild(this.elements[i]);
    }
    // 数组也需要进行清空
    this.elements = [];
  }

在game.js中重写start方法:

// 添加一个游戏开始的方法,方法内初始化蛇和食物
  Game.prototype.start = function () {
    // 1.添加蛇和食物到 地图上
    this.food.render(this.map);
    this.food.render(this.map);
    this.food.render(this.map);
    this.snake.render(this.map);
    this.snake.move();
    this.snake.remove(this.map);
    this.snake.render(this.map);
  }

保存,在index.html中Alt+B运行,就会发现蛇向右移动了一格。 相当于snake调用一次move()、remove()、render()方法三连,就能让蛇对象移动一格。

游戏逻辑书写

分析:将蛇和食物渲染到map上后,需要,
1.让蛇自动运动起来 runSnake();而且运动过程中,吃到食物会增加蛇身,撞到墙壁会游戏结束。
2 通过上下左右箭头控制蛇的运动方向 bindKey();
game.js内容修改如下:

// 自调用函数封闭作用域
(function () {
  // 定义一个全局变量,存储 this 
  var that;
  // 创建一个 游戏 的构造函数
  function Game(map) {
    // 设置三个属性,存储 食物、 蛇、地图
    this.food = new Food();
    this.snake = new Snake();
    this.map = map;
    that = this;
  }
  // 添加一个游戏开始的方法,方法内初始化蛇和食物
  Game.prototype.start = function () {
    // 1.添加蛇和食物到 地图上
    this.food.render(this.map);
    this.food.render(this.map);
    this.food.render(this.map);
    this.snake.render(this.map);
    // 2.让游戏逻辑开始
    // 2.1 让蛇自动运动起来
    runSnake();
    // 2.2 通过上下左右箭头控制蛇的运动方向
    bindKey(); 

  }
  // 封装一个私有函数,控制上下左右按键更改的方向
  function bindKey() {
    // 给文档绑定键盘按下事件
    document.onkeydown = function (e) {
      // console.log(e.keyCode);
      // 键盘的编码
      // 37 -- left
      // 38 -- top
      // 39 -- right
      // 40 -- bottom
      switch (e.keyCode) {
        case 37:
          that.snake.direction = "left";
          break;
        case 38:
          that.snake.direction = "top";
          break;
        case 39:
          that.snake.direction = "right";
          break;
        case 40:
          that.snake.direction = "bottom";
          break;
      }
    };
  }
  // 封装一个私有函数,这个函数只能在模块内部进行调用
  function runSnake() {
    // 开启一个定时器,让蛇连续运动起来
    var timer = setInterval(function () {
      // 定时器函数内部的 this 指向的是 window
      // 让蛇运动起来    
      that.snake.move();
      // 删掉上一次的蛇
      that.snake.remove(that.map);
      // 渲染新位置的蛇
      that.snake.render(that.map);
      // 记录一下最大的位置
      var maxX = that.map.offsetWidth / that.snake.width;
      var maxY = that.map.offsetHeight / that.snake.height;
      // 找到当前蛇头的位置
      var headX = that.snake.body[0].x;
      var headY = that.snake.body[0].y;
      // 每一次蛇走到新的位置,都要判断一下是否吃到食物了
      // 2.3 判断蛇头与食物是否碰撞,吃掉食物 ,让自己增加一节
      // 记录一下食物的坐标
      // var foodX = that.food.x;
      // var foodY = that.food.y;
      // 获取蛇头的具体坐标位置,px值
      var hX = headX * that.snake.width;
      var hY = headY * that.snake.height;
      // 判断
      // 将食物的数组中每一个都要进行对比,谁被吃掉,删除自己,渲染一个新的元素
      for (var i = 0 ; i < that.food.elements.length ; i++) {
        if (that.food.elements[i].offsetLeft === hX && that.food.elements[i].offsetTop === hY) {
          // 吃到了食物
          // 让食物删除,然后渲染一个新的食物
          that.food.remove(that.map,i);
          that.food.render(that.map);
          // 添加一个新的蛇节
          var last = that.snake.body[that.snake.body.length - 1];
          that.snake.body.push({
            x: last.x,
            y: last.y,
            color: last.color
          });
        }
      }
      // 每移动一次,都要判断是否出了地图,游戏是否结束
      // 2.4 判断是否超出地图范围,结束游戏      
      // 进行判断
      if (headX < 0 || headX >= maxX || headY < 0 || headY >= maxY) {
        // 停止定时器
        clearInterval(timer);
        // 弹出提醒
        alert("Game over");
      }
    }, 150);
  }
  // 将构造函数通过 window 暴露
  window.Game = Game;
})();
//测试
var map = document.getElementById("map");
game = new Game(map);
game.start();

至此,游戏功能基本制作完成。下面就是一些无功能实现的优化措施。

代码优化

全部使用自调用函数

我们需要将game.js中的测试部分代码单独封装到一个js文件中,并且以自调用函数的方式使用。于是,我们在js文件夹中新建main.js文件,内容如下:

// 使用自调用函数关住作用域
(function () {
  var map = document.getElementById("map");
  var game = new Game(map);
  game.start();
})();

在index.html中进行引用添加:<script src="js/main.js"></script>
这样,每个js文件都是自调用函数,各自有各自的功能和作用域。

自调用函数的参数

在自调用函数中,我们为了将构造函数通过 window 暴露,使用了window变量。解释器在解析时,每次都会跳出作用域去寻找全局变量window,效率较低,而且在压缩代码时没办法像其他变量一样能被压缩,于是我们需要给自调用函数传入参数window。因为在ie8中undefined可以被重写赋予新值的问题,我们还要传入参数undefined,在自调用匿名函数的作用域内,确保 undefined 是真的未定义。
于是,所有自调用函数都变成了如下形式:

(function (window,undefined) {
})(window,undefined);

js代码压缩

在js文件夹中新建一个index.js,里面内容是按顺序将tools.js、foods.js、snake.js、game.js、main.js内容全部集中在一起:

// 将所有的模块代码要按照一定得顺序引入
// ======================Tools============================
;(function (window,undefined) {
  // 制作一个工具对象,内部添加多种工具的方法
  var Tools = {
    // 获取一个范围内部的随机整数
    getRandom: function (min, max) {
      min = Math.ceil(min);
      max = Math.floor(max);
      return Math.floor(Math.random() * (max - min + 1)) + min; //含最大值,含最小值
    },
    // 获取随机颜色的方法
    getColor: function () {
      // rgb(r,g,b)  三个色值的颜色可以随机获取 0-255 之间的数字
      // 获取三个色值
      var r = this.getRandom(0, 255);
      var g = this.getRandom(0, 255);
      var b = this.getRandom(0, 255);
      // 返回一个 颜色值
      return "rgb(" + r + "," + g + "," + b + ")";
    }
  };
  window.Tools = Tools;
})(window,undefined)
// ==================Food===========================
;(function (window,undefined) {
  // 全局的变量
  var ps = "absolute";
  // 创建 食物 的构造函数
  function Food(option) {
    // 避免传入的参数数据类型不对,或者没有传参
    option = option instanceof Object ? option : {};
    // 传入的数据可能是类似数组等对象,所以需要进一步判断
    this.width = option.width || 20;
    this.height = option.height || 20;
    this.x = option.x || 0;
    this.y = option.y || 0;
    this.color = option.color || "green";
    // 增加一个属性,存储将来这个对象渲染出来的所有 div 元素
    this.elements = [];
  }
  // 渲染一个元素到页面之上,需要添加到原型对象的方法中
  Food.prototype.render = function (map) {
    // 创建一个新的 div 元素
    var ele = document.createElement("div");
    // 每次设置样式之前,都随机获取一个 x 和 y 的值
    this.x = Tools.getRandom(0, map.clientWidth / this.width - 1) * this.width;
    this.y = Tools.getRandom(0, map.clientHeight / this.height - 1) * this.height;
    // 添加对应的样式
    ele.style.width = this.width + "px";
    ele.style.height = this.height + "px";
    ele.style.left = this.x + "px";
    ele.style.top = this.y + "px";
    ele.style.backgroundColor = this.color;
    ele.style.position = ps;
    // 让新元素添加到指定的父级中
    map.appendChild(ele);
    // 将新元素添加的 数组中,方便后期调用删除
    this.elements.push(ele);
  };
  // 删除一个食物 div 元素
  Food.prototype.remove = function (map, i) {
    // 可以通过一些方法获取要被删除的食物的下标
    // 将元素 从 html结构中删除
    map.removeChild(this.elements[i]);
    // 将元素 从 数组中删除
    this.elements.splice(i, 1);
  };
  // 利用 window 对象暴露 Food 函数可以给外部使用
  window.Food = Food;
})(window,undefined)
// ===================Snake=============================
;(function (window,undefined) {
  // 全局变量
  var ps = "absolute";
  // 创建 蛇 的构造函数
  function Snake(option) {
    // 避免传入的参数数据类型不对,或者没有传参
    option = option instanceof Object ? option : {};
    // 给对象添加属性
    // 设置蛇节的宽度和高度属性
    this.width = option.width || 20;
    this.height = option.height || 20;
    // 设置蛇身的数据
    this.body = [
      {x: 3,y: 2,color: "red"},
      {x: 2,y: 2,color: "blue"},
      {x: 1,y: 2,color: "blue"}
    ];
    // 设置蛇移动的方向,还可以设置为 left 、top 、bottom
    this.direction = "right";
    // 添加一个元素的数组,存储所有渲染的 div
    this.elements = [];
  }
  // 添加一个将元素渲染到页面上的方法
  Snake.prototype.render = function (map) {
    // 生成对应个数的 div 元素
    // 遍历数组
    for (var i = 0,len = this.body.length ; i < len ; i++) {
      // 根据数组的每一项的数据生成一个新的 div 元素
      var piece = this.body[i];
      // 创建新元素
      var ele = document.createElement("div");
      // 添加样式
      ele.style.width = this.width + "px";
      ele.style.height = this.height + "px";
      ele.style.left = piece.x * this.width + "px";
      ele.style.top = piece.y * this.height + "px";
      ele.style.position = ps;
      ele.style.backgroundColor = piece.color;
      // 渲染到指定的父级内部
      map.appendChild(ele);
      // 将添加的新元素存在数组里
      this.elements.push(ele);
    }    
  };
  // 添加 蛇 运动的方法
  Snake.prototype.move = function () {
    // 蛇身的每一节都要变成上一节的位置
    // 循环需要从最后一项开始,为了避免前面的数据提前发生变化
    for (var i = this.body.length - 1 ; i > 0 ; i--) {
      this.body[i].x = this.body[i-1].x;
      this.body[i].y = this.body[i-1].y;
    }
    // 存储一下蛇头的数据
    var head = this.body[0];
    // 蛇头要根据方向发生位置变化
    switch (this.direction) {
      case "right":
        head.x += 1;
        break;
      case "left":
        head.x -= 1;
        break;
      case "top":
        head.y -= 1;
        break;
      case "bottom":
        head.y += 1;
    }
  };
  // 删除上一次渲染的蛇的所有div元素
  Snake.prototype.remove = function (map) {
    // 遍历数组删除所有元素
    // 将元素从html结构中删掉
    for (var i = this.elements.length - 1 ; i >= 0  ; i--) {
      map.removeChild(this.elements[i]);
    }
    // 数组也需要进行清空
    this.elements = [];
  }
  // 通过 window 暴露构造函数
  window.Snake = Snake;
})(window,undefined)
// ========================Game===================
;(function (window,undefined) {
  // 定义一个全局变量,存储 this 
  var that;
  // 创建一个 游戏 的构造函数
  function Game(map) {
    // 设置三个属性,存储 食物、 蛇、地图
    this.food = new Food();
    this.snake = new Snake();
    this.map = map;
    that = this;
  }
  // 添加一个游戏开始的方法,方法内初始化蛇和食物
  Game.prototype.start = function () {
    // 1.添加蛇和食物到 地图上
    this.food.render(this.map);
    this.food.render(this.map);
    this.food.render(this.map);
    this.snake.render(this.map);
    // 2.让游戏逻辑开始
    // 2.1 让蛇自动运动起来
    runSnake();
    // 2.2 通过上下左右箭头控制蛇的运动方向
    bindKey(); 

  }
  // 封装一个私有函数,控制上下左右按键更改的方向
  function bindKey() {
    // 给文档绑定键盘按下事件
    document.onkeydown = function (e) {
      // console.log(e.keyCode);
      // 键盘的编码
      // 37 -- left
      // 38 -- top
      // 39 -- right
      // 40 -- bottom
      switch (e.keyCode) {
        case 37:
          that.snake.direction = "left";
          break;
        case 38:
          that.snake.direction = "top";
          break;
        case 39:
          that.snake.direction = "right";
          break;
        case 40:
          that.snake.direction = "bottom";
          break;
      }
    };
  }
  // 封装一个私有函数,这个函数只能在模块内部进行调用
  function runSnake() {
    // 开启一个定时器,让蛇连续运动起来
    var timer = setInterval(function () {
      // 定时器函数内部的 this 指向的是 window
      // 让蛇运动起来    
      that.snake.move();
      // 删掉上一次的蛇
      that.snake.remove(that.map);
      // 渲染新位置的蛇
      that.snake.render(that.map);
      // 记录一下最大的位置
      var maxX = that.map.offsetWidth / that.snake.width;
      var maxY = that.map.offsetHeight / that.snake.height;
      // 找到当前蛇头的位置
      var headX = that.snake.body[0].x;
      var headY = that.snake.body[0].y;
      // 每一次蛇走到新的位置,都要判断一下是否吃到食物了
      // 2.3 判断蛇头与食物是否碰撞,吃掉食物 ,让自己增加一节
      // 记录一下食物的坐标
      // var foodX = that.food.x;
      // var foodY = that.food.y;
      // 获取蛇头的具体坐标位置,px值
      var hX = headX * that.snake.width;
      var hY = headY * that.snake.height;
      // 判断
      // 将食物的数组中每一个都要进行对比,谁被吃掉,删除自己,渲染一个新的元素
      for (var i = 0 ; i < that.food.elements.length ; i++) {
        if (that.food.elements[i].offsetLeft === hX && that.food.elements[i].offsetTop === hY) {
          // 吃到了食物
          // 让食物删除,然后渲染一个新的食物
          that.food.remove(that.map,i);
          that.food.render(that.map);
          // 添加一个新的蛇节
          var last = that.snake.body[that.snake.body.length - 1];
          that.snake.body.push({
            x: last.x,
            y: last.y,
            color: last.color
          });
        }
      }
      // 每移动一次,都要判断是否出了地图,游戏是否结束
      // 2.4 判断是否超出地图范围,结束游戏      
      // 进行判断
      if (headX < 0 || headX >= maxX || headY < 0 || headY >= maxY) {
        // 停止定时器
        clearInterval(timer);
        // 弹出提醒
        alert("Game over");
      }
    }, 150);
  }
  // 将构造函数通过 window 暴露
  window.Game = Game;
})(window,undefined)
// ========================== Main =========================
;(function (window,undefined) {
  var map = document.getElementById("map");
  var game = new Game(map);
  game.start();
})(window,undefined)

在搜索引擎中搜索“代码压缩”,即可找到相关的在线压缩工具,可以对代码进行去除注释,空格等代码来压缩,还可以进行标识符混淆。压缩后传输速度会有所提升。我们将index.js中的内容进行压缩。
在js文件夹中新建一个index.min.js文件,内容就是压缩后的代码:

(function(){var a={getRandom:function(c,b){c=Math.ceil(c);b=Math.floor(b);return Math.floor(Math.random()*(b-c+1))+c},getColor:function(){var e=this.getRandom(0,255);var d=this.getRandom(0,255);var c=this.getRandom(0,255);return"rgb("+e+","+d+","+c+")"}};window.Tools=a})();(function(){var b="absolute";function a(c){c=c instanceof Object?c:{};this.width=c.width||20;this.height=c.height||20;this.x=c.x||0;this.y=c.y||0;this.color=c.color||"green";this.elements=[]}a.prototype.render=function(d){var c=document.createElement("div");this.x=Tools.getRandom(0,d.clientWidth/this.width-1)*this.width;this.y=Tools.getRandom(0,d.clientHeight/this.height-1)*this.height;c.style.width=this.width+"px";c.style.height=this.height+"px";c.style.left=this.x+"px";c.style.top=this.y+"px";c.style.backgroundColor=this.color;c.style.position=b;d.appendChild(c);this.elements.push(c)};a.prototype.remove=function(d,c){d.removeChild(this.elements[c]);this.elements.splice(c,1)};window.Food=a})();(function(){var b="absolute";function a(c){c=c instanceof Object?c:{};this.width=c.width||20;this.height=c.height||20;this.body=[{x:3,y:2,color:"red"},{x:2,y:2,color:"blue"},{x:1,y:2,color:"blue"}];this.direction="right";this.elements=[]}a.prototype.render=function(g){for(var d=0,c=this.body.length;d<c;d++){var e=this.body[d];var f=document.createElement("div");f.style.width=this.width+"px";f.style.height=this.height+"px";f.style.left=e.x*this.width+"px";f.style.top=e.y*this.height+"px";f.style.position=b;f.style.backgroundColor=e.color;g.appendChild(f);this.elements.push(f)}};a.prototype.move=function(){for(var d=this.body.length-1;d>0;d--){this.body[d].x=this.body[d-1].x;this.body[d].y=this.body[d-1].y}var c=this.body[0];switch(this.direction){case"right":c.x+=1;break;case"left":c.x-=1;break;case"top":c.y-=1;break;case"bottom":c.y+=1}};a.prototype.remove=function(d){for(var c=this.elements.length-1;c>=0;c--){d.removeChild(this.elements[c])}this.elements=[]};window.Snake=a})();(function(){var c;function d(e){this.food=new Food();this.snake=new Snake();this.map=e;c=this}d.prototype.start=function(){this.food.render(this.map);this.food.render(this.map);this.food.render(this.map);this.snake.render(this.map);b();a()};function a(){document.onkeydown=function(f){switch(f.keyCode){case 37:c.snake.direction="left";break;case 38:c.snake.direction="top";break;case 39:c.snake.direction="right";break;case 40:c.snake.direction="bottom";break}}}function b(){var e=setInterval(function(){c.snake.move();c.snake.remove(c.map);c.snake.render(c.map);var l=c.map.offsetWidth/c.snake.width;var j=c.map.offsetHeight/c.snake.height;var f=c.snake.body[0].x;var n=c.snake.body[0].y;var m=f*c.snake.width;var k=n*c.snake.height;for(var g=0;g<c.food.elements.length;g++){if(c.food.elements[g].offsetLeft===m&&c.food.elements[g].offsetTop===k){c.food.remove(c.map,g);c.food.render(c.map);var h=c.snake.body[c.snake.body.length-1];c.snake.body.push({x:h.x,y:h.y,color:h.color})}}if(f<0||f>=l||n<0||n>=j){clearInterval(e);alert("Game over")}},150)}window.Game=d})();(function(){var b=document.getElementById("map");var a=new Game(b);a.start()})();

后期会有打包工具压缩,而不用手动压缩了。我们现在是为了了解这个压缩过程,培养这个意识。
index.html中就只要引入index.min.js一个js文件即可,其内容如下:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <link rel="stylesheet" href="css/index.css">
</head>
<body>
  <div class="map" id="map"></div>
  <!-- 引入多个 js 文件 -->
  <!-- <script src="js/tools.js"></script>
  <script src="js/food.js"></script>
  <script src="js/snake.js"></script>
  <script src="js/game.js"></script>
  <script src="js/main.js"></script> -->

  <!-- 为了进行性能优化,需要减少浏览器发送 HTTP 请求的次数 -->
  <!-- <script src="js/index.js"></script> -->
  <script src="js/index.min.js"></script>
</body>
</html>

使用Alt+B快捷键运行,得到一样的游戏效果。
啊啊啊,我技术好菜!!!