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 的参数,简单可以很简单,难起来居然也像这样可以独立一篇出来讨论了!
但无论如何,今天讨论的都不是如何把程序写「对」,最上面第一块程序码区块,就已经是可以正常运作的了。
差别只在於,如何在运作正确之余,让程序规模更容易扩充、除错,这些都将是未来设计更大、更复杂程序会遇到的难题。
背好背包
穿越银河
下一站是全然独立的异世界
>>: Day 16 储存宝石:S3 储存类别 & 生命周期管理
安装kubeadmin 在前一篇提到, 後续范例使用到的工具可以自建或使用现成的工具, 会将自建工具...
ListView再练一个 先新增模组: 要V generate...... activity_mai...
前几篇介绍了 WebRTC 是如何连线的,今天我们要开始在浏览器上使用 WebRTC 的 API。 ...
data: { "key": $("#SearchKeyInput&...
此系列文章会同步发文到个人部落格,有兴趣的读者可以前往观看喔。 测试报告对於执行脚本後是很重要的,...