“程序是写给人读的,只是偶尔让计算机执行一下。” ——Donald Ervin Knuth(高德纳)javascript
咱们经常谈到“素养”一词,是指我的在专业领域内实践训练而成的一种修养,在不一样的领域中有不一样的体现,如在音乐领域中,“音乐素养”是指我的对于音乐的感受程度,对音高节奏的把控,对不一样流派音乐的鉴赏能力等,而在编程领域,也有不一样的素养,反映出对基本功、代码整洁度、专业态度等等方面,所谓“代码素养”,简单来讲,就是指代码写的是否优雅美观可维护。html
绝对完美的代码是不存在的,代码素养并非指完美主义。在翻译领域有“信,达,雅”的标准,“雅”之因此放在最后,是由于要达到它,须要有比较高的水准和经验积累。类比到编程领域,咱们在编程时,第一时间想到的是如何将业务逻辑实现出来,而不是如何把代码优雅地写出来,因此写代码没有所谓的绝对优雅。可是,做为一名专业的前端工程师,确切的说,应该是专业的软件工程师,编写优雅的代码应当是时刻保持的追求,它更像是一个准绳,如同每一个人知道本身该作什么,不应作什么,所谓原则,所谓底线,体现出所谓的“代码素养”
。前端
破窗理论,原义指窗户破损了,建筑无人照管,人们听任窗户继续破损,最终本身也参与破坏活动,在外墙上涂鸦,任垃圾堆积,最后走向倾颓。java
破窗理论在实际中很是容易出现,每每第一我的的代码写的很差,第二我的就会有相似“反正他已经写成这样了,那我也只能这样了”的思想,致使代码越维护越冗杂,最后一刻轰然坍塌,变成无人想去维护的垃圾。android
整洁的代码如同优美的散文,试想读过的一本好书,可以随着做者的笔锋跌宕起伏,充满了画面感,调动了本身的喜怒哀乐。代码虽然没有那样的高潮迭起,但整洁的代码应当充满张力,可以在某一时刻利用这种张力将情节推向高潮。ios
我更喜欢把写代码类比于写文章讲故事,写代码是创做的过程,做者须要将本身想表达的东西经过代码的形式展示出来,而整洁的代码如同讲故事通常,娓娓道来,引人入胜,很差的代码则让人感受毫无头绪,通篇不知道在讲什么。程序员
在现代化的前端开发中,有不少自动化工具能够帮助咱们写出规范的代码,如eslint
,tslint
等各类辅助校验工具,知名的规范如google规范
、airbnb规范
等等也从各个细节方面约束,帮助咱们造成合理规范的代码风格。编程
本小节再也不重复语言层面的代码风格,根据实际重构项目,总结出一系列开发过程当中须要时刻注意的原则,按照重要程度优先级排列。设计模式
相信做为一名软件工程师,你们都据说过最基本的DRY原则,不少设计模式,包括面向对象自己,都是在这条原则上作努力。api
DRY顾名思义,是指“不要重复本身”,它实际上强调了一个抽象性原则,若是一样或相似的代码片断出现了两次以上,那么应该将它抽象成一个通用方法或文件,在须要使用的地方去依赖引入,确保在改动的时候,只需调整一处,全部的地方都改变过来,而不是到每一个地方去找到相应的代码来修改。
在实际工做中,我见过两种在这条原则上各自走向极端的代码:
data.js
的数据处理文件,部分页面中引用了该文件,而更多页面彻底拷贝了该文件中的几个不一样方法代码。而做者的意图则是使人哭笑不得——只用到小部分代码,不必引入那么整个文件。且不论现代化的前端构建层面能够解决这个问题,即便是引入了整个大文件,这部分多余的代码在gzip以后也不会损失多少性能,但这种处处copy的行为带来后续的维护成本是翻倍的。这也是将该原则排在首位的缘由,这种行为致使的重构工做量是最大的,保持良好的代码维护性是一种素养,更是一种责任,若是本身在这方面逃避或偷懒,将把这块工做量翻倍地加在未来别人或本身的身上。
SRP也是一个比较著名的设计原则——单一职责,在面向对象的编程中,认为类应该具备单一职责,一个类的改变更机应当只有一个。
对于前端开发来讲,最须要贯彻的思想是函数应当保持单一职责,一个函数应当只作一件事,这样一来是保证函数的可复用性,更单一的函数有更强的复用性,二来可让总体的代码框架更加清晰,细节都封装在一个个小函数中。另一点也和单一职责有关,就是无反作用的函数,也称纯函数,咱们应当尽可能保证纯函数的数量,非纯函数是不可避免的,但应当尽可能减小它。
把SRP原则排在第二位,由于它很是的重要,没有人愿意看一团乱麻的逻辑,在维护代码时,若是没有一个清晰的逻辑结构,全部的数据定义、数据处理、DOM操做等等一系列细节的代码所有放在一个函数中,致使这个函数很是的冗长,让人本能地产生心理排斥,不肯去查看内部的逻辑。
全部的复杂逻辑放在一个函数中,相信你们看到这样的代码都会眉头一皱:
show: function(a, b) { if (!isInit) { init(); isInit = true; } // reset this.balance = 0; this.isAllBalance = false; var shouldShowLayer = true, preSelectedTermId = 0, needAddress = course.address_state, showTerms, termsObj; var hasPunish = false; this.course = course = course || {}; opt = opt || {}; opt.showMax = opt.showMax || 6; (isIosApp || b.isIAP) && (usekedian = !0, priceSymbol = '<i class="icon-font i-kedian"></i>'), f.splice(b.showMax), layer.show({ $container:b.$container, content:termSelectorTpl({ terms:f, curTermId:b.curTermId || d, name:a.name, hasPunish:h, userInfo:j }, { renderTime:T.render.time.renderCourseTime, renderCourseTime:renderCourseTime, hideUserInfo:b.hideUserInfo, hideTitle:b.hideTitle, hidePayPrice:b.hidePayPrice, confirmText:b.confirmText, sys_time:a.sys_time }), cls:"term-select-new", allowMove:function(a) { return opt.allowMove || ($target.closest('.select-content').length && $('.term-select-new .select-time').height() + $('.term-select-new .select-address').height() + $('.term-select-new .select-discounts').height() > (winWidth > 360 ? 190 : winWidth > 320 ? 175 : 150)); }, afterInit:function(c) { if (needAddress) { that.loadAddress(); // 若是须要地址,且是 app 的话,屏幕可见性切换时须要更新下地址 if (isApp) { $(document).on(visibilityChange, function (e) { // console.log('visibilityChange',document[hidden]); if (!document[hidden]) { // true 参数表示必须刷新 that.loadAddress(true); } }); } } that.afterTermSelect(); $dom.on('click', '.layer-close', function() { setTimeout(function() { !opt.noAutoHide && layer.hide(); }, 100); opt.onCancel && opt.onCancel(); }); $dom.on('click', '.term', function(e) { var $this = $(this); var $terms = $('.term'); if (!$this.hasClass('disabled')) { $terms.removeClass('selected'); $this.addClass('selected'); } that.afterTermSelect(); }); $dom.on('click', '.layer-comfirm', function(e) { var $this = $(this); var termId = $dom.find('.term.selected').data('term-id'); var termName = $dom.find('.term.selected').find('.term-title').html(); var discountId = $dom.find('.discounts-list_item.selected').data('discount-id'); var couId = $dom.find('.discounts-list_item.selected .discounts-coupon').data('cou-id'); var directPay = false; // ios 手Q IAP if (that.toRecharge) { // 须要充值的金额数目 var toRechargePrice = that.curPrice - that.balance; if (isIosApp) { require.async('api', function (api) { api.invoke('api', 'balanceRecharge', { amount: toRechargePrice }); // 充值完成设置回调 api.addEventListener('balanceRechargeCallBack', function(data) { // 支付成功的话 // code=0为成功,其余表示失败 // mode=1表示走充值档位回调,2表示直接充值回调,若是ios 直接充值成功则直接支付 var directPay = data.code === 0 && data.mode === 2; // 执行回调刷新数据 that.toGetBalance(that.course, termId, function() { directPay && $this.trigger('click'); }); }); }); } else { var toRechargePrice = that.curPrice - that.balance; if (that.rechargeMap && Object.keys(that.rechargeMap).indexOf("" + toRechargePrice) > -1) { that.opt.onComfirmClick && that.opt.onComfirmClick(1); iosPay.iosRecharge({ productId: that.rechargeMap[toRechargePrice], count: toRechargePrice, succ: function() { that.toGetBalance(that.course, $('.term.selected').data('term-id')); } }); } else { that.opt.onComfirmClick && that.opt.onComfirmClick(2); // T.jump('/iosRecharge.html?_bid=167&_wv=2147483651'); that.jumpPage('/iosRecharge.html?_bid=167&_wv=2147483651'); } } return; } if (!termId) { require.async(['modules/tip/tip'], function(Tip) { Tip.show(opt.dialogTitle); }); return true; } // check address if (needAddress && !that.addressid) { if (course.must_fill_mailing || !$dom.find('.select-address').hasClass('z-no')) { // 没填地址的话地址框要标红,而后须要滑到视窗让用户看到 var $cnt = $dom.find('.select-content'); var $addressWrap = $dom.find('.select-address_wrapper').addClass('z-err'); var cntRect = $cnt[0].getBoundingClientRect(); var addressBoxRect = $addressWrap[0].getBoundingClientRect(); // console.log('>>>>> ', cntRect, addressBoxRect); if (addressBoxRect.bottom > cntRect.bottom) { $cnt.scrollTop($cnt.scrollTop() + (addressBoxRect.bottom - cntRect.bottom)); } return; } } if (that.isAllBalance && that.opt.onComfirmClick) { that.opt.onComfirmClick(3); } opt.cb && opt.cb(termId, discountId, couId, termName, that.isAllBalance, that.payBalance, that.addressid); setTimeout(function() { !opt.noAutoHide && layer.hide(); }, 300); }); $dom.on('click', '.discounts-list_item', function(e) { var $this = $(this); var $discounts = $('.discounts-list_item'); var isSelected = $this.hasClass('selected'); if (!$this.hasClass('disabled')) { $discounts.removeClass('selected'); $this.addClass(isSelected ? '' : 'selected'); that.setPayPrice(); } }); $dom.on('click', '.address-person .i-edit2, .address-add', function() { var termId = $dom.find('.term.selected').data('term-id'); var courseId = that.course.cid; var src = '/addrEdit.html?_bid=167&_wv=2147483649&ns=1&fr=' + (location.pathname.indexOf('allCourse.html') > -1 ? 4 : location.pathname.indexOf('courseDetail.html') > -1 ? 2 : 3) + '&course_id=' + courseId + '&term_id=' + termId; // T.jump(src); that.jumpPage(src); }).on('click', '.select-address_title .i-right-light', function(e) { var $addressDom = $dom.find('.select-address'); var isOpen = !$addressDom.hasClass('z-no'); if (isOpen) { $addressDom.addClass('z-no'); that.theAddressid = that.addressid; that.addressid = undefined; } else { $addressDom.removeClass('z-no'); that.addressid = that.theAddressid; } }); } }); } else { opt.cb && opt.cb(opt.curTermId || preSelectedTermId); } } 复制代码
单一职责并不必定要经过不少函数来完成,也能够用分段达到目的,如同这样:
show(data) { data && this.setData(data); const renderData = { data: this.data, courseData: this.data.courseData, termList: this.termList, userInfo: this.userInfo, addrList: this.addrList, isIAP: this.isIAP, balance: betterDisplayNum(this.balance), curPrice: betterDisplayNum(this.curPrice), curTermId: this.curTermId, discountList: this.discountList, curDisId: this.curDisId, jdSelectId: this.jdSelectId, curAddrId: this.curAddrId }; const formatters = { // formatters termFormatter, priceFormatter, okBtnFormatter, balanceFormatter, priceFormatterWithDiscount }; console.log('[render data]: ', renderData); const html = payLayerTpl(renderData, formatters); // 记录滚动条位置 this._setScrollTop(); // 防止重复append if (this.$view) { this.$view.replaceWith(html); } else { this.$container.append(html); } afterUIRender(() => { this.$view = $('.' + COMPONENT_NAME).show(); this._setContentHeight(); // 动态设置滚动区域的高度 this._restoreScrollTop(); // 恢复滚动位置 this._initEvent(); this._initCountDown(); // 限时折扣倒计时 }); } 复制代码
虽然这个函数也没有维持单一职责,但经过“分段”的形式清晰的代表了内部的流程逻辑,这样的代码看起来就会比全部细节揉在一个函数中好不少。
对于单一职责来讲,保持起来仍是比较困难的,主要在于职责的拆分,有时过于细致的职责拆分也会给阅读带来比较大的困难,对于这种状况,仍是拿写做来对比,单一职责至关于文章的一个“段落”,对于文章来讲,每一个段落都有它的中心思想,能够用一句话描述出来,若是你发现函数的中心思想很模糊,或者须要不少语言去描述它,那也许它已经有不少个职责该拆分了。
LKP原则是最小知识原则,又称“迪米特”法则,也就是说,一个对象应该对另外一个对象有最少的了解,你内部如何复杂都不要紧,我只关心调用的地方。
保持暴露接口的简介易用性也是API设计的通用规则,在实际中发现了这样的一个UI组件:
module.exports = { show: function(course, opt) { // 此处省略一堆逻辑 }, jumpPage: function(url) { // 此处省略一堆逻辑 }, afterTermSelect: function() { // 此处省略一堆逻辑 }, setPrice: function() { // 此处省略一堆逻辑 }, setBalance: function() { // 此处省略一堆逻辑 }, toGetBalance: function(course, curTermId, cb) { // 此处省略一堆逻辑 }, setDiscounts: function(course, curTermId, curPrice) { // 此处省略一堆逻辑 }, filterDiscounts: function(discounts, curPrice) { // 此处省略一堆逻辑 }, isSuitCoupon: function(cou, curPrice) { // 此处省略一堆逻辑 }, setPayPrice: function() { // 此处省略一堆逻辑 }, setTermTips: function(wording) { // 此处省略一堆逻辑 }, loadAddress: function(needUpdate) { // 此处省略一堆逻辑 }, setAddress: function(addressid) { // 此处省略一堆逻辑 } } 复制代码
这个UI组件暴露了很是多的方法,有业务逻辑,有视图逻辑,还有工具方法,这时会给维护者带来比较大的困扰,本能的觉得这些暴露出去的方法都在被使用,因此想重构其中某些方法都有些很差下手,而实际上,外部调用的方法仅仅是show
而已。
一个好的封装,不管内部多么复杂,它暴露出来的必定是最简洁实用的接口,而内部逻辑是独立维护的,如上述代码,做为一个UI组件来讲,提供最基本的show/hide
方法便可,有必要时可加入update
方法自更新,而无需暴露众多细节,形成调用者和维护者的困扰。
可读性基本定理——“代码的写法应当使别人理解它所需的时间最小化”。
代码风格和原则不是一律而论的,咱们常常须要对一些编码原则和方案进行取舍,例如对于三元表达式的取舍,当咱们以为两种方案都占理时,那么惟一的评判标准就是可读性基本定理,不管写法多么的高超炫技,最好的代码依旧是让人第一时间可以理解的代码。
代码的可读性绝大部分依赖于变量和函数的命名,一个好的名称可以一针见血地帮助维护者理解逻辑,如同写文章中的“文笔”,文笔优异者总能将故事娓娓道来,引人入胜。
不过要起好名称仍是很难的,尤为是咱们不是以英语为母语,更是添加了一层障碍,有些人认为纠结在名称上会致使效率变低,开发第一时间应该完成需求的开发。这样说并无错,咱们在开发过程当中应当专一于功能逻辑,但不要彻底忽视命名,所谓“文笔”是须要锻炼的,思考的越多,命名就会越发的水到渠成,到后来也就不太会影响工做效率了。
在这里推荐鲍勃大叔提到的童子军规,每一次看本身的代码,都进行一次重构,最简单的重构即是更名,也许一开始以为命名还比较贴合,但逻辑越写越不符合初始的命名了,当回顾代码时,咱们能够顺手对变量和方法进行从新命名,现代编辑工具也很容易作到这一点。
文不对题的命名是最可怕的,如:
function checkTimeConflict(opts) { if (opts.param.passcard || (T.bom.get('autopay') && T.bom.get('term_id'))) { selectToPay({ result: {} }, opts); } else { DB.checkTimeConflict({ param: { course_id: opts.param.courseId, term_id: opts.param.termId }, succ: function(data) { selectToPay(data, opts); }, err: function(data) { dealErr(opts, data); } }); } } 复制代码
这个函数被命名为check*
开头的,本意是检测课程时间是否冲突,但内部逻辑却包含了支付整个流程,此时对于调用者来讲,若是不去细看内部逻辑,颇有可能就会错误的认为check
函数没有反作用致使事故发生。
注释是一个比较有争议性的话题,有人认为可读的函数变量就很清晰,不须要额外的注释,且注释有不可维护性,如:
// 1-PC, 2-android手QH5, 3-android APP, 4-ios&非手QH5, 5-IOS APP var platform = isAndroidApp ? 3 : isIosApp ? 5 : 4; 复制代码
实际上,这个字段的含义早已发生了改变,但因为修改者只修改了逻辑,并无注意到这一行注释,致使这个老注释提供了错误信息,此时的注释不只变成了无效注释,甚至会致使维护人的误解,形成bug的产生。
对于这种状况,要么维护注释,要么在注释里面注明接口文档,维护文档,在其余状况下,适当的注释是有必要的,对于复杂的逻辑,若是有一个简练的注释,对于代码可读性的帮助是极大的,但有些没必要要的注释能够去掉,注释的取舍关键在于可读性基本定理,如:
const filterFn = (term) => { if (rule.hideEndTerms && term.is_end) { return false; // 隐藏已结束的期 } if (rule.hideSignEndTerms && term.is_out_of_date) { return false; // 隐藏已结束报名的期 } if (rule.hideAppliedTerms && courseUtil.isTermApplied(term)) { return false; // 隐藏已报名的期 } if (rule.hideZeroAllowedTerms && courseUtil.isTermZero(term)) { return false; // 隐藏名额已满的期 } if (rule.productType === productType.PACKAGE) { return false; // 隐藏课程包的班级 } return true; }; 复制代码
对于上述逻辑来讲,虽然经过变量能够大体猜出功能含义,但一眼看上去就能清晰掌握逻辑结构,归功于注释的简明与清晰。
本文提到的6个代码编写的原则,前三个偏向于代码维护性,后三个偏向于代码可读性,整个可维护性和可读性构成了代码的基本素养。做为一名前端开发工程师,想要拥有良好的代码素养,首先要让本身的代码可维护,不给别人的维护带来巨大的成本和工做量,其次尽可能保证代码的美观可读,整洁的代码人见人爱,如同阅读一本好书,使人心情愉悦。
”代码素养“是一种态度,真正热爱编程的程序员必定不会缺失“代码素养”。咱们一般称“写代码”为“程序设计”
,而不是“程序编写”,“设计”一词体现出了咱们的代码是一件做品,也许不如“艺术品”那么精致,但也不是什么粗麻烂布,若是在写代码时天马行空,得过且过,抱着只要能实现功能的思想,那这部“做品“是不具备观赏价值的,这不只仅体现出代码编写者的”不专业”,更是反映出对待编程这件事的态度,代码的整洁程度、可维护性取决于你是否真正“在乎”你的代码,每一个程序员不必定热爱编程,但请你必定要以“认真”的态度对待本身的专业。
"clean code"
的做者鲍勃大叔提到,有人曾送给他一条腕带,上面写着“Test Obsessed”,他发觉本身带上后再也没法取下了,不只是由于腕带很紧,更是由于它也是一条精神上的紧箍咒。在编程时,咱们下意识的看下本身的手腕,是否能发现一条隐形的腕带呢?