9

I'm writing a Javascript stacktrace library. The library needs to detect wether a particular object or function was created by the programmer or was there as part of the environment (including built-in objects). Host objects are becoming a bit problematic due to their unpredictable behaviour, so I'm after an environment-agnostic way to determine if a particular object in Javascript is a host object (see ECMAScript 3 - 4.3.8). However, distinguishing host objects from native objects and primitive values is useful for programmers in other projects, particularly in browser-less environments, so I'd like to focus on that, rather than the problems host objects cause in my library or on distinguishing programmer-created objects.

So far I've only been able to come up with solutions that depend on the environment running javascript code. For example:

// IE Only: does not implement valueOf() in Host Objects
var isHost = (typeof obj === 'object' && typeof obj.valueOf === 'undefined');

// Firefox Only: Host objects have own constructor
var isHost = (obj.constructor && obj.hasOwnProperty('constructor'));

I noticed that jQuery's own isPlainObject() method is also dependent on environment, and that the logic is rather convoluted.

Perhaps this is because thats the nature of the beast with host objects (since their behaviour is defined by the environment), but I would like to dig a bit further to see if this is possible and was wondering if somebody has run across this particular problem before and has a solution ready.

So. Does anybody know a simple platform-independent solution to test for Host Objects? And if it runs in a browser-less environment such as Node or Rhino all the better for it.

Possible approaches (that may not work):

  • Testing for characteristics of host objects seems like a lost cause, given that there is no specification for their behaviour, however testing whether the object is part of the ES3 specification may be a possibility.
  • I have tried using Object.prototype.toString() given that its defined quite specifically, but the results are inconclusive as some environments (namely IE) choose to return the same value for native and host objects.
  • It may be possible to do this by checking whether the ultimate constructor of an object through the prototype chain really is an instanceof Function.
outis
  • 75,655
  • 22
  • 151
  • 221
Steven de Salas
  • 20,944
  • 9
  • 74
  • 82
  • 7
    What is the problem you're trying to solve? – Camilo Martin Dec 27 '11 at 14:42
  • 1
    I'm quite sure this doesn't work in all environments, but you can try `Object.prototype.toString.call(obj)` and see whether you get `[object Object]` (which is what plain objects/instances produce) or something like `[object HTMLBodyElement]` to determine whether it is a host object. – pimvdb Dec 27 '11 at 14:43
  • @CamiloMartin, Im working on a javascript stacktrace library, host objects are becoming a bit problematic due to their unpredictable behaviour so have to code around them. However the solution here is useful for programmers in other projects, particularly in browser-less environments. – Steven de Salas Dec 27 '11 at 15:00
  • I really doubt you'll find a cross-platform solution, but for browsers, pimvdb's suggestion is the one that I'd use. I have an idea though, I think I'll post it in an answer. – Camilo Martin Dec 27 '11 at 15:27
  • What's the problem you're having specifical to host objects? – Camilo Martin Dec 27 '11 at 16:03
  • I think @pimvdb might be on the ball there, Object.prototype.toString is quite specifically defined in ES3. – Steven de Salas Dec 27 '11 at 16:45
  • @Steven de Salas: I did just notice it doesn't work on IE7. – pimvdb Dec 27 '11 at 16:48
  • Yup, sadly `Object.prototype.toString.call(window) === Object.prototype.toString.call({})` – Steven de Salas Dec 27 '11 at 16:56
  • @StevendeSalas: requests for clarifications in the comments should be addressed by editing the question. SO is a Q&A site, not a forum. Questions should be understandable without having to read the comments. Comments are neither designed nor suited for discussions. – outis Dec 28 '11 at 06:06

5 Answers5

5

When you look at the definition of host object"object supplied by the host environment to complete the execution environment of ECMAScript." — it becomes pretty clear that there's no simple way to determine if an object is host or native.

Unlike native objects, host objects define internal properties (such as [[Prototype]], [[Class]], etc.) in implementation-specific way. That's because specification allows them to do this. However, there's no "MUST" requirement for host objects to implement internal behavior in implementation-specific way; it's a "MAY" type of requirement. So we can't rely on this. These objects may or may not act "weird". There's no way to tell.

There have been few attempts to detect host objects in the past, but all of them obviously rely on observations of certain environments (MSHTML DOM being one of them) — remember that host objects don't have any kind of unique pattern/trait to identify by. Peter Michaux documented most of the inferences here (take a look at "Feature Testing a Host Object" section). The infamous typeof ... == "unknown" comes from MSHTML DOM and its ActiveX -based host objects. Note that Peter talks about host objects mainly in context of browser scripting, and he narrows the checks down to "is this a host method?", "is this a host collection object", etc.

In some environments host objects don't inherit from Object.prototype (making it easy to check against), or have certain properties that throw errors (e.g. "prototype" on some "interface" objects in IE), or even throw errors themselves on access.

It might seem like you could just check if an object is one of those defined in specification, and if not, deem it as host. But that won't really help; it would only give you objects that aren't built-in. Some of these non-standard objects could still be native (meaning that they would implement usual semantics as described in specification).

Your best bet would be to test for particular behavior of your app/script, that host objects may be sensitive to. This is always the safest way. Are you planning to access something off of an object? Delete something from object? Add something to object? Test for it. See if it works. If it doesn't — you're likely dealing with host object.

kangax
  • 38,898
  • 13
  • 99
  • 135
  • Hi @kangax, while I agree with most of your answer the ECMAScript spec also states that *'Any object that is not native is a host object.'*, so it is theoretically possible to test for Host objects by checking they are *not* Native (as defined in the spec). The trick here is how to test that an object really *is* native, as the environment can make it difficult by replicating much of this behaviour in their host objects. – Steven de Salas Jan 03 '12 at 16:01
4

Here's a newer version of isNative that rejects all objects with a native implementation for toString, which solves the problem for the stack trace library but doesn't answer the question posted here satisfactorily. Where this approach fails for the latter is it filters out all built-in type definitions such as Object, Date, String, Math, etc., which are not host objects themselves. Also, this solution is dependent on how the environment outputs native/built-in function definitions (it must include "[native code]" for the function to work). Since the behaviour of the function is different, it's been renamed isUserObject.

// USER OBJECT DETECTION

function isUserObject(obj) {

    // Should be an instance of an Object
    if (!(obj instanceof Object)) return false;

    // Should have a constructor that is an instance of Function
    if (typeof obj.constructor === 'undefined') return false;
    if (!(obj.constructor instanceof Function)) return false;

    // Avoid built-in functions and type definitions
    if (obj instanceof Function && 
      Function.prototype.toString.call(obj).indexOf('[native code]') > -1) 
          return false;

    return true;
}

// CHECK IF AN OBJECT IS USER-CREATED OR NOT

if (typeof myObject === 'object' || typeof myObject === 'function')
   alert(isUserObject(myObject) ? 'User Object' : 'Non-user Object'); 

Here is a list of JsFiddle tests that can be used to test this in various browsers.

// ASSERT HELPER FUNCTION

var n = 0;
function assert(condition, message) {
    n++;
    if (condition !== true) {
       document.write(n + '. FAILS: ' + (message || '(no message)') + '<br/>');
    } else {
       document.write(n + '. PASS: ' + (message || '(no message)') + '<br/>');
    }
}

// USER CREATED OBJECTS

assert(isUserObject({}), '{} -- Plain object');
assert(isUserObject(function() {}), 'function() {} -- Plain function');
assert(isUserObject([]), '[] -- Plain array');

assert(isUserObject(/regex/), '/regex/ - Native regex');
assert(isUserObject(new Date()), 'new Date() - Native date object through instantiation');

assert(isUserObject(new String('string')), 'new String("string") - Native string object through instantiation');
assert(isUserObject(new Number(1)), 'new Number(1) - Native number object through instantiation');
assert(isUserObject(new Boolean(true)), 'new Boolean(true) - Native boolean object through instantiation');
assert(isUserObject(new Array()), 'new Array() - Native array object through instantiation');
assert(isUserObject(new Object()), '{} -- new Object() - Native object through instantiation');
assert(isUserObject(new Function('alert(1)')), '{} -- Native function through instantiation');

// USER OBJECT INSTANTIATION AND INHERITANCE

var Animal = function() {};
var animal = new Animal();

var Dog = function() {};
Dog.prototype = animal;
var dog = new Dog();

assert(isUserObject(Animal), 'Animal -- User defined type');
assert(isUserObject(animal), 'animal -- Instance of User defined type');

assert(isUserObject(Dog), 'Dog -- User defined inherited type');
assert(isUserObject(dog), 'dog -- Instance of User defined inherited type');

// BUILT IN OBJECTS

assert(!isUserObject(Object), 'Object -- Built in');
assert(!isUserObject(Array), 'Array -- Built in');
assert(!isUserObject(Date), 'Date -- Built in');
assert(!isUserObject(Boolean), 'Boolean -- Built in');
assert(!isUserObject(String), 'String -- Built in');
assert(!isUserObject(Function), 'Function -- Built in');

// PRIMITIVE TYPES 

assert(!isUserObject('string'), '"string" - Primitive string');
assert(!isUserObject(1), '1 - Primitive number');
assert(!isUserObject(true), 'true - Primitive boolean');
assert(!isUserObject(null), 'null - Primitive null');
assert(!isUserObject(NaN), 'NaN - Primitive number NotANumber');
assert(!isUserObject(Infinity), 'Infinity - Primitive number Infinity');
assert(!isUserObject(undefined), 'undefined - Primitive value undefined');

// HOST OBJECTS

assert(!isUserObject(window), 'window -- Host object');
assert(!isUserObject(alert), 'alert -- Host function');
assert(!isUserObject(document), 'document -- Host object');
assert(!isUserObject(location), 'location -- Host object');
assert(!isUserObject(navigator), 'navigator -- Host object');
assert(!isUserObject(parent), 'parent -- Host object');
assert(!isUserObject(frames), 'frames -- Host object');​
outis
  • 75,655
  • 22
  • 151
  • 221
2

ALMOST SOLVED

Almost managed to get this to work.

The solution falls short in that Host objects are sometimes undistinguishable from Native ones. The code below fails when testing isNative(window.alert) on Chrome as webkit engine defines an alert function that (so far) looks identical to a native one.

It uses plain javascript as per ES3 and is based on testing that an object is native (as opposed to Host object). However, as per ES3 definition of Host Objects: 'Any object that is not native is a host object.' this function can be used to detect host objects.

// ISNATIVE OBJECT DETECTION

function isNative(obj) {

    switch(typeof obj) {
        case 'number': case 'string': case 'boolean':
            // Primitive types are not native objects
            return false;
    }  

    // Should be an instance of an Object
    if (!(obj instanceof Object)) return false;

    // Should have a constructor that is an instance of Function
    if (typeof obj.constructor === 'undefined') return false;
    if (!(obj.constructor instanceof Function)) return false;

    return true;
}

// CHECK IF AN OBJECT IS HOST OR NATIVE

if (typeof myObject === 'object' || typeof myObject === 'function')
   alert(isNative(myObject) ? 'Native Object' : 'Host Object'); 

Here is a list of JsFiddle tests that can be used to test this in IE / Firefox / Chrome.

I havent tested Non-browser environments as its a bit more of a hassle but since the code is so basic I dont think it'll have any problems.

// ASSERT HELPER FUNCTION

var n = 0;
function assert(condition, message) {
    n++;
    if (condition !== true) {
       document.write(n + '. FAILS: ' + (message || '(no message)') + '<br/>');
    } else {
       document.write(n + '. PASS: ' + (message || '(no message)') + '<br/>');
    }
}

// USER CREATED OBJECTS

assert(isNative({}), '{} -- Plain object');
assert(isNative(function() {}), 'function() {} -- Plain function');
assert(isNative([]), '[] -- Plain array');

assert(isNative(/regex/), '/regex/ - Native regex');
assert(isNative(new Date()), 'new Date() - Native date object through instantiation');

assert(isNative(new String('string')), 'new String("string") - Native string object through instantiation');
assert(isNative(new Number(1)), 'new Number(1) - Native number object through instantiation');
assert(isNative(new Boolean(true)), 'new Boolean(true) - Native boolean object through instantiation');
assert(isNative(new Array()), 'new Array() - Native array object through instantiation');
assert(isNative(new Object()), '{} -- new Object() - Native object through instantiation');
assert(isNative(new Function('alert(1)')), '{} -- Native function through instantiation');

// USER OBJECT INSTANTIATION AND INHERITANCE

var Animal = function() {};
var animal = new Animal();

var Dog = function() {};
Dog.prototype = animal;
var dog = new Dog();

assert(isNative(Animal), 'Animal -- User defined type');
assert(isNative(animal), 'animal -- Instance of User defined type');

assert(isNative(Dog), 'Dog -- User defined inherited type');
assert(isNative(dog), 'dog -- Instance of User defined inherited type');

// BUILT IN OBJECTS

assert(isNative(Object), 'Object -- Built in');
assert(isNative(Array), 'Array -- Built in');
assert(isNative(Date), 'Date -- Built in');
assert(isNative(Boolean), 'Boolean -- Built in');
assert(isNative(String), 'String -- Built in');
assert(isNative(Function), 'Function -- Built in');

// PRIMITIVE TYPES 

assert(!isNative('string'), '"string" - Primitive string');
assert(!isNative(1), '1 - Primitive number');
assert(!isNative(true), 'true - Primitive boolean');
assert(!isNative(null), 'null - Primitive null');
assert(!isNative(NaN), 'NaN - Primitive number NotANumber');
assert(!isNative(Infinity), 'Infinity - Primitive number Infinity');
assert(!isNative(undefined), 'undefined - Primitive value undefined');

// HOST OBJECTS

assert(!isNative(window), 'window -- Host object');
assert(!isNative(alert), 'alert -- Host function'); // fails on chrome
assert(!isNative(document), 'document -- Host object');
assert(!isNative(location), 'location -- Host object');
assert(!isNative(navigator), 'navigator -- Host object');
assert(!isNative(parent), 'parent -- Host object');
assert(!isNative(frames), 'frames -- Host object');
Steven de Salas
  • 20,944
  • 9
  • 74
  • 82
  • `process.binding('evals').NodeScript.constructor instanceof Function === true;` I'm pretty sure NodeScript is defined in C++ in node.js and should arguably qualify as a "host object". Yoru solution is difficult to extend to non browser environments – Raynos Jan 04 '12 at 12:02
  • I'm not sure whether or not this is correct but `alert` itself is a native function according to `isNative`. – pimvdb Jan 04 '12 at 12:10
  • @Steven de Salas: Apparently, `alert` is `instanceof {Object, Function}`, so there is no catch in `isNative`. I don't know what property makes `alert` distinguishable, though... – pimvdb Jan 04 '12 at 12:25
  • @Steven de Salas: Though you can test for `new alert` - it throws a specific error which you can try/catch. But then again this is also becoming just another ugly list of host object tricks, which you said you wanted to avoid. – pimvdb Jan 04 '12 at 12:27
  • Yep the problem here is false positives, since the environment will sometimes emulate behaviour in their host objects to make them undistinguishable from native ones. I guess the only way forward is to test more features see if it fails on any of those but this will never be perfect as host objects *are* allowed to emulate native ones. – Steven de Salas Jan 04 '12 at 12:54
  • `Object.create(null)` is definitely a native object. – Bergi Nov 07 '14 at 00:55
  • *instanceof* tests fail across frames (hence the introduction of [*isArray*](http://ecma-international.org/ecma-262/7.0/index.html#sec-isarray)), so the above will return false negatives. *isNative* also returns true for DOM (host) objects like *HTML DIV* and *document* elements. – RobG Dec 28 '16 at 07:17
0

I believe that the very nature of host objects means there is not a simple, environment-agnostic way to detect them. See this discussion on SO for more, if you're curious.

As you have noted, the jQuery project has also tried to detect host objects and run into similar troubles. The discussion on that bug page is very telling.

Community
  • 1
  • 1
Josh Smith
  • 14,674
  • 18
  • 72
  • 118
0

I have an idea that may not be applicable in all contexts.

Make sure your script is the first one to execute, and wrap it in a closure, much like JS frameworks do.
Then, loop through all objects in your global scope (If you are on something that's not a browser, window will be undefined; hence at the beggining of the script do a window = this), and loop through its children, and so on. All objects except your will be host objects! then you can add that to a local database or even store it and associate it with the running environment for future use.

Camilo Martin
  • 37,236
  • 20
  • 111
  • 154