2.6 JavaScript代码复查实例
最近一位开发人员让我对他的代码进行复查并提供改进建议。虽然我并不是代码复查专家(不要被我上面所说的忽悠),我在这里还是给出我提出的问题和解决方案。
问题1
问题:函数和对象没经过任何类型校验就作为参数传递给其他函数。
回复:类型校验是保证输入类型的必要步骤,如果没有进行检查,可能就有输入类型(字符串、日期、数组等)不确定的风险,这些可以轻易地毁掉你未经防御处理的应用程序。对于函数,至少应该进行以下处理:
1.测试以确保传递的变量真实存在;
2.进行typeof检查以阻止执行的输入为非有效函数。
不幸的是,简单的typeof检查是不够的,正如Angus Croll在“Fixing the typeof operator ”中指出,在对包括函数在内的许多内容进行typeof检查时需要注意大量细节。
例如,对空返回对象进行typedef检查在技术上是错误的。实际上,对于除了函数之外的任何对象类型进行typedef检查时,都会返回对象而不区分它们是数组、日期、RegEx还是什么。
可以利用Object.prototype.toString来调用JavaScript内部对象的属性,即[Class],也就是对象的类属性。不幸的是,内置对象通常会覆盖Object.prototype.toString,但是可以对它们加上通用的toString函数:
Object.prototype.toString.call([1,2,3]); //"[object Array]"
你可能也会发现下面Angus的函数是比typeof更适合的选择,对对象、数组以及其他类型调用betterTypeOf()函数来看看会发生什么。
这里,parseInt()函数被盲目地用来解析用户输入的整数值却没有指定基,这样会引起麻烦。
在"JavaScript:the Good Parts "中,Douglas Crockford指出parseInt()函数的调用是非常危险的。尽管你知道输入字符串变量会返回整数,也应该指定一个基作为第二个变量,否则会返回意想不到的输出,考虑下面的例子:
你会对多少开发人员忽略第二个参数感到吃惊,但实际上这经常发生。记住使用者(如果允许自由输入数值)并不一定会根据标准的数值惯例来输入(因为他们太疯狂了!)。我见过020、29319.jpg20、;29337.jpg20以及其他许多输入方式,所以尽可能为各种方式的输入值进行解析,下列使用parseInt()函数的方式偶尔会更好:
问题2
问题:在整个代码库上重复检查是否满足特定于浏览器的条件(例如:特性监测,检查支持的ES5特性等)。
回复:理想情况下,应保持代码库尽可能的“干燥”,有一些好的解决方案可以解决这个问题。例如,可以从加载时间配置模式(也称为加载时间和初始化时间分支)中获益。基本思想是仅测试条件一次(加载应用时)然后在后续检查中来调用这个结果。这种模式在JavaScript库文件中很常见,这些JavaScript库文件在加载时会自我配置,以针对具体浏览器进行优化。
这种模式可以这样实现:
下面的例子演示了如何规范化得到XMLHttpRequest对象。
有一个很著名的例子,Stoyan Stefanov运用这个来添加和删除跨浏览器的事件监听器,在他的《JavaScript Patterns》一书中有介绍。
问题3
问题:定期扩展本机Object.prototype。
回复:扩展本机类型经常会出问题,很少有(如果有的话)著名的代码库敢于扩展Object.prototype类型。事实是并没有一定要扩展它的情况存在。除非是要破坏JavaScript代码中的对象散列表及增加命名冲突可能性,这种扩展的操作一般被认为是糟糕的,这种操作应该是最后选择项(这同扩展自定义对象属性大有不同)。
如果因为某种原因你需要结束扩展对象原型,确保该方法已经不存在并拟出文件使小组中其他成员知道为什么需要这样做,你可以使用以下代码作为指导:
Juriy Zaytsev 有一篇关于“扩展本机和主机对象 ”的非常著名的帖子,可能你会感兴趣。
问题4
问题:有些代码严重阻塞页面,因为它在进行任何进一步操作之前都要等待进程完成或数据加载。
回复:页面阻塞导致用户使用体验差,有很多不损坏应用的解决方法。
一个解决方法是使用“延迟执行”(通过“许诺”和“将来”的概念)。“许诺”的基本思想是与其让某些调用占用资源,不如直接返回一个“将来”会实现的“许诺”。这样将允许编写可异步运行的非阻塞逻辑。常见的做法是在方程中引入一个调用,当请求完成时执行。
我曾经和Julian Aubourg写过一篇全面介绍这种方法的帖子,如果你对通过jQuery实现它感兴趣可以看看这篇帖子。当然也可以利用JavaScript实现。
微框架Q 提供了一个一般性的JS-兼容的“许诺”、“将来”实现方案,它相对而言比较全面,具体如下:
如果你想找一些更基础的可通读程序,这里是Douglas Crockford关于“许诺”的实现方法:
问题5
问题:通常使用“= =”操作符测试某一属性的显式数值等式,但应该使用的是“= = =”操作符。
回复:正如你可能知道也可能不知道的,“= =”操作符在JavaScript中的使用非常自由,即使两个量的值是完全不同的类型也会认为它们相等。这是因为该操作符会优先进行强制类型转换而不是比较,“= = =”却是在两个类型不一样的情况下不会进行强制类型转换,因而会报错。
我之所以在特定类型比较(本例)时更多地推荐使用“= = =”操作符,是因为“= =”操作符有许多陷阱并被许多开发人员认为是不可靠的。
你可能想知道在抽象化的语言(如CoffeeScript)中,由于其不可靠性,“= =”操作符的使用率相对“= = =”完全处于下风。
与其听我片面之言,不如看看下面运用“= =”进行布尔相等性检查的例子,该例子运行会产生无法预期的结果。
上面列表中许多结果等于true,因为JavaScript是一种弱类型化的语言:它尽量多地使用强制类型转换。如果你对上述表达式等于true的原因感兴趣,可以参阅《Annotated ES5指导》,其中的解释更为精彩。
回到复查上面来,如果100%确信进行比较的量不会被用户干扰,可以谨慎地使用“= =”操作符。一定记住,如果有非预期的输入,使用“= = =”操作符会更好。
问题6
问题:非缓存的数组长度被用于所有的for循环中是非常糟糕的,因为你在利用它遍历整个元素集合。
这里有个例子:
回复:这种方法(我依然看到许多开发人员在使用)的问题在于该数组长度在每个循环的迭代中被不必要的重复访问。这会导致程序运行非常慢,尤其是用在HTMLCollection上时(在这种情况下,正如Nicholas C. Zakas在《 High-Performance JavaScript》一书中提到的,对长度进行缓存可以比反复访问它快上190倍)。以下是对数组长度进行缓存的一些方法。
如果你想研究哪种方法表现最佳的话,使用jsPerf 对循环内外的数组捕捉、前缀增量使用、倒计时等进行测试以比较其性能优劣也是可行的。
问题7
问题:jQuery的$.each()函数用于遍历对象和数组,然而在某些情况下则使用for。
回复:在jQuery中,有两种方法可以无缝地遍历对象和数组。通用的$.each 可以遍历这两种类型,$.fn.each() 函数专门用于遍历jQuery对象(其中标准对象利用$()函数封装,你应该更倾向于使用后者)。低级别的$.each()函数执行效果比$.fn.each()函数好,标准的JavaScript for和while循环比这两个都要好,这是经jsPerf测试验证的。以下是一些运行情况也不错的循环:
你可能会发现,Angus Croll的帖子"Rethinking JavaScript for Loops"是对这些建议的一个有趣的延伸。
考虑一个以数据为中心的应用程序,每一个对象或数组都包含大量数据,你应该考虑进行重构来使用以上方法。从拓展性角度说,你应该尽可能地剔除浪费的毫秒数,因为当页面上有数以千计的元素时,时间会累积到很大。
问题8
问题:JSON字符串在内存中以字符串级联的方式建立。
回复:可以通过更优的方式来实现。例如,为什么不使用可以接收JavaScript对象并返回与JSON格式等效的JSON.stringify()函数呢?对象可以按照需要尽可能的复杂或者深度嵌套,这样将会产生更加简单、有效的解决方法。
额外的调试小技巧,如果你想要使得终端控制台显示的JSON更为美观可读,可以使用stringify()函数的以下额外参数实现:
问题9
问题:使用的命名空间模式在技术上是无效的。
回复:应用程序中其他部分使用的命名空间是正确的,而对其存在性的检查是无效的,现有:
问题在于 !MyNamespace会报错:ReferenceError。因为MyNamespace变量之前未经声明。较好的模式是利用内部变量声明布尔类型的强制转换,如下:
当然,可以通过其他许多方法来实现。如果你想阅读更多关于命名空间模式的内容(以及一些命名空间拓展的思路),可以参阅我最近写的"Essential JavaScript Namespacing Patterns"一文,Juriy Zaytsev也写过一篇关于命名空间模式非常全面的文章。