Los últimos años nos han demostrado que el desarrollo web está cambiando. A medida que se iban añadiendo muchas funciones y API a los navegadores, teníamos que utilizarlas de la forma correcta. El lenguaje al que debemos este honor fue JavaScript.
Al principio, los desarrolladores no estaban convencidos de cómo estaba diseñado y tuvieron impresiones mayoritariamente negativas al utilizar este script. Con el tiempo, resultó que este lenguaje tiene un gran potencial, y los estándares ECMAScript posteriores hacen que algunas de las mecánicas sean más humanas y, simplemente, mejores. En este artículo, echamos un vistazo a algunas de ellas.
Tipos de valores en JS
La verdad bien conocida sobre JavaScript es que aquí todo es un objeto. En realidad, todo: matrices, funciones, cadenas, números e incluso booleanos. Todos los tipos de valores están representados por objetos y tienen sus propios métodos y campos. Sin embargo, podemos dividirlos en dos categorías: primitivos y estructurales. Los valores de la primera categoría son inmutables, lo que significa que podemos reasignar alguna variable con el nuevo valor pero no podemos modificar el propio valor existente. La segunda representa valores que pueden ser alterados, por lo que deben ser interpretados como colecciones de propiedades que podemos reemplazar o simplemente llamar a los métodos que están diseñados para hacerlo.
Ámbito de las variables declaradas
Antes de profundizar, vamos a explicar lo que significa el ámbito. Podemos decir que el ámbito es la única área donde podemos usar las variables declaradas. Antes del estándar ES6 podíamos declarar variables con la sentencia var y darles ámbito global o local. El primero es un ámbito que nos permite acceder a algunas variables en cualquier lugar de la aplicación, el segundo sólo está dedicado a un ámbito específico - principalmente una función.
Desde la norma ES2015, JavaScript tiene tres formas de declarar variables que difieren con la palabra clave. La primera ya se ha descrito: las variables declaradas mediante la palabra clave var se aplican al cuerpo de la función actual. El estándar ES6 nos permite declarar variables de una forma más humana: a diferencia de las sentencias var, las variables declaradas mediante las sentencias const y let sólo se aplican al bloque. Sin embargo, JS trata la sentencia const de forma bastante inusual si se compara con otras sentencias lenguajes de programación - en lugar de un valor persistente, mantiene una referencia persistente al valor. En resumen, podemos modificar las propiedades de un objeto declarado con una sentencia const, pero no podemos sobrescribir la referencia de esta variable. Algunos dicen que la alternativa var en ES6 es en realidad una sentencia let. No, no lo es, y la sentencia var no es y probablemente no será retirada nunca. Una buena práctica es evitar el uso de sentencias var, porque en la mayoría de los casos nos dan más problemas. A su vez, debemos abusar de las sentencias const, hasta que tengamos que modificar su referencia - entonces deberíamos usar let.
Ejemplo de comportamiento inesperado del ámbito de aplicación
Empecemos por lo siguiente código:
(() => {
for (var i = 0; i {
console.log(`Valor de "i": ${i}`);
}, 1000);
}
})();
Si nos fijamos, parece que el bucle for itera el valor i y, al cabo de un segundo, registrará los valores del iterador: 1, 2, 3, 4, 5. Pues no es así. Como hemos dicho antes, la sentencia var trata de mantener el valor de una variable durante todo el cuerpo de la función; esto significa que en la segunda, tercera y sucesivas iteraciones el valor de la variable i será sustituido por un valor siguiente. Finalmente, el bucle termina y los ticks de timeout nos muestran lo siguiente: 5, 5, 5, 5, 5. La mejor manera de mantener un valor actual del iterador es utilizar la sentencia let en su lugar:
(() => {
for (let i = 0; i {
console.log(`Valor de "i": ${i}`);
}, 1000);
}
})();
En el ejemplo anterior, mantenemos el ámbito del valor i en el bloque de iteración actual, es el único ámbito donde podemos utilizar esta variable y nada puede anularla desde fuera de este ámbito. El resultado en este caso es el esperado: 1 2 3 4 5. Veamos cómo manejar esta situación con una sentencia var:
(() => {
for (var i = 0; i {
setTimeout(() => {
console.log(`Valor de "j": ${j}`);
}, 1000);
})(i);
}
})();
Como la declaración var trata de mantener el valor dentro del bloque de función, tenemos que llamar a una función definida que toma un argumento - el valor del estado actual del iterador - y luego simplemente hacer algo. Nada fuera de la función declarada anulará el valor j.
Ejemplos de expectativas erróneas sobre los valores de los objetos
El delito más frecuente que he observado es ignorar el poder de los estructurales y cambiar sus propiedades, que también se modifican en otras partes del código. Echa un vistazo rápido:
const DEFAULT_VALUE = {
favoriteBand: 'The Weeknd'
};
const currentValue = DEFAULT_VALUE;
const bandInput = document.querySelector('#bandaFavorita');
const restoreDefaultButton = document.querySelector('#restore-button');
bandInput.addEventListener('input', () => {
currentValue.favoriteBand = bandInput.value;
}, false);
restoreDefaultButton.addEventListener('click', () => {
currentValue = DEFAULT_VALUE;
}, false);
Desde el principio: supongamos que tenemos un modelo con propiedades por defecto, almacenadas como un objeto. Queremos tener un botón que restaure sus valores de entrada a los predeterminados. Después de rellenar la entrada con algunos valores, actualizamos el modelo. Después de un momento, pensamos que la selección por defecto era simplemente mejor, así que queremos restaurarla. Pulsamos el botón... y no pasa nada. ¿Por qué? Por ignorar el poder de los valores referenciados.
Esta parte: const currentValue = DEFAULTVALUE le está diciendo al JS lo siguiente: toma la referencia al DEFAULTVALUE y asignarle la variable currentValue. El valor real se almacena en la memoria una sola vez y ambas variables apuntan a él. Modificar algunas propiedades en un lugar significa modificarlas en otro. Tenemos algunas formas de evitar situaciones como esa. Una que satisface nuestras necesidades es un operador de dispersión. Arreglemos nuestro código:
const DEFAULT_VALUE = {
favoriteBand: 'The Weeknd'
};
const currentValue = { ...DEFAULT_VALUE };
const bandInput = document.querySelector('#bandaFavorita');
const restoreDefaultButton = document.querySelector('#restore-button');
bandInput.addEventListener('input', () => {
currentValue.favoriteBand = bandInput.value;
}, false);
restoreDefaultButton.addEventListener('click', () => {
currentValue = { ...DEFAULT_VALUE };
}, false);
En este caso, el operador spread funciona así: toma todas las propiedades de un objeto y crea un nuevo objeto lleno de ellas. Gracias a esto, los valores en currentValue y DEFAULT_VALUE ya no apuntan al mismo lugar en la memoria y todos los cambios aplicados a uno de ellos no afectarán a los otros.
Bien, entonces la pregunta es: ¿se trata de utilizar el operador mágico de dispersión? En este caso - sí, pero nuestros modelos pueden requerir más complejidad que este ejemplo. En caso de que utilicemos objetos anidados, arrays o cualquier otra estructura, el operador spread del valor referenciado en el nivel superior sólo afectará al nivel superior y las propiedades referenciadas seguirán compartiendo el mismo lugar en la memoria. Hay muchas soluciones para manejar este problema, todo depende de tus necesidades. Podemos clonar objetos en cada nivel de profundidad o, en operaciones más complejas, utilizar herramientas como immer que nos permite escribir código inmutable casi sin dolor.
Mézclalo todo
¿Es útil una mezcla de conocimientos sobre ámbitos y tipos de valores? Por supuesto que sí. Construyamos algo que utilice ambos:
const useValue = (defaultValue) => {
const value = [...defaultValue];
const setValue = (newValue) => {
value.length = 0; // forma complicada de limpiar el array
newValue.forEach((item, index) => {
valor[índice] = elemento;
});
// hacer otras cosas
};
return [valor, setValue];
};
const [animales, setAnimales] = useValue(['gato', 'perro']);
console.log(animals); // ['gato', 'perro']
setAnimals(['caballo', 'vaca']);
console.log(animals); // ['caballo', 'vaca']);
Vamos a explicar cómo funciona este código línea por línea. Bien, la función useValue está creando un array basado en el argumento defaultValue; crea una variable y otra función, su modificador. Este modificador toma un nuevo valor que se aplica de forma complicada al existente. Al final de la función, devolvemos el valor y su modificador como valores del array. A continuación, usamos la función creada - declaramos animals y setAnimals como valores devueltos. Usamos su modificador para comprobar si la función afecta a la variable animal - ¡sí, funciona!
Pero espera, ¿qué tiene de especial este código? La referencia mantiene todos los nuevos valores y puedes inyectar tu propia lógica en este modificador, como por ejemplo algunas API o parte del ecosistema que potencia tu flujo de datos sin esfuerzo. Ese patrón complicado se utiliza a menudo en las bibliotecas JS más modernas, donde el paradigma funcional en la programación nos permite mantener el código menos complejo y más fácil de leer por otros programadores.
Resumen
La comprensión de cómo funciona la mecánica del lenguaje bajo el capó nos permite escribir código más consciente y ligero. Aunque JS no sea un lenguaje de bajo nivel y nos obligue a tener ciertos conocimientos sobre cómo se asigna y almacena la memoria, todavía tenemos que estar atentos a comportamientos inesperados al modificar objetos. Por otro lado, abusar de los clones de valores no siempre es el camino correcto y un uso incorrecto tiene más contras que pros. La forma correcta de planificar el flujo de datos es considerar qué se necesita y qué posibles obstáculos se pueden encontrar al implementar la lógica de la aplicación.
Más información: