jQuery.grep与Array.filter的性能

在一个问题上 ,讨论了jQuery和本地JS如何相互执行。

虽然香草解决scheme执行速度快很多,因为它不处理整个数组,我提出使用Array.filter ,我相当有信心至less会比$.grep更快。

令人惊讶的是,在添加到testing后,我被教了一课:testing套件

当然Edgecases有不同的结果。

任何人有一个想法,为什么$.grep应该比本地方法Arrray.filter快3倍?

编辑:我修改了testing使用从MDN的filter垫片,结果是非常有趣的:

  • Chrome:即使是MDN填充也比本地方法快,jQuery的方式比以前更快
  • Firefox:比本地方法稍微慢一点,jQuery领先一步

最后是我希望看到的结果

  • Internet Explorer:本地方法是最快的,然后jQuery,垫片是最慢的(也许这只是IE浏览器相当脆弱的JS引擎的结果…)

正如在这篇博客文章 (也做同样的testing)中发现的:

如果你阅读filter的文档,你会明白为什么它慢得多。

  1. 它会忽略数组中已删除的值和空位
  2. 它可以select设置谓词函数的执行上下文
  3. 它阻止谓词函数改变数据

ECMAScript 5.1规范的第15.4.4.20节定义了Array.prototype.filter(callbackfn, thisArg) ,如下所示:

callbackfn应该是一个接受三个参数的函数,并返回一个强制为布尔值truefalse值。 filter按升序为数组中的每个元素调用一次callbackfn ,并为callbackfn返回true构造一个所有值的新数组。 callbackfn仅用于实际存在的数组的元素; 它不会被要求丢失数组的元素。

如果提供了一个thisArg参数,它将被用作每次调用callbackfnthis值。 如果没有提供,则使用undefined

使用三个参数callbackfn :元素的值,元素的索引和被遍历的对象。

filter器不会直接改变它被调用的对象,但是对象可能会被callbackfn的调用所突变。

filter处理的元素范围在第一次调用callbackfn之前设置。 调用filter之后附加到数组的元素将不会被callbackfn访问。 如果数组中的现有元素发生更改,则传递给callbackfn的值将是filter访问它们时的值; 在过滤的调用开始之后和被访问之前被删除的元素不被访问。

这本身已经是很多工作了; ECMAScript引擎需要执行的很多步骤。

然后它继续说:

当使用一个或两个参数调用filter方法时,将采取以下步骤:

O是调用ToObject传递this值作为参数的结果。 让lenValue是用参数length调用O[[Get]]内部方法的结果。 让lenToUint32(lenValue) 。 如果IsCallable(callbackfn)为false,则引发TypeErrorexception。 如果提供了这个Arg,让T为这个Arg; 否则让T不确定。 设A是一个新创build的数组,如同使用expression式new Array()一样,其中Array是具有该名称的标准内置构造函数。 令k为0.令为0.重复,而k <len让Pk为ToString(k)。 假设kPresent是使用参数Pk调用O的[[HasProperty]]内部方法的结果。 如果kPresent为真,那么令kValue是用参数Pk调用O的[[Get]]内部方法的结果。 select是调用callbackfn的[[Call]]内部方法的结果,T为包含kValue,k和O的此值和参数列表。如果ToBoolean(selected)为true,则调用[[DefineOwnProperty]] A的内部方法ToString(to),属性描述符{[[Value]]:kValue,[[Writable]]:true,[[Enumerable]]:true,[[Configurable]]:true}和false。 增加1.将k增加1.返回A.

过滤方法的长度属性为1。

注意filterfunction是有意通用的; 它不要求它的这个值是一个Array对象。 因此可以将其转换为其他types的对象用作方法。 filter函数是否可以成功应用于主机对象取决于实现。

有关此algorithm的一些注意事项:

  • 它阻止谓词函数改变数据
  • 它可以select设置谓词函数的执行上下文
  • 它会忽略数组中已删除的值和空位

在很多情况下,这些东西都不需要。 所以,在编写自己的filter方法时,大部分时间你都不会去执行这些步骤。

每个符合ES5.1的JavaScript引擎必须符合该algorithm,因此每次使用Array#filter时都必须执行所有这些步骤。

任何只执行这些步骤的一部分的自定义编写的方法将会更快一点也不奇怪:)

如果你编写自己的filter函数,很可能不会像上面的algorithm那么复杂。 也许你不会把数组转换成一个对象,根据用例,它可能不需要只是过滤数组。

我发现了一些有趣的东西。 正如MarcoK所说,$ .grep只是一个简单的for循环实现。 filter在大多数情况下是较慢的,所以实现必须不同。 我想我find了答案:

 function seak (e) { return e === 3; } var array = [1,2,3,4,5,6,7,8,9,0], i, before; array[10000] = 20; // This makes it slow, $.grep now has to iterate 10000 times. before = new Date(); // Perform natively a couple of times. for(i=0;i<10000;i++){ array.filter(seak); } document.write('<div>took: ' + (new Date() - before) + '</div>'); // took: 8515 ms (8s) before = new Date(); // Perform with JQuery a couple of times for(i=0;i<10000;i++){ $.grep(array, seak); } document.write('<div>took: ' + (new Date() - before) + '</div>'); // took: 51790 ms (51s) 

本地“filter”在这种情况下要快得多。 所以我认为它迭代了属性而不是数组索引。

现在让我们回到“大”的问题;)。

你的脚本不对?

对于array.filter您正在进行1000次的测量,并将总和除以1000

对于JQuery.grep您正在进行一次测量,并通过将总和除以1000来表示。

这意味着您的grep实际上比您用于比较的值要慢1000倍。

在Firefox中快速testing给出:

 Machine 1: average filter - 3.864 average grep - 4.472 Machine2: average filter - 1.086 average grep - 1.314 

在铬的快速testing给:

 Machine 1: average filter - 69.095 average grep - 34.077 Machine2: average filter - 18.726 average grep - 9.163 

在firefox(50.0)中的结论是你的代码path快得多,filter比jquery.grep快大约10-15%。

Chrome的代码path非常慢,但是grep似乎比array.filter快了50%,比firefox运行慢了900%。

TLDR; Grep速度更快…(提示为什么可以在这里find )

它看起来像我.filter强制它是这样的对象,检查callbackIsCallable,并在其中设置它,以及在每次迭代检查属性的存在,而.grep假定和跳过这些步骤,这意味着有一点点less进行。

这是我用来testing的脚本:

 function test(){ var array = []; for(var i = 0; i<1000000; i++) { array.push(i); } var filterResult = [] for (var i = 0; i < 1000; i++){ var stime = new Date(); var filter = array.filter(o => o == 99999); filterResult.push(new Date() - stime); } var grepResult = []; var stime = new Date(); var grep = $.grep(array,function(i,o){ return o == 99999; }); grepResult.push(new Date() - stime); $('p').text('average filter - '+(filterResult.reduce((pv,cv)=>{ return pv +cv},0)/1000)) $('div').text('average grep - '+(grepResult.reduce((pv,cv)=>{ return pv + cv},0)/1000)) } test(); 
 <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script> <p></p> <div></div>