Perfection Kills

by kangax

Exploring Javascript by example

← back 914 words

Detecting built-in host methods

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?

Did you like this? Donations are welcome

comments powered by Disqus