简单了解JavaScript中常见的反模式


Posted in Javascript onJune 21, 2019

前言

反模式 是指对反复出现的设计问题的常见的无力而低效的设计模式,俗话说就是重蹈覆辙。 这篇文章描述了 JavaScript 中常见的一些反模式,以及避免它们的办法。

硬编码

硬编码(Hard-Coding)的字符串、数字、日期…… 所有能写死的东西都会被人写死。 这是一个妇孺皆知的反模式,同时也是最广泛使用的反模式。 硬编码中最为典型的大概是 平台相关代码(Platform-Related), 这是指特定的机器或环境下才可以正常运行的代码, 可能是只在你的机器上可以运行,也可能是只在 Windows 下可以运行。

例如在 npm script 中写死脚本路径 /Users/harttle/bin/fis3, 原因可能是安装一次非常困难,可能是为了避免重复安装,也可能仅仅是因为这样好使。 不管怎样,这会让所有同事来找你问“为什么我这里会报错”。 解决办法就是把它放到依赖管理,如果有特定的版本要求可以使用 package-lock,如果实在搞不定可以视为外部依赖可以放到本地配置文件并从版本控制(比如 Git) 移除。

例如在 cli 工具中写死特殊文件夹 /tmp, ~/.cache,或者路径分隔符 \\ 或 /。 这类字符串一般可以通过 Node.js 内置模块(或其他运行时 API)来得到, 比如使用 os.homedir, os.tmpdir, path.sep 等。

重复代码

重复代码(Duplication)在业务代码中尤为常见,初衷几乎都是维护业务的稳定性。 举个例子:在页面 A 中需要一个漂亮的搜索框,而页面 B 中恰好有一个。 这时程序员小哥面临一个艰难的选择(如果直接拷贝还会有些许感到不安的话):

  • 把 B 拷贝一份,改成 A 想要的样子。
  • 把 B 中的搜索框重构到 C,B 和 A 引用这份代码。

由于时间紧迫希望早点下班,或者由于改坏 B 需要承担责任 (PM:让你做 A 为啥 B 坏了?回答这个问题比较复杂,这里先跳过), 经过一番思考后决定采取方案 2。

至此整个故事进行地很自然也很顺利,这大概就是重复代码被广泛使用的原因。 这个故事中有几点需要质疑:

  • B 这么容易被改坏,说明 B 的作者 并未考虑复用。这时不应复用 B 的代码,除非决定接手维护它。
  • B 改坏的责任不止程序员小哥:B 的作者是否有 编写测试,测试人员是否 回归测试 B 页面?
  • 时间紧迫不必然导致反模式的出现,不可作为说服自己的原因。短期方案也存在优雅实现。

解决办法就是:抽取 B 的代码重新开发形成搜索框组件 C,在 A 页面使用它。 同时提供给日后的小伙伴使用,包括敦促 B 的作者也迁移到 C 统一维护。

假 AMD

模块化本意是指把软件的各功能分离到独立的模块中,每个模块包含完整的一个细分功能。 在 JavaScript 中则是特指把脚本切分为独立上下文的,可复用的代码单元。

由于 JavaScript 最初作为页面脚本,存在很多引用全局作用域的语法,以及不少基于全局变量的实践方式。 比如 jQuery 的 $, BOM 提供的 window,省略 var 来定义变量等。 AMD 是 JavaScript 社区较早的模块化规范。这是一个君子协定,问题就出在这里。 有无数种方式写出假的 AMD 模块:

  • 没有返回值。对,要的就是副作用。
  • define 后直接 require。对,要的就是立即执行。
  • 产生副作用。修改 window 或其他共享变量,比如其他模块的静态属性。
  • 并发问题。依赖关系不明容易引发并发问题。

全局副作用的影响完全等同于全局变量,几乎有全局变量的所有缺点: 执行逻辑不容易理解;隐式的耦合关系;编写测试困难。下面来一个具体的例子:

// file: login.js
define('login', function () {
fetch('/account/login').then(x => {
window.login = true
})
})
require(['login'])

这个 AMD 模块与直接写在一个 <script> 并无区别,准确地说是更不可控(requirejs 实现是异步的)。 也无法被其他模块使用(比如要实现注销后再次登录),因为它没返回任何接口。 此外这个模块存在并发问题(Race Condition):使用 window.login 判断是否登录不靠谱。

解决办法就是把它抽象为模块,由外部来控制它的执行并获得登录结果。 在一个模块化良好的项目中,所有状态最终由 APP 入口产生, 模块间共享的状态都被抽取到最近的公共上级。

define(function () {
return fetch('/account/login')
.then(() => true)
.catch(e => {
console.error(e)
return false
}
})

注释膨胀

注释的初衷是让读者更好的理解代码意图,但实践中可能恰好相反。直接举一个生活中的例子:

// 判断手机百度版本大于 15
if (navigator.userAgent.match(/Chrome:(\d+))[1] < 15) {
// ...
}

哈哈当你读到这一段时,相信上述注释已经成功地消耗了你的时间。 如果你第一次看到这样的注释可能会不可思议,但真实的项目中多数注释都是这个状态。 因为维护代码不一定总是记得维护注释,况且维护代码的通常不止一人。 C 语言课程的后遗症不止变量命名,“常写注释”也是一个很坏的教导。

解决办法就是用清晰的逻辑来代替注释,上述例子重新编写后的代码如下:

if (isHttpsSupported()) {
// 通过函数抽取 + 命名,避免了添加注释
}
function isHttpsSupported() {
return navigator.userAgent.match(/Chrome:(\d+))[1] < 15
}

函数体膨胀

“通常”认为函数体膨胀和全局变量都是算法课的后遗症。 但复杂的业务和算法的场景确实不同,前者有更多的概念和操作需要解释和整理。 整理业务逻辑最有效的手段莫过于变量命名和方法抽取(当然,还要有相应的闭包或对象)。

但在真实的业务维护中,保持理性并不容易。 当你几十次进入同一个文件添加业务逻辑后,你的函数一定会像懒婆娘的裹脚布一样又臭又长:

function submitForm() {
var username = $('form input#username').val()
if (username === 'harttle') {
username = 'God'
} else {
username = 'Mortal'
if ($('form input#words').val().indexOf('harttle')) {
username = 'prophet'
}
}
$('form input#username').val(username)
$('form').submit()
}

这只是用来示例,十几行还远远没有达到“又臭又长”的地步。 但已经可以看到各种目的的修改让 submitForm() 的职责远不止提交一个表单。 一个可能的重构方案是这样的:

function submitForm() {
normalize()
$('form').submit()
}
function normalize() {
var username = parseUsername(
$('form input#username').val(),
$('form input#words').val()
)
$('form input#username').val(username)
}
function parseUsername(username, words)
if (username === 'harttle') {
return 'God'
}
return words.indexOf('harttle') ? 'prophet' : 'Mortal'
}

在重构后的版本中,我们把原始输入解析、数据归一化等操作分离到了不同的函数, 这些抽离不仅让 submitForm() 更容易理解,也让进一步扩展业务更为方便。 比如在 normalize() 方法中对 input#password 字段也进行检查, 比如新增一个 parseWords() 方法对 input#words 字段进行解析等等。

总结

常见的反模式还有许多,比如 == 和 != 的使用;扩展原生对象;还有 Promise 相关的 等等。

== 要提一下。这是语言的设计缺陷通常使用 Lint 工具来避免使用。与其他 Lint 错误不同的是一旦开始大面积使用后续改正十分困难(因为与 === 确实不等价)。因此强烈建议项目初始化时就添加 Lint。

这些反模式的流行背后都存在很有说服力的原因, 但反模式对可维护性和软件的长期发展有着更为严重的影响。 按照 技术债务 的说法, 每次选择捷径都会产生隐含的代价,而这些代价在将来的某一时刻总要偿还。 那些推迟的重构不仅会影响下一次变更,而且会像经济债务一样持续地叠加利息。

虽然不存在一种具体的考核方法来计算债务大小, 但可以肯定的是如果你能熟练使用 Harttle 博客中总结的各种反模式, 一定能达到每次代码提交债务大于收益的境界。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
jquery预览图片实现鼠标放上去显示实际大小
Jan 16 Javascript
手机号码,密码正则验证
Sep 04 Javascript
text-align:justify实现文本两端对齐 兼容IE
Aug 19 Javascript
jQuery实现多级联动下拉列表查询框
Jan 18 Javascript
JavaScript学习笔记之数组去重
Mar 23 Javascript
angular+bootstrap的双向数据绑定实例
Mar 03 Javascript
js实现4个方向滚动的球
Mar 06 Javascript
jquery.uploadifive插件怎么解决上传限制图片或文件大小问题
May 08 jQuery
js实现可以点击收缩或张开的悬浮窗
Sep 18 Javascript
实现jquery放大镜的两种方法
Feb 22 jQuery
node.js中path路径模块的使用方法实例分析
Feb 13 Javascript
Vue父子传递实例讲解
Feb 14 Javascript
通过图带你深入了解vue的响应式原理
Jun 21 #Javascript
10种JavaScript最常见的错误(小结)
Jun 21 #Javascript
微信小程序开发注意指南和优化实践(小结)
Jun 21 #Javascript
使用Vue开发自己的Chrome扩展程序过程详解
Jun 21 #Javascript
如何测量vue应用运行时的性能
Jun 21 #Javascript
Vue组件之高德地图地址选择功能的实例代码
Jun 21 #Javascript
如何提升vue.js中大型数据的性能
Jun 21 #Javascript
You might like
php下载远程文件类(支持断点续传)
2008/11/14 PHP
php 分页函数multi() discuz
2009/06/21 PHP
php中禁止单个IP与ip段访问的代码小结
2012/07/04 PHP
php中的PHP_EOL换行符详细解析
2013/10/26 PHP
PHP编译安装时常见错误解决办法
2015/05/28 PHP
使用xampp搭建运行php虚拟主机的详细步骤
2015/10/21 PHP
PHP 出现 http500 错误的解决方法
2021/03/09 PHP
javascript removeChild 使用注意事项
2009/04/11 Javascript
xml文档转换工具,附图表例子(hta)
2010/11/17 Javascript
网页中可关闭的漂浮窗口实现可自行调节
2013/08/20 Javascript
js数组转json并在后台对其解析具体实现
2013/11/20 Javascript
jquery实现超简洁的TAB选项卡效果代码
2015/08/28 Javascript
Angularjs中的ui-bootstrap的使用教程
2017/02/19 Javascript
详解nodejs微信公众号开发——6.自定义菜单
2017/04/13 NodeJs
Vue响应式原理详解
2017/04/18 Javascript
Vue+ElementUI实现表单动态渲染、可视化配置的方法
2018/03/07 Javascript
vue :src 文件路径错误问题的解决方法
2018/05/15 Javascript
浅谈Vue 性能优化之深挖数组
2018/12/11 Javascript
Vue实现仿iPhone悬浮球的示例代码
2020/03/13 Javascript
Python标准库之sqlite3使用实例
2014/11/25 Python
利用Python循环(包括while&amp;for)各种打印九九乘法表的实例
2017/11/06 Python
使用python语言,比较两个字符串是否相同的实例
2018/06/29 Python
python要安装在哪个盘
2020/06/15 Python
python 删除excel表格重复行,数据预处理操作
2020/07/06 Python
Python如何创建装饰器时保留函数元信息
2020/08/07 Python
windows下python 3.9 Numpy scipy和matlabplot的安装教程详解
2020/11/28 Python
HTML5视频播放插件 video.js介绍
2018/09/29 HTML / CSS
Html5之webcoekt播放JPEG图片流
2020/09/22 HTML / CSS
来自Ocado的宠物商店:Fetch
2018/07/10 全球购物
护理专业大学生自我推荐信
2014/01/25 职场文书
竞争与合作演讲稿
2014/05/12 职场文书
乡镇干部先进性教育活动个人整改措施
2014/09/16 职场文书
护理见习报告范文
2014/11/03 职场文书
2014年质量管理工作总结
2014/12/01 职场文书
nginx优化的六点方法
2021/03/31 Servers
浅谈mysql哪些情况会导致索引失效
2021/11/20 MySQL