本文深入解析JavaScript中Function.apply()的核心用法,通过Math.max求最大值和数组合并两个实例,展示其相比传统循环的优雅与高效。结合20万数据测试,apply方案性能提升约5倍。此外,文章还总结了循环优化、变量作用域、类型转换及DOM操作等提升JS脚本性能的关键技巧,帮助开发者编写更高效的代码。
Function.apply()核心用法与性能优势
接下来探讨如何利用Function.apply()来增强程序运行效率。
首先从Math.max()函数入手,该函数支持接收任意数量的参数,并返回其中的最大值。
例如:
alert(Math.max(5,8)) //8
alert(Math.max(5,7,9,3,1,6)) //9
然而,在实际开发中,我们往往需要获取数组内的最大元素。
var arr=[5,7,9,1]
alert(Math.max(arr)) // 这样却是不行的。一定要这样写
function getMax(arr){
var arrLen=arr.length;
for(var i=0,ret=arr[0];i<arrLen;i++){
ret=Math.max(ret,arr[i]);
}
return ret;
}
常规写法不仅繁琐且效率低下。若采用apply方法,参考以下代码:
function getMax2(arr){
return Math.max.apply(null,arr)
}
两段代码实现相同功能,但getMax2版本更为简洁、优雅且高效。
以下是性能对比测试:
getMax性能测试
var myArr=new Array()
function fillRnd(arrLen){ //填入 arrLen个1-10的随机数字到数组
for(var i=0,arr=[];i<arrLen;i++){
arr[i]=Math.ceil(Math.random()*10)
}
return arr
}
function getMax(arr){
var arrLen=arr.length;
for(var i=0,ret=arr[0];i<arrLen;i++){
ret=Math.max(ret,arr[i]);
}
return ret;
}
function getMax2(arr){
return Math.max.apply(null,arr)
}
myArr=fillRnd(20*10000) //生成20万个随机数填到数组
var t1=new Date()
var max1=getMax(myArr)
var t2=new Date()
var max2=getMax2(myArr)
var t3=new Date()
if (max1!==max2) alert("error")
alert([t3-t2,t2-t1]) //在我机器上 96,464 .不同的机器,结果可能有差异
经20万条数据测试,getMax2耗时96ms,而getMax耗时464ms,两者性能差距达5倍。
再看数组的push方法示例。
var arr1=[1,3,4];
var arr2=[3,4,5];
若需将arr2展开并逐一追加至arr1,最终使arr1=[1,3,4,3,4,5]。
直接执行arr1.push(arr2)无法达成目标,结果将变为[1,3,4, [3,4,5]]。
通常需借助循环逐个push(也可用arr1.concat(arr2),但concat不修改原数组)。
var arrLen=arr2.length
for(var i=0;i<arrLen;i++){
arr1.push(arr2[i])
}
引入Apply后,处理变得异常简便。
Array.prototype.push.apply(arr1,arr2)
JavaScript脚本性能优化综合指南
附录:JavaScript脚本性能优化策略
随着网络带宽与硬件性能的提升,富客户端技术日益普及,Ajax成为主流。JavaScript作为解释型语言,其执行效率难以与C/Java等编译型语言媲美,限制了客户端处理能力。为改善性能,基于过往大量测试经验,本文分享优化JavaScript脚本的实用建议。
语言层面优化:循环与变量
语言层面优化
循环结构
循环是常用的控制结构,多数功能依赖其实现。JavaScript提供for(;;)、while()和for(in)三种循环。其中,for(in)因需查询散列键,效率极低,应尽量避免使用。for(;;)与while()在常规使用下性能基本等价。
然而,具体使用方式对性能影响显著。测试中发现有趣现象(见附录),结论如下:
若循环变量递增或递减,避免单独赋值,应在最后一次读取时使用嵌套的++或--操作符。
若需与数组长度比较,应预先将数组length属性存入局部变量,以减少查询次数。
局部与全局变量
局部变量访问速度优于全局变量,因为全局变量实为全局对象成员,而局部变量存储于函数栈中。
避免使用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()快。
高级对象与DOM操作优化
字符串遍历操作
对字符串进行循环操作(如替换、查找),应使用正则表达式。因JavaScript循环速度较慢,而正则表达式由C语言API实现,性能优异。
高级对象
自定义高级对象与Date、RegExp对象构造时消耗大量时间。若可复用,应采用缓存方式。
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为解释型,运行时一般不优化字节码,故这些优化仍重要。
当然,部分技巧同样适用于其他解释型语言,供参考。
因测试代码不全,补充部分如下:
var print;
if(typeof document != "undefined" ){
print = function(){
document.write(arguments[0]);
}
}else if(typeof WScript != "undefined" ){
print = function(){
WScript.Echo(arguments[0],arguments[1],arguments[2]);
}
}
function empty(){
}
function benchmark(f){
var i = 0;
var start = (new Date()).getTime();
while(i < pressure){
f(i++);
}
var end = (new Date()).getTime();
WScript.Echo(end-start);
}
/*
i=0
start = (new Date()).getTime();
while(i < 60000){
c = [i,i,i,i,i,i,i,i,i,i];
i++;
}
end = (new Date()).getTime();
WScript.Echo(end-start);
i=0
start = (new Date()).getTime();
while(i < 60000){
c = new Array(i,i,i,i,i,i,i,i,i,i);
i++;
}
var end = (new Date()).getTime();
WScript.Echo(end-start);
*/
function internCast(i){
return "" + i;
}
function StringCast(i){
return String(i)
}
function newStringCast(i){
return new String(i)
}
function toStringCast(i){
return i.toString();
}
function ParseInt(){
return parseInt(j);
}
function MathFloor(){
return Math.floor(j);
}
function Floor(){
return floor(j);
}
var pressure = 50000;
var a = "";
var floor = Math.floor;
j = 123.123;
print("-------------\nString Conversion Test");
print("The empty:", benchmark(empty));
print("intern:", benchmark(internCast));
print("String:");
benchmark(StringCast);
print("new String:");
benchmark(newStringCast);
print("toString:");
benchmark(toStringCast);
print("-------------\nFloat to Int Conversion Test");
print("parseInt");
benchmark(ParseInt);
print("Math.floor");
benchmark(MathFloor);
print("floor")
benchmark(Floor);
function newObject(){
return new Object();
}
function internObject(){
return {};
}
print("------------\nliteral Test");
print("runtime new object", benchmark(newObject));
print("literal object", benchmark(internObject));
附录2
代码1:
for(var i=0;i<100;i++){
arr[i]=0;
}
代码2:
var i = 0;
while(i < 100){
arr[i++]=0;
}
代码3:
var i = 0;
while(i < 100){
arr[i]=0;
i++;
}
在Firefox下测试这两段代码,结果代码2优于代码1和3,代码1一般优于代码3,有时被代码3超过;而在IE 6.0下,测试压力较大时(如10000次以上),代码2和3有时优于代码1,有时远落后;测试压力较小时(如5000次),代码2>代码3>代码1。
代码4:
var i = 0;
var a;
while(i < 100){
a = 0;
i++;
}
代码5:
var a;
for(var i=0;i<100;i++){
a = 0;
}
上述两段代码在Firefox和IE下测试结果性能接近。
代码6:
var a;
var i=0;
while(i<100){
a=i;
i++;
}
代码7:
var a;
var i=0;
while(i<100){
a=i++;
}
代码8:
var a;
for(var i=0;i<100;i++){
a = i;
}
代码9:
var a;
for(var i=0;i<100;){
a = i++;
}
这四段代码在Firefox下6和8性能接近,7和9性能接近,而6, 8 < 7, 9;
最后查看空循环测试
代码10:
for(var i=0;i<100;i++){ }
代码11:
var i;
while(i<100){ i++; }
最后测试出现神奇结果,Firefox下代码10耗时与代码11耗时约24:1。故无参考价值,未放在开头展示。