What is a type in Javascript?
It's hard to succinctly and accurately define `type` in Javascript. A couple of complications are Javascript's dynamic typing (meaning that variables don't have types), and that the tools that Javascript provides don't unambiguously and uniquely divide values in separate groups (meaning that there's overlap and differences between the various ways, and no one single way is more "right" than the others). Given these difficulties, let's forget about coming up with an unambiguous definition of "type" and also forget about what "type" means in other languages. Instead, let's just say that for the purposes of this article, "type" will mean "some way to group objects based on certain similar properties".
What are some of the properties that we can use to group Javascript's values? First, we can look at whether a value is an object or a primitive. We can further divide and group the objects by their prototype chains or constructors. We can divide the primitives into smaller groups based on their primitive types.
However, as I mentioned earlier, there are alternative, meaningful ways to group values. For instance, objects with identical prototype chains can be separated (`arguments` vs `{}`). There is also some overlap between primitives and certain objects -- some primitives are automatically converted to objects under certain circumstances, similar to Java's autoboxing.
This article will take a close look at Javascript's type-inspecting tools, and the different notions of "type" that they provide. Using a small amount of code -- and a large set of test cases -- we'll gather some data about each of the tools. This data will give us insight into the pros and cons of each tool -- and perhaps help to indicate when each should be used.
What are the tools at our disposal?
Object.getPrototypeOf
Can be used recursively to get the full prototype chain of an object. See the MDN docs. Related: Object.prototype.isPrototypeOf.
Object.prototype.toString
Used by Underscore for type inspection.
instanceof
Performs inheritance checks which respect the prototype chain.
typeof
Mostly useful for distinguishing between primitives and objects.
Array.isArray
A special method for checking if an object is an array. (Since this kind of method only exists for arrays, I won't use it in the rest of this article).The code
We want to inspect as many different kinds of values as possible -- so let's make sure that we have each of the primitives, each of the commonly-used built-in object types, functions, user-defined types, `arguments`, object wrappers for primitives, and the root object for good measure. Remember, for each of these example values, we'll try each of the four previously-mentioned tools for inspecting types.
For each example, there's a meaningful string for display purposes, an expression that will evaluate to the desired value, and also a constructor function that we'll use for an `instanceof` check. The last part is pretty arbitrary -- I just use it to show that a given value can satisfy `instanceof` for multiple different constructors.
// put examples inside a function to get access to `arguments` function getExamples() { var functionText = "new Function('x', 'return x + 1;')"; return [ // schema: // 0: human-readable text // 1: expression to be inspected // 2: (optional) constructor for instanceof-checking ['undefined' , undefined , null ], ['null' , null , null ], ["'abc'" , 'abc' , String ], ["new String('abc')", new String('abc') , String ], ['123' , 123 , Number ], ['new Number(123)' , new Number(123) , Number ], ['Infinity' , Infinity , Number ], ['NaN' , NaN , Number ], ['true' , true , Boolean ], ['new Boolean(true)', new Boolean(true) , Boolean ], ['function g(x) {}' , function g(x) {} , Function ], [functionText , new Function('x', 'return x + 1;'), Function ], ["{'a': 1, 'b': 2}" , {'a': 1, 'b': 2} , null ], ['new Object()' , new Object() , null ], ['new ObjectExt()' , new ObjectExt() , ObjectExt], ['[1, 2, 3]' , [1, 2, 3] , Array ], ['new Array()' , new Array() , Array ], ['new ArrayExt()' , new ArrayExt() , Array ], ['/a/' , /a/ , RegExp ], ["new RegExp('a')" , new RegExp('a') , RegExp ], ['new RegExpExt()' , new RegExpExt() , RegExp ], ['new Date()' , new Date() , Date ], ["new Error('!')" , new Error('!') , Error ], ['Math' , Math , null ], ['JSON' , JSON , null ], ['arguments' , arguments , null ], ['this' , this /* the root object, right? */, Window ] ]; }Here's the code for setting up the three user-defined constructors. Each of Array, Object, and RegExp are extended:
// extend Array function ArrayExt() {} ArrayExt.prototype = [1, 2, 3]; // extend Object function ObjectExt() {} // extend RegExp function RegExpExt() {} RegExpExt.prototype = /matt/;Finally, the function used to grab an object's prototype chain. This throws an exception if `obj` is a primitive:
function getParents(obj) { var parents = [], par = obj; while ( true ) { par = Object.getPrototypeOf(par); if ( par === null ) { break; } parents.push(par); } return parents; }
The data
Now we take each of the example values, and apply each of the four tests to it -- plus an extra `instanceof` test to show inheritance. For each expression "e", we'll do:typeof e Object.prototype.toString.call(e) e instanceof Object e instanceof [subtype] getParents(e)Here are the results. Especially surprising, inconsistent, and strange results are in red:
example | typeof | Object.prototype.toString | instanceof Object | instanceof subtype | prototype chain |
---|---|---|---|---|---|
undefined | undefined | [object Undefined] | false | -- | -- |
null | object | [object Null] | false | -- | -- |
'abc' | string | [object String] | false | String: false | -- |
new String('abc') | object | [object String] | true | String: true | String,Object |
123 | number | [object Number] | false | Number: false | -- |
new Number(123) | object | [object Number] | true | Number: true | Number,Object |
Infinity | number | [object Number] | false | Number: false | -- |
NaN | number | [object Number] | false | Number: false | -- |
true | boolean | [object Boolean] | false | Boolean: false | -- |
new Boolean(true) | object | [object Boolean] | true | Boolean: true | Boolean,Object |
function g(x) {} | function | [object Function] | true | Function: true | Function,Object |
new Function('x', 'return x + 1;') | function | [object Function] | true | Function: true | Function,Object |
{'a': 1, 'b': 2} | object | [object Object] | true | -- | Object |
new Object() | object | [object Object] | true | -- | Object |
new ObjectExt() | object | [object Object] | true | ObjectExt: true | ObjectExt,Object |
[1, 2, 3] | object | [object Array] | true | Array: true | Array,Object |
new Array() | object | [object Array] | true | Array: true | Array,Object |
new ArrayExt() | object | [object Object] | true | Array: true | ArrayExt,Array,Object |
/a/ | object | [object RegExp] | true | RegExp: true | RegExp,Object |
new RegExp('a') | object | [object RegExp] | true | RegExp: true | RegExp,Object |
new RegExpExt() | object | [object Object] | true | RegExp: true | RegExpExt,RegExp,Object |
new Date() | object | [object Date] | true | Date: true | Date,Object |
new Error('!') | object | [object Error] | true | Error: true | Error,Object |
Math | object | [object Math] | true | -- | Object |
JSON | object | [object JSON] | true | -- | Object |
arguments | object | [object Arguments] | true | -- | Object |
this | object | [object global] | true | Window: true | Window,EventTarget,Object |
- these results were obtained in Firefox, Javascript 1.5; Chrome, Javascript 1.7
- the results in the last row vary by implementation
The analysis
typeof
`typeof` distinguishes between primitives and objects. However:- `typeof null` is "object"
- it returns "function" for functions, even though they are objects -- this is not wrong, just misleading
- for String, Number, and Boolean: `typeof` return "object" for wrapped values
- it doesn't distinguish between different objects -- arrays, dates, regexps, user-defined, etc.: all are just "object"
instanceof
`instanceof` checks whether a constructor's prototype property is in an object's prototype chain. However:- the 2nd argument must be a constructor. The constructor is used to look up a prototype. This is a problem if creating objects with `Object.create` -- there is no constructor function (that you have access to).
- may not work if there are objects moving across frames or windows
- different results for corresponding objects and primitives
- doesn't tell you what the prototypes actually are
Object.prototype.toString.call(...)
This seems to differentiate between the built-ins correctly. See this for more information. However:- it doesn't differentiate between corresponding objects and primitives
- it reports all primitives as objects
- doesn't seem to work for user-defined constructors and objects. Apparently, it depends on an internal [[Class]] property which can't be touched, according to this.
prototype chain using Object.getPrototypeOf
Gets the prototype objects. However:- can't distinguish `arguments` from `Math`, `JSON`, and other objects. In fact, can't distinguish between any objects that share the same prototype chain.
- doesn't work on primitives -- even those which have corresponding objects
- may fail for passing objects between windows/frames -- like `instanceof` -- (not sure)
No comments:
Post a Comment