Cómo obtener el tipo de datos preciso de una variable en Javascript

16 Ago 2011

Introducción

Una de las características del lenguaje Javascript más controvertidas es su tipado blando o dinámico del que ya hemos hablado en alguna ocasión. Esto significa que, a diferencia de otros lenguajes clásicos como C o Java, cuando declaramos variables no necesitamos especificar el tipo de datos que contendrán. Es más, una misma variable, puede poseer varios tipos diferentes en distintos momentos de ejecución: puede comenzar siendo un entero, pasar a ser un array y más adelante, por ejemplo, acabar siendo un objeto.

Esta flexibilidad tiene ventajas muy interesantes para el desarrollo de aplicaciones complejas ya que no existen a priori limitaciones en cuanto a lo que una variable puede almacenar. Sin embargo, para aquellos que provienen de otros lenguajes de tipado duro, uno de los problemas de este concepto es que resulta muy difícil identificar el tipo de datos concreto que una variable posee en un momento dado de la ejecución.

Identificando el tipo

Efectivamente, Javascript no posse un medio unívoco para identificar el tipo de datos que contiene una variable en un momento dado. De forma nativa, poseemos dos instrucciones que pueden darnos un valor aproximado pero que, desgraciadamente, no resultan definitivas: typeof e instanceof.

El operador typeof

typeof es un operador unario, lo que quiere decir que solo acepta (y opera) sobre un único argumento; en este caso una variable:

typeof 2; // number
typeof 'Hello World' // string
typeof [1,2,3] // object

Este operador no es una función; sin embargo, pueden utilizarse paréntesis para agrupar los términos a evaluar:

typeof(2); // number
typeof ('Hello World'); // string
typeof('foo', 4); // number

NOTA: Las agrupaciones de argumentos en Javascript determinan el valor final de un elemento mediante el uso de operadores internos. Estos operadores indican el orden en que son evaluados los términos, como la coma, la cual realiza dicha evaluación de izquierda a derecha para devolver el valor del segundo operando. Para más información sobre este aspecto del lenguaje, se recomienda el artículo The Javascript Comma Operator.

La siguiente tabla muestra la correspondencia de objetos Javascript y el valor obtenido con typeof:

Tipo Resultado
Undefined «undefined»
Null «object»
Boolean «boolean»
Number «number»
String «string»
Host Object (dentro del entorno JS) depende de la implementación
Objeto Function «function»
Objeto XML E4X «xml»
Objeto XMLList E4X «xml»
Cualquier otro objeto «object

 

Como podemos observar, no hay referencia en la tabla anterior a elementos como los arrays, fechas, expresiones regulares, etc... Esto quiere decir que para typeof, esos elementos son un objeto más. Además, vemos algunos resultados que pueden parecer confusos como el de null en el que obtenemos como tipo de nuevo un ‘object‘:

typeof NaN; // number
typeof Infinity; // number
typeof (1/0); // number
typeof (typeof []);  // string

Parece que no podemos confiar demasiado en este operador para determinar el tipo de datos que evaluamos, por lo que puede ser interesante buscar otra solución más segura o completa.

Object.prototype.toString

La función toString, devuelve una cadena que representa al objeto que se le indica como argumento:

Object.prototype.toString();  // [object Object]

Observemos el resultado: «[object Object]»: según la especificación ECMASCript5, Object.prototype.toString devuelve como resultado la concatenación de la cadena ‘object’ más el valor interno del objeto que se pasa (eso que llamamos clase – Class)…

[[Class]]

Todos los objetos Javascript tienen una propieadad interna conocida como [[Class]] (la notación con doble corchete es la misma que se utiliza en la especificación ES5). Según el ES5, [[Class]] es una cadena con un valor único (no editable) que identifica a cada objeto. De ahí que un objeto invocado con el constructor y que no ha sido modificado, devuelva como valor de esta propiedad el tipo de objeto preciso al que pertenece:

var o = new Object();
o.toString(); // [object Object]

Sin embargo, vemos que este operador también resulta frágil cuando lo aplicamos sobre objetos comunes:

['foo', 'bar', 1].toString(); // "foo, bar, 1"
"Hello World".toString(); // "Hello World"
/a-z/.toString(); // "/a-z/"

Esto es así porque los objetos personalizados sobreescriben en su mayoría el método Object.prototype.toString con los suyos propios. Una forma de solucionar esto es invocar este método directamente desde el objeto Object y utilizar la función call para inyectarle el argumento deseado:

Object.prototype.toString.call(['foo', 'bar', 1]); // [object Array]
Object.prototype.toString.call("Hello World"); // [object String]
Object.prototype.toString.call(/a-z/); // [object RegExp]

De este modo, evitamos sobreescribir nada y el resultado obtenido es el esperado: tenemos el tipo de dato correcto.

Creando una función para determinar el tipo de datos

Extraída directamente del artículo de Angus Croll, podemos utilizar la siguiente función para obtener el tipo correcto de datos de una variable u objeto:

var toType = function(obj) {
  return ({}).toString.call(obj).match(/\s([a-z|A-Z]+)/)[1].toLowerCase()
}

Examinémosla por partes:

  • ({}).toString es una abreviación (un shortcut) de Object.prototype.toString ya que, en todo objeto nuevo, el método toString se refiere a la definción dada por Object.prototype como vimos más arriba.
  • call lo utilizamos aquí para que el método anterior se efectúe sobre el argumento que indiquemos, en este caso, otro objeto.
  • match: utilizamos una expresión regular para extraer el tipo de datos sin la cadena inicial ‘object’ y los corchetes. Empleamos una expresión regular en lugar de un slice u otro método debido al mejor rendimiento que ésta ofrece sobre el resto.
  • toLowerCase: pasamos la [[Class]] a minúsculas para diferenciar el tipo de lo que sería la referencia a la instancia de un objeto y que, por lo general, se escribe en mayúsculas.

Veamos cómo se comporta la función:

toType({a: 4}); //"object"
toType([1, 2, 3]); //"array"
(function() {console.log(toType(arguments))})(); //arguments
toType(new ReferenceError); //"error"
toType(new Date); //"date"
toType(/a-z/); //"regexp"
toType(Math); //"math"
toType(JSON); //"json"
toType(new Number(4)); //"number"
toType(new String("abc")); //"string"
toType(new Boolean(true)); //"boolean"

Y comparémosla con lo que obtendríamos con typeof:

typeof {a: 4}; //"object"
typeof [1, 2, 3]; //"object"
(function() {console.log(typeof arguments)})(); //object
typeof new ReferenceError; //"object"
typeof new Date; //"object"
typeof /a-z/; //"object"
typeof Math; //"object"
typeof JSON; //"object"
typeof new Number(4); //"object"
typeof new String("abc"); //"object"
typeof new Boolean(true); //"object"

La diferencia en la mayoría de los casos es importante: de la imprecisión de typeof pasamos a obtener tipos concretos.

Comparación con instanceof

El operador instanceof chequea la cadena prototípica del primer operando buscando la presencia de la propiedad prototípica del segundo, el cual se espera que corresponda con un constructor:

new Date instanceof Date; //true
[1,2,3] instanceof Array; //true

El problema de este operador es que algunos objetos en Javascript no tienen asociado un constructor por lo que no pueden ser evaluados correctamente por instanceof:

Math instanceof Math //TypeError

También existe el problema de aquellos entornos con múltiples frames donde la presencia de varios contextos globales (uno por cada frame) impiden garantizar que un objeto dado sea una instancia de un determinado constructor:

var iFrame = document.createElement('IFRAME');
document.body.appendChild(iFrame);
 
var IFrameArray = window.frames[1].Array;
var array = new IFrameArray();
 
array instanceof Array; //false
array instanceof IFrameArray; //true;

Limitaciones

La función toType no puede prevenirnos de errores frente a tipos de datos desconocidos:

Object.toType(fff); //ReferenceError

Concretamente, es la llamada a toType la que lanza el error, no la función en si misma. La única forma de prevenir esto sería utilizando la comparación implícita que nos permite el sistema de circuito corto en Javascript:

window.fff && Object.toType(fff);

Si la primera condición se cumple, se continúa con la siguiente; en caso contrario, se corta el circuito y se evita el error.

Conclusión

En Javacript, no podemos determinar con precisión el tipo de datos de una variable utilizando los métodos nativos que proporciona el propio lenguaje. Por lo general, cuando necesitamos identificar tipos, solemos recurrir a técnicas de aproximanción como el Duck Typing. Sin embargo, en aplicaciones donde la integridad es crítica, esta técnica no permite ese rigor exigido.

Tanto typeof como instanceof no ofrecen un control riguroso, fiable y unívoco sobre los tipos que podríamos necesitar durante un desarrollo. Para tratar de solucionar esta carencia, usamos algunas de las peculiaridades del lenguaje como el valor [[Class]], para conseguir un resultado mucho más preciso.

Más información

Este artículo es una traducción expandida del original de Angus Croll: Fixing the JavaScript typeof operator.

Más:

{5} Comentarios.

  1. yeikos

    Ya se quedó anticuada la ya vieja función typeOf de Douglas Crockford’s.

    Una pequeña anotación: /\s([a-z|A-Z]+)/ –> /\s([a-z]+)/i

    var toType = function toType(obj) {
    return ({}).toString.call(obj).match(/\s([a-z]+)/i)[1].toLowerCase()
    }

  2. yeikos

    Error tipográfico renombrando la función :/

  3. Julio Treviño

    Hola me he dado cuenta de una cosa, aunque el match está para eso haciendo unas pruebas en jsperf, concretamente aquí:

    http://jsperf.com/comparaci-n-entre-match-y-replace

    Me he dado cuenta de que salvo en chrome que el match lo supera por una amplia diferencia, los demás o replace supera a match o se quedan muy cerca (caso de opera), por lo que propongo esta solución:

    var toType = function(obj) {
    return ({}).toString.call(obj).replace(/\s([a-z|A-Z]+)/, «$1»).toLowerCase();
    }

    Además tengo una pregunta: ¿Para qué necesitamos la barra | en el bloque [a-z|A-Z] si ningún objeto contiene esa barra en su nombre? ¿o si?

    Un saludo

  4. Julio Treviño

    Perdon me acabo de dar cuenta de que el código que he puesto esta mal, pero este si funciona:

    var toType = function(obj) {
    return ({}).toString.call(obj).replace(/^\[.+?\s(\w+)\]$/,»$1″).toLowerCase();
    }

  5. Jose Alexander

    muchas cosas pasan por que es mejor que sean de ese modo, por que asi es como te enseña la vida

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *