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",id和class就属於key
    於是,这个叫做苹果的物件,可以透过关键字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每个物件的里面,能达成这一目的概念就叫做继承

什麽是继承呢?请期待本章节後续的文章!


<<:  Day_05 : 让 Vite 来开启你的Vue 之 前进Vite

>>:  Day 2 Mac 环境小工具

资安学习路上-渗透测试实务3

漏洞利用 利用侦测到的现有漏洞取得初始控制权 上图取自台科大资安社课教材 1. Exploit-db...

实战练习 - 使用 RxJS 实作「自动完成 / 搜寻 / 排序 / 分页」功能

今天我们用实际的例子来练习各种 RxJS operators 的组合运用!在一般的应用程序里面,资料...

[day5] Python发送Request接收Response与永丰API串接参数

Python实作 Request发送 如果你的Python环境没有requests模组 pip in...

【後转前要多久】# Day08 CSS - CSS Reset

HTML的注解是使用 <!-- --> 而CSS的注解是使用 /* */ 浏览器预设样式...

Day 17 ( 中级 ) 灯光绕圈圈 ( 数字函式 )

灯光绕圈圈 ( 数字函式 ) 教学原文参考:灯光绕圈圈 ( 数字函式 ) 这篇文章会介绍如何使用「函...