iT邦幫忙

2021 iThome 鐵人賽

DAY 10
0
Modern Web

從零開始打造網頁遊戲-造輪子你也辦的到!系列 第 10

Chapter2 - 用物件看真實世界(I)寫程式為什麼需要物件?如何簡化畫落葉的流程?

物件是什麼?為什麼需要它呢?

讓我們接續上回

完成昨天的演示後,也許有人會覺得,處理落葉動畫的流程很簡單,就是「讓落葉自然落下」然後「在畫布上繪製落葉」兩步驟而已,然而實作上總是比較複雜,可能還會有「被風吹起」、「被車撞到」、「被蜘蛛絲捕獲」等等的不同狀態,也有可能是同時發生,概念上像是這樣:
https://ithelp.ithome.com.tw/upload/images/20210917/201351974MsM931f0q.jpg

會有許多不同的可能讓落葉發生狀態的改變

隨著功能越來越豐富,我們會逐漸開始遇到程式碼維護上的困難,還有一個狀況是,如果我們要做一個暫停效果呢?大概很直覺就會寫成這樣:

function MouseAnime(){
    if(paused == false){
        // ......
        // 一大堆程式碼
        // ......
    }
    else{
        // 在畫布上繪製不動的落葉
    }
}

這樣寫最大的問題是,中間程式碼越來越多時,逐漸閱讀困難,為了讓它一目了然,就會需要包成幾個函式/方法:

function 自然落下方法(){ ...... }
function 被風吹起方法(){ ...... }
function 被車撞到方法(){ ...... }
function 被蜘蛛網捕獲方法(){ ...... }

fucntion MouseAnime(){
    if(paused == false){
        自然落下方法();
        被風吹起方法();
        被車撞到方法();
        被蜘蛛網捕獲方法();
    }
    else{
        // 在畫布上繪製不動的落葉
    }
}

現在看起來乾淨多了,但另一個問題開始出現,所有的變數(包含以上function)都在幾乎都在最外層,乍看之下命名還算清楚,但是這些變數都暴露在外,這樣的潛台詞就相當於說,這些方法都是公開的,沒有限制誰都可以使用,此時若有數十個上百個變數都在同一個範疇下,很快就會在讀程式碼時開始困惑「被車撞到方法是給誰用的?」或「這個那個是幹嘛的」,必須Ctrl+F來回比對才知道這些變數是設計給誰用的,因為「不知道這些變數」屬於誰。

牛頓真的有被蘋果砸到嗎?

要解決這個問題,首先我們要先理解一個蘋果的本質,為此,JS引入了一個概念,稱之為「物件」,可以看到,一個紅色飽滿的富士蘋果長這個樣子:

let Apple = {
    'variety': 'Fuji',
    'color': 'red',
    'taste': 'juicy'
}
console.log(Apple.variety);  // 'Fuji'
console.log(Apple.color);    // 'red'
console.log(Apple.taste);    // 'juicy'
  • 物件的定義的方式是最外層外面一個大括號 {}
  • 內層包含數個屬性,每個屬性由key和value組合,並在中間加上冒號,寫作{key: value}
  • 每個屬性之間已逗號相隔{key1: value1, key2: value2}
  • key 是由字串組成的關鍵字
  • value 可以是任何型別的值
  • HTML中的標籤其實就是物件,它們id="Apple"和class="apple"的寫法,各自對應了key跟value
    於是,這個叫做蘋果的物件,可以透過關鍵字key來配對,找到當中的值value,並且它的品種、顏色、口感,都是渾然天成,在建立之初就擁有的屬性。

反過來講,也可以後天塑造而成:

let fakeApple = {};
fakeApple.variety = 'Fuji';
fakeApple.color = 'red';
fakeApple.taste = 'juicy';
console.log(fakeApple.variety);  // 'Fuji'
console.log(fakeApple.color);    // 'red'
console.log(fakeApple.taste);    // 'juicy'

像是這個假貨,看起來很好吃,實際上只是偽裝成富士蘋果

沒禮貌的蘋果

讓我們檢查一下剛剛的蘋果是否受地心引力制約:

let Apple = {
    'variety': 'Fuji',
    'color': 'red',
    'taste': 'juicy',
    'gravity': 9.8,
    'velocity': 0,
    'height': 100,
    'fall': function(){
                this.velocity+= this.gravity*0.016;
                this.height-= this.velocity;
                console.log(this.height);
            },
    'manner': 'bad'
}
for(let N=0; N<40; N++){
    Apple.fall();
}
console.log(Apple.manner)  // bad

剛剛提到value可以是任何型別,也包括了函式!因此我們可以用Apple.fall呼叫它,是Apple專用的函式呢!還可以透過this呼叫自己來取得自己的其他屬性。
格式相當於常見的函式命名法let fall = function(){......}

原來,地心引力確實存在,至於...有沒有掉到牛頓頭上呢?這顆蘋果這麼沒禮貌,也是有可能故意跑去砸牛頓,然後在他頭上爆開,我只能說:不排除這個可能性!

回到我們的落葉

確實,我們可以立刻開始著手修改我們昨天設計的落葉,改成像上面蘋果的格式一樣,然而,這邊會一個問題,如果我今天要用兩片葉子怎麼辦,總不會同樣的格式再寫第二遍吧?然後要N片就跑N遍迴圈...唉呀!用想的就累,其實,我們還缺少一個重要的步驟,就是替它設計一個建構式(Constructor)

Constructor

概念上就是,我們可以設計一個物件產生器,然後需要落葉的時候,就用產生器創造一個出來。咦?鳩豆麻蝶,這段敘述有沒有覺得有點熟悉呢?是不是跟這句話很像呢:「我們可以設計一個陣列產生器,然後需要陣列的時候,就用產生器創造一個出來」,寫成代碼如下:

let myGirlfriend = new Array(10);
let myMoney = new Number(1000000);

這樣的寫法是不是很熟悉呢?其實我們之所以能使用陣列的各種方法諸如splice、reduce、forEach,便是因為有這個稱之為「Array」的建構式,它把陣列常用的方法全都定義好了,因此myGirlfriend就會有很多方法可以用,像是三人行剛剛的Apple有fall這個方法可以用一樣。

那麼,建構式要怎麼寫呢?最簡易的形式如下,當中的this所指涉的對象,是當你使用建構式時,它會回傳的對象return this;,那麼我們就是從一開始設計蘋果時寫apple.key=value,改寫成this.key=value,就能成為一個模板,比如,我們拿昨天的落葉動畫來修改,可以這樣寫:

function leafMaker(){
    this.timestamp = Date.now();
    this.lifeCycle = 6;
    //......
    // 省略(寬高、起始點、角速度等等昨天寫的所有參數)
    //......
    this.fall = function(context ,timestamp){
        let deltaTS = (timestamp - this.timestamp) / 1000;
        if(this.lifeCycle > deltaTS){
            let rotateNow = this.rotateTheta + this.rotateOmega * deltaTS;
            let revolveNow = this.revolveTheta + this.revolveOmega * deltaTS;
            let cursorX = this.originX + 500 * Math.sin(revolveNow);
            let cursorY = this.originY + 200 * Math.sin(revolveNow)
                                       + 100 * deltaTS;
            context.save();
            context.translate(cursorX, cursorY);
            context.rotate(rotateNow);
            context.drawImage(leafImg, -this.width/2, -this.height/2, this.width, this.height);
            context.restore();
        }
    }
    // 這邊JS省略了return this的寫法
}

還有一個小重點是,這個leafMaker只是一個建構式,並沒有leafMaker.fall這樣的方法,就像平常都是用new Array建構式來建立陣列資料,那麼你就不會期待可以用Array.splice一樣。

有了建構式後,我們可以在滑鼠點擊的當下,產生一個落葉物件,並賦值給名為leaf的變數:

let leaf;
canvas.addEventListener('click', SetMouse);
function SetMouse(e){
    leaf = new leafMaker();
}

原本初始化落葉的代碼都塞在SetMouse裡面,現在變得很乾淨

並且在每一偵的動畫循環,只需要這樣寫:

function MouseAnime(){
    Clear(context);
    leaf.fall(context);
}

原本計算落葉的方程式都塞在MouseAnime裡面,現在變得很乾淨

說到這,我想大家應該稍微明白了物件的魅力在哪裡了吧?原本落葉的相關程式碼四散各處

  • 變數定義在最外層
  • 初始化寫在滑鼠事件
  • 方程式寫在動畫循環
    這樣會使得維護上起來有困難,相比現在,以上三者皆在leafMaker這個建構式裡面,要進行修改和維護,只須從這個地方下手,便可以一次解決。而在程式的流程架構SetMouse和MouseAnime裡面,就只需要分別簡單的寫下一行代碼,除了一目了然外,只要架構上不變,就無須修改。

這時候,若想要實現一開始的流程圖繪製的各種落葉效果(被風吹起等等),是不是方便許多了呢?

物件是基於對真實世界的理解

還記得一開始談到屬於誰的概念嗎?在人類世界,看待與理解每一件事情,都會一層又一層,並且和其他類似的產生關連,想到蘋果,就會聯想到一些屬性,像是「長在樹上」、「會掉到地上」、「表面有一層蠟」,接著又會聯想到香蕉也長在樹上、葡萄、藍莓表皮也有果蠟,也因其錯綜複雜的關係,像是生物界就被歸類為了「界門綱目科屬種」。

要如何實現一環扣一環和共用相同的屬性,便是物件誕生之初的使命和意義,接下來幾篇我們將會前進到更深入的環節,試想,所有在地球上的物體都會受到地心引力的影響,也就是說,這是一個共用的屬性,因此,我們不需要把重力公式寫在Apple、Banana、Lemon每個物件的裡面,能達成這一目的概念就叫做繼承

什麼是繼承呢?請期待本章節後續的文章!


上一篇
Chapter2 - Canvas動畫(III)讓我們跳過微積分 用輕鬆的方式畫落葉吧
下一篇
Chapter2 - 用物件看真實世界(II)仍然對物件感到疑惑嗎?用你最愛點的豚骨拉麵做比喻
系列文
從零開始打造網頁遊戲-造輪子你也辦的到!31

尚未有邦友留言

立即登入留言