8

由重构进阶前端开发入门 (四) 面向对象

 2 years ago
source link: https://blog.krimeshu.com/2018/01/31/basic-knowledge-points-for-beginner-3/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

了解了在浏览器环境下,使用 JS 编程的基础概念之后,开始思考如何组织优化自己的代码,从编程技巧上提升开发和维护工作的效率吧。

相关文章:

由重构进阶前端开发入门 (一) DOM 操作

由重构进阶前端开发入门 (二) 事件与事件对象

由重构进阶前端开发入门 (三) 事件冒泡与事件代理

(四) 面向对象

DRY (Don’t Repeat Yourself) 原则

JavaScript 是一门编程语言,和其它计算机语言一样,在你编码的过程中需要有避免重复代码和逻辑的意识,注意不断优化自己的代码。

以免写出看似体系庞大而可靠,实则冗余且迟滞的代码,埋下 bug 隐患,影响团队伙伴阅读代码、协作开发的效率,也耽误自己以后优化和迭代项目。

因此,其中重要的原则之一就是 DRY (Don’t Repeat Yourself) 原则。

当你第一次写下某段代码,之后在另一个地方又写下或粘贴同样的代码,你就应该有需要消除和提取重复代码的冲动了。

等到第三次,再另一个地方又出现同样的代码时,就可以考虑行动起来,提取共用的代码而不是又重复一遍。

函数复用、公用库

最基本的方法,就是把重复代码提取成复用的函数。

对话框展示函数

例如,在页面某处有一个弹出 Dialog 的逻辑,写下了这样的代码:

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 消息:

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

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 的原型链和面相对象的关系,本文暂不深入说明,以免初学者混淆。

大家可以先学会运用现有的方式,先知其然后知其所以然,通过实践记忆之后再深入了解原理也会更容易上手。

// 对话框类的构造函数
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的 $dom 不会受到任何影响,反之亦然)。

然后,增加了几个 Dialog 原型函数 show, hide, destroy。这几个函数被称为类的方法

所有对话框都可以调用这些方法,与构造函数一样,其中也可以操作 this 来达成不同实例互不干扰的效果。

对话框实例

完成了最基本的可复用对话框类的创建,只需要通过 new 就可以实例化后使用了。

// 创建对话框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 的例子,并扩展出宽高、坐标属性,和对应的调整大小、位置的函数,作为这一期的课后练习吧~


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK