public
Last active

Incorrect ES5 fallbacks

  • Download Gist
gistfile1.md
Markdown

Incorrect ES5 fallbacks

Over the weekend I implemented a few Array methods in plain JavaScript to avoid recently patched Rhino bugs. That got my thinking about ES5 fallback implementations in various JavaScript libs/frameworks/transpilers. I decided to compile a not-so-complete list of ES5 related discrepancies found in many of them. Differences in native vs. fallback implementations create cross-browser inconsistencies and increase the chance of usage errors. I hope this post will raise awareness of just how hard it is to follow spec (during my research I found a few issues in my own projects too). All library developers should to take a closer look at their code and make the small changes needed to follow the specification (especially if your code forks for native methods).

Common Issues

Most implementations suffer from the following issues:

Buggy Array#indexOf

Incorrect handling of a negative fromIndex argument in Array#indexOf.

The correct implementation looks something like:

if (fromIndex < 0) {
  fromIndex = Math.max(0, length + fromIndex);
}

Example:

var array = ['a', 'b', 'c'];
console.log(array.indexOf('c', -1e9)); // 2 (without freezing the browser)

Buggy Array#lastIndexOf

Incorrect handling of a fromIndex argument greater than the object length minus 1 in Array#lastIndexOf.

The correct implementation looks something like:

fromIndex = Math.min(fromIndex, length - 1);

Example:

var array = ['a', 'b', 'c'];
console.log(array.lastIndexOf('c', 100)); // 2

Lack of sparse array support

Array functions like every(), filter(), forEach(), indexOf(), lastIndexOf(), map(), reduce(), reduceRight() and some() are required to skip non-existant indexes.

Example:

// make a sparse array
var array = ['a', 'b'];
array[3] = 'd';

// in IE8 press F12 to enable developer tools and console.log() support
console.log(array); // ['a', 'b', undefined, 'd'];
console.log(2 in array); // false

// Array#some should skip the non-existant index and not match an undefined value
console.log(array.some(function(value) { return typeof value == 'undefined'; })); // false

Lack of array-like-object / generic-this support

Many native methods are intentionally generic. This means that the method in question can be successfully called with its this binding set to other values.

Example:

// convert an arguments object to an array
var args = [].slice.call(arguments);

// array-like-object as its `this` binding
var object = { '0': 'a', '1': 'b', '2': 'c', 'length': 3 };
console.log([].filter.call(object, function(value, index) { return index > 1; })); // ['c']

// string as its `this` binding (in browsers that support String#charAt by index)
var str = "hello";
console.log([].map.call(str, function(value) { return value.toUpperCase(); })); // ['H', 'E', 'L', 'L', 'O']

Bound functions as constructors

The result of Function#bind should still be able to work as a constructor.

Example:

function Alien(type) {
  this.type = type;
}

var thisArg = {};
var Tribble = Alien.bind(thisArg, 'Polygeminus grex');

// `thisArg` should **not** be used for the `this` binding when called as a constructor
var fuzzball = new Tribble;
console.log(fuzzball.type); // "Polygeminus grex"

The Breakdown

CoffeeScript v1.1.2

OK, by a show of hands, who wants to monkey-patch a transpiler?

  1. undefined in array (uses Array#indexOf internally)

    • no sparse array support
  2. (x) => @foo(@bar, x) (Function#bind equivalent-ish)

    • no support for bound functions called as constructors
    • no support for curried arguments

Dojo v1.6.1

Dojo clearly documents that it does not follow ES5 spec when it comes to sparse arrays. Unlike other libs listed, Dojo avoids cross-browser inconsistencies by opting not to use native methods.

  1. dojo.every(array)

    • no sparse array support
  2. dojo.filter(array)

    • no sparse array support
  3. dojo.forEach(array)

    • no sparse array support for array
  4. dojo.indexOf(array)

    • no sparse array support
    • incorrect negative fromIndex support
  5. dojo.lastIndexOf(array)

    • no sparse array support
    • incorrect negative fromIndex and fromIndex greater than length - 1 support
  6. dojo.map(array)

    • no sparse array support
  7. dojo.reduce(array)

    • no sparse array support
  8. dojo.some(array)

    • no sparse array support
  9. dojo.trim(str)

    • no generic-this support for str

es5-shim v1.2.4

I'm a little harder on es5-shim because its goal is to follow the specification as closely as possible. Besides the listed implementation issues I noticed heavy use of named function expressions which will leak a super-tiny bit of memory in IE (this aggravates my dev OCD more than any real world memory consumption concerns).

  1. Array#every

    • incorrect ToUint32() equivalent
    • no sparse array support
  2. Array#filter

    • incorrect ToUint32() equivalent
    • no sparse array support
  3. Array#forEach

    • incorrect ToUint32() equivalent
  4. Array#indexOf

    • incorrect or lack of ToInteger() and ToUint32() equivalents
    • incorrect negative fromIndex support
  5. Array#lastIndexOf

    • incorrect or lack of ToInteger() and ToUint32() equivalents
  6. Array#map

    • incorrect ToUint32() equivalent
  7. Array#reduce

    • incorrect ToUint32() equivalent
  8. Array#reduceRight

    • incorrect ToUint32() equivalent
  9. Array#some

    • incorrect ToUint32() equivalent
    • no sparse array support
  10. Function#bind

    • doesn't curry boundArgs for bound function's call and apply methods
    • incorrect handling of bound functions called as constructors
      • doesn't return correct value if the constructor returns a custom object value
      • doesn't have checks in call and apply to avoid executing as a constructor if a bound instance is passed as thisArg
  11. Object.defineProperty

  12. String#trim

ExtJS v3.1.0

  1. Array#indexOf
    • no sparse array support

jQuery v1.6.2

  1. jQuery.inArray() (uses Array#indexOf internally)
    • no sparse array support

MooTools v1.3.2

MooTools overwrites existing native String#trim with their own :(

  1. Array#map

    • incorrect length set for sparse arrays with non-existant last element
  2. Function#bind

    • no support for bound functions called as constructors
    • doesn't curry boundArgs for bound function's call and apply methods
  3. Object.create

    • Non-compliant Object.create exists because of custom Function#create method (caused by the v1.2 compatibility layer)
  4. String#trim

    • overwrites native function
    • no generic-this support

Prototype v1.7.0

Prototype overwrites existing native Array methods with their own >:(

  1. Array#every

    • overwrites native function
    • no array-like-object or generic-this support
  2. Array#filter

    • overwrites native function
    • no array-like-object or generic-this support
  3. Array#indexOf

    • no sparse array support
    • incorrect negative fromIndex support
  4. Array#lastIndexOf

    • no sparse array support
    • no array-like-object or generic-this support
    • incorrect fromIndex greater than length - 1 support
  5. Array#map

    • overwrites native function
    • no sparse array support
    • no array-like-object or generic-this support
  6. Array#reverse

    • overwrites native function
    • no array-like-object or generic-this support support when custom inline argument is passed
  7. Array#some

    • overwrites native function
    • no array-like-object or generic-this support
  8. Function#bind

    • overwrites native function
    • no support for bound functions called as constructors
    • doesn't curry boundArgs for bound function's call and apply methods
  9. String#strip (like String#trim)

    • no generic-this support

Sproutcore v1.6.0

  1. Array#forEach

    • no sparse array support
  2. Array#every

    • no sparse array support
  3. Array#filter

    • no sparse array support
  4. Array#indexOf

    • no sparse array support
    • incorrect negative fromIndex support
  5. Array#lastIndexOf

    • no sparse array support
    • incorrect fromIndex greater than length - 1 support
  6. Array#map

    • no sparse array support
  7. Array#reduce

    • no sparse array support
    • incorrect explicitly passed initialValue as undefined support
  8. SC.Array#slice

    • incorrect beginIndex greater than length support
    • incorrect negative beginIndex and endIndex support
  9. Array#some

    • no sparse array support

Underscore v1.1.7

  1. _.bind(func)

    • no support for bound functions called as constructors
    • doesn't curry boundArgs for bound function's call and apply methods
  2. _.each(obj)

    • no generic-this support for obj
  3. _.every(obj)

    • no generic-this support for obj
  4. _.filter(obj)

    • no generic-this support for obj
  5. _.indexOf(obj)

    • no sparse array support
  6. _.lastIndexOf(obj)

    • no sparse array support
  7. _.map(obj)

    • no generic-this support for obj
    • incorrect length set for sparse arrays with non-existant last element
  8. _.reduce(obj)

    • no generic-this support for obj
    • no support for explicitly passing an undefined value for initialValue (restricts internal native Array#reduce calls for consistency)
  9. _.reduceRight(obj)

    • no generic-this support for obj
    • no support for explicitly passing an undefined value for initialValue (restricts internal native Array#reduceRight calls for consistency)
  10. _.some(obj)

    • no generic-this support for obj

Valentine v1.1.9

I decided to include Valentine to show that libs new and old are affected by ES5 compliance issues.

  1. v.bind()

    • no support for bound functions called as constructors
    • doesn't curry boundArgs for bound function's call and apply methods
  2. v.each(a)

    • no generic-this support for a
  3. v.every(a)

    • no array-like-object or generic-this support for a
  4. v.filter(a)

    • no array-like-object or generic-this support for a
  5. v.indexOf(a)

    • no array-like-object or generic-this support for a
    • no support for a negative fromIndex
  6. v.lastIndexOf(a)

    • no array-like-object or generic-this support for a
    • fromIndex incorrectly set to length instead of length - 1 when it's greater than length - 1
  7. v.map(a)

    • no generic-this support for a
    • incorrect length set for sparse arrays with non-existant last element
  8. v.some(a)

    • no array-like-object or generic-this support for a
  9. v.trim(s)

    • no generic-this support for s

YUI v3.3.0

  1. Y.Array.each(a)

    • no sparse array support for a
    • no generic-this support for a
  2. Y.Array.indexOf(a)

    • no sparse array support for a
    • no generic-this support for a
  3. Y.Array.some(a)

    • no sparse array support for a
    • no generic-this support for a
  4. Y.Lang.trim(s)

    • no generic-this support for s

YUI's Y.Array.each() and Y.Array.some() methods (along with lots of other array methods) were updated to handle sparse arrays in the 3.4.0 release (coming out this month).
I missed Y.Array.indexOf(), but I'll fix that and Y.Lang.trim() for the next release.

@rgrove Awesome!

Trim is already fixed for MooTools, and we're working on fixing Function#bind too: https://github.com/mootools/mootools-core/issues/2004

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.