Escuela Frontend
JavaScript

Entiende el Concepto de Elevación (Hoisting) en JavaScript

Horacio Herrera
Autor
Horacio Herrera

En este artículo vas a aprender una de las partes fundamentales de la ejecución de nuestro código en JavaScript: La elevación o Hoisting. Puede que conocer sobre Hoisting suene algo extraño y con poco uso práctico, pero a la larga verás que te ayudará a entender mejor cómo los navegadores interpretan tu código.

¿Por qué debo aprender sobre Hoisting?

Aprender de Hoisting es importante por que:

  • Puedes identificar errores relacionados con la elevación rápidamente
  • Puedes estructurar tus programas de una manera más legibles y separando bien cada concepto

En el artículo anterior, sobre expresiones y declaraciones de funciones comenté un poco sobre Hoisting y como cambia la manera en la que nuestro código se ejecuta.

Veamos en más detalle qué es realmente lo que el intérprete de JavaScript hace para ejecutar nuestro código y así entender alguno que otro error habitual.

¿Qué es Hoisting?

Hoisting significa elevación. Lo que quiere decir es que ciertas partes de nuestro código se elevarán y se ejecutarán antes. Veamos el siguiente código:

function obtenerMisArticulos(lista) {
var articulos = []
for(var i = 0; i< lista.length; i++) {
var elemento = lista[i]
if (elemento.autor == 'Horacio') {
articulos.push(elemento)
}
}
return articulos;
}

Este bloque de código para nuestro intérprete se verá así:

function obtenerMisArticulos(lista) {
var articulos;
var i;
var elemento;
articulos = []
for(i = 0; i< lista.length; i++) {
elemento = lista[i]
if (elemento.autor == 'Horacio') {
articulos.push(elemento)
}
}
return articulos;
}

Como estamos usando el tipo de variable var, todas las declaraciones suben al inicio de la función en la cual fueron declaradas(*), guardando en memoria su valor para poder acceder a ellas desde cualquier parte de la función. Todo esto tiene que ver con los contextos de ejecución.

Contextos de Ejecución o "Scope"

Los contextos de ejecución (o scope) son creados cada vez que se ejecuta una función. Se crea un scope por cada función. Veamos un ejemplo simple:

function Bienvenida(nombre) {
var saludo = "Hola " + nombre
console.log(saludo)
}
Bienvenida('Lauro') // 👈🏼 creamos un scope para `Bienvenida`

El contexto de ejecución se crea para almacenar las variables que se utilizan dentro de la misma. Hay dos fases en los contextos de ejecución: Fase de Creación y fase de Ejecución.

En la fase de creación ocurren 4 cosas:

  • Primero, se define el valor de this dentro de la función. Podríamos decir que es el "dueño" de la función o el objeto que la está ejecutando. En este caso sería window (pero en modo "estricto" el valor sería undefined).
  • Segundo, crea un espacio de memoria para arguments , que es un objeto de tipo array que almacena todas las variables que le pasamos a la función al ejecutarla.
  • Tercero, todos los parámetros pasados se almacenan fuera por su nombre (en nuestro ejemplo, la variable nombre), para que podamos utilizarlos por el mismo dentro de la función.
  • Finalmente, es donde ocurre el Hoisting, se crea un espacio en memoria para todas las declaraciones de variables de tipo var y se les asigna el valor undefined (si, undefined es un valor).

En la fase de ejecución el intérprete irá evaluando línea por línea hasta llegar al final de la función.

Lo bueno es que como todas nuestras declaraciones var han sido "subidas" al inicio de la función, podríamos utilizar las variables incluso antes de la línea en la que las declaramos.

No sólo var sufre del hoisting...

Las declaraciones con var no son las únicas que sufren el efecto del Hoisting, las declaraciones de funciones también son afectadas, de ahí es donde podemos llamar a estas funciones incluso antes de ser declaradas. A diferencia de las declaraciones con var que les asigna el valor undefined, las funciones suben por completo.

Todo esto no se podría hacer si usamos una expresión de función en vez de una declaración. En una expresión de función lo que "sube" es justo su declaración y no la función en sí ¡Hay que tener cuidado con esto!

saludar("Matías") // ✅ Válido
function saludar(nombre) {
var saludo = "Hola " + nombre
console.log(saludo)
}
// ------------------
despedir('Horacio') // 🚫 No válido
var despedir = function saludar(nombre) {
var despedida = "Adiós " + nombre
console.log(saludo)
}

Otra propiedad que no sufre del Hoisting son las clases en JavaScript. No podemos inicializar una instancia de una clase antes de la declaración de la misma.

var estudiante = new Persona()
class Persona {
// ...
}
// 🚫 Error!

¿Qué más se ve afectado por el Hoisting?

  • Módulos estáticos de ECMAScript 2015: No hay nada que nos impida escribir nuestros imports al final de nuestros archivos, ¡Pero no es nada recomendable! Algo que seguro no podemos hacer es importar un módulo dentro de otro scope que no sea el global (dentro de otra función, un if, un for...)

  • let y const: Sí, let y const también son elevadas, pero tienen un tratamiento diferente en cuanto a memoria. Pronto hablaremos de la "Temporal Dead Zone". Por eso es que no podemos acceder al valor de una variable declarada con let o const porque están en esta zona muerta temporal.

¿Cómo podemos sacarle ventaja a la elevación o Hoisting?

Uno de los beneficios de aprender sobre elevación es que te permite escribir código más legible, de manera que puedes describir mejor lo que hace tu código y otros compañeros o compañeras lo entiendan mucho mejor.

Compara este fragmento de código:

function imprimirRegistros(recordIds) {
var records = recordIds.map(function getStudentFromId(studentId) {
return studentRecords.find(function matchId(record){
return (record.id == studentId);
});
});
records.sort(function sortByNameAsc(record1,record2){
if (record1.name < record2.name) return -1;
else if (record1.name > record2.name) return 1;
else return 0;
});
records.forEach(function printRecord(record){
console.log(`${record.name} (${record.id}): ${record.paid ? "Paid" : "Not Paid"}`);
});
}

Con éste otro:

function imprimirRegistros(recordIds) {
var registros = recordIds.map(obtenerEstudiantePorId);
registros.sort(ordenarPorNombreAsc);
records.forEach(imprimirRegistro);
}
function obtenerEstudiantePorId(idEstudiante) {
return registros.find(matchId);
function matchId(registro){
return (registro.id == idEstudiante);
}
}
function ordenarPorNombreAsc(registro1,registro2){
if (registro1.name < registro2.name) return -1;
else if (registro1.name > registro2.name) return 1;
else return 0;
}
function imprimirRegistro(registro){
console.log(`${registro.name} (${registro.id}): ${registro.paid ? "Pagado" : "Por Pagar"}`);
}

Se puede apreciar que aunque ambos fragmentos de código hacen lo mismo, el segundo es más fácil de leer y de reutilizar. Recuerda que uno de los objetivos a la hora de escribir nuestros programas es la legibilidad. Estamos escribiendo código para que otros lo puedan leer, no solo para que la máquina lo ejecute.

Desafío

Mira el siguiente fragmento de código:

function test() {
console.log(a);
console.log(foo());
var a = 1;
function foo() {
return 2;
}
}
test();

Responde a lo siguiente:

  1. Si ejecutamos este código, que veremos en la consola?
    1. 1 y 2?
    2. undefined y 1?
    3. undefined y 2?

La respuesta al final del post! 👇🏻

Crédito Extra

  1. ¿Cambiaría algo si en vez de var a = 1 usamos const a = 1?

Conclusión

Lo que me gustaría que recuerdes cuando veas la palabra Hoisting en JavaScript es que tengas claro que la declaración y la inicialización de variables y funciones ocurre en momentos distintos, incluso cuando los escribes en la misma línea.

Nota

(*) - En realidad no es que cambie nuestro código, pero el efecto que ocasiona es el de "subir" las declaraciones. en realidad todo se queda igual a como lo hemos escrito!

Respuesta al Desafío:

La respuesta correcta es c , te describo la ejecución paso a paso:

  1. Al entrar a la función test, imprimimos el valor de a por consola. Como no le hemos dado valor, veremos primero impreso en la consola undefined. Recuerda que la declaración de la función "sube" por el Hoisting
  2. Luego imprimimos en la consola el resultado de la llamada a la función foo. El resultado es lo que devuelve la función foo, que es 2

Artículos Relacionados

¿Quieres mejorar tus habilidades de frontend?