Looking At Prototypal Inheritance To Determine Data Types In JavaScript
Yesterday, I was working on some JavaScript that needed to execute slightly different actions when a given value was either an Object or an Array. Typically for this, I would use something like Underscore.js or Lodash.js, which converts the value to a String and looks at the result; but, as I was writing the code, I wondered if we could use the given value's Prototype chain as a means to determine its true type.
In most of the type-checking methods that I see in JavaScript, a value will be converted to a string using the core toString() method provided by the Object prototype. And this works really well. But, as an experiment, I wanted to see if the same type check could be performed by augmenting the root Prototypes and then checking to see which value was inherited (via the prototype chain) by the given value.
NOTE: This is only a fun experiment.
<!doctype html>
<html>
<head>
<title>
Using Prototypal Inheritance To Test Data Type
</title>
<script type="text/javascript">
// The usual approach to type checking is to convert the value
// to a string and check the result against a known value.
function usualIsArray( value ) {
var toString = Object.prototype.toString;
return( toString.call( value ) === "[object Array]" );
}
// The usual approach to type checking is to convert the value
// to a string and check the result against a known value.
function usualIsObject( value ) {
var toString = Object.prototype.toString;
return( toString.call( value ) === "[object Object]" );
}
// -------------------------------------------------- //
// -------------------------------------------------- //
// I check to see which core type references gets dynamically
// inherited by the given value.
function typeReference( value ) {
// Quick check for null.
if ( value === null ) {
return( null );
}
// Quick check for undefined.
var undefined = Object.prototype.undefined;
if ( value === undefined ) {
return( undefined );
}
// Temporarily inject root references.
Object.prototype.__type_reference__ = Object;
Array.prototype.__type_reference__ = Array;
String.prototype.__type_reference__ = String;
Number.prototype.__type_reference__ = Number;
Boolean.prototype.__type_reference__ = Boolean;
Date.prototype.__type_reference__ = Date;
// Get prototypically inherited type reference that we
// temporarily injected above.
var reference = value.__type_reference__;
// Clean up temp references.
delete( Object.prototype.__type_reference__ );
delete( Array.prototype.__type_reference__ );
delete( String.prototype.__type_reference__ );
delete( Number.prototype.__type_reference__ );
delete( Boolean.prototype.__type_reference__ );
delete( Date.prototype.__type_reference__ );
return( reference );
}
// Checks data type by seeing if the given value inherits a
// core prototype property.
function isObject( value ) {
return( typeReference( value ) === Object );
}
// Checks data type by seeing if the given value inherits a
// core prototype property.
function isArray( value ) {
return( typeReference( value ) === Array );
}
// Checks data type by seeing if the given value inherits a
// core prototype property.
function isString( value ) {
return( typeReference( value ) === String );
}
// Checks data type by seeing if the given value inherits a
// core prototype property.
function isNumber( value ) {
return( typeReference( value ) === Number );
}
// Checks data type by seeing if the given value inherits a
// core prototype property.
function isBoolean( value ) {
return( typeReference( value ) === Boolean );
}
// Checks data type by seeing if the given value inherits a
// core prototype property.
function isDate( value ) {
return( typeReference( value ) === Date );
}
// Checks data type by seeing if the given value inherits a
// core prototype property.
function isNull( value ) {
return( typeReference( value ) === null );
}
// Checks data type by seeing if the given value inherits a
// core prototype property.
function isUndefined( value ) {
return( typeReference( value ) === window.undefined );
}
// -------------------------------------------------- //
// -------------------------------------------------- //
// Testing for an object.
console.log( "- - isObject - -" );
console.log( "Object:", isObject( {} ) );
console.log( "Array:", isObject( [] ) );
console.log( "String:", isObject( "test" ) );
console.log( "Number:", isObject( 123 ) );
console.log( "Boolean:", isObject( true ) );
console.log( "Date:", isObject( new Date() ) );
console.log( "Null:", isObject( null ) );
console.log( "Undefined:", isObject( window.undefined ) );
console.log( " " );
// Testing for an array.
console.log( "- - isArray - -" );
console.log( "Object:", isArray( {} ) );
console.log( "Array:", isArray( [] ) );
console.log( "String:", isArray( "test" ) );
console.log( "Number:", isArray( 123 ) );
console.log( "Boolean:", isArray( true ) );
console.log( "Date:", isArray( new Date() ) );
console.log( "Null:", isArray( null ) );
console.log( "Undefined:", isArray( window.undefined ) );
console.log( " " );
// Testing for a String.
console.log( "- - isString - -" );
console.log( "Object:", isString( {} ) );
console.log( "Array:", isString( [] ) );
console.log( "String:", isString( "test" ) );
console.log( "Number:", isString( 123 ) );
console.log( "Boolean:", isString( true ) );
console.log( "Date:", isString( new Date() ) );
console.log( "Null:", isString( null ) );
console.log( "Undefined:", isString( window.undefined ) );
console.log( " " );
// Testing for a Number.
console.log( "- - isNumber - -" );
console.log( "Object:", isNumber( {} ) );
console.log( "Array:", isNumber( [] ) );
console.log( "String:", isNumber( "test" ) );
console.log( "Number:", isNumber( 123 ) );
console.log( "Boolean:", isNumber( true ) );
console.log( "Date:", isNumber( new Date() ) );
console.log( "Null:", isNumber( null ) );
console.log( "Undefined:", isNumber( window.undefined ) );
console.log( " " );
// Testing for a Boolean.
console.log( "- - isBoolean - -" );
console.log( "Object:", isBoolean( {} ) );
console.log( "Array:", isBoolean( [] ) );
console.log( "String:", isBoolean( "test" ) );
console.log( "Number:", isBoolean( 123 ) );
console.log( "Boolean:", isBoolean( true ) );
console.log( "Date:", isBoolean( new Date() ) );
console.log( "Null:", isBoolean( null ) );
console.log( "Undefined:", isBoolean( window.undefined ) );
console.log( " " );
// Testing for a Date.
console.log( "- - isDate - -" );
console.log( "Object:", isDate( {} ) );
console.log( "Array:", isDate( [] ) );
console.log( "String:", isDate( "test" ) );
console.log( "Number:", isDate( 123 ) );
console.log( "Boolean:", isDate( true ) );
console.log( "Date:", isDate( new Date() ) );
console.log( "Null:", isDate( null ) );
console.log( "Undefined:", isDate( window.undefined ) );
console.log( " " );
// Testing for a Null.
console.log( "- - isNull - -" );
console.log( "Object:", isNull( {} ) );
console.log( "Array:", isNull( [] ) );
console.log( "String:", isNull( "test" ) );
console.log( "Number:", isNull( 123 ) );
console.log( "Boolean:", isNull( true ) );
console.log( "Date:", isNull( new Date() ) );
console.log( "Null:", isNull( null ) );
console.log( "Undefined:", isNull( window.undefined ) );
console.log( " " );
// Testing for a Undefined.
console.log( "- - isUndefined - -" );
console.log( "Object:", isUndefined( {} ) );
console.log( "Array:", isUndefined( [] ) );
console.log( "String:", isUndefined( "test" ) );
console.log( "Number:", isUndefined( 123 ) );
console.log( "Boolean:", isUndefined( true ) );
console.log( "Date:", isUndefined( new Date() ) );
console.log( "Null:", isUndefined( null ) );
console.log( "Undefined:", isUndefined( window.undefined ) );
// -------------------------------------------------- //
// -------------------------------------------------- //
// Test how this behaves when sub-classing array.
// NOTE: This not really how you would want to subclass an
// array since the array has "magic" properties (ex. length).
function MyArray() {}
MyArray.prototype = Object.create( Array.prototype );
// Create instance of new sub-classed array.
var myArray = new MyArray();
console.log( " " );
console.log( "- - Sub-Classed Array - -" );
console.log( "usualIsArray:", usualIsArray( myArray ) );
console.log( "isArray:", isArray( myArray ) );
</script>
</head>
<body>
<!-- Left intentionally blank. -->
</body>
</html>
As you can see, I am injecting a circular JavaScript type reference into the Prototype of each JavaScript data type. Then, I am checking to see which type reference is inherited through prototypal inheritance. When I run the above code, I get the following console output:
- isObject - -
Object: true
Array: false
String: false
Number: false
Boolean: false
Date: false
Null: false
Undefined: false
- isArray - -
Object: false
Array: true
String: false
Number: false
Boolean: false
Date: false
Null: false
Undefined: false
- isString - -
Object: false
Array: false
String: true
Number: false
Boolean: false
Date: false
Null: false
Undefined: false
- isNumber - -
Object: false
Array: false
String: false
Number: true
Boolean: false
Date: false
Null: false
Undefined: false
- isBoolean - -
Object: false
Array: false
String: false
Number: false
Boolean: true
Date: false
Null: false
Undefined: false
- isDate - -
Object: false
Array: false
String: false
Number: false
Boolean: false
Date: true
Null: false
Undefined: false
- isNull - -
Object: false
Array: false
String: false
Number: false
Boolean: false
Date: false
Null: true
Undefined: false
- isUndefined - -
Object: false
Array: false
String: false
Number: false
Boolean: false
Date: false
Null: false
Undefined: true
- Sub-Classed Array - -
usualIsArray: false
isArray: true
As you can see, I was able to determine each data type based on the reference provided by the Prototype chain. And, what's more, this method also correctly identifies a sub-classed Array as an Array, rather than as an Object (which is the incorrect result the toString() approach will provide).
I'm not actually advocating this approach; the Object.prototype.toString() approach is much faster (I assume) and requires significantly less code. I only found this experiment interesting because it leverages the existing prototypal inheritance mechanism rather than piggy-backing on some coincidental behavior exhibited by the toString() method.
Want to use code from this post? Check out the license.
Reader Comments
I remember seeing something like this in SmallTalk, but it went far and above what you just described here. In the little that I learned of SmallTalk, it seemed typical that every class would define a function identifying itself (and any descendants) as an instance of that class.
It would be simple to implement this in JavaScript. For instance, I would create a class Ball and one of its methods would be isBall(). It would also define isBall() as a method in Object. Object.isBall() would return false and Ball.isBall() would return true. If you defined descendants of Ball, Basketball, Baseball, and SoccerBall, the isBall() method would return true for them as well.
@Ben I like this. You can get a significant speed boost in real usage by removing the typeReference function and having the add/remove property stuff inside the isBlah functions.
e.g.
function isNumber(value){
/* null and undefined checks go here if desired */
Number.prototype.__type_reference__ = Number;
var reference = value.__type_reference__;
delete (Number.prototype.__type_reference__);
return (reference === Number);
}
@Grumpy,
Good call; and good point that I don't necessarily need to be setting ALL the reference types for each type-check. As long as I set / check the single reference type, it will work.
@Paul,
Are you saying that the base object would contain isXYZ methods for all derived types?
@Ben,
Yes, that would be the result. In JavaScript, you would end up with an Object class that has isBall, isBasketball, isBaseball, isSoccerBall, etc.
The trade-off is having a function defined that will always return a boolean value at the cost of having all of those functions defined for every class.
@Ben
are you in a vacation? looking for more of your angular posts.
@Aladdin,
Ha ha, I wish!!! Work has just been eating up my free time lately :( I actually just got one AngularJS post up today; but it's a quick one. I keep waiting for some more free time, but there's not enough. I have some interesting ideas in the queue, though, so when I have time, expect more!
I appreciate that. Thank you very much.