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

简介: 大家好,这里是「 从零开始学 Web 系列教程 」,并在下列地址同步更新......github:https://github.com/Daotin/Web微信公众号:Web前端之巅博客园:http://www.cnblogs.com/lvonve/CSDN:https://blog.csdn.net/lvonve/在这里我会从 Web 前端零基础开始,一步步学习 Web 相关的知识点,期间也会分享一些好玩的项目。

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

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

fgx.png

一、复习

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

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();

fgx.png

Web前端之巅

目录
相关文章
|
18天前
|
数据采集 Web App开发 JavaScript
Puppeteer的高级用法:如何在Node.js中实现复杂的Web Scraping
随着互联网的发展,网页数据抓取已成为数据分析和市场调研的关键手段。Puppeteer是一款由Google开发的无头浏览器工具,可在Node.js环境中模拟用户行为,高效抓取网页数据。本文将介绍如何利用Puppeteer的高级功能,通过设置代理IP、User-Agent和Cookies等技术,实现复杂的Web Scraping任务,并提供示例代码,展示如何使用亿牛云的爬虫代理来提高爬虫的成功率。通过合理配置这些参数,开发者可以有效规避目标网站的反爬机制,提升数据抓取效率。
Puppeteer的高级用法:如何在Node.js中实现复杂的Web Scraping
|
8天前
|
前端开发 机器人 测试技术
【RF案例】Web自动化测试弹窗处理
在进行Web自动化测试时,常会遇到不同类型的弹窗,如ajax、iframe、新窗口及alert/Confirm等。这些弹窗可通过Selenium进行定位与处理。其中,ajax弹窗直接定位处理;iframe需先选中再操作;新窗口类似iframe处理;而alert/Confirm则需特殊方法应对。在Robot Framework中,需先定义并获取窗口后使用特定关键字处理。此外,还有部分div弹窗需在消失前快速定位。希望本文能帮助大家更好地处理各类弹窗。
18 6
【RF案例】Web自动化测试弹窗处理
|
9天前
|
前端开发 数据安全/隐私保护
【前端web入门第二天】03 表单-下拉菜单 文本域 label标签 按钮 【附注册信息综合案例】
本文档详细介绍了HTML表单的多种元素及其用法,包括下拉菜单(`&lt;select&gt;` 和 `&lt;option&gt;`)、文本域(`&lt;textarea&gt;`)、标签解释(`&lt;label&gt;`)、各类按钮(`&lt;button&gt;`)及表单重置功能、无语义布局标签(`&lt;div&gt;` 和 `&lt;span&gt;`)以及字符实体的应用。此外,还提供了一个完整的注册信息表单案例,涵盖个人信息、教育经历和工作经历等部分,展示了如何综合运用上述元素构建实用的表单。
【前端web入门第二天】03 表单-下拉菜单 文本域 label标签 按钮 【附注册信息综合案例】
|
8天前
|
前端开发
【前端web入门第五天】03 清除默认样式与外边距问题【附综合案例产品卡片与新闻列表】
本文档详细介绍了CSS中清除默认样式的方法,包括清除内外边距、列表项目符号等;探讨了外边距的合并与塌陷问题及其解决策略;讲解了行内元素垂直边距的处理技巧;并介绍了圆角与盒子阴影效果的实现方法。最后通过产品卡片和新闻列表两个综合案例,展示了所学知识的实际应用。
22 11
|
8天前
|
前端开发
前端web入门第四天】03 显示模式+综合案例热词与banner效果
本文档介绍了HTML中标签的三种显示模式:块级元素、行内元素与行内块元素,并详细解释了各自的特性和应用场景。块级元素独占一行,宽度默认为父级100%,可设置宽高;行内元素在同一行显示,尺寸由内容决定,设置宽高无效;行内块元素在同一行显示,尺寸由内容决定,可设置宽高。此外,还提供了两个综合案例,包括热词展示和banner效果实现,帮助读者更好地理解和应用这些显示模式。
|
11天前
|
JavaScript 前端开发
【前端web入门第一天】03 综合案例 个人简介与vue简介
该网页采用“从上到下,先整体再局部”的制作思路,逐步分析并编写代码实现个人简介页面。内容涵盖尤雨溪的背景、学习经历及主要成就,同时介绍其开发的Vue.js框架特点。代码结构清晰,注重细节处理,如使用快捷键提高效率,预留超链接位置等,确保最终效果符合预期。
|
17天前
|
JavaScript 前端开发
JavaScript基础知识-原型(prototype)
关于JavaScript基础知识中原型(prototype)概念的介绍。
24 1
|
20天前
|
Java 数据库连接 数据库
从零到精通:揭秘 Hibernate 构建持久层服务的全过程,你离数据持久化大师还有多远?
【8月更文挑战第31天】本文详细介绍了如何从零开始使用 Hibernate 构建一个持久层服务。首先,通过在 Maven 项目中添加必要的依赖,确保项目具备使用 Hibernate 的条件。接着,配置 `hibernate.cfg.xml` 文件以连接 MySQL 数据库,并设置了基本属性。然后定义了一个简单的 `User` 实体类及其映射关系。此外,还创建了一个 `HibernateUtil` 工具类来管理 `SessionFactory`。
28 0
|
20天前
|
Java 开发者 关系型数据库
JSF与AWS的神秘之旅:如何在云端部署JSF应用,让你的Web应用如虎添翼?
【8月更文挑战第31天】在云计算蓬勃发展的今天,AWS已成为企业级应用的首选平台。本文探讨了在AWS上部署JSF(JavaServer Faces)应用的方法,这是一种广泛使用的Java Web框架。通过了解并利用AWS的基础设施与服务,如EC2、RDS 和 S3,开发者能够高效地部署和管理JSF应用。文章还提供了具体的部署步骤示例,并讨论了使用AWS可能遇到的挑战及应对策略,帮助开发者更好地利用AWS的强大功能,提升Web应用开发效率。
42 0
|
20天前
|
API UED 开发者
如何在Uno Platform中轻松实现流畅动画效果——从基础到优化,全方位打造用户友好的动态交互体验!
【8月更文挑战第31天】在开发跨平台应用时,确保用户界面流畅且具吸引力至关重要。Uno Platform 作为多端统一的开发框架,不仅支持跨系统应用开发,还能通过优化实现流畅动画,增强用户体验。本文探讨了Uno Platform中实现流畅动画的多个方面,包括动画基础、性能优化、实践技巧及问题排查,帮助开发者掌握具体优化策略,提升应用质量与用户满意度。通过合理利用故事板、减少布局复杂性、使用硬件加速等技术,结合异步方法与预设缓存技巧,开发者能够创建美观且流畅的动画效果。
43 0