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

jerrysun發表於2021-09-09

一、複習

例項物件和建構函式之間的關係:

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 是一個物件,樣式時一種屬性

3、建構函式,例項物件,原型物件三者的關係

1、例項物件是由建構函式建立的;

2、建構函式中有個屬性prototype,指向原型物件;

3、原型物件中有一個構造器,指向建構函式;

4、例項物件中的下劃線原型物件__proto__ 指向原型物件 prototype

5、原型物件中的方法可以被例項物件訪問,雖然例項物件中沒有這個方法,但是例項物件中 __proto__ 指向 prototype,所以所有的例項物件共享原型物件中的方法。

4、原型物件的簡單語法

什麼樣的資料需要新增到原型物件呢?

需要資料共享的資料,不論是屬性還是方法。

既然 prototype 是一個物件,那麼需要新增的屬性和方法就可以以物件的方法新增:


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

5、原型物件中的方法相互訪問

我們知道,例項物件中的方法是可以相互訪問的,那麼原型物件中的方法可以相互訪問嗎?

也是可以的。

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

例項物件使用的屬性和方法會先在例項中查詢,找不到才會到__proto__ 指向的建構函式的原型物件 prototype 中找,找到了則使用,找不到則報錯。

7、為內建物件新增原型方法

像正常為自定義建構函式新增原型方法一樣。

8、把區域性變數變成全域性變數

把函式中的區域性變數暴露給瀏覽器頂級物件 window,那麼這個區域性變數將變成 window 的一個屬性,可以被整個瀏覽器所訪問。

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

9、把區域性物件變成全域性物件


這裡把自定義的產生隨機數的 Random 物件暴露給頂級物件 window,那麼 Random 從區域性物件變成全域性物件。

10、案例:隨機小方塊

html>
    
    Title    

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;

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 

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 

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

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 = maxX)) {
            // 撞牆了
            clearInterval(timeId);
            alert("Oops, Game Over!");
        }

        // Y方向邊界
        if ((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();

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/151/viewspace-2810074/,如需轉載,請註明出處,否則將追究法律責任。

相關文章