Shim sniffing

Extending native objects and prototypes is bad. If not vile, mean and Jesuitic.

// Noooooo!
Array.prototype.map = function() {
  // stuff
};

Unless it's desirable, for example for adding ECMAScript5 methods in legacy browsers.

In which case we do something like:

if (!Array.prototype.map) {
  Array.prototype.map = function() {
    // stuff
  };
}

If we're paranoid enough we can even try to protect from somebody defining map as something unexpected like true or "the peaches are this way":

if (typeof Array.prototype.map !== "function") {
  Array.prototype.map = function() {
    // stuff
  };
}

(Although that ultimately breaks the other developer's map to the peach trees)

But in a hostile dog-eat-dog cut-throat environment (in other words when you provide or consume a library), you trust no one. What if that other smartass JS loads before your badass JS and defines map() in a way that is not really ES5-compliant and your code doesn't work anymore?

You can always trust browsers though. If Webkit implements map() you can relax that it should probably work OK. Otherwise you'd want to go ahead with your shim.

Luckily that's easy to do in JavaScript. When you call toString() of a native function it should return a string with a function that has a body of [native code]

So for example in Chrome's console:

> Array.prototype.map.toString();
"function map() { [native code] }"

A proper check is ever-so-slightly painful because browsers seem to be a little frivolous with white spaces and new lines. Testing:

Array.prototype.map.toString().replace(/\s/g, '*');
// "*function*map()*{*****[native*code]*}*"  // IE
// "function*map()*{*****[native*code]*}" // FF
// "function*map()*{*[native*code]*}" // Chrome

Simply stripping all \s will give you something more workable:

Array.prototype.map.toString().replace(/\s/g, '');
// "functionmap(){[nativecode]}"

You can opt in for a reusable shim() function so you don't have to repeat all that !Array.prototype... jazz. It can take an object to augment (e.g. Array.prototype), a property to add (e.g. 'map') and a function that implements the shim.

function shim(o, prop, fn) {
  var nbody = "function" + prop + "(){[nativecode]}";
  if (o.hasOwnProperty(prop) &&
      o[prop].toString().replace(/\s/g, '') === nbody) {
    // native!
    return true;
  }
  // shim
  o[prop] = fn;
}

Testing:

// this is native, cool
shim(
  Array.prototype, 'map',
  function(){/*...*/}
); // true
 
//  this is new
shim(
  Array.prototype, 'mapzer',
  function(){alert(this)}
);
 
[1,2,3].mapzer(); // alerts 1,2,3

p.s. And then
there's JJD's! (backstory)

15 Responses to “Shim sniffing”

  1. Kai Sellgren Says:

    You forgot “typeof” from the third code example.

    I think this is a bit exaggerated…

  2. stoyan Says:

    Thanks Kai, fixed!

    can you elaborate on “exaggerated”?

  3. Callum Macrae Says:

    I think Kai means over the top. When I first read it it seemed a bit over the top, but having thought about it, that function could turn out to be pretty useful.

    Nice article, thanks!

  4. Peter van der Zee Says:

    Array.prototype.map = Array.prototype.forEach

    And now you have [native code] problems ;)

    Nah I like the approach, I just think it’s very very defensive. Might as well implement your own if you want to take this route.

  5. Andrea Giammarchi Says:

    Agreed with Peter van der Zee … but if you want to trust a bit more the fact a function is native or not I would use a different approach:

    var isNativeFunction = function(Function){
    var s, toString = Function.prototype.toString;
    return function isNativeFunction(f) {
    try {
    s = toString.call(f);
    try {
    Function(“return ” + s);
    } catch(_) {
    return true;
    }
    } catch(_) {}
    return false;
    };
    }(Function);

  6. Stoyan Says:

    @Peter, @Andrea Unfortunatelly the need for this has come up in practice not just an excercise :)

    @Andrea, why the first `try`? How about a simpler: https://gist.github.com/2878985

  7. Andrea Giammarchi Says:

    f.toString does not ensure it’s a function in first place, neither the toString is the original one.
    Function.prototype.toString can be used for functions/callable objects only so it ensure, without needing extra checks, that the given object is a function, it will fail otherwise.

    As summary, Function.prototype.toString.call({}) will throw an error since it’s not a function, and not native :-)

  8. Andrea Giammarchi Says:

    so … two examples here, the reason I have used those try/catch
    https://gist.github.com/2878985#comments

  9. Peter van der Zee Says:

    So how do/did you defend against replacing one native method with another?

  10. Andrea Giammarchi Says:

    the easiest way to check that is something like isNativeFunction([].map) && [].map.name == “map” … this should work cross platform except for IE less than 9

  11. stoyan Says:

    And what about Function.prototype.toString = alert; Obviously we have to stop somewhere :)

    The point here was not to be overly defensive. isNative doesn’t have to deal with non-function input etc. Just defend against shims that are well-intentioned but possibly slightly off

  12. Peter van der Zee Says:

    Andrea: the array (and object) literal will only use the built-in constructor. They still use the (global) Array.prototype. As such, replacing Array.prototype.map with something else will cause [].map to reflect that.

    Stoyan: Okay, that sounds valid :)

  13. Peter van der Zee Says:

    Oh by the way, there used to be a trick where if you deleted a built-in global object you would get back the built-in one. I just tried on Chrome and it didn’t work so maybe I’m doing it wrong, maybe it’s a different browser, or maybe that just doesn’t work anymore :)

  14. Shim sniffing | JSSpy | The ultimate resource for Javascript info. Says:

    [...] http://www.jspatterns.com/shim-sniffing/ Share on  Twitter  Facebook  Google Reader  Ping.fm No Comments Related [...]

  15. Michel Schakel Says:

    I have been surfing online greater than three hours today, but I by no means found any fascinating article like yours. It is pretty price sufficient for me. In my opinion, if all website owners and bloggers made good content material as you did, the web will be much more useful than ever before.

Leave a Reply