En este artículo, explicaré qué tipos de valores tiene JavaScript y como se puede categorizarlos. Eso te ayudará a comprender mejor cómo funciona el lenguaje. Además te ayudará con tareas de programación avanzadas, como crear una librería, donde a veces tienes que tratar con todo tipo de valores siendo recibidos. Con el conocimiento obtenido aquí, serás capaz de evitar errores provocados por diferencias sutiles entre los valores.
Te mostraré cuatro formas en que se pueden categorizar los valores: a través de la propiedad oculta [[Class]], a través del operador typeof, a través del operador instanceof, y a través de la función Array.isArray(). También te explicare los objetos prototipo de constructores incorporados, los cuales producen resultados inesperados de categorización.
Revisando lo básico
Antes de comenzar con el tema, necesitamos revisar algunos conocimientos necesarios.
Primitivos contra objetos
Todos los valores en JavaScript o son primitivos o son objetos.
Primitivos: los siguientes valores son primitivos.
- undefined
- null
- Boolean
- Number
- String
Los primitivos son inmutables; no se les puede agregar propiedades:
1 2 3 4 5 |
var str = "abc"; str.foo = 123; // intenta agregar la propiedad "foo"<br> 123 str.foo // sin cambio undefined |
Además, los primitivos son comparados por valor, lo cual significa que son considerados igual si tienen el mismo contenido:
1 2 |
> "abc" === "abc" true |
Objetos: todos los valores no-primitivos son objetos. Los objetos son mutables.
1 2 3 4 5 |
var obj = {}; obj.foo = 123; // intenta agregar la propiedad "foo" 123 obj.foo // la propiedad "foo" ha sido agregada 123 |
Los objetos son comparados por referencia. Cada objeto tiene su propia identidad y, debido a esto dos objetos son considerados iguales únicamente si son, de hecho, el mismo objeto:
1 2 3 4 5 |
{} === {} false var obj = {}; obj === obj true |
Envolviendo tipos de objetos
Los tipos primitivos boolean, number y string tienen el contenedor de tipo de objeto correspondiente: Boolean, Number y String. Las instancias de estos tipos son objetos y son diferentes de los primitivos a los que contienen.
1 2 3 4 5 6 |
typeof new String("abc") > 'object' > typeof "abc"<br> 'string' > new String("abc") === "abc" false |
Los contenedores de tipos de objetos son raramente utilizados de manera directa, pero sus objetos prototipo definen los métodos de los primitivos. Por ejemplo, String.prototype es el objeto prototipo del contenedor para el tipo String. Todos sus métodos también están disponibles para string. Tomemos el método contenedor String.prototype.indexOf. Los string primitivos tienen el mismo método, no un método diferente con el mismo nombre, literalmente es el mismo método:
1 2 |
> String.prototype.indexOf === "".indexOf true |
Propiedades internas
Las propiedades internas son aquellas a las cuales no se puede acceder directamente desde JavaScript, pero si afectan como trabaja. Los nombres de las propiedades inician con una letra mayúscula y están escritas en dobles corchetes. Por ejemplo, [[Extensible]] es una propiedad interna que mantiene un aviso booleano que determina si una propiedad puede o no ser agregada a un objeto. Su valor solamente puede ser manipulado de manera indirecta. Object.isExtensible() revisa su valor, mientras que Object.preventExtentions() establece su valor a false. Una vez que su valor es false, no hay forma de cambiarlo a true.
Terminología: prototipos contra objetos prototipo
En JavaScript, el término prototipo esta por desgracia un poco sobrecargado.
1. Por otra parte, está el prototipo de relación entre objetos. Cada objeto tiene una propiedad oculta [[Prototype]] que bien, o apunta a su prototipo o es null. El prototipo es una continuación del objeto. Varios objetos pueden tener el mismo prototipo.
2. Además, si, por ejemplo, se implementa un tipo para un constructor Foo, ese constructor tendrá entonces una propiedad Foo.prototype que mantendrá al tipo objeto prototipo.
Para hacer la diferencia clara, los desarrolladores los llaman (1) “prototipos” y (2) “objetos prototipo”. Hay tres métodos que ayudan al tratar con prototipos:
- Object.getPrototypeOf(obj) retorna el prototipo del objeto:
1 2 |
> Object.getPrototypeOf({}) === Object.prototype true |
- Object.create(proto) crea un objeto vacío cuyo prototipo es proto:
1 2 |
> Object.create(Object.prototype) {} |
Object.create() puede hacer más, pero está más allá del objetivo de este artículo.
- proto.isPrototypeOf(obj) retorna true si proto es un prototipo de obj (o un prototipo de un prototipo, y así):
1 2 |
> Object.prototype.isPrototypeOf({}) true |
La propiedad “constructor”
Dado un constructor de la función Foo, el objeto prototipo Foo.prototype tiene una propiedad Foo.prototype.constructor que se remite a Foo. Esa propiedad se establece automáticamente para cada función.
1 2 3 4 5 |
> function Foo() { } > Foo.prototype.constructor === Foo true > RegExp.prototype.constructor === RegExp true |
Todas las instancias de un constructor heredan esa propiedad del objeto prototipo. Por lo tanto, la puedes usar para determinar qué constructor creó una instancia:
1 2 3 4 |
> new Foo().constructor [Function: Foo] > /abc/.constructor [Function: RegExp] |
Categorizando valores
Hay cuatro formas de categorizar valores:
- [[Class]] es una propiedad interna con una cadena que clasifica un objeto.
- typeof es un operador que categoriza primitivos y ayuda a distinguirlos de los objetos.
- instanceof es un operador que categoriza objetos.
- Array.isArray() es una función que determina si un valor es un arreglo.
[[Class]]
Es una propiedad interna cuyo valor es uno de las siguientes cadenas:
«Arguments», «Array», «Boolean», «Date», «Error», «Function», «JSON», «Math», «Number», «Object», «RegExp», «String»
La única forma de acceder a esta desde JavaScript es a través del método predeterminado toString(), el cual puede ser invocado generalmente de esta forma:
Object.prototype.toString.call(value)
Tal invocación devuelve:
- “[object Undefined]” si el valor es indefinido.
- “[object Null]” si el valor es nulo.
- “[object “ + value.[[Class]] + “]” si el valor es un objeto.
- “[object “ + value.[[Class]] + “]” si el valor es un primitivo (es convertido a un objeto y manejado como en la regla anterior).
Ejemplos:
1 2 3 4 5 6 |
> Object.prototype.toString.call(undefined) '[object Undefined]' > Object.prototype.toString.call(Math) '[object Math]' > Object.prototype.toString.call({}) '[object Object]' |
Por lo tanto, la siguiente función puede ser utilizada para recuperar el [[Class]] de un valor x:
1 2 3 4 |
<p>function getClass(x) { var str = Object.prototype.toString.call(x); return /^\[object (.*)\]$/.exec(str)[1]; } |
Esta función en acción:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
getClass(null) 'Null' > getClass({}) 'Object' > getClass([]) 'Array' > getClass(JSON) 'JSON' > (function () { return getClass(arguments) }()) 'Arguments' > function Foo() {} > getClass(new Foo()) 'Object' |
typeof
Categoriza primitivos y permite distinguir entre primitivos y objetos usando la siguiente sintaxis:
1 |
typeof valor |
Devuelve una de las siguientes cadenas, dependiendo del operando:
Operando | Resultado |
undefined | «undefined» |
null | «object» |
Boolean value | «boolean» |
Number value | «number» |
String value | «string» |
Function | «function» |
Todos los demás valores | «object» |
El operador typeof retorna “object” para null, esto debido a un bug en JavaScript. Desafortunadamente, este error no tiene solución, debido a que “rompería” el código existente. Observa que mientras que una función también es un objeto, typeof hace una distinción. Los arreglos, por otra parte, son considerados objetos. Estas peculiaridades hacen complicado el revisar si un valor es un objeto:
1 2 3 4 |
function isObject(x) { return x !== null && (typeof x === 'object' || typeof x === 'function'); } |
instanceof
Revisa si un valor es una instancia de algún tipo, con esta sintaxis:
valor instanceof Tipo
El operador revisa Type.prototype para ver si se encuentra en la cadena prototipo de valor. Eso es, si estuvieras implementando instanceof, debería lucir algo como esto (menos algunos errores, como el tipo null):
1 2 3 |
<p>function myInstanceof(value, Type) {<br> return Type.prototype.isPrototypeOf(value);<br> }</p> |
instanceof siempre devuelve false para los valores primitivos:
1 2 3 4 |
> "" instanceof String false > "" instanceof Object false |
Array.isArray()
Existe debido a un problema en particular en los navegadores: cada frame tiene su propio ambiente global. Por ejemplo: dado un frame A y un frame B (donde cualquiera de ellos puede ser el documento), el código en A puede pasar un valor al código de B. El código en B no puede usar instanceof Array para revisar si el valor es un arreglo, debido a que el Array en B es diferente del que hay en A. Por ejemplo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
<html> <head> <script> // test() es llamado desde el iframe function test(arr) { var iframeWin = frames[0]; console.log(arr instanceof Array); // false console.log(arr instanceof iframeWin.Array); // true console.log(Array.isArray(arr)); // true } </script> </head> <body> <iframe></iframe> <script> // Llena el iframe var iframeWin = frames[0]; iframeWin.document.write( '<script>window.parent.test([])</'+'script>'); </script> </body> </html> |
Por lo tanto, ECMAScript 5 introdujo Array.isArray(), el cual usa [[Class]] para determinar si un valor es un arreglo. No obstante, el problema descrito arriba en el ejemplo de los frames, existe para todos los tipos cuando se utiliza en conjunto con instanceof.
Objetos prototipo incorporados
Los objetos prototipo del tipo incorporado son valores extraños: se comportan como instancias de los tipos, pero cuando son examinados a través de instanceof, no son instancias. Algunos resultados de categorización para objetos prototipo también son inesperados. Para intentar comprender lo que sucede, puedes profundizar tu comprendimiento de categorización.
1 |
Object.prototype |
Es un objeto vacío. Es impreso como uno y no tiene propiedades propias:
1 2 3 4 |
> Object.prototype {} > Object.keys(Object.prototype) [] |
Unexpected. Object.prototype es un objeto, pero no es una instancia de Object. Por un lado, ambos typeof y [[Class]] lo reconocen como un objeto:
1 2 3 4 |
> getClass(Object.prototype) 'Object' > typeof Object.prototype 'object' |
Por otra parte, instanceof no lo considera una instancia de Object.
1 2 |
> Object.prototype instanceof Object false |
Para que el resultado de arriba sea true, Object.prototype tendría que estar en su propia cadena prototipo, provocando un ciclo que empiece y termine con Object.prototype.La cadena prototipo ya no sería linear, lo cual no es algo que queramos para una estructura de datos que tiene que ser fácil de atravesar, Por lo tanto, Object.prototype no tiene un prototipo. Es el único objeto incorporado que no tiene uno.
1 2 |
> Object.getPrototypeOf(Object.prototype) null |
Este tipo de paradoja mantiene true para todos los objetos incorporados: ya que son considerados instancias de sus tipos por todos los mecanismos, excepto instanceof.
Expected. [[Class]], typeof e instanceof coinciden en la mayoría de los demás objetos:
1 2 3 4 5 6 |
> getClass({}) 'Object' > typeof {} 'object' > {} instanceof Object true |
Function.prototype
En sí misma es una función. Acepta cualquier argumento y retorna indefinido:
1 2 |
> Function.prototype("a", "b", 1, 2) undefined |
Unexpected. Function.prototype es una función, pero no una instancia de Function: Por un lado, typeof, el cual revisa si un método [[Call]] interno está presente, dice que Function.prototype es una función:
1 2 |
> typeof Function.prototype 'function' |
La propiedad [[Class]] dice lo mismo:
1 2 |
> getClass(Function.prototype) 'Function' |
Por otra parte, instanceof dice que Function.prototype no es una instancia de Function:
1 2 |
> Function.prototype instanceof Function false |
Esto debido a que no tiene a Function.prototype en su cadena prototipo. En su lugar, su prototipo es Object.prototype:
1 2 |
> Object.getPrototypeOf(Function.prototype) === Object.prototype true |
Expected. Con todas las demás funciones, no hay sorpresas:
1 2 3 4 5 6 |
> typeof function () {} 'function' > getClass(function () {}) 'Function' > function () {} instanceof Function true |
Function siempre es considerado una función en cada caso:
1 2 3 4 5 6 |
> typeof Function 'function' > getClass(Function) 'Function' > Function instanceof Function true |
Array.prototype
Es un arreglo vacío. Es visualizado de forma que tiene una longitud de 0.
1 2 3 4 |
> Array.prototype [] > Array.prototype.length 0 |
[[Class]] también lo considera un arreglo:
1 2 |
> getClass(Array.prototype) 'Array' |
También lo hace Array.isArray(), el cual está basado en [[Class]]:
1 2 |
> Array.isArray(Array.prototype) true |
Naturalmente, instanceof no lo hace:
1 2 |
> Array.prototype instanceof Array false |
Así que para no ser redundante, no mencionare que los objetos prototipo no son instancias de su tipo.
RegExp.prototype
Es una expresión regular que iguala todo:
1 2 3 4 |
> RegExp.prototype.test("abc") true > RegExp.prototype.test("") true |
RegExp.prototype también es aceptada para String.prototype.match, el cual revisa si su argumento es una expresión regular a través de [[Class]]. Que revisa si es positivo para ambos, expresiones regulares y objetos prototipo.
1 2 3 4 |
> getClass(/abc/) 'RegExp' > getClass(RegExp.prototype) 'RegExp' |
Excursion: la expresión regular vacía. RegExp.prototype es equivalente para la “expresión regular vacía”. Esa expresión es creada en cualquiera de las dos formas:
1 2 |
<p>new RegExp("") // constructor /(?:)/ // literal |
No deberías usar el constructor de RegExp si estas creando una expresión regular de manera dinámica. Expresar una expresión regular vacía literalmente es complicado por el hecho de que no puedes utilizar / /, lo cual comenzaría un comentario. El grupo vacío no capturado (?:) se comporta igual que la expresión regular vacía: iguala todo y no crea capturas en una igualdad:
1 2 3 4 |
> new RegExp("").exec("abc") [ '', index: 0, input: 'abc' ] > /(?:)/.exec("abc") [ '', index: 0, input: 'abc' ] |
Un grupo vacío que no mantiene una igualdad completa en el índice 0, pero también captura (primero) ese grupo en índice 1:
1 2 3 4 5 |
> /()/.exec("abc") [ '', // indice 0 '', // indice 1 index: 0, input: 'abc' ] |
Curiosamente, ambos una expresión regular vacía creada a través del constructor y RegExp.prototype son visualizadas como el vacío literal:
1 2 3 4 5 |
> new RegExp("") /(?:)/ > RegExp.prototype /(?:)/ Date.prototype |
También es una fecha:
1 2 3 4 |
> getClass(new Date()) 'Date' > getClass(Date.prototype) 'Date' |
Las fechas contienen números. Citando a la especificación de ECMAScript 5.1:
Un objeto Date contiene un número que indica un instante de tiempo (en milisegundos) en particular. Como un número es llamado un valor de tiempo. Un valor de tiempo también puede ser un NaN, que indique que el objeto Date no representa un instante de tiempo específico.
En ECMAScript el tiempo se mide en milisegundos desde 01 Enero, 1970 UTC.
Hay dos maneras de acceder al valor de tiempo al llamar valueOf, o al convertir una fecha a un número:
1 2 3 4 5 |
> var d = new Date(); // ahora > d.valueOf() 1347035199049 > Number(d) 1347035199049 |
El valor de tiempo de Date.prototype es NaN:
1 2 3 4 |
> Date.prototype.valueOf() NaN > Number(Date.prototype) NaN |
Date.prototype es visualizado como una fecha inválida, al igual que las fechas que han sido creadas a través de NaN:
1 2 3 4 |
> Date.prototype Invalid Date > new Date(NaN) Invalid Date</p> |
Number.prototype
Es muy parecido a new Number (0):
1 2 |
> Number.prototype.valueOf() 0 |
La conversión a número retorna el valor primitivo envuelto:
1 2 |
> +Number.prototype 0 |
Comparado a:
1 2 |
> +new Number(0) 0 |
String.prototype
Es muy parecida a new String(“”):
1 2 |
> String.prototype.valueOf() '' |
La conversión a cadena retorna el valor primitivo envuelto:
1 2 |
> "" + String.prototype '' |
Comparado a:
1 2 |
> "" + new String("") '' |
Boolean.prototype
Es muy parecido a new Boolean(false):
1 2 |
> Boolean.prototype.valueOf() false |
Los objetos booleanos pueden ser convertidos a valores booleanos primitivos, pero el resultado de esa conversión siempre es true, debido a que convertir cualquier objeto a boolean siempre será true:
1 2 3 4 5 6 |
> !!Boolean.prototype true > !!new Boolean(false) true > !!new Boolean(true) true |
Eso es diferente dado que los objetos son convertidos a números o cadenas. Si un objeto contiene estos primitivos, el resultado de una conversión es el primitivo envuelto.
Recomendaciones
Estas son recomendaciones para categorizar mejor los valores en JavaScript.
Tratar los objetos prototipo como miembros principales de sus tipos
Un objeto primitivo siempre será un miembro principal de un tipo? No, eso solo es válido para los tipos incorporados. En general, el comportamiento de los objetos prototipos es meramente una curiosidad. Es mejor pensar en ellos como análogos a clases: contienen propiedades compartidas por todas las instancias (usualmente métodos).
Que mecanismo de categorización usar
Cuando se decide qué mecanismo de categorización usar, se tiene que distinguir entre código normal y código que podría trabajar con valores de otros frames.
Código normal. Para código normal, usa typeof e instanceof (olvídate de [[Class]] y Array.isArray()). Se debe ser consciente de las peculiaridades de typeof: null es considerado un “object” y tiene dos categorías no primitivas, “object” y “function”. Por ejemplo, una función que determine si un valor es un objeto podría implementarse de esta forma:
1 2 3 4 |
function isObject(v) { return (typeof v === "object" && v !== null) || typeof v === "function"; } |
Implementar este método luciria asi:
1 2 3 4 5 6 7 8 |
> isObject({}) true > isObject([]) true > isObject("") false > isObject(undefined) false |
Código que funciona con valores de otros frames. Si se espera recibir valores de otros frames, instanceof no es confiable. Se debe considerar [[Class]] y Array.isArray(). Una alternativa es trabajar con el nombre del constructor del objeto, pero esa es una solución frágil ya que no todos los objetos registran su constructor, no todos los constructores tienen nombre, y hay riesgo de “choques” de nombres. La siguiente función muestra cómo recibir el nombre del constructor de un objeto.
1 2 3 4 5 6 7 |
function getConstructorName(obj) { if (obj.constructor && obj.constructor.name) { return obj.constructor.name; } else { return ""; } } |
Otra cosa que vale la pena destacar es que la propiedad nombre de funciones (como obj.constructor) no es estándar y, por ejemplo, no son soportados por Internet Explorer. Intenta:
1 2 3 4 5 6 7 8 9 |
> getConstructorName({}) 'Object' > getConstructorName([]) 'Array' > getConstructorName(/abc/) 'RegExp' > function Foo() {} > getConstructorName(new Foo()) 'Foo' |
Si se aplica getConstructorName() a un valor primitivo, se obtendrá el nombre asociado al tipo de contenedor:
1 2 |
> getConstructorName("") 'String' |
Esto es debido a que el valor primitivo obtiene la propiedad constructor del tipo de contenedor:
1 2 |
> "".constructor === String.prototype.constructor true |
A donde ir desde aquí
En este artículo, has aprendido cómo categorizar valores en JavaScript. Desafortunadamente se necesita conocimiento detallado para realizar esta tarea apropiadamente, como las primeras dos categorizaciones: typeof e insteadof (cada uno con sus particularidades). El artículo incluye recomendaciones para trabajar con estas.
Como siguiente paso, puedes aprender más acerca de herencia en JavaScript. Los siguientes blogs te ayudarán a comenzar:
Prototipos como clases -una introducción a la herencia en JavaScript
Herencia en JavaScript por ejemplo
Patrones ejemplares en JavaScript
Datos privados para objetos en JavaScript
La versión original de este artículo está publicada en Adobe Devnet bajo licencia Creative Commons, fue traducido y adaptado en nuestro blog por Jesús Macedo.