了解了在浏览器环境下,使用 JS 编程的基础概念之后,开始思考如何组织优化自己的代码,从编程技巧上提升开发和维护工作的效率吧。
相关文章: 由重构进阶前端开发入门 (一) DOM 操作 由重构进阶前端开发入门 (二) 事件与事件对象 由重构进阶前端开发入门 (三) 事件冒泡与事件代理
(四) 面向对象
DRY (Don’t Repeat Yourself) 原则
JavaScript 是一门编程语言,和其它计算机语言一样,在你编码的过程中需要有避免重复代码和逻辑的意识,注意不断优化自己的代码。
以免写出看似体系庞大而可靠,实则冗余且迟滞的代码,埋下 bug 隐患,影响团队伙伴阅读代码、协作开发的效率,也耽误自己以后优化和迭代项目。
因此,其中重要的原则之一就是 DRY (Don’t Repeat Yourself) 原则。
当你第一次写下某段代码,之后在另一个地方又写下或粘贴同样的代码,你就应该有需要消除和提取重复代码的冲动了。
等到第三次,再另一个地方又出现同样的代码时,就可以考虑行动起来,提取共用的代码而不是又重复一遍。
函数复用、公用库
最基本的方法,就是把重复代码提取成复用的函数。
对话框展示函数
例如,在页面某处有一个弹出 Dialog 的逻辑,写下了这样的代码:
代码语言:javascript复制var $dialog1 = $('<div class="dialog-box">'
' <p class="dialog-msg"></p>'
' <a class="dialog-btn" close-dialog>确定</a>'
'</div>')
.on('click','[close-dialog]', function () {
$(this).closest('.dialog-box').hide();
})
.find('.dialog-msg')
.text('登陆成功!');
$('body').append($dialog1);
之后又增加了一个类似的 Toast 消息:
代码语言:javascript复制var $dialog2 = $('<div class="dialog-box">'
' <p class="dialog-msg"></p>'
' <a class="dialog-btn" close-dialog>确定</a>'
'</div>')
.on('click', '[close-dialog]', function () {
$(this).closest('.dialog-box').hide();
})
.find('.dialog-msg')
.text('评论发送失败!');
$('body').append($dialog2);
这段代码与上面那段几乎所是相同的,区别只在于提示语。
明显没必要这样重复,可以提取出一个通用的兑换框展示函数 showDialog:
代码语言:javascript复制function showDialog(msg) {
var $dialog = $('<div class="dialog-box">'
' <p class="dialog-msg"></p>'
' <a class="dialog-btn" close-dialog>确定</a>'
'</div>')
.on('click', '[close-dialog]', function () {
$(this).closest('.dialog-box').hide();
})
.find('.dialog-msg')
.text(msg);
$('body').append($dialog);
}
showDialog('登陆成功!');
showDialog('评论发送失败!');
提取共用函数可以说是最基本的编程思想了。
这样之后需要增加新的消息,或是对原有的所有提示消息做调整和修复时,不需要修改散落在四处的代码,只需修改一处,效率大大提升。
有一些代码甚至不止可以用于一个项目,还可以在今后的项目开发中继续复用,这些函数逻辑可以提取成公用代码库,节省今后项目开发的时间。
抽象成对象/类
上面的思想概括起来,其实就是将处理一类事务的过程,以函数的形式复用。
是一种相对初级的复用思想,随着业务逻辑逐渐复杂,这种办法的效果也越来越弱。
结果就是,这样写出来的 js 文件,到达一定规模之后,其中虽然没什么重复代码,但却有着几十上百个函数。阅读者理清其中的顺序和关系会很耗时,难以保证可读性。
而且函数形式的复用,并不能很好的处理带属性、状态一类的情况。
比如上面的对话框函数,如果要给对话框增加拖动的处理函数,还要在记录坐标、层级、打开状态等等属性时,需要手动从外部传入很多变量来处理。
导致原本是对话框相关的逻辑和数据,却被分散到了文件内的不同地方,需要做属性增减时很难集中调整。
继续增加函数形式的控制逻辑,也容易与其他函数混在一起。项目合作的同事稍不注意,就容易插入其他函数把它们打散。
最后赶出来的项目或许能正常运行,但内部代码却是互相穿插、混乱不堪的意大利面条代码,几乎无法维护。
所以计算机软件工程的前人们,探索出了面向对象的编程思想。
对话框类的定义
让我们从头想想,对话框是什么呢?
它应该是具有特定坐标、宽高、背景色等样式,可以设定其内容、坐标、控制按键等属性的绝对定位的特定元素。
那么有没有这样一种办法,使我们可以在需要使用对话框时,做到:
- 简单快速地创建对话框;
- 调用API就可以调整内容、移动、展示、收起对话框;
- 并且使不同对话框操作接口一致,自身数据却互不干扰;
- 有必要时,还可以在原有接口基础上快速增加新的特性呢?
刚才我们提到的这些,可以通过面向对象的继承、封装和多态来实现。
不过由于 JavaScript 的特殊性,多态在鸭子模式下的体现并不明显,暂且不提。先从一些基本概念开始说起。
上一步里,我们抽象出了对话框的基本概念,也就是我们需要的对话框大致上是个什么的东西。
运用面向对象的思想,我们可以把它们作为其成员属性、方法,来定义出一个对话框类。
为了方便新同学直接在浏览器里测试代码,这里采用 ES5 的类写法举例:
代码语言:javascript复制关于 JavaScript 的原型链和面相对象的关系,本文暂不深入说明,以免初学者混淆。 大家可以先学会运用现有的方式,先知其然后知其所以然,通过实践记忆之后再深入了解原理也会更容易上手。
// 对话框类的构造函数
function Dialog(options) {
var self = this;
// 创建相应 dom,记录到当前构建的对象内
this.$dom = $('<div class="dialog-box">'
' <p class="dialog-msg"></p>'
' <a class="dialog-btn" close-dialog>确定</a>'
'</div>')
.on('click', '[close-dialog]', function () {
self.hide();
});
$('body').append(this.$dom);
}
// 对话框的可用方法
Dialog.prototype = {
// 记录构造函数
constructor: Dialog,
// 设置内容
setContent: function (content) {
this.$dom.find('.dialog-msg').text(content);
},
// 展示对话框
show: function () {
this.$dom.show();
},
// 收起对话框
hide: function () {
this.$dom.hide();
}
};
首先声明对话框类 Dialog 的构造函数,之后每个对话框都将通过这个函数构建出具体的实例。
其中通过操作 this,可以使所有对话框都有 DOM 对象可供操作,且互相独立不受干扰(比如对话框1和对话框2都具有 dom 的属性,修改对话框1的 dom 时,对话框2的
然后,增加了几个 Dialog 原型函数 show, hide, destroy。这几个函数被称为类的方法。
所有对话框都可以调用这些方法,与构造函数一样,其中也可以操作 this 来达成不同实例互不干扰的效果。
对话框实例
完成了最基本的可复用对话框类的创建,只需要通过 new 就可以实例化后使用了。
代码语言:javascript复制// 创建对话框1
var dialog1 = new Dialog();
// 设置其内容
dialog1.setContent('登陆成功!');
// 展示对话框1
dialog1.show();
// 创建对话框2
var dialog2 = new Dialog();
// 设置其内容(不影响对话框1)
dialog2.setContent('评论发送失败!');
// 展示对话框2
dialog2.show();
// 3秒后自动收起(同样不影响对话框1)
setTimeout(function () {
dialog2.hide();
}, 3000);
与 showDialog 函数相比,这样写有什么优势呢?
首先是逻辑和属性集中化,方便对同一类的成员进行维护和扩展。
通过 this 操作每个实例,避免重复的传参,无需手动区分不同实例,灵活又便捷。
而且通过 IDE 的解析推断,可以根据对象所属的类型,自动给出属性和方法的智能提示,提升开发效率,避免在函数海中苦苦搜寻,甚至混淆调用。
目前主流的前端自动化都有脚本打包功能,根据类和基本逻辑划分项目文件结构后,维护起来十分清晰便利。
合作开发的同事可以通过查看项目结构,对于流程有个大致概念。
对于顶层逻辑只需要了解主要流程,底层逻辑都被封装入类内对外透明。
每个文件内只需要处理自身相关的逻辑,代码量基本可以控制在400行内,属于最适合维护阅读的程度。
小结
有其他语言的面向对象开发经验的同学,可能会对 JavaScript 内的类生命写法不解,为什么看起来会这么奇怪。
这是因为 JavaScript 的面向对象是基于原型而非基于类来实现的。
最直观的区别就是其实并不存在真正的类,而是基于对象实例,通过将实例作为构造函数的原型,再通过调用构造函数来产生继承于此的新对象。
这种模式非常灵活,适合 JavaScript 动态脚本语言的开发模式。
但对于新手来说可能会更难理解,实际操作中实现较完美的继承扩展,区分原型和实例的函数也有一定难度,容易造成误解和混淆。
所以 ES6 中提供了更方便的 class 定义方式,目前主流的前端开发框架 React、Vue、Angluar 也都推荐使用 ES6 的新写法。
大家编写 ES5 的模拟类体验和理解后,再通过这些框架的脚手架或者 babel 的 repl 感受 ES6 中定义类的便捷性。
有兴趣的话,可以尝试使用 ES6 再实现上面 dialog 的例子,并扩展出宽高、坐标属性,和对应的调整大小、位置的函数,作为这一期的课后练习吧~