Artículo original escrito por: Zach Snoek
Artículo original: What is hoisting in JavaScript?
Traducido y adaptado por: Gemma Fuster

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. ¡Puedes contactar conmigo en LinkedIn si quieres o si tienes alguna pregunta!