我們最近完成了報社的實習計劃,這不禁讓我想起,當我還是年輕開發者時,有什麼是我想學的。答案是,我希望我更懂除錯。
如何使用各種瀏覽器開發工具之類的資源多不勝數,每天都有新工具出現。它們都很棒,作為一個多年前學過 JavaScript 的人,我很羨慕新開發者能有這些工具。在瀏覽器中設定中斷點,檢視環境中的所有數值,走訪傳呼堆疊,真是一大變革。
然而,你最強大的除錯工具其實是雙耳之間的腦袋。世界上所有神秘的除錯祕訣,都不足以取代你對自己所寫下程式的了解。
開發工作一定會犯錯
撰寫程式碼必定會犯錯。你所遇見最棒的開發者也要為臭蟲負責。他們坐在電腦前,搔著頭狐疑著,問題出在哪裡?
開發者通常認為寫程式就是解決問題,其實寫程式比較像是下廚。就像煮東西一樣,沒有完美的程式,只有比較好或比較差。你可以試試不同的香料,你可以用火雞肉取代雞肉,你可以用大火或小火。這裡沒有盡善盡美,只有盡力而為。晚餐在你開動之時就結束了。
要掌握除錯一事,你必須預期會有臭蟲存在。如果有人報告有臭蟲,你得接受它。有程式碼自然就會有臭蟲。
臭蟲來自何處
假如你是寫程式的新手,最可能造成臭蟲的,是你對平台的瞭解不足。如果你對陣列運作方式沒有精確的了解,你很可能錯用陣列,導致臭蟲。只有經驗能幫你。
這份手冊假定你已經有某種程度的專業知識,會有臭蟲並非因為你不了解特定功能的運作。別擔心,就算這樣你還是會製造臭蟲。
你可能會遇到兩種棘手的臭蟲:
每當你要說,我不了解為何 x 會,這樣的句子時,請停止,並回想起假如 x 做了某件事,那對 x 絕對是合理的。只是 x 的行為對你而言令人費解罷了。
從電腦與程式的角度來看,所有行為都正如預期。如果你不了解為何你的程式會有這個結果,問題出在你瞭解的不夠。
電腦永遠是對的。電腦永遠是對的。電腦永遠是對的。某個寫了 10 年以上程式的人說:這機器的運算機制沒有一次故障。
容易疏忽的錯別字
大部份的錯別字都很醒目,給出不存在的名稱,而丟出變數未定義的錯誤。你的工具會出現紅字,所以很容易抓到。有害的臭蟲來自打進一個定義過,但卻不是你想要的名稱或數值。你可以把 linter 納入你的工具中,不過不能依賴它來抓到所有錯誤。
例如說,你有一個雜湊。在 JavaScript 裡,雜湊中的變數如果沒有值的話,會傳回 null。如果這個值是個有方法的物件,呼叫不存在的物件應該會出現有意義的錯誤。四處出現的紅色錯誤很有幫助。不過如果你只是存取一個剛好是 null 的值,你可能會拿到不易追蹤的錯誤數學數值。我的 console 告訴我,null - 3 == -3。
錯誤的物件型別
在 Gmail 與 Facebook 的年代裡,用戶應用程式可能有成千上萬行程式碼,彼此間有複雜的物件階層。由框架控制器控制的傳輸物件,會遞交資料模型給模版引擎來呈現。
現代應用程式有許多物件型別,如果你剛好使用了錯誤的型別,如果型別彼此相似,又或者是其父或子類別的話,大部份會運作無誤,不過有些情況則不是這樣。重要的是檢查產生臭蟲的物件,是不是你預期中的型別,而在該物件內你使用的所有變數,其型別是否如你預期。
舉例而言,假設你把一些重要的資料存在某個雜湊裡。留意,以下的範例是 CoffeeScript,不過僅具示意性。把它們看作要執行的虛擬程式碼 (pseudocode):
Animals = "Fido": DogObject("Fido") "Samantha": CatObject("Samantha") ...
你的程式有時預期看到 key,有時則是實際物件。常見的錯誤之一是拿到預期外的東西:
# 這是物件還是字串 Fido? addAgeToAnimal: (animal, age) -> animal.setAge(age)
或者假定 DogObject 繼承了 AnimalObject,而你正從資料庫抓取資料。你產生許多 AnimalObject 物件並自動填入資料。在呼叫該方法時,有時該方法會取得實際的 DogObject,有時則是填入狗的資料的 AnimalObject。但是你之後要改變 DogObject 時,你手動的 AnimalObject 物件們卻缺乏現在所需的資料。要找出這個問題有點棘手。
# AnimalObject 沒有 buyBone 方法。 buyDogNewBone: (dog) -> dog.buyBone()
下一步:問題所在
一旦你找出問題所在,你得確定數值為何錯誤。
你的腦袋或許不太能理解非同步操作。就算不把時間納入考量時,追蹤龐大的應用程式狀態樹已經夠困難的了。然而,非同步操作卻是 JavaScript 應用程式中的王道。如果某個物件型別有誤或數值不對,很有可能出在非同步操作(AJAX 呼叫、非同步資料庫或 worker 呼叫)產生的競賽情況 (race condition)。或者你可能只是在使用 Node。
# a 的值為何?得看你什麼時候問。
@a = "Default" jQuery.getJSON destinationUrl, (data) => @a = data.people[0].firstName @a = "Bob"
當某臭蟲出現,你需要在你的除錯器中使用中斷點來暫停執行,並檢視不同時間點的應用程式狀態。如果找不出問題,你可能必須檢查同一份程式碼好幾遍。可從非同步取得的資料來開始著手。
這常見於在程式設計師笑談中。當依次訪問某個物件時,確認該物件的型別如你預期,包含所有你預期的屬性,且具有一致性。如果元件之間共享狀態,像是上述的非同步情況中,迴圈有可能在迴圈執行間遭到破壞。如果你快取了某個記數值,並在沒有檢查其內容是否有效前就用以執行迴圈,將大大增加迴圈錯誤的機會。如果你同時在以 1 為基礎的系統與以 0 為基礎的陣列中進行記數,臭蟲出現的機會將迅速提高。
jQuery.getJSON remoteUrl, (data) => ### # 資料的形式為: [ # { # "id": 1, # "first": "Bob", # "last": "Smith" # }... #] ### names = [] data.forEach (item) => names[item.id] = item.first + ' ' + item.last ### # 噢噢。此處有賴於 id 得以 0 為基礎。但是這裡的 id 是以 1 為 # 基礎,但陣列不是。 # for (var i=0, len=... 就會發生預期之外的結果。 ###
變數作用範圍在許多環境下都是個問題,只不過在 JavaScript 中這個問題特別討厭。一個變數取得預期之外數值的最簡單方式之一,就是其作用範圍和預期不同。假如你定義了一個 local 變數卻沒有使用 var,其值將泄露到封閉範圍。JavaScript 關鍵字 this 的意義有時和你預期的有所不同。當暫停在中斷點時,確認 this 是你預期中的物件。最容易出錯的是在事件處理函式或 setTimeout 之中。在這兩種情況下,預設 this 會是全域的 window 物件。我自己的解決方法是使用 CoffeeScript 與寬箭頭 (fat arrow)。下一代的 JavaScript 也就是 ES6,也有解決方案。
# 在 setTimeout 中,這裡會被轉譯為 window.removeFlag unflag = -> @removeFlag() setTimeout(unflag, 500)
跟其他人討教
最後,或許更重要的,是你應該和別人討論你的臭蟲。我聽說它有很多代號,不過對我來說,它叫做鴨子除錯。做法是在你的電腦旁放隻橡皮鴨,當你遇上臭蟲時,跟鴨子解釋一下。不過,最好還是跟其他開發者談談。
你經常會發現,透過將問題口頭表達,聽你說話的人在開口前,你的腦袋就已經把問題解決了。
臭蟲是程式設計的基本面向。在你的職涯中你還是會持續製造它們。修復臭蟲是我們身為開發者最重要的任務。越快解決臭蟲,你就能越快回到有趣的功能與程式碼設計上。
臭蟲代表你對問題的理解不夠完全或錯誤。現實是最後的仲裁者。在盯著螢幕一整天後,你總會遇上想把螢幕丟出窗外的那種時刻。之後,不知何故,或許是透過鴨子除錯,或許只是偶然的運氣,你會打破僵局。這就是你的獎勵。得來不易的知識,會在你的心中迸發,你會對圍繞你的世界有更多的了解。我無法說明這種感覺是多麼美好。你已經向遙不可及的完美更進一步。
恭喜你。你已經是個除錯員了。
◎本文翻譯自 The New York Times,原作者為 Andre Behrens:https://open.blogs.nytimes.com/2013/08/27/the-young-developers-guide-to-debugging-javascript/?_r=1