Update: I'm rephrasing this question, because the important point to me is identifying the object literal:
How can I tell the difference between an object literal and any other Javascript object (e.g. a DOM node, a Date object, etc.)? How can I write this function:
function f(x) {
if (typeof x === 'object literal')
console.log('Object literal!');
else
console.log('Something else!');
}
So that it only prints Object literal!
as a result of the first call below:
f({name: 'Tom'});
f(function() {});
f(new String('howdy'));
f('hello');
f(document);
Original Question
I'm writing a Javascript function that is designed to accept an object literal, a string, or a DOM node as its argument. It needs to handle each argument slightly differently, but at the moment I can't figure out how to differentiate between a DOM node and a plain old object literal.
Here is a greatly simplified version of my function, along with a test for each kind of argument I need to handle:
function f(x) {
if (typeof x == 'string')
console.log('Got a string!');
else if (typeof x == 'object')
console.log('Got an object literal!');
else
console.log('Got a DOM node!');
}
f('hello');
f({name: 'Tom'});
f(document);
This code will log the same message for the second two calls. I can't figure out what to include in the else if
clause. I've tried other variations like x instanceof Object
that have the same effect.
I understand that this might be bad API/code design on my part. Even if it is, I'd still like to know how to do this.
How can I tell the difference between an object literal and any other Javascript object (e.g. a DOM node, a Date object, etc.)?
The short answer is you can't.
An object literal is something like:
var objLiteral = {foo: 'foo', bar: 'bar'};
whereas the same object created using the Object constructor might be:
var obj = new Object();
obj.foo = 'foo';
obj.bar = 'bar';
I don't think there is any reliable way to tell the difference between how the two objects were created.
Why is it important?
A general feature testing strategy is to test the properties of the objects passed to a function to determine if they support the methods that are to be called. That way you don't really care how an object is created.
You can employ "duck typing", but only to a limited extent. You can't guarantee that just because an object has, for example, a getFullYear()
method that it is a Date object. Similarly, just because it has a nodeType property doesn't mean it's a DOM object.
For example, the jQuery isPlainObject
function thinks that if an object has a nodeType property, it's a DOM node, and if it has a setInterval
property it's a Window object. That sort of duck typing is extremely simplistic and will fail in some cases.
You may also note that jQuery depends on properties being returned in a specific order - another dangerous assumption that is not supported by any standard (though some supporters are trying to change the standard to suit their assumed behaviour).
Edit 22-Apr-2014: in version 1.10 jQuery includes a support.ownLast property based on testing a single property (apparently this is for IE9 support) to see if inherited properties are enumerated first or last. This continues to ignore the fact that an object's properties can be returned in any order, regardless of whether they are inherited or own, and may be jumbled.
Probably the simplest test for "plain" objects is:
function isPlainObj(o) {
return typeof o == 'object' && o.constructor == Object;
}
Which will always be true for objects created using object literals or the Object constructor, but may well give spurious results for objects created other ways and may (probably will) fail across frames. You could add an instanceof
test too, but I can't see that it does anything that the constructor test doesn't.
If you are passing ActiveX objects, best to wrap it in try..catch as they can return all sorts of weird results, even throw errors.
Edit 13-Oct-2015
Of course there are some traps:
isPlainObject( {constructor: 'foo'} ); // false, should be true
// In global scope
var constructor = Object;
isPlainObject( this ); // true, should be false
Messing with the constructor property will cause issues. There are other traps too, such as objects created by constructors other than Object.
Since ES5 is now pretty much ubiquitous, there is Object.getPrototypeOf to check the [[Prototype]]
of an object. If it's the buit–in Object.prototype, then the object is a plain object. However, some developers wish to create truly "empty" objects that have no inherited properties. This can be done using:
var emptyObj = Object.create(null);
In this case, the [[Prototype]]
property is null. So simply checking if the internal prototype is Object.prototype isn't sufficient.
There is also the reasonably widely used:
Object.prototype.toString.call(valueToTest)
that was specified as returning a string based on the internal [[Class]]
property, which for Objects is [object Object]. However, that has changed in ECMAScript 2015 so that tests are performed for other types of object and the default is [object Object], so the object may not be a "plain object", just one that isn't recognised as something else. The specification therefore notes that:
"[testing using toString] does not provide a reliable type testing mechanism for other kinds of built-in or program defined objects."
http://www.ecma-international.org/ecma-262/6.0/index.html#sec-object.prototype.tostring
So an updated function that allows for pre–ES5 hosts, objects with a [[Prototype]]
of null and other object types that don't have getPrototypeOf (such as null, thanks Chris Nielsen) is below.
Note that there is no way to polyfill getPrototypeOf, so may not be useful if support for older browsers is required (e.g. IE 8 and lower, according to MDN).
/* Function to test if an object is a plain object, i.e. is constructed
** by the built-in Object constructor and inherits directly from Object.prototype
** or null. Some built-in objects pass the test, e.g. Math which is a plain object
** and some host or exotic objects may pass also.
**
** @param {} obj - value to test
** @returns {Boolean} true if passes tests, false otherwise
*/
function isPlainObject(obj) {
// Basic check for Type object that's not null
if (typeof obj == 'object' && obj !== null) {
// If Object.getPrototypeOf supported, use it
if (typeof Object.getPrototypeOf == 'function') {
var proto = Object.getPrototypeOf(obj);
return proto === Object.prototype || proto === null;
}
// Otherwise, use internal class
// This should be reliable as if getPrototypeOf not supported, is pre-ES5
return Object.prototype.toString.call(obj) == '[object Object]';
}
// Not an object
return false;
}
// Tests
var data = {
'Host object': document.createElement('div'),
'null' : null,
'new Object' : {},
'Object.create(null)' : Object.create(null),
'Instance of other object' : (function() {function Foo(){};return new Foo()}()),
'Number primitive ' : 5,
'String primitive ' : 'P',
'Number Object' : new Number(6),
'Built-in Math' : Math
};
Object.keys(data).forEach(function(item) {
document.write(item + ': ' + isPlainObject(data[item]) + '<br>');
});