YOU DON’T KNOW JS – 04: Types & Grammar en Castellano

You Don’t Know JS: Types & Grammar


Prólogo


Se dijo una vez, “JavaScript es el único lenguaje que los desarrolladores no aprenden a usar antes de usarlo.”

Me río cada vez que escucho esa cita porque era cierta para mí y sospecho que lo era para muchos otros desarrolladores. JavaScript, y tal vez incluso CSS y HTML, no eran un lenguaje básico de informática que se enseñaba en la universidad en los primeros tiempos de Internet, por lo que el desarrollo personal se basaba en gran medida en la capacidad de búsqueda del desarrollador en ciernes y en la capacidad de “ver el código fuente” para unir estos lenguajes web básicos.

Todavía recuerdo mi primer proyecto de sitio web de la escuela secundaria. La tarea era crear cualquier tipo de tienda web, y siendo yo un fan de James Bond, decidí crear una tienda Goldeneye. Tenía de todo: la canción del midi Goldeneye sonando en el fondo, una retícula de JavaScript siguiendo al ratón alrededor de la pantalla, y un sonido de disparo que se escuchaba con cada clic. Q habría estado orgulloso de esta obra maestra de un sitio web.

Cuento esa historia porque hice lo que muchos desarrolladores están haciendo hoy en día: Copié y pegué trozos de código JavaScript en mi proyecto sin tener ni idea de lo que estaba pasando. El uso generalizado de kits de herramientas JavaScript como jQuery ha perpetuado, a su manera, este patrón de no aprendizaje del núcleo de JavaScript.

No estoy menospreciando el uso del kit de herramientas de JavaScript; después de todo, ¡soy miembro del equipo de JavaScript de MooTools! Pero la razón por la que los kits de herramientas JavaScript son tan poderosos como lo son es porque sus desarrolladores conocen los fundamentos, y sus “gotchas”, y las aplican magníficamente. Por muy útiles que sean estos kits de herramientas, sigue siendo increíblemente importante conocer los conceptos básicos del lenguaje, y con libros como la serie You Don’t Know JS de Kyle Simpson, no hay excusa para no aprenderlos.

Types and Grammar, la tercera entrega de la serie, es un excelente vistazo a los fundamentos básicos de JavaScript que los kits de herramientas de copiar y pegar y de JavaScript no le enseñan y nunca podrían enseñarle. La coerción y sus trampas, los nativos como constructores y toda la gama de conceptos básicos de JavaScript se explican a fondo con ejemplos de código enfocados. Al igual que los otros libros de esta serie, Kyle va directo al grano: sin pelusas ni forja de palabras, exactamente el tipo de libro tecnológico que me encanta.

Disfruta de Tipos y Gramática y no dejes que se aleje demasiado de tu escritorio!

David Walsh
http://davidwalsh.name, @davidwalshblog
Desarrollador Web Senior, Mozilla

Prefacio


Estoy seguro de que te has dado cuenta, pero “JS” en la serie de libros no es una abreviatura de las palabras que se usan para maldecir sobre JavaScript, aunque maldecir las peculiaridades del lenguaje es algo con lo que probablemente todos podemos identificarnos.

Desde los primeros días de la web, JavaScript ha sido una tecnología fundamental que impulsa la experiencia interactiva en torno al contenido que consumimos. Mientras que las pistas de ratón parpadeantes y las molestas ventanas emergentes pueden ser el punto de partida de JavaScript, casi dos décadas más tarde, la tecnología y la capacidad de JavaScript ha crecido muchos órdenes de magnitud, y pocos dudan de su importancia en el corazón de la plataforma de software más ampliamente disponible del mundo: la web.

Pero como lengua, siempre ha sido objeto de muchas críticas, debido en parte a su patrimonio, pero aún más a su filosofía de diseño. Incluso el nombre evoca, como Brendan Eich lo dijo una vez, el estatus de “hermano menor tonto” junto a su hermano mayor más maduro “Java”. Pero el nombre es un mero accidente de la política y el marketing. Los dos idiomas son muy diferentes en muchos aspectos importantes. “JavaScript” está tan relacionado con “Java” como “Carnival” con “Car”.

Debido a que JavaScript toma prestados conceptos y modismos de sintaxis de varios lenguajes, incluidas las orgullosas raíces procesales de estilo C, así como raíces funcionales sutiles y menos obvias de estilo Esquema/Lisp, es extremadamente accesible a una amplia audiencia de desarrolladores, incluso a aquellos con poca o ninguna experiencia en programación. El “Hello World” de JavaScript es tan simple que el lenguaje es atractivo y fácil de usar durante la exposición temprana.

Mientras que JavaScript es quizás uno de los lenguajes más fáciles de usar, sus excentricidades hacen que el dominio sólido del lenguaje sea mucho menos común que en muchos otros lenguajes. Donde se necesita un conocimiento bastante profundo de un lenguaje como C o C++ para escribir un programa a gran escala, la producción a gran escala de JavaScript puede, y a menudo lo hace, apenas rasguñar la superficie de lo que el lenguaje puede hacer.

Los conceptos sofisticados que están profundamente arraigados en el lenguaje tienden, en cambio, a salir a la superficie de formas aparentemente simplistas, como pasar funciones como llamadas de retorno, lo que anima al desarrollador JavaScript a usar el lenguaje tal cual está y no preocuparse demasiado por lo que está ocurriendo bajo el capó.

Es simultáneamente un lenguaje simple y fácil de usar que tiene un amplio atractivo, y una compleja y matizada colección de mecánicas de lenguaje que sin un estudio cuidadoso eludirá la verdadera comprensión incluso para los desarrolladores más experimentados de JavaScript.

Ahí radica la paradoja de JavaScript, el talón de Aquiles del lenguaje, el reto al que nos enfrentamos actualmente. Debido a que JavaScript puede ser utilizado sin entender, la comprensión del lenguaje a menudo nunca es alcanzada.

Misión


Si en cada punto que encuentras una sorpresa o frustración en JavaScript, tu respuesta es añadirlo a la lista negra, como algunos están acostumbrados a hacer, pronto serás relegado a una cáscara hueca de la riqueza de JavaScript.

Mientras que a este subconjunto se le ha dado el famoso sobrenombre de “Las Partes Buenas”, le imploro, querido lector, que en su lugar lo considere “Las Partes Fáciles”, “Las Partes Seguras”, o incluso “Las Partes Incompletas”.

Esta serie de libros de You Don’t Know JavaScript ofrece un desafío contrario: aprender y entender profundamente todo el JavaScript, incluso y especialmente “Las partes difíciles”.

Aquí nos referimos a la tendencia de los desarrolladores de JS a aprender “lo justo” para sobrevivir, sin forzarse nunca a aprender exactamente cómo y por qué el lenguaje se comporta de la manera en que lo hace. Además, evitamos el consejo común de retroceder cuando el camino se torna difícil.

Yo no estoy contento, ni usted debería estarlo, con dejar de hacer algo una vez que simplemente funciona, y sin saber realmente por qué. Te reto suavemente a viajar por ese “camino menos transitado” y abrazar todo lo que JavaScript es y puede hacer. Con ese conocimiento, ninguna técnica, ningún marco, ningún acrónimo popular de la semana, estará más allá de su comprensión.

Cada uno de estos libros toma partes específicas del núcleo del lenguaje que comúnmente son malentendidas o subentendidas, y se sumergen muy profunda y exhaustivamente en ellas. Deberías salir de la lectura con una firme confianza en tu comprensión, no sólo de lo teórico, sino de lo práctico “lo que necesitas saber”.

El JavaScript que usted conoce en este momento es probablemente partes que le han sido transmitidas por otras personas que han sido quemadas por una comprensión incompleta. Ese JavaScript no es más que una sombra del verdadero lenguaje. Aún no conoces JavaScript, pero si indagas en esta serie, lo harás. Sigan leyendo, amigos míos. JavaScript le espera.

Resumen


JavaScript es impresionante. Es fácil de aprender parcialmente, y mucho más difícil de aprender completamente (o incluso lo suficiente). Cuando los desarrolladores encuentran confusión, generalmente culpan al lenguaje en vez de a su falta de entendimiento. El objetivo de estos libros es arreglar eso, inspirando un fuerte aprecio por el lenguaje que usted puede, y debe, conocer profundamente.

Nota: Muchos de los ejemplos de este libro asumen entornos de motor JavaScript modernos (y de alcance futuro), como ES6. Algún código puede no funcionar como se describe si se ejecuta en motores anteriores (pre-ES6).

Capítulo 1: Tipos


La mayoría de los desarrolladores dirían que un lenguaje dinámico (como JS) no tiene tipos. Veamos qué dice la especificación ES5.1 (http://www.ecma-international.org/ecma-262/5.1/) sobre el tema:

Los algoritmos dentro de esta especificación manipulan valores cada uno de los cuales tiene un tipo asociado. Los tipos de valores posibles son exactamente los que se definen en esta cláusula. Los tipos se subclasifican a su vez en tipos de lenguaje ECMAScript y tipos de especificaciones.
Un tipo en el lenguaje ECMAScript corresponde a valores que son manipulados directamente por un programador ECMAScript usando el lenguaje ECMAScript. Los tipos en el lenguaje ECMAScript son Undefined, Null, Boolean, String, Number y Object.

Ahora, si eres un fan de los lenguajes fuertemente tipados (estáticamente tipados), puedes oponerte a este uso de la palabra “tipo”. En esos idiomas, “tipo” significa mucho más que aquí en JS.

Algunas personas dicen que JS no debería decir que tiene “tipos”, sino que debería llamárseles “etiquetas” o quizás “subtipos”.

¡Bah! Vamos a usar esta definición aproximada (la misma que parece guiar la redacción de la especificación): un tipo es un conjunto intrínseco e incorporado de características que identifica de manera única el comportamiento de un valor en particular y lo distingue de otros valores, tanto para el motor como para el desarrollador.

En otras palabras, si tanto el motor como el desarrollador tratan el valor 42 (el número) de forma diferente que tratan el valor “42” (la cadena), entonces esos dos valores tienen tipos diferentes — número y cadena, respectivamente. Cuando usas 42, tienes la intención de hacer algo numérico, como matemáticas. Pero cuando usas “42”, tienes la intención de hacer algo textualizado, como dar salida a la página, etc. Estos dos valores tienen tipos diferentes.

Esa no es una definición perfecta. Pero es suficiente para esta discusión. Y es consistente con la forma en que JS se describe a sí mismo.

Un tipo por cualquier otro nombre…


Más allá de los desacuerdos de definición académica, ¿por qué importa si JavaScript tiene tipos o no?

Tener un entendimiento apropiado de cada tipo y su comportamiento intrínseco es absolutamente esencial para entender cómo convertir los valores a diferentes tipos de manera apropiada y precisa (ver Coerción, Capítulo 4). Casi todos los programas de JS que se hayan escrito necesitarán manejar la coerción de valores de alguna forma, por lo que es importante que lo haga de manera responsable y con confianza.

Si tienes el valor numérico 42, pero quieres tratarlo como una cadena, como sacar el “2” como un carácter en la posición 1, obviamente primero debes convertir (coaccionar) el valor de número a cadena.

Eso parece bastante simple.

Pero hay muchas maneras diferentes en que puede ocurrir tal coerción. Algunas de estas formas son explícitas, fáciles de razonar y confiables. Pero si no tienes cuidado, la coerción puede ocurrir de maneras muy extrañas y sorprendentes.

La confusión de coacción es quizás una de las frustraciones más profundas para los desarrolladores de JavaScript. A menudo se la ha criticado por ser tan peligrosa que se la considera un defecto en el diseño del lenguaje, se la rechaza y se la evita.

Armados con una comprensión completa de los tipos de JavaScript, nuestro objetivo es ilustrar por qué la mala reputación de la coerción es en gran medida sobrevalorada y algo inmerecida – para cambiar su perspectiva, para ver el poder y la utilidad de la coerción. Pero primero, tenemos que controlar mucho mejor los valores y los tipos.

Tipos incorporados


JavaScript define siete tipos incorporados:

  • null
  • undefined
  • boolean
  • number
  • string
  • object
  • symbol — añadido en ES6!

Nota: Todos estos tipos, excepto el objeto, se denominan “primitivos”.

El operador typeof inspecciona el tipo del valor dado, y siempre devuelve uno de los siete valores de cadena — sorprendentemente, no hay una coincidencia exacta 1-a-1 con los siete tipos incorporados que acabamos de enumerar.

Estos seis tipos listados tienen valores del tipo correspondiente y devuelven un valor de cadena del mismo nombre, como se muestra. El símbolo es un nuevo tipo de datos a partir de ES6, y se tratará en el Capítulo 3.

Como habrás notado, no he excluido nulo de la lista anterior. Es especial – especial en el sentido de que es “erroneo” cuando se combina con el operador typeof:

Hubiera sido bueno (¡y correcto!) si hubiera devuelto “nulo”, pero este error original en JS ha persistido durante casi dos décadas, y probablemente nunca será corregido porque hay demasiado contenido web existente que se basa en su comportamiento “erroneo” y “corregir” el error crearía más “errores” y rompería un montón de software web.

Si desea comprobar un valor nulo utilizando su clase, necesita una condición compuesta:

null es el único valor primitivo que es “falsy” (también conocido como “false-like”; véase el capítulo 4) pero que también devuelve “object” en la comprobación typeof.

Entonces, ¿cuál es el séptimo valor de cadena que typeof puede devolver?

Es fácil pensar que la función sería un tipo incorporado de nivel superior en JS, especialmente dado este comportamiento del operador typeof. Sin embargo, si lees la especificación, verás que en realidad es un “subtipo” de objeto. Específicamente, una función es referida como un “objeto llamable” — un objeto que tiene una propiedad interna [[Call]] que le permite ser invocado.

El hecho de que las funciones sean realmente objetos es muy útil. Lo más importante es que pueden tener propiedades. Por ejemplo:

El objeto de función tiene una propiedad length definida por el número de parámetros formales con los que se declara.

Puesto que declaró la función con dos parámetros formales nombrados (b y c), la “longitud de la función” es 2.

¿Qué hay de las matrices? Son nativos de JS, ¿así que son un tipo especial?

No, sólo objetos. Lo más apropiado es pensar en ellos también como un “subtipo” de objeto (ver Capítulo 3), en este caso con las características adicionales de estar indexados numéricamente (en vez de tener indice nombrados como objetos planos) y mantener una propiedad .length actualizada automáticamente.

Valores como tipos


En JavaScript, las variables no tienen tipos — los valores tienen tipos. Las variables pueden contener cualquier valor, en cualquier momento.

Otra forma de pensar en los tipos JS es que JS no tiene “forrzado de tipos”, en el sentido de que el motor no insiste en que una variable siempre contenga valores del mismo tipo inicial con el que comienza. Una variable puede, en una expresión de asignación, tener un string, y en la siguiente retener un número, y así sucesivamente.

El valor 42 tiene un tipo de número intrínseco y su tipo no se puede modificar. Otro valor, como “42” con el tipo de cadena, puede crearse a partir del valor numérico 42 mediante un proceso llamado coacción (véase el capítulo 4).

Si usas typeof contra una variable, no es preguntar “cuál es el tipo de la variable” como puede parecer, ya que las variables JS no tienen tipos. En vez de eso, es preguntar “¿cuál es el tipo de valor de la variable?”

El tipo de operador siempre devuelve una cadena. Así que:

El primer tipo de 42 devuelve “número”, y el tipo de “número” es “cadena”.

undefined vs “undeclared”

Las variables que actualmente no tienen valor, en realidad tienen el valor indefinido. El tipo de llamada contra tales variables devolverá “indefinido”:

Es tentador para la mayoría de los desarrolladores pensar en la palabra “indefinido” y pensar en ella como sinónimo de “no declarado”. Sin embargo, en JS, estos dos conceptos son muy diferentes.

Una variable “indefinida” es aquella que ha sido declarada en el ámbito accesible, pero que por el momento no tiene ningún otro valor. Por el contrario, una variable “no declarada” es aquella que no ha sido formalmente declarada en el ámbito accesible.

Considere:

Una molesta confusión es el mensaje de error que los navegadores asignan a esta condición. Como puede ver, el mensaje es “b no está definido”, lo cual es por supuesto muy fácil y razonable de confundir con “b es indefinido”. Una vez más, “indefinido” y “no definido” son cosas muy diferentes. Sería bueno que los navegadores dijeran algo como “b no se encuentra” o “b no se declara”, para reducir la confusión.

También hay un comportamiento especial asociado con el tipo de variables no declaradas que refuerza aún más la confusión. Considere:

El operador typeof devuelve “indefinido” incluso para variables “no declaradas” (o “no definidas”). Note que no hubo ningún error cuando ejecutamos typeof b, aunque b es una variable no declarada. Se trata de un resguardo de seguridad especial en el comportamiento de typeof.

Similar a lo anterior, hubiera sido bueno que el typeof usado con una variable no declarada hubiera devuelto “no declarada” en lugar de confundir el valor del resultado con el caso diferente “no definido”.

typeof No Declarado

Sin embargo, este protector de seguridad es una característica útil cuando se trata de JavaScript en el navegador, donde múltiples archivos de script pueden cargar variables en el espacio de nombres global compartido.

Nota: Muchos desarrolladores creen que nunca debería haber variables en el espacio de nombres global, y que todo debería estar contenido en módulos y espacios de nombres privados/separados. Esto es grandioso en teoría pero casi imposible en la práctica; ¡aún así es una buena meta por la que esforzarse! Afortunadamente, ES6 agregó soporte de primera clase para los módulos, lo que finalmente lo hará mucho más práctico.

Como ejemplo simple, imagine tener un “modo de depuración” en su programa que es controlado por una variable global (flag) llamada DEBUG. Querría comprobar si esa variable fue declarada antes de realizar una tarea de depuración, como registrar un mensaje en la consola. Una declaración global var DEBUG = true de nivel superior sólo se incluiría en un archivo “debug.js”, que sólo se carga en el navegador cuando se está en desarrollo/prueba, pero no en producción.

Sin embargo, debe tener cuidado al buscar la variable global DEBUG en el resto de su código de aplicación, para no lanzar un ReferenceError. El guardia de seguridad es nuestro amigo en este caso.

Este tipo de comprobación es útil incluso si no está tratando con variables definidas por el usuario (como DEBUG). Si está realizando una comprobación de funciones para una API incorporada, también le puede resultar útil comprobar sin lanzar un error:

Nota: Si está definiendo un “polyfill” para una característica que no existe aún, probablemente quiera evitar usar varpara hacer la declaración atob. Si declara var atob dentro de la sentencia if, esta declaración se eleva (ver el título Scope & Closures de esta serie) a la parte superior del ámbito de aplicación, incluso si la condición if no pasa (¡porque atob global ya existe!). En algunos navegadores y para algunos tipos especiales de variables globales incorporadas (a menudo llamadas “objetos de host”), esta declaración duplicada puede provocar un error. La omisión de var impide esta declaración de izado.

Otra manera de hacer estas comprobaciones contra variables globales pero sin la característica de protección de seguridad de tipeof es observar que todas las variables globales son también propiedades del objeto global, que en el navegador es básicamente el objeto window. Por lo tanto, las comprobaciones anteriores se podrían haber hecho (con bastante seguridad) como:

A diferencia de hacer referencia a variables no declaradas, no se lanza ReferenceError si intenta acceder a una propiedad de objeto (incluso en el objeto ventana global) que no existe.

Por otro lado, la referencia manual de la variable global con una referencia a window es algo que algunos desarrolladores prefieren evitar, especialmente si su código necesita ejecutarse en múltiples entornos JS (no sólo navegadores, sino node.js del lado del servidor, por ejemplo), donde el objeto global no siempre puede llamarse window.

Técnicamente, esta protección de seguridad en typeof es útil incluso si no está utilizando variables globales, aunque estas circunstancias son menos comunes, y algunos desarrolladores pueden encontrar este enfoque de diseño menos deseable. Imagine una función de utilidad que quiere que otros copien y peguen en sus programas o módulos, en la que quiere comprobar si el programa de inclusión ha definido una determinada variable (para que usted pueda usarla) o no:

doSomethingCool() prueba una variable llamada FeatureXYZ, y si se encuentra, la usa, pero si no, usa la suya propia. Ahora, si alguien incluye esta utilidad en su módulo/programa, comprueba con seguridad si ha definido FeatureXYZ o no:

Aquí, FeatureXYZ no es en absoluto una variable global, pero todavía estamos usando el resguardo de seguridad typeof para hacer que sea seguro de comprobar. Y lo que es más importante, aquí no hay ningún objeto que podamos usar (como hicimos para las variables globales con window.___) para hacer la comprobación, así que typeof es bastante útil.

Otros desarrolladores preferirían un patrón de diseño llamado “inyección de dependencia”, donde en lugar de que doSomethingCool() inspeccionara implícitamente para que FeatureXYZ se definiera fuera/alrededor de él, necesitaría que la dependencia se pasara explícitamente, como:

Hay muchas opciones a la hora de diseñar esta funcionalidad. Ningún patrón aquí es “correcto” o “incorrecto” – hay varias compensaciones para cada enfoque. Pero en general, es bueno que el guardia de seguridad no declarado de typeof nos dé más opciones.

Revisión

JavaScript tiene siete tipos incorporados: null, undefined, boolean, number, string, object, symbol. Pueden identificarse con el operador typeof.

Las variables no tienen tipos, pero los valores en ellas sí. Estos tipos definen el comportamiento intrínseco de los valores.

Muchos desarrolladores asumirán que “undefined” y “undeclared” son más o menos la misma cosa, pero en JavaScript, son bastante diferentes. undefined es un valor que una variable declarada puede contener. “No declarado” significa que una variable nunca ha sido declarada.

Lamentablemente, JavaScript confunde estos dos términos, no sólo en sus mensajes de error (“ReferenceError: a no está definido”) sino también en los valores de retorno de tipeof, que es “indefinido” para ambos casos.

Sin embargo, el resguardo de seguridad (que previene un error) en typeof cuando se usa contra una variable no declarada puede ser útil en ciertos casos.


Capítulo 2: Valores


Matrices, cadenas y números son los bloques de construcción más básicos de cualquier programa, pero JavaScript tiene algunas características únicas con estos tipos que pueden encantarle o confundirlo.

Veamos varios de los tipos de valores incorporados en JS, y exploremos cómo podemos entender mejor y aprovechar correctamente sus comportamientos.

Arrays


En comparación con otros lenguajes con forzado de tipos, las matrices JavaScript son sólo contenedores para cualquier tipo de valor, desde una cadena hasta un número, pasando por un objeto e incluso otro array (que es como se obtienen matrices multidimensionales).

No necesita inicializar sus arrays con un valor o longitud (ver “Arrays” en el Capítulo 3), sólo tiene que declararlos y añadir los valores que desee:

Advertencia: Usar delete en un valor de array eliminará esa posición del array, pero incluso si elimina el elemento final, no actualiza la propiedad length, ¡así que tenga cuidado! En el Capítulo 5 trataremos el tema del operador delete con más detalle.

Tenga cuidado al crear matrices “dispersas” (dejando o creando espacios vacíos o faltantes):

Mientras esto funciona, puede llevar a un comportamiento confuso con las “posiciones vacías” que dejas en el medio. Mientras que la posición parece tener el valor indefinido, no se comportará de la misma manera que si la posición está definida explícitamente (a[1] = indefinido). Ver “Arrays” en el Capítulo 3 para más información.

Los arrays están indexados numéricamente (como era de esperar), pero lo difícil es que también son objetos a los que se les pueden añadir claves/propiedades de cadena (pero que no cuentan para la longitud del array):

Sin embargo, un problema que hay que tener en cuenta es que si un valor de cadena destinado a ser un índice puede ser coaccionado a un número estándar base-10, entonces se asume que usted quería usarlo como un índice numérico en lugar de como un índice de cadena!

Generalmente, no es una buena idea añadir claves/propiedades de cadena a los arrays. Utilice objetos para retener valores en claves/propiedades y guarde matrices para valores estrictamente indexados numéricamente.

Similares a Arrays

Habrá ocasiones en las que necesitará convertir un valor similar a un array (una colección de valores indexados numéricamente) en un array verdadero, normalmente para que pueda llamar a las utilidades del array (como indexOf(...), concat(..), forEach(...), etc.) contra la colección de valores.

Por ejemplo, varias operaciones de consulta DOM devuelven listas de elementos DOM que no son matrices auténtivas pero que son lo suficientemente parecidas a las matrices para nuestros propósitos de conversión. Otro ejemplo común es cuando las funciones exponen el objeto arguments (parecido a un array) (a partir de ES6, obsoleto) para acceder a arguments como una lista.

Una manera muy común de hacer tal conversión es pedir prestada la utilidad slice(..) contra el valor:

Si slice() es llamado sin ningún otro parámetro, como efectivamente está en el fragmento de arriba, los valores por defecto para sus parámetros tienen el efecto de duplicar el array (o, en este caso, lo parecido al array).

A partir de ES6, también hay una utilidad incorporada llamada Array.from(...) que puede hacer la misma tarea:

Nota: Array.from(...) tiene varias capacidades poderosas, y será cubierto en detalle en el título ES6 & Beyond de esta serie.

Strings


Es una creencia muy común que las cadenas son esencialmente sólo matrices de caracteres. Aunque la implementación bajo las cubiertas puede o no utilizar matrices, es importante darse cuenta de que las cadenas JavaScript no son realmente lo mismo que las matrices de caracteres. La similitud es en su mayor parte sólo superficial.

Por ejemplo, consideremos estos dos valores:

Las cadenas tienen un parecido superficial con las matrices — parecidas a las matrices, como en el caso anterior — por ejemplo, ambas tienen una propiedad length, un método indexOf(..) (versión de array sólo a partir de ES5), y un método concat(..):

Así que, ambos son básicamente sólo “matrices de personajes”, ¿verdad? No exactamente:

Las cadenas JavaScript son inmutables, mientras que los arrays son bastante mutables. Además, el método de acceso a la posición a[1] no siempre era un JavaScript válido. Las versiones anteriores de IE no permitían esa sintaxis (pero ahora sí). En cambio, el enfoque correcto ha sido a.charAt(1).

Otra consecuencia de las cadenas inmutables es que ninguno de los métodos de cadena que alteran su contenido puede modificarse in situ, sino que debe crear y devolver nuevas cadenas. Por el contrario, muchos de los métodos que cambian el contenido de las matrices realmente se modifican in situ.

Además, muchos de los métodos de arrays que podrían ser útiles mientras que no están presentes cuando se trata de cadenas, pero podemos “tomar prestados” métodos de arrays sin mutación contra nuestra cadena:

Tomemos otro ejemplo: invertir una cadena (¡por cierto, una pregunta común de entrevista en JavaScript!). Los arrays tienen un método de mutador in situ reverse(), pero las cadenas no:

Desafortunadamente, este “préstamo” no funciona con mutadores de matrices, ya que las cadenas son inmutables y por lo tanto no pueden ser modificadas en su lugar:

Otra solución (o hack) es convertir la cadena en una matriz, realizar la operación deseada, y luego volver a convertirla en una cadena.

Si eso se siente feo, lo es. Sin embargo, funciona para cadenas simples, así que si necesitas algo rápido y sucio, a menudo este enfoque hace el trabajo.

Advertencia: ¡Ten cuidado! Este enfoque no funciona para cadenas con caracteres complejos (unicode) en ellas (símbolos astrales, caracteres multibyte, etc.). Necesita utilidades de biblioteca más sofisticadas que sean conscientes de unicode para que estas operaciones se manejen con precisión. Consulte el trabajo de Mathias Bynens sobre el tema: Esrever (https://github.com/mathiasbynens/esrever).

La otra manera de ver esto es: si usted está haciendo tareas más comúnmente en sus “cadenas” que las tratan como básicamente matrices de caracteres, tal vez sea mejor almacenarlas como matrices en lugar de como cadenas. Probablemente se ahorrará muchas molestias al convertir de cadena a array cada vez. Siempre puede llamar a join(“”) en la matriz de caracteres siempre que necesite la representación de la cadena.

Números


JavaScript sólo tiene un tipo numérico: número. Este tipo incluye tanto valores “enteros” como números decimales fraccionarios. Digo “entero” entre comillas porque ha sido durante mucho tiempo una crítica a JS que no hay números enteros verdaderos, como los hay en otros lenguajes. Eso puede cambiar en algún momento en el futuro, pero por ahora, sólo tenemos números para todo.

Así, en JS, un “entero” es sólo un valor que no tiene valor decimal fraccionario. Es decir, 42.0 es tanto un “entero” como 42.

Como la mayoría de los lenguajes modernos, incluyendo prácticamente todos los lenguajes de scripting, la implementación de los números de JavaScript se basa en el estándar “IEEE 754”, a menudo llamado de “coma flotante”. JavaScript utiliza específicamente el formato de “doble precisión” (también conocido como “binario de 64 bits”) del estándar.

Hay muchos grandes artículos en la Web sobre los detalles de cómo los números binarios de punto flotante se almacenan en la memoria, y las implicaciones de esas opciones. Debido a que la comprensión de los patrones de bits en memoria no es estrictamente necesaria para entender cómo usar correctamente los números en JS, lo dejaremos como un ejercicio para el lector interesado si desea profundizar en los detalles de IEEE 754.

Sintaxis Numérica

Los literales numéricos se expresan en JavaScript generalmente como literales decimales base-10. Por ejemplo:

La parte inicial de un valor decimal, si es 0, es opcional:

Del mismo modo, la parte final (la fracción) de un valor decimal después de ., si es 0, es opcional:

Advertencia: 42. es bastante poco común, y probablemente no sea una buena idea si estás tratando de evitar la confusión cuando otras personas leen tu código. Pero es, sin embargo, válido.

De forma predeterminada, la mayoría de los números se emitirán como decimales base-10, y se eliminarán los 0 fraccionarios finales. Así que:

Los números muy grandes o muy pequeños se emitirán por defecto en forma de exponente, igual que la salida del método toExponential(), como:

Dado que los valores numéricos pueden encajonarse con la envoltura del objeto Number (véase el Capítulo 3), los valores numéricos pueden acceder a los métodos que están incorporados en Number.prototype (véase el Capítulo 3). Por ejemplo, el método toFixed(..) le permite especificar con cuántos decimales fraccionarios desea que se represente el valor:

Observe que la salida es en realidad una representación de cadena del número, y que el valor se rellena con 0 en el lado derecho si solicita más decimales de los que contiene el valor.

toPrecision(..) es similar, pero especifica cuántos dígitos significativos deben utilizarse para representar el valor:

No es necesario utilizar una variable con el valor en ella para acceder a estos métodos; puede acceder a estos métodos directamente en los literales numéricos. Pero hay que tener cuidado con el operador .. Puesto que . es un carácter numérico válido, se interpretará primero como parte del número literal, si es posible, en lugar de ser interpretado como un atributo de propiedad.

42.toFixed(3) es una sintaxis inválida, porque el . se trata como parte del literal 42. (que es válido – ¡vea arriba!), y entonces no hay ningún operador de propiedad presente para hacer el .toFixed acceda.

42..toFixed(3) funciona porque el primero . es parte del número y el segundo . es el operador de la propiedad. Pero probablemente parezca extraño, y de hecho es muy raro ver algo así en código JavaScript real. De hecho, es muy poco común acceder a los métodos directamente sobre cualquiera de los valores primitivos. Poco común no significa malo o incorrecto.

Nota: Hay librerías que amplían Number.prototype incorporado (vea el Capítulo 3) para proporcionar operaciones adicionales en/con números, por lo que en esos casos, es perfectamente válido usar algo como 10..makeItRain()para activar una animación de 10 segundos de lluvia de dinero, o algo más tonto que eso.

Esto también es técnicamente válido (fíjese en el espacio):

Sin embargo, con el literal número específicamente, esto es particularmente un estilo de codificación confuso y no servirá para otro propósito que el de confundir a otros desarrolladores (y a su futuro yo). Evítalo.

Los números también se pueden especificar en forma de exponente, lo que es común cuando se representan números más grandes, como por ejemplo:

Nota: Empezando con ES6 + modo estricto, la forma 0363 de literales octales ya no es permitida (ver abajo para la nueva forma). La forma 0363 todavía está permitida en modo no estricto, pero deberías dejar de usarla de todos modos, para ser amigable con el futuro (¡y porque ya deberías estar usando el modo estricto!).

A partir de ES6, también son válidos los siguientes nuevas formas:

Por favor, haz un favor a tus compañeros desarrolladores: nunca uses la forma 0O363. 0 al lado de O mayúscula es sólo pedir confusión. Utilice siempre los predicados en minúsculas 0x, 0b y 0o.

Valores decimales pequeños

El efecto secundario más famoso del uso de números binarios de coma flotante (que, recuerde, es cierto para todos los lenguajes que usan IEEE 754 — no sólo JavaScript como muchos asumen/pretenden) es:

Matemáticamente, sabemos que esa afirmación debe ser cierta. ¿Por qué es false?

En pocas palabras, las representaciones para 0.1 y 0.2 en coma flotante binaria no son exactas, así que cuando se suman, el resultado no es exactamente 0.3. Está muy cerca: 0.300000000000000000004, pero si su comparación falla, “cerca” es irrelevante.

Nota: ¿Debería JavaScript cambiar a una implementación de número diferente que tenga representaciones exactas para todos los valores? Algunos piensan que sí. A lo largo de los años se han presentado muchas alternativas. Ninguno de ellos ha sido aceptado todavía, y tal vez nunca lo sea. Por muy fácil que parezca agitar una mano y decir: “¡Arregla ya ese error! Si lo fuera, definitivamente habría cambiado hace mucho tiempo.

Ahora, la pregunta es, si no se puede confiar en que algunos números sean exactos, ¿significa eso que no podemos usar números en absoluto? Por supuesto que no.

Hay algunas aplicaciones en las que hay que tener más cuidado, especialmente cuando se trata de valores decimales fraccionados. También hay un montón de aplicaciones (¿tal vez la mayoría?) que sólo tratan con números enteros (“enteros”), y además, sólo tratan con números en los millones o trillones como máximo. Estas aplicaciones han sido, y siempre serán, perfectamente seguras para usar operaciones numéricas en JS.

¿Y si tuviéramos que comparar dos números, como 0.1 + 0.2 a 0.3, sabiendo que la simple prueba de igualdad falla?

La práctica más comúnmente aceptada es utilizar un pequeño valor de “error de redondeo” como tolerancia para la comparación. Este pequeño valor es a menudo llamado “máquina epsilon”, que es comúnmente 2^-52 (2.220446049250313e-16) para el tipo de números en JavaScript.

A partir de ES6, Number.EPSILON está predefinido con este valor de tolerancia, por lo que le gustaría utilizarlo, pero puede realizar un polifyll de forma segura en la definición para pre-ES6:

Podemos utilizar este número EPSILON para comparar dos números para “igualdad” (dentro de la tolerancia de error de redondeo):

El valor máximo en coma flotante que se puede representar es aproximadamente 1.798e+308 (¡que es realmente, realmente, realmente enorme!), predefinido para usted como Number.MAX_VALUE. En el extremo pequeño, Number.MIN_VALUE es aproximadamente 5e-324, que no es negativo pero es realmente cerca de cero!

Rangos enteros seguros

Debido a cómo se representan los números, hay un rango de valores “seguros” para los números “enteros”, y es significativamente menor que Number.MAX_VALUE.

El número entero máximo que puede representarse “con seguridad” (es decir, se garantiza que el valor solicitado es realmente representable sin ambigüedades) es 2^53 - 1, que es 900719999254740991. Si usted inserta sus comas, verá que esto es un poco más de 9 cuatrillones. Así que eso es bastante grande para que los números sean mayores.

Este valor se predefine automáticamente en ES6, como Number.MAX_SAFE_INTEGER. No es de extrañar que haya un valor mínimo, -900719925254740991, y está definido en ES6 como Number.MIN_SAFE_INTEGER.

La principal manera en la que los programas de JS se enfrentan con números tan grandes es cuando se trata de IDs de 64 bits de bases de datos, etc. Los números de 64 bits no pueden representarse con precisión con el tipo número, por lo que deben almacenarse en (y transmitirse a/desde) JavaScript mediante la representación de cadenas.

Las operaciones numéricas en valores de números de ID tan grandes (además de la comparación, que estará bien con las cadenas) no son tan comunes, afortunadamente. Pero si necesitas realizar cálculos matemáticos con estos valores tan grandes, por ahora necesitarás usar una utilidad de números grandes. Los números grandes pueden obtener soporte oficial en una futura versión de JavaScript.

Pruebas para números enteros

Para probar si un valor es un número entero, puede usar Number.isInteger(..) de la especificación ES6:

Para representarlo en entornos pre ES6:

Para probar si un valor es un número entero seguro, utilice Number.isSafeInteger(..) de la especificación ES6:

Para representarlo en entornos pre ES6:

Números enteros de 32 bits (con signo)

Mientras que los números enteros pueden tener un rango seguro de hasta 9 cuatrillones (53 bits), hay algunas operaciones numéricas (como los operadores de bits) que sólo se definen para números de 32 bits, por lo que el “rango seguro” para los números utilizados de esa manera debe ser mucho más pequeño.

El rango es entonces Math.pow(-2,31) (-2147483648, aproximadamente -2,1 billones) hasta Math.pow(2,31)-1 (2147483647, aproximadamente +2,1 billones).

Para forzar un valor numérico en a a un valor entero con signo de 32 bits, use a | 0. Esto funciona porque el operador | bit a bit sólo funciona para valores enteros de 32 bits (lo que significa que sólo puede prestar atención a 32 bits y cualquier otro bit se perderá). Entonces, hacer un “OR” con cero es esencialmente “hablar” bit a bit.

Nota: Ciertos valores especiales (que cubriremos en la siguiente sección) como NaN e Infinity no son “seguros para 32 bits”, en el sentido de que esos valores cuando se pasan a un operador bitwise pasarán a través de la operación abstracta ToInt32 (ver Capítulo 4) y se convertirán simplemente en el valor +0 para el propósito de esa operación bitwise.

Valores especiales


Existen varios valores especiales repartidos entre los distintos tipos que el desarrollador de JS actual debe conocer y utilizar correctamente.

Los valores que no son valores

Para el tipo indefinido, hay un solo valor: undefined. Para el tipo nulo, hay un solo valor: null. Así que para ambos, la etiqueta es tanto su tipo como su valor.

Tanto los valores indefinidos como los nulos se consideran a menudo intercambiables como valores “vacíos” o “no” valores. Otros desarrolladores prefieren distinguir entre ellos con matices. Por ejemplo:

  • null es un valor vacío
  • undefined es un valor que falta

O:

  • undefined no ha tenido un valor todavía
  • null tenía un valor y ya no lo tiene

Independientemente de cómo elijas “definir” y usar estos dos valores, null es una palabra clave especial, no un identificador, y por lo tanto no puedes tratarla como una variable a la que asignar (¿por qué lo harías?). Sin embargo, undefined es (desafortunadamente) un identificador. Uh oh.

undefined

En modo no estricto, en realidad es posible (¡aunque increíblemente desaconsejable!) asignar un valor al identificador indefinido proporcionado globalmente:

Sin embargo, tanto en modo no estricto como en modo estricto, puede crear una variable local con el nombre undefined. Pero de nuevo, ¡es una idea terrible!

Los amigos no dejan que los amigos sobreescriban undefined. Nunca.

Operador void

Mientras que indefinido es un identificador incorporado que mantiene (a menos que se modifique – ¡ver arriba!) el valor indefinido incorporado, otra forma de obtener este valor es el operador void (vacío).

La expresión void ___ “vacía” cualquier valor, de modo que el resultado de la expresión es siempre el valor indefinido. No modifica el valor existente; sólo asegura que ningún valor vuelva de la expresión del operador.

Por convención (sobre todo de la programación en lenguaje C), para representar el valor indefinido por sí solo usando void, se usaría void 0 (aunque claramente incluso void true o cualquier otra expresión void hace lo mismo). No hay diferencia práctica entre void 0, void 1 e indefinido.

Pero el operador void puede ser útil en algunas otras circunstancias, si necesitas asegurarte de que una expresión no tiene valor de resultado (incluso si tiene efectos secundarios).

Por ejemplo:

Aquí, la función setTimeout(..) devuelve un valor numérico (el identificador único del intervalo del temporizador, si desea cancelarlo), pero queremos “anularlo” para que el valor de retorno de nuestra función no dé un falso positivo con la sentencia if.

Muchos desarrolladores prefieren hacer estas acciones por separado, lo que funciona de la misma manera pero no utiliza el operador void:

En general, si alguna vez hay un lugar donde existe un valor (a partir de alguna expresión) y en su lugar encuentras útil que el valor sea indefinido, usa el operador void. Eso probablemente no será muy común en sus programas, pero en los raros casos en que usted lo necesita, puede ser muy útil.

Números especiales

El tipo número incluye varios valores especiales. Echaremos un vistazo a cada uno en detalle.

El número no número

Cualquier operación matemática que se realice sin que ambos operandos sean números (o valores que puedan ser interpretados como números regulares en base 10 o base 16) resultará en que la operación no produzca un número válido, en cuyo caso se obtendrá el valor NaN.

NaN significa literalmente “no es un número”, aunque esta etiqueta/descripción es muy pobre y engañosa, como veremos en breve. Sería mucho más exacto pensar en NaN como “número inválido”, “número fallido”, o incluso “número malo”, que pensar en él como “no un número”.

Por ejemplo:

En otras palabras: “¡El tipo de no-es-un-número es ‘número’!” Hurra por confundir nombres y semántica.

NaN es una especie de “valor centinela” (un valor normal al que se le asigna un significado especial) que representa un tipo especial de condición de error dentro del conjunto de números. La condición de error es, en esencia: “Intenté realizar una operación matemática pero fallé, así que aquí está el resultado del número fallido.”

Por lo tanto, si tienes un valor en alguna variable y quieres probar si es este número especial fallido NaN, podrías pensar que podrías compararlo directamente con el propio NaN, como puedes hacerlo con cualquier otro valor, como nulo o indefinido. No es así.

NaN es un valor muy especial en el sentido de que nunca es igual a otro valor de NaN (es decir, nunca es igual a sí mismo). Es el único valor, de hecho, que no es reflexivo (sin la característica Identidad x ==== x). Entonces, NaN !== NaN. Un poco extraño, ¿eh?

Entonces, ¿cómo hacemos la prueba si no podemos compararnos con NaN (ya que esa comparación siempre fallaría)?

Bastante fácil, ¿verdad? Utilizamos la utilidad global integrada llamada isNaN(...) y nos dice si el valor es NaN o no. Problema resuelto!

No tan rápido.

La utilidad isNaN(...) tiene un defecto fatal. Parece que trató de tomar el significado de NaN (“No es un número”) demasiado literalmente – que su trabajo es básicamente: “comprueba si lque se ha pasado es o no un número.” Pero eso no es del todo exacto.

Claramente, “foo” no es literalmente un número, pero definitivamente tampoco es el valor NaN! Este bug ha estado en JS desde el principio (más de 19 años de ups).

A partir de ES6, finalmente se ha proporcionado una utilidad de reemplazo: Number.isNaN(...). Un simple polifyll para que pueda comprobar con seguridad los valores de NaN ahora, incluso en navegadores pre-ES6 es:

En realidad, podemos implementar un polifyll de Number.isNaN(...) aún más fácil, aprovechando ese hecho peculiar de que NaN no es igual a sí mismo. NaN es el único valor en todo el lenguaje donde eso es cierto; cualquier otro valor es siempre igual a sí mismo.

Así que:

Raro, ¿eh? ¡Pero funciona!

Los NaNs son probablemente una realidad en muchos programas de JS del mundo real, ya sea a propósito o por accidente. Es una muy buena idea usar una prueba confiable, como Number.isNaN(..) tal y como se proporciona (o con polifyll), para reconocerlos correctamente.

Si estás usando isNaN(...) en un programa, la triste realidad es que tu programa tiene un error, ¡incluso si aún no te ha mordido!

Infinitos

Los desarrolladores de lenguajes compilados tradicionales como C están probablemente acostumbrados a ver un error de compilador o una excepción de tiempo de ejecución, como “Divide by zero”, para una operación como:

Sin embargo, en JS, esta operación está bien definida y da como resultado el valor Infinito (o Number.POSITIVE_INFINITY). No es de extrañar:

Como puedes ver, -Infinity (o Number.NEGATIVE_INFINITY) resulta de una división por cero donde cualquiera (¡pero no ambos!) de los operandos divididos es negativo.

JS utiliza representaciones numéricas finitas (IEEE 754 en coma flotante, que ya hemos tratado anteriormente), por lo que, contrariamente a las matemáticas puras, parece que es posible desbordar incluso con una operación como sumar o restar, en cuyo caso se obtendría Infinity o -Infinity

Por ejemplo:

Según la especificación, si una operación como la suma da como resultado un valor demasiado grande para representarlo, el modo “redondo a cercano” de IEEE 754 especifica cuál debería ser el resultado. Así, en un sentido crudo, Number.MAX_VALUE + Math.pow( 2, 969) está más cerca del Number.MAX_VALUE que de infinito, por lo que “redondea hacia abajo”, mientras que el Number.MAX_VALUE + Math.pow( 2, 970) está más cerca de infinito, por lo que “redondea hacia arriba”.

Si piensas demasiado en eso, te dolerá la cabeza. Así que no lo hagas. En serio, ¡detente!

Sin embargo, una vez que te desbordas a cualquiera de los dos infinitos, no hay vuelta atrás. En otras palabras, en un sentido casi poético, se puede pasar de lo finito a lo infinito, pero no de lo infinito a lo finito.

Es casi filosófico preguntar: “¿Qué es el infinito dividido por el infinito? Nuestros ingenuos cerebros probablemente dirían “1” o tal vez “infinito”. Resulta que ninguno de los dos es verdad. Tanto matemáticamente como en JavaScript, Infinity / Infinity no es una operación definida. En JS, esto da como resultado NaN.

¿Pero qué hay de cualquier número finito positivo dividido por Infinito? Eso es fácil! 0. ¿Y qué hay de un número finito negativo dividido por Infinito? Sigue leyendo!

Ceros

Aunque puede confundir al lector de mentalidad matemática, JavaScript tiene tanto un cero 0 normal (también conocido como un cero +0 positivo) como un cero -0 negativo. Antes de explicar por qué existe el -0, deberíamos examinar cómo lo maneja JS, porque puede ser bastante confuso.

Además de ser especificado literalmente como -0, el cero negativo también resulta de ciertas operaciones matemáticas. Por ejemplo:

La suma y la resta no pueden dar como resultado un cero negativo.

Un cero negativo cuando se examina en la consola del desarrollador normalmente revelará -0, aunque ese no era el caso común hasta hace poco, por lo que algunos navegadores más antiguos pueden reportarlo como 0.

Sin embargo, si se intenta encadenar un valor cero negativo, siempre se informará como “0”, según la especificación.

Curiosamente, las operaciones inversas (pasando de cadena a número) no mienten:

Advertencia: El comportamiento JSON.stringify( -0) de “0” es particularmente extraño cuando se observa que es inconsistente con lo contrario: JSON.parse( "-0") reporta -0 como usted esperaría correctamente.

Además de que el encadenamiento del cero negativo es engañoso para ocultar su verdadero valor, los operadores de comparación también están configurados (intencionalmente) para mentir.

Claramente, si quieres distinguir un -0 de un 0 en tu código, no puedes confiar sólo en lo que sale de la consola del desarrollador, así que vas a tener que ser un poco más inteligente:

Ahora, ¿por qué necesitamos un cero negativo, además de las trivialidades académicas?

Hay ciertas aplicaciones donde los desarrolladores usan la magnitud de un valor para representar una pieza de información (como la velocidad de movimiento por frame de animación) y el signo de ese número para representar otra pieza de información (como la dirección de ese movimiento).

En esas aplicaciones, por ejemplo, si una variable llega a cero y pierde su signo, entonces se perdería la información de en qué dirección se estaba moviendo antes de llegar a cero. Preservar el signo del cero previene la pérdida de información potencialmente no deseada.

Igualdad Especial

Como vimos anteriormente, el valor NaN y el valor -0 tienen un comportamiento especial cuando se trata de comparación de igualdad. NaN nunca es igual a sí mismo, así que tiene que usar Number.isNaN(..) (o un polyfill) de ES6. De manera similar, -0 miente y pretende que es igual (incluso === estrictamente igual — ver Capítulo 4) a 0positivo regular, así que tienes que usar la utilidad algo hackish isNegZero(...) que sugerimos arriba.

A partir de ES6, hay una nueva utilidad que se puede utilizar para probar dos valores de igualdad absoluta, sin ninguna de estas excepciones. Se llama Object.is(...):

Hay un relleno bastante simple para Object.is(...) para entornos pre-ES6:

Object.is(...) probablemente no debería usarse en los casos en los que se sabe que == o === son seguros (ver Capítulo 4 “Coacción”), ya que los operadores son probablemente mucho más eficientes y ciertamente son más naturales. Object.is(...) es principalmente para estos casos especiales de igualdad.

Valor vs. Referencia


En muchos otros idiomas, los valores se pueden asignar/pasar por copia de valor o por copia de referencia, dependiendo de la sintaxis que se utilice.

Por ejemplo, en C++ si quieres pasar una variable numérica a una función y tener el valor de esa variable actualizado, puedes declarar el parámetro de la función como int& myNum, y cuando pasas en una variable como x, myNumserá una referencia a x; las referencias son como una forma especial de punteros, donde obtienes un puntero a otra variable (como un alias). Si no declara un parámetro de referencia, el valor transferido siempre se copiará, incluso si se trata de un objeto complejo.

En JavaScript, no hay punteros, y las referencias funcionan un poco diferente. No se puede tener una referencia de una variable JS a otra variable. Eso no es posible.

Una referencia en puntos JS con un valor (compartido), por lo que si tiene 10 referencias diferentes, todas son siempre referencias distintas a un único valor compartido; ninguna de ellas son referencias o punteros entre sí.

Además, en JavaScript, no hay pistas sintácticas que controlen el valor frente a la asignación/aprobación de referencias. En su lugar, el tipo de valor sólo controla si ese valor se asignará por copia de valor o por copia de referencia.

Vamos a ilustrar:

Los valores simples (también conocidos como primitivos escalares) siempre se asignan/pasan por valor-copia: null, undefined, string, number, boolean y symbol de ES6.

Los valores compuestos — objetos (incluyendo matrices y todas las envolturas de objetos — ver Capítulo 3) y funciones — siempre crean una copia de la referencia al asignarla o pasarla.

En el fragmento anterior, debido a que 2 es un primitivo escalar, a tiene una copia inicial de ese valor, y a b se le asigna otra copia del valor. Cuando se cambia b, no se está cambiando de ninguna manera el valor en a.

Pero tanto c como d son referencias separadas al mismo valor compartido [1,2,3], que es un valor compuesto. Es importante notar que ni c ni d “poseen” el valor [1,2,3] — ambos son referencias iguales al valor. Por lo tanto, cuando se usa cualquier referencia para modificar (.push(4)) el valor real de la matriz compartida, está afectando sólo al valor compartido, y ambas referencias harán referencia al valor recién modificado [1,2,3,4].

Dado que las referencias apuntan a los valores en sí mismos y no a las variables, no se puede utilizar una referencia para cambiar donde se apunta otra referencia:

Cuando hacemos la asignación b =[4,5,6], no estamos haciendo absolutamente nada para afectar donde a todavía está haciendo referencia ([1,2,3]). Para hacer eso, b tendría que ser un puntero a a en vez de una referencia a la matriz — ¡pero tal capacidad no existe en JS!

La forma más común en que se produce esta confusión es con los parámetros de función:

Cuando pasamos en el argumento a, asigna una copia de la referencia a x. x y a son referencias separadas apuntando al mismo valor [1,2,3]. Ahora, dentro de la función, podemos usar esa referencia para mutar el valor mismo (push(4)). Pero cuando hacemos la asignación x = [4,5,6], esto no afecta en modo alguno hacia dónde apunta la referencia inicial a a — todavía apunta al valor (ahora modificado) [1,2,3,4].

No hay manera de usar la referencia x para cambiar donde a está apuntando. Sólo podríamos modificar el contenido del valor compartido al que apuntan a la vez a y x.

Para lograr cambiar a para tener el contenido del valor [4,5,6,7], no se puede crear un nuevo array y asignarlo — se debe modificar el valor del array existente:

Como puede ver, x.length = 0 y x.push(4,5,6,7) no estaban creando un nuevo array, sino modificando el array compartido existente. Así que, por supuesto, a referencia al nuevo contenido de [4,5,6,7].

Recuerde: no puede controlar directamente/sobreescribir el valor-copia vs. referencia — esa semántica es controlada completamente por el tipo del valor subyacente.

Para pasar efectivamente un valor compuesto (como un array) por un valor-copia, necesita hacer manualmente una copia de él, para que la referencia pasada no apunte al original. Por ejemplo:

slice(...) sin parámetros por defecto hace una copia completamente nueva (superficial) del array. Por lo tanto, pasamos en una referencia sólo a la matriz copiada, y así foo(..) no puede afectar el contenido de a.

Para hacer lo contrario — pasar un valor primitivo escalar de una manera en la que sus actualizaciones de valor puedan ser vistas, como una referencia — tienes que envolver el valor en otro valor compuesto (objeto, matriz, etc.) que pueda ser pasado por una copia de referencia:

Aquí, obj actúa como una envoltura para la propiedad primitiva escalar a. Cuando se pasa a foo(..), se pasa una copia de la referencia obj y se establece en el parámetro wrapper. Ahora podemos usar la referencia de la envoltura para acceder al objeto compartido y actualizar su propiedad. Una vez finalizada la función, obj.a verá el valor actualizado 42.

Puede que se te ocurra que si quisieras pasar una referencia a un valor primitivo escalar como 2, podrías simplemente encajonar el valor en su envoltura de objeto Number (ver Capítulo 3).

Es cierto que una copia de la referencia a este objeto Number será pasada a la función, pero desafortunadamente, tener una referencia al objeto compartido no le dará la habilidad de modificar el valor primitivo compartido, como puede esperar:

El problema es que el valor primitivo escalar subyacente no es mutable (lo mismo ocurre con String y Boolean). Si un objeto Number contiene el valor primitivo escalar 2, ese objeto Number exacto nunca puede cambiarse para contener otro valor; sólo puede crear un objeto Number completamente nuevo con un valor diferente.

Cuando x se usa en la expresión x + 1, el valor primitivo escalar subyacente 2 se descompone (extrae) del objeto Number automáticamente, por lo que la línea x = x + 1 cambia muy sutilmente x de ser una referencia compartida al objeto Number, a simplemente mantener el valor primitivo escalar 3 como resultado de la operación de suma 2 + 1. Por lo tanto, b en el exterior todavía hace referencia al objeto Number original no modificado/imutable que contiene el valor 2.

Puede añadir propiedades encima del objeto Number (sólo que no cambiar su valor primitivo interno), por lo que podría intercambiar información indirectamente a través de esas propiedades adicionales.

Esto no es tan común, sin embargo; probablemente no sería considerado una buena práctica por la mayoría de los desarrolladores.

En lugar de utilizar la envoltura del objeto Number de esta manera, probablemente sea mucho mejor utilizar el enfoque manual de envoltura de objeto (obj) del fragmento anterior. Eso no quiere decir que no hay usos inteligentes para las envolturas de objetos incorporadas como Number – sólo que probablemente debería preferir la forma de valor primitivo escalar en la mayoría de los casos.

Las referencias son bastante poderosas, pero a veces se interponen en tu camino, y a veces las necesitas donde no existen. El único control que tiene sobre el comportamiento de referencia frente al comportamiento de copia de valores es el tipo de valor en sí, por lo que debe influir indirectamente en el comportamiento de asignación/pasaje mediante el cual decide utilizar los tipos de valores.

Revisión

En JavaScript, las matrices son simplemente colecciones indexadas numéricamente de cualquier tipo de valor. Las cadenas son algo “parecidas a las matrices”, pero tienen comportamientos distintos y se debe tener cuidado si se desea tratarlas como matrices. Los números en JavaScript incluyen tanto “números enteros” como valores en coma flotante.

Dentro de los tipos primitivos se definen varios valores especiales.

El tipo nulo tiene un solo valor: null, y del mismo modo el tipo undefined tiene sólo el valor undefined. undefined es básicamente el valor por defecto en cualquier variable o propiedad si no hay otro valor presente. El operador void le permite crear el valor no definido a partir de cualquier otro valor.

Los números incluyen varios valores especiales, como NaN (supuestamente “No es un número”, pero en realidad es más apropiado “número inválido”); +Infinity y -Infinity; y -0.

Las primitivas escalares simples (cadenas, números, etc.) se asignan/pasan por copia de valor, pero los valores compuestos (objetos, etc.) se asignan/pasan por copia de referencia. Las referencias no son como las referencias/punteros en otros idiomas, nunca apuntan a otras variables/referencias, sólo a los valores subyacentes.


Capítulo 3: Nativos


Varias veces en los Capítulos 1 y 2, aludimos a varios elementos incorporados, usualmente llamados “nativos”, como String y Number. Vamos a examinarlos en detalle ahora.

Aquí hay una lista de los nativos de uso más común:

  • String()
  • Number()
  • Boolean()
  • Array()
  • Object()
  • Function()
  • RegExp()
  • Date()
  • Error()
  • Symbol() — añadido en ES6!

Como puede ver, estos nativos son en realidad funciones incorporadas.

Si viene a JS desde un lenguaje como Java, String() de JavaScript se parecerá al constructor String(..) al que está acostumbrado para crear valores de cadena. Así que, rápidamente observarás que puedes hacer cosas como:

Es cierto que cada uno de estos nativos puede ser utilizado como constructor nativo. Pero lo que se está construyendo puede ser diferente de lo que piensas.

El resultado del constructor del valor (new String("abc")) es una envoltura de objeto alrededor del valor primitivo (“abc”).

Importantemente, typeof muestra que estos objetos no son sus propios tipos especiales, si bien son más apropiadamente subtipos del tipo de objeto.

Esta envoltura de objeto puede ser observada además con:

La salida de esa declaración varía dependiendo de su navegador, ya que las consolas de desarrollo son libres de elegir, pero consideran que es apropiado serializar el objeto para la inspección del desarrollador.

Nota: En el momento de escribir este artículo, la última versión de Chrome imprime algo como esto: String {0: "a", 1: "b", 2: "c", length: 3, [[PrimitiveValue]]: "abc"}. Pero las versiones anteriores de Chrome solían imprimir esto: String {0: "a", 1: "b", 2: "c"}. El último Firefox actualmente imprime String ["a", "b", "c"], pero se utiliza para imprimir “abc” en cursiva, lo que permite hacer clic para abrir el inspector de objetos. Por supuesto, estos resultados están sujetos a cambios rápidos y su experiencia puede variar.

El punto es que newString("abc") crea una envoltura de objeto de cadena alrededor de “abc”, no sólo el valor primitivo “abc” en sí mismo.

[[Class]] Interna


Los valores que typeof "object" (como un array) se etiquetan adicionalmente con una propiedad [[Class]]interna (piense en esto más como una clasificación interna que relacionada con las clases de la codificación tradicional orientada a clases). Esta propiedad no puede ser accedida directamente, pero generalmente puede ser revelada indirectamente tomando prestado el método por defecto Object.prototype.toString(..) llamado contra el valor. Por ejemplo:

Así, para el array de este ejemplo, el valor interno [[Class]] es “Array”, y para la expresión regular, es “RegExp”. En la mayoría de los casos, este valor [[Class]] interno corresponde al constructor nativo incorporado (ver abajo) que está relacionado con el valor, pero no siempre es así.

¿Qué hay de los valores primitivos? Primero, null y undefined:

Observará que no hay constructores nativos Null() o Undefined(), pero sin embargo los valores “Null” e “Undefined” son los valores [[Class]] internos expuestos.

Pero para los otros primitivos simples como la cadena, el número y el booleano, en realidad aparece otro comportamiento, que generalmente se llama “empaquetado” — “boxing” (ver la sección “Envolturas de empaquetado” — “Boxing Wrappers” — a continuación):

En este fragmento, cada una de los primitivos simples son automáticamente encajonados por sus respectivas envolturas de objeto, por lo que “String”, “Number” y “Boolean” se revelan como los respectivos valores [[Class]]internos.

Nota: El comportamiento de toString() y [[Class]] como se ilustra aquí ha cambiado un poco de ES5 a ES6, pero cubrimos esos detalles en el título ES6 & Beyond de esta serie.

Envolturas de empaquetado


Estas envolturas de objetos sirven para un propósito muy importante. Los valores primitivos no tienen propiedades ni métodos, así que para acceder a .length o .toString() necesitas un envoltorio de objeto alrededor del valor. Afortunadamente, JS encajonará automáticamente (también conocido como envoltura) el valor primitivo para proporcionarlos.

Por lo tanto, si vas a acceder a estas propiedades/métodos en tus valores de cadena regularmente, como por ejemplo una condición i < a.length en un bucle, puede parecer lógico tener la forma del objeto del valor desde el principio, por lo que el motor JS no necesita crearlo implícitamente para ti.

Pero resulta que es una mala idea. Hace mucho tiempo que los navegadores optimizaron el rendimiento en casos comunes como el de .length, lo que significa que su programa realmente irá más lento si intenta “preoptimizar” utilizando directamente a forma de objeto (que no está en la ruta optimizada).

En general, básicamente no hay razón para usar la forma de objeto directamente. Es mejor dejar que el encajonado suceda implícitamente donde sea necesario. En otras palabras, nunca haga cosas como new String ("abc"), new Number(42), etc — siempre prefiera usar los valores literales primitivos “abc” y 42.

Problemas de envoltura de objetos

Hay algunos problemas con el uso directo de las envolturas de objeto que conocer si usted decide usarlos alguna vez.

Por ejemplo, considere los valores booleanos envueltos:

El problema es que has creado una envoltura de objeto alrededor del valor false, pero los objetos mismos son “verdaderos” (ver Capítulo 4), por lo que usar el objeto se comporta de manera opuesta a usar el valor falsesubyacente en sí mismo, lo cual es bastante contrario a las expectativas normales.

Si desea encajonar manualmente un valor primitivo, puede utilizar la función Object(..) (sin la palabra clave new):

Una vez más, se desaconseja el uso directo de la envoltura de objeto empaquetado (como b y c arriba), pero puede haber algunas raras ocasiones en las que se encontrará con que pueden ser útiles.

Desempaquetado


Si tiene una envoltura de objeto y desea obtener el valor primitivo subyacente, puede utilizar el método valueOf():

El desempaquetado también puede ocurrir implícitamente, cuando se utiliza un valor de envoltura de objeto de una manera que requiere el valor primitivo. Este proceso (coerción) se tratará con más detalle en el Capítulo 4, pero brevemente:

Los nativos como constructores


Para los valores de matriz, objeto, función y expresión regular, es casi universalmente preferible que use la forma literal para crear los valores, pero la forma literal crea el mismo tipo de objeto que el constructor (es decir, no hay valor no envuelto).

Tal como hemos visto anteriormente con los otros nativos, estas formas constructoras deben ser generalmente evitadas, a menos que realmente sepas que las necesitas, sobre todo porque introducen excepciones y problemas con las que probablemente no quieras lidiar.

Array(..)

Nota: El constructor Array(…) no requiere palabra clave new delante de él. Si lo omites, se comportará como si lo hubieras usado de todos modos. Así que Array(1,2,3) da el mismo resultado que el nuevo Array(1,2,3).

El constructor de Array tiene una forma especial en la que si sólo se pasa un argumento numérico, en lugar de proporcionar ese valor como contenido de la matriz, se toma como una longitud para “frefijar la longitud del array” (bueno, más o menos).

Es una idea terrible. En primer lugar, puede tropezar con esa forma accidentalmente, ya que es fácil olvidarlo.

Pero lo más importante es que no existe tal cosa como realmente “prefijar” la matriz. En su lugar, lo que está creando es un array que de otro modo estaría vacío, pero configurando la propiedad length del array con el valor numérico especificado.

Un array que no tiene valores explícitos en sus posiciones, pero tiene una propiedad de longitud que implica que las posiciones existen, es un tipo de estructura de datos exótica y extraña en JS con un comportamiento muy extraño y confuso. La capacidad de crear tal valor viene puramente de funcionalidades antiguas, obsoletas e históricas (“objetos parecidos a una matriz” como el objeto de los argumentos).

Nota: Un array con al menos una “posición vacía” se denomina a menudo “array disperso”.

No ayuda el hecho de que éste sea otro ejemplo más en el que las consolas de los navegadores varíen en la forma en que representan un objeto de este tipo, lo que genera más confusión.

Por ejemplo:

La serialización de una en Chrome es (en el momento de escribir este artículo): [ undefined x 3 ]. Esto es realmente desafortunado. Implica que hay tres valores indefinidos en posiciones de este array, cuando en realidad las posiciones no existen (las llamadas “posiciones vacías”, ¡también un mal nombre!).

Para visualizar la diferencia, pruebe esto:

Nota: Como puede ver con c en este ejemplo, las posiciones vacías en un array pueden ocurrir después de la creación del array. Cambiando la longitud de un array para ir más allá de su número de posiciones realmente definidos, se introducen implícitamente posiciones vacías. De hecho, podrías incluso llamar a delete b[1] en el fragmento anterior, e introduciría una posición vacía en el centro de b.

Para b (en Chrome, actualmente), encontrará [ undefined, undefined, undefined ] como la serialización, a diferencia de [ undefined x 3 ] para a y c. ¿Confundido? Sí, como todos los demás.

Peor aún, en el momento de escribir este artículo, Firefox devuelve [ , , , ] para a y c. ¿Captaste por qué es tan confuso? Mira atentamente. Tres comas implican cuatro espacios, no tres espacios como cabría esperar.

¿¡Qué!? Firefox pone un , extra al final de su serialización aquí porque a partir de ES5, las comas de arrastre en las listas (valores de array, listas de propiedades, etc.) están permitidas (y por lo tanto dejadas de lado e ignoradas). Así que si escribiera un valor [ , , , ] en su programa o en la consola, obtendría el valor subyacente que es como [ , , ] (es decir, una matriz con tres espacios vacíos). Esta elección, aunque confusa si se lee la consola del desarrollador, se defiende porque hace que el comportamiento de copiar y pegar sea preciso.

Si está moviendo la cabeza o volteando los ojos en este momento, ¡no está solo!.

Desafortunadamente, empeora. La salida de la consola, más que simplemente confundir, muestra que a y b del fragmento de código anterior se comportan de la misma manera en algunos casos pero de manera diferente en otros:

Uff.

La llamada a.map(...) falla porque las posiciones no existen, así que map(..) no tiene nada que iterar. join(...) funciona de forma diferente. Básicamente, podemos pensar en ello implementado de esta manera:

Como puede ver, join(...) funciona simplemente asumiendo que las posiciones existen y haciendo un bucle hasta el valor de length. Sea lo que sea que map(...) haga internamente, (aparentemente) no hace tal suposición, así que el resultado de la extraña matriz de “posiciones vacías” es inesperado y es probable que falle.

Por lo tanto, si querías crear una matriz de valores reales indefinidos (no sólo “posiciones vacías”), ¿cómo podrías hacerlo (además de manualmente)?

¿Confundido? Sí. Así es como funciona.

apply(...) es una utilidad disponible para todas las funciones, que llama a la función con la que se usa pero de una manera especial.

El primer argumento es un vínculo al objeto this (cubierto en el título this & Object Prototypes de esta serie), que no nos importa aquí, así que lo ponemos a nulo. El segundo argumento se supone que es un array (o algo así como un array — también conocido como “objeto parecedi a un array”). Los contenidos de este “array” se “extienden” como argumentos a la función en cuestión.

Por lo tanto, Array.apply(...) está llamando a la función Array(...) y extendiendo los valores (del valor { length: 3 } del objeto ) como sus argumentos.

Dentro de apply(...), podemos imaginarnos que hay otro bucle for (algo así como el join(...) de arriba) que va desde 0 hasta, pero sin incluir, length (3 en nuestro caso).

Para cada índice, recupera esa clave del objeto. Así que si el parámetro array-objeto se denominara arr internamente dentro de la función apply(..), el acceso a la propiedad sería efectivamente arr[0], arr[1], y arr[2]. Por supuesto, ninguna de esas propiedades existe en el valor del objeto { length: 3 }, por lo que los tres accesos a esas propiedades devolverían el valor indefinido.

En otras palabras, termina llamando a Array(...) básicamente así: Array (indefinido, indefinido, indefinido), que es como terminamos con un array lleno de valores indefinidos, y no sólo esos (locos) espacios vacíos.

Mientras que Array.apply( null, { length: 3 }) es una forma extraña y verbosa de crear un array lleno de valores indefinidos, es mucho mejor y más fiable que lo que se obtiene con las posiciones vacías de la forma Array(3).

En resumen: nunca, bajo ninguna circunstancia, debe crear y utilizar intencionadamente estos exóticos arreglos de posiciones vacías. Pero no lo hagas. Están locos.

Object(..), Function(..) y RegExp(..)

Los constructores Object(...)/Function(...)/RegExp(...) también son generalmente opcionales (y por lo tanto deben evitarse a menos que se requiera específicamente):

No hay prácticamente ninguna razón para usar el constructor new Object(), especialmente porque obliga a agregar propiedades una por una en lugar de muchas a la vez en la forma literal del objeto.

El constructor Function es útil sólo en los casos más raros, donde es necesario definir dinámicamente los parámetros de una función y/o su cuerpo de función. No se limite a tratar a Function(...) como una forma alternativa de eval(...). Casi nunca necesitará definir dinámicamente una función de este modo.

Las expresiones regulares definidas en forma literal (/^a*b+/g) son las preferidas, no sólo por la facilidad de sintaxis sino también por razones de rendimiento – el motor JS las precompila y almacena en caché antes de la ejecución del código. A diferencia de las otras formas constructoras que hemos visto hasta ahora, RegExp(...) tiene alguna utilidad razonable: definir dinámicamente el patrón para una expresión regular.

Este tipo de escenario ocurre legítimamente en los programas de JS de vez en cuando, por lo que necesitaría usar la forma new RegExp("pattern", "flags").

Date(..) y Error(..)

Los constructores nativos Date(..) y Error(..) son mucho más útiles que los otros nativos, porque no hay forma literal para ninguno de ellos.

Para crear un valor de objeto fecha, debe utilizar new Date(). El constructor Date(..) acepta argumentos opcionales para especificar la fecha/hora a usar, pero si se omite, se asume la fecha/hora actual.

La razón más común por la que se construye un objeto de fecha es para obtener el valor actual de la marca de tiempo (un número entero firmado de milisegundos desde el 1 de enero de 1970). Puede hacer esto llamando a getTime()en una instancia de objeto date.

Pero una forma aún más fácil es llamar a la función de ayuda estática definida a partir de ES5: Date.now(). Y un polifyll para pre-ES5 es bastante fácil:

Nota: Si llama a Date() sin new, obtendrá una representación de cadena de la fecha/hora en ese momento. La forma exacta de esta representación no está especificada en la especificación del lenguaje, aunque los navegadores tienden a ponerse de acuerdo en algo parecido: "Fri Jul 18 2014 00:31:02 GMT-0500 (CDT)".

El constructor Error(..) (muy parecido al Array() anterior) se comporta de la misma manera con la palabra clave new presente u omitida.

La razón principal por la que querría crear un objeto de error es que captura el contexto de la pila de ejecución actual en el objeto (en la mayoría de los motores JS, revelado como una propiedad .stack de sólo lectura una vez construida). Este contexto de pila incluye la función llamada a pila(stack) y el número de línea donde se creó el objeto de error, lo que facilita mucho la depuración de ese error.

Normalmente se utiliza un objeto de error de este tipo con el operador de lanzamiento:

Las instancias de objetos de error generalmente tienen al menos una propiedad message, y a veces otras propiedades (que debería tratar como de sólo lectura), como type. Sin embargo, aparte de inspeccionar la propiedad .stack mencionada anteriormente, normalmente es mejor llamar toString() en el objeto de error (ya sea explícita o implícitamente a través de coacción — vea el Capítulo 4) para obtener un mensaje de error de formato amigable.

Consejo: Técnicamente, además del nativo general de Error(..), hay varios otros nativos de tipo específico de error: EvalError(..), RangeError(..), ReferenceError(..), SyntaxError(..), TypeError(..), y URIError(..). Pero es muy raro usar manualmente estos nativos de error específicos. Se utilizan automáticamente si su programa sufre realmente de una excepción real (como hacer referencia a una variable no declarada y obtener un error ReferenceError).

Symbol(..)

A partir de ES6, se ha añadido un tipo de valor primitivo adicional, llamado “Símbolo”. Los símbolos son valores especiales “únicos” (¡no estrictamente garantizados!) que pueden ser utilizados como propiedades en objetos con poco miedo a cualquier colisión. Están diseñados principalmente para comportamientos especiales incorporados de construcciones ES6, pero también puede definir sus propios símbolos.

Los símbolos se pueden utilizar como nombres de propiedad, pero no se puede ver ni acceder al valor real de un símbolo desde el programa ni desde la consola del desarrollador. Si evalúa un símbolo en la consola de desarrollo, lo que se muestra se parece a Symbol(Symbol.create), por ejemplo.

Hay varios símbolos predefinidos en ES6, a los que se accede como propiedades estáticas del objeto de función Symbol como Symbol.create, Symbol.iterator, etc. Para usarlos, haz algo como:

Para definir sus propios símbolos personalizados, utilice el nativo Symbol(..). El “constructor” nativo de Symbol(...) es único en el sentido de que no se puede usar new con él, ya que al hacerlo se producirá un error.

Mientras que los símbolos no son realmente privados (Object.getOwnPropertySymbols(..) reflexiona sobre el objeto y revela los símbolos públicamente), su uso para propiedades privadas o especiales es probablemente su principal caso de uso. Para la mayoría de los desarrolladores, pueden tomar el lugar de los nombres de las propiedades con _ prefijos subrayados, que son casi siempre, por convención, usados para decir, “hey, esto es una propiedad privada / especial / interna, así que déjalo en paz!”

Nota: Los símbolos no son objetos, son simples primitivos escalares.

Prototipos Nativos

Cada uno de los constructores nativos incorporados tiene su propio objeto .prototypeArray.prototype, String.prototype, etc.

Estos objetos contienen un comportamiento único para su subtipo de objeto particular.

Por ejemplo, todos los objetos string, y por extensión (a través del empaquetado) los primitivos string, tienen acceso al comportamiento por defecto según los métodos definidos en el objeto String.prototype.

Nota: Por convención de documentación, String.prototype.XYZ se acorta a String#XYZ, y del mismo modo para todos los demás prototipos.

  • String#indexOf(...): busca la posición en la cadena de otra subcadena
  • String#charAt(...): accede al carácter en una posición de la cadena
  • String#substring(...), String#substring(...), y String#slice(..): extrae una parte de la cadena como nueva cadea
  • String#toUpperCase() y String#toLowerCase(): crea una nueva cadena que se convierte a mayúsculas o minúsculas.
  • String#trim(): crea una nueva cadena que está despojada de cualquier espacio en blanco anterior o posterior.

Ninguno de los métodos modifica la cadena en su lugar. Las modificaciones (como la conversión de casos o el recorte) crean un nuevo valor a partir del valor existente.

En virtud de la delegación de prototipos (ver el título this & Object Prototypes en esta serie), cualquier valor de cadena puede acceder a estos métodos:

Los otros prototipos del constructor contienen comportamientos apropiados a sus tipos, tales como Number#toFixed(..) (encadenando un número con un número fijo de dígitos decimales) y Array#concat(..)(combinando matrices). Todas las funciones tienen acceso a apply(..), call(..), y bind(..) porque Function.prototype las define.

Pero, algunos de los prototipos nativos no son simples objetos:

Una idea particularmente mala, puede incluso modificar estos prototipos nativos (no sólo añadiendo propiedades como probablemente conoce):

Como puede ver, Function.prototype es una función, RegExp.prototype es una expresión regular, y Array.prototype es un array. Interesante y genial, ¿eh?

Prototipos por defecto

Function.prototype es una función vacía, RegExp.prototype es una expresión regular “vacía” (por ejemplo, no coincidente), y Array.prototype es una matriz vacía, por lo que todos ellos son valores “por defecto” agradables para asignar a variables si esas variables no hubieran tenido ya un valor del tipo apropiado.

Por ejemplo:

Nota: A partir de ES6, ya no es necesario utilizar el truco de sintaxis de valores por defecto vals = vals || ..(ver Capítulo 4), ya que los valores por defecto se pueden establecer para los parámetros a través de la sintaxis nativa en la declaración de funciones (ver Capítulo 5).

Un beneficio secundario menor de este enfoque es que los prototipos ya están creados e incorporados, por lo que sólo se crean una vez. Por el contrario, usar valores [], function(){}, y /(?:)/ para esos valores por defecto (probablemente, dependiendo de las implementaciones del motor) sería recrear esos valores (y probablemente recogerlos más tarde) para cada llamada de isThisCool(...). Eso podría ser un desperdicio de memoria/CPU.

Además, tenga mucho cuidado de no utilizar Array.prototype como valor predeterminado que será modificado posteriormente. En este ejemplo, vals se usa sólo de lectura, pero si en su lugar hicieras cambios en vals, estarías modificando el propio Array.prototype, ¡lo que llevaría a los problemas mencionados anteriormente!

Nota: Mientras estamos señalando estos prototipos nativos y algo de utilidad, ten cuidado de confiar en ellos y más aún de modificarlos de cualquier manera. Consulte el Apéndice A “Prototipos nativos” para obtener más información.

Revisión


JavaScript proporciona envolturas de objetos alrededor de valores primitivos, conocidos como nativos (String, Number, Boolean, etc). Estas envolturas de objetos dan acceso a los valores a comportamientos apropiados para cada subtipo de objeto (String#trim() y Array#concat(...)).

Si tiene un valor primitivo escalar simple como “abc” y accede a su propiedad longitud o a algún método String.prototype, JS automáticamente “empaqueta” el valor (lo envuelve en su respectiva envoltura de objeto) para que se pueda tener acceso a las propiedads/métodos.


Capítulo 4: Coerción


Ahora que entendemos mucho mejor los tipos y valores de JavaScript, centramos nuestra atención en un tema muy controvertido: la coerción/coacción.

Como mencionamos en el Capítulo 1, los debates sobre si la coerción es una característica útil o un defecto en el diseño del lenguaje (¡o algo intermedio!) se han desatado desde el primer día. Si has leído otros libros populares sobre JS, sabes que el mensaje que prevalece abrumadoramente es que la coerción es mágica, malvada, confusa y simplemente una mala idea.

En el mismo espíritu general de esta serie de libros, en lugar de huir de la coerción porque todos los demás lo hacen, o porque te muerde alguna rareza, creo que deberías correr hacia lo que no entiendes y tratar de entenderlo más plenamente.

Nuestra meta es explorar completamente los pros y los contras (¡sí, hay pros y contras!) de la coerción, para que usted pueda tomar una decisión informada sobre su conveniencia en su programa.

Conversión de valores


Convertir un valor de un tipo a otro a menudo se llama “type casting”, cuando se hace explícitamente, y “coerción” cuando se hace implícitamente (forzado por las reglas de cómo se usa un valor).

Nota: Puede que no sea obvio, pero las coerciones de JavaScript siempre resultan en uno de los valores primitivos escalares (vea el Capítulo 2), como cadena, número o booleano. No hay coerción que resulte en un valor complejo como objeto o función. El capítulo 3 cubre el “empaquetado”, que envuelve los valores primitivos escalares en sus contrapartes objetuales, pero esto no es realmente coerción en un sentido preciso.

Otra manera de distinguir estos términos es la siguiente: “El “type casting” (o “conversión de tipo”) se produce en lenguajes escritos estáticamente en tiempo de compilación, mientras que el “type coercion” es una conversión en tiempo de ejecución para lenguajes escritos dinámicamente.

Sin embargo, en JavaScript, la mayoría de la gente se refiere a todos estos tipos de conversiones como coerción, por lo que prefiero distinguir entre “coerción implícita” y “coerción explícita”.

La diferencia debería ser obvia: “coerción explícita” es cuando es obvio al mirar el código que una conversión de tipo está ocurriendo intencionalmente, mientras que “coerción implícita” es cuando la conversión de tipo ocurrirá como un efecto secundario menos obvio de alguna otra operación intencional.

Por ejemplo, considere estos dos enfoques de la coerción:

Para b, la coacción que ocurre, ocurre implícitamente, porque el operador + combinado con uno de los operandos que es un valor de cadena (“”) insistirá en que la operación sea una concatenación de cadena (sumando dos cadenas juntas), lo que como efecto secundario (oculto) forzará al valor 42 en a a a ser coaccionado a su equivalente de cadena: “42”.

Por el contrario, la función String(..) hace bastante obvio que está tomando explícitamente el valor en a y coaccionándola a una representación de cadena.

Ambos enfoques logran el mismo efecto: “42” viene de 42. Pero es la forma en que esto está en el centro de los acalorados debates sobre la coerción de JavaScript.

Nota: Técnicamente, hay algunas diferencias de comportamiento más allá de la diferencia estilística. Más adelante en el capítulo, en la sección “Implícitamente: Sección “Strings <–> Numbers”.

Los términos “explícito” e “implícito”, u “obvio” y “efecto secundario oculto”, son relativos.

Si sabes exactamente lo que hace a + "" y lo haces intencionadamente para coaccionar a una cadena, puedes sentir que la operación es lo suficientemente “explícita”. Por el contrario, si nunca has visto la función String(..)utilizada para coaccionar cadenas, su comportamiento puede parecer lo suficientemente oculto como para que te parezca “implícito”.

Pero estamos teniendo esta discusión de “explícito” vs. “implícito” basado en las opiniones probables de un desarrollador devoto de la especificación JS, promedio, razonablemente informado, pero no experto. En cualquier medida en que te encuentres o no encajando perfectamente en ese cubo, necesitarás ajustar tu perspectiva en nuestras observaciones aquí en consecuencia.

Sólo recuerda: a menudo es raro que escribamos nuestro código y seamos los únicos que lo leamos. Incluso si usted es un experto en todos los pormenores de JS, considere cómo se sentirá un compañero de equipo menos experimentado cuando lea su código. ¿Será “explícito” o “implícito” para ellos de la misma manera que lo es para usted?

Operaciones de Valor Abstracto


Antes de que podamos explorar la coerción explícita vs. la coerción implícita, necesitamos aprender las reglas básicas que gobiernan cómo los valores se convierten en una cadena, número o booleano. La especificación ES5 de la sección 9 define varias “operaciones abstractas” (en inglés, fancy spec-speak para “funcionamiento sólo interno”) con las reglas de conversión de valores. Prestaremos especial atención a: ToString, ToNumber y ToBoolean y, en menor medida, ToPrimitive.

ToString

Cuando cualquier valor que no sea de cadena es coaccionado a una representación de cadena, la conversión es manejada por la operación abstracta de ToString en la sección 9.8 de la especificación.

Los valores primitivos incorporados tienen un encadenamiento natural: lo nulo se convierte en “nulo”, lo indefinido se convierte en “indefinido” y lo verdadero se convierte en “verdadero”. Los números se expresan generalmente de la manera natural que uno esperaría, pero como ya discutimos en el Capítulo 2, los números muy pequeños o muy grandes se representan en forma exponencial:

Para objetos regulares, a menos que especifique los suyos propios, el valor predeterminado toString() (ubicado en Object.prototype.toString()) devolverá la [[Class]] interna (ver Capítulo 3), como por ejemplo "[object object]".

Pero como se mostró anteriormente, si un objeto tiene su propio método toString(), y usas ese objeto en forma de cadena, su toString() será llamado automáticamente, y el resultado de la cadena de esa llamada será usado en su lugar.

Nota: La forma en que un objeto es coaccionado a una cadena pasa técnicamente por la operación abstracta de ToPrimitive (especificación ES5, sección 9.1), pero esos matizes se tratan con más detalle en la sección ToNumber más adelante en este capítulo, por lo que los omitiremos aquí.

Las matrices tienen un valor por defecto sustituido toString() que se encadena como la concatenación (cadena) de todos sus valores (cada uno encadenado a sí mismo), con una “,” entre cada valor:

Una vez más, toString() puede llamarse explícitamente, o se llamará automáticamente si se utiliza una no cadena en un contexto de cadena.

“Textualización” de JSON

Otra tarea que parece terriblemente relacionada con ToString es cuando se usa la utilidad JSON.stringify(..)para serializar un valor a un valor de cadena compatible con JSON.

Es importante señalar que esta “textualización” no es exactamente lo mismo que la coerción. Pero como está relacionado con las reglas de ToString anteriores, vamos a tomar una pequeña desviación para cubrir los comportamientos de la transformación a string de JSON aquí.

Para la mayoría de los valores simples, la “stringification” de JSON se comporta básicamente igual que las conversiones toString(), excepto que el resultado de la serialización es siempre una cadena:

Cualquier valor de “JSON-safe” puede ser pasado a string por JSON.stringify(..). ¿Pero qué es JSON-safe? Cualquier valor que se pueda representar válidamente en una representación JSON.

Puede ser más fácil considerar valores que no son seguros para JSON. Algunos ejemplos: indefinidos, funciones, símbolos (ES6+) y objetos con referencias circulares (donde las referencias de propiedades en una estructura de objeto crean un ciclo interminable entre sí). Todos estos valores son ilegales para una estructura estándar de JSON, sobre todo porque no son portables a otros lenguajes que consumen valores de JSON.

La utilidad JSON.stringify(..) omitirá automáticamente los valores de funciones y símbolos indefinidos cuando se encuentre con ellos. Si tal valor se encuentra en una matriz, ese valor es reemplazado por nulo (para que no se altere la información de posición de la matriz). Si se encuentra como una propiedad de un objeto, esa propiedad simplemente será excluida.

Considere:

Pero si intenta JSON.stringify(..) un objeto con referencia(s) circular(es) en él, se lanzará un error.

El stringification de JSON tiene el comportamiento especial de que si un valor de objeto tiene definido un método toJSON(), se llamará primero a este método para obtener un valor a utilizar para la serialización.

Si tiene la intención de encadenar JSON a un objeto que pueda contener valores JSON ilegales, o si sólo tiene valores en el objeto que no son apropiados para la serialización, debería definir un método toJSON() para él que devuelva una versión JSON-safe del objeto.

Por ejemplo:

Es un error muy común que toJSON() devuelva una representación de string de JSON. Esto es probablemente incorrecto, a menos que quiera pasar a string el string en sí (¡normalmente no!). toJSON() debería devolver el valor regular real (de cualquier tipo) que sea apropiado, y JSON.stringify(...) en sí mismo se encargará de la encadenamiento.

En otras palabras, toJSON() debe interpretarse como “a un valor seguro de JSON adecuado para la stringificación”, no “a una cadena de JSON” como muchos desarrolladores suponen erróneamente.

Considere:

En la segunda llamada, pasamos a string la cadena devuelta en lugar de la matriz en sí, lo que probablemente no era lo que queríamos hacer.

Mientras hablamos de JSON.stringify(...), comentemos de algunas funcionalidades menos conocidas que todavía pueden ser muy útiles.

Un segundo argumento opcional puede ser pasado a JSON.stringify(..) llamado sustituto. Este argumento puede ser una matriz o una función. Se utiliza para personalizar la serialización recursiva de un objeto proporcionando, un mecanismo de filtrado para el cual las propiedades deben y no deben ser incluidas, de forma similar a como toJSON() puede preparar un valor para la serialización.

Si el sustituto es una matriz, debe ser una matriz de cadenas, cada una de las cuales especificará un nombre de propiedad que se puede incluir en la serialización del objeto. Si existe una propiedad que no está en esta lista, se omitirá.

Si el sustituto es una función, se llamará una vez para el objeto mismo, y luego una vez para cada propiedad en el objeto, y cada vez se pasan dos argumentos, clave y valor. Para omitir una clave en la serialización, el retorno es indefinido. De lo contrario, devuelva el valor proporcionado.

Nota: En el caso del sustituto de funciones, el argumento clave k es indefinido para la primera llamada (donde se está pasando el propio objeto). La sentencia if filtra la propiedad "c". La cadena de caracteres es recursiva, por lo que el array [1,2,3] tiene cada uno de sus valores (1, 2 y 3) pasados como v al sustituto, con índices (0, 1 y 2) como k.

Un tercer argumento opcional también puede pasarse a JSON.stringify(..), llamado espacio, que se utiliza como indentación para una salida más bonita y amigable para el ser humano. el espacio puede ser un entero positivo para indicar cuántos caracteres de espacio se deben utilizar en cada nivel de indentación. O bien, el espacio puede ser una cadena, en cuyo caso se utilizarán hasta los primeros diez caracteres de su valor para cada nivel de sangría.

Recuerde, JSON.stringify(...) no es directamente una forma de coerción. Lo tratamos aquí, sin embargo, por dos razones que relacionan su comportamiento con la coerción de ToString:

  1. Todos los valores string, number, booleanos y null stringifican para JSON básicamente de la misma manera que coaccionan a valores string a través de las reglas de la operación abstracta ToString.
  2. Si pasa un valor de objeto a JSON.stringify(...), y ese objeto tiene un método toJSON(), toJSON()se le llama automáticamente para “coaccionar” el valor a ser JSON-safe antes del encadenamiento.

ToNumber

Si cualquier valor no numérico se usa de una manera que requiere que sea un número, tal como una operación matemática, la especificación ES5 define la operación abstracta ToNumber en la sección 9.3.

Por ejemplo, true se convierte en 1 y false en 0. undefined se convierte en NaN, pero (curiosamente) null se convierte en 0.

ToNumber para un valor de cadena esencialmente funciona en su mayor parte como las reglas/sintaxis para literales numéricos (ver Capítulo 3). Si falla, el resultado es NaN (en lugar de un error sintáctico como en los literales numéricos). Un ejemplo de diferencia es que los números octales con el prefijo 0 no se manejan como octales (igual que los decimales normales de base-10) en esta operación, aunque dichos octales son válidos como literales numéricos (ver Capítulo 2).

Nota: Las diferencias entre la gramática literal de números y ToNumber en un valor de cadena son sutiles y tienen muchos matices, y por lo tanto no se tratarán más adelante. Consulte la sección 9.3.1 de la especificación ES5 para obtener más información.

Los objetos (y matrices) primero serán convertidos a su valor primitivo equivalente, y el valor resultante (si es un primitivo pero no ya un número) es coaccionado a un número de acuerdo a las reglas de ToNumber que se acaban de mencionar.

Para convertir a este valor primitivo equivalente, la operación abstracta ToPrimitive (especificación ES5, sección 9.1) consultará el valor (usando la operación interna DefaultValue — especificación ES5, sección 8.12.8) en cuestión para ver si tiene un método valueOf(). Si está disponible valueOf() y devuelve un valor primitivo, ese valor se usa para la coacción. Si no, pero está disponible toString(), proporcionará el valor para la coerción.

Si ninguna de las dos operaciones puede proporcionar un valor primitivo, se lanza un TypeError.

A partir de ES5, puede crear un objeto no coercible — uno sin valuerOf() y toString() — si tiene un valor nulo para su [[Prototype]]], típicamente creado con Object.create(null). Vea el título This & Object Prototypes de esta serie para más información sobre [[Prototype]].

Nota: Cubrimos cómo coaccionar a los números más adelante en este capítulo en detalle, pero para este siguiente fragmento de código, sólo asume que la función Number(..) lo hace.

Considere:

ToBoolean

A continuación, vamos a tener una pequeña charla sobre cómo se comportan los booleanos en JS. Hay mucha confusión y conceptos erróneos flotando alrededor de este tema, ¡así que preste mucha atención!

En primer lugar, JS tiene palabras clave true y false, y se comportan exactamente como uno esperaría de los valores booleanos. Es un error común creer que los valores 1 y 0 son idénticos a verdadero/falso. Mientras que eso puede ser cierto en otros idiomas, en JS los números son números y los booleanos son booleanos. Puede coaccionar de 1 a verdadero (y viceversa) o de 0 a falso (y viceversa). Pero no son lo mismo.

Valores Falsos

Pero ese no es el final de la historia. Necesitamos discutir cómo se comportan otros valores además de los dos booleanos cada vez que coaccionas a su equivalente booleano.

Todos los valores de JavaScript se pueden dividir en dos categorías:

  1. valores que se volverán falsos si se les coacciona a los booleanos
  2. todo lo demás (que obviamente se hará realidad)

No estoy bromeando. La especificación JS define una lista específica y estrecha de valores que coaccionarán a falsecuando se les coaccione a un valor booleano.

¿Cómo sabemos cuál es la lista de valores? En la especificación ES5, la sección 9.2 define una operación abstracta ToBoolean, que dice exactamente qué sucede con todos los valores posibles cuando se intenta forzarlos “a booleanos”.

De esa tabla, obtenemos lo siguiente como la llamada lista de valores “falsy”:

  • undefined
  • nulo
  • false
  • 0, -0 y NaN
  • ""

Eso es todo. Si un valor está en esa lista, es un valor “falsy”, y coaccionará a false si fuerzas una coacción booleana sobre él.

En conclusión lógica, si un valor no está en esa lista, debe estar en otra lista, que llamamos la lista de valores “truthy”. Pero JS no define realmente una lista “truthy” per se. Da algunos ejemplos, como decir explícitamente que todos los objetos son true, pero la mayoría de las veces la especificación sólo implica: cualquier cosa que no esté explícitamente en la lista false es, por lo tanto, true.

Objetos Falsos

Espera un minuto, ese título de la sección incluso suena contradictorio. Literalmente acabo de decir que la especificación llama a todos los objetos truthy, ¿verdad? No debería haber tal cosa como un “objeto falsy”.

¿Qué podría significar eso?

Usted podría estar tentado a pensar que significa un envoltorio de objeto (vea el Capítulo 3) alrededor de un valor falsy (como "", 0 o false). Pero no caigas en esa trampa.

Nota: Esa es una broma sutil de la especificación que algunos de ustedes pueden entender.

Considere:

Sabemos que los tres valores aquí son objetos (ver Capítulo 3) envueltos alrededor de valores obviamente falsos. Pero, ¿se comportan estos objetos como verdaderos o como falsos? Eso es fácil de responder:

Por lo tanto, los tres se comportan como verdaderos, ya que esa es la única manera en que d podría terminar como verdadero.

Consejo: Fíjate en el Boolean(..) envuelto alrededor de la expresión a && b && c — tal vez te preguntes por qué está ahí. Volveremos a eso más adelante en este capítulo, así que anótelo mentalmente. Para una mirada furtiva (trivial), prueba por ti mismo lo que será d si lo haces d = a && b && c sin la llamada Boolean(..)!

Entonces, si los “objetos falsos” no son sólo objetos envolviendo valores falsos, ¿qué diablos son?

La parte difícil es que pueden aparecer en su programa JS, pero en realidad no son parte de JavaScript en sí.

¿¡Qué!?

Hay ciertos casos en los que los navegadores han creado su propio tipo de comportamiento de valores exóticos, a saber, esta idea de “objetos falsy”, además de la semántica regular de JS.

Un “objeto falsy” es un valor que parece y actúa como un objeto normal (propiedades, etc.), pero cuando lo coaccionas a un booleano, coacciona a un valor falso.

¿¡Por qué!?

El caso más conocido es document.all: un array (objeto) proporcionado a su programa JS por el DOM (no el motor JS en sí mismo), que expone los elementos de su página a su programa JS. Solía comportarse como un objeto normal… actuaría como true. Pero ya no más.

document.all en sí mismo nunca fue realmente “estándar” y desde hace mucho tiempo ha sido obsoleto/abandonado.

“¿No pueden quitarlo, entonces?” Lo siento, buen intento. Ojalá pudieran. Pero hay demasiadas bases de código JS heredadas que dependen de su uso.

Entonces, ¿por qué hacer que actúe como si fuera falso? Porque las coacciones de document.all a booleano (como en el caso de las sentencias) casi siempre se utilizaban como medio para detectar IE antiguos y no estándar.

IE ha cumplido con los estándares desde hace mucho tiempo, y en muchos casos está haciendo avanzar la web tanto o más que cualquier otro navegador. Pero todo ese código antiguo if (document.all) { /* es IE *//} sigue ahí fuera, y gran parte de él probablemente nunca desaparecerá. Todo este código heredado sigue asumiendo que se está ejecutando en IE desde hace una década, lo que sólo conduce a una mala experiencia de navegación para los usuarios de IE.

Por lo tanto, no podemos eliminar document.all completamente, pero IE no quiere que ìf (document.all) { .. } funcione nunca más para que los usuarios en el IE moderno obtengan una lógica de código nueva que cumpla con los estándares.

“¿Qué debemos hacer?” “¡Lo tengo! Vamos a hacer ‘bastardo’ el sistema de tipo JS y fingir que document.all es falso!”

Ugh. Eso apesta. Es una locura que la mayoría de los desarrolladores de JS no entienden. Pero la alternativa (no hacer nada acerca de los problemas antes mencionados) apesta un poco más.

Así que… eso es lo que tenemos: locos y no estándar “falsy objects” añadidos a JavaScript por los navegadores. Yay!

Valores verdaderos

Volvamos a la lista de valores true. ¿Cuáles son exactamente los valores true? Recuerde: un valor es verdadero si no está en la lista false.

Considere:

¿Qué valor esperas que d tenga aquí? Tiene que ser true o false.

Es true. Por qué? Porque a pesar de que el contenido de esos valores de cadena se parece a los valores falsy, los valores de cadena en sí son todos verdaderos, porque "" es el único valor de cadena en la lista falsy.

¿Qué hay de estos?

Sí, lo adivinaste, d sigue siendo true aquí. Por qué? Por la misma razón que antes. A pesar de lo que pueda parecer, [], {}, y function(){} no están en la lista falsy, y por lo tanto son valores true.

En otras palabras, la lista truthy es infinitamente larga. Es imposible hacer esa lista. Sólo puedes hacer una lista falsy finita y consultarla.

Tómese cinco minutos, escriba la lista falsy en una nota adhesiva para el monitor de su computadora, o memorícela si lo prefiere. De cualquier manera, usted podrá fácilmente construir una lista truthy virtual cuando la necesite, simplemente preguntándose si está en la lista falsa o no.

La importancia de la true y false está en entender cómo se comportará un valor si lo coaccionas (explícita o implícitamente) a un valor booleano. Ahora que usted tiene esas dos listas en mente, podemos sumergirnos en ejemplos de coerción.

Coerción explícita


La coerción explícita se refiere a las conversiones de tipo que son obvias y explícitas. Existe una amplia gama de usos de conversión de tipos que claramente caen dentro de la categoría de coerción explícita para la mayoría de los desarrolladores.

El objetivo aquí es identificar patrones en nuestro código donde podamos dejar claro y obvio que estamos convirtiendo un valor de un tipo a otro, para no dejar baches en los que puedan tropezar los futuros desarrolladores. Cuanto más explícitos seamos, más probable es que más tarde alguien pueda leer nuestro código y comprender sin un esfuerzo extra cuál era nuestra intención.

Sería difícil encontrar desacuerdos significativos con coerción explícita, ya que se alinea más estrechamente con la forma en que funciona la práctica comúnmente aceptada de la conversión de tipos en lenguajes estáticamente tipados. Como tal, daremos por sentado (por ahora) que la coerción explícita puede ser acordada para no ser malvada o controvertida. Volveremos a hablar de esto más tarde.

Explícitamente: Cadenas <–> Números

Comenzaremos con la operación de coerción más simple y quizás la más común: coaccionar valores entre la representación de cadenas y números.

Para coaccionar entre cadenas y números, usamos las funciones incorporadas String(..) y Number(..) (a las que nos referimos como “constructores nativos” en el Capítulo 3), pero lo más importante es que no usamos la palabra clave new delante de ellas. Como tal, no estamos creando envolturas de objetos.

En lugar de eso, estamos coaccionando explícitamente entre los dos tipos:

String(..) coacciona desde cualquier otro valor a un valor de cadena primitivo, usando las reglas de la operación ToString discutida anteriormente. Number(..) coacciona desde cualquier otro valor a un valor numérico primitivo, usando las reglas de la operación ToNumber discutida anteriormente.

Llamo a esto coerción explícita porque, en general, es bastante obvio para la mayoría de los desarrolladores que el resultado final de estas operaciones es la conversión de tipo aplicable.

De hecho, este uso se parece mucho al que se hace en otros lenguajes de programación estáticos.

Por ejemplo, en C/C++, puedes decir (int)x o int(x), y ambos convertirán el valor en x a un entero. Ambas formas son válidas, pero muchos prefieren la segunda, que parece una llamada de función. En JavaScript, cuando dices Number(x), se ve terriblemente similar. ¿Importa que en realidad sea una llamada a una función en JS? La verdad es que no.

Además de String(...) y Number(...), hay otras formas de convertir estos valores entre cadena y número:

Llamar a.toString() es ostensiblemente explícito (bastante claro que “toString” significa “a una cadena”), pero hay cierta implicidad oculta aquí. toString() no puede ser llamado con un valor primitivo como 42. Por lo tanto, JS automáticamente “empaqueta” (ver Capítulo 3) 42 en una envoltura de objeto, de modo que toString() puede ser llamado contra el objeto. En otras palabras, podría llamarse “explícitamente implícito”.

+c aquí se muestra la forma de operador unario (operador con un solo operando) del operador +. En lugar de realizar una suma matemática (o concatenación de cadenas — ver abajo), el unario + coacciona explícitamente a su operando (c) a un valor numérico.

¿Es +c una coerción explícita? Depende de tu experiencia y perspectiva. Si sabes (¡lo que sabes, ahora!) que el unario + está explícitamente destinado a la coerción numérica, entonces es bastante explícito y obvio. Sin embargo, si usted nunca lo ha visto antes, puede parecer terriblemente confuso, implícito, con efectos secundarios ocultos, etc.

Nota: La perspectiva generalmente aceptada en la comunidad JS de código abierto es que el unario + es una forma aceptada de coerción explícita.

Incluso si realmente te gusta la forma +c, definitivamente hay lugares donde puede parecer terriblemente confuso. Considere:

El operador unario - también coacciona como lo hace +, pero también invierte el signo del número. Sin embargo, no se pueden poner dos, uno al lado del otro, para dar la vuelta al signo, ya que se analiza como el operador de decremento. En su lugar, tendría que hacer: - - "3.14" con un espacio intermedio, y eso resultaría en coerción a 3.14.

Probablemente puedes inventar todo tipo de combinaciones horribles de operadores binarios (como + por adición) junto a la forma unaria de un operador. Aquí hay otro ejemplo loco:

Debe considerar evitar la coerción con el unario + (o -) cuando está inmediatamente adyacente a otros operadores. Aunque lo anterior funciona, casi universalmente se consideraría una mala idea. Incluso d = +c (o d =+ c para lo mismo!) puede ser fácilmente confundido con d += c, que es completamente diferente!

Nota: Otro lugar extremadamente confuso para el unario + para ser usado adyacente a otro operador sería el operador de incremento ++ y el operador de decremento --. Por ejemplo: a +++b, a + ++b, y a + + +b. Consulte “Efectos secundarios de expresiones” en el Capítulo 5 para obtener más información sobre ++.

Recuerde, estamos tratando de ser explícitos y reducir la confusión, ¡no empeorarla!

Fecha a número

Otro uso común del operador unario + es coaccionar a un objeto Date para que se convierta en un número, ya que el resultado es la representación del valor fecha/hora mediante la marca de tiempo unix (milisegundos transcurridos desde el 1 de enero de 1970 a las 00:00:00 UTC):

El uso más común de este modismo es obtener el momento actual como una marca de tiempo, por ejemplo:

Nota: Algunos desarrolladores son conscientes de un peculiar “truco” sintáctico en JavaScript, que son los ()establecidos en una llamada de constructor (una función llamada con new) es opcional si no hay argumentos para pasar. Por lo tanto, puede usar la forma var timestamp = +new Date;. Sin embargo, no todos los desarrolladores están de acuerdo en que omitir () mejora la legibilidad, ya que es una excepción sintáctica poco común que sólo se aplica a la llamada de tipo new fn() y no a la llamada de tipo fn() normal.

Pero la coacción no es la única manera de obtener la marca de tiempo de un objeto Date. Un enfoque de no coacción es quizás incluso preferible, ya que es aún más explícito:

Pero una opción aún más preferible es usar la función estática Date.now() añadida por ES5:

Y si quieres hacer un relleno “polifyll” de Date.now() en navegadores antiguos, es bastante simple:

Recomendaría saltarse las formas de coerción relacionados confechas. Utilice Date.now() para las marcas de hora actuales y Date(..).getTime() para obtener una marca de hora de una fecha/hora específica no actual que necesite especificar.

El curioso caso del ~

Un operador coercitivo de JS que a menudo se pasa por alto y que suele ser muy confuso es el operador “tilde” ~(también conocido como “bitwise NOT” (NO bit)). Muchos de los que incluso entienden lo que hace a menudo todavía prefieren evitarlo. Pero apegándonos al espíritu de nuestro enfoque en este libro y en esta serie, escarbemos en él para averiguar si ~ tiene algo útil que darnos.

En la sección “Números enteros (con signo) de 32 bits” del Capítulo 2, cubrimos cómo los operadores de bits en JS se definen sólo para operaciones de 32 bits, lo que significa que obligan a sus operandos a ajustarse a representaciones de valores de 32 bits. Las reglas de cómo ocurre esto están controladas por la operación abstracta ToInt32(especificación ES5, sección 9.5).

ToInt32 primero hace una coacción de ToNumber, lo que significa que si el valor es “123”, primero se convertirá en 123 antes de que se apliquen las reglas de ToInt32.

Aunque técnicamente no es coerción en sí (¡ya que el tipo no cambia!), usar operadores de bits (como | o ~) con ciertos valores numéricos especiales produce un efecto coercitivo que resulta en un valor numérico diferente.

Por ejemplo, consideremos primero el operador | “bit OR” que (como se mostró en el Capítulo 2) esencialmente sólo hace la conversión a ToInt32:

Estos números especiales no son representativos de 32 bits (ya que provienen del estándar IEEE 754 de 64 bits — ver Capítulo 2), por lo que ToInt32 sólo especifica 0 como resultado de estos valores.

Es discutible si 0 | __ es una forma explícita de esta operación coercitiva de ToInt32 o si es más implícita. Desde la perspectiva de la especificación, es incuestionablemente explícito, pero si no entiendes las operaciones de bits a este nivel, puede parecer un poco más implícitamente mágico. Sin embargo, en consonancia con otras afirmaciones de este capítulo, lo llamaremos explícito.

Por lo tanto, volvamos a centrar nuestra atención en ~. El operador ~ primero “coacciona” a un valor numérico de 32 bits, y luego realiza una negación a bit (invirtiendo la paridad de cada bit).

Nota: Esto es muy similar a cómo ! no sólo coacciona su valor a booleano, sino que también invierte su paridad (ver discusión del “unario !” más adelante).

Pero… ¿qué? ¿Por qué nos preocupan los bits que se invierten? Eso es algo bastante especializado, con matices. Es bastante raro que los desarrolladores de JS necesiten razonar sobre bits individuales.

Otra forma de pensar acerca de la definición de ~ viene de la vieja escuela de ciencias de la computación/matemáticas discretas: realiza el complemento de dos. Genial, gracias, ¡está totalmente claro!

Intentémoslo de nuevo: ~x es aproximadamente lo mismo que -(x+1). Eso es raro, pero un poco más fácil de razonar. Así que:

Probablemente te sigas preguntando de qué diablos trata este ~, o por qué realmente importa para una discusión de coacción. Vayamos rápidamente al grano.

Considere -(x+1). ¿Cuál es el único valor en el que puede realizar esa operación que producirá un resultado 0 (o -0 técnicamente!)? -1. En otras palabras, ~ usado con un rango de valores numéricos producirá un valor falsy (fácilmente coercionable a falso), 0 para el valor de entrada -1 y cualquier otro número verdadero de otra manera.

¿Por qué es eso relevante?

-1 es comúnmente llamado “valor centinela”, que básicamente significa un valor al que se le ha dado un significado semántico arbitrario dentro del mayor conjunto de valores de su mismo tipo (números). El lenguaje C utiliza el valor centinela -1 para muchas funciones que devuelven valores >= 0 para “éxito” y -1 para “fracaso”.

JavaScript adoptó este precedente al definir la operación de cadena indexOf(..), que busca una subcadena y, si se encuentra, devuelve su posición de índice basada en cero, ó -1 si no se encuentra.

Es bastante común intentar usar indexOf(...) no sólo como una operación para obtener la posición, sino como una comprobación booleana de la presencia/ausencia de una subcadena en otra cadena. Así es como los desarrolladores suelen realizar estas comprobaciones:

Me parece un poco feo el mirar >= 0 ó == -1. Es básicamente una “abstracción fugaz”, en el sentido de que está filtrando el comportamiento de implementación subyacente — el uso de centinela -1 para “fracaso” — en mi código. Preferiría ocultar ese detalle.

Y ahora, finalmente, vemos por qué ~ ¡podría ayudarnos! Usando ~ con indexOf() “coacciona” (en realidad sólo transforma) el valor para que sea apropiadamente booleano coercible:

~ toma el valor de retorno de indexOf(...) y lo transforma: para el -1 en “fallo” obtenemos el falsy 0, y cualquier otro valor es verdadero.

Nota: El pseudo-algoritmo -(x+1) para ~ implicaría que ~-1 es -0, pero en realidad produce 0 porque la operación subyacente es en realidad de bit, no matemática.

Técnicamente, si (~a.indexOf(...)) sigue dependiendo de la coerción implícita de su resultante 0 a false o “un no cero” a true. Pero en general, ~ todavía me parece más como un mecanismo de coerción explícito, siempre y cuando sepas lo que se pretende hacer en este lenguaje.

Encuentro que este código es más limpio que el anterior desorden >= 0 / == -1.

Cambiando bits

Hay un lugar más en que ~ puede aparecer en el código con el que te encuentres: algunos desarrolladores usan la doble tilde ~~ para truncar la parte decimal de un número (es decir, “coaccionarlo” a un número “entero”). Comúnmente (aunque erróneamente) se dice que este es el mismo resultado que llamar a Math.floor(..).

Cómo ~~ funciona es que el primer ~ aplica la “coacción” de ToInt32 y hace el salto de bits, y luego el segundo ~hace otro salto de bits, invirtiendo todos los bits al estado original. El resultado final es sólo la “coacción” de ToInt32(también conocida como truncamiento).

Nota: La inversión doble de bits de ~~ es muy similar al comportamiento de paridad de doble negación !!, explicado en la sección “Explícitamente: * –> Boolean” más tarde.

Sin embargo, ~~ necesita algo de precaución/clarificación. En primer lugar, sólo funciona de forma fiable con valores de 32 bits. Pero lo más importante es que no funciona igual que Math.floor(...) en números negativos!

Dejando a un lado la diferencia con Math.floor(...), ~~x puede truncar a un entero (32-bit). Pero también lo hace x | 0, y aparentemente con (ligeramente) menos esfuerzo.

Entonces, ¿por qué elegirías entonces ~~x sobre x | 0? Precedencia del operador (véase el capítulo 5):

Al igual que con todos los demás consejos aquí, utilice ~ y ~~ como mecanismos explícitos para la “coerción” y la transformación de valores sólo si todos los que leen/escriben dicho código son conscientes de cómo funcionan estos operadores!

Explícitamente: Análisis de cadenas numéricas

Un resultado similar al de coaccionar una cadena a un número puede lograrse analizando un número a partir del contenido de los caracteres de una cadena. Hay, sin embargo, diferencias claras entre este análisis y la conversión de tipo que examinamos anteriormente.

Considere:

El análisis (parseInt(..)) de un valor numérico a partir de una cadena es tolerante con los caracteres no numéricos (simplemente deja de analizar de izquierda a derecha cuando se encuentra), mientras que la coacción(Number(..)) no es tolerante y falla dando como resultado el valor NaN.

El análisis no debe ser visto como un sustituto de la coerción. Estas dos tareas, aunque similares, tienen propósitos diferentes. Analice una cadena como un número cuando no sabe o no le importa qué otros caracteres no numéricos puede haber en el lado derecho. Coercione una cadena (a un número) cuando los únicos valores aceptables son numéricos y algo como “42px” debe ser rechazado como un número.

Consejo: parseInt(..) tiene un gemelo, parseFloat(...), que (tal como suena) extrae un número de coma flotante de una cadena.

No olvide que parseInt(..) funciona con valores de cadena. No tiene ningún sentido pasar un valor numérico a parseInt(...). Tampoco tendría sentido pasar ningún otro tipo de valor, como true, function(){..} ó [1,2,3].

Si pasas una “no cadena”, el valor que pasas será automáticamente coaccionado a una cadena primero (ver “ToString” más arriba), lo que claramente sería una especie de coerción implícita oculta. Es una muy mala idea confiar en tal comportamiento en su programa, así que nunca use parseInt(..) con un valor que no sea de cadena.

Antes de ES5, existía otro problema con parseInt(..), que era el origen de muchos errores de los programas de JS. Si no pasaste un segundo argumento para indicar qué base numérica (o radix) usar para interpretar el contenido de la cadena numérica, parseInt(...) miraría a los caracteres iniciales para hacer una suposición.

Si los dos primeros caracteres eran “0x” o “0X”, la suposición (por convención) era que querías interpretar la cadena como un número hexadecimal (base-16). De lo contrario, si el primer carácter era “0”, la suposición (de nuevo, por convención) era que querías interpretar la cadena como un número octal (base 8).

Las cadenas hexadecimales (con el 0x ó 0X delante) no son muy fáciles de mezclar. Pero el número octal resultó ser muy común. Por ejemplo:

Parece inofensivo, ¿verdad? Intente seleccionar 08 para la hora y 09 para el minuto. Tendrás 0:0. ¿Por qué? Porque ni el 8 ni el 9 son caracteres válidos en la base octal 8.

El arreglo pre-ES5 era simple, pero tan fácil de olvidar: siempre pasar 10 como segundo argumento. Esto era totalmente seguro:

A partir de ES5, parseInt(..) ya no adivina octal. A menos que digas lo contrario, asume base-10 (o base-16 para prefijos “0x”). Eso es mucho mejor. Sólo ten cuidado si tu código tiene que ejecutarse en entornos pre-ES5, en cuyo caso todavía tienes que pasar 10 para el radix.

Análisis de no cadenas

Un ejemplo un tanto infame del comportamiento de parseInt(..) se destaca en una broma sarcástica de hace unos años, burlándose de este comportamiento de JS:

La afirmación asumida (pero totalmente inválida) fue: “Si paso en el Infinito, y analizo un entero de eso, debería recuperar el Infinito, no 18.” Seguramente, JS debe estar loco por este resultado, ¿verdad?

Aunque este ejemplo es obviamente artificioso e irreal, permitamos la locura por un momento y examinemos si JS es realmente tan loco.

En primer lugar, el pecado más obvio que se comete aquí es pasar una no-cadena a parseInt(...). Eso es un no-no. Hazlo y te estarás buscando problemas. Pero incluso si lo haces, JS coacciona cortésmente lo que pasas en una cadena que puede tratar de analizar.

Algunos argumentarían que este es un comportamiento irrazonable, y que parseInt(..) debería negarse a operar con un valor no de cadena. ¿Debería acaso arrojar un error? Eso sería muy parecido a Java, francamente. Me estremezco al pensar que JS debería empezar a tirar errores por todas partes para que el try...catch sea necesario alrededor de casi todas las líneas.

¿Debería devolver NaN? Tal vez. Pero… ¿qué hay de…?

¿Debería fallar eso también? Es un valor que es una cadena. Si quiere que la envoltura de ese objeto String se descomponga a “42”, ¿es tan inusual que 42 se convierta en “42” para que 42 pueda ser analizado de nuevo?

Yo diría que esta coerción medio explícita y medio implícita que puede ocurrir a menudo puede ser muy útil. Por ejemplo:

El hecho de que parseInt(...) coaccione por la fuerza su valor a una cadena para que ejecute el análisis es muy sensato. Si usted entra en la basura, y usted consigue sacarla, no culpe al cubo de basura — apenas hizo su trabajo fielmente.

Por lo tanto, si usted pasa en un valor como Infinito (el resultado de 1 / 0 obviamente), ¿qué tipo de representación de cadena tendría más sentido para su coerción? Sólo se me ocurren dos opciones razonables: “Infinity” y “∞”. JS eligió “Infinity”. Me alegro de que lo hiciera.

Creo que es bueno que todos los valores en JS tengan algún tipo de representación de cadena por defecto, para que no sean cajas negras misteriosas que no podamos depurar y razonar.

Ahora, ¿qué hay de la base 19? Obviamente, completamente falso e inventado. Ningún programa real de JS usa base-19. Es absurdo. Pero de nuevo, dejémonos llevar por el ridículo. En base-19, los caracteres numéricos válidos son 09 y ai (insensible a mayúsculas y minúsculas).

Así que, volviendo a nuestro ejemplo de parseInt( 1/0, 19). Es esencialmente parseInt ("Infinito", 19). ¿Cómo se analiza? El primer carácter es “I”, que es el valor 18 en la base tonta-19. El segundo carácter “n” no está en el conjunto válido de caracteres numéricos, y como tal el análisis simplemente se detiene cortésmente, igual que cuando se cruzó con “p” en “42px”.

¿El resultado? 18. Exactamente como debería ser. Las conductas involucradas para llevarnos allí, y no a un error o al Infinito mismo, son muy importantes para JS, y no deben ser descartadas tan fácilmente.

Otros ejemplos de este comportamiento con parseInt(..) que pueden ser sorprendentes pero que son bastante sensatos incluyen:

parseInt(...) es realmente bastante predecible y consistente en su comportamiento. Si lo usas correctamente, obtendrás resultados sensatos. Si lo usas incorrectamente, los resultados locos que obtienes no son culpa de JavaScript.

Explícitamente: * –> Boolean

Ahora, examinemos la coerción de cualquier valor no boleano a un booleano.

Al igual que con String(...) y Number(...) de arriba, Boolean(...) (¡sin new, por supuesto!) es una forma explícita de forzar la coerción ToBoolean:

Mientras que Boolean(..) es claramente explícito, no es en absoluto común o significativo.

Así como el operador unario + coacciona un valor a un número (ver arriba), el operador unario ! de negación coacciona explícitamente un valor a un booleano. El problema es que también cambia el valor de truthy a falsy o viceversa. Por lo tanto, la forma más común en que los desarrolladores de JS coaccionan explícitamente a boleanos es usar el operador doble-negación !!, ya que el segundo ! invertirá la paridad de nuevo al original:

Cualquiera de estas coacciones ToBoolean ocurriría implícitamente sin Boolean(..) ó !!, si se utiliza en un contexto booleano como una sentencia if(..) .. Pero el objetivo aquí es forzar explícitamente el valor a un booleano para hacer más claro que la coacción ToBoolean es intencionada.

Otro ejemplo de caso de uso de la coerción explícita ToBoolean es si se desea forzar una coerción de valor verdadero/falso en la serialización JSON de una estructura de datos:

Si viene a JavaScript desde Java, puede que reconozca este modismo:

El operador ternario probará a para ser true, y basado en esa prueba asignará true o false a b, en consecuencia.

En su superficie, este modismo parece una forma de coerción explícita de tipo ToBoolean, ya que es obvio que sólo verdadero o falso salen de la operación.

Sin embargo, hay una coerción implícita oculta, en el sentido de que la expresión tiene que ser coaccionada primero a boleana para realizar la prueba de veracidad. Yo llamaría a este modismo “explícitamente implícito”. Además, te sugiero que evites este modismo completamente en JavaScript. No ofrece ningún beneficio real y, lo que es peor, se hace pasar por algo que no es.

Boolean(a) y !!a son mucho mejores que las opciones de coerción explícitas.

Coerción implícita


La coerción implícita se refiere a las conversiones de tipos que están ocultas, con efectos secundarios no evidentes que ocurren implícitamente de otras acciones. En otras palabras, las coacciones implícitas son conversiones de cualquier tipo que no son obvias (para usted).

Si bien está claro cuál es el objetivo de la coacción explícita (hacer que el código sea explícito y más comprensible), puede ser demasiado obvio que la coacción implícita tiene el objetivo opuesto: hacer que el código sea más difícil de entender.

Tomado al pie de la letra, creo que de ahí proviene gran parte de la ira hacia la coerción. La mayoría de las quejas sobre la “coerción de JavaScript” están dirigidas (se den cuenta o no) a la coerción implícita.

Nota: Douglas Crockford, autor de “JavaScript: The Good Parts”, ha afirmado en muchas conferencias y escritos que se debe evitar la coerción de JavaScript. Pero lo que parece querer decir es que la coerción implícita es mala (en su opinión). Sin embargo, si lees su propio código, ¡encontrarás muchos ejemplos de coerción, tanto implícita como explícita! En realidad, su angustia parece estar dirigida principalmente a la operación ==, pero como verás en este capítulo, eso es sólo una parte del mecanismo de coerción.

Entonces, ¿la coerción implícita es malvada? ¿Es peligroso? ¿Es un defecto en el diseño de JavaScript? ¿Debemos evitarlo a toda costa?

Apuesto a que la mayoría de los lectores se inclinan a animar con entusiasmo: “¡Sí!”

No tan rápido. Escúchame.

Tomemos una perspectiva diferente sobre lo que es y lo puede ser la coerción implícita, y no sólo que es “lo opuesto a la buena coerción explícita”. Eso es demasiado estrecho y pierde un matiz importante.

Definamos el objetivo de la coerción implícita como: reducir la verbosidad, la repetitividad, y/o los detalles de implementación innecesarios que desordenan nuestro código con ruido que distrae de la intención más importante.

Simplificación implícita

Antes de que lleguemos a JavaScript, permítanme sugerirles un pseudo-código de algún lenguaje teórico fuertemente tipado para ilustrar:

En este ejemplo, tengo algún tipo arbitrario de valor en y que quiero convertir al tipo SomeType. El problema es que este lenguaje no puede pasar directamente de lo que sea que sea y actualmente a SomeType. Necesita un paso intermedio, donde primero se convierte a AnotherType, y luego de AnotherType a SomeType.

Ahora, ¿qué pasaría si ese lenguaje (o definición que podrías crear tú mismo con el lenguaje) te dejara decir:

¿No está de acuerdo en que simplificamos la conversión de tipo aquí para reducir el “ruido” innecesario del paso intermedio de conversión? Quiero decir, ¿es realmente tan importante, justo aquí en este punto del código, ver y lidiar con el hecho de que y va a AnotherType primero antes de ir a SomeType?

Algunos argumentarían, al menos en algunas circunstancias, que sí. Pero creo que se puede argumentar de la misma manera que en muchas otras circunstancias aquí, la simplificación realmente ayuda en la legibilidad del código al abstraer u ocultar tales detalles, ya sea en el propio lenguaje o en nuestras propias abstracciones.

Sin duda, entre bastidores, en alguna parte, el paso intermedio de conversión todavía está ocurriendo. Pero si ese detalle está oculto a la vista aquí, podemos razonar que para que y obtenga el tipo SomeType como una operación genérica y ocultar los detalles sucios.

Aunque no es una analogía perfecta, lo que voy a argumentar a lo largo del resto de este capítulo es que la coerción implícita de JS puede considerarse como una ayuda similar a su código.

Pero, y esto es muy importante, no es una afirmación ilimitada y absoluta. Definitivamente hay muchos males acechando alrededor de la coerción implícita, que dañarán su código mucho más que cualquier mejora potencial en la legibilidad. Claramente, tenemos que aprender a evitar tales construcciones para no envenenar nuestro código con todo tipo de errores.

Muchos desarrolladores creen que si un mecanismo puede hacer alguna cosa útil A pero también puede ser abusado o mal usado para hacer alguna cosa horrible Z, entonces deberíamos tirar ese mecanismo por completo, sólo para estar seguros.

Mi consegjo para ti es: no te conformes con eso. No “tires al bebé con el agua de la bañera”. No asumas que la coerción implícita es mala porque todo lo que crees que has visto es sus “partes malas”. Creo que hay “partes buenas” aquí, y quiero ayudar e inspirar a más de ustedes para que las encuentren y las acepten!

Implícitamente: Cadenas <–> Números

Anteriormente en este capítulo, exploramos explícitamente la coerción entre los valores de cadena y de número. Ahora, exploremos la misma tarea pero con enfoques de coerción implícitos. Pero antes de hacerlo, tenemos que examinar algunos matices de las operaciones que implícitamente fuerza la coerción.

El operador + puede servir tanto para la adición de números como para la concatenación de cadenas. Entonces, ¿cómo sabe JS qué tipo de operación desea utilizar? Considere:

¿Qué es lo que causa la diferencia entre "420" y 42? Es un concepto erróneo común que la diferencia es si uno o ambos operandos es una cadena, ya que eso significa que + asumirá la concatenación de cadenas. Aunque eso es parcialmente cierto, es más complicado que eso.

Considere:

Ninguno de estos operandos es una cadena, pero claramente ambos fueron coaccionados a cadenas y luego la concatenación de cadenas sucedió. Entonces, ¿qué está pasando realmente?

(Advertencia: ¡se acercan espectros muy arenosos, así que salta los siguientes dos párrafos si eso te intimida!)


De acuerdo con la sección 11.6.1 de la especificación ES5, el algoritmo + (cuando un valor de objeto es un operando) concatenará si cualquiera de los operandos ya es una cadena, o si los siguientes pasos producen una representación de cadena. Por lo tanto, cuando + recibe un objeto (incluyendo el array) para cualquiera de los operandos, primero llama a la operación abstracta ToPrimitive (sección 9.1) en el valor, que luego llama al algoritmo [[DefaultValue]]] (sección 8.12.8) con una pista de contexto de número.

Si está prestando mucha atención, notará que esta operación es ahora idéntica a cómo la operación abstracta ToNumber maneja los objetos (vea la sección “ToNumber”). La operación valueOf() en el array fallará en producir un primitivo simple, por lo que entonces cae a una representación de toString(). Las dos matrices se convierten así en "1,2" y "3,4", respectivamente. Ahora, + concatena las dos cadenas como normalmente esperarías: "1,23,4".


Dejemos a un lado esos detalles confusos y volvamos a una explicación anterior y simplificada: si cualquiera de los operandos a sumar es una cadena (¡o se convierte en uno con los pasos anteriores!), la operación será una concatenación de cadenas. De lo contrario, siempre es una suma numérica.

Nota: Una coerción problemática comúnmente citada es [] + {} vs. {} + [] Como esas dos expresiones resultan, respectivamente, en "[object Object]" y 0. Hay más, sin embargo, y cubrimos esos detalles en “Bloques” en el Capítulo 5.

¿Qué significa eso para la coerción implícita?

Puede coaccionar un número a una cadena simplemente “sumando” el número y la cadena "" vacía:

Consejo: La suma numérica con el operador + es conmutativa, lo que significa que 2 + 3 es lo mismo que 3 + 2. La concatenación de cadenas con + obviamente no es generalmente conmutativa, pero con el caso específico de "", es efectivamente conmutativa, ya que un + "" y un "" + a producirán el mismo resultado.

Es extremadamente común coaccionar (implícitamente) el número para que encadene con una operación + "". De hecho, curiosamente, incluso algunos de los críticos más enérgicos de la coerción implícita siguen utilizando ese enfoque en su propio código, en lugar de una de sus alternativas explícitas.

Creo que este es un gran ejemplo de una forma útil de coerción implícita, ¡a pesar de la frecuencia con que se critica el mecanismo!

Comparando esta coerción implícita de un + "" con nuestro ejemplo anterior de coerción explícita String(a), hay una peculiaridad adicional que debemos tener en cuenta. Debido a cómo funciona la operación abstracta de ToPrimitive, un + "" invoca el valueOf() en el valor a, cuyo valor de retorno se convierte finalmente en una cadena a través de la operación abstracta interna de ToString. Pero String(a) sólo invoca toString()directamente.

Ambos enfoques resultan en una cadena de texto, pero si estás usando un objeto en lugar de un valor numérico primitivo regular, ¡no necesariamente obtendrás el mismo valor de cadena!

Considere:

Generalmente, este tipo de problema no te afectará a menos que estés intentando crear estructuras de datos y operaciones confusas, pero debes tener cuidado si estás definiendo tus propios métodos valueOf() y toString()para algún objeto, ya que la forma en que coaccionas el valor podría afectar el resultado.

¿Qué hay de la otra dirección? ¿Cómo podemos coaccionar implícitamente de cadena a número?

El operador - se define sólo para la sustracción numérica, por lo que a - 0 fuerza el valor de a a ser coaccionado a un número. Aunque mucho menos común, a * 1 o a / 1 lograría el mismo resultado, ya que esos operadores también se definen sólo para operaciones numéricas.

¿Qué pasa con los valores de los objetos con el operador -? Historia similar a la de + de arriba:

Ambos valores de matriz tienen que convertirse en números, pero terminan siendo coaccionados primero a cadenas (usando la serialización esperada toString()), y luego son coaccionados a números, para que la sustracción -funcione.

Entonces, ¿es la coerción implícita de las cadenas y los valores numéricos peor que siempre has oído en historias de terror? Personalmente no lo creo.

Comparar b = String(a) (explícito) con b = a + "" (implícito). Creo que se pueden hacer casos para que ambos enfoques sean útiles en tu código. Ciertamente b = a + "" es un poco más común en los programas de JS, demostrando su propia utilidad sin importar los sentimientos acerca de los méritos o riesgos de la coerción implícita en general.

Implícitamente: Boleanos –> Números

Creo que un caso en el que la coerción implícita puede realmente brillar es en la simplificación de ciertos tipos de lógica booleana complicada en una simple suma numérica. Por supuesto, no se trata de una técnica de propósito general, sino de una solución específica para casos específicos.

Considere:

Esta utilidad onlyOne(...) sólo debería devolver true si exactamente uno de los argumentos es true / truthy. Es el uso de la coerción implícita sobre las comprobaciónes de veracidad y la coerción explícita sobre los demás, incluyendo el valor de retorno final.

Pero, ¿y si necesitáramos esa utilidad para poder manejar cuatro, cinco o veinte banderas de la misma manera? Es bastante difícil imaginar la implementación de código que maneje todas esas permutaciones de comparaciones.

Pero aquí es donde coaccionar los valores booleanos a números (0 ó 1, obviamente) puede ser de gran ayuda:

Nota: Por supuesto, en lugar del bucle for en onlyOne(...), podrías usar la utilidad ES5 reduce(...), pero no quería ocultar los conceptos.

Lo que estamos haciendo aquí es confiar en el 1 para coacciones true / truthy, y sumarlas numéricamente. sum += argumentos[i] usa la coerción implícita para hacer que eso suceda. Si uno y sólo un valor en la lista de argumentos es verdadero, entonces la suma numérica será 1, de lo contrario la suma no será 1 y por lo tanto no se cumple la condición deseada.

Por supuesto, podríamos hacerlo con una coerción explícita:

Primero usamos !!arguments[i] para forzar la coerción del valor a verdadero o falso. Esto es para que puedas pasar valores no booleanos, como onlyOne( "42", 0), y todavía funcione como se espera (de lo contrario terminarías con una concatenación de cadenas y la lógica sería incorrecta).

Una vez que estamos seguros de que es booleano, hacemos otra coacción explícita con Number(..) para asegurarnos de que el valor es 0 ó 1.

¿Es “mejor” la forma de coerción explícita de esta utilidad? Evita la trampa de NaN como se explica en los comentarios del código. Pero, en última instancia, depende de sus necesidades. Personalmente creo que la versión anterior, basada en la coerción implícita, es más elegante (si no vas a pasar undefined o NaN), y la versión explícita es innecesariamente más verbosa.

Pero como con casi todo lo que estamos discutiendo aquí, es un juicio de valor.

Nota: Independientemente de los enfoques implícitos o explícitos, usted podría fácilmente hacer variaciones onlyTwo(..) o onlyFive(..) simplemente cambiando la comparación final de 1, a 2 ó 5, respectivamente. Eso es drásticamente más fácil que añadir un montón de expresiones && y ||. Así que, generalmente, la coerción es muy útil en este caso.

Implícitamente: * –>Booleano

Ahora, volvamos nuestra atención a la coerción implícita de valores booleanos, ya que es de lejos la más común y también la más problemática.

Recuerde, la coerción implícita es lo que se activa cuando usted usa un valor de tal manera que fuerza a que el valor se convierta. Para las operaciones numéricas y de cadena, es bastante fácil ver cómo pueden ocurrir las coacciones.

Pero, ¿qué tipo de operaciones de expresión requieren/fuerzan (implícitamente) una coacción booleana?

  1. La expresión de prueba en una sentencia if (..).
  2. La expresión de prueba (segunda cláusula) en el encabezado a for (... ; ; ; ...).
  3. La expresión de prueba en bucles while (...) y do...while(...).
  4. La expresión de prueba (primera cláusula) en expresiones ternarias ? :.
  5. El operando a la izquierda (que sirve como una expresión de prueba — ¡véase abajo!) de los operadores ||(“logical or”) y && (“logical and”).

Cualquier valor usado en estos contextos que aún no sea booleano será implícitamente coaccionado a un booleano usando las reglas de la operación abstracta ToBoolean cubierta anteriormente en este capítulo.

Veamos algunos ejemplos:

En todos estos contextos, los valores no boleanos están implícitamente coaccionados a sus equivalentes booleanos para tomar las decisiones de la prueba.

Operadores || y &&

Es muy probable que haya visto los operadores || (“logical or”) y && (“logical and”) en la mayoría o todos los demás idiomas que ha utilizado. Así que sería natural asumir que funcionan básicamente igual en JavaScript que en otros lenguajes similares.

Hay un matiz muy poco conocido, pero muy importante, aquí.

De hecho, yo diría que estos operadores ni siquiera deberían llamarse “operadores lógicos”, ya que ese nombre está incompleto al describir lo que hacen. Si les diera un nombre más preciso (aunque más torpe), los llamaría “operadores de selector”, o más completamente, “operadores de selector de operador”.

Por qué? Porque en realidad no dan como resultado un valor lógico (también conocido como booleano) en JavaScript, como lo hacen en otros lenguajes.

Entonces, ¿en qué resultan? Resultan en el valor de uno (y sólo uno) de sus dos operandos. En otras palabras, seleccionan uno de los dos valores del operando.

Citando la especificación ES5 de la sección 11.11:

El valor producido por un operador && o || no es necesariamente de tipo booleano. El valor producido será siempre el valor de una de las dos expresiones del operando.

Vamos a ilustrarlo:

Espera, ¿¡qué!? Piensa en eso. En lenguajes como C y PHP, esas expresiones resultan en verdadero o falso, pero en JS (¡y en Python y Ruby, de igual manera!), el resultado viene de los valores mismos.

Ambos operadores || y && realizan una prueba booleana en el primer operando (a o c). Si el operando no es booleano (como no lo es, aquí), se produce una coerción ToBoolean normal, para que se pueda realizar la prueba.

Para el operador ||, si la prueba es verdadera, la expresión || resulta en el valor del primer operando (a o c). Si la prueba es falsa, la expresión || da como resultado el valor del segundo operando (b).

Inversamente, para el operador &&, si la prueba es verdadera, la expresión && resulta en el valor del segundo operando (b). Si la prueba es falsa, la expresión && da como resultado el valor del primer operando (a o c).

El resultado de una expresión || o && es siempre el valor subyacente de uno de los operandos, no el resultado (posiblemente coaccionado) de la prueba. En c && b, c es nulo, y por lo tanto falsy. Pero la expresión && en sí misma resulta en nula (el valor de c), no en elfalse` coaccionado usado en la prueba.

¿Ve cómo estos operadores actúan ahora como “selectores de operandos”?

Otra forma de pensar sobre estos operadores:

Nota: Yo digo que a || b es “aproximadamente equivalente” a a ? a : b porque el resultado es idéntico, pero hay una diferencia con matices. En a ? a : b, si a fuera una expresión más compleja (como por ejemplo una que pudiera tener efectos secundarios como llamar a una función, etc.), entonces la expresión a posiblemente sería evaluada dos veces (si la primera evaluación fuera verídica). Por el contrario, para a || b, la expresión a se evalúa sólo una vez, y ese valor se utiliza tanto para la prueba coercitiva como para el valor del resultado (si procede). El mismo matiz se aplica a las expresiones a && b y a ? b : a.

Un uso extremadamente común y útil de este comportamiento, que hay una buena posibilidad de que usted haya usado antes y no haya entendido completamente, es:

El modismo a = a || "hello" (a veces se dice que es la versión de JavaScript del “operador coalescente nulo” de C#) actúa para probar a y si no tiene ningún valor (o sólo un valor falsy no deseado), proporciona un valor por defecto de copia de seguridad (“hello”).

¡Ten cuidado, sin embargo!

¿Ves el problema? "" como segundo argumento, es un valor falsy (ver ToBoolean más arriba en este capítulo), así que la prueba b = b || "world" falla, y el valor por defecto “world” es sustituido, aunque la intención probablemente era que el valor asignado a b fuera explícitamente “”.

Este modismo es extremadamente común, y bastante útil, pero sólo se debe usar en los casos en los que se deben omitir todos los valores falsy. De lo contrario, tendrás que ser más explícito en tu prueba, y probablemente usar un ? :ternario en su lugar.

Este lenguaje de asignación de valores por defecto es tan común (¡y útil!) que incluso aquellos que pública y vehementemente desacreditan la coerción de JavaScript a menudo lo usan en su propio código.

¿Y qué hay de &&?

Hay otro modismo que es un poco menos común utilizado manualmente, pero que es usado frecuentemente por los minifiers de JS. El operador && “selecciona” el segundo operando si y sólo si el primer operando prueba ser veraz, y este uso es a veces llamado “operador de protección” (ver también “Cortocircuito” en el Capítulo 5) — la primera prueba de expresión “protege” la segunda expresión: