Como funciona Javascript – Parte 2

Como funciona Javascript

En la entrada anterior sobre como funciona Javascript aprendimos algunos conceptos sobre el funcionamiento interno de Javascript.

Todo estos conceptos son importante conocerlos ya que nos permite entender como funciona Javascript, y de esta forma crear un código óptimo.

Para ello continuaremos viendo nuevos conceptos que se apoyan en los aprendidos en la entrada anterior de Javascript.

El entorno léxico (Lexical environment)

En Javascript existe un termino llamado entorno léxico (lexical environment), y lo podemos definir como el entorno donde esta escrito nuestro código.

Como veremos a continuación, el contexto de ejecución (execution context) nos dice que entorno léxico (lexical environment) se esta ejecutando en ese momento.

El contexto de ejecución (Execution context)

El contexto de ejecución (Execution context), lo podemos definir como el entorno donde se ejecuta una función y el ámbito de una variable.

En base a la definición anterior, cada vez que se inicia nuestro motor (engine) de Javascript, este genera un contexto de ejecución global, y cada vez que ejecutamos una función se genera un nuevo contexto que es totalmente independiente entre si.

Esta acción nos ofrece en el contexto global los objetos window y this, que aunque en este punto son lo mismo, veremos que el objeto this tiene un comportamiento un tanto peculiar.

console.log('Contexto de ejecución global'); 
// Contexto de ejecución global

console.log('WINDOW:', window);
/* WINDOW: Window {0: global, 1: Window, window: Window, self: Window, document: document, name: "", location: Location, …} 
*/
console.log('THIS:', this);
/* THIS: Window {0: global, 1: Window, window: Window, self: Window, document: document, name: "", location: Location, …} 
*/

const day = 'Friday';
function suma(a, b) {
   console.log('Contexto de ejecución en la función "suma()"');
   console.log('Que día es: ', day);
   return a + b;
}

console.log(suma(10, 10));

/*
Contexto de ejecución en la función "suma()"
Que día es: Friday
20
*/

Como hemos observado en el código anterior, el contexto de ejecución tiene una fase que se realiza automáticamente y que se llama fase de creación.

En ese punto hemos visto que se crean los espacios de memoria, el objeto global window y el objeto this que tendrá el valor del contexto que se este ejecutando en ese momento, de allí el comportamiento peculiar que habíamos comentado anteriormente.

A continuación veremos que esa asignación de memoria que se esta produciendo en la fase de creación, tiene un termino conocido como hoisting.

¿Que es el hoisting?

Se puede definir como el movimiento de las variables y las funciones al inicio de nuestro código (siempre en su respectivo entorno), pero…. en realidad esto no es correcto.

Lo que esta sucediendo es que las variables y las funciones están siendo asignadas en memoria y por eso tenemos acceso a ellas, realmente el código continua en su sitio.

(() => {
    function suma(a, b) { 
        return a + b;
    }
    console.log(suma(5, 5));       // 10
    console.log(resta(100, 20));   // 80
    function resta(a, b) {
        return a - b; 
    }
})();

Otro ejemplo para entender como funciona el hoisting, lo podemos comprobar cuando realizamos la definición de una función, y es que no es lo mismo tener una función declarativa que una función expresiva.

En la función declarativa se aplica el hoisting de tal forma que se esta reservando espacio en la memoria y podemos invocarla.

En la función expresiva no se aplica el hoisting como es normal.

(() => {
    fnDeclaration(); // fnDeclaration
    fnExpresion();   // Uncaught TypeError: fnExpresion is not a function
    const fnExpresion = function() {
        console.log('fnExpresion');
    }
    function fnDeclaration() {
        console.log('fnDeclaration');
    }
})();

Aunque esto funcione, personalmente me parece muy mala practica, genera caos y un código poco legible.

La segunda fase llamada fase de ejecución, ejecutara el código que se ha definido en la fase de creación, en nuestro caso la función suma.

Continuando en la fase de ejecución podemos encontrar otro concepto llamado Scope y que hace referencia al contexto actual de ejecución.

¿Que es el Scope?

Lo podemos definir como el alcance o la capacidad que tenemos para acceder a los valores que son accesibles o referenciados.

Esto significa que si tenemos por ejemplo una variable y esta no existe en el contexto actual de ejecución (scope), entonces no estará disponible.

A continuación podemos ver algunos ejemplos de scope para entender como funciona

(() => {
    const global = 'Global';

    const fnScope1 = () => {
        const scope1 = '1';
        console.log(scope1);
        console.log(global);
    }

    const fnScope2 = () => {
        const scope2 = '2';
        console.log(scope2);
        console.log(global);
    }

    const fnScope3 = () => {
        const scope3 = '3';
        console.log(scope3);
        console.log(global);
    }

    fnScope1();  // 1, Global
    fnScope2();  // 2, Global
    fnScope3();  // 3, Global
})();

En el ejemplo anterior tenemos el scope (global) que es accesible desde cada función, y a su vez cada función tiene su propio scope (scope1, scope2, scope3) que solo es accesible desde su propio scope pero pueden acceder al scope superior.

Otro ejemplo interesante donde podemos ver el uso de los scopes son en las closures.

A continuación un pequeño ejemplo.

(() => {
    const data = a => {
        let scope1 = a;
        console.log('SCOPE 1', scope1); // SCOPE 1 1
        return b => {
            let scope2 = b;
            console.log('SCOPE 2', scope1, scope2); // SCOPE 2 1 2
            return c => {
                let scope3 = c;
                console.log('SCOPE 3', scope1, scope2, scope3); // SCOPE 3 1 2 3
                return `${scope1}-${scope2}-${scope3}`;
            }
        }
    }
    const a = data(1);
    const b = a(2);
    const c = b(3);
    console.log('RESULT:', c); // RESULT: 1-2-3

    // Forma abreviada
    const dataAbrev = a => b => c => `${a}-${b}-${c}`;
    const r = dataAbrev(10)(20)(30);
    console.log('RESULT 2:', r); // RESULT 2: 10-20-30

})();

En el ejemplo anterior podemos acceder desde un nivel inferior a un nivel superior sin problemas, pero si intentásemos acceder desde un nivel superior a un nivel inferior fallaría ya que cada función tiene su scope.

Ahora que sabemos que es el scope, ya podemos entender la diferencia entre el alcance en una función (function scope) y el alcance de bloque (block scope).

La diferencia entre function scope y block scope es la siguiente:

  • En el function scope cualquier variable declarada dentro de la misma es visible en cualquier sitio dentro de esa función.
  • En el block scope las variables definidas son visibles solo en el bloque encerrado entre las llaves.

A continuación un pequeño ejemplo.

(() => {
    // Function scope (IIFE Start)
    let data1 = 'DATA1';
    function fnScope() {
        // Function scope (Start)
        let data2 = 100;
        console.log('Function Scope', data2, data1);  // Function Scope 100 DATA1
        // Function scope (End);
    }

    {
        // Block scope (Start)
        let data3 = true;
        console.log('Other block scope', data3, data1); // Other block scope true DATA1
        // Block scope (End)    
    }
    console.log(data2);     // Uncaught ReferenceError: data2 is not defined
    console.log(data3);     // Uncaught ReferenceError: data3 is not defined
    fnScope();
    for (let cont = 0; cont < 10; cont++) {
       // Block scope (Start)
       console.log(cont); // 0 1 2 3 4 5 6 7 8 9
       // Block scope (End)
    }

    // Function scope (IIFE End)
})();

En el ejemplo anterior podemos ver que tenemos hasta 4 scopes diferentes:

  • El primero que es muestra IIFE y que esta limitado por la llaves que van de la línea 1 y la línea 27. (function scope).
  • El segundo que esta limitado por las llaves que van de la línea 4 a la línea 9. (function scope).
  • El tercero que esta limitado por las llaves que van de la línea 11 a línea 16. (block scope).
  • Y el cuarto y último scope que va desde la línea 20 a la línea 24. (block scope).

Debemos saber que esto es posible gracias al uso de let const cuando declaramos nuestras variables ya que respeta el scope donde fue declarada, cosa que no ocurre si usamos var.

Otro concepto que debemos entender y que esta muy relacionado con todo lo aprendido hasta ahora, es el famoso objeto this.

¿Que es this?

Al comienzo de esta entrada vimos que en la fase de creación, se creaban dos objetos muy importantes, window this.

Como dijimos, el objeto this es bastante peculiar ya que su contenido varía dependiendo de el lugar en el que se invoca, así que vamos a realizar algunos ejemplos para entender su comportamiento.

Vamos a crear una función llamada Suma desde la consola del navegador, por lo tanto el contexto actual es el contexto global y podemos decir que es window.

this1

this2

function Suma(a, b) {
   console.log('Suma', this);
   return a + b; 
}

console.log(Suma(1, 2));
// Window: { 0: global, ..... }
// 3

console.log(window.Suma(3, 3));
// Window: { 0: global, ..... }
// 6

Ahora vamos hacer un ejemplo para ver como cambia el valor del objeto this.

const obj = {
   status: true,
   getStatus: function() {
      console.log('Get Status', this);
      return this.status;
   },
   getStatus2: () => {
      console.log ('Get Status 2', this);
      return this.status;
   }
}

console.log(obj.getStatus());
// Ges Status {status: true, getStatus: ƒ, getStatus2: ƒ}
// true

console.log(obj.getStatus2());
// Get Status 2 Window {0: global, window: Window, self: Window, document: document, …}
// ""

Si observamos el resultado anterior podemos comprobar que cuando en el objeto obj usamos funciones flecha para definir un método, este toma para this el valor del entorno léxico y como el objeto obj forma parte de window, el valor de this sería window.

Esto es debido a que en la versión de ECMAScript 2015 (ES6) las arrow function no tienen su propio this, usando para el this el correspondiente al entorno léxico adjunto.

Como el método getStatus() esta definido como una función convencional podemos decir que el el entorno léxico no sería window, por lo tanto en este caso this tendría el valor del objeto obj.

En el siguiente punto veremos que existen otras formas de trabajar con this usando los métodos bind, call y apply para gestionar el valor de this.

Usando los métodos bind, call y apply

Bind, call y apply son unos métodos que nos permiten modificar el valor del objeto this, ya que como vimos al principio de la entrada dependiendo del contexto de ejecución (execution context), el valor de this puede cambiar de valor.

Ahora que sabemos que con estos métodos podemos manipular el valor de this, vamos a ver como hacerlo con algunos ejemplos.

  • bind: El método bind nos permite crear una función nueva con el mismo contenido que la función vinculada pero pudiendo asociar el valor que necesitemos al objeto this.
    • const externalData = {
          title: 'external',
          value: 100,
          active: true
      };
      
      const personalData = {
          title: 'internal',
          value: 0,
          active: false,
          getTitle: function() {
              return this.title;
          },
          getValue: function() {
              return this.value;
          },
          getActive: function() {
              return this.active;
          },
          showArguments: function() {
              console.log(...arguments, this);
          }
      };
      
      // function
      console.log('title:', personalData.getTitle());    // title: internal
      console.log('value:', personalData.getValue());    // value: 0
      console.log('active:', personalData.getActive());  // active: false
      
      // BIND
      const fnTitle = personalData.getTitle;
      const resultTitle = fnTitle.bind(externalData);
      
      console.log('bind data:', resultTitle());          // bind data: external
      
      const fnShow = personalData.showArguments.bind(externalData, 1, 2, 3);  
      fnShow('*', 5, 6, 7, 8, 9, '*'); 
      // 1 2 3 "*" 5 6 7 8 9 "*" {title: "external", value: 100, active: true}
      
      function showThis() {
         console.log('THIS:', this);
      }
      
      showThis(); // Window { window: Window, self: Window, document: document, ...}
      
      const fn = showThis.bind({ data: 100});
      fn();       // THIS: { data: 100 }
      
      showThis.bind({ data: 200 })();  // THIS: { data: 200 }
    • En el código anterior podemos observar en las líneas 31 32 como estamos haciendo el uso de bind y quizás ahora es mas sencillo entender su definición.
    • La sintaxis del método bind recibe como primer argumento el valor que tendrá el objeto this, y los siguientes argumentos se enviaran junto a los que se tenga la función vinculada (linea 36 y 37).
  • call: Con este método podemos llamar a una función indicando el valor para el objeto this y además enviar los argumentos individualmente.
    • function showThis() {
          console.log('THIS:', this, arguments);
      }
      
      showThis.call(null, 4, 5, 6); 
      // THIS: Window {0: global, …} Arguments(3) [4, 5, 6, callee: ƒ, Symbol(Symbol.iterator): ƒ]
      showThis.call({ data: 100 }, 7, 8, 9); 
      // THIS: {data: 100} Arguments(3) [7, 8, 9, callee: ƒ, Symbol(Symbol.iterator): ƒ]
  • apply: Funciona igual que el método call, pero en apply los argumentos se pasan mediante un array.
    • function showThis(...data) {
          console.log('THIS:', this, arguments, data);
      } 
      
      showThis.apply(null, [4, 5, 6]);             // THIS: Window {0: global, window: Window, …} Arguments(3) [4, 5, 6, callee: (...), Symbol(Symbol.iterator): ƒ] (3) [4, 5, 6]
      showThis.apply({ data: 100 }, [4, 5, 6]);    // THIS: {data: 100} Arguments(3) [4, 5, 6, callee: (...), Symbol(Symbol.iterator): ƒ] (3) [4, 5, 6]

Con esto terminamos la segunda parte de este pequeño curso sobre como funciona Javascript.