什么是可維護(hù)性的代碼今天我們不聊性能優(yōu)化,只是從后期維護(hù)代碼的角度談?wù)勅绾蝺?yōu)雅的書(shū)寫(xiě)代碼 在開(kāi)發(fā)的過(guò)程中,迭代和維護(hù)是再正常不過(guò)的操作了 那么就必然要閱讀別人的代碼 你有沒(méi)有遇到過(guò)一些尷尬的事情: 1、看不懂別人的代碼,不知從何下手 2、修改一個(gè)功能,得讀兩天代碼,改完發(fā)現(xiàn) bug 最少的時(shí)候是修改以前 3、只是修改了一行代碼,發(fā)現(xiàn)控制臺(tái)報(bào)錯(cuò)好幾十個(gè) ... 如果代碼的可維護(hù)性高了,那么可以避免很多這些問(wèn)題 編寫(xiě)可維護(hù)性高的代碼, 從我做起 ^_^
容易理解: 不需要求助源代碼書(shū)寫(xiě)人員,就能看得懂 符合常識(shí): 代碼書(shū)寫(xiě)的自然通透 容易適配: 當(dāng)數(shù)據(jù)發(fā)生變化的時(shí)候,不至于完全重寫(xiě) 容易擴(kuò)展: 對(duì)于核心功能有可擴(kuò)展性(適當(dāng)利用策略模式) 容易調(diào)試: 當(dāng)出現(xiàn)問(wèn)題的時(shí)候,能給出明確且詳細(xì)的錯(cuò)誤提示,可以直接定位問(wèn)題源
從下面幾點(diǎn)做起:一、代碼可讀性想要好維護(hù), 那么第一任務(wù)就是你寫(xiě)的代碼要讓別人看得懂 因?yàn)槲覀兊拇a,當(dāng)他不運(yùn)行的時(shí)候,就是一個(gè)純文本 想要讓別人看得懂你寫(xiě)的一堆文本,那么就要從一切自定義的內(nèi)容開(kāi)始做起
二、代碼縮進(jìn)for (var i = 0; i < 100; i++) { if (true) { function fn() { for (var j = 0; j < 100; j++) { } } for (var j = 0; j < 100; j++) { } } }
整整齊齊的就是看不懂 for (var i = 0; i < 100; i++) { if (true) { function fn() { for (var j = 0; j < 100; j++) { } } for (var j = 0; j < 100; j++) { } } }
三、注釋在任何一個(gè)語(yǔ)言里面,都是有注釋的 語(yǔ)言規(guī)范里定義注釋?zhuān)皇菫榱俗屇銓W(xué)了玩的,就是為了讓你對(duì)代碼進(jìn)行一些標(biāo)注的 大型代碼塊,和大量變量堆積的地方,都要有清楚的注釋?zhuān)脕?lái)表明這個(gè)代碼塊或者說(shuō)這一堆變量是干什么用的,尤其是函數(shù),盡量做到每一個(gè)函數(shù)的前面都有一個(gè)說(shuō)明注釋。 /* * fn 獲取范圍之間隨機(jī)整數(shù)的函數(shù) * @param {Number} a 范圍開(kāi)始的數(shù)字 * @param {Number} b 范圍結(jié)束的數(shù)字 * @return {Number} 范圍內(nèi)的隨機(jī)整數(shù) */ function fn(a, b) { ... }
每一個(gè)函數(shù)都應(yīng)該有參數(shù)說(shuō)明,是否有返回值,返回值是什么 因?yàn)檫@些內(nèi)容在函數(shù)定義中是不能直觀看到了,需要閱讀代碼才可以 當(dāng)你寫(xiě)明了這些以后,閱讀性就大大提高了 假設(shè),你的函數(shù)塊里面涉及到很復(fù)雜的算法,最好也是在說(shuō)明注釋里面標(biāo)注出來(lái)
當(dāng)你對(duì)于一些瀏覽器問(wèn)題做出的修復(fù),你使用了一些黑科技 四、變量和函數(shù)命名變量的命名和函數(shù)的命名,是最能體現(xiàn)我們自定義的地方 對(duì)于每一個(gè)變量和函數(shù)的命名,我們都盡量準(zhǔn)確的給到一個(gè)語(yǔ)義,不管你是使用 大駝峰 還是 小駝峰,都要保證看到名字就能知道這個(gè)變量或者函數(shù)的意義 從變量來(lái)說(shuō) 1、盡量使用名詞,而不是動(dòng)詞 比如:car / person / show / ...
2、常量來(lái)說(shuō),要使用大寫(xiě)字母來(lái)表示 比如:TEST / BROWSER / ...
3、區(qū)分全局和私有變量,函數(shù)內(nèi)的私有變量我會(huì)以 _ 開(kāi)頭 比如: _this / ...
從函數(shù)來(lái)說(shuō) 1、當(dāng)函數(shù)返回布爾值的時(shí)候, 一般會(huì)以 is 開(kāi)頭 比如:isEnabled() / isSelected() / ...
2、獲取類(lèi)的函數(shù)一般以 get 開(kāi)頭 比如:getUserList() / getUserInfo() / ...
3、設(shè)置類(lèi)的一般使用 set 開(kāi)頭 比如:setName() / setUserInfo() / ...
4、修改類(lèi)的一般使用 update 開(kāi)頭 比如:updateName() / updatePrice() / ...
4、程序處理類(lèi)函數(shù)使用 handler 結(jié)尾 比如:showEditHandler() / submitHandler() / ...
5、盡可能的通過(guò)名字描述清楚函數(shù)的作用,不用擔(dān)心太長(zhǎng),因?yàn)楹笃诖虬ぞ邥?huì)幫我們處理掉的 比如: getUserInfoById() / delGoodsParamsById() / ...
五、變量類(lèi)型透明化因?yàn)?JS 是一個(gè)弱類(lèi)型語(yǔ)言,在定義變量的時(shí)候,不會(huì)限制數(shù)據(jù)類(lèi)型 但是我們?cè)诮o變量賦值的時(shí)候,也要盡可能的做到數(shù)據(jù)類(lèi)型統(tǒng)一 當(dāng)你需要定義一些變量,在后期操作中進(jìn)行賦值的時(shí)候 盡可能在定義的時(shí)候,給一個(gè)初始值表示一下你變量將來(lái)要存儲(chǔ)的數(shù)據(jù)類(lèi)型 比如: var count = 0; var name = ''; var boo = false; var person = null; var todoList = [ ];
如果你實(shí)在不想給一個(gè)初始值 也可以使用注釋的形式表明一下你定義的變量, 將來(lái)存儲(chǔ)的是什么類(lèi)型的數(shù)據(jù) var count /* Number */; var name /* String */; var boo /* Boolean */;
六、代碼書(shū)寫(xiě)習(xí)慣我們要保證一個(gè)良好的代碼書(shū)寫(xiě)習(xí)慣 七、鏈?zhǔn)骄幊痰牧?xí)慣我們來(lái)看一下下面這個(gè)代碼 [ ... ].map(function () { // code ... }).filter(function () { // code ... }).reduce(function () { // code ... })
其實(shí)沒(méi)啥問(wèn)題, 而且也挺好的 更甚至當(dāng)代碼簡(jiǎn)單一些的時(shí)候有人把它寫(xiě)成一行
[ ... ].map(function () { ... }).filter(function () { ... }).reduce(function () { ... })
但是到了后期修改的時(shí)候,問(wèn)題就會(huì)逐步顯示,一旦修改了第一個(gè),那么后面的都有可能會(huì)出現(xiàn)問(wèn)題 而且當(dāng)代碼量過(guò)大的時(shí)候,很難保證你不修改串行了
[ ... ] .map(function () { // code ... }) .filter(function () { // code ... }) .reduce(function () { // code ... })
這樣的話(huà),看起來(lái)會(huì)舒服的多 而且可以利用編輯器的代碼折疊,一個(gè)函數(shù)一個(gè)函數(shù)的來(lái)書(shū)寫(xiě) 八、書(shū)寫(xiě)運(yùn)算符的習(xí)慣很多人喜歡相對(duì)緊湊的書(shū)寫(xiě)結(jié)構(gòu) 比如下面的代碼 if (year%4===0&&year%100!==0||year%400===0) { ... }
很簡(jiǎn)單的一個(gè)判斷閏年的代碼 但是當(dāng)你的運(yùn)算符很緊湊的時(shí)候,那么看起來(lái)就會(huì)比較費(fèi)眼睛 相對(duì)來(lái)說(shuō),我更喜歡在運(yùn)算符兩邊都加上空格 讓結(jié)構(gòu)相對(duì)松散一些,看起來(lái)可能也容易一些 我們也不用擔(dān)心這些空格,后期處理都會(huì)幫我們處理掉的 if ( year % 4 === 0 && year % 100 !== 0 || year % 400 === 0) { ... }
還有一種寫(xiě)法 if ( year % 4 === 0 && year % 100 !== 0 || year % 400 === 0 ) { ... }
這個(gè)適用于條件比較長(zhǎng)的時(shí)候使用看起來(lái)會(huì)更加清晰一些 九、函數(shù)調(diào)用傳遞參數(shù)當(dāng)調(diào)用一個(gè)函數(shù),需要傳遞一個(gè)函數(shù)作為參數(shù)的時(shí)候 我們通常都會(huì)直接書(shū)寫(xiě)一個(gè)匿名函數(shù)或者箭頭函數(shù)在參數(shù)位置 或者說(shuō)傳遞一個(gè)復(fù)雜數(shù)據(jù)類(lèi)型作為參數(shù)的時(shí)候,都會(huì)直接講對(duì)應(yīng)的數(shù)組或者對(duì)象寫(xiě)在參數(shù)位置 比如下面這段代碼
$.get('/xxx', { a: 100, b: 200 }, function (res) { // code ... }, 'json')
代碼沒(méi)有問(wèn)題,但是一旦對(duì)象中數(shù)據(jù)過(guò)多或者函數(shù)中代碼過(guò)多的時(shí)候后期看起來(lái)就會(huì)很復(fù)雜 我會(huì)建議把這些內(nèi)容單獨(dú)書(shū)寫(xiě)出來(lái) var params = { a: 100, b: 200 }
function success(res) { // code ... }
$.get('/xxx', params, success, 'json')
這樣一來(lái), 不管是修改, 還是增加一些內(nèi)容, 都會(huì)比較方便了 十、功能性函數(shù)的單獨(dú)封裝把我們自定義的一些功能性函數(shù)進(jìn)行單獨(dú)的封裝,放在一個(gè)單獨(dú)的 JS 文件中進(jìn)行引入或者導(dǎo)入使用,其實(shí)就是模塊化的概念 十一、松散耦合對(duì)于比較難以閱讀的代碼來(lái)說(shuō),強(qiáng)耦合的代碼是最難閱讀的,JS 代碼本身層面上的耦合我們就不說(shuō)了,大家都應(yīng)該了解面向?qū)ο缶幊毯湍K化編程 十二、HTML 和 JavaScript 的耦合在前端開(kāi)發(fā)中,我們經(jīng)常會(huì)見(jiàn)到有些人寫(xiě)代碼會(huì)把一些簡(jiǎn)單的事件直接寫(xiě)到 html 結(jié)構(gòu)上 <button onclick="doSomething()" ></button> 從代碼層面上來(lái)說(shuō)完全沒(méi)有問(wèn)題但是實(shí)際上,這個(gè)是 HTML 和 JavaScript 的強(qiáng)耦合現(xiàn)象第一: 每次對(duì)于代碼進(jìn)行的修改,都要從 HTML 和 JavaScript 兩個(gè)位置去進(jìn)行修改第二: 代碼引入位置不可變,一定要保證在用戶(hù)點(diǎn)擊之前就已經(jīng)有函數(shù)存在了,不然一定會(huì)報(bào)錯(cuò)的 比較好的方法就是進(jìn)行 HTML 和 JavaScript 的分離 在 .js 文件中獲取 DOM 元素 通過(guò)事件綁定的形式來(lái)完成操作
var btn = document.querySelector('button') btn.addEventListener('click', doSomething)
還有一種情況更常見(jiàn), 就是在 JS 代碼中為了渲染頁(yè)面而進(jìn)行字符串拼接 container.innerHTML = ` <div> ... <p> ... </p> <span> ... </span> </div> `
這個(gè)代碼也是完全沒(méi)有問(wèn)題的,而且大部分同學(xué)都會(huì)這樣書(shū)寫(xiě)代碼,因?yàn)槭r(shí)省力但是這樣的情況,一旦渲染到頁(yè)面上,出現(xiàn)樣式問(wèn)題需要調(diào)整的時(shí)候我們?cè)?HTML 結(jié)構(gòu)中很難找到內(nèi)容來(lái)修改,必須要到 JavaScript 代碼里面去修改如果我們的字符串拼接是在循環(huán)里面完成的話(huà),那么有可能你添加一個(gè)或者刪除一個(gè)標(biāo)簽的時(shí)候,導(dǎo)致整個(gè)頁(yè)面崩潰 比較好的做法 使用一些第三方小腳本或者模板引擎來(lái)進(jìn)行渲染: 比如:art-template / e.js / ... 真的需要這樣渲染的時(shí)候,那么在原始 html 結(jié)構(gòu)中以注釋的形式留下一部分渲染內(nèi)容
<div class="container"> <!-- 商品詳情信息渲染結(jié)構(gòu) <div> ... <p> ... </p> <span> ... </span> </div> --> </div>
十三、CSS 和 JavaScript 的耦合在前端的開(kāi)發(fā)中,使用 JS 來(lái)操作一些元素的樣式,是在常見(jiàn)不過(guò)的事情了 比如我們經(jīng)常會(huì)寫(xiě) ele.style.color = 'red' ; ele.style.display = 'none' ;
這樣書(shū)寫(xiě)代碼其實(shí)沒(méi)有大問(wèn)題 對(duì)于渲染也不會(huì)造成很大的困擾 但是,一旦我們需要修改樣式的時(shí)候,那么就比較麻煩了 因?yàn)橛械臉邮娇赡苄枰?.css 文件內(nèi)修改,有的樣式需要在 .js 文件內(nèi)修改
ele.classList.add('active') ele.classList.remove('active')
這樣做保證了樣式和行為的分離,我們?cè)谡{(diào)整頁(yè)面樣式的時(shí)候,不需要 JS,直接在 CSS 中修改就可以 十四、事件處理 和 應(yīng)用邏輯 的耦合在開(kāi)發(fā)過(guò)程中, 我們經(jīng)常要處理一些事件,并且在事件里面要進(jìn)行一些邏輯的運(yùn)算 比如:我們?cè)邳c(diǎn)擊登錄的時(shí)候,要對(duì)用戶(hù)填寫(xiě)的內(nèi)容進(jìn)行一個(gè)正則的驗(yàn)證,然后提交到服務(wù)器 ele.addEventListener('submit', function () { let username = xxx.value let password = xxx.value // 正則驗(yàn)證 if ( ... ) { ... } if ( ... ) { ... } // 提交到服務(wù)器 var xhr = new XMLHttpRequest() xhr.open( ... ) xhr.send( ... ) xhr.onload = function () { ... } })
這是一段合法的代碼 但是函數(shù)里面包含了太多的內(nèi)容 有事件處理 有邏輯處理 有請(qǐng)求發(fā)送 這樣就相當(dāng)于在一個(gè)函數(shù)里面做了太多的事情 這個(gè)代碼的邏輯運(yùn)算還是比較少的,但是一旦邏輯運(yùn)算多了以后,那么后期閱讀的時(shí)候就很麻煩了
我們可以把里面的邏輯運(yùn)算和請(qǐng)求發(fā)送都單獨(dú)提取出來(lái),變成下面這個(gè)形式: function validateValue(val) { // 正則驗(yàn)證 if ( ... ) { ... } if ( ... ) { ... } // 將驗(yàn)證結(jié)果返回 return true // or false }
function sendAjax() { // 發(fā)送請(qǐng)求的業(yè)務(wù)邏輯 }
ele.addEventListener('submit', function () { let username = xxx.value let password = xxx.value // 正則驗(yàn)證 if (!validateValue( xxx )) return // 提交到服務(wù)器 sendAjax() })
這樣一來(lái),只要我們給函數(shù)寫(xiě)好注釋?zhuān)敲春笃诘臅r(shí)候,哪里出現(xiàn)問(wèn)題,我們可以快速準(zhǔn)確的定位問(wèn)題所在位置 十五、尊重對(duì)象所有權(quán)JavaScript 的動(dòng)態(tài)天性決定了沒(méi)有什么是不能修改的 從代碼層面出發(fā),我們可以修改任何內(nèi)容,包括向 Object 的 prototype 上擴(kuò)展一些方法,,向 Array 的 prototype 上擴(kuò)展一些方法 但是在真實(shí)的企業(yè)級(jí)開(kāi)發(fā)過(guò)程中,我們要絕對(duì)的尊重每一個(gè)對(duì)象的所有權(quán)
不要修改任何不屬于你的代碼,如果某一個(gè)對(duì)象不是由你負(fù)責(zé)創(chuàng)建或者維護(hù),那么你也不要修改他的構(gòu)造函數(shù) 在好久好久以前: 我接觸過(guò)一個(gè)叫做 prototype 的第三方庫(kù) 它里面向 document 對(duì)象上擴(kuò)展了一個(gè)叫做 getElementsByClassName 的方法 是不是看起來(lái)很無(wú)聊,但是在沒(méi)有 getElementsByClassName 的年代,確實(shí)很好用 并且,擴(kuò)展的這個(gè) getElementsByClassName 方法的返回值是一個(gè) Array 并不是我們后來(lái)使用的 NodeList 而且還在實(shí)例身上擴(kuò)展了一個(gè)叫做 each() 的方法,專(zhuān)門(mén)用來(lái)遍歷 我們用起來(lái)的時(shí)候就會(huì)很方便
document.getElementsByClassName('item').each()
這個(gè)很好,而且對(duì)代碼開(kāi)發(fā)進(jìn)行了簡(jiǎn)化 但是,一旦瀏覽器廠商開(kāi)始支持這個(gè)方法了,那么你的方法就會(huì)出現(xiàn)問(wèn)題了 后來(lái),在所有瀏覽器廠商都支持了 getElementsByClassName 以后 當(dāng)在使用這個(gè)方法的時(shí)候,因?yàn)楹驮闹孛?br>會(huì)出現(xiàn)代碼的大面積報(bào)錯(cuò)
這個(gè)就是尊重代碼所有權(quán) 因?yàn)槟悴恢罏g覽器廠商什么時(shí)候會(huì) 告知 或 不告知 的更新一些內(nèi)容,或者修改一些 API 所以,不要修改任何不屬于你的內(nèi)容 十六、盡量不聲明全局變量和尊重對(duì)象所有權(quán)有密切關(guān)系的就是盡可能少的聲明全局變量 拋開(kāi)變量污染的層面不說(shuō),我們的每一個(gè)全局變量其實(shí)都是在向 window 上添加成員 var name = 'Jack' function getInfo() { ... }
這都是全局變量,用起來(lái)也沒(méi)什么問(wèn)題 但是也確實(shí)是在 window 上掛載了兩個(gè)名字
我們?cè)陂_(kāi)發(fā)自己的代碼的時(shí)候, 盡可能的在全局制作一個(gè)命名空間,然后把我們所有需要的內(nèi)容全部放在里面 var myApp = { name: 'jack', getInfo () { ... } }
這樣一來(lái), 我們只是向 window 上掛載了一個(gè) myApp 剩下的所有東西都在我自己的命名空間里面 一旦出現(xiàn)問(wèn)題,你能準(zhǔn)確的知道是你自己定義的變量或者方法出現(xiàn)了問(wèn)題,還是原生的變量或者方法出現(xiàn)了問(wèn)題
這個(gè)也是前端從沒(méi)有模塊化到模塊化開(kāi)發(fā)的演變過(guò)程的原始階段: 獨(dú)立命名空間 IIFE AMD / CMD CommonJS ES6 模塊化
十七、習(xí)慣使用常量我們?cè)陂_(kāi)發(fā)的過(guò)程中,經(jīng)常要使用一些變量來(lái)操作某些內(nèi)容 任何出現(xiàn)一次以上的內(nèi)容,都應(yīng)該提取出來(lái)變成一個(gè)常量的定義 任何一個(gè)需要顯示給用戶(hù)看到的文本內(nèi)容,都應(yīng)該提取出來(lái)變成一個(gè)常量 任何一個(gè)變量,在定義的時(shí)候都要考慮,將來(lái)會(huì)不會(huì)發(fā)生變化,如果不發(fā)生變化,那么就直接定義成常量 包括我們?cè)诓僮饕恍╊?lèi)名的時(shí)候,應(yīng)該把這些類(lèi)名提取出來(lái)做成常量,然后統(tǒng)一操作
這樣一來(lái),我們可以避免因?yàn)椴恍⌒男薷淖兞慷鴮?dǎo)致出現(xiàn)的問(wèn)題,也可以在代碼的各個(gè)部分保證代碼數(shù)據(jù)的統(tǒng)一,避免一個(gè)東西這里修改了,那里沒(méi)有修改的問(wèn)題
|