如何编写高质量JS代码
想写出高效的javascript类库却无从下手;
尝试阅读别人的类库,却理解得似懂给懂;
打算好好钻研js高级函数,但权威书上的内容太零散,
即使记住“用法”,但到要“用”的时候却没有想“法”。
也许你和我一样,好像有一顾无形的力量约束着我们的计划,让我们一再认为知识面的局限性,致使我们原地踏步,难以向前跨越。
这段时间,各种作业、课程设计、实验报告,压力倍增。难得挤出一点点时间,绝不睡懒觉,整理总结往日所看的书,只为了可以离写自己的类库近一点。
本文参考自《javascript语言精粹》和《EffectiveJavaScript》。例子都被调试过,理解过后,我想把一些“深奥”的道理说得浅显一点点。
1.变量作用域
作用域对于程序员来说就像氧气。它无处不在,甚至,你往往不会去想他。但当它被污染时(例如使用全局对象),你会感觉到窒息(例如应用响应变慢)。javascript核心作用域规则很简单,被精心设计,且很强大。有效地使用javascript需要掌握变量作用域的一些基本概念,并了解一些可能导致难以捉摸的、令人讨厌的问题的极端情况。
1.1尽量少用全局变量
javascript很容易在全局命名空间中创建变量。创建全局变量毫不费力,因为它不需要任何形式的声明,而且能被整个程序的所有代码自动地访问。
对于我们这些初学者,遇到某些需求(例如,传输的数据被记录下来、等待某时机某函数调用时使用;或者是某函数被经常使用)时,好不犹豫想到全局函数,甚至大一学到的C语言面向过程思想太根深蒂固,系统整整齐齐地都是满满函数。定义全局变量会污染共享的公共命名空间,并可能导致意外的命名冲突。全局变量也不利于模块化,因为它会导致程序中独立组件间的不必要耦合。严重地说,过多的全局(包括样式表,直接定义div或者a的样式),整合到多人开发过称将会成为灾难性错误。这就是为什么jQuery的所有代码都被包裹在一个立即执行的匿名表达式——自调用匿名函数。当浏览器加载完jQuery文件后,自调用匿名函数立即开始执行,初始化jQuery的各个模块,避免破坏和污染全局变量以至于影响到其他代码。
(function(window,undefined){ varjQuery=... //... window.jQuery=window.$=jQuery; })(window);
另外,你或许会认为,“先怎么怎么写,日后再整理”比较方便,但优秀的程序员会不断地留意程序的结构、持续地归类相关的功能以及分离不相关的组件,并这些行为作为编程过称中的一部分。
由于全局命名空间是javascript程序中独立的组件经行交互的唯一途径,因此,利用全局命名控件的情况是不可避免的。组件或程序库不得不定义一些全局变量。以便程序中的其他部分使用。否则最好使用局部变量。
this.foo;//undefined foo="globalfoo"; this.foo;//"globalfoo" varfoo="globalfoo"; this.foo="changed"; foo;//changed
javascript的全局命名空间也被暴露在程序全局作用域中可以访问的全局对象,该对象作为this关键字的初始值。在web浏览器中,全局对象被绑定在全局window变量。这就意味你创建全局变量有两种方法:在全局作用域内使用var声明他,或者将其加入到全局对象中。使用var声明的好处是能清晰地表达全局变量在程序范围中的影响。
鉴于引用为绑定的全局变量会导致运行时错误,因此,保存作用域清晰和简洁会使代码的使用者更容易理解程序声明了那些全局变量。
由于全局对象提供了全局环境的动态反应机制,所以可以使用它查询一个运行环境,检测在这个平台下哪些特性可用。
eg.ES5引入了一个全局的JSON对象来读写JSON格式的数据。
if(!this.JSON){ this.JSON={ parse:.., stringify:... } }
如果你提供了JSON的实现,你当然可以简单无条件地使用自己的实现。但是由宿主环境提供的内置实现几乎更适合的,因为它们是用C语言写进浏览器的。因为它们按照一定的标准对正确性和一致性进行了严格检查,并且普遍来说比第三方实现提供更好的性能。
当初数据结构课程设计模拟串的基本操作,要求不能使用语言本身提供的方法。javascript对数组的基本操作实现得很好,如果只是出于一般的学习需要,模拟语言本身提供的方法的想法很好,但是如果真正投入开发,无需考虑第一时间选择使用javascript内置方法。
1.2避免使用with
with语句提供任何“便利“,让你的应用变得不可靠和低效率。我们需要对单一对象依次调用一系列方法。使用with语句可以很方便地避免对对象的重复引用:
functionstatus(info){ varwidget=newWidget(); with(widget){ setBackground("blue"); setForeground("white"); setText("Status:"+info); show(); } }
使用with语句从模块对象中”导入“(import)变量也是很有诱惑力的。
functionf(x,y){ with(Math){ returnmin(round(x),sqrt(y));//抽象引用 } }
事实上,javascript对待所有的变量都是相同的。javascript从最内层的作用域开始向外查找变量。with语言对待一个对象犹如该对象代表一个变量作用域,因此,在with代码块的内部,变量查找从搜索给定的变量名的属性开始。如果在这个对象中没有找到该属性,则继续在外部作用域中搜索。with块中的每个外部变量的引用都隐式地假设在with对象(以及它的任何原型对象)中没有同名的属性。而在程序的其他地方创建或修改with对象或其原型对象不一定会遵循这样的假设。javascript引擎当然不会读取局部代码来获取你使用了那些局部变量。javascript作用域可被表示为高效的内部数据结构,变量查找会非常快速。但是由于with代码块需要搜索对象的原型链来查找with代码里的所有变量,因此,其运行速度远远低于一般的代码块。
替代with语言,简单的做法,是将对象绑定在一个简短的变量名上。
functionstatus(info){ varw=newWidget(); w.setBackground("blue"); w.setForeground("white"); w.setText("Status:"+info); w.show(); }
其他情况下,最好的方法是将局部变量显式地绑定到相关的属性上。
functionf(x,y){ var min =Math.min, round =Math.round, sqrt =Math.sqrt; returnmin(round(x),sqrt(y)); }
1.3熟练掌握闭包
理解闭包有单个概念:
a)javascript允许你引用在当前函数以外定义的变量。
functionmakeSandwich(){ varmagicIngredient="peanutbutter"; functionmake(filling){ returnmagicIngredient+"and"+filling; } returnmake("jelly"); } makeSandwich();//"peanutbutterandjelly"
b)即使外部函数已经返回,当前函数仍然可以引用在外部函数所定义的变量
functionmakeSandwich(){ varmagicIngredient="peanutbutter"; functionmake(filling){ returnmagicIngredient+"and"+filling; } returnmake; } varf=sandwichMaker(); f("jelly"); //"peanutbutterandjelly" f("bananas"); //"peanutbutterandbananas" f("mallows"); //"peanutbutterandmallows"
javascriptd的函数值包含了比调用它们时所执行所需要的代码还要多的信息。而且,javascript函数值还在内部存储它们可能会引用的定义在其封闭作用域的变量。那些在其所涵盖的作用域内跟踪变量的函数被称为闭包。
make函数就是一个闭包,其代码引用了两个外部变量:magicIngredient和filling。每当make函数被调用时,其代码都能引用这两个变量,因为闭包存储了这两个变量。
函数可以引用在其作用域内的任何变量,包括参数和外部函数变量。我们可以利用这一点来编写更加通用的sandwichMaker函数。
functionmakeSandwich(magicIngredient){ functionmake(filling){ returnmagicIngredient+"and"+filling; } returnmake; } varf=sandwichMaker(”ham“); f("cheese"); //"hamandcheese" f("mustard"); //"hamandmustard"
闭包是javascript最优雅、最有表现力的特性之一,也是许多习惯用法的核心。
c)闭包可以更新外部变量的值。事实上,闭包存储的是外部变量的引用,而不是它们的值的副本。因此,对于任何具有访问这些外部变量的闭包,都可以进行更新。
functionbox(){ varval=undefined; return{ set:function(newval){val=newval;}, get:function(){returnval;}, type:function(){returntypeofval;} }; } varb=box(); b.type();//undefined b.set(98.6); b.get();//98.6 b.type();//number
该例子产生一个包含三个闭包的对象。这三个闭包是set,type和get属性,它们都共享访问val变量,set闭包更新val的值。随后调用get和type查看更新的结果。
1.4理解变量声明提升
javascript支持此法作用域(对变量foo的引用会被绑定到声明foo变量最近的作用域中),但不支持块级作用域(变量定义的作用域并不是离其最近的封闭语句或代码块)。
不明白这个特性将会导致一些微妙的bug:
functionisWinner(player,others){ varhighest=0; for(vari=0,n=others.length;i<n;i++){ varplayer=others[i]; if(player.score>highest){ highest=player.score; } } returnplayer.score>highest; }
1.5当心命名函数表达式笨拙的作用域
functiondouble(x){returnx*2;} varf=function(x){returnx*2;}
同一段函数代码也可以作为一个表达式,却具有截然不同的含义。匿名函数和命名函数表达式的官方区别在于后者会绑定到与其函数名相同的变量上,该变量作为该函数的一个局部变量。这可以用来写递归函数表达式。
varf=functionfind(tree,key){ //.... returnfind(tree.left,key)|| find(tree.right,key); }
值得注意的是,变量find的作用域只在其自身函数中,不像函数声明,命名函数表达式不能通过其内部的函数名在外部被引用。
find(myTree,"foo");//error:findisnotdefined; varconstructor=function(){returnnull;} varf=function(){ returnconstructor(); }; f();//{}(inES3environments)
该程序看起来会产生null,但其实会产生一个新的对象。
因为命名函数变量作用域内继承了Object.prototype.constructor(即Oject的构造函数),就像with语句一样,这个作用域会因Object.prototype的动态改变而受到影响。在系统中避免对象污染函数表达式作用域的办法是避免任何时候在Object.prototype中添加属性,以避免使用任何与标准Object.prototype属性同名的局部变量。
在流行的javascript引擎中另外一个缺点是对命名函数表达式的声明进行提升。
varf=functiong(){return17;} g();//17(innonconformatenvironment)
一些javascript环境甚至把f和g这两个函数作为不同的对象,从而导致不必要的内存分配。
1.6当心局部块函数声明笨拙的作用域
functionf(){return"global";} function test(x){ functionf(){return"local";} varresult=[]; if(x){ result.push(f()); } result.push(f()); resultresult; } test(true); //["local","local"] test(false); //["local"]
functionf(){return"global";} function test(x){ varresult=[]; if(x){ functionf(){return"local";} result.push(f()); } result.push(f()); resultresult; } test(true); //["local","local"] test(false); //["local"]
javascript没有块级作用域,所以内部函数f的作用域应该是整个test函数。一些javascript环境确实如此,但并不是所有javascript环境都这样,javascript实现在严格模式下将这类函数报告为错误(具有局部块函数声明的处于严格模式下的程序将报告成一个语法错误),有助于检测不可移植代码,为未来的标准版本在给局部块函数声明给更明智和可以的语义。针对这种情况,可以考虑在test函数内声明一局部变量指向全局函数f。