En construcción
Inicio: 09/01/2021
Estreno-aprox: 15/02/2021
% Avance:
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.
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.
Cada navegador tiene su propio motor de javascript, entre los más conocidos tenemos los siguientes:
Revisaremos uno de los más utilizados, el V8 de Chrome:
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.
Mostremos en código el bytecode y el machine code:
Fuente: Franziska Hinkelmann
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.
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
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.
El Asincronismo es...
Los objetos son...
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:
Como vemos esa estructura está vacía, tiene un lenght de 0 y no contiene data.
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);
}
}
//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();
//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();
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);
//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);
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();
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: 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.
Mayormente hacer tests se siente como una tarea pesada, por lo cual al aplicar TDD, los tests se convierten en parte del desarrollo.
Cuando aplicamos TDD, solemos escribir el mínimo código posible para hacer que el código pase.
Al usar TDD se escriben mas tests, lo cual tiende a reducir el numero de bugs.
Existe una versión corta con 2 reglas basadas en las propuestas por Uncle Bob:
Conocer la anatomía de las etiquetas HTML te dará muchas ventajas al ...
El uso del HTML Semántico se remonta a...
El parseo del codigo...
Las Reglas, así como las etiquetas en HTML, son los ladrillos sobre los que se construyen las
aplicaciones, paginas y sitios webs.
.block {
}
Ejemplo:
.header {
}
.block__element {
}
Ejemplo:
.header__navbar {
}
.block--modifier {
}
ó Modificador de un elemento:
.block__element--modifier {
}
Ejemplo práctico:
.hero__copy--languages {
}
ó Modificador de un elemento:
Ejemplo práctico:
A continuación podemos ver un ejemplo visual de esta metodología:
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:
A continuación, las 3 formas de como controlar el orden al declarar en CSS son (prioridad va de mayor 1. a menor 3.):
Recordar que mientras más alta sea alguna de estas 3 formas, más alta la relevancia o prioridad será la declaración CSS
!important
Una regla práctica para recordar la importancia de las declaraciones CSS, es la siguiente:
Donde la importancia es mayor en 1. y menor en 8.
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
Donde la especificidad es mayor en 1. y menor en 4.
Donde la posicion es mayor en 1. y menor en 3.
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!