Perfection kills

Exploring Javascript by example

Detecting built-in host methods

February 7th, 2009 by kangax (Permalink)

isNative

Diego Perini mentions that he often needs to detect whether a method is “native” (i.e. provided by the host environment) or is simply declared/overwritten by a third party script. He uses the following snippet:

// detect native method in object
// not same scope of isHostObject
var isNative = function(object, method) {
  return object && method in object &&
    typeof object[method] != 'string' &&
    // IE & W3C browser return "[native code]"
    // Safari <= 2.0.4 will return "[function]"
    (/\{\s*\[native code\]\s*\}|^\[function\]$/).test(object[method]);
}

It’s a good idea to understand what this method really does. And it does so-called function decompilation – a “technique” that’s been quite unreliable, has already caused problems and is only backed by a wishful thinking.

The problem

If you look closely, isNative actually doesn’t test whether method is built-in; it first checks that a property exists either directly on an object, or in its prototype chain (method in object), is not a string (typeof object[method] != "string"), and that its string representation (i.e. result of its toString method) is either “[native code]” or “[function]“. The last step is exactly what function decompilation is all about – parsing result of function’s toString method. The explanation is a bit convoluted, but a simple example can demonstrate the issue better:

window.test = {
  toString: function() {
    return '[function]';
  }
};
isNative(window, 'test'); // true

Is test method really provided by the browser in this case? Obviously, it is not; It just happens to return “[function]” as a result of its toString method. The problem with such approach is not only that there is a chance of false positives (as you can see from the previous example); It is that there’s no standard, specified behavior for host methods having “[native code]” or “[function]” strings in their string representations. Host method might as well just return “Hello world!” and the browser would still be ECMA/DOM1/2/3 compliant. That’s because nothing in ECMA or DOM specs put requirements on the way resulting string should be. ECMA simply allows Function.prototype.toString to return implementation-dependent representation. As you can see from the comment by Diego, older Safari represents host methods as simple as “[function]“. Mobile Opera employs its own representation (something without even beginning “function”), and Blackberry 9500 sometimes decides to be extra explicit : )

XMLHttpRequest + '';
// function XMLHttpRequest(){ [native code for XMLHttpRequest.XMLHttpRequest, arity=1] }

Does this remind you of navigator.userAgent sniffing? It sure does so for me – not only it is flawed in theory (as behavior is not specified) but even practical side is known to deviate from one browser to another. Testing for all existing combination sounds silly and might still “break” with the introduction of new browsers.

The solution

The solution I came up with is not perfect. It doesn’t rely on any specified behavior either; That’s because there doesn’t seem to be any standard, describing what built-in host method should be like / act like / represent itself like. The most robust solution seems to be to test for a presence of a method in a different DOM context. For example, to check whether native window.addEventListener is present, we could use something along these lines:

var isNativeWindowAddEventListenerPresent = (function(global){
  // feature test following methods as needed
  var el = document.createElement('iframe');
  var root = document.body || document.documentElement;
  root.appendChild(el);
  var frame = global.frames[global.frames.length-1];
  var result = (typeof frame.addEventListener != "undefined");
  root.removeChild(el);
  el = null;
  return result;
})(this);

In all browsers I tested, same-named methods from any of two non-modified DOM contexts behave identically. This means that if window.addEventListener is present in one non-modified context, it will be present in another non-modified context. It will also function identically. This seems to be a de-facto standard; unlike string representation, all browsers seem to be pretty consistent in this regard.

Downsides

There are few problems with this approach. First, not all browsers support multiple contexts. Latest Blackberry browser (v. 4.7, 9500 “Storm” model), for example, does not allow to access window.frames “collection”, yet it’s a pretty functional browser which supports most of the DOM L2 methods. Somewhat similar issue is that Caja project does not support iframe creation at the moment (due to security restrictions, as I understand it). While Caja folks might support this in the future, the fact that some of the mobile browsers lack window.frames is very unfortunate.

Another downside is the execution speed. Creation and addition of iframe element to the document is relatively slow (comparing to such simple operations as string comparison or invocation of RegExp.prototype.test – as in original “toString” example). Using this approach in frequent, low-level methods is not a good idea. It’s, nevertheless, a viable solution as a one-time check, during application/script initialization.

Finally, it’s not quite easy to create a generic method that would utilize such non-modified context. Instead of passing just object/property pair (as in the original “toString” example), one would need to pass a string, and then try to somehow split that string into properties. A more reasonable solution could be to simply provide a method that returns a “clean” window object, but then there’s a problem of removing and discarding iframe element (which, when removed, discards returned object useless).

Despite these annoyances, I find iframe approach the only reliable solution at the moment. I suggest to never rely on function decompilation, especially when it comes to host methods.

What do you think?

Categories: iframes 14 Comments »

Comments (14)

  1. Gravatar

    Nicolas said:

    I’m not sure this is a good a approach or not, since I haven’t tested it enough, but I’ve noticed that built-in methods aren’t visible when iterating on the object.

    So for example Math has a bunch of built-in methods that aren’t visible when iterating on the object. However, added properties or methods are visible when iterating on the object:

    for (var i in Math) i; //nothing
     
    Math.lala = 'lala';
    for (var i in Math) i; //lala

    However, checking for the existence of the method works:

    'sin' in Math; //true
    

    The same goes in Array:

    'slice' in Array.prototype; //true
    
    for (var i in Array.prototype) i; //doesn't print 'slice'
    

    So would this work?

    function isNative(object, method) {
      var native = false;
      if(method in object) {
        native = true;
        for(var i in object) {
          if(i === method) {
           native = false;
          }
        }
      }
      return native;
    }
    
    isNative(Math, 'sin'); //true
    isNative(Math, 'lala'); //false
    
  2. Gravatar

    Nicolas said:

    Meh, should have checked with document first, :P, false alarm ;)

  3. Gravatar

    kangax (article author) said:

    @Nicolas

    What do you mean by “should have checked with document first”?

  4. Gravatar

    Nicolas said:

    Well, it seems that you can’t iterate through built-in methods in classes like Array, Object, Date or built-in objects like Math.

    However you can iterate through built-in methods of objects like window or document, so my isNative proposal wouldn’t work for (at least) those cases.

  5. Gravatar

    Andrea Giammarchi said:

    .. uhm, I wrote a comment with an alternative solution … not even a message like “ok, posted, just wait” … btw, here I am with a different snippet to make kangax suggestion smaller:
    var isNativeWindowAddEventListenerPresent = (function(global){
    var el = (document.body || document.documentElement).appendChild(document.createElement(‘iframe’)),
    result = typeof el.contentWindow.addEventListener != “undefined”;
    el.parentNode.removeChild(el);
    el = null;
    return result;
    })(this);

  6. Gravatar

    Diego Perini said:

    Juriy,
    the isNative() method showed above have lost the escape characters in front of each square bracket in the regexp, so it doesn’t actually work by just cutting & pasting it.

    Your testing still relies on several native methods and many functionalities you assume are present and working in every browser. You are in the same situation as for the toString(), hackers excluded…

    Any of those methods overwritten my fold your test but as I said this is not the point.

    You see, better to leave out that type of reasoning and concentrate on “by incident” and “due to bugs”. These are the only real facts we are facing and in need to solve, the rest is just a self achievement and a good low level javascript training.

    Diego

  7. Gravatar

    kangax (article author) said:

    Thanks, Diego.
    I updated post to escape brackets properly.

  8. Gravatar

    Andrea Giammarchi said:

    ehr … I posted my reply in Ajaxian, let’s find a page to discuss or I will start havig headache :D

  9. Gravatar

    eshow said:

    Did not get back to how significant the value of

Trackbacks

  1. Ajaxian » Building an isNative method said:

    [...] posted on the issue where he took a note Diego Perini on an impl: PLAIN TEXT [...]

  2. Building an isNative method | How2Pc said:

    [...] posted on the issue where he took a note Diego Perini on an impl: PLAIN TEXT [...]

  3. Building an isNative method | Wooost Blog said:

    [...] posted on the issue where he took a note Diego Perini on an impl: PLAIN TEXT [...]

  4. Building an isNative method | Guilda Blog said:

    [...] posted on the issue where he took a note Diego Perini on an impl: PLAIN TEXT [...]

  5. Javascript Kung Fu – Object, Array and Literals | Karmagination said:

    [...] as Object itself being native. Detecting natives is not as trivial as it seems, according to Juriy Zaytsev. Let us make this fail badly. // native prototype overloaded, some js libraries extends them [...]

Leave a Comment

Allowed tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre lang="" line="" escaped="">

Please, don't forget to escape your input (<, > and &). Wrap code sections with <pre>