Array 跟 Object 兩兄弟的故事告一段落了,接著是 Object 在外面養(?)的另外一個兄弟 - function
浪漫一點來說,function 其實就像是另一個平行宇宙,從 function 的呼叫處一躍而下,穿越蟲洞到了另一個與世隔絕的小空間,而在這個空間發生的事、定義的變數,(基本上)不會影響到原來的時空。
所以我稱進入 function 是一場「時空旅行」,好啦可能時間還是照常流動(同步),不過起碼空間是換了一個 context。
今天我們先來聊聊,要進入時空旅行之前,總是要帶一些乾糧或行李,我們把它稱作:
蛤?function 的參數太基本了吧!呼叫的時候一個一個丟進去,使用的時候一個一個取出來,同學你不是來騙讚的吧!?
嘿啊,我也知道 function 帶參數是基本中的基本了,所以這個問題應該改成:
參數怎麼帶比較「好」
我們舉個例會比較清楚:
// 查詢網銀交易的紀錄
/*
* userId : 使用者 id
* startDate : 查詢區間開始
* endDate : 查詢區間結束
* searchKeyword: 搜尋備註關鍵字
* type : 'deposit'(存款) 或 'withdraw'(提款)
*/
const queryTransaction = (userId, startDate, endDate, searchKeyword, type) => {
// 實作到 DB query 資料的部分
};
queryTransaction('612b06609ea3b35614c0edbd', '2021-09-16', '2021-10-15', '同事代墊', 'deposit');
上面的例子可以看出,當一個 function 的帶入參數很多時,很容易會遇到這些問題:
稍微改進一下會變這樣:
// 查詢網銀交易的紀錄
/*
* userId : 使用者 id
* startDate : 查詢區間開始
* endDate : 查詢區間結束
* searchKeyword: 搜尋備註關鍵字
* type : 'deposit'(存款) 或 'withdraw'(提款)
*/
const queryTransaction = ({ userId, startDate, endDate, searchKeyword, type }) => {
// 實作到 DB query 資料的部分
};
queryTransaction({
userId: '612b06609ea3b35614c0edbd',
startDate: '2021-09-16',
endDate: '2021-10-15',
searchKeyword: '同事代墊',
type: 'deposit'
});
可以發現是在函式定義的地方做了小改進,把原本的 5 個參數,塞成一個 object,變成 1 個參數,再透過 object destructuring 變回 5 個,函式定義的地方如果看不太懂,可以這樣理解:
const queryTransaction = ({ userId, startDate, endDate, searchKeyword, type }) => {
// 實作到 DB query 資料的部分
};
// 上下兩段是一樣的效果
const queryTransaction = (option) => {
const { userId, startDate, endDate, searchKeyword, type } = option;
// 實作到 DB query 資料的部分
};
如果是上述的版本,因為是帶入一個 object 當作參數
userId
搬到 type
的後面,也不用去改函式呼叫的地方。以上三點就把剛才上面提到的三個問題都解決了,變成更有彈性,且即便後人接手 refactor,也比較不會因為參數的增減、順序而造成 bug。
可以想像我現在是帶著一個後背包出門,裡面裝了我所有的旅行用品,但外表看起來就是一個後背包,所以裡面少帶了什麼其實不一定會發現
所以我們把後背包裡面,比較重要的東西拿出來握在手上,告訴自己一定要手上有東西才可以出門,有點像是出門先喊「手機、鑰匙、錢包!」一樣
因此這邊可以再做一個小優化,因為上述提到的三個問題,都比較是因為參數過多,而且有一些其實是選填參數所造成的。
因此我可以只把選填參數包成 object,而像 userId
這種肯定是必填的欄位,要避免其它同事不小心漏掉,就可以直接用原本的方式放在前面:
const queryTransaction = (userId, option) => {
const { startDate, endDate, searchKeyword, type } = option;
// 實作到 DB query 資料的部分
};
const option = {
startDate: '2021-09-16',
endDate: '2021-10-15',
searchKeyword: '同事代墊',
type: 'deposit'
};
queryTransaction(
'612b06609ea3b35614c0edbd',
option
);
用 fetch 發送 request 的時候,必要的欄位是 URL,其它都放在 option
const fetchOption = {
method: 'POST',
body: JSON.stringify(data),
headers: {
'user-agent': 'Mozilla/4.0 MDN Example',
'content-type': 'application/json'
}
};
fetch('這裡是 URL', fetchOption)
.then(response => response.json())
用 mongoose 連線 DB 時,必要的欄位是 URL,其它都放在 option
const mongooseOption = {
ssl: true,
autoIndex: true,
serverSelectionTimeoutMS: 5000,
};
mongoose.createConnection('這裡是 URL', mongooseOption);
用 Fuse 搜尋資料時,必要的欄位是 dataList,其它都放在 option
const fuseOption = {
isCaseSensitive: true,
threshold: 0.6,
distance: 100,
keys: ['name', 'price', 'note'],
};
new Fuse([這裡放 dataList], fuseOption);
以上都是關於帶入參數的方式,進行了一些優化,過程中也發現這也不是唯一解,因為關於「參數過多」,有時候是因為這個 function 「能做的事太多了」。
可以把 function 切割成多個「目標不同」的 function,重新給予命名,然後根據需要狀況呼叫,就可以不用那麼多參數了,這點可以過兩天再來討論。
沒錯,時空旅行就是單純去旅行就好,背包裡的行李不要變質。。。
翻成白話就是,「不會有任何一個參數,會因為執行了這個 function,而產生任何變化」。
這是之後會提到的 Functional Programming 的其中一個重點,也就是避免像下面這種 side effects:
const arr = ['Jack', 'Allen', 'Alice', 'Susan'];
const sortArr = () => {
arr.sort();
};
console.log(arr);
sortArr();
console.log(arr);
執行結果
['Jack', 'Allen', 'Alice', 'Susan']
["Alice", "Allen", "Jack", "Susan"]
這種狀況通常會在 function 最開頭,先把參數拷貝一份,然後用拷貝的資料來修改,保持正本 read-only:
const arr = ['Jack', 'Allen', 'Alice', 'Susan'];
const sortArr = () => {
const copiedArr = [ ...arr ];
copiedArr.sort();
};
console.log(arr);
sortArr();
console.log(arr);
執行結果
['Jack', 'Allen', 'Alice', 'Susan']
['Jack', 'Allen', 'Alice', 'Susan']
這個問題只會出現在參數是 non-primitive 的時候。
因為如果是把 primitive 變數當作參數,會是 call by value 的方式,丟一個拷貝後的副本進去 function,不管怎麼改都不會影響到正本。
而 non-primitive 則是 call by reference,會直接把正本丟到 function 裡面,如果改了就會連帶影響外面。
主要是為了避免不可預期的 bug,因為如果每次執行這個 function 都會造成外面的變數變化,那代表如果這個 function 出現問題寫錯了,就會造成其它地方也著火,到時會很難釐清問題點到底在哪,對於 unit testing 也是相當不利的。
前面提到,不要修改參數的原因是怕「不可預期」的狀況,反之,如果是「可預期」的,那麼直接修改參數其實是效能更好的哦!
最常見的例子就是前兩天介紹的 Array 組合技 reduce
,當初要做「Array 轉換成 Object」的例子時,就直接塞新的 property 給 reduce
內的那個 prev (第7行):
const arr = [
{ id: 'item1', name: 'TV', price: 13500 },
{ id: 'item2', name: 'washing machine', price: 8200 },
{ id: 'item3', name: 'laptop', price: 25000 },
];
const resultObject = arr.reduce((prev, curr) => {
prev[curr.id] = curr;
return prev;
}, {});
console.log(resultObject);
可以直接進行參數修改的原因是,reduce
裡面的這個 function,本身是「可預期的」,因為我們已經把初始值定為 {}
,所以不管這個 reduce
執行多少遍,裡面的 function 都是從 {}
開始跑,永無例外。
在這種非常確定可以直接修改的情況下,「直接修改」比起「先拷貝再修改」的效能還快得多,因為拷貝本身真的很吃效能(尤其 deep copy),有興趣可以參考這篇,有實測數據可以參考。
function 的參數,簡單可以很簡單,難起來居然也像這樣可以獨立一篇出來討論了!
但無論如何,今天討論的都不是如何把程式寫「對」,最上面第一塊程式碼區塊,就已經是可以正常運作的了。
差別只在於,如何在運作正確之餘,讓程式規模更容易擴充、除錯,這些都將是未來設計更大、更複雜程式會遇到的難題。
背好背包
穿越銀河
下一站是全然獨立的異世界
還有一種是收集剩下的參數 :p
const myFun = (first, second, ...rest) => {
console.log(first, second, rest)
}
myFun(1,2,3,4,5,6) // 1 2 [3, 4, 5, 6]
沒錯~~~當函式被呼叫的時候,會產生一個帶有索引特性的 arguments 物件,而且可以用 Array.from() 轉成陣列。