Create a range of natural numbers with spread syntax - javascript

On the one hand, a lack of an equivalent to Python 3's range is an annoyance in ES6. On the other hand, there are lots of workarounds. My question is why one workaround I tried actually works. To illustrate:
[...Array(10).keys()];
In case the reason I find this mysterious is not obvious, note that Array(10).keys() is at least apparently empty.
I'm aware this wastefully creates two arrays, as do most of the popular workarounds, and, that (at the cost of creating a generator function) using a generator can avoid that. E.g.,
[...(function*(){let i = 0; while(i<10) yield i++;})()];
My question is only about why the first workaround produces the desired result.
Edit:
Judging from the answers, some people believe that the evaluation of Array(10) is equivalent to the evaluation of Array.apply(null,Array(10)). They are not. For example, .hasOwnProperty(0) is false for the former but true for the latter. However, I am open to being persuaded they are the same in some way that matters here, since my understanding is clearly lacking at some key point. I suspect that the answer is that the result of iterating over the keys is determine by the length property, which both share, rather than the actual array indexes that have been defined. If so, I would like to know that this behavior is normative.

Array#keys returns an Array iterator. Spread syntax then completely exhausts that iterator by accessing the next value in the iterator until there are no more values. Then it collects all the values from the iterator and spreads them into a new array.
Array(10) creates an array exotic object that does not have any actual integer-indexed keys, but just a length property -- so it would 'empty' but Array(10).keys() is not. The fact is that using Array#keys doesn't depend on actual elements, just the length property. The internal operation CreateArrayIterator creates a key iterator from an array by creating an iterator via the intrinsic %ArrayIteratorPrototype% object. Looking at %ArrayIteratorPrototype%.next(), you'll see that the length of the array is used. For Array#keys the index is continually incremented until it reaches the length of the array. That's how the iterator is created that gives you all the keys of the array without actually having said integer keys in the first place.
If you're curious about the abstract steps, see Section 12.2.5.2 ArrayAcculumation of the ECMAScript Language Specification, particularly the SpreadElement : ... AssignmentExpression production which outlines the process of stepping through an iterator that is used in conjunction with spread syntax.
To see the abstract steps for collecting these values into a new array, see Section 12.2.5.3 Evaluation. Specifically, the ArrayLiteral : [ ElementList ] production is the production [...Array.keys()] falls under. The aforementioned ArrayAcculumation process is performed which aggregates iterates through the iterator and sets them into the new array.

Array.prototype.keys is returning a new Array Iterator.
Array(10) is an array of 10 (empty) slots.
When you spread the array's keys, you are iterating over it and creating a new array. But this time the new array's items are the slots from the first array.
You can see it with for of loop:
const arr = Array(10).keys();
for (let key of arr) {
console.log(key);
}
By the way, you can use Array.from which takes a map function as it's second arguments. so you can just return the index or whatever you want:
const arr = Array.from(Array(10), (_, idx) => idx);
console.log(arr);
Edit
As a followup to your edited question about the Array(10) result:
The DOCS mentions:
If the only argument passed to the Array constructor is an integer
between 0 and 232-1 (inclusive), this returns a new JavaScript array
with its length property set to that number (Note: this implies an
array of arrayLength empty slots, not slots with actual undefined
values). If the argument is any other number, a RangeError exception
is thrown.
So you actually get:
A new array
The length property is updated with the number provided to the
function
You get n-slots
Now .keys will create an Iterator, which will return each slot index as the value.
for example:
let it = Array(10).keys();
and calling:
it.next();
Will return:
{value: 0, done: false}
Calling it again and again will yield
{value: n, done: false}
up until it will get to the last slot:
{value: 9, done: false}
then the next .next call will return
{value: undefined, done: true}
which will flag that this is the end of the iterations.
running example:
const it = Array(10).keys();
console.log(it.next());
console.log(it.next());
console.log(it.next());
console.log(it.next());
console.log(it.next());
console.log(it.next());
console.log(it.next());
console.log(it.next());
console.log(it.next());
console.log(it.next());
console.log(it.next()); // done: true

Array(10) creates a length-10 array, each element of which is undefined.
Array(10).keys() returns an iterator, which iterates over the keys of the array...the numbers 0 through 9.
When the spread syntax is used to define an array literal (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_operator), it fully-iterates any iterator it's given and adds the returns from the iterator to the created array (in order).

Related

forEach loop quirky behavior with undefined values?

Was writing a script in JS to make some dummy data for testing my API and ran into an interesting quirk with the forEach loop in JS.
const dictionary = {};
const undefinedArray = Array(3); // [undefined, undefined, undefined]
undefinedArray.forEach((_, index) => {
console.log('Logging at index: ', index)
const someObject = { id: index };
if (!dictionary[someObject.id]) {
dictionary[someObject.id] = someObject
}
});
console.log(dictionary);
After checking the output of this snippet, you'll see that nothing inside the forEach loop is logged and the dictionary is still an empty object. I was talking with my coworker about this behaviour and he said he ran into this particular issue before and offered this as a solution.
const dictionary = {};
const undefinedArray = [...Array(3)]; // [undefined, undefined, undefined]
undefinedArray.forEach((_, index) => {
console.log('Logging at index: ', index)
const someObject = { id: index };
if (!dictionary[someObject.id]) {
dictionary[someObject.id] = someObject
}
});
console.log(dictionary);
By wrapping the Array constructor in square brackets and utilizing the spread operator, now the array is looped through and the object is built correctly. This fascinated me, so I went to the documentation for the Array object and found this:
arrayLength
If the only argument passed to the Array constructor is an integer between 0 and 2^32 - 1 (inclusive), this returns a new JavaScript array with its length property set to that number (Note: this implies an array of arrayLength empty slots, not slots with actual undefined values). If the argument is any other number, a RangeError exception is thrown.
So apparently it is not assigning each value undefined, but only setting its length property to whatever is passed in the constructor. This is not apparent when you log Array(n) to the console because it shows an array with n undefined values. I assume the toString method for the Array object is based on its length property and uses a normal for or for of loop to construct the string.
It does begin to make a little bit more sense, however, when you explicitly set an index of the newly defined array. In the snippet below, the same array is initialized, but the zero index is explicitly assigned undefined as a value. Since this is an "actual undefined value" in Mozilla's words, the forEach loop exectues at index zero.
const dictionary = {};
const undefinedArray = Array(3); // [undefined, undefined, undefined]
undefinedArray[0] = undefined
undefinedArray.forEach((_, index) => {
console.log('Logging at index: ', index)
const someObject = { id: index };
if (!dictionary[someObject.id]) {
dictionary[someObject.id] = someObject
}
});
console.log(dictionary);
Array.map() behaves the same way. So I guess my main question would be, are there other ways to execute forEach and map without filling the array or by using the quirky hack I mentioned earlier?
To recap: these are the two work arounds I've found for this particular use case:
[...Array(n)] OR Array(n).fill(). Both of these mutations to the array will allow a forEach loop to iterate over all values in the array.
So apparently it is not assigning each value undefined, but only setting its length property to whatever is passed in the constructor.
Correct. (Provided you pass only a single argument and it's a number. If you pass a non-number, or pass more than one argument, they're used as elements for the array. So Array("3") results in ["3"]; Array(3, 4) results in [3, 4].)
This is not apparent when you log Array(n) to the console because it shows an array with n undefined values.
It depends on what console you use. The devtools in Chromium browsers show (3) [empty x 3] for exactly that reason, to differentiate between empty array slots and ones containing the value undefined.
So I guess my main question would be, are there other ways to execute forEach and map without filling the array or by using the quirky hack I mentioned earlier?
If you want forEach and map to visit elements of the array, they have to actually exist. Those methods (and several others) are defined such that they don't call your callback for empty slots in sparse arrays. If by "quirky hack" you mean [...Array(3)], that's also filling the array (and is fully-specified behavior: [...x] uses the iterator x provides, and the array iterator is defined that it yields undefined for empty slots rather than skipping them as forEach, map, and similar do). Doing that (spreading the sparse array) is one way to create an array filled with undefined (not empty) elements. Array.fill is another. Here's a third: Array.from({length: 3})
const a = Array.from({length: 3});
a.forEach(value => {
console.log(`value = ${value}`);
});
Which you use is up to you. Array.from is very simple and direct. Similarly Array(3).fill(). I probably wouldn't use the spread version (just because I think it's fairly unclear to people who don't have a deep knowledge of how the array iterator works), but it's a matter of style.

Why does spread syntax convert my string into an array?

Why does spread syntax convert my string into an array?
var v = 'hello';
var [, ...w] = v; // ["e", "l", "l", "o"]
Why is w not a string?
Spread syntax (actually a punctuator as noted by RobG) allows for iterables to be spread into smaller bits. Since strings are iterables (they're character arrays internally, more specifically ordered sequences of integers representing characters), they can be spread into individual characters.
Next, destructuring assignment is performed on the array to unpack and group the spread values. Since you ommit the first element of the character array with , and don't assign a reference, it's lost, and the rest of the iterable object is saved into w, spread into it's individual parts, single characters of the character array.
The specific semantics of this operation are defined in the ECMAScript 2015 Specification by the ArrayAssignmentPattern : [ Elisionopt AssignmentRestElement ] production:
12.14.5.2 Runtime Semantics: DestructuringAssignmentEvaluation
with parameter value
[...]
ArrayAssignmentPattern : [ Elisionopt AssignmentRestElement ]
Let iterator be GetIterator(value).
ReturnIfAbrupt(iterator).
Let iteratorRecord be Record {[[iterator]]: iterator, [[done]]: false}.
If Elision is present, then
a. Let status be the result of performing IteratorDestructuringAssignmentEvaluation of Elision with iteratorRecord as the argument.
b. If status is an abrupt completion, then
    i. If iteratorRecord.[[done]] is false, return IteratorClose(iterator, status).
    ii. Return Completion(status).
Let result be the result of performing IteratorDestructuringAssignmentEvaluation of AssignmentRestElement with iteratorRecord as the argument.
If iteratorRecord.[[done]] is false, return IteratorClose(iterator, result).
Return result.
Here, Elision refers to an omitted element when spreading with one or more commas (,), comparable to omitted syllables as the name suggests, and AssignmentRestElement refers to the target that will receive the spread and destructured values, w in this case.
What this does is first get the iterator of the object, from the internal ##iterator method and steps through that iterator, skipping however many elements indicated by the elision's width by the Elision production in IteratorDestructuringAssignmentEvaluation. Once that's done, it will step through the iterator of the AssignmentRestElement production, and assign a new array with all the spread values -- that's what w is. It receives the spread out single-character array, unpacked to exclude the first character.
The ##iterator method in which the iteration is gotten from is a well-known Symbol and changing it for an object can change how it's iterated, as in Emissary's answer. Specifically, the default implementation of the ##iterator method for String is as follows:
21.1.3.27 String.prototype [ ##iterator ]( )
When the ##iterator method is called it returns an Iterator object (25.1.1.2) that iterates over the code points of a String value, returning each code point as a String value.
Thus, the iterator allows for iteration through single code points, or characters of a string -- and consequently spreading the string will result in an array of its characters.
In ES2015 the spread syntax is specifically acting against the internal String ##iterator property - any object can be iterated in this way by assigning your own iterator or generator / function* to the obj[Symbol.iterator] property.
For example you could change the default behaviour of your new array...
const a = [...'hello'];
a[Symbol.iterator] = function* (){
for(let i=0; i<this.length; ++i)
yield `${this[i]}!`;
};
console.log([...a]);
You could change your string iterator too but you'd have to explicitly create a String object.
Spread syntax can be applied only to iterable objects. Since String is iterable Spread operator works fine and splits your char array(String) in to char's.
You can check that with below sample which demonstrate that String is iterable by default.
var s = 'test';
for (k in s) {
console.log(k);
}
And ECMAScript6 specification even mentioned about this specific String case.
Spread Operator
Spreading of elements of an iterable collection (like an array or even a string) into both literal elements and individual function parameters.
http://es6-features.org/#SpreadOperator
var str = "foo";
var chars = [ ...str ]; // [ "f", "o", "o" ]
And it is worth mentioning that this is a specific case and only happens when you use a direct String with spread operator. When you give a single String inside an array, the whole array will be treated as the iterable object and not the string inside.
var str = [ "hello" ,2 ];
var other = [ ...str ]; // [ "hello" ,2 ]
I know the above example doesn't make much sense, but just to convey the fact that the String will be treated differently in this case.
Because a string in javascript is sort of treated like an array of characters.
For example, when you do a for each loop over a string (let's say hello), with lodash:
_.forEach('hello', function(i) {
console.log(i)
})
it outputs:
h
e
l
l
o
Functions like slice() also work on both strings and arrays.
There are two things going on here.
First, you're using array destructuring. This allows you to take an iterable and assign values from it to individual variables.
Second, you're using a rest parameter. This converts any remaining output of the iterable into an array.
So your string 'hello' is iterated into its individual characters, the first one is ignored (because you omitted the destination), then the remaining characters are turned into an array and assigned to w.

Difference between Array.apply(null, Array(x) ) and Array(x)

What exactly is the difference between:
Array(3)
// and
Array.apply(null, Array(3) )
The first returns [undefined x 3] while the second returns [undefined, undefined, undefined]. The second is chainable through Array.prototype.functions such as .map, but the first isn't. Why?
There is a difference, a quite significant one.
The Array constructor either accepts one single number, giving the lenght of the array, and an array with "empty" indices is created, or more correctly the length is set but the array doesn't really contain anything
Array(3); // creates [], with a length of 3
When calling the array constructor with a number as the only argument, you create an array that is empty, and that can't be iterated with the usual Array methods.
Or... the Array constructor accepts several arguments, whereas an array is created where each argument is a value in the array
Array(1,2,3); // creates an array [1,2,3] etc.
When you call this
Array.apply(null, Array(3) )
It get's a little more interesting.
apply accepts the this value as the first argument, and as it's not useful here, it's set to null
The interesting part is the second argument, where an empty array is being passed in.
As apply accepts an array it would be like calling
Array(undefined, undefined, undefined);
and that creates an array with three indices that's not empty, but have the value actually set to undefined, which is why it can be iterated over.
TL;DR
The main difference is that Array(3) creates an array with three indices that are empty. In fact, they don't really exist, the array just have a length of 3.
Passing in such an array with empty indices to the Array constructor using apply is the same as doing Array(undefined, undefined, undefined);, which creates an array with three undefined indices, and undefined is in fact a value, so it's not empty like in the first example.
Array methods like map() can only iterate over actual values, not empty indices.
The .map() API does not iterate over completely uninitialized array elements. When you make a new array with the new Array(n) constructor, you get an array with the .length you asked for but with non-existent elements that will be skipped by methods like .map().
The expression Array.apply(null, Array(9)) explicitly populates the newly-created array instance with undefined, but that's good enough. The trick is whether or not the in operator will report that the array contains an element at the given index. That is:
var a = new Array(9);
alert(2 in a); // alerts "false"
That's because there really is no element at position 2 in the array. But:
var a = Array.apply(null, Array(9));
alert(2 in a); // alerts "true"
The outer call to the Array constructor will have explicitly populated the elements.
This is an artifact of how apply works. When you do:
new Array(9)
an empty array is created with a length of 9. map does not visit non–existent members, so does nothing at all. However, apply turns the array into a list using CreateListFromArrayLike so it turns the formerly empty array into a parameter list like:
[undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined];
that is passed to Array to create an array with 9 members, all with a value of undefined. So now map will visit them all.
BTW, ECMAScript 2015 has Array.prototype.fill for this (also see MDN) so you can do:
Array(9).fill(0);
Because the first array would not have ordered properties arr[0] === undefined and the second does. Array functions like forEach and map will iterate from 0 to the array's length - 1 and the lack of order to the properties of the first is an issue. The second version produces an array with the correct ordering, i.e.
arr = Array.apply(null, Array(3));
arr[0] === undefined //true
arr[1] === undefined //true
//etc.
The first version as you noticed doesn't. Also, adding new to the first version would not make it work.
In the first case you have one operation
Array(3)
Its creates an array with three empty slots. Not an array with the three undefined values but exactly - empty.
At the second case
Array.apply(null, Array(3) )
we can spread it to the three operations:
first: Array(3) - you get an array with 3 empty slots;
second: Array(3) spreads by Function.prototype.apply() function to 3 parameters that it passes to Array() function. At this stage 3 empty slots in given array transformes by apply() to 3 undefined values (it looks like if apply() sees an empty slot it automaticaly turns it to undefined in any sparsed array).
third: we get an Array(undefined, undefined, undefined). And that will do to us an array with 3 undefined (not empty) values.
Because now you have 3 undefined but not empty slots, you can use them with map() function.
Note that not only Function.prototype.apply() have such behavior of decomposing arrays by such way. You can also do this in ECMAScript 6 by "..." - spread operator.
Array(...new Array(3));
This will also returns an array with 3 undefined and respectively can be mapped slots.
Here i giving more detailed explanation.
https://stackoverflow.com/a/56814230/11715665

Is a JavaScript array index a string or an integer?

I had a generic question about JavaScript arrays. Are array indices in JavaScript internally handled as strings?
I read somewhere that because arrays are objects in JavaScript, the index is actually a string. I am a bit confused about this, and would be glad for any explanation.
Formally, all property names are strings. That means that array-like numeric property names really aren't any different from any other property names.
If you check step 6 in the relevant part of the spec, you'll see that property accessor expressions are always coerced to strings before looking up the property. That process is followed (formally) regardless of whether the object is an array instance or another sort of object. (Again, it just has to seem like that's what's happening.)
Now, internally, the JavaScript runtime is free to implement array functionality any way it wants.
edit — I had the idea of playing with Number.toString to demonstrate that a number-to-string conversion happens, but it turns out that the spec explicitly describes that specific type conversion as taking place via an internal process, and not by an implicit cast followed by a call to .toString() (which probably is a good thing for performance reasons).
That is correct so:
> var a = ['a','b','c']
undefined
> a
[ 'a', 'b', 'c' ]
> a[0]
'a'
> a['0']
'a'
> a['4'] = 'e'
'e'
> a[3] = 'd'
'd'
> a
[ 'a', 'b', 'c', 'd', 'e' ]
Yes, technically array-indexes are strings, but as Flanagan elegantly put it in his 'Definitive guide':
"It is helpful to clearly distinguish an array index from an object property name. All indexes are property names, but only property names that are integers between 0 and 232-1 are indexes."
Usually you should not care what the browser (or more in general 'script-host') does internally as long as the outcome conforms to a predictable and (usually/hopefully) specified result. In fact, in case of JavaScript (or ECMAScript 262) is only described in terms of what conceptual steps are needed. That (intentionally) leaves room for script-host (and browsers) to come up with clever smaller and faster way's to implement that specified behavior.
In fact, modern browsers use a number of different algorithms for different types of arrays internally: it matters what they contain, how big they are, if they are in order, if they are fixed and optimizable upon (JIT) compile-time or if they are sparse or dense (yes it often pays to do new Array(length_val) instead of ninja []).
In your thinking-concept (when learning JavaScript) it might help to know that arrays are just special kind of objects. But they are not always the same thing one might expect, for example:
var a=[];
a['4294967295']="I'm not the only one..";
a['4294967296']="Yes you are..";
alert(a); // === I'm not the only one..
although it is easy and pretty transparent to the uninformed programmer to have an array (with indexes) and attach properties to the array-object.
The best answer (I think) is from the specification (15.4) itself:
Array Objects
Array objects give special treatment to a certain class of property
names. A property name P (in the form of a String value) is an array
index if and only if ToString(ToUint32(P)) is equal to P and
ToUint32(P) is not equal to 232−1. A property whose property name is
an array index is also called an element. Every Array object has a
length property whose value is always a nonnegative integer less than
232. The value of the length property is numerically greater than the name of every property whose name is an array index; whenever a
property of an Array object is created or changed, other properties
are adjusted as necessary to maintain this invariant. Specifically,
whenever a property is added whose name is an array index, the length
property is changed, if necessary, to be one more than the numeric
value of that array index; and whenever the length property is
changed, every property whose name is an array index whose value is
not smaller than the new length is automatically deleted. This
constraint applies only to own properties of an Array object and is
unaffected by length or array index properties that may be inherited
from its prototypes.
An object, O, is said to be sparse if the following algorithm returns
true:
Let len be the result of calling the [[Get]] internal method of O with argument "length".
For each integer i in the range 0≤i<ToUint32(len)
a. Let elem be the result of calling the [[GetOwnProperty]] internal method of O with argument ToString(i).
b. If elem is undefined, return true.
Return false.
Effectively the ECMAScript 262 spec just ensures to the JavaScript-programmer unambiguous array-references regardless of getting/setting arr['42'] or arr[42] up to 32-bit unsigned.
The main difference is for example (auto-updating of) array.length, array.push and other array-sugar like array.concat, etc.
While, yes, JavaScript also lets one loop over the properties one has set to an object, we can not read how much we have set (without a loop). And yes, to the best of my knowledge, modern browsers (especially chrome in what they call (but don't exactly specify)) 'small integers' are wicked fast with true (pre-initialized) small-int arrays.
Also see for example this related question.
Edit: as per #Felix Kling's test (from his comment above):
After arr[4294967294] = 42;, arr.length correctly shows 4294967295. However, calling arr.push(21); throws a RangeError: Invalid array length. arr[arr.length] = 21 works, but doesn't change length.
The explanation for this (predictable and intended) behavior should be clear after this answer.
Edit2:
Now, someone gave the comment:
for (var i in a) console.log(typeof i) shows 'string' for all indexes.
Since for in is the (unordered I must add) property iterator in JavaScript, it is kind of obvious it returns a string (I'd be pretty darned if it didn't).
From MDN:
for..in should not be used to iterate over an Array where index order
is important.
Array indexes are just enumerable properties with integer names and
are otherwise identical to general Object properties. There is no
guarantee that for...in will return the indexes in any particular
order and it will return all enumerable properties, including those
with non–integer names and those that are inherited.
Because the order of iteration is implementation dependent, iterating
over an array may not visit elements in a consistent order. Therefore
it is better to use a for loop with a numeric index (or Array.forEach
or the for...of loop) when iterating over arrays where the order of
access is important.
So.. what have we learned? If order is important to us (often is with arrays), then we need this quirky array in JavaScript, and having a 'length' is rather useful for looping in numerical order.
Now think of the alternative: Give your objects an id/order, but then you'd need to loop over your objects for every next id/order (property) once again...
Edit 3:
Someone answered along the lines of:
var a = ['a','b','c'];
a['4'] = 'e';
a[3] = 'd';
alert(a); // returns a,b,c,d,e
Now using the explanation in my answer: what happened is that '4' is coercible to integer 4 and that is in the range [0, 4294967295] making it into a valid array index also called element. Since var a is an array ([]), the array element 4 gets added as array element, not as property (what would have happened if var a was an object ({}).
An example to further outline the difference between array and object:
var a = ['a','b','c'];
a['prop']='d';
alert(a);
see how it returns a,b,c with no 'd' to be seen.
Edit 4:
You commented: "In that case, an integer index should be handled as a string, as it is a property of the array, which is a special type of JavaScript object."
That is wrong in terms of terminology because: (strings representing) integer indexes (between [0, 4294967295]) create array indexes or elements; not properties.
It's better to say: Both an actual integer and a string representing an integer (both between [0, 4294967295]) is a valid array index (and should conceptually be regarded as integer) and creates/changes array elements (the 'things'/values (only) that get returned when you do arr.join() or arr.concat() for example).
Everything else creates/changes a property (and should conceptually be regarded as string).
What the browser really does, usually shouldn't interest you, noting that the simpler and clearer specified you code, the better chance the browser has to recognize: 'oh, let’s optimize this to an actual array under the hood'.
Let's see:
[1]["0"] === 1 // true
Oh, but that's not conclusive, since the runtime could be coercing "0" to +"0" and +"0" === 0.
[1][false] === undefined // true
Now, +false === 0, so no, the runtime isn't coercing the value to a number.
var arr = [];
arr.false = "foobar";
arr[false] === "foobar" // true
So actually, the runtime is coercing the value to a string. So yep, it's a hash table lookup (externally).
In JavaScript there are two type of arrays: standard arrays and associative arrays (or an object with properies)
[ ] - standard array - 0 based integer indexes only
{ } - associative array - JavaScript objects where keys can be any strings
So ...
var arr = [ 0, 1, 2, 3 ];
... is defined as a standard array where indexes can only be integers. When you do arr["something"] since something (which is what you use as index) is not an integer you are basically defining a property to the arr object (everything is object in JavaScript). But you are not adding an element to the standard array.

Performance of jQuery.grep vs. Array.filter

In a question it was discussed on how jQuery and native JS would perform against each other.
While of course the vanilla solution performs a lot faster because it does not process the whole array I proposed the usage of Array.filter which I was pretty confident would be at least faster than $.grep.
Surprisingly after adding it to the test I was taught a lesson: Testsuite
Edgecases of course have a different outcome.
Anyone having an idea why $.grep is supposed to be over 3 times faster than the native method Arrray.filter?
Edit: I modified the test to use the filter shim from MDN and the results are pretty interesting:
Chrome: Even MDN shim is faster than the native method, jQuery way ahead
Firefox: shim a bit slower than native method, jQuery way ahead
and finally a result like i was hoping it to see in
Internet Explorer:
native method is the fastest, then jQuery, shim is slowest (perhaps this is just the result of IEs rather weak JS-engine...)
As found on this blog post (which also does the same kind of tests):
If you read the documentation for filter, you will see why it's so much slower.
It ignores deleted values and gaps in the array
It optionally sets the execution context of the predicate function
It prevents the predicate function from mutating the data
Section 15.4.4.20 of the ECMAScript 5.1 spec defines Array.prototype.filter(callbackfn, thisArg) as follows:
callbackfn should be a function that accepts three arguments and
returns a value that is coercible to the Boolean value true or
false. filter calls callbackfn once for each element in the
array, in ascending order, and constructs a new array of all the
values for which callbackfn returns true. callbackfn is called
only for elements of the array which actually exist; it is not called
for missing elements of the array.
If a thisArg parameter is provided, it will be used as the this
value for each invocation of callbackfn. If it is not provided,
undefined is used instead.
callbackfn is called with three arguments: the value of the element,
the index of the element, and the object being traversed.
filter does not directly mutate the object on which it is called but
the object may be mutated by the calls to callbackfn.
The range of elements processed by filter is set before the first call
to callbackfn. Elements which are appended to the array after the
call to filter begins will not be visited by callbackfn. If existing
elements of the array are changed their value as passed to
callbackfn will be the value at the time filter visits them;
elements that are deleted after the call to filter begins and before
being visited are not visited.
That in itself is already a lot of work; a lot of steps that the ECMAScript engine needs to perform.
Then it goes on to say the following:
When the filter method is called with one or two arguments, the
following steps are taken:
Let O be the result of calling ToObject passing the this value as the
argument.
Let lenValue be the result of calling the [[Get]] internal
method of O with the argument length.
Let len be ToUint32(lenValue).
If IsCallable(callbackfn) is false, throw a TypeError exception. If
thisArg was supplied, let T be thisArg; else let T be undefined. Let A
be a new array created as if by the expression new Array() where Array
is the standard built-in constructor with that name. Let k be 0. Let
to be 0. Repeat, while k < len Let Pk be ToString(k). Let kPresent be
the result of calling the [[HasProperty]] internal method of O with
argument Pk. If kPresent is true, then Let kValue be the result of
calling the [[Get]] internal method of O with argument Pk. Let
selected be the result of calling the [[Call]] internal method of
callbackfn with T as the this value and argument list containing
kValue, k, and O. If ToBoolean(selected) is true, then Call the
[[DefineOwnProperty]] internal method of A with arguments
ToString(to), Property Descriptor {[[Value]]: kValue, [[Writable]]:
true, [[Enumerable]]: true, [[Configurable]]: true}, and false.
Increase to by 1. Increase k by 1. Return A.
The length property of
the filter method is 1.
NOTE The filter function is intentionally generic; it does not require
that its this value be an Array object. Therefore it can be
transferred to other kinds of objects for use as a method. Whether the
filter function can be applied successfully to a host object is
implementation-dependent.
Some things to note about this algorithm:
It prevents the predicate function from mutating the data
It optionally sets the execution context of the predicate function
It ignores deleted values and gaps in the array
In a lot of cases, none of these things are needed. So, when writing a filter method of your own, most of the time you wouldn’t even bother to perform these steps.
Every ES5.1-compliant JavaScript engine must conform to that algorithm, and must thus perform all those steps every time you use Array#filter.
It should be no surprise that any custom-written method that only performs a part of those steps will be faster :)
If you write your own filter function, chances are it’s not gonna be as complex as the above algorithm. Perhaps you won’t be converting the array to an object at all, as depending on the use case it may not be needed just to filter the array.
I found out something interesting. As explained by MarcoK, $.grep is just a simple implementation with a for loop. Filter is slower in most cases, so the implementation must be different. I think i found the answer:
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)
The native 'filter' is much faster in this case. So i think it iterates the properties rather than the array index.
Now lets get back to 'big' problems ;).
Isn't your script wrong?
For array.filter you are doing the measurement 1000 times and present it by taken the sum divided by 1000
For JQuery.grep you are doing the measurement 1 time and present it by taken the sum divided by 1000.
That would mean your grep is actually 1000 times slower than the value you use for comparison.
Quick test in firefox gives:
Machine 1:
average filter - 3.864
average grep - 4.472
Machine2:
average filter - 1.086
average grep - 1.314
Quick test in chrome gives:
Machine 1:
average filter - 69.095
average grep - 34.077
Machine2:
average filter - 18.726
average grep - 9.163
Conclusion in firefox (50.0) is a lot faster for your code path, and filter is about 10-15% faster than jquery.grep.
Chrome is extremely slow for your code path, but grep seems to be 50% faster than array.filter here making it 900% slower than the firefox run.
TLDR; Grep is faster by a magnitude... (hint on why can be found here)
It looks to me like .filter forces it's this to Object, checks the
callback IsCallable and sets this in it as well as checking for
existence of property in each iteration whereas .grep assumes and
skips these steps, meaning there is slightly less going on.
Here is the script I used for 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>

Categories