16

Consider the following:

var o1 = {}
var O = function () {
  return this
}
var o2 = new O()
var o3 = function() {}
var o4 = [o1, o1]

var output = [
    [_.isObject(o1), _.isObject(o2), _.isObject(o3), _.isObject(o4)], 
    [_.isPlainObject(o1), _.isPlainObject(o2), _.isPlainObject(o3), _.isPlainObject(o4)],
    [typeof o1 === 'object', typeof o2 === 'object', typeof o3 === 'object', typeof o4 === 'object'],
    [o1 instanceof Array, o2 instanceof Array, o3 instanceof Array, o4 instanceof Array]
]

/* outputs:

[
    [true,true,true,true],
    [true,false,false,false],
    [true,true,false,true],
    [false,false,false,true]
]

*/

Clearly we can see that there is a disconnect between .isObject():

Which checks if value is the language type of Object. (e.g. arrays, functions, objects, regexes, new Number(0), and new String(' ')

.isPlainObject():

Which if value is a plain object, that is, an object created by the Object constructor or one with a Prototype of null

And our good ol' trusty friend typeof x === 'object'.

I have three questions:

  1. Was there a conscious design decision to make .isObject and .isPlainObject behave differently than the native .js type checking?
  2. If my first question is true, what was the design decision and what are the benefits of doing it this way?
  3. Is there any native (or ) is* function that behaves exactly the same as typeof x === 'object'?

Obviously I can just continue to use typeof, but syntactically it's a bit weird to use one or the other in some places, for example the usage of .isObject will return false positives when checking for typeof x === 'object' && typeof x !== 'function'. I don't really see any benefit of .isObject returning true for functions when .isFunction already exists.

brandonscript
  • 68,675
  • 32
  • 163
  • 220
  • Do you see any benefit to isObject return false for functions when !.isFunction already exists? It seems your argument is that you expect lodash to be a transliteration of lower level Javascript constructs. – djechlin Dec 09 '15 at 17:33
  • @djechlin I don't know tbh. It seems like the helpers are a bit two-faced - why have one behave one way and one another? Why not mirror the lower level js constructs? Why be different? – brandonscript Dec 09 '15 at 17:35
  • Why not check lodash/underscore source on github? – huysentruitw Dec 09 '15 at 17:35
  • isPlainObject well explained here http://stackoverflow.com/questions/5876332/how-can-i-differentiate-between-an-object-literal-other-javascript-objects – Cory Danielson Dec 09 '15 at 17:46

3 Answers3

15

typeof has nothing to do with whether something is an object. Functions, strings, and {} have different typeof and they are all objects. Functions are of course first-class objects, just like strings are first-class objects, therefore isObject must return true for strings and objects.

For the record the documentation covers this:

Checks if value is the language type of Object. (e.g. arrays, functions, objects, regexes, new Number(0), and new String(''))

Wow! that really is a lot to test for without having a handy isObject method. To be fair most return typeof as object, but the point of higher level methods, especially in libraries like lodash, is so the programmer can forget about that nonsense.

If you care about the typeof an argument, then use typeof. If you care about objects that are not functions, you have a couple options: you can use typeof and check strings specially, or you can use isObject && !isFunction. I would prefer the latter. The latter does happen to say exactly what you are trying to convey so it really is the correct thing to code. If you think when you say "object" that you implicitly don't mean functions, then you do not think functions are first-class objects, or rather you would like your code to more closely resemble a language in which they're not. But then you can't blame lodash for being a library that extensively uses the fact that functions are first-class objects to make a language in which functions are first-class objects more expressive.

I believe that was the bulk of your question. I believe the use case for isPlainObject is to answer the question "is this just data?" or "is this code?" so objects created as pseudo-classes (new of something) don't count.

djechlin
  • 59,258
  • 35
  • 162
  • 290
  • Thanks for the detailed response. For the record, I was aware of the docs (you can see that in my OP ;) – brandonscript Dec 09 '15 at 17:48
  • @remus indeed. I added a sentence on `isPlainObject` if it helps. – djechlin Dec 09 '15 at 17:51
  • Yup, I know about that too. But where that returns false positives (at least in comparison to `typeof`) is when checking arrays. `typeof` returns true for arrays, but `.isPlainObject` returns false. It gets confusing really quick when you're trying to check multiple types and need to use the correct lib or native implementation that doesn't return false positives. – brandonscript Dec 09 '15 at 17:53
14

Sometimes code speaks louder than words. Here's the source from lodash:

function isObject(value) {
  var type = typeof value;
  return !!value && (type == 'object' || type == 'function');
}
function isPlainObject(value) {
  var Ctor;
  if (!(isObjectLike(value) && objToString.call(value) == objectTag && !isHostObject(value) && !isArguments(value)) ||
      (!hasOwnProperty.call(value, 'constructor') && (Ctor = value.constructor, typeof Ctor == 'function' && !(Ctor instanceof Ctor)))) {
    return false;
  }
  var result;
  if (lodash.support.ownLast) {
    baseForIn(value, function(subValue, key, object) {
      result = hasOwnProperty.call(object, key);
      return false;
    });
    return result !== false;
  }
  baseForIn(value, function(subValue, key) {
    result = key;
  });
  return result === undefined || hasOwnProperty.call(value, result);
}

According to the lodash docs:

isObject

Checks if value is the language type of Object. (e.g. arrays, functions, objects, regexes, new Number(0), and new String(''))

isPlainObject

Checks if value is a plain object, that is, an object created by the Object constructor or one with a [[Prototype]] of null.

What is an object?

Perhaps the existence of an isObject function is inherently a bit confusing when in JavaScript so many things act like objects. The key idea is that some values in JavaScript are considered primitives and others considered full-blown objects. Strings, numbers, and booleans are treated differently at an internal level than objects created with the object literal or constructor functions (this doesn't end up have much practical significance because primitives will automatically cast themselves to objects when necessary).

The fact that typeof null === 'object' probably originates from the fact that objects are reference types and null is often returned from functions in lieu of an object. (A quirk which likely hearkens back to the concept of pointers and the NULLPTR in C and C++. The type void * has many values but the NULLPTR is still considered a valid value for the pointer type.)

Ethan Lynn
  • 1,009
  • 6
  • 13
  • Frankly this is the line that gets me worked up: `return !!value && (type == 'object' || type == 'function');` Right there it checks that it's an object or a function. I realize it does that by design, I guess I just don't agree with the choice. – brandonscript Dec 13 '15 at 21:44
  • 1
    Updated answer with descriptions from lodash. Basically `typeof null` returns `"object"` which doesn't really make sense so the `!!value` part takes care of that. I think the reason the function test was put in there is because functions are also objects in JavaScript. The whole notion of there existing a single "object" type is what's confusing. There are many different specifications for the word "object" in JavaScript because many things behave like objects to varying degrees. – Ethan Lynn Dec 13 '15 at 23:47
  • Yeah that makes sense. – brandonscript Dec 13 '15 at 23:51
  • I was kind of curious myself, read the spec a little bit and it looks like the object language type is basically anything that isn't null, undefined, or a primitive string, number, or boolean. Naturally, a function isn't any of these things so based on the spec you must consider it an object. – Ethan Lynn Dec 13 '15 at 23:55
3
  1. The lodash docs describes Lodash as "a JavaScript utility library delivering consistency, modularity, performance, & extras." isObject and isPlainObject would be an extra. They are utility functions. I'm sure they were aware that they are different then typeof which is why they may be useful to some people. They probably thought the performance, consistency, and syntax of the native typeof didn't warrant the making of .typeOf that does the same thing.
  2. As mentioned above, the benefits are that it functions a little differently than typeof which may be handy to some people. Also lodash focuses on better performance as well, although I'm not sure if there performance of such a simple function could be significantly improved. It is said to improve the performance of loops though. You can test the performance difference using JSPerf and let us know! As far as why they decided to have .isObject return true for functions is because functions in JS are objects. It is kind of deceiving when native typeof returns function and not object. It is further convoluted by the fact that typeof [] doesnt return array and instead returns object so why should it return function and not object on a function.
  3. Neither lodash or underscore appear to have a function that is the same as typeof. Underscore has _.isObject which is the same as the lodash .isObject. You could use lodash's .isFunction and .isObject to create your own typeof function that returns the same thing native typeof would.
Daniel Kobe
  • 9,376
  • 15
  • 62
  • 109