从零开始学 Web 之 JavaScript 高级(一)原型,贪吃蛇案例

大家好,这里是「 从零开始学 Web 系列教程 」,并在下列地址同步更新......

在这里我会从 Web 前端零基础开始,一步步学习 Web 相关的知识点,期间也会分享一些好玩的项目。现在就让我们一起进入 Web 前端学习的冒险之旅吧!

一、复习

实例对象和构造函数之间的关系:

1、实例对象是通过构造函数来创建的,创建的过程叫实例化。

2、如何判断一个对象是不是某种数据类型?

  • 通过构造器的方法。实例对象.constructor === 构造函数名字
  • (推荐使用)实例对象 instanceof 构造函数名字

二、原型

1、原型的引入

由来:构造函数的问题。如果一个构造函数中有一个匿名方法,那么每实例化一个对象,然后在对象调用这个方法的时候,由于每个对象的方法都是各自的,所以每次调用这个方法的时候都会在内存中开辟一块空间存储这个方法,这样就造成内存资源的浪费。

解决方法:定义一个函数代替匿名方法。

由这个思想,提出原型的概念。

原型的作用之一:共享数据,节省内存空间。

1.1、用原型对象添加构造函数中的方法

function Person(name, age) {
        this.name = name;
        this.age= age;
    }
Person.prototype.eat = function () {
  	console.log("haha");
};

var per1 = new Person("Daotin", 18);
var per2 = new Person("lvonve", 18);
console.log(per1);
console.log(Person);
console.log(per1.eat === per2.eat); // true

1、为 Person 构造函数添加 eat 方法,使得 Person 创建的实例对象调用的 eat 方法,共享内存空间。

2、实例对象 per 中有个属性 __proto__ 也是对象,叫原型,它不是标准的属性(IE8 不支持,谷歌和火狐支持)。

3、构造函数中有一个属性 prototype 也是对象,叫原型。它是标准属性,供开发人员使用。

4、per1.eat === per2.eat; 所以原型的作用之一:共享数据,节省内存空间。

2、案例:点击按钮改变div属性

(使用面向对象思想)

面向对象思想:按钮是一个对象,div 是一个对象,样式时一种属性

<body>
<input type="button" value="按钮" id="btn">
<div id="dv"></div>

<script src="common.js"></script>
<script>
    /*
    * 操作 Obj1 设置 Obj2 的属性
    * 属性列表在 json 里面
    * */
    function ChangeStyle(Obj1, handle, Obj2, json) {
        this.Obj1 = Obj1;
        this.Obj2 = Obj2;
        this.handle = handle;
        this.json = json;
    }

    ChangeStyle.prototype.init = function () {
        var that = this;
        that.Obj1[that.handle] = function () {
            for (var key in that.json) {
                that.Obj2.style[key] = that.json[key];
            }
        };
    };

    var json = {"width": "200px", "height": "200px", "backgroundColor": "red"};
    var cs = new ChangeStyle(my$("btn"), "onclick", my$("dv"), json);

    cs.init();
</script>
</body>

3、构造函数,实例对象,原型对象三者的关系

1、实例对象是由构造函数创建的;

2、构造函数中有个属性 prototype,指向原型对象;

3、原型对象中有一个构造器,指向构造函数;

4、实例对象中的下划线原型对象__proto__ 指向原型对象 prototype

5、原型对象中的方法可以被实例对象访问,虽然实例对象中没有这个方法,但是实例对象中 __proto__ 指向 prototype,所以所有的实例对象共享原型对象中的方法。

4、原型对象的简单语法

什么样的数据需要添加到原型对象呢?

需要数据共享的数据,不论是属性还是方法。

既然 prototype 是一个对象,那么需要添加的属性和方法就可以以对象的方法添加:

<script>
    function Person(name, age) {
        this.name = name;
        this.age= age;
    }

    Person.prototype = {
        // 手动修改构造器指向
        constructor: Person,
        sex: "man",
        eat: function () {
            console.log("eat");
        },
        study: function () {
            console.log("study");
        }
    };

    var per = new Person("lvovne", 18);
    console.log(per);
</script>

需要注意的是:这种写法需要手动修改构造器指向,原型对象中将无这个构造器。

5、原型对象中的方法相互访问

我们知道,实例对象中的方法是可以相互访问的,那么原型对象中的方法可以相互访问吗?

也是可以的。

6、原型中属性和方法的使用顺序

实例对象使用的属性和方法会先在实例中查找,找不到才会到__proto__ 指向的构造函数的原型对象 prototype 中找,找到了则使用,找不到则报错。

7、为内置对象添加原型方法

像正常为自定义构造函数添加原型方法一样。

8、把局部变量变成全局变量

把函数中的局部变量暴露给浏览器顶级对象 window,那么这个局部变量将变成 window 的一个属性,可以被整个浏览器所访问。

(function () {
    var num = 10;
    window.num = num;
})();
console.log(num);

9、把局部对象变成全局对象

<script>
	// 产生随机数对象
    (function () {
        function Random() {}
        Random.prototype.getRandom = function (min, max) { // 范围 min ~ max-1
            return Math.floor(Math.random()*(max-min) + min);
        };
        window.Random = Random;
    })();

    var rd = new Random();
    var num = rd.getRandom(0,5);

    console.log(num);
    
</script>

这里把自定义的产生随机数的 Random 对象暴露给顶级对象 window,那么 Random 从局部对象变成全局对象。

10、案例:随机小方块

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style>
        .map {
            width: 800px;
            height: 600px;
            background-color: #ccc;
            position: relative;
        }
    </style>
</head>
<body>
<div class="map"></div>

<script src="common.js"></script>
<script>

    // 获取地图对象
    var map = document.getElementsByClassName("map")[0];

    // 产生随机数对象
    (function () {
        // 产生随机数的构造函数
        function Random() {

        }
        // 在原型对象中添加方法
        Random.prototype.getRandom = function (min, max) { // 范围 min ~ max-1
            return Math.floor(Math.random()*(max-min) + min);
        };

        window.Random = Random;
    })();

    // 产生小方块对象
    (function () {
        // 食物的构造函数
        function Food(width, height, color) {
            this.width = width||20;
            this.height = height||20;
            this.color = color||"green";
            this.x = 0; // left属性
            this.y = 0; // top属性
            this.element = document.createElement("div"); // 食物实例对象
        }

        // 初始化小方块的显示效果及位置
        Food.prototype.init = function () {
            var div = this.element;

            div.style.position = "absolute";
            div.style.width = this.width + "px";
            div.style.height = this.height + "px";
            div.style.backgroundColor = this.color;
            map.appendChild(div);
            this.product(map);
        };

        // 产生小方块的随机位置
        Food.prototype.product = function () {
            var div = this.element;
            var x = new Random().getRandom(0, map.offsetWidth/this.width) * this.width;
            var y = new Random().getRandom(0, map.offsetHeight/this.height) * this.height;
            this.x = x;
            this.y = y;

            div.style.left = this.x + "px";
            div.style.top = this.y + "px";
        };

        window.Food = Food;

    })();

    var food = new Food(20,20,"red");
    food.init(map);

</script>
</body>
</html>

1、产生小方块对象也是个自调用函数,这里面有一个构造函数 Food,两个原型函数 init 和 product,其中构造函数中包括小方块的所有属性,比如小方块是个 div,小方块的宽高,颜色,left,top 的值等。

2、init 方法初始化小方块的宽高,颜色以及将 div 加入到地图 map 中。

3、product 方法是产生随机位置,并赋值给小方块的 left,top。

4、最后,在产生小方块对象的最后,将 Food 对象暴露给 window,这样在 Food 自调用函数的外面也可以产生小方块。

11、案例:贪吃蛇

这个案例按照面向对象的思想,我们把它分成四个模块。

第一,地图模块;

第二,食物模块,也就是上面小方块的模块;

第三,小蛇模块;

第四,整个游戏也可以封装成一个模块,这个模块是将上面三个模块再次封装起来。

11.1、地图模块

地图模块最简单了,就是一块 div,设置宽高,背景就可以了。

注意:由于食物和小蛇都要在背景之上移动,所以食物和小蛇是脱标的,所以地图需要设置 position: relative;

<style>
    .map {
        width: 800px;
        height: 600px;
        background-color: #ccc;
        position: relative;
    }
</style>

<div class="map"></div>

11.2、食物模块

首先,食物的主体是由一个 div 组成,另外还有宽高,背景颜色属性,在食物脱标之后还有left,top属性,所以为了创建一个食物对象,就需要一个食物的构造函数,这个构造函数要设置食物的属性就是上面提到的属性。

function Food(width, height, color) {
        this.width = width || 20;
        this.height = height || 20;
        this.color = color || "green";
        this.x = 0; // left属性
        this.y = 0; // top属性
        this.element = document.createElement("div"); // 食物实例对象
    }

别忘了将 Food 暴露给 window

window.Food = Food;

之后需要初始化食物的显示效果和位置。

1、由于经常使用 init 函数,所以将其写入原型对象。

2、每次在创建食物之前先删除之前的小方块,保证map中只有一个食物

3、我们需要在自调用函数中定义一个数组,用来保存食物,方便每次创建食物之前的删除和后来小蛇吃掉食物后的删除。

Food.prototype.init = function (map) {
        // 每次在创建小方块之前先删除之前的小方块,保证map中只有一个小方块
        remove();

        var div = this.element;
        map.appendChild(div);

        div.style.width = this.width + "px";
        div.style.height = this.height + "px";
        div.style.backgroundColor = this.color;
        div.style.borderRadius = "50%";
        div.style.position = "absolute";

        this.x = new Random().getRandom(0, map.offsetWidth / this.width) * this.width;
        this.y = new Random().getRandom(0, map.offsetHeight / this.height) * this.height;
        div.style.left = this.x + "px";
        div.style.top = this.y + "px";

        // 把div加到数组中
        elements.push(div);
    };

食物的删除函数。设置为私有函数,其实就是自调用函数中的一个函数,保证不被自调用函数外面使用。

function remove() {
        for (var i = 0; i < elements.length; i++) {
            elements[i].parentElement.removeChild(elements[i]);
            elements.splice(i, 1); // 清空数组,从i的位置删除1个元素
        }
    }

11.3、小蛇模块

小蛇模块也得现有构造函数。

1、direction是小蛇移动的方向;

2、beforeDirection 是小蛇在键盘点击上下左右移动之前移动的方法,作用是不让小蛇回头,比如小蛇正往右走,不能点击左按键让其往左走,这时只能上下和右走。

3、小蛇最初的身体是三个 div,所以每个 div 都是一个对象,有自己的宽高和背景颜色和坐标。,所以这里用一个数组保存小蛇的身体。

function Snack(width, height, direction) {
        // 小蛇每一块的宽高
        this.width = width || 20;
        this.height = height || 20;
        this.direction = direction || "right";
        this.beforeDirection = this.direction;
        // 小蛇组成身体的每个小方块
        this.body = [
            {x: 3, y: 2, color: "red"},
            {x: 2, y: 2, color: "orange"},
            {x: 1, y: 2, color: "orange"}
        ];
    }

还是需要暴露给 window

window.Snack = Snack;

小蛇的初始化函数就就是设置构造函数中的属性。由于有多个 div 组成,所以要循环设置。初始化之前也要先删除小蛇。

Snack.prototype.init = function (map) {
        // 显示小蛇之前删除小蛇
        remove();

        // 循环创建小蛇身体div
        for (var i = 0; i < this.body.length; i++) {
            var div = document.createElement("div");
            map.appendChild(div);

            div.style.width = this.width + "px";
            div.style.height = this.height + "px";
            div.style.borderRadius = "50%";
            div.style.position = "absolute";

            // 移动方向,移动的时候设置

            // 坐标位置
            var tempObj = this.body[i];
            div.style.left = tempObj.x * this.width + "px";
            div.style.top = tempObj.y * this.width + "px";
            div.style.backgroundColor = tempObj.color;

            // 将小蛇添加到数组
            elements.push(div);
        }
    };

接下来是小蛇移动的方法。

1、小蛇移动的方法分两步,第一步是身体的移动;第二步是头部的移动。

2、当小蛇头坐标和食物的坐标相同时,表示吃到食物,这个时候小蛇要增长,怎么增长呢?将小蛇的尾巴赋值一份添加到小蛇的尾部。

3、之后删除食物,并重新生成食物。

Snack.prototype.move = function (food, map) {
        var index = this.body.length - 1; // 小蛇身体的索引
        // 不考虑小蛇头部
        for (var i = index; i > 0; i--) { // i>0 而不是 i>=0
            this.body[i].x = this.body[i - 1].x;
            this.body[i].y = this.body[i - 1].y;
        }

        // 小蛇头部移动
        switch (this.direction) {
            case "right" :
                // if(this.beforeDirection !== "left") {
                this.body[0].x += 1;
                // }
                break;
            case "left" :
                this.body[0].x -= 1;
                break;
            case "up" :
                this.body[0].y -= 1;
                break;
            case "down" :
                this.body[0].y += 1;
                break;
            default:
                break;
        }

        // 小蛇移动的时候,当小蛇偷坐标和食物的坐标相同表示吃到食物
        var headX = this.body[0].x * this.width;
        var headY = this.body[0].y * this.height;

        // 吃到食物,将尾巴复制一份加到小蛇body最后
        if ((headX === food.x) && (headY === food.y)) {
            var last = this.body[this.body.length - 1];
            this.body.push({
                x: last.x,
                y: last.y,
                color: last.color
            });

            // 删除食物
            food.init(map);
        }
    };

11.4、游戏模块

首先创建游戏对象需要游戏构造函数,这个构造函数应该包含三个部分:食物,小蛇和地图。

这个 that 是为了以后进入定时器后的 this 是 window,而不是 Game 做的准备。

function Game(map) {
        this.food = new Food(20, 20, "purple");
        this.snack = new Snack(20, 20);
        this.map = map;
        that = this;
    }

然后是游戏初始化函数,初始化游戏的目的就是让游戏开始,用户可以开始玩游戏了。

这里面调用了两个函数:autoRun 和 changeDirection。

Game.prototype.init = function () {
    this.food.init(this.map);
    this.snack.init(this.map);

    this.autoRun();
    this.changeDirection();
};

autoRun 是小蛇自动跑起来函数。这个函数主要是让小蛇动起来,并且在碰到边界时结束游戏。

注意:这里有一个在函数后面使用 bind 函数:使用bind,那么 setInterval 方法中所有的 this 都将被bind 的参数 that 替换,而这个 that 就是 Game。

Game.prototype.autoRun = function () {
    var timeId = setInterval(function () {
        this.snack.move(this.food, this.map);
        this.snack.init(this.map);

        // 判断最大X,Y边界
        var maxX = this.map.offsetWidth / this.snack.width;
        var maxY = this.map.offsetHeight / this.snack.height;

        // X方向边界
        if ((this.snack.body[0].x < 0) || (this.snack.body[0].x >= maxX)) {
            // 撞墙了
            clearInterval(timeId);
            alert("Oops, Game Over!");
        }

        // Y方向边界
        if ((this.snack.body[0].y < 0) || (this.snack.body[0].y >= maxY)) {
            // 撞墙了
            clearInterval(timeId);
            alert("Oops, Game Over!");
        }

    }.bind(that), 150); // 使用bind,那么init方法中所有的this都将被bind的参数that替换
};

changeDirection 是监听按键,改变小蛇的走向。

这里面按键按下的事件是 onkeydown 事件。每个按键按下都会有一个对应的 keyCode 值,通过这个值就可以判断用户按下的是哪个键。

Game.prototype.changeDirection = function () {

    addAnyEventListener(document, "keydown", function (e) {

        // 每次按键之前保存按键方向
        this.snack.beforeDirection = this.snack.direction;

        switch (e.keyCode) {
            case 37: // 左
                this.snack.beforeDirection !== "right" ? this.snack.direction = "left" : this.snack.direction = "right";
                break;
            case 38: // 上
                this.snack.beforeDirection !== "down" ? this.snack.direction = "up" : this.snack.direction = "down";
                break;
            case 39: // 右
                this.snack.beforeDirection !== "left" ? this.snack.direction = "right" : this.snack.direction = "left";
                break;
            case 40: // 下
                this.snack.beforeDirection !== "up" ? this.snack.direction = "down" : this.snack.direction = "up";
                break;
            default:
                break;
        }
    }.bind(that));

};

11.5、开始游戏

最后,我们只需要两行代码就可以开启游戏。

var game = new Game(document.getElementsByClassName("map")[0]);
game.init();

Web前端之巅

posted @ 2018-06-29 17:25  Daotin  阅读(689)  评论(0编辑  收藏  举报