Cosas de JavaScript que ojalá hubiera aprendido antes

En todo mi entorno, JavaScript siempre fue un poco el lenguaje “raro”.

Nadie conseguía saber por qué motivos el código fallaba o se comportaba de formas que no querían. Siempre era muy “divertido” todo y lo dí por algo normal (craso error): “Si a los que saben mucho más que yo les pasa, será normal y poco importante”.

No era el lenguaje que más usara en su momento ni lo es ahora, pero llega un punto en el que determinados comportamientos del lenguaje empiezan a entorpecer lo que sabemos que podemos hacer, pero que el condenado JavaScript “no nos deja porque hace cosas raras”… o al menos no de la manera en la que sabemos hacerlo en otros lenguajes.

¿Qué cosas he aprendido estos días que me hubiera gustado saber antes? Ya verás qué divertido es esto. Son todas cosas con las que me he pegado en su momento, no las entendí pero encontré la solución que apliqué a ciegas y seguí adelante… todos cometemos errores y en algún momento los enmendamos o pagamos el precio.

Conclusión final: Se debería estudiar en profundidad cómo se comporta el lenguaje a un nivel bastante profundo diría yo. Porque llegará un momento que queramos hacer cosas más complejas, y las particularidades de JS nos volverán locos (y luego va y resulta que es algo “normal” en JavaScript).

Retrocompatibilidad del lenguaje

Bueno… empezamos bien. Esto es algo gordo y marca toda la “forma de ser” del lenguaje.

No tenía ni idea de que JS se construye de forma que un código escrito con JS del 2000 funciona en los navegadores de hoy día.

Esto no ocurre con otros lenguajes. Por ejemplo PHP y muchos otros lenguajes va poniendo avisos de desuso de determinadas funciones y alertas de que ya no se usan, por lo que un código de hace unos años puede no funcionar en un servidor de hoy.

Esto convierte a JS en un ente raro, con cosas que no le veo sentido, duplicidades (varias formas de hacer lo mismo), bugs que no se solucionan (porque romperían esa retrocompatibilidad)…

Por ello van saliendo funciones nuevas, keywords nuevas que lo que hacen es hacer lo mismo que otra función o keyword pero “mejor”… porque no pueden cambiar la implementación de la otra función porque romperían la retrocompatibilidad.

No pueden arreglar determinado bug, porque se liberó con ese bug y si lo cambian algún código antiguo podría no funcionar porque haga un pequeño hack para solventar ducho bug.

Es decir, si enseñara JS, es lo primero que contaría a mis alumnos. Es una pieza de contexto demasiado importante como para no tenerla en cuenta. Modifica la forma en la que aprendes algo en JS y cómo lo usas.

“use strict”

Nunca le vi mayor utilidad… claro, si no me conocía los entresijos, el lado oscuro de JS es normal, siempre me las arreglaba sin mayores problemas que esos casos extraños en los que no entendía lo que ocurría (como el 90% de los programadores no especializados en JS diría yo).

¿Para qué sirve, o al menos, con qué cosas me he quedado yo?

  • evita contaminar accidentalmente el scope global: las variables no definidas con (var, let, const…), no crean variables globales por defecto
  • prohíbe el uso de delete en variables, funciones o argumentos
  • mayor seguridad si usamos eval() y declaramos variables: evita que al definir una variable dentro de eval() pueda usarse fuera de eval()
  • evita confusiones del valor de this en funciones, y por tanto de recuperar muchos valores undefined al asignar propiedades al objeto erróneo: por defecto, dentro de una función, this vale undefined en lugar de window

Lo hubiera usado siempre dese el principio.

Problema de “null”

Tenemos 5 tipos de datos primitivos y uno no primitivo

  • Boolean
  • Number
  • String
  • Null
  • Undefined
  • Object (el tipo de dato no primitivo)

Cuando usamos typeof(<variable>) para conocer el tipo de dato de algo… ¿Pero qué pasa typeof(null)? Que devuelve “object”.

Se puede considerar un bug en JS que nos puede traer de cabeza si una validación la hemos basado en esta función por ejemplo (me ha pasado… no preguntes por qué usaba typeof en una validación porque la verdad, ni me acuerdo).

Me hubiera ahorrado mucho tiempo o hubiera hecho ese trozo de código de otra forma (era mi primer trabajo, es un recuerdo traumático jeje)

Cambio de tipos automático (“casteo” automático)

El conocido casteo en el uso de “==” está claro. Es decir que hacer “2” == 2 nos dará true, porque el string “2” se castea a número 2 con Number(“2”) de forma interna aunque no lo veamos.

El que me llevó de cabeza e incluso me costó de entender fue el de NaN. Más abajo lo explico.

¿Es NaN?

Querer saber si una variable es concretamente “NaN”, es algo relativamente frecuente si hacemos cálculos… Por ejemplo una división entre 2 variables que desconocemos su valor inicialmente, si la operación resulta en 2/”a” obtendremos “NaN” (not a number) porque no podemos mezclar números y strings.

¿Cómo podemos (sin entrar en si es la mejor forma de validar la operación, supongamos el caso que necesitamos saber si una variable es concretamente “NaN”) estar seguros de que una variable es “NaN”?

En su momento, hablo de mis primero trabajos, lo primero que pensé y me acuerdo bien fue “Pues, typeof(mivariable) === ‘NaN'”… Error! Eso nos devuelve “number”.

El tipo de NaN, es number. La primera en la frente.

Acabé buscando un código que me hiciera el favor, pero lo siguiente por lógica que pensaríamos es “mivariable === NaN” sabiendo que mivariable de hecho es NaN. Error! Tampoco. NaN es el único valor que comparado (con triple igual) con sigo mismo, es false.

Es decir: 1 === 1 devuelve true, “e” === “e” devuelve true , null === null devuelve true , undefined === undefined devuelve true… pero NaN === NaN devuelve false. Otra en la frente.

Lo siguiente es buscarlo, y ver que existe la función isNaN() que en su momento también me daba inconsistencias. ¿Por qué? Porque isNaN() revisa que conceptualmente la variable no sea un número. Es decir, que isNaN(“abc”) devuelve true, ok porque no es un número es un string. Pero isNaN(“132”) devuelve false, es decir sí es un número porque es un string vale pero de números.

Por tanto isNaN() no vale para saber si mi variable es NaN, sino para saber si mi variable, hechos los casteos posibles internamente sin que me dé cuenta, representa o no a un número. Nada tiene que ver con el valor NaN (si no puedo controlar la entrada de datos y acabo con un string de números, isNaN() es inconsistente)

La solución es un truco. Ya que NaN comparado con sigo mismo es el único valor que no es truemivariable !== NaN devolverá true solo y unicamente que mivariable sea NaN.

¿Ya podrían meter una función interna del tipo isNaNValue() no? Que verifique si el valor es NaN y no si es conceptualmente un número.

Closures y su referencia a variables

Contexto: ¿quién no ha intentado hacer funciones dinámicas de este tipo alguna vez?

var foo = [];

for (var i = 0; i < 5; i++) {
    foo[i] = function() {return i};
}

console.log(foo[0]());
console.log(foo[1]());
console.log(foo[2]());

Es decir, que en cada iteración del bucle, definamos una function que dependa de algo del índice o de algo del bucle?

¿Sabes qué devuelven los 3 console.log? 3, 3, 3… Pero dentro del bucle, haces un console.log(i) y tiene los valores correctos!

Te ha pasado y lo sabes 🙂

Mi revelación es que los closures, almacenan la referencia al valor actual (hincapié en actual) de una variable de su interior.

Es decir que fuera del bucle, ¿qué vale i si la imprimiéramos suelta? Ya han pasado todas las iteraciones y se ha ido sobreescribiendo su valor. Por tanto contiene el último valor del bucle.

¿Cómo solucionamos esto? Con IIFE (inmediately invoked function expresions) que crean un contexto para ellas mismas y no contaminan el contexto global y una variable creada expresamente en su interior (que será independiente del contexto global).

var foo = [];

for (var i = 0; i < 5; i++) {
    (function () {

        // cacheamos internamente en la IIFE
        var y = i;
        
        // misma function de antes, devuelve var cacheada
        foo[i] = function() {return y};
        
    })()
}

console.log(foo[0]());
console.log(foo[1]());
console.log(foo[2]());

Arreglado, ahora sí imprime 0, 1, 2

¿Por qué hay tantos bucles?

Tenemos

  • for
  • for in
  • for of
  • forEach

El for y el forEach lo entiendo, pero ¿y los otros? Sí… for in es para objetos (cada índice del bucle es un string), y for of para arrays (cada índice del buble es un number)… ¿pero por qué añades funciones en vez de arreglar o extender el for normal?

Por la maravillosa retrocompatibilidad que decía al principio. ¿No te gusta como funciona el for normal? No puedes tocarlo, rompes compatibilidad con código antiguo. Añade un for nuevo y arreglado.

Esto hace que muchas veces los que no estamos especializados en JavaScript, vayamos mareados y tengamos la sensación de que es un lenguaje inconsistente o “raro” cuando es todo lo contrario.

El circo del this

Es muy largo y tedioso de exponer pero básicamente, es que leí en muchos artículos y tutoriales que this en una función, hace referencia al contexto donde se llama la función. ¿Verdad?

"use strict";

var foo = [];

var checkIt = {
    checkThis: function () {
        console.log('1', this);

        function checkThisInner () {
            console.log('2', this);
        }

        checkThisInner();
    }
}

checkIt.checkThis();

Nos imprime

1 {checkThis: ƒ}
2 undefined

El segundo console.log, nos imprimirá Window si no estamos en modo estricto.

Lo importante: ¿Por qué el segundo console.log es undefined o Window? Si la función se llama CON contexto en checkIt.CheckThis() ¿no?

Pues no… checkThisInner se llama sin contexto ninguno. En modo estricto, this no tiene asignado nada (es undefined) y fuera de modo estricto this tiene asignado el objeto global Window.

La solución más estandarizada y fácil es cachear this, cosa que hacía sin entender muy bien esta explicación de más arriba…

var foo = [];

var checkIt = {
    checkThis: function () {
        // cache this value
        var that = this;

        // use that instead
        console.log('1', that);

        function checkThisInner () {
            // use that instead
            console.log('2', that);
        }

        checkThisInner();
    }
}

checkIt.checkThis();

Ahora sí tenemos un valor estable para this, lo hemos estabilizado mediante su cacheo en un punto en el que vale lo que nos interesa que valga: justo al empezar nuestra función. Y nos devuelve

1 {checkThis: ƒ}
2 {checkThis: ƒ}

También podemos “fijar” nosotros manualmente el valor de this usando call(), apply() o bind() y no haría falta cachear nada porque ya lo especificamos nosotros directamente

var checkIt = {
    checkThis: function () {
        console.log('1', this);

        function checkThisInner () {
            console.log('2', this);
        }

        // explicitely use the this outside of checkThisInner
        checkThisInner.call(this);
    }
}

checkIt.checkThis();

Funciones de flecha, su contexto y su this

He leído muchas veces que las funciones de flecha (fat arrow functions), mejoran la sintaxis y legibilidad de las funciones pero… hacen más cosas.

El valor de this dentro una función viene determinado por el uso o no del modo estricto, pero la regla es que vale lo que valga el contexto en el que ha sido llamada (lo vimos arriba).

Pues la función de flecha hace que this valga lo que vale el contexto en el que fue definida la función de flecha.

Lo cual lo cambia todo y puede llevarnos de cabeza y en el momento de refactorizar, que digamos “voy a poner todo funciones de flecha que es más bonito” y todo deje de funcionar porque nuestros this ya nos lo que eran dentro de cada una de esas funciones.

Orientación a objetos

Bueno… me ha sorprendido la “dificultad”. Pero claro… esto viene determinado por no conocer en profundidad el uso de los prototipos.

Lo único que destacaré aquí es que

  • las funciones tienen una propiedad prototype que es un objeto con propiedades y funciones
  • los objetos tienen una propiedad __proto__ que apunta al prototype (al objeto) de una función.

De forma se crea una cadena (la prototype chain) y cuando hacemos “herencia” en JS, lo único que hacemos es crear objetos que serán usados como prototypes en funciones, a las que apuntarán determinados objetos desde su propiedad __proto__

Que la nueva sintaxis de ES6, con las keywords class y extends por debajo hacen esto mismo con los prototipos.

Y por último que hay 2 patrones en JS para la herencia. El patrón de prototipos (el más puro) y el pseudo-clásico que usa funciones constructoras (también llamado patrón constructor). Hay que, al menos, conocer ambos por si nos encontramos código siguiendo justo el patrón que no estamos acostumbrados a usar.