En construcción
Inicio: 09/01/2021
Estreno-aprox: 15/02/2021
% Avance:

10 Fundamentos que todo desarrollador Front-End debe dominar

Existen muchas y muchos desarrolladores con experiencia en esta industria, los cuales a lo largo de los años han logrado abrirse paso en medio de este mar de tecnologías, lenguajes y frameworks del mundo de la programación y lo han hecho bien hasta ahora, pero una parte de ellos(y no una insignificante) aún siente que mucho de esto funciona como una suerte de Arte de Magia, lo cual desde un punto de vista holístico, como el de la Ingeniería de Software, no es lo ideal.

El conocer respuestas a interrogantes tales como: "¿Qué es y como funciona el motor de JavaScript?", "¿Como crear y en que puedo utilizar cierta Estructura de Datos?" o "¿Cómo funciona el asincronismo en JavaScript?", entre otras, puede llevarte al siguiente nivel.

En las siguientes líneas encontrarás temas que puedes haber oído, quizás solo superficialmente, pero es momento de cambiar eso. Aquí inicia este artículo de los 5 Fundamentos que todo desarrollador Front-End debe dominar.

  1. Motores de JavaScript

    Como desarrolladores web, es bueno conocer los fundamentos del lenguaje Javascript y de su motor, este último con el fin de comprender como a partir de un lenguaje que los humanos podemos entender, se llega a convertir en algo que las maquinas pueden comprender y ejecutar.

    Diferentes Motores de Javascript

    Cada navegador tiene su propio motor de javascript, entre los más conocidos tenemos los siguientes:

    Infografía de los motores de Javascript
    Definición y distintos motores de JavaScript

    Revisaremos uno de los más utilizados, el V8 de Chrome:

    Infografía de V8 Chrome Engine
    V8 Chrome Engine

    Pero ¿cómo funciona?, veamos todo el recorrido que hace nuestro código en Javascript desde nuestro editor de código hasta que sirve a su propósito.

    Infografía de como funciona el motor V8 de Chrome
    Funcionamiento del motor V8 de Chrome

    Mostremos en código el bytecode y el machine code:

    Infografía de comparación de lenguajes de alto a bajo nivel: javascript - bytecode - machine code
    Javascript - Bytecode - Machine code

    Fuente: Franziska Hinkelmann

  2. Scope & Closures

    Scope

    Es el contexto de ejecución, en el cual los valores y expresiones son visibles o pueden ser referenciados, scopes pueden ser colocados en jerarquía, así el hijo o scope interno (inner scope) tiene acceso al padre o scope externo (outer scope), pero no viceversa. Scope es definido en tiempo de parseo.

    Ámbito Léxico / Lexical Scope

    Se basa en el lugar donde una variable fue declarada para determinar dónde esta variable estará disponible. Las funciones anidadas tienen acceso a las variables declaradas en su ámbito exterior.

    Pasemos a ver unos ejemplos de scope, pero con la siguiente pregunta en mente: ¿Cómo funciona realmente Javascript?

                
    //Exercise 1
    let a = 3;
    function addTwo(x) {
      let ret = x + 2;
      return ret;
    }
    let b = addTwo(a);
    console.log(b);
    //5
    
    //The compiler asks the global scope manager(gsm) if already exists a variable named "a", the gsm doesn't find it, so it creates it. then assigns to it the aritmethic expression, a literal value of 3, after that the gsm has a new request for creating a variable named addTwo, as it doesn't exists yet, it's created and is assigned to it a function definition. Still in the global scope a b variable is also created and assign for now an undefined, in that line, occurs a call expression addTwo() the gsm check if it exits in that scope and there it is, so executes it sending an a argument, so a is also checked and its value is 3 in the global scope, so the addTwo function have a parameter x, so it's created in the local execution context, and it's assigned the value of 3, so x = 3; then a ret variable is created in the function execution context and it's assign to it a binary expression (x + 2), the x = 3 and 2; 5 is assigned to ret, the next line is a return statement and finish the function, the 5 is returned when it's called and immediately the function instance as well as its local execution context and its variables (x, ret) are destroyed by the garbage colector.
    //Then again in the global scope the b variable is assigned with the 5 returned. and finally it's put to the console the value of b (5).
    
    
    
    //Exercise 2
    //Here we can see the lexical scope definition in practice: 
    //Lexical scope is when a function can access a variable declared in its outer scope(next outer scope and so on)
    //Or when a variable defined outside a function can be accessible inside that function
    
    //Lexical scope / Ambito Léxico / Scope Chain
    let val1 = 2;
    function multiplyThis(n) {
      let ret = n * val1;
      return ret;
    }
    let multiplied = multiplyThis(6);
    console.log('example of scope:', multiplied);
    //example of scope: 12
    
    
    //Exercise 3
    //A function that returns a function example is essential to understand closures
    let val = 7;
    function createAdder() {
      function addNumbers(a, b) {
        let ret = a + b;
        return ret;
      }
      return addNumbers;
    }
    let adder = createAdder();
    let sum = adder(val, 8);
    console.log('example of function returning a function:', sum);
    //example of function returning a function: 15
    
    
    // Line 55: We declare a variable val in the global execution context and assign the value 7 to that variable
    
    // Line 56: We declare a variable named createAdder and we assign a function definition to it (we put "function" before createAdder) all this happens int the global exection context, we have to remember that whatever is between the curly brackets {} is not executed, not even evalated, just stored the function definition into a variable for future use
    
    // Line 63: we declare a new variable, named adder in the global execution context, temporarily, undefined is assigned to adder. , we see round brackets() so we need to execute or call the function. let's query the gec's memory and look for a variable named createrAdder, it was created  before so let's call it.
    
    // Line 56: a new local execution context is created.the engine adds the new context to the call MediaStreamTrack, the function has no arguments, so let's jump right into the body of it.
    
    // Line 57-60, we have a new function declaration, we create the variable addNumbers in the local execution context( addNumbers just exists in this lec) and assign a function definition to it (function addNumber) , we store the function definition in the local variable named addNumber.
    
    // Line 61: We return the content of the variable addNumbers. The engine looks for a variable named addNumbers and finds it. It's a function definition, it's ok because this function(addNumbers) can return anything, including another function definition, so it return addNumbers definition- anything between the brackets on lines 58 and 59 makes up the function definition, we also remove the local execution context from the call stack.
    
    // After return, the lec is destroyed , the addNumbers variable doesn't exist anymore. the function definition still exists though, it was returned from the createAdder function and assigned to the variable adder, which was previously created.
    
    // Now in line 64, we define a new sum variable in the gec/global scope, we assign undefined to it.
    
    // Then we need to execute a function, the function named adder, we look it up in the global scope/gec and we find it, this function takes two parameters 
    
    // Let's retrieve these parameteres, so we can call the function (with the correct arguments) and pass it, remember that the addNumbers function definition was assigned to adder because of that we call adder(2 parameters), in this case val was defined before and the second parameter is 8
    
    // Now we have to execute that function (adder), a new local execution context (local scope) is created. Within the lec two new variables are created: a and b. they were assigned the values 7 and 8, as the arguments we passed before
    
    //A new variable is declared, name ret, it's declared in the lec, its value is set to undefined. Then an adition is performed, where we add the content of variable a and b, the result (15) is assigned to the ret variable
    
    // After that ret is returned, the lec is destroyed , it is removed from the call stack, the variables a, b and ret no longer exist.
    
    // The returned value is assigned to the variable named sum
    
    // We print out the value of sum to the console.
    
    
    
    // Finally we'll see a closure:
    function createCounter() {
      let counter = 0;
      const myFunction = function() {
        counter = counter + 1;
        return counter;
      }
      return myFunction;
    }
    const increment = createCounter();
     const c1 = increment();
     const c2 = increment();
     const c3 = increment();
     console.log('example increment:', c1, c2, c3);
     //example increment: 1 2 3
    
    
    //Line 101: The compiler finds a formal variable declaration with a function definition attach to it, then asks the global scope manager if the creatCounter variable exists, but the gsm says doesn't find any, so the compiler produce code that at execution time ask to create a new variable called students in that scope bucket.
    
    // Everything inside the createCounter function (beetween the curly brackets) it's not going to be processed yet (but it was already parsed- to make the AST). In modern web browser this is known as lazy compiling, it's going to be compiled when it's executed(called/invocked)
    
    //Now we are going to shorten the explanation
    
    //Line 109: An increment variable is going to be created in the global scope and then assigned a createCounter(), the "()" means that we call that function, so we find that variable declared and defined at line 101, a new local execturion context is created (a function scope).
    
    //Line 102: a counter variable is created in the createCounter scope and we assign the 0 to it.
    
    //Line 103: a MyFunction variable is declared and has a function definition assign to it, WE ALSO CREATE A CLOSURE AND INCLUDE IT AS PART OF THE FUNCTION DEFINITION. THE CLOSURE CAONTAINES THE VARIABLES THAT ARE IN SCOPE, IN THEIS CASE THE VARIABLE counter.
    
    //Everything from line 104 to 105 is not proccesed yet.
    
    //Line 107: we have a return statement , and the variable MyFunction is returned, the local scope manager is asked if the myFunction variable already exists, and the answer is yes, so because it has a function definition attached to it, it will return  the function definition (whatever is from line 104 to 105) and its closure. The garbage collector takes care of the local scopes and local variables created until now. 
    
    //Next we assign line 104-105 to the increment variable; so now that function definition is not labeled myFunction anymore, now is called increment and has a function definition including its closure.
    
    //Line 110: a c1 variable is created in the global scope and is assigned a callingExpression increment(), Now we are going to execute line 104-105, so it is created a increment local scope when a counter variables is checked, before looking in the local and global scope, let's check the CLOSURE, it contains the variable named counter, and after the expression in line 104, its value is set to 1, now the closure contains counter with its value 1.
    
    //The counter value is returned (1) and it's assigned to c1
    
    //Line 111: We repeat steps in line 133, c2 gets assigned 2
    
    //Line 112: We repeat steps in line 133, c2 gets assigned 3.
    
    //Line 113: The content of variables c1, c2 and c3 is logged in console.
    
    //So now we understand how closures works, the key to remember is that when a function gets declared, it contains a function definition and a closure. The closure is a collection of all the variables in the function's scope.
    
    // This works even in the global scope, yes it is created a closure as well but since these functions were created in the global scope, they have access to all the variables in the global scope, and the closure concept is not that relevant
    
    
    //Another example with 2 functions returned (two closures), we'll see the same effect
    let glob = "g";
    function f1() {
        let loc = "l";
        glob = glob + loc;
        function f2() {
           let loc2 = "l2";
           function f3() {
              loc2 = loc2 + loc + glob;
              return loc2;
           }
           return f3;
        }
        return f2;
    }
    const g1 = f1(); 
    const g2 = g1();
    const c1 = g2(); 
    const c2 = g2(); 
    console.log(c1); //l2lgl
    console.log(c2); //l2lgllgl
    
    
    
    //Bibliography:
    
    // Olivier De Meulder,"I never understood javascript closures.", Medium, https://medium.com/dailyjs/i-never-understood-javascript-closures-9663703368e8
    
    //Simpson, Kyle. You Don't Know JS Yet: Scope & Closures
    
    //MDN, Scope, https://developer.mozilla.org/en-US/docs/Glossary/Scope
                
              

    Closures

    Como ya vimos en el ejemplo práctico lineas arriba, un closure es formado por una función y es la combinación de la definición de esta función, así como su ámbito léxico que comprenden las variables locales definidas en este scope.

  3. Asincronismo

    El Asincronismo es...

  4. Objetos

    Los objetos son...

  5. Estructuras de Datos

    Según Wikipedia una estructura de datos es una forma particular de organizar datos en una computadora para que puedan ser utilizados de manera eficiente.

    También podemos definirla como una forma de organizar datos. Comprenden colecciones de valores, las relaciones entre ellos y las funciones y operaciones que se les puedan aplicar.

    El uso "eficiente" que se le dará dependerá del motivo a necesitar cierta estructura de datos. Quizás necesitemos una estructura donde se requiere una búsqueda rápida de elementos, o una inserción eficiente, etc, todo dependerá de que aplicación estemos haciendo.

    Aquí entra un tema importante: la complejidad.

    La Complejidad

    Este término hace referencia a como se expresan las ventajas y desventajas que tiene cada estructura de datos al utilizarse en un problema en particular. La complejidad se puede expresar en 2 ejes: El espacio y el tiempo.

    El espacio

    La complejidad del espacio representa el consumo de memoria de una estructura de datos.

    El tiempo

    La complejidad del tiempo se necesita expresar para varias operaciones que se puede usar en estructuras de datos, tales como insertar(agregar), eliminar, buscar y/ o acceder elementos.
    Infografía de tipos de estructuras de datos
    Principales tipos de Estructuras de Datos

    Podemos agrupar estas estructuras en 3 diferentes tipos:

    1. Las estructuras del tipo-Array, como los Arrays, Stacks y Queues las cuales se diferencian principalmente en la forma de insertar y remover elementos de ellas.
    2. Las Hash Tables dependen de hash functions para localizar y guardar información.
    3. Linked Lists, Trees y Graphs son estructuras con nodos que guardan referencias con otros nodos.
    ¿Quieres ver el código .js de todas estas estructuras de datos?. Puedes encontrarlo en GitHub en el repositorio Data Structures-JS

    Arrays

    Los Arrays(arreglos) son una colección de elementos, los cuales son emparejados con un key(índice), la forma más básica de esta estructura es el array lineal(array unidimensional).

    Los arrays son una de las estructuras más antiguas e importantes, así como de las más utilizadas en casi toda aplicación.

    Creo que todos hemos utilizado un array en JavaScript, tan solo inicializándolo así:
                
    var array1 = [1,3,4,5,7];
                
              
    Pero ya que estamos en el fundamento de: Estructura de Datos, vamos a crear una clase MyArray y en ella, crear métodos para buscar, agregar, remover valores del array. En pocas palabras veremos como crear desde cero métodos similares a los built-in methods .push(), .pop(), unshift(), .shift(), etc.
                
     class MyArray { 
       constructor() {
         this.length = 0;
         this.data = {};
       }
     }              
                
              
    Luego instanciaremos la clase MyArray:
    
              
    myArray = new MyArray();
              
              
    *Podemos utilizar el Inspector de elementos para trabajar esto, lo que nos da el siguiente output:
                
                  
    //MyArray {length: 0, data: {…}}
                  
                
              
    Como vemos esa estructura está vacía, tiene un lenght de 0 y no contiene data.

    Ahora creemos unos cuantos métodos para poder hacer operaciones sobre nuestro array. Los crearemos dentro de nuestra clase MyArray, previamente creada:
                
    
     class MyArray {
       constructor() {
         this.length = 0;
         this.data = {};
       }
       //Nuestro método get devuelve el valor del elemento con índice index
       get(index) {
         return this.data[index];
       }
       //Nuestro método push añade un valor al final del array
       push(item) {
         this.data[this.length] = item;
         this.length++;
         return this.data;
       }
       //Nuestro método pop remueve el último elemento del array
       pop() {
         const lastItem = this.data[this.length - 1];
         delete this.data[this.length -1];
         this.length--;
       }
       //Nuestro método delete elimina el elemento de indice index y devuelve el elemento removido.
       delete(index) {
         const item = this.data[index];
         this.shiftIndex(index);
         return item;
       }
       shiftIndex(index) {
         for(let i = index; i < this.length - 1; i++) {
           this.data[i] = this.data[i + 1];
         }
         delete this.data[this.length - 1];
         this.length--;
       }
       //Nuestro método unshiftItem añade un elemento(item) al inicio de nuestro array y devuelve la longitud del array
       unshiftItem(item) {
         this.length++;
         this.unshiftIndex();
         this.data[0] = item;
         
         return this.length;
       }
       unshiftIndex() {
         for(let i = this.length - 1; i > 0 ; i--) {
           this.data[i] = this.data[i-1];
         }
       }
       //Para nuestro método shiftItem vamos a remover el primer elemento de nuestro array, usaremos el método delete() creado anteriormente.
       shiftItem() {
         return this.delete(0);
       }
     }
                  
              

    Stacks

    Los stacks(pilas) son también una colección de elementos, están apilados y cuenta con 3 operaciones principales, peek(), push() y pop(), la forma en que se añaden y remueven elementos es a través del ordenamiento LIFO(Last in, first out), es decir el último que entra es el primero que sale. Un ejemplo práctico y conocido de un stack es el call stack.
    Imagen de un stack
    Diagrama de un stack
    Ahora pasemos a lo interesante, crearemos un stack usando clases como en el caso del array.
    Pero antes de eso expliquemos el concepto de nodos, que utilizaremos a lo largo del tema de estructura de datos, y si bien mencionamos que los Linked Lists, Trees y Graphs son estructuras basadas en nodos, también podemos tratar a cada elemento del stack(y más adelante al queue), como un nodo que tiene un valor(value) y que tiene una referencia a través de un puntero al siguiente elemento(el próximo en entrar).
                
     //Creamos una clase Node(nodo) para no repetir código en los métodos
     class Node {
       constructor(value) {
         this.value = value;
         this.next = null;
       }
     }
    
    class Stack {
      constructor() {
        this.top = null;
        this.bottom = null;
        this.length = 0;
      }
      //Nuestro método peek nos devuelve el elemento top(último en ingresar)
      peek() {
        return this.top;
      }
      //Nuestro método push agrega un elemento al (top) del stack y nos devuelve el stack
      push(value) {
        const newNode = new Node(value);
        if(this.length === 0) {
          this.top = newNode;
          this.bottom = this.top;
        } else {
          const holdingPointer = newNode;
          this.top.next = holdingPointer;
          this.top = holdingPointer;
        }
        this.length++;
    
        return this;
      }
      //Nuestro método pop remueve el elemento top y nos devuelve el stack
      pop() {
        var penultimo = myStack.bottom;
        for(let i = 0; i < this.length-2; i++) {
          penultimo = penultimo.next;
        }
    
        this.top = penultimo;
        this.top.next = null;
        this.length--;
    
        return this;
      }
    }
    //Instanciamos la clase Stack
    const myStack = new Stack();
                
              

    Queues

    Los queues son colas, sus operaciones principales son agregar elementos al final de la cola(enqueue) y remover el primer elemento de la cola (dequeue), usa el protocolo FIFO (First in First Out), lo que significa que el primero que entra a la cola, es el primero que sale al remover un elemento.
    Imagen de un queue
    Diagrama de un Queue
    Veamos el código:
                
    //Creamos una clase Node(nodo) para no repetir código en los métodos
    class Node {
      constructor(value) {
        this.value = value;
        this.next = null;
      }
    }
    
    class Queue {
      constructor() {
        this.first = null;
        this.last = null;
        this.length = 0;
      }
      //Nuestro método peek nos devuelve el elemento first(primero en la cola)
      peek() {
        return this.first;
      }
      //Con nuestro método enqueue agregaremos un elemento al final de la cola (last)
      enqueue(value) {
        const newNode = new Node(value);
        if(this.length === 0) {
          this.first = newNode;
          this.last = newNode;
        } else {
          this.last.next = newNode;
          this.last = newNode;
        }
        this.length++;
    
        return this;
      }
      //Con nuestro método dequeue removeremos el primer elemento de la cola
      dequeue() {
        if(this.length !== 0) {
        const second = myQueue.first.next;
        this.first = second;
        this.length--;
    
        return this;
        } else {
          console.log("Queue is empty, you cannot dequeue!")
        }
      }
      
    }
    //Instanciamos la clase Queue
    const myQueue = new Queue();
    
                
              

    Hash Tables

    Un Hash Table es una estructura de datos usada para implementar un array asociativo, donde se puede emparejar keys y values. Es similar a un objeto de JavaScript, con la diferencia que la hash table tiene un Hash Function que determina en que lugar de la memoria se ubicarán estos datos. Esta Hash Function recibe como argumento al key y retorna la dirección(address) de la locación en donde estará esta información. Pueden existir colisiones, lo que significa que puede haber dos keys diferentes para un mismo address, pero con el método set que veremos más adelante al enviar como parámetro al key, nos devolverá su value respectivo.
    Imagen de un hash table
    Diagrama de Hash Table
                
     class HashTable {
       constructor(size) {
         this.data = new Array(size);
       }
       //Lo siguiente es un Hash Function que fue creado arbitrariamente,existen muchos Hash Functions en GitHub
       hashMethod(key) {
        let hash = 0;
        for (let i = 0; i < key.length; i++) {
          hash = (hash + key.charCodeAt(i) * i) % this.data.length;
        }
        return hash;
       }
       //El metodo set nos inserta un elemento con su key y value(puede haber colisiones)
       set(key, value) {
        const address = this.hashMethod(key);
        if (!this.data[address]) {
          this.data[address] = [];
        }
        this.data[address].push([key, value]);
        return this.data;
       }
       //El metodo get nos devuelve el valor que le corresponde al key, en caso no exista el key enviado nos devolverá undefined
       get(key) {
        const address = this.hashMethod(key);
        const currentBucket = this.data[address];
        if (currentBucket) {
          for (let i = 0; i < currentBucket.length; i++) {
            if (currentBucket[i][0] === key) {
              return currentBucket[i][1];
            }
          }
        }
        return undefined;
       }
     }     
     //Instanciamos el HashTable con 50 espacios libres
     const myHashTable = new HashTable(50);  
    
                
              

    Linked Lists

    Los Linked Lists almacenan data en forma secuencial, pero en lugar de mantener índices, mantienen pointers(punteros) a otros elementos. El primer nodo es llamado head y el último nodo tail.

    Existen dos tipos de linked list:
    1. Singly Linked List:
      Cada nodo tiene solo un puntero hacia el siguiente nodo (next).
    2. Doubly Linked List:
      En este caso además del next, cada nodo tiene un puntero adicional hacia el nodo previo(prev).

    Imagen de Singly Linked List
    Diagrama de un Singly Linked List
    Imagen de Doubly Linked List
    Diagrama de un Doubly Linked List
    Démosle un vistazo al código:
    Singly Linked List:
                
    //Creamos una clase Node(nodo) para no repetir código en los métodos
     class Node {
       constructor(value) {
         this.value = value;
         this.next = null;
       }
     }
     
     class MySinglyLinkedList {
       constructor(value) {
         this.head = {
           value: value,
           next: null
         }
         this.tail = this.head;
         this.length = 1;
       }
       //Nuestro método append agregará un elemento al final(tail) del Singly Linked List
       append(value) {
         const newNode = new Node(value);
         this.tail.next = newNode;
         this.tail = newNode;
         this.length++;
     
         return this;
       }
       //Nuestro método prepend agregará un elemento al inicio(head) del Singly Linked List
       prepend(value) {
         const newNode = new Node(value);
         newNode.next = this.head;
         this.head = newNode;
     
         this.length++;
       }
       //Nuestro método insert insertará un elemento con índice index en el Singly Linked List
       insert(index, value) {
         if(index >= this.length) {
           console.log("No hay suficientes elementos, será enviado al final");
           return this.append(value);
         }
     
         const newNode = new Node(value); 
         const firstPointer = this.getTheIndex(index - 1);
         //Se crea una const holdingPointer que servirá para no perder el puntero next del firstPointer.
         const holdingPointer = firstPointer.next;
         firstPointer.next = newNode;
         newNode.next = holdingPointer;
     
         this.length++;
     
         return this;
       }
     
       getTheIndex(index) {
         let currentNode = this.head;
     
         for(let counter = 0; counter < this.length; counter++) {
           if(counter !== index) {
             currentNode = currentNode.next;
           } else{
             return currentNode;
           }
         }
       }
     }
     //Instanciando MySinglyLinkedList:
     let myLinkedList = new MySinglyLinkedList(1;              
                
              
    Doubly Linked List:
                
    //Como podremos ver el código para el doubly linked list solo se diferencia por ser unas cuantas líneas más larga:
    
    class Node {
      constructor(value) {
        this.value = value;
        this.next = null;
        this.prev = null;
      }
    }
     
    class MyDoublyLinkedList {
      constructor(value) {
        this.head = {
          value: value,
          next: null,
          // Doubly: se añade la siguiente linea
          prev: null,
        };
        this.tail = this.head;
     
        this.length = 1;
      }
      append(value) {
        const newNode = new Node(value);
        newNode.prev = this.tail;
        this.tail.next = newNode;
        this.tail = newNode;
        this.length++;
     
        return this;
      }
      prepend(value) {
        const newNode = new Node(value);
        // Doubly: se añade la siguiente linea
        this.head.prev = newNode;
        
        newNode.next = this.head;
        this.head = newNode;
     
        this.length++;
      }
      insert(index, value) {
        if(index >= this.length) {
          console.log("No hay suficientes elementos, será enviado al final");
          return this.append(value);
        }
     
        const newNode = new Node(value);
        let firstPointer = this.getTheIndex(index - 1);
        let secondPointer = this.getTheIndex(index);
        let holdingPointer = firstPointer.next;
        firstPointer.next = newNode;
        newNode.next = holdingPointer;
        // Doubly: se añaden las siguientes 3 líneas
        holdingPointer.prev = newNode;
        firstPointer = newNode.prev;
        holdingPointer = secondPointer;
     
        this.length++;
     
        return this;
     
      }
     
      getTheIndex(index) {
        
        let currentNode = this.head;
     
        for(let counter = 0; counter < this.length; counter++) {
          if(counter !== index) {
            currentNode = currentNode.next;
          } else{
            return currentNode;
          }
        }
      }
      
    }
    //Instanciando MyDoublyLinkedList:
    let myDoublyLinkedList = new MyDoublyLinkedList(1);              
                
              

    Trees

    Una estructura de datos en forma de árbol o Tree simula una estructura de árbol jerárquica, con un nodo padre, un valor root e hijos(children). Cada nodo contiene un valor y una referencia a su(s) hijo(s).

    En este caso veremos un Binary Search Tree, un tipo de Tree usado para insertar y buscar valores en la estructura. Un Binary Searh Tree consta de un root donde a partir de este elemento se inicia la búsqueda, cada elemento solo puede tener dos descendientes(children), los children que son mayores que el elemento padre se colocan debajo del padre pero al lado derecho y los menores al lado izquierdo.

    Imagen de un Tree
    Diagrama de un Tree
    Imagen de un Binary Search Tree
    Diagrama de un Binary Search Tree
    Ahora sí, para este código, armaremos el binary search tree de arriba.
                          
      class Node {
        constructor(value) {
          this.left = null;
          this.right = null;
          this.value = value;
        }
      }
      
      class BinarySearchTree {
        constructor() {
          this.root = null;
        }
        //Con este método insert, podremos agregar nodos con sus valores y referencias según la condición si son menor o mayor al padre.
        insert(value) {
          const newNode = new Node(value);
          if(this.root === null) {
            this.root = newNode;
          } else {
            let currentNode = this.root;
            while(true) {
              if(value < currentNode.value) {
                if(!currentNode.left) {
                  currentNode.left = newNode;
                  return this;
                }
                currentNode = currentNode.left;
              } else {
                if(!currentNode.right) {
                  currentNode.right = newNode;
                  return this;
                }
                currentNode = currentNode.right;
              }
            } 
          }
        }
        //El método search nos permitirá devolver el nodo en el que se encuentra el valor(value) enviado como argumento. Recordar que el nodo es el conjunto de información como: value, left child, right child. En caso de recorrerse el binary search tree y no encontrar el valor buscado nos indicará que no existe el valor.
        search(value) {
          var msj = "no se encuentra el valor";
          if(this.root === null) {
            console.log(`The tree is empty!`);
          } else {
            let currentNode = this.root;
             while(true) {
              if(value === currentNode.value) {
                return currentNode;
              } else {
                if(value < currentNode.value) {
                  if(currentNode.left) {
                    currentNode = currentNode.left;
                  } else {
                    return msj;
                  }
                } else {
                  if(currentNode.right) {
                    currentNode = currentNode.right;
                  } else {
                    return msj;
                  }
                }
              }
              
             }
            } 
        }   
      }
      //Instanciamos BinarySearchTree
      const myBinarySearchTree = new BinarySearchTree();
                 
               

    Graphs

    Los grafos o Graphs consisten en una colección finita de vertices(nodos) unidos por bordes(edges), existen varios tipos de grafos, según diferentes criterios:

    Diagrama de un Graph
    Diagrama de un Graph
    Tipos de Graph: Ponderado y no ponderado
    Tipos de Graph: Ponderado y no ponderado
    Tipos de Graph: Cíclico y acíclico
    Tipos de Graph: Cíclico y acíclico
    Tipos de Graph: Dirigido y no dirigido
    Tipos de Graph: Dirigido y no dirigido
    Existen varias maneras de representar un grafo, pero 2 de las principales son:

    1. Lista de Adyacencia:
      Para cada vértice, se almacena una lista de vértices adyacentes.
    2. Matriz de Adyacencia:
      La información es almacenada en una matriz bidimensional, en la cual las filas representan los vértices y las columnas representan los vértices a donde apuntan, la información en los bordes y vértices deben almacenarse externamente.

    Para el código usaremos la representación con lista adjacente, del tipo no dirigido, y usaremos la forma y nodos del grafo de la imagen: Diagrama de un Graph.
                 
     class Graph {
       constructor() {
         this.nodes = 0;
         this.adjacentList = {};
       }
       //Se agregan nodos(vértices)
       addVertex(node) {
         this.adjacentList[node] = [];
         this.nodes++;
       }
       //Se agregan bordes(edges) pasando como argumentos a nodos(nodo1 y nodo2), el grafo del ejercicio es del tipo no dirigido, así que debe colocarse el método push en ambas direcciones.
       addEdge(node1, node2) {
         this.adjacentList[node1].push(node2);
         this.adjacentList[node2].push(node1);
       }
     }
     
     const myGraph = new Graph();
     
     //Creamos los vértices(nodos):
     myGraph.addVertex(1);
     myGraph.addVertex(3);
     myGraph.addVertex(4);
     myGraph.addVertex(5);
     myGraph.addVertex(6);
     myGraph.addVertex(8);
     
     //Creamos los Edges(bordes), ya que hay 7 bordes en el gráfico, deben haber 7 addEdge:
     myGraph.addEdge(8,4);
     myGraph.addEdge(4,5);
     myGraph.addEdge(4,1);
     myGraph.addEdge(1,6);
     myGraph.addEdge(3,6);
     myGraph.addEdge(1,3);
     myGraph.addEdge(5,3);   
                 
               
    Comparemos el output luego de ingresar el código:

    Grafo a construir
    Grafo a construir
    Output en Inspector de Elementos-Consola
    Output en el Inspector de Elementos-Consola
    Como vemos en el output, al llamar a myGraph en su adjacentList nos muestra los nodos y sus respectivos nodos unidos por bordes, por ejemplo el nodo 1 tiene como nodos adyacentes(unidos por un borde) a los nodos 4,6 y 3. Lo cual es rápidamente verificable viendo el diagrama del grafo

    Conclusiones

    Existen muchas más estructuras de datos las cuales responden de una mejor o no tan eficiente manera al uso que se les quiera dar, algunas resaltan por su eficiencia en tiempo de búsqueda, otras por la forma de insertar o remover elementos, aquí entra en juego la habilidad y experiencia del desarrollador/ingeniero para saber cuando y por qué usar una estructura de datos en particular.

    Las formas de crear las estructuras de datos de este artículo es una de tantas maneras de llegar al mismo resultado o similar, esta vez se usó clases, pero también se puede crear con funciones.

  6. Test Driven Development (TDD)

    Qué es TDD?

    Es un proceso en el cual se hacen los tests antes del codigo, se basa en el ciclo red, green, refactor, donde red(rojo) es hacer un test que falla(debe fallar), luego en la fase green(verde), creamos el codigo necesario para que el test pase, y en la ultima fase, refactor, el codigo puede ser mejorado/optimizado.

    Red, green, refactor cycle in TDD
    Ciclo TDD

    Beneficios

    Testing se convierte en parte del desarrollo

    Mayormente hacer tests se siente como una tarea pesada, por lo cual al aplicar TDD, los tests se convierten en parte del desarrollo.

    Escribiremos código más limpio

    Cuando aplicamos TDD, solemos escribir el mínimo código posible para hacer que el código pase.

    Se reducen los bugs

    Al usar TDD se escriben mas tests, lo cual tiende a reducir el numero de bugs.

    Reglas del juego

    Existe una versión corta con 2 reglas basadas en las propuestas por Uncle Bob:


    1. Escribe solo lo necesario para que un unit test falle.
    2. Escribe solo suficiente codigo para hacer que el test que falló, pase.

    Ejercicio: Aplicación en React para calcular el volumen de una esfera:

    Intro TDD - Calcular volumen Estrella de la muerte
    Fases del ciclo red, green, refactor del TDD
    Primer test que falla (red)
    Primer test que pasa (green)
    Segundo Test que falla (red)
    Segundo Test que pasa (green)
    Tercer Test que falla (red)
    Tercer Test que pasa (green)
    Imagen de la aplicacion en el navegador - Calculadora de volumen de esfera
    Consejos y retos finales sobre el app
    Fuente: gsvidal.web
  1. Anatomía de una etiqueta HTML

    Conocer la anatomía de las etiquetas HTML te dará muchas ventajas al ...

  2. Etiquetas

    Entre las etiquetas HTML más conocidas tenemos a:

  1. Anatomía de una Regla CSS

    Las Reglas, así como las etiquetas en HTML, son los ladrillos sobre los que se construyen las aplicaciones, paginas y sitios webs.

  2. Metodología BEM

    Existen varias metodologías o convenciones para nombrar a las classes en CSS. Entre las principales tenemos a BEM, OOCSS, SMACSS, SUITCSS, etc.

    Estas se utilizan para organizar nuestro código en CSS y ayuda a mantener CSSs extensos.

    ¿Qué es BEM?

    BEM es una convención o standard para nombrar clases en CSS.

    ¿Porqué utilizar BEM?

    Primero veamos por qué utilizarlo sobre las otras metodologías mencionadas líneas arriba, es menos confusa que otros métodos (SMACSS) pero aun nos brinda una buena arquitectura(OOCSS) y con una terminología que podemos reconocer.

    Podemos encontrar 3 beneficios al utilizar BEM:
    1. Comunica propósito o función.
    2. Comunica estructura de compentes.
    3. Proporciona una especificidad baja.

    BEM significa:

    Blocks

    1. Los bloques son contenedores o el contexto donde están situados los elementos.
    2. Suelen ser etiquetas semánticas. ejem: (main, section, header, footer, etc.)
    3. Son independientes de otros bloques/elementos.

    Código en CSS:
                
    .block {
    
    }
                
              
    Ejemplo:
                
    .header {
    
    }
                
              

    Elements

    1. Los elementos son parte de un bloque y no tienen significado semántico fuera de ese bloque.
    2. Los elementos son escritos usando el nombre del bloque conectado por dos guiones abajo (underscore) a ellos.
    Código en CSS:
                
    .block__element {
    
    }
                
              
    Ejemplo:
                
    .header__navbar {
    
    }
                
              

    Modifiers

    1. Los modificadores son un flag en un bloque o un elemento, se usa para cambiar apariencia, comportamiento o estado.
    2. Ofrecen una gran ventaja: Modularidad.
    3. Al usar modificadores se facilita la reutilización de código e incentiva a la programación por componentes.

    Codigo en CSS(Modificador de un bloque):
                    
        .block--modifier {
        
        }
                    
                  
    ó Modificador de un elemento:
                    
        .block__element--modifier {
        
        }
                    
                  
    Ejemplo práctico:
    CSS:
                  
      .hero__copy--languages {
      
      }
                  
                
    Codigo en HTML(Modificador de un bloque):
                      
    
                      
                    
    ó Modificador de un elemento:
                      
    
                      
                    
    Ejemplo práctico:
    HTML:
                    
    
                    
                  

    A continuación podemos ver un ejemplo visual de esta metodología:

    Ejemplo metodología BEM

    Puedes ver la aplicación de la metodología BEM en el proyecto de maquetación del sitio web de la plataforma CODACY, codeada por el grupo Bit-Hug. Puedes encontrar BEM en las siguientes secciones del repo/web:

    1. section-1 (hero)
    2. section-2
    3. section-7
    4. footer

  3. Orden de declaración

    Antes de pasar a conocer como se controla el orden al declarar en CSS, recordemos algo sobre la Herencia, la cual...
    Ahora sí, veamos las 3 formas de como controlar el orden al declarar en CSS:

    1. Importancia
      1. Primero se aplican los estilos del navegador
      2. Luego se aplican los estilos de nuestras declaraciones en nuestros archivos .css
      3. Finalmente se aplican las declaraciones con| !important


      Notas: Recordar que el uso de !important es considerado una mala práctica, ya que puede romper nuestros estilos o llegar al punto de usarlo cada vez que no sepamos como funciona CSS

    2. Especificidad
    3. Orden en las fuentes
    4. Aquí se aplica el algoritmo de Cascada que tiene CSS, las declaraciones y/o reglas CSS que estén por debajo sobreescribirán o anularán a las que estén por arriba, en caso de conflicto.

      Notas: También tener en cuenta que al llamar en el archivo .html varios archivos .css, los de abajo también pueden reescribir a los de arriba.

Mide cuanto realmente aprendiste:

Quiz Time!