lunes, 11 de febrero de 2019

TypeScript: Let, la nueva forma de declarar variables en Javascript

Una de los primeras cosas que aprendimos hace ya más de décadas cuando íbamos descubriendo poco a poco el lenguaje BASIC fue el comando LET para asignar valores a nuestras variables. Es patente que la retroinformática está de moda y que la Historia está destinada a repetirse una y otra vez porque será a finales de este 2014 cuando el nuevo ECMAScript 6 resucite esta palabra reservada para precisamente eso: asignar valor a nuestras variables en Javascript.

Veamos qué cosas implica esta nueva forma de declarar variables…


El contexto léxico.

Cuando un programador llega por primera vez a Javascript, especialmente si proviene de otro lenguaje como Java, se encuentra con lo arbitrario que parece el contexto al que se asocian las variables… En Javascript, cuando definimos una variable con la palabra reservada ‘var’, su visibilidad viene marcada por la ‘closure’ en la que se encuentra. Y las closures en este lenguaje las delimitan las funciones.

function foo () {
    var bar = 'Hello World';

    console.info( bar ); // Hello World
}

console.info( bar ); // ReferenceError: bar is not defined

Este ejemplo es muy intuitivo: si preguntamos por ‘bar’ dentro de la función, el intérprete nos devuelve el valor correspondiente. Si lo hacemos desde fuera, obtenemos un error por indefinición. Poca novedad hasta aquí.

Sin embargo, este otro ejemplo puede no ser tan natural para ciertos desarrolladores:

for( var x = 0; x < 1000; x++ ) {
    // Awesome code goes here...
}

console.info( x ); // 1000

En ese último caso, la variable ‘x’ que hemos usado como contador, ‘trasciende’ más allá del bucle para continuar existiendo fuera de él. En términos de Javascript tiene sentido ya que como hemos dicho, las closures, las marcan las funciones y en un bucle for no hay función. Sin embargo, desde un punto de vista de arquitectura del software, no tiene sentido que la variable contador del bucle siga existiendo más allá contaminando el contexto global.

De igual forma en Javascript aunque es poco frecuente, podemos estructurar nuestro código utilizando bloques. Ya lo vimos en un viejo artículo de este mismo blog, encontrándonos con el mismo problema:

{
    var foo = 'Hello World',
bar = 'Goodbye Lenin',
concat,
result;

    concat = function ( ...params ) {
return params;
    }

    result = concat( foo, bar );

    console.info( result );
}

console.info( result );
// ["Hello World", "Goodbye Lenin"]

Como vemos, aunque tenemos un bloque que claramente delimita nuestro código, las variables que creamos dentro tienen visibilidad más allá de ese contexto léxico. De nuevo, se asocian a su closure por encima (ya sea una función o el mismo contexto global).

Por tanto, llegados a este punto podemos decir que hasta ahora, Javascript ha tenido una importante carencia a la hora de asignar variables a contextos léxicos en lugar de closures. En realidad, lo que queremos decir con el contexto léxico es aquel delimitado por llaves en lugar de funciones, lo cual tiene mucho sentido en estructuras de tipo bucles, condicionales, bloques, etc…

LET asigna valor únicamente al contexto entre llaves.

Pues esa es la idea: let permite asignar valor a nuestras variables con visibilidad / validez únicamente dentro del contexto léxico (llaves) en el que se ha definido:

for ( let x = 0; x < 2; x++ ) {
    console.info( x ); // 0, 1
}

console.info( x ); // ReferenceError: x is not defined

Esa es la idea: la variable contador no tiene porqué vivir más allá del bucle para el que ha sido creada… igual ocurre con cualquier otra estructura de bloque (un if, un switch…):

if ( true ) {
    let y = 'Hello World';

    console.info( y ); // 'Hello World'
}

console.info( y ); // ReferenceError: y is not defined

De nuevo vemos como el bloque del if que está marcado por llaves, delimita la visibilidad de las variables declaradas con let.

Esto significa tanto una mejor optimización del código como un uso más eficiente del recolector de basura del intérprete Javascript.

Múltiples declaraciones.

Esta nueva fórmula let permite, al igual que var, encadenar múltiples asignaciones de forma consecutiva separadas por comas:

{
    let foo = 'Hello World',
        bar = 'Goodbye Lenin';

    console.info( foo, bar );
}

console.info( foo, bar );

Declaraciones todo terreno.

Como ya hemos mencionado, let es la nueva forma de declarar variables y, dado que en Javascript se puede asignar como valor cualquier otro objeto, todo lo que sigue a continuación es perfectamente válido:

{
    let
        // A primitive
        fooPrimitive = new Boolean( true ),

        // A falsy value
        fooFalsy = null,

        // An array
        fooArr = [ 'Hello World', 'Goodbye Lenin' ],

        // An object
        fooObj = { keyOne: 'value', keyTwo: 'value' },

        // A constructor
        FooConstructor = function () {
            this.foo = 'paramOne';
            this.bar = 'paramTwo';
        },

        // An immediately-Invoked Function Expression
        fooIIFE = ( function () {
            let foo = function () {
                console.info( 'Hello from fooIIFE' );
            };

            return {
                foo: foo
            }
        } )();

    console.info( fooPrimitive, fooFalsy, fooArr, fooObj, new FooConstructor(), fooIIFE.foo() );
}

A priori, no encontramos ninguna restricción o diferencia con respecto a var.

Estructuración de las declaraciones.

Como en el caso de las closures tradiciones con var, una variable definida dentro de un bloque léxico con let tiene preferencia sobre una variable externa con igual nombre:

var foo = 'Goodbye Lenin';

if ( foo ) {
    let foo = 'Hello World';

    console.info( foo ); // 'Hello World'
}

console.info( foo ); // Goodbye Lenin

Aquí vemos que aunque la variable ‘foo’ se ha definido fuera del bloque if como global, al redefinirse dentro de su contexto léxico con let, su nuevo valor tiene preferencia. Una vez salimos de las llaves, volvemos a su valor dentro de la closure.

Hoisting.

El hoisting es una peculiaridad del lenguaje que ya vimos en su momento, pero que básicamente quiere decir que una variable declarada ‘sube’ o ‘flota’ hasta lo más alto de la función (closure) en la que se encuentre independientemente de dónde la declaremos:

var foo = function () {
    console.log( bar );

    var bar = 'Hello World';
}

foo(); // undefined

Ojo al ejemplo anterior; aunque pregunto por ‘bar’ antes de declararla, Javascript no me da un error indicando que la variable no está definida, sino que me dice que no tiene valor… Esto es así porque, por detrás, el intérprete está reescribiendo el código anterior de la siguiente forma:

var foo = function () {
    var bar;
    console.log( bar );

    bar = 'Hello World';
}

foo(); // undefined

Javascript ‘observa’ que hay una declaración de una variable dentro de la función y, antes de ejecutarla, sube la declaración de esta variable hasta arriba del todo, aunque no le dé el valor hasta que llegue al punto del código en el que nosotros indicamos la declaración. Por eso, cuando el console pregunta por bar, realmente si está definida, aunque no tiene valor. Ese particular comportamiento de Javascript es lo que llamamos Hoisting.

¿Y que ocurre con let y el hoisting? Se conserva.

Quizá habría sido interesante aprovechar esta nueva estructura para corregir ese comportamiento y volverlo más natural o intuitivo, pero quizá eso provocaría más dudas por lo inconsistente de ambas fórmulas (let y var). Sea como fuere, la cuestión a tener en cuenta es esa: con let, continuamos sufriendo el hoisting.

Integridad declarativa.

Otra funcionalidad importante que si se ha incorporado a este tipo de declaración, es la integridad declarativa dentro un bloque. Eso quiere decir que, no es posible declarar dos veces la misma variable dentro de un mismo contexto léxico:

{
    let foo = 'Hello World';
    let foo = 'Goodbye Lenin';
}

// TypeError: redeclaration of variable foo
Esto también es extensible si usamos var dentro de un bloque:

{
    let foo = 'Hello World';
    var foo = 'Goodbye Lenin';
}

// TypeError: redeclaration of variable foo

Como podemos observar, esto permite cierta integridad que no era posible con ‘var’ y nos salvaguarda de re declarar una variable accidentalmente.

Anidación.

Como ya hemos visto antes, pero lo rematamos ahora, cada bloque crea su propio contexto y eso nos permite redefinir de nuevo las variables del anterior:

{
    let foo = 'block-1';
    console.info( foo ); // block-1
    {
        let foo = 'block-2';
        console.info( foo ); // block-2
        {
            let foo = 'block-3';
            console.info( foo ); // block-3
            {
                let foo = 'block-4';
                console.info( foo ); // block-4
            }
        }
    }
}

El ejemplo anterior es autoexplicativo: podemos redefinir indefinidamente la misma variable en cada nivel de anidación. En la práctica, nunca tendremos un código con tanta profundidad, pero ilustra bien este supuesto.

Y eso es todo.

No hay comentarios:

Publicar un comentario