本文深入解析 JavaScript 运行时的性能瓶颈,涵盖循环结构选择、变量作用域影响、字符串与类型转换的高效写法,以及 DOM 节点创建与查询的优化策略。通过对比不同实现方式,提供提升脚本执行速度和内存管理的具体建议,帮助开发者编写更高效的代码。
循环结构的选择与优化
循环是编程中常用的控制结构,绝大多数逻辑处理都依赖它。在 JavaScript 中,主要存在 for(;;)、while() 和 for(in) 三种循环方式。实际上,for(in) 的效率极低,因为它需要查询散列键,因此只要条件允许,应尽量避免使用。相比之下,for(;;) 和 while 循环的性能在常规使用中基本等价。
然而,如何具体使用这两种循环却大有讲究。在测试中发现了一些有趣的现象,详见附录部分。最终得出的结论如下:
若循环变量是递增或递减操作,应避免单独对循环变量进行赋值,而应在最后一次读取时使用嵌套的 ++ 或 -- 操作符。
当循环条件涉及数组长度比较时,应预先将数组的 length 属性赋值给一个局部变量,从而减少重复查询的次数。
局部变量和全局变量
变量作用域与 Eval 的使用
局部变量的访问速度优于全局变量,因为全局变量实际上是全局对象的成员,而局部变量存储在函数的调用栈中,访问路径更短。
不使用 Eval
使用 eval 相当于在运行时重新调用解释引擎解析内容,这会消耗大量时间。此时,可以利用 JavaScript 支持的闭包来实现函数模板(关于闭包的详细内容请参考函数式编程相关章节)。
减少对象查找
对象查找与字符串处理
由于 JavaScript 的解释型特性,表达式 a.b.c.d.e 至少需要进行 4 次查询操作:先检查 a,再检查 a 中的 b,接着是 b 中的 c,依此类推。因此,如果此类表达式重复出现,应尽量减少其使用频率,可通过局部变量将其存入临时位置以加速查询。
这一点可与循环结合使用。我们经常根据字符串或数组的长度进行循环,而该长度通常是不变的。例如,每次查询 a.length 都会额外增加一次操作,若预先执行 var len=a.length,则可省去后续的查询开销。
字符串连接
在追加字符串时,最好使用 s+=anotherStr 操作,而非 s=s+anotherStr。
如果需要连接多个字符串,应减少连续使用 += 的操作,例如:
s+=a;s+=b;s+=c;
建议改写为:
s+=a + b + c;
如果是收集字符串,即多次对同一个字符串执行 += 操作,最好使用缓存机制。具体做法是使用 JavaScript 数组进行收集,最后通过 join 方法连接,如下所示:
var buf = new Array();for(var i = 0; i < 100; i++){ buf.push(i.toString());}var all = buf.join("");
类型转换
类型转换与直接量应用
类型转换是开发者常犯的错误,因为 JavaScript 是动态类型语言,无法显式指定变量类型。
1. 将数字转换为字符串,推荐使用 "" + 1。虽然写法略显不雅,但实际效率最高。从性能角度比较:
("" +) > String() > .toString() > new String()
这与下文的“直接量”原理类似,尽量使用编译时即可确定的内部操作,优于运行时由用户定义的操作。
String() 属于内部函数,因此速度较快;而 .toString() 需要查询原型中的函数,速度稍逊;new String() 用于返回精确的副本。
2. 浮点数转换为整型,这更容易出错。许多人喜欢使用 parseInt(),但实际上 parseInt() 用于将字符串转换为数字,而非在浮点数和整型之间转换。正确的做法是使用 Math.floor() 或 Math.round()。
此外,与第二节对象查找中的问题不同,Math 是内部对象,因此 Math.floor() 几乎没有查询和调用的时间开销,速度最快。
3. 对于自定义对象,如果定义了 toString() 方法进行类型转换,推荐显式调用 toString()。因为内部操作在尝试所有可能性后,会尝试对象的 toString() 方法以转换为 String,直接调用该方法效率更高。
使用直接量
其实这一点对性能影响较小,可以忽略。所谓使用直接量,例如 JavaScript 支持使用 [param,param,param,...] 直接表达数组。以往我们常使用 new Array(param,param,...),前者由引擎直接解释,后者需调用 Array 内部构造器,因此前者略快。
同样,var foo = {} 的方式比 var foo = new Object(); 更快,var reg = /../; 也比 var reg=new RegExp() 快。
字符串遍历操作
字符串遍历与高级对象管理
对字符串进行循环操作(如替换、查找)时,应使用正则表达式。因为 JavaScript 本身的循环速度较慢,而正则表达式的操作由 C 语言编写的 API 实现,性能更优。
高级对象
自定义高级对象和 Date、RegExp 对象在构造时会消耗大量时间。如果可以复用,应采用缓存方式。
DOM 相关
DOM 操作与节点创建
插入 HTML
许多开发者喜欢在 JavaScript 中使用 document.write 来生成页面内容。事实上,这种方式效率较低。如果需要直接插入 HTML,应选择一个容器元素(如 div 或 span),并设置其 innerHTML 属性来插入代码。
对象查询
使用 [“”] 查询比 .items() 更快,这与前文减少对象查找的思路一致,调用 .items() 会增加一次查询和函数调用。
创建 DOM 节点
通常我们可能会使用字符串直接编写 HTML 来创建节点,但这样做
无法保证代码的有效性
字符串操作效率低
因此,应使用 document.createElement() 方法。如果文档中存在现成的样板节点,应使用 cloneNode() 方法,因为使用 createElement() 后需多次设置元素属性,而 cloneNode() 可减少属性设置次数——同样,如果需要创建大量元素,应预先准备一个样板节点。
定时器与脚本引擎差异
定时器
如果针对的是不断运行的代码,不应使用 setTimeout,而应使用 setInterval。setTimeout 每次都需要重新设置一个定时器。
其他
脚本引擎
据测试,Microsoft 的 JScript 效率较 Mozilla 的 Spidermonkey 差很多,无论是执行速度还是内存管理,因为 JScript 目前已基本停止更新。但 SpiderMonkey 无法使用 ActiveXObject。
文件优化
文件优化与总结
文件优化也是一个有效手段,删除所有空格和注释,将代码放入一行内,可以加快下载速度。注意,这是下载速度的提升,而非解析速度。在本地环境中,注释和空格不会影响解释和执行速度。
总结
本文总结了我在 JavaScript 编程中发现的提高运行性能的一些方法,这些经验主要基于以下几条原则:
直接拿手头现成的东西比较快,例如局部变量比全局变量快,直接量比运行时构造对象快等。
尽可能减少执行次数,例如预先缓存需要多次查询的数据。
尽可能使用语言内置的功能,例如字符串连接。
尽可能使用系统提供的 API,因为这些 API 是编译好的二进制代码,执行效率很高。
同时,一些基本的算法优化同样适用于 JavaScript,例如运算结构的调整,此处不再赘述。但由于 JavaScript 是解释型语言,通常不会在运行时对字节码进行优化,因此这些优化仍然非常重要。
当然,这里的一些技巧同样适用于其他一些解释型语言,大家可以参考。