【總結(jié)】1712- 如何寫(xiě)一個(gè)優(yōu)雅的函數(shù)?

對(duì)于程序員而言,相信大家曾經(jīng)都有這樣的經(jīng)歷,要去修改別人的代碼,每次接到這樣的任務(wù),心里都是有苦說(shuō)不出呀,于是乎硬著頭皮上吧,但是看到?jīng)]有任何注釋?zhuān)粋€(gè)函數(shù)好幾百行的代碼時(shí),內(nèi)心更是趨于崩潰,心想還不如自己重寫(xiě)一遍呢。
之所以出現(xiàn)這樣的原因,一方面是因?yàn)榭赡軐?duì)原有的業(yè)務(wù)邏輯并不熟悉,另一方面其實(shí)更多是因?yàn)橹暗拇a寫(xiě)的太爛啦,業(yè)務(wù)邏輯不熟悉,我們找產(chǎn)品,找同事對(duì)一下,梳理一下就清楚啦,但是太爛的代碼會(huì)成本的增加我們的工作量,而且修改完以后,內(nèi)心還是一萬(wàn)個(gè)不放心,生怕又改出新的問(wèn)題。
因此,如何寫(xiě)出更加優(yōu)雅,更加可維護(hù)的代碼,就變的十分重要,要想讓自己的代碼更加優(yōu)雅和整潔,要從命名,函數(shù),注釋?zhuān)袷降榷鄠€(gè)方面去養(yǎng)成良好習(xí)慣,因此,本專(zhuān)欄 代碼整潔之道-理論與實(shí)踐 就是從命名,函數(shù),注釋等多個(gè)方面從理論到實(shí)戰(zhàn)進(jìn)行總結(jié),希望可以讓大家有一個(gè)更加清晰的認(rèn)識(shí)。
這里要強(qiáng)調(diào)兩句話:
- 我們寫(xiě)代碼是給人的,是給我們程序猿自己看的,不是給機(jī)器看的,因此,當(dāng)我們寫(xiě)代碼的時(shí)候,要經(jīng)常思考,我們寫(xiě)下的這段代碼,別人如果此時(shí)看了是否可以比較清晰的理解代碼的含義,如果覺(jué)得不太好理解,是否意味著我們的代碼可以進(jìn)一步優(yōu)化呢,是否意味著我們需要加一些注釋呢。總之,都是為了 代碼能夠讓別人也看得懂。
- 代碼寫(xiě)的好不好,和技術(shù)能力本身不是成正相關(guān)的,也就是說(shuō)代碼要寫(xiě)好,更多的還是要養(yǎng)成良好的習(xí)慣,并且從態(tài)度層面去重視這個(gè)事情,和技術(shù)能力本身沒(méi)有強(qiáng)相關(guān)的關(guān)系,當(dāng)然技術(shù)能力本身也很重要,它可以加成讓我們的代碼可以使用更好的設(shè)計(jì)模式等去組織代碼。
前言
我們平時(shí)項(xiàng)目開(kāi)發(fā)的過(guò)程中,一定會(huì)寫(xiě)各種各樣的函數(shù),說(shuō)到函數(shù),可能第一時(shí)間想到的就是:函數(shù)名,函數(shù)參數(shù),函數(shù)體,函數(shù)返回值。確實(shí)函數(shù)基本就包含以上四部分,但是,每一部分其實(shí)又包含了不少細(xì)節(jié)需要我們?nèi)プ⒁猓@就是本節(jié)需要去討論的事情。
在此之前,自己首先想一下自己在平時(shí)寫(xiě)函數(shù)的時(shí)候,有沒(méi)有想過(guò)以下這些問(wèn)題:
- 函數(shù)名如何去定義才比較規(guī)范?
- 函數(shù)參數(shù)傳幾個(gè)最合適,參數(shù)的前后順序有要求嗎?還是說(shuō)就隨便寫(xiě)啦,哪個(gè)在前哪個(gè)在后無(wú)所謂?
- 函數(shù)體寫(xiě)多少行代碼最合適?自己最多寫(xiě)過(guò)多長(zhǎng)的函數(shù)?
- 返回值該什么時(shí)候加呢?到底什么樣的函數(shù)需要返回?cái)?shù)據(jù)?什么樣的函數(shù)不需要返回?cái)?shù)據(jù)?
之所以問(wèn)這些問(wèn)題,主要就是想強(qiáng)調(diào)一下,要真正寫(xiě)出一個(gè)優(yōu)雅的函數(shù),其實(shí)有很多細(xì)節(jié)需要去注意,而且很多細(xì)節(jié)是我們平時(shí)寫(xiě)的時(shí)候可能就從來(lái)沒(méi)有思考過(guò)的注意點(diǎn),至少我剛開(kāi)始寫(xiě)代碼的時(shí)候,是這樣的,哈哈哈。
那老規(guī)矩,繼續(xù)從以下三方面去闡述:
- 理論篇
- 規(guī)范篇
- 實(shí)戰(zhàn)篇
理論篇
只做一件事兒
顧名思義,我們要保證我們的函數(shù)功能是拆分非常清晰的,每個(gè)函數(shù)都只做一件事兒,當(dāng)發(fā)現(xiàn)該函數(shù)越來(lái)越大時(shí),我們就需要考慮是否可以再進(jìn)一步拆分出多個(gè)子函數(shù),從而保證我們每個(gè)函數(shù)實(shí)現(xiàn)的功能都是只做了一件事兒,這樣函數(shù)也會(huì)更加簡(jiǎn)潔和純粹。
無(wú)副作用
說(shuō)到函數(shù)副作用,大家可能會(huì)想到函數(shù)式編程中的純函數(shù),即保證同樣的輸入每次都有相同的輸出,不能有任何的副作用,純函數(shù)固然是美好的,我們也不用擔(dān)心有其他意想不到的結(jié)果出現(xiàn)。
但是在我們平時(shí)采用vue,react等框架開(kāi)發(fā)時(shí),完全使用純函數(shù)那是不可能的,也做不到。不過(guò)這種思想我們是可以延續(xù)到我們平時(shí)的代碼中的,即我們要盡可能保證一個(gè)函數(shù)是純粹的,這里的純粹不是指純函數(shù),而是只做一件事兒,盡可能去減少副作用。
例如:我們寫(xiě)一個(gè)讀取文件的函數(shù),正常思路也就是三步:讀取-數(shù)據(jù)格式轉(zhuǎn)換-輸出。但是我們卻在該方法中又讀取了數(shù)據(jù)庫(kù),很顯然該函數(shù)的功能就不是只做一件事兒啦。
明確函數(shù)場(chǎng)景
什么是函數(shù)的場(chǎng)景?其實(shí)說(shuō)白了就是用函數(shù)去做什么事情?從我們平時(shí)開(kāi)發(fā)來(lái)說(shuō)無(wú)外乎下面兩種情況:
- 去執(zhí)行某種操作:可能是是連續(xù)的幾個(gè)動(dòng)作的執(zhí)行。
- 去獲取數(shù)據(jù):典型的就是從后端發(fā)送請(qǐng)求獲取數(shù)據(jù)。
如果還不是特別理解,我們換個(gè)角度,從參數(shù)和返回值的角度來(lái)分析:
- 無(wú)參數(shù),無(wú)返回值
- 無(wú)參數(shù),有返回值
- 有參數(shù),無(wú)返回值
- 有參數(shù),有返回值
我們最后再兩者結(jié)合起來(lái)看:
- 如果是執(zhí)行某種操作,一般都是沒(méi)有返回值的,參數(shù)可能有,也可能沒(méi)有,要看該操作是否依賴(lài)其他數(shù)據(jù)。
- 如果是去獲取數(shù)據(jù):一般都是有返回值的,參數(shù)可能有,也可能沒(méi)有.
為什么要說(shuō)這些呢?其實(shí)就是我們寫(xiě)一個(gè)函數(shù)時(shí),要明確去到底是屬于哪種場(chǎng)景,不要混用,例如:下面的函數(shù)
function?set(attr,?val)?{
??this[attr]?=?val;
??if?(this['age']?>?30)?{
????console.log('true')
????return?true;
??}?else?{
????console.log('false')
????return?false;
??}
}
let?person?=?{};
person.set('name',?'kobe');
person.set('age',?41)
復(fù)制代碼
上面的代碼有什么問(wèn)題呢?我們一看到set函數(shù),就會(huì)覺(jué)得該函數(shù)的大概功能是要為某個(gè)數(shù)據(jù)設(shè)置新的屬性和值,正常是沒(méi)有返回值的,結(jié)果我們卻發(fā)現(xiàn)該函數(shù)體中,還有一部分代碼是校驗(yàn)?zāi)挲g,有返回值。很顯然,這段代碼犯了一下兩個(gè)錯(cuò)誤:
- 沒(méi)有只做一件事兒
- 沒(méi)有明確函數(shù)的場(chǎng)景,正常邏輯set函數(shù)一般是不會(huì)有返回值的,而這里卻還返回true/false,這是很迷惑的。
那如何修改呢?
- 按照 函數(shù)只做一件事兒的思想,我們需要把校驗(yàn)?zāi)挲g的邏輯單獨(dú)抽成一個(gè)函數(shù)。
- 修改函數(shù)名,保證其名與其內(nèi)部實(shí)現(xiàn)的功能是一致的。
function?setAndCheckAge(attr,?val)?{
??this[attr]?=?val;
??return?checkAge();
}
function?checkAge(age)?{
??if?(age?>?30)?{
????console.log('true')
????return?true;
??}?else?{
????console.log('false')
????return?false;
??}
}
let?person?=?{};
person.set('name',?'kobe');
person.set('age',?41)
復(fù)制代碼
注意:上面的代碼大家可以不用過(guò)多在意其實(shí)際邏輯是否合理哈,例如:怎么在set方法里校驗(yàn)?zāi)挲g呀,是的,實(shí)際開(kāi)發(fā)中,很可能不會(huì)有這樣的業(yè)務(wù)邏輯,這里只是借助說(shuō)明其思想。
函數(shù)參數(shù)
最理想的情況是參數(shù)是零(零參數(shù)函數(shù)),其次是一(一參數(shù)函數(shù)),再次是二(二參數(shù)函數(shù)),應(yīng)該盡量避免三(三參數(shù)函數(shù)),必須有足夠的理由才可以使用三或者三個(gè)以上的參數(shù)。
因?yàn)閰?shù)越多,各種組合情況也就越多,那么也就意味著函數(shù)內(nèi)部的邏輯會(huì)越復(fù)雜。
對(duì)于函數(shù)參數(shù),總結(jié)了一下以下思想:
盡可能的減少函數(shù)參數(shù)的個(gè)數(shù)。如果有多個(gè)參數(shù),那就涉及到參數(shù)的順序,我們就需要考慮哪些參數(shù)應(yīng)該放在前面,哪些參數(shù)應(yīng)該放在后面。如果參數(shù)確實(shí)特別多,就要考慮是否可以把同類(lèi)型的參數(shù)封裝到一個(gè)參數(shù)對(duì)象中。
別重復(fù)自己
即我們一定要保證代碼的可復(fù)用性,函數(shù)更是重中之重,如果有一些公共的函數(shù),我們一定要單獨(dú)抽象出來(lái)。千萬(wàn)別重復(fù)定義相同功能的函數(shù)。
規(guī)范篇
規(guī)范篇,我們分別從以下幾個(gè)方面去說(shuō)明:
- 函數(shù)聲明
- 函數(shù)參數(shù)
- 函數(shù)調(diào)用
- 箭頭函數(shù)
函數(shù)聲明
【可選】 使用命名的函數(shù)表達(dá)式代替函數(shù)聲明 eslint: func-style
原因:使用函數(shù)聲明的方式會(huì)存在生命提升,也就是說(shuō)在函數(shù)聲明之前調(diào)用也不會(huì)報(bào)錯(cuò)。雖然從語(yǔ)法層面是可以運(yùn)行成功,但是從代碼可讀性以及可維護(hù)性等角度來(lái)考慮的話,這樣的邏輯顯然不符合正常思維,即先聲明后調(diào)用的邏輯。
//?bad?case
function?foo()?{
??//?...
}
//?good?case?
const?foo?=?()?=>?{
??//?...
}
復(fù)制代碼
【必須】 把立即執(zhí)行函數(shù)包裹在圓括號(hào)里。
原因:主要也是從代碼可讀性的角度來(lái)考慮,函數(shù)立即調(diào)用屬于一個(gè)相對(duì)獨(dú)立的單元,外面統(tǒng)一用一層小括號(hào)包裹,更清晰。
//?bad?case
(function()?{
??//?...
})();
//?good?case
(function()?{
??//?...
}())
復(fù)制代碼
【必須】 切記不要在非功能塊中聲明函數(shù) (if, while, 等)。
//?bad?case
if?(flag)?{
??function?foo()?{
????console.log('foo')
??}?
}
//?good?case
let?foo;
if?(flag)?{
??foo?=?()?=>?{
????console.log('foo')
??}
}
復(fù)制代碼
【推薦】 永遠(yuǎn)不要使用函數(shù)構(gòu)造器來(lái)創(chuàng)建一個(gè)新函數(shù)。
//?bad?case
let?foo?=?new?Function('a',?'b',?'return?a?+?b');
復(fù)制代碼
【必須】 函數(shù)聲明語(yǔ)句中需要空格
//?bad?case?
const?a?=?function(){};
const?b?=?function?(){};
const?c?=?function()?{};
function?d?()?{
??//?...
}?
//?good
const?a?=?function?()?{};
const?b?=?function?a()?{};
function?c()?{
??//?...
}
復(fù)制代碼
函數(shù)參數(shù)
【必須】 永遠(yuǎn)不要給一個(gè)參數(shù)命名為 arguments。這將會(huì)覆蓋函數(shù)默認(rèn)的 arguments 對(duì)象。
//?bad?case
function(arguments)?{
??//?...
}
//?good?case
function(args)?{
??//?...
}
復(fù)制代碼
【推薦】 使用 rest 語(yǔ)法 ... 代替 arguments
這里,主要是說(shuō)明,如何獲取arguments的參數(shù)。
- Array.prototype.slice.call(arguments)
- ...arguments 【推薦】
//?bad?case
function?foo()?{
??const?args?=?Array.prototype.slice.call(arguments);
??return?args.join('');
}
//?good?case
function?foo(...args)?{
??return?args.join('');
}
復(fù)制代碼
【推薦】 使用默認(rèn)的參數(shù)語(yǔ)法,而不是改變函數(shù)參數(shù)。
這里主要是想說(shuō)明,如何給參數(shù)設(shè)置默認(rèn)值,方法其實(shí)有很多種:
- 判斷參數(shù)是否為空,然后手動(dòng)賦值一個(gè)默認(rèn)值
- 使用默認(rèn)值語(yǔ)法【推薦】
//?bad?case
function?foo(options)?{
??if?(!options)?{
????options?=?{};
??}
}
//?bad?case
function?foo(options)?{
??options?=?options?||?{};
??//?...
}
//?good?case
function?foo(options?=?{})?{
??//?...
}
復(fù)制代碼
但是要注意:設(shè)置默認(rèn)值的時(shí)候,一定要避免副作用。例如:
let?opts?=?{};
function?foo(options?=?opts)?{
??//?...
}
opts.name?=?'kobe';
opts.age?=?41;
復(fù)制代碼
說(shuō)明:上面這個(gè)case就是說(shuō),雖然使用了參數(shù)默認(rèn)值,但是該默認(rèn)值引用的是外部的一個(gè)引用對(duì)象,很顯然,這是存在副作用的,因?yàn)橥獠康膶?duì)象隨時(shí)可能會(huì)變化。一旦變化,就會(huì)導(dǎo)致我們的默認(rèn)值也會(huì)改變。因此這些寫(xiě)法是有問(wèn)題的,避免使用!
【推薦】 總是把默認(rèn)參數(shù)放在最后。
//?bad?case
function?foo(options?=?{},?name)?{
??//?...
}
//?good?case
function?foo(name,?options?=?{})?{
??//?...
}
復(fù)制代碼
【推薦】 不要改變?nèi)雲(yún)ⅲ膊粚?duì)參數(shù)進(jìn)行重新賦值. eslint: no-param-reassign
原因:當(dāng)我們把一個(gè)變量當(dāng)作參數(shù)傳入函數(shù)以后,如果在函數(shù)內(nèi)部對(duì)該變量又重新賦值或者修改,會(huì)直接導(dǎo)致該變量發(fā)生變化,那其他地方如果引用了該變量,很可能造成意想不到的問(wèn)題。(注意:這里的變量主要是指的是引用數(shù)據(jù)類(lèi)型,因?yàn)榛A(chǔ)數(shù)據(jù)類(lèi)型當(dāng)作函數(shù)參數(shù)時(shí)會(huì)直接copy一份)
//?bad?case
function?foo(a)?{
??a?=?1;
}
//?good?case
function?foo(a)?{
??let?b?=?a?||?1;
}
復(fù)制代碼
說(shuō)明:因?yàn)槲覀冊(cè)谡{(diào)用的時(shí)候,不確定傳入的a是引用數(shù)據(jù)類(lèi)型,還是基本數(shù)據(jù)類(lèi)型,所以一律要求不對(duì)入?yún)⑦M(jìn)行修改, 但是此時(shí)可能會(huì)有一個(gè)疑問(wèn)?因?yàn)樵趈s修改入?yún)⒌膱?chǎng)景還是挺多的,典型的就是:遍歷一個(gè)列表,手動(dòng)添加索引或者標(biāo)識(shí)位等。例如:下面的代碼:
const?list?=?[];
list.forEach((item,?index)?=>?{
??item.index?=?index;
??item.isShow?=?index?>?2;
})
復(fù)制代碼
以上代碼其實(shí)還是比較常見(jiàn)的,如果遇到這種情況,eslint會(huì)提示 no-param-reassign。怎么解決呢?
- 在當(dāng)前代碼出關(guān)閉該規(guī)則校驗(yàn),注意:不是全局關(guān)閉,因?yàn)榇蠖鄶?shù)場(chǎng)景下,還是不建議對(duì)入?yún)⑦M(jìn)行修改的。
- 使用深克隆,先拷貝一份item出來(lái),再對(duì)其進(jìn)行修改,然后再return新的item。
函數(shù)調(diào)用
【推薦】 優(yōu)先使用擴(kuò)展運(yùn)算符 ... 來(lái)調(diào)用可變參數(shù)函數(shù)
//?good?case
console.log(...[1,?2,?3,?4]);
復(fù)制代碼
箭頭函數(shù)
【推薦】 當(dāng)你必須使用匿名函數(shù)時(shí) (當(dāng)傳遞內(nèi)聯(lián)函數(shù)時(shí)), 使用箭頭函數(shù)。
//?bad?case
[1,?2,?3].map(function?(x)?{
??const?y?=?x?+?1;
??return?x?*?y;
});
//?good?case
[1,?2,?3].map((x)?=>?{
??const?y?=?x?+?1;
??return?x?*?y;
});
復(fù)制代碼
【推薦】只有一個(gè)參數(shù)是可以不使用括號(hào),超過(guò)一個(gè)參數(shù)使用括號(hào)
//?bad?case
[1,?2,?3].map((item)?=>?item?+?1);
//?good?case
[1,?2,?3].map(item?=>?item?+?1);
復(fù)制代碼
【推薦】當(dāng)函數(shù)體是一個(gè)沒(méi)有副作用的表達(dá)式組成時(shí),刪除大括號(hào) 和return,否則保留。
//?bad?case
[1,?2,?3].map(item?=>?{
??return?item?+?1;
})
//?good?case
[1,?2,?3].map(item?=>?item?+?1)
復(fù)制代碼
同時(shí),也要注意,如果表達(dá)式中包含>=,<=等比較運(yùn)算符時(shí),推薦使用圓括號(hào)隔離一下,因?yàn)樗麄兒图^函數(shù)符號(hào)=>容易混淆。
//?bad?case?
[1,?2,?3].map(item?=>?item?>=?1???1?:?0)
//?good?case
[1,?2,?3].map(item?=>?(item?>=?1???1?:?0));
復(fù)制代碼
實(shí)戰(zhàn)篇
通過(guò)理論篇和規(guī)范篇,我們基本已經(jīng)了解到了,寫(xiě)好一個(gè)函數(shù),有哪些需要注意的地方,其中有一個(gè)點(diǎn)十分重要:明確函數(shù)場(chǎng)景,換句話說(shuō),明確函數(shù)什么時(shí)候該有參數(shù),什么時(shí)候該有返回值?
針對(duì)這一點(diǎn),這里再多強(qiáng)調(diào)一下,因?yàn)槠綍r(shí)寫(xiě)代碼的時(shí)候,確實(shí)會(huì)寫(xiě)很多函數(shù),也遇到很多看起來(lái)不是特別整潔,清晰的函數(shù),這里我們從一個(gè)實(shí)際例子出發(fā)再進(jìn)一步說(shuō)明一下:
例如:我們要從后端獲取表格數(shù)據(jù),渲染出來(lái),但是,后端返回的數(shù)據(jù)不符合表格的格式,需要我們手動(dòng)轉(zhuǎn)換一下,于是,我們很容易寫(xiě)出下面這樣的代碼:
let?rawData?=?[];
let?tableData?=?[];
const?transformRawData?=?()?=>?{
????tableData?=?rawData.map(item?=>?{
????????//?經(jīng)過(guò)一系列處理...
????});
}
const?render?=?()?=>?{
??rawData?=?await?fetchRawData();
??transformRawData();
}
復(fù)制代碼
以上代碼,有什么問(wèn)題呢?問(wèn)題還是出在 transformRawData 方法上,顧名思義,該方法的主要作用就是轉(zhuǎn)換數(shù)據(jù),那么也就意味著應(yīng)該有一個(gè)入?yún)ⅲ瑫r(shí)轉(zhuǎn)換之后的結(jié)果,也應(yīng)該體現(xiàn)在返回值上,所以也應(yīng)該有返回值。而我們現(xiàn)在沒(méi)有這樣處理,而是直接依賴(lài)全局變量,直接進(jìn)行轉(zhuǎn)換。
雖然功能上沒(méi)有什么問(wèn)題,但是從代碼層面是可以進(jìn)一步優(yōu)化的。
let?rawData?=?[];
let?tableData?=?[];
const?transformRawData?=?(data)?=>?{
????return?data.map(item?=>?{
????????//?經(jīng)過(guò)一系列處理...
????});
}
const?render?=?async?()?=>?{
??rawData?=?await?fetchRawData();
??tableData?=?transformRawData(rawData);
}
復(fù)制代碼
改成這樣以后,transformRawData就變成了一個(gè)更加純粹的函數(shù)。不依賴(lài)全局變量,它的作用就是對(duì)傳入的參數(shù)進(jìn)行數(shù)據(jù)格式轉(zhuǎn)換,轉(zhuǎn)換之后,返回新的數(shù)據(jù)。
總結(jié)
相信通過(guò)本節(jié)的學(xué)習(xí),大家對(duì)函數(shù)如何去寫(xiě)有了進(jìn)一步的認(rèn)識(shí),最后,我們?cè)趶?qiáng)調(diào)兩點(diǎn):
保證函數(shù)只做一件事兒,減少其副作用明確函數(shù)的使用場(chǎng)景【注意:這一點(diǎn)其實(shí)是平時(shí)寫(xiě)代碼時(shí)常犯的錯(cuò)誤】
同時(shí),結(jié)合aslant,prettier等格式化工具,對(duì)函數(shù)定義的格式進(jìn)行進(jìn)一步的校驗(yàn),希望大家一起在2022年,能夠把函數(shù)寫(xiě)的越來(lái)越好呀,一起加油!
-
本文作者:沉默抒懷者
-
本文鏈接:https://juejin.cn/post/7061842017487159333
往期回顧
#
如何使用 TypeScript 開(kāi)發(fā) React 函數(shù)式組件?
# #6 個(gè) Vue3 開(kāi)發(fā)必備的 VSCode 插件
# #6 個(gè)你必須明白 Vue3 的 ref 和 reactive 問(wèn)題
#6 個(gè)意想不到的 JavaScript 問(wèn)題
#試著換個(gè)角度理解低代碼平臺(tái)設(shè)計(jì)的本質(zhì)
回復(fù)“加群”,一起學(xué)習(xí)進(jìn)步
