CS50 problem set 8 作業回顧

陳雁智 (Marat Y. C. Chen)
Manjeaneer
Published in
7 min readNov 11, 2017

--

cs50 最後一個作業把 Python Flask, Database, JavaScript, jQuery, Ajax 都用上了,完成一個將地圖與新聞結合 (mashup) 的 web application,若對 pset 7 還有印象的話,第十週的課程內容特別提及了 JavaScript 讓瀏覽器不必刷新整個頁面而更新部分結果,也就是說 pset 7 作業實際只要一個 html 檔,其餘都可以用 JavaScript 讓使用者保持在同一頁面操作

pset8 要求如下:
1. 查詢必須提示城市、州、或郵遞區號
2. 附近區域的城市一併顯示
3. 按擊城市後顯示該城市相關新聞

一開始有點難想像如何完成這個複雜需求,幸虧 cs50 團隊己經將大致架構完成,也推薦了套件跟相關文件,重點仍然擺在完成 JavaScript — Flask — Database 彼此的互動,這次經驗三個較耗費時間的點:
1. JavaScript 中的 callback 跟 ajax 行為與語法熟悉度較低
2. Google API 參數較多,需要時間找到符合需要的參數
3. JavaScript 變數範圍及除錯與 C 或 Python 完全不同

第一點與第二相對好解決,反覆參數課堂範例、作業既有的程式碼或查資料就可以解決,在第三點上面吃了很大的虧,耗費蠻多時間以為是語法錯誤而實際上是因為對 JavaScript不夠了解造成一直用不適合的思維寫,稍後再做細部說明

實作時,可以多花一點時間反覆讀作業關於架構及各支程式的介紹,會增加不少對撰寫 web application 的熟悉度

讀了幾次程式碼跟作業介紹後,這次簡圖聚焦於 JavaScript ,也就是 client side 的程式,與 Python Flask,server side 的程式間的關係:

Scheme for 2017 cs50 pset 8 (mashup)

很明顯看得出來 Javascript 的部分比例相對大,時常寫到函式間相互呼叫的情況就迷路,畫一次圖就對函式間的關係清楚多了,script.js 主要有四個部分
1. Google Map 初始化,地圖相關的設定
2. configure() 加上了google.maps.event.addListener(map,"dragend",function())
google.maps.event.addListener(map,"zoom_changed",function()) 這兩個 listener 讓 JavascriptS 碰到拖拉 drag 或縮放 zoom時就呼叫 update() 將城市點 (marker) 加入地圖
3. 搜尋框內鍵入文字時,不斷將參數 (q=…) 送給 Flask.url_for('search') 查詢 database 內是否有類似記錄
4. addMark(place) 內也加上了一個 listener 監聽點擊事件,觸發時就呼叫 Flask.url_for('article') 並組合出相對應的 html 回傳到 client 端

寫 JavaScript 中,持續記得執行順序從上而下,有時為了讓程式簡單會想抽離函式獨立寫出來,導致順序或變數可能要重新安排,是寫 anonymous function 反而不太會出錯,需要再深入了解 JavaScript 比較能善用它的設計

變數呼叫與變數範圍比較 C, Python, JavaScript

寫 javascript 時,沿用 c 或 python 對變數的操作吃了蠻多虧,檢討起來是對變數範圍差異不甚清楚,以下比較:

C 語言

變數呼叫
call by value 或 call by reference (指標),若是 call by value, 使用該變數的函數會複製一份而不影響原值,在 wk4 介紹指標時己經強調了差異。

變數範圍
local 或 global,內層的區塊 (block) 可以取用外層的變數,例如:

int a = 2;
for (int i = 0 ; i < 5 ; i++)
{
printf(“%i\t”, a*i); // output 0, 2, 4, 6, 8
}

//below causes error during compilation
printf(“%i\n”,i);

在外層宣告的 a 在迴圈內可以被呼叫,而嘗試使用內層宣告的 int i 若在外層區塊則會導致編繹失敗

Python

變數呼叫
所有變數在 Python 中都視為一個物件,在呼叫時其實是 call by object,引用 Jeff Knupp 的一篇 blog 為例:

 
some_guy = ‘Fred’
first_names = []
first_names.append(some_guy)
another_list_of_names = first_names
another_list_of_names.append(‘George’)
some_guy = ‘Bill’
print (some_guy, first_names, another_list_of_names)

在 Python 中,assignment (=) 的行為實際上是將一個名字 (name) 跟一個物件 (object) 綑 (bind) 在一起的操作,在上面的例子的第六行 another_list_of_names = first_names 創造了新的名字(name) another_list_of_names 並指向同一個物件,所以 appned('Geroge') 對該物件加上一個元素 (‘George’) 後 first_nameanother_list_of_names 會印出相同結果

變數範圍
改寫 C 的範例:

a = 2for i in range(0,5):
print(a*i) # output 0, 2, 4, 6, 8
print(i) # output 4

不同於 c 裡宣告的 i 在區塊外無法被呼叫,此時的 i (name) 己經指向在迴圈中產生的整數 4, 所以在迴圈外仍然可以呼叫

JavaScript

呼叫方式與變數範圍
call by sharing,傻了,字面上完全看不出來,更亂的是連 Wikipedia 上也寫 Python 也使用這個方式,好在 stack overflow 上有相當不錯的討論串可以參考,寫作業需要訣竅的話,一言以蔽之

不要輕易嘗試修改函數範圍外的變數,除非知道自己在做什麼

將同樣例子改寫成 JavaScript

function test() {
var a = 2;
function test2(a) {
for (var i = 0; i < 5 ; i++)
{
console.log(a*i); // output 0,2,4,6,8
}
a = 4;
console.log(a); // output 4
console.log(i); / output 5
}
test2(a); // output 2
console.log(a); // output 2
};
test();

從執行結果可以看到:
1. test2 呼叫的 a 做的操作 a=4 在外層沒有影響,類似 call by value 的效果
2. 然而迴圈初始化的 i 又能在迴圈外層呼叫,與 c 的行為不同

同時撰寫 Flask 跟 JavaScript 時,在不熟悉兩者差別的情況下,蠻容易誤用 Python 變數呼叫及範圍的特性到 JavaScript 的程式碼,耗費相當多時間在除錯上

完成作業期間還卡在一個無奈的點,當時 cs50.io 有些不明問題導致 Degraded Performance,查看修改時常碰到 connection timeout 的狀況,慘到寫不下去就去 Google Cloud Platform 自己設置一個環境開發了,作法記錄在下次的回顧

cs50.io Degraded Performance

完成最後一個作業的成就感還好,比對三個月前的自己,對記憶體、檔案格式、演算法、資料結構、網路有更完整的了解,收穫最多是在 Web Programming 部分,一直以來對 JavaScript 抱持著敬畏的心情,整個課程完成後,雖然還是怕怕的,但在閱讀相關材料時吸收的程度加強了不少,也確定自己沒有喜歡寫前端的部分,意外的收穫。

--

--

陳雁智 (Marat Y. C. Chen)
Manjeaneer

project manager/savvy programmer/marathon runner/critical reader