本文深入解析JavaScript代码优化的8个核心要点,涵盖松耦合架构、全局变量治理、事件处理隔离、配置数据管理、选择器优化以及函数与循环的重构技巧。通过具体代码对比,展示如何提升代码可维护性、性能及团队协作效率,帮助开发者写出更清晰、健壮的JS代码。
本文将深入探讨JavaScript编程风格中几个关键的优化要点,旨在提升代码质量与可维护性。
松耦合
松耦合的核心在于组件间的独立性:当修改其中一个组件时,无需对其他组件进行任何更改。
实现松耦合的具体策略包括以下四个方面:
//不好的做法
.box{width: expression(document.body.offsetWidth + 'px')}
首先,JS与CSS分离:避免使用CSS表达式,防止样式逻辑侵入JavaScript。
//不好的做法一
ele.style.color = 'red';
ele.style.left= '10px';
//不好的做法二
ele.style.cssText ='color:red;left:10px;';
.reveal{color:red;left:10px;}
//好的做法一
ele.className += 'reveal';
//好的做法二
ele.classList.add('reveal');
其次,CSS与JS分离:在JavaScript中修改样式时,应通过操作className或classList属性来实现,严禁逐条直接修改style对象属性。
第三,JS与HTML分离:将JavaScript代码提取至独立的外部文件中,避免内联脚本。
第四,HTML与JS分离:在JavaScript中生成DOM结构时,禁止使用innerHTML拼接字符串,而应采用字符串模板引擎(如Handlebars)来确保结构清晰与安全。
在团队开发的大型项目中,创建全局变量被视为不良实践。随着代码规模扩大,全局变量会引发严重的可维护性问题,且全局变量数量增加会显著提高引入错误的概率。
创建全局变量被视为不良编程实践,尤其在团队协作的大型项目中,这会引发严重的可维护性问题。随着代码库规模扩大,全局变量的增加会显著抬高引入错误的概率,导致代码难以维护。
一般而言,解决全局变量问题有以下三种主要方案:
第一种方案是零全局变量。
(function(){
var doc = win.document;
})(window);
具体实现方式是使用立即调用函数表达式(IIFE),将所有脚本代码包裹其中。然而,这种模式的适用场景有限:如果代码需要被其他模块依赖,或者需要在运行时动态扩展和修改,则无法采用此方式。
第二种方案是单全局变量与命名空间。
该策略旨在尽可能减少全局变量的数量,即只创建一个全局变量,采用单变量模式(如YUI或jQuery的做法)。
所谓单全局变量,是指创建一个独一无二的全局对象名,并将所有功能代码挂载到该对象上。这样,所有潜在的全局变量都成为该唯一全局对象的属性,从而避免了多个全局变量的产生。
命名空间则是通过全局对象的单一属性来表示的功能性分组。例如,Y.DOM下的所有方法均与DOM操作相关,Y.Event下的方法与事件处理相关。常见的约定是,每个文件通过新的全局对象来声明自己的命名空间。
第三种方案是使用模块。
模块是一种通用的功能片段,它不创建新的全局变量或命名空间。相反,所有代码都存放于一个执行特定任务或发布接口的单函数中。模块可以通过名称引用,并可以依赖其他模块。
事件处理
将事件处理代码与事件环境紧密耦合会导致可维护性极差。
优化事件处理的最佳实践包括:
第一,隔离应用逻辑。应将应用逻辑从所有事件处理程序中抽离出来,实现应用逻辑与事件处理代码的分离。
//不好的做法
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);
//好的做法
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);
});
第二,不要分发事件对象。
应用逻辑不应依赖event对象来完成功能,方法接口应明确表明所需的数据。代码不清晰会导致bug。最佳做法是让事件处理程序利用event对象处理事件,提取所需数据后传递给应用逻辑。
//改进的做法
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);
});
在处理事件时,最好让事件处理程序成为唯一接触event对象的函数。事件处理程序应在进入应用逻辑前,针对event对象执行任何必要操作(包括阻止事件冒泡),这些操作应直接包含在事件处理程序中。
//改进的做法
var MyApplication = {
handleClick: function(event){
event.preventDefault();
event.stopPropagation();
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);
});
配置数据
代码本质上是计算机执行指令的集合。我们通常将数据传入计算机,由指令对数据进行操作并产生结果。当不得不修改硬编码在代码中的数据时,可能会带来不必要的风险。因此,应当将关键数据从代码中抽离出来。
配置数据指的是应用中写死的、且未来可能被修改的值,主要包括以下内容:
1、URL
2、需要展现给用户的字符串
3、重复的值
4、配置项
5、任何可能发生变更的值
以下是未处理配置数据的典型反面案例。
//不好的做法
function validate(value){
if(!value){
alert('Invalid value');
location.href="/errors/invalid.php" rel="external nofollow" ;
}
}
function toggleSelected(element){
if(hasClass(element,'selected')){
removeClass(element,'selected');
}else{
addClass(element,'selected');
}
}
在下面的代码中,配置数据保存在config对象中,config对象的每个属性保存一个数据片段,每个属性名带有前缀以表明数据类型(MSG表示展示给用户的消息,URL表示网络地址,CSS表示这是一个className)。当然,也可以将整个config对象放到单独的文件中,这样对配置数据的修改可以完全和使用该数据的代码隔离开来。
//好的做法
var config = {
MSG_INVALID_VALUE: 'Invalid value',
URL_INVALID:'/errors/invalid.php',
CSS_SELECTED:'selected'
}
function validate(value){
if(!value){
alert(config.MSG_INVALID_VALUE);
location.href=config.URL_INVALID;
}
}
function toggleSelected(element){
if(hasClass(element,config.CSS_SELECTED)){
removeClass(element,config.CSS_SELECTED);
}else{
addClass(element,config.CSS_SELECTED);
}
}
选择器优化
应将选择器选中的元素作为对象的静态属性集中到一个地方进行统一管理。
initializeElements: function() {
var eles = app.Eles;
for (var name in eles) {
if (eles.hasOwnProperty(name)) {
this[name] = $(eles[name]);
}
}
}
以下是一个具体的优化示例。
//好的做法app.Eles = {
widgetDiv: ".left-widget div",
inputResize: '.input-resize',
hr: '.hr',
txt: '.input-group-btn button',
cus: '#paper-type-cus',
hid: '#hidden',
mainCon: '#mainCon',
rulerX: '.ruler-x',
rulerY: '.ruler-y',
};
函数优化
【提炼函数】
在JavaScript开发中,大部分时间都在与函数打交道,因此希望这些函数拥有良好的命名,且函数体内逻辑清晰明了。如果一个函数过长,不得不依靠若干注释才能使其易读,那么这些函数就有必要进行重构。
如果在函数中有一段代码可以被独立出来,最好将这些代码放入另一个独立的函数中。这是一种常见的优化工作,其好处主要有以下几点:
1、避免出现超大函数。
2、独立出来的函数有助于代码复用。
3、独立出来的函数更容易被覆写。
4、独立出来的函数如果拥有良好的命名,它本身就起到了注释的作用。
例如,在一个负责获取用户信息的函数中,如果需要打印与用户信息相关的日志,那么打印日志的语句就可以被封装在一个独立的函数里。
var getUserInfo = function(){
ajax( 'http:// xxx.com/userInfo', function( data ){
console.log( 'userId: ' + data.userId );
console.log( 'userName: ' + data.userName );
console.log( 'nickName: ' + data.nickName );
});
};
//改成:
var getUserInfo = function(){
ajax( 'http:// xxx.com/userInfo', function( data ){
printDetails( data );
});
};
var printDetails = function( data ){
console.log( 'userId: ' + data.userId );
console.log( 'userName: ' + data.userName );
console.log( 'nickName: ' + data.nickName );
};
【尽量减少参数数量】
如果调用一个函数时需要传入多个参数,该函数会令人望而生畏,因为必须搞清楚这些参数代表的含义,并小心翼翼地将它们按顺序传入。在实际开发中,向函数传递参数不可避免,但应尽量减少函数接收的参数数量。以下是一个简单示例:有一个画图函数draw,它目前只能绘制正方形,接收3个参数,分别是图形的width、height以及square(面积)。
var draw = function(width,height,square){};
但实际上,正方形的面积可以通过width和height计算得出,因此我们可以从draw函数中移除square参数。
var draw = function( width, height ){
var square = width * height;
};
假设未来draw函数开始支持绘制圆形,需要将参数width和height替换为半径radius,但图形的面积square始终不应由客户传入,而应在draw函数内部,由传入的参数加上一定规则计算得出。此时,可以使用策略模式,使draw函数成为一个支持绘制多种图形的函数。
【传递对象参数代替过长的参数列表】
有时一个函数可能接收多个参数,参数数量越多,函数越难理解和使用。使用该函数的人首先得明白全部参数的含义,使用时还要小心翼翼,以免少传参数或搞反参数位置。如果想在第3个参数和第4个参数之间增加一个新参数,就会涉及大量代码修改,代码如下:
var setUserInfo = function( id, name, address, sex, mobile, qq ){
console.log( 'id= ' + id );
console.log( 'name= ' +name );
console.log( 'address= ' + address );
console.log( 'sex= ' + sex );
console.log( 'mobile= ' + mobile );
console.log( 'qq= ' + qq );
};
setUserInfo( 1314, 'xiaohuochai', 'beijing', 'male', '150********', 121631835 );
此时,可以把参数都放入一个对象内,然后将该对象传入setUserInfo函数,setUserInfo函数需要的数据可以自行从该对象里获取。现在不用再关心参数的数量和顺序,只要保证参数对应的key值不变即可:
var setUserInfo = function( obj ){
console.log( 'id= ' + obj.id );
console.log( 'name= ' + obj.name );
console.log( 'address= ' + obj.address );
console.log( 'sex= ' + obj.sex );
console.log( 'mobile= ' + obj.mobile );
console.log( 'qq= ' + obj.qq );
};
setUserInfo({
id: 1314,
name: 'xiaohuochai',
address: 'beijing',
sex: 'male',
mobile: '150********',
qq: 121631835
});
条件优化
【合并条件片段】
如果一个函数体内包含一些条件分支语句,且这些条件分支内部散布了重复代码,那么就有必要进行合并去重工作。假设有一个分页函数paging,该函数接收一个参数currPage,表示即将跳转的页码。在跳转之前,为防止currPage传入过小或过大的数字,需手动对其值进行修正,详见如下伪代码:
var paging = function( currPage ){
if ( currPage <= 0 ){
currPage = 0;
jump( currPage ); // 跳转
}else if ( currPage >= totalPage ){
currPage = totalPage;
jump( currPage ); // 跳转
}else{
jump( currPage ); // 跳转
}
};
可以看到,负责跳转的代码jump(currPage)在每个条件分支内都出现了,因此完全可以将这句代码独立出来。
var paging = function( currPage ){
if ( currPage <= 0 ){
currPage = 0;
}else if ( currPage >= totalPage ){
currPage = totalPage;
}
jump( currPage ); // 把jump 函数独立出来
};
【把条件分支语句提炼成函数】
在程序设计中,复杂的条件分支语句是导致程序难以阅读和理解的重要原因,且容易导致一个庞大的函数。假设现在有一个需求是编写一个计算商品价格的getPrice函数,商品的计算只有一个规则:如果当前正处于夏季,那么全部商品将以8折出售。代码如下:
var getPrice = function( price ){
var date = new Date();
if ( date.getMonth() >= 6 && date.getMonth() <= 9 ){ // 夏天
return price * 0.8;
}
return price;
};
观察这句代码:
date.getMonth()>=6&&date.getMonth()<=9
这句代码要表达的意思很简单,即判断当前是否正处于夏天(7~10月)。尽管这句代码很短小,但代码表达的意图和代码自身还存在一些距离,阅读代码的人必须要多花一些精力才能明白它传达的意图。其实可以把这句代码提炼成一个单独的函数,既能更准确地表达代码的意思,函数名本身又能起到注释的作用。代码如下:
var isSummer = function(){
var date = new Date();
return date.getMonth() >= 6 && date.getMonth() <= 9;
};
var getPrice = function( price ){
if ( isSummer() ){ // 夏天
return price * 0.8;
}
return price;
};
【提前让函数退出代替嵌套条件分支】
许多程序员都有这样一种观念:“每个函数只能有一个入口和一个出口。”现代编程语言都会限制函数只有一个入口。但关于“函数只有一个出口”,往往会有一些不同的看法。下面这段伪代码是遵守“函数只有一个出口的”典型代码:
var del = function( obj ){
var ret;
if ( !obj.isReadOnly ){ // 不为只读的才能被删除
if ( obj.isFolder ){ // 如果是文件夹
ret = deleteFolder( obj );
}else if ( obj.isFile ){ // 如果是文件
ret = deleteFile( obj );
}
}
return ret;
};
嵌套的条件分支语句绝对是代码维护者的噩梦。对于阅读代码的人来说,嵌套的if、else语句相比平铺的if、else,在阅读和理解上更加困难。嵌套的条件分支往往是由一些深信“每个函数只能有一个出口的”程序员写出的。但实际上,如果对函数的剩余部分不感兴趣,那就应该立即退出。引导阅读者去看一些没有用的else片段,只会妨碍他们对程序的理解。
于是可以挑选一些条件分支,在进入这些条件分支之后,就立即让这个函数退出。要做到这一点,有一个常见的技巧,即在面对一个嵌套的if分支时,可以把外层if表达式进行反转。重构后的del函数如下:
var del = function( obj ){
if ( obj.isReadOnly ){ // 反转if 表达式
return;
}
if ( obj.isFolder ){
return deleteFolder( obj );
}
if ( obj.isFile ){
return deleteFile( obj );
}
};
循环优化
【合理使用循环】
在函数体内,如果有些代码实际上负责的是重复性工作,那么合理利用循环不仅可以完成同样的功能,还可以使代码量更少。下面有一段创建XHR对象的代码,为了简化示例,只考虑版本9以下的IE浏览器,代码如下:
var createXHR = function(){
var xhr;
try{
xhr = new ActiveXObject( 'MSXML2.XMLHttp.6.0' );
}catch(e){
try{
xhr = new ActiveXObject( 'MSXML2.XMLHttp.3.0' );
}catch(e){
xhr = new ActiveXObject( 'MSXML2.XMLHttp' );
}
}
return xhr;
};
var xhr = createXHR();
下面灵活地运用循环,可以得到跟上面代码一样的效果:
//下面我们灵活地运用循环,可以得到跟上面代码一样的效果:
var createXHR = function(){
var versions= [ 'MSXML2.XMLHttp.6.0ddd', 'MSXML2.XMLHttp.3.0', 'MSXML2.XMLHttp' ];
for ( var i = 0, version; version = versions[ i++ ]; ){
try{
return new ActiveXObject( version );
}catch(e){
}
}
};
var xhr = createXHR();
【用return退出多重循环】
假设在函数体内有一个两重循环语句,需要在内层循环中判断,当达到某个临界条件时退出外层的循环。大多数时候会引入一个控制标记变量:
var func = function(){
var flag = false;
for ( var i = 0; i < 10; i++ ){
for ( var j = 0; j < 10; j++ ){
if ( i * j >30 ){
flag = true;
break;
}
}
if ( flag === true ){
break;
}
}
};
第二种做法是设置循环标记:
var func = function(){
outerloop:
for ( var i = 0; i < 10; i++ ){
innerloop:
for ( var j = 0; j < 10; j++ ){
if ( i * j >30 ){
break outerloop;
}
}
}
};
这两种做法无疑都让人头晕目眩,更简单的做法是在需要中止循环的时候直接退出整个方法:
var func = function(){
for ( var i = 0; i < 10; i++ ){
for ( var j = 0; j < 10; j++ ){
if ( i * j >30 ){
return;
}
}
}
};
当然,用return直接退出方法会带来一个问题:如果在循环之后还有一些将被执行的代码呢?如果提前退出了整个方法,这些代码就得不到被执行的机会:
var func = function(){
for ( var i = 0; i < 10; i++ ){
for ( var j = 0; j < 10; j++ ){
if ( i * j >30 ){
return;
}
}
}
console.log( i ); // 这句代码没有机会被执行
};
为了解决这个问题,可以把循环后面的代码放到return后面;如果代码比较多,就应该把它们提炼成一个单独的函数。
var print = function( i ){
console.log( i );
};
var func = function(){
for ( var i = 0; i < 10; i++ ){
for ( var j = 0; j < 10; j++ ){
if ( i * j >30 ){
return print( i );
}
}
}
};func();