重构你的JS代码

最近写JS的时候总是发现写出来的东西结构不够清晰,就想着提高一下对JS的重构能力,看了一些这方面的资料,总结了一些我认为比较重要的方法,后续发现有更多的实用方案会陆续加进来

UI层的松耦合

这里不必多说,就是HTML、CSS、JS需要做分层

  • CSS中不能出现JS,也即不要使用CSS表达式
  • 从JS中抽离CSS

除了要对元素做定位,可以在JS中使用style.topstyle.leftstyle.rightstyle.bottom来对元素做定位

在其他情况下修改元素样式,最好是用JS操作CSS的className,比如:

1
2
3
4
5
6
7
8
9
10
11
12
//bad
element.style.color = "red";

//good
.reveal {
color:red;
left:10px;
top:100px;
visibility:visible;
}

element.className += "reveal";

这样在页面的生存周期中,JS可以随意添加和删除元素的className,由于className的样式都定义在CSS里,所以需要修改样式可以直接在CSS里修改,而不必修改JS,这样就实现了JS和CSS的松耦合。

  • HTML中不要嵌入JS

经典的bad写法

1
<button onclick="doSomething()" id="btn">click</button>

当然这个是在很久之前大量流行的写法,现在基本大家都不会这么写了,都会在一个单独的JS文件里做绑定事件等工作了

  • JS中不要出现大量的HTML

这种情况经常发生在给innerHTML赋值,比如

1
2
var div = document.getElementById('div');
div.innerHTML = '<h3>error</h3><p>invalid e-mail address</p>'

这样会造成调试的复杂性,不容易追踪bug,因此需要将HTML从JS中抽离

推荐使用类似handlebars的JS模板引擎

避免使用全局变量

过多的全局变量可能会造成命名冲突,造成代码脆弱不易维护。同时,全局环境是用来定义JS内置对象的地方,若在这个作用域中添加了自己定义的变量,可能会由读取浏览器自带的内置变量的风险

要解决上述问题推荐使用模块化编程

AMD异步模块定义,通过制定模块名称、依赖和一个工厂方法,依赖加载完成后执行这个工厂方法。

1
2
3
4
5
define('module-name', ['dependency1', 'dependency2'], function(dependency1, dependency2) {

//模块内容

})

使用模块加载器来使用AMD模块,常用的requirejs,它使用全局函数require()来加载指定的依赖并执行回调函数,比如:

1
2
3
require(['module-name'], function(name) {
name.doSomething(); //执行模块中的方法
});

事件处理

很多事件处理的相关代码和事件的环境紧紧耦合,导致维护性极差

对于事件处理的优化主要有两个规则:1、隔离应用逻辑 2、不要分发事件对象

通过一个例子的改进来说明上述两点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//bad
.reveal {
color:red;
left:10px;
top:100px;
visibility:visible;
}

function handleClick(event) {
var popup = document.getElementById('popup');
popup.style.left = event.clientX + 'px';
popup.style.top = event.clientY + 'px';
popup.className = 'reveal';
}

addListener(element, 'click', handleClick);

上面代码是有问题的

首先,事件处理程序包含了应用逻辑,也就是包含了跟应用相关的功能性代码,而不是和用户行为相关的。

上面的代码是要在点击时在一个特定位置显示一个弹出框,这个弹出框的class是reveal,看似没有问题,但实际上如果在其他地方也会触发同样的逻辑时(比如按下键盘上某个键也会触发同样逻辑),就会造成多个事件处理执行了同样的逻辑,代码就需要被复制很多次;除此之外,测试时需要直接触发功能代码,而不需要通过模拟对元素的点击来触发。

因此将应用逻辑从所有事件处理中抽离是一种最佳实践,对上面的代码做重构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//good
var MyApplication = {
handleClick: function(event) {
this.showPopup(event);
},

showPopup: function(event) {
var popup = document.getElementById('popup');
popup.style.left = event.clientX + 'px';
popup.style.top = event.clientY + 'px';
popup.className = 'reveal';
}
};

addListener(element, 'click', function(event) {
MyApplication.handleClick(event);
});

这样一来,处理应用逻辑的程序转移到了MyApplication.showPopup()中,MyApplication.handleClick()只做一件事,就是调用MyApplication.showPopup()方法。这时对.showPopup()的调用就不需要依赖于click事件了。这一步骤就是优化事件处理的第一步,隔离应用逻辑。

做好上面的工作以后,还有一个问题就是event对象被无节制分发,从匿名事件处理函数传到了MyApplication.handleClick(),又传到了MyApplication.showPopup()中,由于event对象包含很多与事件相关的额外信息,而这里的代码只用到了其中的两个clientXclientY,解决这个问题的最好办法就是让事件处理程序使用event对象,然后得到所有需要的数据后传给应用逻辑。

继续重构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//better
var MyApplication = {
handleClick: function(event) {
this.showPopup(event.clientX, event.clientY);
},

showPopup: function(x, y) {
var popup = document.getElementById('popup');
popup.style.left = x + 'px';
popup.style.top = y + 'px';
popup.className = 'reveal';
}
};

addListener(element, 'click', function(event) {
MyApplication.handleClick(event);
});

这样一来,对于MyApplication.showPopup()传入的参数就变得非常清晰,且可以随意测试或者在代码的任何位置调用这段逻辑。

当处理事件时,最好让事件处理程序成为唯一接触到event对象的函数,也就是说在事件处理程序中完成所有对event对象的必要操作,不要让应用逻辑对event产生依赖。