¿Qué es Hoisting en JavaScript?
En JavaScript, hoisting te permite usar funciones y variables antes de que se hayan declarado. En este post, aprenderemos qué es el hoisting y cómo funciona.
¿Qué es hoisting?
Echa un vistazo a este código y adivina qué sucede cuando se ejecuta:
console.log(foo);
var foo = 'foo';
Puede que te sorprenda saber que este código genera undefined
y que no falla o genera un error – a pesar de que foo
se asigna después de la línea console.log
Esto se debe a que el intérprete de JavaScript divide la declaración y asignación de funciones y variables: JavaScript «hoists» o «alza» tus declaraciones a la parte superior de su scope (ámbito) antes de la ejecución.
A esto se le llama hoisting, y nos permite usar foo
antes de su declaración en el ejemplo anterior.
Echemos un vistazo más profundo a las funciones y al hoisting de variables para comprender qué significa esto y cómo funciona.
Hoisting de variables en JavaScript
Como recordatorio, en JavaScript, declaramos una variable con var
, let
, yconst
. Por ejemplo:
var foo;
let bar;
Asignamos un valor a una variable usando el operador de asignación:
// Declaracion
var foo;
let bar;
// Asignacion
foo = 'foo';
bar = 'bar';
En muchos casos, podemos combinar la declaración y la asignación en un solo paso:
var foo = 'foo';
let bar = 'bar';
const baz = 'baz';
El hoisting de variables actúa de manera diferente dependiendo de cómo se declare la variable. Comencemos por comprender el comportamiento de las variables declaradas con var
.
Hoisting de variables con var
Cuando el intérprete hace hoisting de una variable declarada con var
, inicializa su valor a undefined
. La primera línea de código a continuación muestra undefined
:
console.log(foo); // undefined
var foo = 'bar';
console.log(foo); // "bar"
Como hemos dicho antes, hoisting proviene de que el intérprete de JavaScript divida la declaración y la asignación de variables. Podemos lograr lo mismo manualmente si dividimos la declaración y la asignación en dos pasos:
var foo;
console.log(foo); // undefined
foo = 'foo';
console.log(foo); // "foo"
Recuerda que la primera console.log(foo)
muestraundefined
porque a la variable foo
se le hace hoisting y se le asigna un valor por defecto (no porque la variable nunca sea declarada). El uso de una variable no declarada nunca mostrará un ReferenceError
en lugar de un undefined
:
console.log(foo); // Uncaught ReferenceError: foo is not defined
El uso de una variable no declarada antes de su asignación también mostrará un ReferenceError
porque no se ha hecho hoisting a ninguna declaración:
console.log(foo); // Uncaught ReferenceError: foo is not defined
foo = 'foo'; // asignar una variable que no esta declarada es válido
A estas alturas, debes estar pensando, «Es un poco raro que JavaScript nos permita acceder a las variables antes de que se declaren.» Este comportamiento es una parte inusual de JavaScript y puede conducir a errores. Por lo general, no es recomendable usar una variable antes de que sea declarada.
Afortunadamente, declarar variables con let
y const
, introducidas en ECMAScript 2015, cambia este comportamiento.
Hoisting de variables con let
y const
Las variables declaradas con let
y const
también reciben hoisting, pero no son inicializadas con un valor por defecto. Acceder a una variable declarada con let
o const
antes de que sea declarada resulta en un ReferenceError
:
console.log(foo); // Uncaught ReferenceError: Cannot access 'foo' before initialization
let foo = 'bar'; // lo mismo para variables declaradas con const
Observa que el intérprete sigue haciendo hoisting a foo
: el mensaje de error nos dice que la variable se inicializa en algún lugar.
La zona muerta temporal
La razón por la que obtenemos un error de referencia cuando intentamos acceder a una variable declarada con let
o const
antes de su declaración se debe a la zona muerta temporal (temporal dead zone, TDZ).
La TDZ comienza al principio del ámbito de la variable y finaliza cuando se declara. El acceso a la variable en esta zona TDZ lanza un ReferenceError
.
Aquí va un ejemplo con un bloque explícito que muestra el inicio y el final de la TDZ de foo:
{
// Comienzo de TDZ de foo
let bar = 'bar';
console.log(bar); // "bar"
console.log(foo); // ReferenceError porque estamos en la TDZ de foo
let foo = 'foo'; // Final de TDZ de foo
}
La TDZ también está presente en los parámetros de función predeterminados (por defecto), que se evalúan de izquierda a derecha. En el siguiente ejemplo, bar
está en la zona TDZ hasta que se establece su valor predeterminado:
function foobar(foo = bar, bar = 'bar') {
console.log(foo);
}
foobar(); // Uncaught ReferenceError: Cannot access 'bar' before initialization
Pero este código funciona porque podemos acceder a foo
desde fuera de su TDZ:
function foobar(foo = 'foo', bar = foo) {
console.log(bar);
}
foobar(); // "foo"
typeof
en la TDZ
El uso de una variable let
o const
como operando del operador typeof
en la TDZ arrojará un error:
console.log(typeof foo); // Uncaught ReferenceError: Cannot access 'foo' before initialization
let foo = 'foo';
Este comportamiento es consistente con los otros casos de let
y const
que hemos visto en la TDZ. Encontramos un ReferenceError
porque foo
es declarada, pero no inicializada – debemos tener en cuenta que la estamos usando antes de la inicialización.
Sin embargo, no pasa cuando se utiliza una variable con var
antes de que sea declarada porque se inicializa con undefined
cuando se le hace el hoisting:
console.log(typeof foo); // "undefined"
var foo = 'foo';
Además, esto es sorprendente porque podemos comprobar el tipo de una variable que no existe sin que nos dé un error. typeof
devuelve una cadena de forma segura:
console.log(typeof foo); // "undefined"
De hecho, con la introducción de let
y const
se rompe la garantía de typeof
de siempre devolver un valor cadena para cualquier operando.
Hoisting de funciones en JavaScript
Las declaraciones de funciones también son sometidas a hoisting. Esto nos permite llamar a funciones antes de que sean definidas. Por ejemplo, el código siguiente termina con éxito y devuelve "foo"
:
foo(); // "foo"
function foo() {
console.log('foo');
}
Ten en cuenta que solamente se hace hoisting a las declaraciones de función, no a las expresiones de función. Esto debería tener sentido: como acabamos de ver, a las asignaciones de variables no se les hace hoisting.
Si intentamos llamar a la variable a la que se asignó la expresión de la función, obtendremos un TypeError
o ReferenceError
, dependiendo del ámbito de la variable:
foo(); // Uncaught TypeError: foo is not a function
var foo = function () { }
bar(); // Uncaught ReferenceError: Cannot access 'bar' before initialization
let bar = function () { }
baz(); // Uncaught ReferenceError: Cannot access 'baz' before initialization
const baz = function () { }
Esto difiere de llamar a una función que nunca ha sido declarada, que arroja una ReferenceError
:
foo(); // Uncaught ReferenceError: baz is not defined
Cómo utilizar hoisting en JavaScript
Hoisting de variables
Debido a la confusión que el hoisting con variables var
puede crear, es mejor evitar usar las variables antes de que sean declaradas. Si estás escribiendo código en un proyecto greenfield, deberías usar let
y const
para que esto se cumpla.
Si estás trabajando en una base de código anterior o tienes que usar var
por otros motivos, MDN recomienda que escribas las declaraciones var
lo más cerca posible de la parte superior de su scope. Con esto se consigue que el scope de las variables sea más claro.
También puedes considerar usar la regla de ESLint no-use-before-define
, que asegura que no se use una variable antes de su declaración.
Hoisting de funciones
El Hoisting de funciones es útil porque podemos dejar la implementación de la función más abajo en el archivo y dejar que el lector se concentre en lo que está haciendo el código. En otras palabras, podemos abrir un archivo y ver qué hace el código sin entender primero cómo está implementado.
Toma el siguiente ejemplo:
reiniciarPuntos();
dibujarTablero();
poblarTablero();
comenzarJuego();
function reiniciarPuntos() {
console.log("Reinicializando puntos");
}
function dibujarTablero() {
console.log("Dibujando tablero");
}
function poblarTablero() {
console.log("Poblando tablero");
}
function comenzarJuego() {
console.log("Comenzando juego");
}
Inmediatamente, tenemos una idea de lo que hace este código sin tener que leer todas las declaraciones de funciones.
Sin embargo, el uso de funciones antes de su declaración es una cuestión de preferencia personal. Algunos desarrolladores, como Wes Bos, prefieren evitar esto y colocar funciones en módulos que se pueden importar según sea necesario.
La guía de estilo de Airbnb lleva esto más allá y fomenta las expresiones de función nombradas sobre las declaraciones para evitar la referencia antes de la declaración:
Las declaraciones de funciones estan sometidas a hoisting, lo que significa que es fácil, demasiado fácil, hacer referencia a la función antes de que se defina en el archivo. Esto perjudica la legibilidad y la facilidad de mantenimiento.
Si encuentras que la definición de una función es lo suficientemente grande o compleja como para interferir con la comprensión del resto del archivo, entonces quizás sea el momento de extraerla a su propio módulo.
Conclusión
Gracias por leer, espero que esta publicación te haya ayudado a aprender sobre hoisting en JavaScript. Si quieres aprender mucho más sobre Javascript, entonces haz clic en este link.