學(xué)習(xí)目標(biāo)
- 理解函數(shù)斟薇、參數(shù)卤唉、聲明和調(diào)用的概念
- 掌握用變量和函數(shù)應(yīng)對變化的方法
學(xué)習(xí)用時:60分鐘
通過前幾課的學(xué)習(xí)硅卢,我們已經(jīng)掌握了用代碼來畫點计盒、線和方塊的方法渴频,現(xiàn)在大家都能在畫布上畫出自己喜歡的圖案了。
接下來北启,我們希望讓畫面能夠動起來卜朗。比如說,讓你畫出的小人從畫面的一邊咕村,移動到另一邊去场钉。
但是,怎樣才能把我們畫出的圖案懈涛,移動到另一個位置呢逛万?
工匠的困境
在很久很久以前,有一位法老想要給自己建造一座雕像批钠。于是他找來了一位手藝精湛的工匠泣港,并選好了一座山頭。于是工匠拿來錘子和鑿子价匠,叮叮當(dāng)當(dāng)?shù)亻_工了……
就這樣日復(fù)一日当纱、年復(fù)一年;不知不覺間踩窖,五十年過去了……
工匠終于完成了雕像坡氯,此時他已是個白發(fā)蒼蒼的老人。他把幾乎一生的心血都花在了這座雕像上洋腮,看著自己完成的作品箫柳,心中感到無比地自豪和驕傲。然而啥供,法老在視察完他的工作成果后悯恍,冒出輕描淡寫的一句話來,讓他頓時萬念俱灰:
“挺好的伙狐,就是有點歪涮毫。往右邊挪上一米吧瞬欧!”
上面這個故事來源于《The Art of Readable Code》一書。這個故事生動地闡釋了下面這條原則:
Change Is The Only Constant:唯一不變的就是變化
根據(jù)默菲定律罢防,任何可能發(fā)生的事情艘虎,只要給足夠的時間,就一定會發(fā)生咒吐。所以我們在編程時野建,要盡可能地考慮到需求變化的可能性。否則需求一旦發(fā)生變化恬叹,就會不可避免地陷入到工匠的困境中去候生。
移動一下試試看
請在Chrome瀏覽器中打開下面的鏈接:
http://codepen.io/zhangshenjia/pen/ZKbWEz
網(wǎng)頁加載完成之后,應(yīng)該能看到這樣的界面:
在這個程序中绽昼,我們畫好了一個十字陶舞,像不像射擊游戲里的準(zhǔn)星?這是一個僅由五個點組成绪励,簡單得不能再簡單的圖形了。
如果我們想把這個準(zhǔn)星向右挪動一個像素唠粥,該怎么修改程序呢疏魏?
我們首先想到的是,應(yīng)該把所有點的水平坐標(biāo)都加一晤愧,也就是把所有畫點語句中的第一個數(shù)字加一大莫。讓我們來試試看:
到現(xiàn)在為止,你感覺還OK吧官份?那是因為這個圖案只有五個點只厘。那如果我讓你把上節(jié)課的作業(yè)里的圖案移動一下呢?
現(xiàn)在舅巷,我們正面臨著和工匠一樣的困境:由于在我們用代碼畫出的圖案里羔味,每一個點的坐標(biāo)都是固定的數(shù)字。因此如果想要移動圖案钠右,哪怕只有一個像素的距離赋元,都必須修改所有畫點語句中的坐標(biāo)。如果我們的圖案由成百上千的點組成飒房,那全部修改一遍簡直就是個噩夢搁凸!
還記得上節(jié)課我們學(xué)過DRY原則(Don’t Repeat Yourself)嗎?有沒有覺得這樣的修改很重復(fù)呢狠毯?要是能夠只修改一個地方护糖,就自動同步到所有使用的地方就好了……
用變量來適應(yīng)變化
不妨先想一想,在生活中遇到會變化的需求時嚼松,我們是怎么處理的呢嫡良?
首先锰扶,我們得設(shè)計一個可以變化的組件,并通過它來對變化進(jìn)行適應(yīng)皆刺。比如汽車座椅中可以調(diào)節(jié)角度的軸承少辣、活動扳手中的可以調(diào)節(jié)卡口尺寸的蝸輪。
那在編程中羡蛾,有沒有可以這樣變化的東西呢漓帅?當(dāng)然有,那就是上節(jié)課我們就用過的變量痴怨。
首先刷新一下頁面忙干,把代碼復(fù)原。然后在第一句畫點代碼上方添加一個空行浪藻,輸入下面的代碼:
var x = 0;
這樣我們就定義了一個變量 x用來保存水平的坐標(biāo)捐迫,并給它賦值為 0。接下來爱葵,我們把所有畫點代碼中第一個數(shù)字前面都加上 x +:
注意:在符號 + 左右各有一個空格施戴。它們雖然沒有實際意義,但能使我們代碼顯得更清晰萌丈、讀起來更省力赞哗。對此感興趣的同學(xué)可以課后自行搜索一下“代碼風(fēng)格”。
現(xiàn)在我們可以修改 var x = 0 中的初始值看看辆雾,比方說改成 5 :
Oh Yeah肪笋,我們只修改了一行代碼,就可以讓整個圖案進(jìn)行水平移動了度迂!
接下來藤乙,我們可以用同樣的方法來實現(xiàn)圖案的垂直移動。定義一個變量 y 惭墓,并在所有畫點代碼中的第二個數(shù)字前面都加上 y +:
這樣一來坛梁,我們就可以通過修改變量 x 和 y 的值,把圖案移動到畫布的任何位置腊凶。再也不怕修改位置的需求了罚勾!
想要更多怎么辦?
要知道吭狡,需求的任何一部分都可能發(fā)生變化尖殃。除了圖案所處的位置之外,圖案的數(shù)量也會可能會變』螅現(xiàn)在畫布上只有一個十字送丰,要是我們需要畫更多的十字怎么辦?
有同學(xué)說:這好辦弛秋,只要把畫十字的代碼再復(fù)制一份器躏,然后修改 x 和 y 的值就可以了嘛俐载!想畫多少十字,就復(fù)制多少次唄登失!
那如果我們要畫100個十字該怎么辦遏佣,把代碼復(fù)制100次嗎?我們畫十字的這段代碼只有短短幾行揽浙,多復(fù)制幾遍貌似還可以接受状婶。但如果我們畫的是一個復(fù)雜的圖案,需要幾百行代碼來完成呢馅巷?
復(fù)制代碼確實可以簡單粗暴地臨時解決問題膛虫,但事后修改起來就很麻煩了。比如說钓猬,我們想把畫面上所有的十字都改成紅色稍刀,就需要在所有復(fù)制出來的代碼里都加上一行更換顏色的代碼。萬一改完發(fā)現(xiàn)還是黑色好看的話敞曹,還得把剛才添加的代碼一行行刪掉……
需求只發(fā)生了一個很小的變動账月,就要修改一大堆重復(fù)代碼,業(yè)內(nèi)把這樣的情況稱之為“霰彈式修改”澳迫。由于我們是人不是機(jī)器局齿,這樣做很累自不用說,在做大量修改時也難免會發(fā)生疏忽纲刀,比如漏加了一處代碼,又或者在刪除換顏色代碼時錯把畫點代碼刪掉……
又有同學(xué)說:那我們能不能用上節(jié)課學(xué)過的循環(huán)呢暂论?把畫十字的代碼放在循環(huán)體里面褐,然后每次循環(huán)改變 x 和 y 的值不就行了嗎?這樣畫十字的代碼就只會出現(xiàn)一次了呀取胎!
問題是展哭,循環(huán)只能用來處理連續(xù)性的重復(fù)工作,對非連續(xù)的重復(fù)無能為力闻蛀。我們可以用循環(huán)來一次性畫出N個十字匪傍,但是不能中途停下來。然而觉痛,有很多重復(fù)性的工作都不是連續(xù)性的役衡。
比如在某個網(wǎng)絡(luò)游戲中,獲取經(jīng)驗值有很多方法(殺死敵人薪棒、完成任務(wù)手蝎、掛機(jī)……)榕莺,經(jīng)驗值滿了之后就需要升級,然后提升人物的一系列屬性棵介,還有可能學(xué)得新的技能钉鸯。那么在獲取經(jīng)驗值之后,判斷是否需要升級的邏輯就需要多次重復(fù)運行邮辽,但獲取經(jīng)驗值的邏輯卻散落在程序中多個不同的地方……這樣的需求唠雕,是無法通過循環(huán)來解決的。
那除了循環(huán)之外逆巍,還有什么辦法可以讓一段代碼能夠重復(fù)使用呢及塘?答案就是:函數(shù)。
什么是函數(shù)锐极?
想象一下,如果你家沒有醬油了灵再,需要去超市買肋层,但你自己又不想跑腿,正好孩子放學(xué)回家翎迁,就想讓他去打醬油栋猖。因為孩子之前沒干過這事,所以你得教他具體該怎么做汪榔。
「打醬油」的流程:
- 帶上足夠的錢蒲拉,出門去超市
- 找到調(diào)味品區(qū),拿一瓶醬油
- 在收銀臺結(jié)帳痴腌,收好找零
- 把醬油拿回家雌团,交到你手上
這樣一來,以后再需要買醬油的時候士聪,只要告訴孩子“打醬油去”就行了锦援,而不用再把整個流程重新講一遍了。( 你說啥剥悟,都忘光了灵寺?那我再給你講一遍……)
「打醬油」就是一個函數(shù),同時它也是這個函數(shù)的函數(shù)名区岗。而打醬油的具體流程略板,就是這個函數(shù)的函數(shù)體。
函數(shù)(Function):可以在程序內(nèi)被重復(fù)調(diào)用的一段代碼
函數(shù)名(Function Name):函數(shù)對外的名稱
函數(shù)體(Function Statement):函數(shù)內(nèi)部執(zhí)行的具體流程
教孩子怎么打醬油慈缔,就是在聲明這個函數(shù)蚯根。對孩子說“打醬油去”,就是在調(diào)用這個函數(shù)。而孩子最后交到你手上的醬油颅拦,就是函數(shù)的返回值蒂誉。
聲明(Declare):告知程序的執(zhí)行者有這么一個函數(shù)存在
調(diào)用(Call):在程序運行的過程中,要求執(zhí)行某個函數(shù)
返回值(Return Value):函數(shù)調(diào)用完畢后的返回結(jié)果
顯然距帅,如果你從來沒有教過孩子,就讓他去打醬油,他肯定會蒙圈的瞬测。一個函數(shù)必須得先經(jīng)過聲明,才能進(jìn)行調(diào)用。因為如果不進(jìn)行聲明摔寨,程序的執(zhí)行者根本不知道有這個函數(shù)存在,當(dāng)然也就無法去執(zhí)行了菜职。
函數(shù)的返回值并不是必須提供的。有的函數(shù)要求提供一個明確的返回值邮屁,比如「買醬油」這個函數(shù),就明確要求拿到一瓶醬油是尔,即便因為各種原因沒有買到殉了,那也得給出個說法;而有的函數(shù)則只看重運行的過程拟枚,比如「冥想」這個函數(shù)薪铜,并不需要最后拿出個什么成果來。
函數(shù)可以使一段邏輯在不同地方被重復(fù)調(diào)用恩溅。可以用函數(shù)來解決那些循環(huán)無法解決的非連續(xù)性重復(fù)問題隔箍。由于每個調(diào)用的地方只會出現(xiàn)函數(shù)名,而不會出現(xiàn)具體的邏輯脚乡。這樣在需求發(fā)生變化時蜒滩,不管這個函數(shù)被調(diào)用了多少次,我們都只需要修改函數(shù)體里的邏輯就行。
當(dāng)然俯艰,更改函數(shù)名的時候捡遍,所有調(diào)用這個函數(shù)的地方還是不可避免地要同步修改。所以起一個好名字竹握,非常非常地重要稽莉!關(guān)于怎么給函數(shù)起一個好名字來盡量避免修改,同學(xué)們可以在課后搜索一下涩搓。
需求有變化怎么辦污秆?
不過,這樣的函數(shù)雖然解決了在不同地方重復(fù)調(diào)用的問題昧甘,但每次執(zhí)行的邏輯都是固定不變的良拼。比如「打醬油」函數(shù),在不出意外(超市關(guān)門充边、沒貨……)的情況下庸推,每次都會得到一瓶醬油。
然而我們知道:需求是不可能一成不變的浇冰。今天我們需要一瓶醬油贬媒,明天可能要十個饅頭,后天則可能要一打可樂……要怎樣才讓函數(shù)可以應(yīng)對這些變化呢肘习?
首先想到的是际乘,我們能不能給購買每種商品的流程都聲明一個函數(shù),并在需要的時候調(diào)用它們呢漂佩?就像這樣:「買饅頭」脖含、「買可樂」……
這樣雖然貌似解決了問題,卻產(chǎn)生了一大堆邏輯雷同的函數(shù)投蝉。如果購買流程中的任一環(huán)節(jié)的邏輯變更养葵,就需要同步修改所有的函數(shù)民轴。何況即便是相同的商品荣倾,每次買的數(shù)量也可能不同,難道還要聲明「打醬油」蝉揍、 「打2瓶醬油」庸娱、「打3瓶醬油」……這樣一系列的函數(shù)嗎着绊?
我們可以把函數(shù)調(diào)整修改一下,來應(yīng)對可能發(fā)生的變化:
「買東西」的流程:(調(diào)用時需要說明要買的「東西」及「數(shù)量」)
- 帶上足夠的錢涌韩,出門去超市
- 找到貨架畔柔,拿「數(shù)量」的「東西」
- 在收銀臺結(jié)帳氯夷,收好找零
- 拿回家臣樱,交到你手上
「買東西」也是一個函數(shù)。但和「打醬油」有所不同的是,在調(diào)用「買東西」時需要指明「數(shù)量」和「東西」雇毫,它們都是函數(shù)的參數(shù)玄捕。
參數(shù)(Arguments):調(diào)用函數(shù)時所提供的數(shù)據(jù)
在函數(shù)體內(nèi),可以用與參數(shù)同名的變量棚放,來訪問傳入的數(shù)據(jù)枚粘。假設(shè)我們在調(diào)用「買東西」函數(shù)時傳入的「數(shù)量」是 3、「東西」是** 辣條飘蚯,那么函數(shù)的第二步實際執(zhí)行的流程是這樣的:“找到貨架馍迄,拿三包辣條”。
參數(shù)不一定都是必須提供的局骤,提供了默認(rèn)值的參數(shù)可以省略攀圈。有的參數(shù)是必須提供的,比如要買的「東西」峦甩,如果不說清楚赘来,就根本不知道要買啥;而有的參數(shù)是可以省略的凯傲,比如要買的「數(shù)量」犬辰,在沒有提供的情況下,那就默認(rèn)只買一份冰单。
通過更換傳入的參數(shù)幌缝,我們不需要對函數(shù)內(nèi)部邏輯進(jìn)行改動,就能控制邏輯的變化诫欠。比如狮腿,我們可以發(fā)起這樣調(diào)用:「買兩包鹽」、「買五瓶啤酒」……
用函數(shù)來畫十字
接下來呕诉,我們要聲明一個「畫十字」的函數(shù)缘厢,在調(diào)用時把坐標(biāo)當(dāng)成參數(shù)傳進(jìn)去,這樣就可以在畫布的任意坐標(biāo)位置畫出十字了甩挫。如果想畫多個十字的話贴硫,多調(diào)用幾次就行了。
首先伊者,我們把剛才添加那兩行 var 語句刪掉英遭,替換成下面的代碼:
function drawCross(x, y) {
然后,在最后一句畫點語句后面增加一個空行亦渗,輸入一個符號 } :
這樣我們就聲明了一個函數(shù)挖诸,名為 drawCross (draw是“畫”,cross是“十字”法精,聯(lián)合起來就是“畫十字”的意思)多律。這個函數(shù)有兩個參數(shù):x 和 y痴突,指定了十字在水平和垂直兩個方向上的位置坐標(biāo)。在函數(shù)體內(nèi)會自動聲明兩個和參數(shù)同名的對應(yīng)變量 x 和 y狼荞,它們只能在函數(shù)體內(nèi)部使用辽装。
需要注意的是,函數(shù)名里是不允許有空格的相味。像drawCross這樣把多個單詞直接連起來拾积,并讓首字母大寫的方法叫做駝峰命名法。也有draw_cross這樣的命名法丰涉,不過還是駝峰命名法比較常用拓巧。雖然我們也可以直接用中文「畫十字」來當(dāng)函數(shù)名,但我強(qiáng)烈建議不要這么做一死。
現(xiàn)在的函數(shù)體沒有縮進(jìn)玲销,看起來結(jié)構(gòu)不清晰。讓我們選中函數(shù)體里所有的畫點代碼摘符,按下 TAB 鍵增加縮進(jìn)贤斜,這樣代碼看起來就舒服多了:
但是現(xiàn)在畫布是空的,我們的十字到哪里去了呢逛裤?原來我們只聲明了函數(shù)瘩绒,并沒有調(diào)用它,所以函數(shù)體里的邏輯并不會被執(zhí)行带族。接下來锁荔,就讓我們添加一個函數(shù)調(diào)用吧。
在程序的最底部添加一個空行蝙砌,輸入下面的代碼:
drawCross(0, 0);
十字出現(xiàn)了阳堕!在程序執(zhí)行到我們剛剛添加的這一句時,就會跳轉(zhuǎn)到 drawCross 函數(shù)內(nèi)部去執(zhí)行择克,執(zhí)行完后再回來繼續(xù)往下走恬总。就像我們讀外文書時,發(fā)現(xiàn)一個不認(rèn)識的單詞就停下來去查字典肚邢,查完回來接著讀一樣壹堰。
現(xiàn)在我們可以通過繼續(xù)調(diào)用這個函數(shù),在畫面的不同位置畫出更多的十字了骡湖。試著在程序底部添加這幾行代碼:
drawCross(0, 3);
drawCross(3, 0);
drawCross(3, 3);
我們通過四個十字組合出了一個符號 #贱纠,顯然這是個更復(fù)雜的圖案。那如果我們想要把這個圖案移動到其他位置响蕴,該怎么做呢谆焊?
在函數(shù)里調(diào)用函數(shù)
這個圖案是通過對 drawCross 函數(shù)進(jìn)行四次調(diào)用畫出來的。那么我們直接修改這四行代碼里的調(diào)用參數(shù)行不行呢浦夷?
當(dāng)然可以辖试!畢竟要修改的只有四行代碼辜王,但要是我們的圖案是由100個十字組成的呢?那要修改多少行代碼剃执?
我們再一次遭遇了工匠的困境,那我們是不是還可以用變量來隔離變化呢懈息?
當(dāng)然可以肾档!不過,如果我們需要畫多個符號 # 呢辫继?還是得復(fù)制一堆代碼……這樣一下霰彈式修改還是無法避免怒见。那么,我們能不能像聲明 drawCross 函數(shù)來畫十字一樣姑宽,再聲明一個 drawHash 函數(shù)來畫符號 # 呢遣耍?
當(dāng)然可以!要知道函數(shù)體是一段代碼炮车,而函數(shù)的調(diào)用也是一行代碼舵变。所以我們可以在函數(shù)體里再調(diào)用別的函數(shù),就可以我們在循環(huán)體內(nèi)使用循環(huán)一樣瘦穆。
那能不能在函數(shù)里聲明函數(shù)呢纪隙?當(dāng)然也可以,但這樣聲明出來新函數(shù)只能在舊函數(shù)里使用扛或。關(guān)于函數(shù)作用域的內(nèi)容绵咱,感興趣的同學(xué)可以課后搜索一下。
我們在四句對 drawCross 函數(shù)的調(diào)用前面加上一句代碼:
function drawHash(x, y) {
然后在程序最后面加上一個 }熙兔,這樣就定義了一個 drawHash 函數(shù)悲伶。不要忘記給函數(shù)體縮進(jìn)噢:
現(xiàn)在圖案消失了,因為我們還沒有添加調(diào)用呢住涉。隨便給個坐標(biāo)麸锉,調(diào)用一下看看吧:
為什么圖案還是畫在左上角,沒有畫在我們指定的坐標(biāo)呢舆声?因為在 drawHash 函數(shù)里對 drawCross 函數(shù)進(jìn)行調(diào)用時淮椰,并沒有把我們指定的坐標(biāo)傳遞過去。雖然這兩個函數(shù)里都有 x 和 y 這兩個參數(shù)纳寂,各自函數(shù)體里都有同名的兩個變量主穗,但是它們互相是沒有關(guān)系的。
我們在調(diào)用 drawHash 函數(shù)時使用的參數(shù)是 10, 10毙芜,所以在 drawHash 函數(shù)的變量 x 和 y 的值都是 10忽媒。但在調(diào)用 drawCross 函數(shù)時的參數(shù)就不一樣了,比如第二次調(diào)用時的參數(shù)是 0, 3 腋粥,那在 drawCross 函數(shù)內(nèi)的變量 x 和 y 的值就分別為 0, 3晦雨。
每個函數(shù)的參數(shù)變量都只能在函數(shù)內(nèi)部使用架曹,外部是無法訪問的,只能通過調(diào)用時傳入?yún)?shù)來對其進(jìn)行賦值闹瞧。關(guān)于變量作用域的內(nèi)容绑雄,感興趣的同學(xué)可以課后搜索一下。
如果想讓我們給 drawHash 函數(shù)傳遞的參數(shù)影響 drawCross 函數(shù)奥邮,就得在調(diào)用 drawCross 函數(shù)時改變參數(shù)万牺,也就是把 x 和 y 加進(jìn)去:
大功告成!現(xiàn)在我們有了 drawCross 和 drawHash 兩個函數(shù)洽腺,可以用一行代碼畫出十字脚粟,也可以用一行代碼畫出#。當(dāng)然蘸朋,你總是可以在現(xiàn)有函數(shù)的基礎(chǔ)上核无,構(gòu)造出更復(fù)雜的函數(shù)……最終,你就可以僅僅用一行代碼藕坯,就畫出一個很復(fù)雜的圖案來团南。
能不能在drawHash函數(shù)里再調(diào)用drawHash函數(shù)自己呢?理論上是可以的炼彪,這種做法叫做遞歸(Recursion)已慢。遞歸是一種比較有難度的編程技巧,需要精心設(shè)計控制流程霹购,避免發(fā)生無限調(diào)用∮踊荩現(xiàn)在我們還用不著它,感興趣的同學(xué)可以課后搜索一下齐疙。
「自底而上」vs「自頂向下」
到目前為止膜楷,我們做了下面這些事:
- 先想辦法畫一個點
- 用同樣的方法畫一堆點來組成圖案
- 把這一堆畫點的代碼聲明為一個函數(shù)
- 通過調(diào)用函數(shù)和畫點,畫出更復(fù)雜的圖案
- 把這一堆畫圖的代碼再聲明為一個函數(shù)
- ……
這種“先看看能做點什么贞奋,然后再看看能做點別的什么”的思考和行動模式赌厅,我們稱之為自底而上(Bottom-up)。每走一步就能看到對應(yīng)變化轿塔,一步一個腳印特愿,走得很踏實。
然而勾缭,在解決實際問題時揍障,僅僅靠「自底而上」是不行的。因為能做的事情實在太多了俩由,但可能絕大多數(shù)都和我們現(xiàn)在想做的事情沒什么關(guān)系毒嫡。只著眼于當(dāng)下能做什么,而不思考我們想做什么幻梯,就可能會迷失方向兜畸,一直在原地踏步努释;甚至于南轅北轍,離目標(biāo)越來越遠(yuǎn)……
另外一種思路是咬摇,先確定好要達(dá)成的目標(biāo)伐蒂,制定一個整體規(guī)劃,再分解成具體的行動計劃并執(zhí)行肛鹏。這正是我們之前學(xué)過的萬金油思路——「拆分」逸邦。這種“先想清楚要做什么,然后再看看怎么去做”的模式龄坪,我們稱之為自頂向下(Top-down)昭雌。
當(dāng)然复唤,僅僅靠「自頂向下」也是不行的健田。我們想做的很多事情,現(xiàn)在是做不到的佛纫〖司郑總是紙上談兵,想太多不切實際的東西呈宇,只會浪費時間好爬。結(jié)合使用「自底而上」和「自頂向下」這兩種模式,理論聯(lián)合實際才是王道甥啄。
在用「自頂向下」的思路來分解目標(biāo)存炮,作出初步的規(guī)劃設(shè)想的同時;也需要根據(jù)目前具備的資源和能力蜈漓,用「自底而上」的思路來檢驗設(shè)想的可行性穆桂。只有當(dāng)我們在這兩種思路之間找到了結(jié)合點,才能將設(shè)想進(jìn)一步細(xì)化成計劃進(jìn)而執(zhí)行融虽。
當(dāng)設(shè)想不可行時享完,是放棄目標(biāo)或降低標(biāo)準(zhǔn),還是去獲取現(xiàn)在不具備的資源和能力呢有额?這得看目標(biāo)的優(yōu)先級有多高般又、是否是核心需求,在達(dá)成目標(biāo)的期望價值和獲取資源能力的代價中反復(fù)做權(quán)衡……這已經(jīng)遠(yuǎn)遠(yuǎn)超出了本教程的范疇巍佑,容我不再細(xì)表茴迁。
寫一個畫笑臉的函數(shù)
假設(shè)我們現(xiàn)在的目標(biāo)是:在畫布上畫出一個笑臉。由于這是一個獨立且完整的任務(wù)萤衰,所以我們可以聲明一個 drawFace 函數(shù)來完成它:
先用「自底而上」的思路分析:我們已經(jīng)具備了在畫布的任何位置用任何顏色畫出像素的能力笋熬,而畫布上的笑臉肯定是由一堆像素構(gòu)成的,所以這個目標(biāo)必然是可達(dá)成的腻菇。 所以胳螟,盡管此時我們的函數(shù)里一行代碼都沒有昔馋,但我們完全可以相信,這個函數(shù)的功能是可以實現(xiàn)的糖耸。
所以秘遏,這個函數(shù)也沒必要現(xiàn)在就寫,可以先去做更重要或更緊迫的事嘉竟;依賴這個函數(shù)的工作(比如寫一個畫小人的函數(shù)drawPerson)現(xiàn)在就可以同步開展邦危,而不必非得等到這個函數(shù)完成后再進(jìn)行。只要在必須在畫布上看到笑臉時舍扰,把它完成就好倦蚪。
隨后我們可以用「自頂向下」的思路來分解這個函數(shù)。一般來說边苹,一個笑臉由眼睛陵且、嘴、鼻子个束、眉毛等部分組成慕购。其中眼睛和嘴巴是必需的,所以我們可以再添加兩個函數(shù) drawEye 和 drawMouth茬底,其余非必須的部分可以先寫成注釋沪悲,以后有時間再添加:
基于同樣的原因,我們斷定 drawEye 和 drawMouth 函數(shù)是可以實現(xiàn)的阱表。所以這時盡管這兩個函數(shù)現(xiàn)在還是空的殿如,我們也可以宣告 drawFace 函數(shù)完成了,因為它已經(jīng)完成了自己的使命:羅列所有必要的組成部分最爬,并整理好它們之間的關(guān)系涉馁。
當(dāng)然,眼睛和嘴巴之間的距離可能還需要不斷調(diào)整烂叔,但這無關(guān)緊要谨胞。至于眼睛和嘴巴到底畫了沒有,畫得怎么樣蒜鸡,我們在驗收 drawFace 函數(shù)時并不關(guān)心胯努。因為那是 drawEye 和 drawMouth 函數(shù)要完成的任務(wù)。
接下來的任務(wù)就是完成 drawEye 和 drawMouth 函數(shù)了逢防。我們可以找時間分別來完成它們叶沛,也可以分配給別人來干。為簡單起見忘朝,我們只畫一個點來當(dāng)眼睛灰署,畫四個點來當(dāng)嘴巴:
笑臉完成了!
函數(shù)的價值和意義
“工欲善其事,必先利其器” —— 《論語?衛(wèi)靈公》
在「我的世界」這款游戲里溉箕,玩家一開始手里空空如也晦墙,什么都沒有。只能赤手空拳去擼樹肴茄,然后拿到木頭做成斧頭等工具晌畅,再去高效率地采集更多的資源。
寫函數(shù)的過程寡痰,就是打造工具的過程抗楔。雖然寫函數(shù)的過程比較吃力,但寫出來的函數(shù)可以大大地方便我們之后的工作拦坠。雖然函數(shù)內(nèi)部的代碼邏輯會比直接堆代碼要復(fù)雜一點點连躏,但在調(diào)用函數(shù)時的代碼卻簡潔了許多。這和解魔方一樣贞滨,用初級方法會比較簡單易懂入热,但步數(shù)要多一些;而用高級方法會比較復(fù)雜疲迂,但步數(shù)會少一些才顿。
當(dāng)一段邏輯需要多次使用時莫湘,簡單地復(fù)制粘貼一遍代碼貌似是第一時間就能想到的方法尤蒿。要是需求稍有變化,那就做一點適當(dāng)?shù)母膭臃濉=Y(jié)果可能就會產(chǎn)生一大堆雷同或者大同小異的代碼:
我們把一段需要多次使用的邏輯封裝成函數(shù)后再調(diào)用腰池,顯著地減少了重復(fù)代碼。從而避免了直接復(fù)制代碼可能導(dǎo)致的“霰彈式修改”忙芒,可以更好的適應(yīng)需求的不斷變化示弓。關(guān)于這一點,我們已經(jīng)通過上面的實踐得到了深刻的體會呵萨。
函數(shù)隱藏了不必要的實現(xiàn)細(xì)節(jié)奏属,同時降低了在修改代碼的過程中出錯的可能性。以機(jī)械表為例潮峦,如果不用表盤遮住內(nèi)部囱皿,就會給使用者帶來不必要的心理壓力,也很容易損壞其內(nèi)部精密的結(jié)構(gòu)忱嘹。
我們還通過函數(shù)名傳達(dá)了邏輯意圖 嘱腥,使本來需要注釋的代碼意圖變得更直觀,更容易理解拘悦。用術(shù)語來說齿兔,就是提升了代碼的可讀性。很明顯,面對一堆畫點語句分苇,你不看注釋或者不手動運行測試一下添诉,根本不可能明白它畫的是什么。而對一個名為drawCross的函數(shù)進(jìn)行調(diào)用医寿,則明明白白地告訴了讀者這行代碼的作用:我要畫一個十字吻商。
最重要的一點是,我們通過函數(shù)隔離出了一個抽象層次糟红。這使我們可以將當(dāng)前的思維局限在某個環(huán)節(jié)之中艾帐,將全部的注意力用于在當(dāng)前層次上進(jìn)行完整自洽的思考上。于是我們得以自頂向下地進(jìn)行框架式思考盆偿,將一個復(fù)雜的任務(wù)不斷地拆分到可以在單位時間內(nèi)完成的粒度柒爸,并最終逐步完成。
內(nèi)容回顧
函數(shù)(Function):可以在程序內(nèi)被重復(fù)調(diào)用的一段代碼
函數(shù)名(Function Name):函數(shù)對外的名稱
函數(shù)體(Function Statement):函數(shù)內(nèi)部執(zhí)行的具體流程
聲明(Declare):告知程序的執(zhí)行者有這么一個函數(shù)存在
調(diào)用(Call):在程序運行的過程中事扭,要求執(zhí)行某個函數(shù)
返回值(Return Value):函數(shù)調(diào)用完畢后的返回結(jié)果
參數(shù)(Arguments):調(diào)用函數(shù)時所提供的數(shù)據(jù)
課后作業(yè)
在Chrome中打開下面的地址:
http://codepen.io/zhangshenjia/pen/MmyreE
這里已經(jīng)寫好了兩個函數(shù) drawPoint 和 drawBox捎稚,分別實現(xiàn)了畫點和畫長方形的功能,請先體驗一下它們的威力求橄。
1今野、用「自頂向下」的方式來實現(xiàn)一個函數(shù),畫出自己喜歡的圖案罐农。你可能需要基于 drawPoint 和 drawBox条霜,聲明更多的自定義函數(shù),并組合使用它們涵亏;
2宰睡、在每個函數(shù)的聲明之前增加一行注釋來說明函數(shù)的作用(可參考已有的兩個函數(shù)),除此之外盡量少寫或不寫注釋气筋,在函數(shù)命名上多下功夫拆内,讓代碼簡明易懂。
有的同學(xué)可能會疑惑宠默,為什么函數(shù)聲明可以放在函數(shù)調(diào)用的下面麸恍?程序不是按從上向下的順序執(zhí)行代碼的嗎?執(zhí)行到函數(shù)調(diào)用那一行時搀矫,函數(shù)還沒聲明不是嗎抹沪?這個是因為JS獨有的提升(Hoisting)機(jī)制,感興趣的同學(xué)可以課后搜索一下艾君。