public
Last active — forked from 140bytes/LICENSE.txt

140byt.es polyfill for Array.filter

  • Download Gist
LICENSE.txt
1 2 3 4 5 6 7 8 9 10 11 12 13
DO WHAT THE **** YOU WANT TO PUBLIC LICENSE
Version 2, December 2004
 
Copyright (C) 2011 Eli Perelman http://eliperelman.com
 
Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.
 
DO WHAT THE **** YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
 
0. You just DO WHAT THE **** YOU WANT TO.
README.md
Markdown

140byt.es polyfill for Array.filter

Thanks to atk and jdalton for making the solution more robust and compliant! :)

annotated.js
JavaScript
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
[].filter || (Array.prototype.filter = // Use the native array filter method, if available.
function(a, //a function to test each value of the array against. Truthy values will be put into the new array and falsy values will be excluded from the new array
b, // placeholder
c, // placeholder
d, // placeholder
e // placeholder
) {
c = this; // cache the array
d = []; // array to hold the new values which match the expression
for (e in c) // for each value in the array,
~~e + '' == e && e >= 0 && // coerce the array position and if valid,
a.call(b, c[e], +e, c) && // pass the current value into the expression and if truthy,
d.push(c[e]); // add it to the new array
return d // give back the new array
})
package.json
JSON
1 2 3 4 5 6 7 8 9 10 11 12
{
"name": "arrayFilter140Polyfill",
 
"description": "A tweet-sized polyfill for Array.prototype.filter",
 
"keywords": [
"array",
"filter",
"polyfill",
"prototype"
]
}
test.html
HTML
1 2 3 4 5 6 7 8 9 10 11 12 13 14
<!DOCTYPE html>
<title>Array filter polyfill</title>
<div>Expected value: filter the array values outside of our range (less than 6)</div>
<script>
[].filter||(Array.prototype.filter=function(a,b,c,d,e){c=this;d=[];for(e in c)~~e+''==e&&e>=0&&a.call(b,c[e],+e,c)&&d.push(c[e]);return d});
 
var arr = [1,2,3,4,5,6,7,8,9,10];
 
// creates new array of [1,2,3,4,5] and reassigns it to arr.
arr = arr.filter(function(value) {
return value < 6;
});
 
</script>

This fallback is not ES5 compliant.
Also browsers like Chrome have a bug where Array.prototype.filter = [].filter will cause Array#filter to become enumerable.

@eliperelman: I would advise to document the lack of full ES5 compliance in the README.md to take care of the first point. The missing part will mostly be lack of hasOwnProperty-Support, so prototypical methods of Objects might get tried against the filter function, too. Actually, currently the thisArg support is missing, to, but we can easily take care of this as well as a few other issues and securing Chrome against overwriting of the native prototypical method by using the following version:

[].filter||(Array.prototype.filter=function(a,b,c,d,e,f,g){for(c=this,d=e=f=[];e<c.length;a.call(b,g=c[+e],e++,c)&&(d[f++]=g));return d})

// only if filter is not present, execute the following statement
[].filter || 
  // put function into Array.prototype filter (almost ES5-compliant, except for hasOwnProperty check)
  (Array.prototype.filter = function(
    a, // filter callback function
    b, // optional thisArg context in which the filter function will be called
    c, // placeholder for this
    d, // result array
    e, f, g // counter, item placeholder
  ){
    for (
      // initialize c, result array and the counters (use coercion of [] => 0)
      c = this, d = e = f = [];
      // as long as counter e is less than the length of the object
      e < c.length;
      // call callback in the context of b, with arguments 1. current item, 2. counter (increment in the same step) and 3. this
      a.call(b, g=c[+e], e++, c) && 
        // if callback result is true-ish, put item into result, increase result counter
        (d[f++] = g)
    );
    // return result
    return d
  })

@eliperelman

To avoid the compliance issue you can simply assign to a namespace and make the function generic instead (this will also save on character count).
For example:

(_b = this._b || {}).filter = function(array, callback, thisArg) { ... };

If you want to add filter to the Array.prototype then following spec is the way to go (but probably won't work for the 140bytes restriction).


On the compliance side:

  • Array#filter does not require a [[HasOwnProperty]] check, but rather a [[HasProperty]], (index in array) check.
  • The length should be computed as smth like l = c.length >>> 0, (a way of getting the ToUint32(...) value).
  • The c.length should be stored in a variable as the callback can modify the array/object length and that could affect the run time (see example A).

Example A:

var a = [1,2,3];
// should execute the callback 3 times, but with the gist's code will go into an infinite death spiral of doom
a.filter(function(value){
   a.push(value);
   return value % 2; // gimme the odds
});

Other considerations, which are already handled by this gist indirectly, though with misleading error messages, are filter must throw a TypeError if the callback is not a function and a TypeError must be thrown if this is null or undefined.

Making it generic may have the advantage of added compliance, but the drawback that it is in fact not a polyfill anymore.

Btw. the latter issue can be fixed by using:

[].filter||(Array.prototype.filter=function(a,b,c,d,e,f,g){for(c=this,d=e=[],g=c.length;e<g;a.call(b,f=c[+e],e++,c)&&d.push(f));return d})

Now only the following, though not very common issues remain:

  • non integer length
  • hasProperty check

Please document those issues in the Readme.md. Please include a link to a compatible ES5 shim, e.g. https://github.com/seajs/dew/tree/master/src/es5-safe.

Many popular libs/frameworks like MooTools, Dojo, jQuery, and YUI have patches in place to address their [[HasProperty]] (a.k.a. sparse array) issues, so hopefully soonish we will start seeing things like this addressed in a wide range of projects. Also sparse arrays are easier to accidentally achieve in IE. For example

// this is *not* a sparse array in most engines but in JScript in IE < 9 it is.
var a = ['a', 'b', undefined, 'd'];
alert(2 in a); // false in IE < 9; true in other browsers

Please document those issues in the Readme.md. Please include a link to a compatible ES5 shim, e.g

That would be awesome! A link to the spec wouldn't hurt either :D

Thanks for clarification of the sparse array issue. As far as I understand it, hasProperty would only return false on objects without the corresponding properties, so for a = { 0: 'a', 1: 'b', 3: 'c' }, .filter should not try the callback on a[2]. Please correct me if I'm mistaken.

As far as I understand it, hasProperty would only return false on objects without the corresponding properties,

Yap that's the idea. The [[HasProperty]] check will recursively walk back the object's [[Prototype]] chain, if it doesn't find the property on itself, until it finds a property of the given name or [[Prototype]] is null.

There are several ways to create sparse arrays:
For example:

// an array with a length of 23 but no index properties defined
var a = Array(23);
1 in a; // false
4 in a; // false

// or 
var a = [];
a.length = 23;
1 in a; // false
4 in a; // false

// sparse by way of array initialiser
var a = ['a', , , 'd'];
0 in a; // true
1 in a; // false
2 in a // false
3 in a // true
a[3] // 'd'

// IE < 9's buggy array initialiser
a = ['a', undefined, undefined, 'd'];
0 in a; // true for everyone and IE
1 in a; // false for IE; true for everyone else
2 in a // false for IE; true for everyone else
3 in a // true for everyone and IE
a[3] // 'd'

// sparse through manual assignment
var a = ['a'];
a[2] = 'c';
a; // ['a', ,'c']
0 in a; // true
1 in a; // false
2 in a; // true

// assuming a non-modified native Array#forEach
[].forEach.call(a, function(value, index) {
  console.log(index, value);
});

// logs
// 0: "a"
// 2: "c"

and array-like-objects like:

// the `length` property *is* required or it will be resolved to 0,
// when following spec like `l = c.length >>> 0`, or undefined if not
var a = { '0': 'a', '2': 'c', 'length': 3 };
0 in a; // true
1 in a; // false
2 in a; // true

// or inherited prop values
function Klass() {
  // no-op
}

Klass.prototype = { '2': 'c', 'length': 3 };

var a = new Klass;
a[0] = 'a';
0 in a; // true
1 in a; // false
2 in a // true

// assuming a non-modified native Array#forEach
[].forEach.call(a, function(value, index) {
  console.log(index, value);
});

// logs
// 0: "a"
// 2: "c"

Since hasProperty is not present in any known environment that is lacking filter support, even the attempt to include the method checking in a 140bytes shim would be rather pointless. Yet, hasProperty only checks if the element or its protypical twin is anything but undefined, one could add a (currently 9 bytes too long) polyfill to address this issue:

[].filter||(Array.prototype.filter=function(a,b,c,d,e,f){for(c=this,d=[],e=0,f=c.length;e<f;e++)c[e]!==d&&a.call(b,c[e],e,c)&&d.push(c[e]);return d})

This way, a hasProperty/getProperty shim is not necessary and the method should work as expected even for sparse arrays.

@atk

Since hasProperty is not present in any known environment that is lacking filter support, even the attempt to include the method checking in a 140bytes shim would be rather pointless.

I'm not sure what you mean. [[HasProperty]] is an internal operation that's part of how the in operator works. All environments support the in operator (even those without Array#filter).

Yet, hasProperty only checks if the element or its protypical twin is anything but undefined, one could add a (currently 12 bytes too long) polyfill to address this issue:

The array element can be undefined. For example

var u, a = [];
a[0] = u;
0 in a; // true

This way, a hasProperty/getProperty shim is not necessary and the method should work as expected even for sparse arrays.

This seems really hacky. I don't think ES5 fallbacks are the best choice for code golf in general because things should be really tight and follow spec as these methods are added to native prototypes.

Please keep in mind: this is a Array.filter polyfill, not necessarily an ES5 fallback. Were it possible to provide an compliant ES5 fallback in 140bytes, this would be rather great. So currently, this boils down to the following function:

function(a,b,c,d,e,f){for(c=this,d=[],e=0,f=c.length;e<f;e++)e in c&&a.call(b,c[e],e,c)&&d.push(c[e]);return d}

used as shim, it would be

[].filter||(Array.prototype.filter||function(a,b,c,d,e,f){for(c=this,d=[],e=0,f=c.length;e<f;e++)e in c&&a.call(b,c[e],e,c)&&d.push(c[e]);return d})

...still 8 bytes too long.

122bytes solution:

[].filter||(Array.prototype.filter||function(a,b,c,d,e){c=this;d=[];for(e in c)a.call(b,c[e],e,c)&&d.push(c[e]);return d})

136 bytes solution that avoids the common for...in array issues:

[].filter||(Array.prototype.filter=function(a,b,c,d,e){c=this;d=[];for(e in c)/^\d+$/.test(e)&&a.call(b,c[e],e,c)&&d.push(c[e]);return d}

Nice one, too; 3bytes shorter: replace /^\d+$/.test(e) with e>=0&&~~e==e (like in d is coercible to a positive integer number).

Both /^\d+$/.test(e) and e>=0&&~~e==e suffer from allowing keys like "03"... a regexp like )/^0$|^[1-9]\d+?$/.test(e) would prevent it but it's a bit too large

How about we recoerce it? Like in ~~e+''==e&&e>=0 -> 138 bytes.

Ah yes re-coerce :heart:!
So even this +e+''==e&&e>-1 -> 137 bytes.

nope, this will not work on "1.5", but my solution will.

So

[].filter||(Array.prototype.filter||function(a,b,c,d,e){c=this;d=[];for(e in c)~~e+''==e&&e>=0&&a.call(b,c[e],e,c)&&d.push(c[e]);return d}

will do...

Another stupid idea, since [].slice should normalize an object to array form (135bytes):

[].filter||(Array.prototype.filter||function(a,b,c,d,e){d=[];c=d.slice.call(this);for(e in c)a.call(b,c[e],e,c)&&d.push(c[e]);return d}

Any catches?

Another stupid idea, since [].slice should normalize an object to array form (135bytes):
<snippet>
Any catches?

Yap the same old for..in gotchas with Array.prototype modifications.

In all implementations I have tested, slice returns a normalized array, yet the object itself remains untouched. So only those keys that can be converted to array form will be available to for..in.

So pretending this is a browser that gets this implemented:

Array.prototype.filter = function(a,b,c,d,e){d=[];c=d.slice.call(this);for(e in c)a.call(b,c[e],e,c)&&d.push(c[e]);return d};

[0, 1, 2].filter(function(value, index, array) {
  alert(index);
  return value;
});

// incorrectly alerts smth like `0` then `1` then `2` then `filter`

OK, I get it. Let's stick with our working solution, then.

Sorry I've been busy guys. Which one is the agreed upon solution?

Updated from the comments. Thanks guys!

@eliperelman

The line containing

a.call(b, c[e], e, c) &&

should be

a.call(b, c[e], +e, c) &&

So the index passed to the callback is a number and not a string.

Also devs should keep in mind that the order in which for..in iterates over properties is not spec'ed.

So for example this fallback may produce:

var a = [];
a[2] = 'c';
a[1] = 'b';
a[0] = 'a';

var b = a.filter(function(v) { return v; });
b; // ['c', 'b', 'a'];

[].filter||(Array.prototype.filter=function(f,c,d){d=[];this.forEach(function(v,k,s){f.call(c,v,k,s)&&d.push(v)});return d}) 125 bytes :P

@bga that defeats the purpose of this needing to be an ES5 polyfill.

@eliperelman heh but you have Array#forEach polyfill too :)

Agreed, but then this script wouldn't be able to operate on its own.

I just don't understand what the difference between [].filter and Array.prototype.filter is.

If you set [].filter, you set the filter method of an anonymous Array, but if you change Array.prototype.filter, you set the filter method of all Arrays. Still, [].filter is shorter, but allows to check if a (native) filter method is present.

So why don't just use Array.prototype.filter ?

If we overwrite an existing native class, we risk negative effects e.g. in Chrome. So we rather stick with this safe method.

Isn't Array.prototype.filter || (Array.prototype.filter = ...) doing the same?

Yes, but longer than [].filter||(Array.prototype.filter=...)

That is the point.
Why is there [].filter||(Array.prototype.filter||function()...?

Compared to what alternative?

Why is there [].filter||(Array.prototype.filter||function()...?

It's a typo. It should be:
[].filter||(Array.prototype.filter=function()...

@jdalton Are you saying this wouldn't work:

Array.prototype.filter = [].filter || (function()...

@ jdalton Are you saying this wouldn't work:
Array.prototype.filter = [].filter || (function()...

Chrome has a bug where Array.prototype.filter = [].filter will cause Array#filter to become enumerable.

Also there's still a typo in the test.html snippet.

This

Array.prototype.filter=[].filter||(Array.prototype.filter=

should be this

[].filter||(Array.prototype.filter=...)

Ah, I see. We don't even want to execute the polyfill if the method exists. No reassignment.

Save 2 bytes.

[].filter||(Array.prototype.filter=function(a,b,c,d,e){d=[];for(e in c=this)~~e+''==e&&e>=0&&a.call(b,c[e],+e,c)&&d.push(c[e]);return d})

Is there a reason why you do ~~e+''==e and e>=0? From what I know both do (almost) the same, checking if e is a number (to be more precise, the first checks for an int, the second for a double). I think it's shorter to use a plain for loop. The following version is 133 bytes and works in IE7 and IE8.

[].filter||(Array.prototype.filter=function(a,b,c,d,e){c=this;d=[];for(e=0;e<c.length;e++)a.call(b,c[e],e,c)&&d.push(c[e]);return d})

This also fixes a problem we came across in our getElementsByClassName. In IE7 and IE8 the for-in loop does not return numeric indices for elements with an ID. It returns the IDs instead for these elements. If you do (for example) document.getElementsByTagName('*') on a document where all elements contain an ID, all these elements will be skipped. Forcing e to be a number solves this problem.

@maettig the ~~e+''==e and e>=0 is to ensure the key is a number and it's not negative.
Your version skips the in check for sparse arrays. You really shouldn't be using these snippets in production code.

e>=0 is enough to check for non-negative numbers. Is it really necessary to check for decimal places? I forgot about sparse arrays. But as said, using for-in is broken in IE on NodeList's.

e>=0 is enough to check for non-negative numbers. Is it really necessary to check for decimal places?

the ~~e+''==e also guards against keys like 08 too.

I forgot about sparse arrays. But as said, using for-in is broken in IE on NodeList's.

There is only so many characters allowed. As I have said before code-golf is not appropriate for production code so please don't look for it to be a robust and to-the-letter solution. As it is, the currently solution meets mark for a workable approximation of Array#filter. The important part is we have at least brought up/made aware the issues with each variation.

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.