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?
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:
However, checking for the existence of the method works:
The same goes in Array:
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'); //falseNicolas said:
#Meh, should have checked with document first, :P, false alarm ;)
kangax (article author) said:
#@Nicolas
What do you mean by “should have checked with document first”?
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.
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);
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
kangax (article author) said:
#Thanks, Diego.
I updated post to escape brackets properly.
Andrea Giammarchi said:
#ehr … I posted my reply in Ajaxian, let’s find a page to discuss or I will start havig headache :D
eshow said:
#Did not get back to how significant the value of