從零開始學 Web 之 JavaScript 高級(一)原型,貪吃蛇案例

你們好,這裏是「 從零開始學 Web 系列教程 」,並在下列地址同步更新......javascript

在這裏我會從 Web 前端零基礎開始,一步步學習 Web 相關的知識點,期間也會分享一些好玩的項目。如今就讓咱們一塊兒進入 Web 前端學習的冒險之旅吧!html

1、複習

實例對象和構造函數之間的關係:前端

一、實例對象是經過構造函數來建立的,建立的過程叫實例化。java

二、如何判斷一個對象是否是某種數據類型?git

  • 經過構造器的方法。實例對象.constructor === 構造函數名字
  • (推薦使用)實例對象 instanceof 構造函數名字

2、原型

一、原型的引入

由來:構造函數的問題。若是一個構造函數中有一個匿名方法,那麼每實例化一個對象,而後在對象調用這個方法的時候,因爲每一個對象的方法都是各自的,因此每次調用這個方法的時候都會在內存中開闢一塊空間存儲這個方法,這樣就形成內存資源的浪費。github

解決方法:定義一個函數代替匿名方法。json

由這個思想,提出原型的概念。數組

原型的做用之一:共享數據,節省內存空間。瀏覽器

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

一、爲 Person 構造函數添加 eat 方法,使得 Person 建立的實例對象調用的 eat 方法,共享內存空間。微信

二、實例對象 per 中有個屬性 __proto__ 也是對象,叫原型,它不是標準的屬性(IE8 不支持,谷歌和火狐支持)。

三、構造函數中有一個屬性 prototype 也是對象,叫原型。它是標準屬性,供開發人員使用。

四、per1.eat === per2.eat; 因此原型的做用之一:共享數據,節省內存空間。

二、案例:點擊按鈕改變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>

三、構造函數,實例對象,原型對象三者的關係

一、實例對象是由構造函數建立的;

二、構造函數中有個屬性prototype,指向原型對象;

三、原型對象中有一個構造器,指向構造函數;

四、實例對象中的下劃線原型對象__proto__ 指向原型對象 prototype

五、原型對象中的方法能夠被實例對象訪問,雖然實例對象中沒有這個方法,可是實例對象中 __proto__ 指向 prototype,因此全部的實例對象共享原型對象中的方法。

四、原型對象的簡單語法

什麼樣的數據須要添加到原型對象呢?

須要數據共享的數據,不管是屬性仍是方法。

既然 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>

須要注意的是:這種寫法須要手動修改構造器指向,原型對象中將無這個構造器。

五、原型對象中的方法相互訪問

咱們知道,實例對象中的方法是能夠相互訪問的,那麼原型對象中的方法能夠相互訪問嗎?

也是能夠的。

六、原型中屬性和方法的使用順序

實例對象使用的屬性和方法會先在實例中查找,找不到纔會到__proto__ 指向的構造函數的原型對象 prototype 中找,找到了則使用,找不到則報錯。

七、爲內置對象添加原型方法

像正常爲自定義構造函數添加原型方法同樣。

八、把局部變量變成全局變量

把函數中的局部變量暴露給瀏覽器頂級對象 window,那麼這個局部變量將變成 window 的一個屬性,能夠被整個瀏覽器所訪問。

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

九、把局部對象變成全局對象

<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 從局部對象變成全局對象。

十、案例:隨機小方塊

<!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>

一、產生小方塊對象也是個自調用函數,這裏面有一個構造函數 Food,兩個原型函數 init 和 product,其中構造函數中包括小方塊的全部屬性,好比小方塊是個 div,小方塊的寬高,顏色,left,top 的值等。

二、init 方法初始化小方塊的寬高,顏色以及將 div 加入到地圖 map 中。

三、product 方法是產生隨機位置,並賦值給小方塊的 left,top。

四、最後,在產生小方塊對象的最後,將 Food 對象暴露給 window,這樣在 Food 自調用函數的外面也能夠產生小方塊。

十一、案例:貪吃蛇

這個案例按照面向對象的思想,咱們把它分紅四個模塊。

第一,地圖模塊;

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

第三,小蛇模塊;

第四,整個遊戲也能夠封裝成一個模塊,這個模塊是將上面三個模塊再次封裝起來。

11.一、地圖模塊

地圖模塊最簡單了,就是一塊 div,設置寬高,背景就能夠了。

注意:因爲食物和小蛇都要在背景之上移動,因此食物和小蛇是脫標的,因此地圖須要設置 position: relative;

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

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

11.二、食物模塊

首先,食物的主體是由一個 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;

以後須要初始化食物的顯示效果和位置。

一、因爲常用 init 函數,因此將其寫入原型對象。

二、每次在建立食物以前先刪除以前的小方塊,保證map中只有一個食物

三、咱們須要在自調用函數中定義一個數組,用來保存食物,方便每次建立食物以前的刪除和後來小蛇吃掉食物後的刪除。

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.三、小蛇模塊

小蛇模塊也得現有構造函數。

一、direction是小蛇移動的方向;

二、beforeDirection 是小蛇在鍵盤點擊上下左右移動以前移動的方法,做用是不讓小蛇回頭,好比小蛇正往右走,不能點擊左按鍵讓其往左走,這時只能上下和右走。

三、小蛇最初的身體是三個 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);
        }
    };

接下來是小蛇移動的方法。

一、小蛇移動的方法分兩步,第一步是身體的移動;第二步是頭部的移動。

二、當小蛇頭座標和食物的座標相同時,表示吃到食物,這個時候小蛇要增加,怎麼增加呢?將小蛇的尾巴賦值一份添加到小蛇的尾部。

三、以後刪除食物,並從新生成食物。

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.四、遊戲模塊

首先建立遊戲對象須要遊戲構造函數,這個構造函數應該包含三個部分:食物,小蛇和地圖。

這個 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.五、開始遊戲

最後,咱們只須要兩行代碼就能夠開啓遊戲。

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

Web前端之巔

相關文章
相關標籤/搜索