Comprender las variables, el alcance y la elevación en JavaScript

Veamos en detalle var, let y const. ¿Cómo trabajan? sus diferencias? y por qué es tan importante conocerlos.

Las variables son uno de los bloques fundamentales de cualquier lenguaje de programación, la forma en que cada lenguaje define cómo declaramos e interactuamos con las variables puede hacer o deshacer un lenguaje de programación. Por lo tanto, cualquier desarrollador debe comprender cómo trabajar de manera efectiva con variables, sus reglas y particularidades. En el tutorial de hoy, aprenderemos cómo declarar, interactuar y aplicar el alcance de variables en JavaScript. Introduciremos nuevos conceptos y palabras clave JavaScript importantes como varletconst.


Declaración de variables

Hoy en día, JavaScript tiene tres palabras clave diferentes para declarar una variable varletconst,. Cada uno con sus propias propiedades y particularidades. Comencemos haciendo una tabla comparativa simple de los 3 y luego entremos en los detalles.

Palabra clave Alcance Izar Se puede reasignar
var Función si si
dejar Bloquear No si
constante Bloquear No No

No se preocupe si, por ahora, no está seguro de lo que queremos decir con alcance, elevación o cualquiera de los otros atributos. Los cubriremos en detalle a continuación.


Alcance variable

El alcance en JavaScript se refiere al contexto (o parte) del código que determina la accesibilidad (visibilidad) de las variables. En JavaScript, tenemos 2 tipos de alcance, local y global . Aunque el alcance local puede tener diferentes significados.

Analicemos las definiciones dando algunos ejemplos de cómo funciona el alcance. Digamos que define una variable message:

const message = 'Hello World'
console.log(message) // 'Hello World'

Como es de esperar, la variable messageutilizada en console.logexistiría y tendría el valor Hello World. No hay dudas ahí, pero qué pasa si cambio un poco donde declaro la variable:

if (true) {
    const message = 'Hello World'
}
console.log(message) // ReferenceError: message is not defined

Ups … Parece que lo rompimos, pero ¿por qué? La cuestión es que la ifdeclaración crea un alcance de bloque local y, dado que usamos const, la variable solo se declara para ese alcance de bloque y no se puede acceder desde el exterior.

Hablemos un poco más sobre los alcances de bloques y funciones.

Alcance del bloque

Un bloque es básicamente una sección de código (cero o más declaraciones) que está delimitada por un par de llaves y, opcionalmente, puede etiquetarse.

Como ya comentamos, el uso de letconstnos permite definir variables que viven dentro del alcance del bloque. A continuación, crearemos ejemplos muy similares utilizando diferentes palabras clave para generar nuevos ámbitos:

const x1 = 1
{
    const x1 = 2
    console.log(x1) // 2
}
console.log(x1) // 1

Expliquemos este, ya que puede parecer un poco extraño al principio. En nuestro ámbito externo, estamos definiendo la variable x1con un valor de 1. Luego creamos un nuevo alcance de bloque simplemente usando llaves, esto es extraño, pero totalmente legal dentro de JavaScript, y en este nuevo alcance, creamos una nueva variable (separada de la del alcance externo) también nombrada x1. Pero no se confunda, esta es una variable completamente nueva, que solo estará disponible dentro de ese alcance.

El mismo ejemplo ahora con un alcance con nombre:

const x2 = 1
myNewScope: { // Named scope
    const x2 = 2
    console.log(x2) // 2
}
console.log(x2) // 1

Mientras que el ejemplo ( NO EJECUTE EL CÓDIGO A CONTINUACIÓN !!!!!!!!!!!!!!!! )

const x3 = 1
while(x3 === 1) {
    const x3 = 2
    console.log(x3) // 2
}
console.log(x3) // Never executed

¿Puedes adivinar qué pasa con ese código? ¿Y qué pasaría si lo ejecutas? … Déjame explicarte, x3como se declara en el alcance externo se usa para la comparación while x3 === 1, normalmente dentro de la declaración while, podría reasignar x3un nuevo valor y salir del ciclo, sin embargo, como estamos declarando un nuevo x3dentro del alcance del bloque, ya no podemos cambiar x3del alcance externo y, por lo tanto, la condición while siempre se evaluará para trueproducir un bucle infinito que colgará su navegador, o si está usando una terminal para ejecutarlo en NodeJS imprimirá mucho 2.

Arreglar este código en particular podría ser complicado a menos que realmente cambie el nombre de cualquiera de las variables.

Hasta ahora en nuestro ejemplo, usamos const, pero exactamente el mismo comportamiento sucedería con let. Sin embargo, vimos en nuestra tabla de comparación que la palabra clave vares en realidad el alcance de la función, entonces, ¿qué significa para nuestros ejemplos? Bueno … echemos un vistazo:

var x4 = 1
{
    var x4 = 2
    console.log(x4) // 2
}
console.log(x4) // 2

¡Asombroso! a pesar de que volvimos a declarar x4dentro del alcance, cambió el valor a 2en el alcance interno así como en el alcance externo. Esta es una de las diferencias más importantes entre letconsty, var y suele estar sujeta (de una forma u otra) a las preguntas de la entrevista.

Alcance de la función

Un ámbito es la función de una manera también un ámbito de bloque, por lo que letconstse comportaría de la misma manera que lo hicieron en los ejemplos anteriores. Sin embargo, los ámbitos de función también encapsulan las variables declaradas con var. pero veamos eso continuando con nuestros xnejemplos:

constletejemplo:

const x5 = 1
function myFunction() {
    const x5 = 2
    console.log(x5) // 2
}
myFunction()
console.log(x5) // 1

Exactamente como lo esperábamos, y ahora con var

var x6 = 1
function myFunction() {
    var x6 = 2
    console.log(x6) // 2
}
myFunction()
console.log(x6) // 1

En este escenario, var funcionó de la misma manera que letconst. Además:

function myFunction() {
    var x7 = 1
}
console.log(x7) // ReferenceError: x7 is not defined

Como podemos ver, las vardeclaraciones solo existen dentro de la función en la que fueron creadas y no se puede acceder a ellas desde el exterior.

Pero hay más, como siempre, JS ha estado evolucionando y se han creado nuevos tipos de ámbitos.

Alcance del módulo

Con la introducción de módulos en ES6, era importante que las variables de un módulo no afectaran directamente a las variables de otros módulos. ¿Te imaginas un mundo en el que la importación de módulos de una biblioteca entrara en conflicto con tus variables? ¡Ni siquiera JS es tan desordenado! Así por módulos de definición de crear su propio ámbito que encapsula todas las variables creadas con varletconst, similar al ámbito de la función.

Sin embargo, hay formas en las que los módulos proporcionan para exportar variables para que se pueda acceder a ellas desde fuera del módulo, y eso ya lo cubrí en el artículo Introducción a los módulos de JavaScript .

Hasta ahora hablamos sobre diferentes tipos de ámbitos locales , ahora vamos a sumergirnos en los ámbitos globales .

Alcance global

Una variable definida fuera del alcance de cualquier función, bloque o módulo tiene alcance global. Se puede acceder a las variables de alcance global desde cualquier lugar de la aplicación.

El alcance global a veces se puede confundir con el alcance del módulo, pero este no es el caso, se puede utilizar una variable de alcance global en todos los módulos, aunque esto se considera una mala práctica y por buenas razones.

¿Cómo haría para declarar una variable global? Depende del contexto, es diferente en un navegador que en una aplicación NodeJS. En el contexto del navegador, puede hacer algo tan simple como:

<script>
    let MESSAGE = 'Hello World'
    console.log(MESSAGE)
</script>

O usando el objeto de ventana:

<script>
    window.MESSAGE = 'Hello World'
    console.log(MESSAGE)
</script>

Hay algunas razones por las que quieres hacer algo como esto, sin embargo, siempre ten cuidado cuando lo hagas.

Ámbitos de anidamiento

Como probablemente ya habrá adivinado, es posible anidar ámbitos, es decir, crear un ámbito dentro de otro ámbito, y es una práctica muy común. Simplemente agregando una ifdeclaración dentro de una función, estamos haciendo esto. Veamos un ejemplo:

function nextedScopes() {
    const message = 'Hello World!'

    if (true) {
        const fromIf = 'Hello If Block!'
        console.log(message) // Hello World!
    }

    console.log(fromIf) // ReferenceError: fromIf is not defined
}

nextedScopes()

Alcance léxico

En cierto modo, ya hicimos uso del alcance léxico, aunque no lo sabíamos. El ámbito léxico simplemente significa que los ámbitos secundarios tienen acceso a las variables definidas en los ámbitos externos.

Veámoslo con un ejemplo:

function outerScope() {
    var name = 'Juan'
    function innerScope() {
        console.log(name) // 'Juan'
    }

    return innerScope
}

const inner = outerScope()
inner()

Parece más extraño de lo que es, así que vamos a explicarlo. La función outerScopedeclara una variable namecon valor Juany una función nombrada innerScope. El último no declara ninguna variable para su propio ámbito, pero hace uso de la variable namedeclarada en el ámbito de la función externa.

Cuando outerScope()se llama, devuelve una referencia a la innerScopefunción, que luego se llama desde el ámbito más externo. Al leer este código por primera vez, puede confundirse en cuanto a por innerScopequé console.logel valor Juancomo lo llamamos desde el alcance global, o el alcance del módulo, donde nameno se declara.

La razón por la que esto funciona es gracias a los cierres de JavaScript. Los cierres son un tema en sí mismo y puede leer más sobre él en los documentos de MDN . Estoy planeando un artículo para explicar los cierres en términos simples, pero no está listo en el momento de escribir este artículo.


Izar

Elevar en términos de JavaScript significa que una variable se crea en la memoria durante la fase de compilación y, por lo tanto, se pueden usar antes de que se declaren. Suena muy confuso, veámoslo mejor en código.

Así es como se vería un flujo normal:

function displayName(name) {
    console.log(name)
}

displayName('Juan')

//***********************
// Outputs
//***********************
// 'Juan'

¡Increíble! como se esperaba que funciona, pero ¿qué pensaría de lo siguiente:

hoistedDisplayName('Juan')

function hoistedDisplayName(name) {
    console.log(name)
}

//***********************
// Outputs
//***********************
// 'Juan'

Espera espera espera…. ¿Qué? Por loco que parezca, dado que la función se asigna a la memoria antes de que el código se ejecute realmente, la función hoistedDisplayNameestá disponible antes de su definición real, al menos en términos de líneas de código.

Las funciones tienen esta propiedad particular, pero también las variables declaradas con var. Veamos un ejemplo:

console.log(x8) // undefined
var x8 = 'Hello World!'

¿No es lo que adivinaste? El hecho de que la variable sea “creada” antes de su definición real en el código no significa que su valor ya esté asignado, es por eso que cuando hacemos console.log(x8)lo no obtenemos un error diciendo que la variable no está declarada, pero más bien la variable tiene valor undefined. Muy interesante, pero ¿qué pasa si usamos letconst? Recuerde en nuestra tabla que no comparten esta propiedad.

console.log(x9) // Cannot access 'x9' before initialization
const x9 = 'Hello World!'

Lanzó un error.

La elevación es una propiedad menos conocida de las variables de JavaScript, pero también es importante. Asegúrese de comprender las diferencias, es importante para su código y puede ser un tema para una pregunta de entrevista.


Reasignación de variables

Este tema cubre específicamente las variables declaradas con la palabra clave const. Una variable declarada con constno se puede reasignar, lo que significa que no podemos cambiar su valor por uno nuevo, pero hay un truco. Veamos algunos ejemplos:

const c1 = 'hello world!'
c1 = 'Hello World' // TypeError: Assignment to constant variable.

Como esperábamos, no podemos cambiar el valor de una constante, ¿o no?

const c2 = { name: 'Juan' }
console.log(c2.name) // 'Juan'
c2.name = 'Gera'
console.log(c2.name) // 'Gera'

¿Acabamos de cambiar el valor de un constvalor? La respuesta corta es NO . Nuestra constante hace c2referencia a un objeto con una propiedad namec2es una referencia a ese objeto, ese es su valor. Cuando lo hacemos c2.name, realmente estamos llevando el puntero al c2objeto y accediendo a la propiedad desde allí. Lo que estamos cambiando cuando lo hacemos c2.namees el valor de la propiedad nameen el objeto, pero no la referencia almacenada en c2, y por lo tanto c2permanece constante aunque el valor de la propiedad ahora es diferente.

Vea lo que sucede cuando en realidad intentamos actualizar el valor de manera diferente:

const c3 = { name: 'Juan' }
console.log(c3.name) // 'Juan'
c3 = { name: 'Gera' } // TypeError: Assignment to constant variable.
console.log(c3.name)

Aunque el objeto tiene el mismo aspecto, en realidad estamos creando un nuevo objeto { name: 'Gera' }e intentando asignar ese nuevo objeto a c3, pero no podemos, ya que se declaró como constante.