匿名函数参数(window,undefined) 这是什么野路子?

142 阅读4分钟

今天看面向对象的应用做一个贪吃蛇小游戏,结构很简单,index.html , index.css ,index,js

端详了一下js代码,感觉有点奇怪 代码中若干个这样子的函数

(function (window, undefined) {
    // 模块内的代码
})(window, undefined);

1. 代码风格

为何若干个匿名函数?

2. 函数参数

为何匿名函数参数为undefined?

带着这两个疑问请教Google

第一个问题

这是 ES6 之前的老写法,是早期 JavaScript 模块化的常用方案,虽然现在有了 ES6 模块、CommonJS 等更现代的模块化方案,但这种方式仍然是一种有效的封装方式。

看一下这么写的优点

1. 安全性

  • 变量不会泄露到全局作用域

  • 防止外部代码意外修改内部变量

2. 模块化

  • 每个功能都是独立的模块

  • 通过 window 对象只暴露必要的接口

3. 可维护性

  • 代码结构清晰

  • 模块之间的依赖关系明确

4. 性能优化

  • 局部变量查找比全局变量快

  • 压缩代码时可以安全地压缩局部变量名

第二个问题

在 IIFE 中传入 undefined 参数是一个历史遗留的安全性考虑。

  1. undefined 的特殊性

在老版本的 JavaScript 中(ES5 之前),undefined 是可以被重写的:

undefined = "我被改变了"; // 可以重写全局的 undefined

  1. 使用参数形式的保护
(function (window, undefined) {
    // 这里的 undefined 一定是真正的 undefined
    // 因为没有传第二个参数,所以 undefined 参数的值就是 undefined
})(window);
  1. 为什么这样做是安全的

即使外部的 undefined 被修改了:

window.undefined = "被改变了";

(function (window, undefined) {
    console.log(undefined); // 仍然是真正的 undefined
})(window);
  • 函数参数如果没有传值,默认就是 undefined
  • 参数作用域是本地的,不受外部影响
  • 无法从外部修改函数内的参数值
  1. 额外好处

代码压缩:

// 原代码
(function (window, undefined) {
    // 使用很多次 undefined
})(window);

// 压缩后
(function(w,u){
    // u 代替 undefined,节省字节
})(window);

现代开发中

在现代 JavaScript 中(ES5 及以后),undefined 已经不可被重写了,所以这种写法主要是历史遗留和兼容性考虑。但它仍然提供了:

  • 更好的代码压缩机会

  • 更快的 undefined 值查找(局部变量比全局变量快)

  • 更好的代码可读性

源码附上

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>贪吃蛇</title>
    <link rel="stylesheet" href="css/index.css" />
  </head>
  <body>
    <header><span>游戏说明:←↑→↓控制方向,空格暂停</span><button id="btn">重新开始</button></header>
    <div class="map" id="map"></div>
    <script src="js/index.js"></script>
  </body>
</html>

index.css

* {
  margin: 0;
  padding: 0;
}
header{
    width: 100%;
    height: 100px;
    background-color: #fff;
    text-align: center;
}
header span{
    width: 800px;
    height: 100px;
    line-height: 100px;
}
header button{
    margin-left: 50px;
    padding: 10px 20px;
    border-radius: 10px;
    background-color: rgb(188, 228, 241);
    font-size: 16px;
    cursor: pointer;
}
.map {
  position: relative;
  width: 800px;
  height: 600px;
  margin: 0 auto;
  background-color: #eee;
}

index.js

// tool;
(function (window, undefined) {
  const Tools = {
    getRandom: (min, max) => {
      min = Math.ceil(min);
      max = Math.floor(max);
      return Math.floor(Math.random() * (max - min + 1)) + min;
    },
    getColor: () => {
      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) {
  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';
    this.elements = [];
  }
  const position = 'absolute';
  Food.prototype.render = function (map) {
    let ele = document.createElement('div');
    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.backgroundColor = this.color;
    ele.style.position = position;
    ele.style.top = this.y + 'px';
    ele.style.left = this.x + 'px';
    map.appendChild(ele);
    this.elements.push(ele);
  };
  Food.prototype.remove = function (map, i) {
    map.removeChild(this.elements[i]);
    this.elements.splice(i, 1);
  };
  window.Food = Food;
})(window, undefined);
// snake
(function (window, undefined) {
  function Snake(option) {
    option = option instanceof Object ? option : {};
    this.width = option.width || 20;
    this.height = option.height || 20;
    this.body = [
      {
        x: 2,
        y: 0,
        color: 'red',
      },
      {
        x: 1,
        y: 0,
        color: 'blue',
      },
      {
        x: 0,
        y: 0,
        color: 'blue',
      },
    ];
    this.direction = 'right';
    this.elements = [];
  }
  const position = 'absolute';
  Snake.prototype.render = function (map) {
    for (let i = 0, len = this.body.length; i < len; i++) {
      const piece = this.body[i];
      const ele = document.createElement('div');
      ele.style.width = this.width + 'px';
      ele.style.height = this.height + 'px';
      ele.style.left = this.width * piece.x + 'px';
      ele.style.top = this.height * piece.y + 'px';
      ele.style.position = position;
      ele.style.backgroundColor = piece.color;
      map.appendChild(ele);
      this.elements.push(ele);
    }
  };
  Snake.prototype.move = function () {
    for (let 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;
    }
    const head = this.body[0];
    switch (this.direction) {
      case 'right':
        head.x += 1;
        break;
      case 'left':
        head.x -= 1;
        break;
      case 'up':
        head.y -= 1;
        break;
      case 'down':
        head.y += 1;
        break;
    }
  };
  Snake.prototype.remove = function (map) {
    for (var i = this.elements.length - 1; i >= 0; i--) {
      map.removeChild(this.elements[i]);
    }
    this.elements = [];
  };
  window.Snake = Snake;
})(window, undefined);
// game
(function (window, undefined) {
  let that;
  function Game(map) {
    this.food = new Food();
    this.snake = new Snake();
    this.map = map;
    that = this;
  }
  Game.prototype.start = function () {
    const num = prompt('请输入食物数量(1~10)\n点击任意按钮开始游戏');
    const n = parseInt(Number(num));
    if (n === NaN || n === 0) {
      this.food.render(this.map);
    } else if (n > 10) {
      for (i = 0; i < 10; i++) {
        this.food.render(this.map);
      }
    } else {
      for (i = 0; i < n; i++) {
        this.food.render(this.map);
      }
    }
    this.snake.render(this.map);
    runSnake();
    bindKey();
  };
  let timer;
  function runSnake() {
    timer = setInterval(function () {
      that.snake.move();
      that.snake.remove(that.map);
      that.snake.render(that.map);
      const hX = that.snake.width * that.snake.body[0].x;
      const hY = that.snake.height * that.snake.body[0].y;
      for (let 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);
          const body = that.snake.body;
          const last = body[body.length - 1];
          that.snake.body.push({
            x: last.x,
            y: last.y,
            color: last.color,
          });
        }
      }
      const maxX = that.map.offsetWidth / that.snake.width;
      const maxY = that.map.offsetHeight / that.snake.height;
      const headX = that.snake.body[0].x;
      const headY = that.snake.body[0].y;
      if (headX < 0 || headX >= maxX || headY < 0 || headY >= maxY) {
        clearInterval(timer);
        alert('Game Over');
      }
    }, 150);
  }
  let isgoing = true;
  function bindKey() {
    document.onkeydown = function (e) {
      switch (e.keyCode) {
        case 38:
          that.snake.direction = 'up';
          break;
        case 40:
          that.snake.direction = 'down';
          break;
        case 37:
          that.snake.direction = 'left';
          break;
        case 39:
          that.snake.direction = 'right';
          break;
        case 32:
          if (isgoing) {
            isgoing = false;
            clearInterval(timer);
          } else {
            runSnake();
            isgoing = true;
          }
          break;
      }
    };
  }
  window.Game = Game;
})(window, undefined);
// main
(function () {

  const btn = document.getElementById('btn')
  btn.onclick=()=>{
    location.reload();
  }
  const map = document.getElementById('map');
  const game = new Game(map);
  game.start();
})();