Apuntes Javascript


Eloquent Javascript (3rd edition, 2019), Marijin Haverbeke


Convenciones Javascript

Indentación

Indentación:    2 espacios.
Indentación:    4 espacios.

Comentarios

// Comentario de una línea.
/* Comentario de varias líneas. */

Nombres

Variable:       carSpeed
Constante:      CAR_SPEED
Función:        carSpeed()
Constructor:    Number()


Paso por valor y referencia

Variables:      valor.
Objetos:        referencia.


Variables

Las variables se puede declarar tanto con let como con var:

if (...) {
    let letNumber = 10;
    var varNumber = 11;
}

console.log(letNumber);     // Undefined.
console.log(varNumber);     // 11;


El entorno

Cuando un programa inicia, por ejemplo el browser, se carga consigo una serie de variables y funciones parte del lenguaje, que permiten interactuar con el sistema subyacente y dispositivos como el mouse o el teclado.



Part I: language

1. Values, Types, and Operators

1.1 Números

var number = 10;    // Antes del 2015.
let number = 10;    // A partir del 2015.

Tipos especiales de números: -Infinity, Infinity y NaN (not a number).

1.2 Strings

let text = "Lie on the ocean"
let text = 'Float on the ocean'
let text = `Down on the sea`

Este último, también llamado template literals, no necesita declarar los saltos de línea \n para crearlos y permite anidar variables a diferencia de los otros dos: Hello, ${name} (string entre backticks).

Obteniendo substrings de una cadena:

let name = "John Doe";

console.log(name.slice(1,3));           // oh
console.log(name.substr(2,3));          // 'ohn '
console.log(name.substring(2,3));       // h


Retornar el valor de un índice dado:

console.log(name.charAt(0));            // valor en el índice 0.

En lugar de charAt también se puede utilizar name[0] que es más moderno.

Buscar coincidencias con startsWith y endsWith:

console.log(name.startsWith("Jo"));     // True
console.log(name.endsWith("xyz"));      // False

Buscar coincidencias con indexOf o lastIndexOf:

console.log(name.indexOf('h'));         // Índice del substring 'h'.
console.log(name.lastIndexOf('h', 4));  // Búsqueda de 'h' desde el índice 4.

El método indexOf o lastIndexOf de una cadena, puede buscar un substring que contenga más de un caracter, a diferencia de un arreglo, donde solo se puede buscar un elemento.

Buscar coincidencias con indexOf o lastIndexOf (operador NOT a nivel de bits):

console.log(~2)   \\ -(2+1)             // -3
console.log(~1)   \\ -(1+1)             // -2
console.log(~0)   \\ -(0+1)             // -1
console.log(~-1)  \\ -(-1+1)            // -0

if(~name.indexOf("Jo"))                 // Posición de la subcadena.
    console.log("Found it!");

Truco a nivel de bits que se usa para resolver el inconveniente con indexOf que devuelve 0 (false) si el substring se ubica al principio.

Buscar coincidencias con includes:

console.log(name.includes("Jo", 2)      // Busca a partir del índice 2.

if(name.includes("Jo")                  // True/False
    console.log(Found it!");

Devuelve true/false dependiendo de si existe el substring o no.

Separar/Unir cadenas:

name = "John Doe Doe";

name.split(' ');                        // ["John", "Doe", "Doe"]
name.join('.');                         // "John.Doe.Doe"

Otros métodos útiles:

name.trim()                             // Elimina espaciones en blanco.
name.repeat(3);                         // Repite n veces el string.
console.log(String(7).padStart(3, '0')  // Imprime 7 con 3 ceros a la izq.

Retornar el código de un caracter en una posición dada:

console.log("z".codePointAt(0));        // 122
console.log("Z".codePointAt(0));        // 90

Obtener el valor de un caracter dado:

```js
console.log(String.fromCodePoint(90));  // Z


1.3 Booleanos

Signos de comparación: <, >, <=, >=, != e ==.

console.log("Aardvark" <= "Zoroaster")
console.log(NaN == NaN)

NaN es el resultado de una computación absurda, por lo que su comparación siempre es false.

Operadores lógicos disponibles: and, or y not como &&, || y !.

console.log(false or true)
console.log(1 + 1 == 2 && 10 * 10 > 50)

En el último caso, la precendencia es el siguiente (menor a mayor): ||, &&, operadores de comparación y el resto.

1.4 Operador ternario

console.log(true ? 1 : 2)

1.5 Valores vacíos

Valores pero que no contienen información: null y undefined.

La diferencia de significado entre ambos valores es un error de diseño de Javascript; tratarlos, en la mayoría de los casos, como intercambiables.

1.6 Conversión automática de tipos

Conversiones automáticas que permiten operaciones raras:

console.log(8 * null)       // 0
console.log("5" - 1)        // 4
console.log("5" + 1)        // 51
console.log("five" * 2)     // NaN
console.log(false == 0)     // true


Para valores de diferentes tipos, los operadores OR y AND convierten el valor izquierdo en booleano para decidir que hacer posteriormente.

console.log(null || "user")         // user
console.log("Agnes" || "user")      // Agnes
console.log(0 || 3)                 // 3


console.log(null || "user")         // null
console.log("Agnes" || "user")      // user
console.log(0 || 3)                 // 0

El operador AND intenta devolver el valor false, basandose únicamente en la comprobación del valor izquierdo (contrario al OR).

2. Program structure

2.1 Variables y constantes

Crear variables o enlaces con let:

let one = 1, two = 2;
console.log(one + two);             // 3

Es preferible imaginar los enlaces o variables como tentáculos que sujetan valores, en lugar de cajas que los contienen.

Crear variables o enlaces con var:

var $name = "Ayda";
var greeting = "Hello ";

console.log(greeting + $name);

Crear constantes:

const PI = 3.14;

Desempacar valores de arreglos en variables (destructuring):

let [a, b, ...numbers] = [10, 20, 30, 40, 50];

console.log(a);                             // 10
console.log(b);                             // 20
console.log(numbers);                       // [30, 40, 50]


Desempacar valores de objetos en variables (destructuring):

let a, b, numbers;

({data, a, d} = {a: 10, b: 20, data: 30, d: 40});
console.log(a);                                     // 10
console.log(d);                                     // 40
console.log(data);                                  // 30

Asignar nombres a nuevas variables: const {a: aa, b: bb} = {a: 10, b: 20}, ahora quienes tienen los valores 10 y 20 son las variables aa y bb.

Desempacar valores de retorno de funciones (destructuring):

function f() {
    return [10, 20];
}

let [a, b] = f();

Para ignorar algunos o todos los valores de retorno: [a,,b] = f() en caso f() retornase más de 2 valores.


Nota: Este apartado será ampliado posteriormente dependiendo de la necesidad y frecuencia de uso de las “posibilidades”.


2.2 Condicionales

Condicional if-else:

if (num < 10) {
    console.log("Small");
} else if (num < 100) {
    console.log("Medium");
} else {
    console.log("Large");
}

Condicional switch:

switch (prompt("What is the weather like?")) {
    case "rainy":
        console.log("Remember to bring an umbrella");
        break;
    case "sunny":
        console.log("Dress lighty");
        break;
    case "cloudy":
        console.log("Go outside");
        break;
    default:
        console.log("Unknown weather type!");
        break;
}

2.3 Bucles

Bucle while:

while (i <= 10) {
    console.log(i);
    i++;
}

Bucle do...while:

do {
    console.log(i);
    i++;
} while (i <= 10)

Bucle for:

for (let i=0; i < 10; i++) {
    if(x == 5)
        break;          // Sale del bucle.
    if(x == 6)
        continue;       // Continua con la siguiente iteración.

    console.log(x);
}


Bucle for y arreglos u objetos:

for (let i=0; i < array.length; i++) {
    console.log(i);
}

Bucle for/of:

for (let value of array) {
    console.log(value);
}


Bucle for/in:

for (let value in array) {
    console.log(array[value]);
}


2. Funciones

Declaración de función:

console.log(square(3));

function square (x) {
    return x * x;
}

La declaración de función puede no formar parte del flujo de control regular.

Arrow functions (funciones de flecha):

const power = (x) => {return x * x; };  // Forma regular.
const power = x => x * x;               // Forma corta.
const print = () => {...};              // Sin parámetros.

La palabra function es reemplazado por la flecha =>.

Función como constante:

const square = function(x) {
    return x * x;
};

Función como valor:

let launchMissiles = function() {
    missileSystem.launch("now");
};

if(safeMode) {
    launchMissiles = function () {...};
}

Anidamiento de funciones:

const hummus = function(factor) {
    const ingredient = function(amount, unit, name) {
        ...
    };
};

Llamando a una función con argumentos extra:

function square(x) { return x * x; }
console.log(square(8, true, "hello"));      // 64

Los argumentos restantes son ignorados y si hay parámetros faltantes, a este se le asigna el valor undefined.

Función con argumentos por defecto:

function power(base, exponent = 2) {
    ...
}

Verificar parámetro omitido, nulo, undefined o vacío de una función:

function showMessage(text) {
    text = text || "empty";
}

Verificar parámetro undefined o null mediante el operador de fusión null (nullish coalescing operator):

let user;
console.log(user ?? "empty");                   // empty 


Función con un indeterminado número de argumentos (rest arguments):

function max(...numbers) {
    let result = -Infinity;
    for (let number of numbers) {
        if (number > result) result = number;
    }
    return result;
}

console.log(max(4, 1, 9, -2, 11, 3));   # 11

Las funciones de este tipo, vinculan los argumentos en un arreglo, por lo que es posible crear un arreglo de números y utilizarlos de la siguiente manera: max(4, ...numbers, 8).

Función con un destructing como parámetro:

// Normal
function phi(table) {
    return (table[3] * table[0] - table[2] * table[1]) /
        Math.sqrt((table[2] + table[3]) *
                  (table[0] + table[1]) *
                  (table[1] + table[3]) *
                  (table[0] + table[2]));
}

// Destructuring
function phi([n00, n01, n10, n11]) {
    return (n11 * n00 - n10 * n01) /
        Math.sqrt((n10 + n11) * (n00 + n01) *
                  (n01 + n11) * (n00 + n10));
}

2.1 Closure

Oh, malditos closures!

function multiplier(factor) {
    return number => number * factor;
}

let twice = multiplier(2);
console.log(twice(5));                  // 10


2.2 Recursividad

// simple example:
function power(base, exponent) {
    if (exponent == 0)
        return 1;
    else
        return base * power(base, exponent - 1);
}

/* Given a number, tries to find a sequence of additions and multiplications
 * (5,3) 
*/
function findSolution(target) {
    function find( current, history) {
        if (current == target) {
            return history;
        } else if (current > target) {
            return null;
        } else {
            return find(current + 5, `(${history} + 5`)
            || find(current * 3, `${history} * 3)`);
        }
    }
    return find(1, "1");
}

console.log(findSolution(28));
(((1 + 5 * 3) + 5 + 5))

Es necesario distinguir la importancia de la elegancia vs la velocidad.

4. Data Structures: Objects and Arrays

4.1 Arreglos

let array1   = new Array("one", 2, "three", 4, "five");
let array1   = ["one", 2, "three", 4, "five"];

console.log(array1[2]);                                  // 'three'

Si new Array() recibe un solo argumento, este es la longitud del arreglo.

array1.push("six")       // Agregar nuevo elemento al final.
array1.pop()             // Quita y retorna el último elemento.
array1.unshift("six")    // Agrega nuevo elemento al principio.
array1.shift()           // Quita y retorna el primer elemento.


let names = ["John", "Edward", "Peter", "Sue"];

console.log(names.indexOf("Peter"));            // 2
console.log(names.lastIndexOf("Peter"));        // 2


array.slice(1, 4);

En notación de intervalos sería: [) (cerrado, abierto).

let array = [1, 2, 3, 4, 5, 7];
array = array.reverse();
array2.concat(array.slice(2, 3));

Si no se le pasa a concat un argumento de tipo arreglo, este agrega el elemento al nuevo arreglo.

let numbers = [1, 2, 3];
let values = ['a', ...numbers, 'b'];    // ['a', 1, 2, 3, 'b']

A esto también se le conoce como notación de triple punto y su uso está relacionado con las funciones que reciben argumentos indeterminados.

// Matriz:
let matrix  = [
    [1, 2, 3],
    [4, 5, 6]
];

console.log(matrix[0][1]);      // 2
array.name = "Esther";      // Crea una propiedad.
array[99] = "value";        // Indice mayor que la longitud.
array.length = 7;           // length es editable.


Objetos

Crear objetos:

let obj = new Object();                                 // Empty obj (cnstrctr)
let obj = {};                                           // Empty obj (obj. lit)

let obj   = {name: "John", lastName: "Doe", age: 38,};
const obj = {name: "John", lastName: "Doe", age: 38,};


Crear objetos con propiedades calculadas (computed properties):

let fruit = prompt("bla bla bla");

// Simple:
let obj = {
    [fruit]: 10
}
// Complejo:
let obj = {
    [fruit + "computers"]: 10
}

El valor de fruit será el que el usuario ingrese o el valor que se haya definido.

Objeto con nombre de propiedad de varias palabras:

let obj = {
        name: "John",
        age:  32,
        "card number": "111-222-3333"
    }

Para acceder a este tipo de propiedad se usa la notación de corchetes: obj["card number"].

Acceder a las propiedades de un objeto:

...

console.log(obj.name)                               // John
console.log(obj["name"])                            // John

La notación de corchete puede ser usado para permitir al usuario ingresar el nombre de la clave de un objeto.

function makeUser(name, age) {
    return {
        name: name,
        age: age
    };
}

Es posible omitir name: name y en su lugar usar simplemente name o usar ambas formas.

delete obj.age
console.log(obj.age)                                // Undefined

Si la propiedad no existe, Javascript no muestra ningun error, en su lugar retorna undefined.

console.log("property" in obj);                     // True/False

También se podría utilizar comparaciones con undefined para saber si una propiedad existe; pero a diferencia de in, este no funciona cuando el valor almacena undefined.

console.log(Object.keys(obj));                      // Devuelve un arreglo.
Object.assign(obj1, obj2);                          // obj1 ← obj2


JSON

Convertir un arreglo/objeto a JSON:

let object = {
    name: "John",
    age: 30,
    isAdmin: false,
    courses: ["html", "css", "js"],
    wife: null
}

let json = JSON.stringify(object);  // Devuelve un string codificado en JSON.

Es posible serializar cualquier casi cualquier tipo de dato: objects, arrays, strings, numbers, boolean e incluso null.

Convertir de JSON a arreglo/objeto:

...

let object = JSON.parse(json);


5. Higher-Order Functions

Una función que toma otra función como argumento o la retorna como resultado, se denomina higher-order function o función de alto nivel. Estas permiten abstraer tanto sobre valores como acciones y son ampliamente utilizados en el procesamiento de datos (map, filter y reduce).

Abstracting Repetition

Función que repite n veces una función pasada como argumento:

function repeat(n, action) {
    for (let i=0; i < n; i++)
        action(i);
}

repeat(3, console.log);

// → 0, 1, 2

Función que repite n veces una función creada en el espacio del argumento:

.
.
.

let labels = [];

repeat(3, i => {labels.push(`Unit ${i}`)});
console.log(labels);                    

// → 'Unit 0', 'Unit 1', 'Unit 2'


Filtering Arrays

Definiendo una función filter:

function filter(array, test) {
    let passed = [];
    for(let element of array) {
        if(test(element))
            passed.push(element);
    }
    return passed;
}

console.log(filter(SCRIPTS, e => e.living));

// → [{name: "Adlam", ...}, ...]

La función filter de JavaScript, recibe 4 argumentos: valor actual, índice, array y valor a utilizar com this. El dataset que se está utilizando es SCRIPTS y está disponible en eloquentjavascript.

Transforming with map

function map(array, transform) {
    let mapped = [];

    for(let element of array)
        mapped.push(transform(element));

    return mapped;
}

let rltScripts = SCRIPTS.filter(s => s.direction == "rtl");
console.log(map(rtlScripts, s => s.name));                  

// → ["Adlam", ...]

Parámetros de la función map de JavaScript: valor actual, índice, array y valor a usar como this. La función filter usada con SCRIPTS es la función implementada por JavaScript, más no la que ha sido creada anteriormente.

Summarizing with reduce

function reduce(array, combine, start) {
    let current = start;
    
    for(let element of array)
        current = combine(current, element);
    
    return current;
}

let array = [1, 2, 3, 4];
console.log(reduce(array, (a, b) => a + b, 0));

// → 10

La función reduce propia de JavaScript, recibe 4 argumentos: acumulador, valor actual, índice actual y array.

Composability

Composición de funciones:

// Dado un código de caracter, devuelve el script al que pertence:
function characterScript(code) {
    for(let script of SCRIPTS) {
        if(script.ranges.some(([from, to]) => {
            return code >= from && code < to;
        })) {
            return script;
        }
    }
    return null;
}

console.log(characterScript(121));
// → {name: "Latin", ...}

// Cuenta la cantidad de elementos encontrados en un grupo:
function countBy(items, groupName) {
    let counts = [];

    for(let item of items) {
        let name = groupName(item);
        let known = counts.findIndex(c => c.name == name);
        if(known == -1) {
            counts.push({name, count: 1});
        } else {
            counts[known].count++;
        }
    }
    return counts;
}

console.log(countBy([1, 2, 3, 4, 5], n => n > 2));
// → [{name: false, count: 2}, {name: true, count: 3}]

// Indica el script que se utiliza en un fragmento de texto:
function textScripts(text) {
    let scripts = countBy(text, char => {
        let script = characterScript(char.codePointAt(0));
        return script ? script.name : "none";
    }).filter(({name}) => name != "none");

    let total = scripts.reduce((n, {count}) => n + count, 0);
    if(total == 0) return "No scripts found";

    return scripts.map(({name, count}) => {
        return `${Math.round(count * 100 / total)} % ${name}`;
    }).join(", ");
}

console.log(textScripts(英国的狗说"woof", 俄罗斯的狗说"тяв"'));
61% Han, 22% Latin, 17% Cyrillic

6. The Secret Life of Objects

Encapsulation

Methods

Los métodos son propiedades que almacenan funciones:

let rabbit = {};
rabbit.speak = function(line) {
    console.log(`The rabbit says "${line}"`);
};

rabbit.speak("I'm alive.");

// → The rabbit says "I'm alive."

Accediendo a la propiedad del objeto desde el método con this:

function speak(line) {
    console.log(`The ${this.type} rabbit says "${line}"`);
}

let whiteRabbit = {type: "white", speak};
let hungryRabbit = {type: "hungry", speak};

whiteRabbit.speak("Oh my ears and whiskers!");
hungryRabbit.speak("I could use a carrot right now.");

// → The white rabbit says "Oh my ears and whiskers!"
// → The hungry rabbit says "I could use a carrot right now."


Accediendo a la propiedad del objeto de manera explícita:

...

speak.call(hungryRabbit, "Burp!");

// → The hungry rabbit says "Burp!"


Accediendo al this de una función desde un arrow function:

function normalize() {
    console.log(this.coords.map(n => n / this.length));
}

let obj = {coords: [0, 2, 3], length: 5};

normalize.call(obj);

// → [0, 0.4, 0.6]

Prototypes

La mayoría de los objetos cuentan con otra propiedad/objeto oculto llamado prototype que es usado como una fuente alternativa de propiedades. Cuando una propiedad es solicitada, este se busca en el objeto, luego en el prototipo, en el prototipo del prototipo, y así sucesivamente hasta llegar al prototipo ancestral Object.prototype; en programación a esto se le conoce como herencia prototípica.

Algunos objetos no tienen directamente el prototipo Object.prototype, en su lugar derivan de otros como Function.prototype para las funciones, Number.prototype para los números, Array.prototype para los arreglos, etc:

Object.prototype == Object.getPrototypeOf({});
Array.prototype == Object.getPrototypeOf([]);
Function.prototype == Object.getPrototypeOf(Math.max);

Object.prototype == Object.getPrototypeOf([]);

// → true
// → true
// → true
// → false

El método Object.getPrototypeOf retorna el prototipo de un objeto.

Crear un objeto con un prototipo específico:

let protoRabbit = {
    speak(line) {
        console.log(`The ${this.type} rabbit says "${line}"`);
    }
};

// Forma 1:
let killerRabbit = Object.create(protoRabbit);

// Forma 2:
let killerRabbit = {};
killerRabbit.__proto__ = protoRabbit;

// Forma 3:
let killerRabbit = {};
Object.setPrototypeOf(killerRabbit, protoRabbit);
killerRabbit.type = "killer";
killerRabbit.speak("I'm gonna kill you!");

// → The killer rabbit says "I'm gonna kill you!"


Crear un objeto puro sin prototipos:

let obj = Object.create(null);

Se crea un objeto “limpio”, sin los métodos heredados como toString o __proto__ que incluso podria tener proto como clave sin ocasionar problemas inesperados.

Agregar una función a un prototipo nativo:

String.prototype.myFunction = function() {
    console.log(this.toString + " says Hello!");
};

let myText = "My text";

myText.myFunction();

// → My text says Hello! 


Préstamo de prototipos:

let obj = {
    0: "hello",
    1: "world!",
    length: 2
}

obj.join = Array.prototype.join;

console.log(obj.join('-');

// → hello, world!


Classes

Para hacer uso de la programación orientada a objetos, en JavaScript se puede hacer uso de los prototipos, que vienen a ser como una manera informal de la OOP.

Crear objetos mediante una función constructora de objetos:

// Constructor function:
function Rabbit (type) {
    this.type = type;
}

Rabbit.prototype.speak = function(line) {
    console.log(`The ${this.type} rabbit says "${line}"`);
};

// Creating object:
let weirdRabbit = new Rabbit("weird");
weirdRabbit.speak("hello, I'm a weird rabbit");

// → the weird rabbit says "hello, I'm a weird rabbit"


Crear objetos mediante la notación de clase:

class Rabbit {

    constructor(type) {
        this.type = type;
    }

    speak(line) {
        console.log(`The ${this.type} rabbit says "${line}"`);
    }
}

let killerRabbit = new Rabbit("killer");
let blackRabbit = new Rabbit("black");

killerRabbit.speak("I'm gonna kill you!");

// → The killer rabbit says "I'm gonna kill you!"


Overriding Derived Properties

Maps

Un objeto Map permite almacenar cualquier tipo de valor como clave, y a diferencia de un Object, este no cuenta con las propiedades heredadas del prototipo.

let values = new Map();

values.set(functionName, "I'm a function");
values.set(1, 11);
values.get(1);
values.get(functionName);
values.has(1);

// → true
// → 11
// → I'm a function


Polimorphism

El código polimórfico es aquel que puede funcionar con valores de diferentes formas, siempre y cuando estos tengan la interfaz esperada por el código polimórfico:

Rabbit.prototype.toString = function() {
    return `a ${this.type} rabbit`;
}

blackRabbit.toString();

// → a black rabbit

Un ejemplo claro de polimorfismo, es el bucle for/of que puede iterar sobre diferentes tipos de estructuras.

Symbols

Como en muchos lenguajes, en Javascript, no es posible crear propiedades de objeto con el mismo nombre sin que estos se sobreescriban, a menos a que estos se creen mediante symbols o símbolos.

Los símbolos, al igual que los strings, son valores primitivos de JavaScript, con la particularidad de que estos (los símbolos) pueden coexistir con otras propiedades del mismo nombre y no ser listados por iteradores como for/in pero si incluidos al clonar el objeto.

Crear symbol:

// (1)
let name = Symbol();
let name2 = Symbol();

// (2)
let sym1 = Symbol("symbol1")
let sym2 = Symbol("symbol1")

console.log(name === name2);
console.log(sym1 === sym2);

// → false
// → false


Agregar propiedades símbolo en un objeto:

let user = new Object();        // user object
let name = Symbol();            // name symbol

// (1)
user = {
    name: "John String",        // string key
    [name]: "John Symbol"       // symbol key
}

// (2)
user["name"] = "John String";
user[name] = "John Symbol";


Agregar símbolo como propiedad función de un objeto:

const speak = Symbol();

let obj = {
    speak() {
        return `Hello String!`;
    },
    [speak]() {
        return `Hello Symbol!`;
    }
};

obj.speak();
obj[speak]();

// → Hello String!
// → Hello Symbol!

Agregrar una propiedad símbolo a prototipos:

const symbolLength = Symbol();

Array.prototype[symbolLength] = function() {
    return `Array length is ${this.length}`;
};

[1, 4, 3, 1, 5][symbolLength]();

// → Array length is 5

Crear símbolo en el global symbol registry o runtime global del registro de símbolos:

let id = Symbol.for("id");
let id2 = Symbol.for("id");

id === id2;

// → true


The Iterator Interface

Los objetos iterable son todos aquellos que pueden ser recorridos por el bucle for/of e implementan el método Symbol.iterator. Al ser llamado Symbol.iterator, retorna el objeto iterator, que, mediante su método next(), permite saber si el objeto iterable aún cuenta con valores por iterar.

Funcionamiento directo de iterator:

let array = ['h', 'e', 'l', 'l', 'o'];

let iterator = array[Symbol.iterator]();
iterator.next();
iterator.next();
iterator.next();
iterator.next();
iterator.next();

// → {value: 'h', done: false}
// → {value: 'e', done: false}
// → {value: 'l', done: false}
// → {value: 'l', done: false}
// → {value: 'o', done: true}

El método next() opera sobre un objeto iterable u objeto generador que puede, también, ser devuelto por las funciones generadoras (tema tratado más adelante).

Recorrido de un objeto iterable con el bucle while:

let array = ['h', 'o', 'l', 'a'];

let iterator = array[Symbol.iterator]();

while(true) {
    let result = iterator.next();
    if(result.done) break;
    console.log(result.value);
}

// → hola

Implementación de un objeto iterable (1):

let range = {
    from: 1,
    to: 5
};

range[Symbol.iterator] = function() {

    return {
        current: this.from,
        last: this.to,
        next() {
            if(this.current <= this.last)
                return {done: false, value: this.current++};
            else
                return {done: true};
        }
    };
};

for(let value of range)
    console.log(value);

// → 1, 2, 3, 4, 5


Implementación de un objeto iterable (2):

let range = {
    from: 1,
    to: 5,
    
    [Symbol.iterator]() {
        this.current = this.from;
        return this;
    },

    next() {
        if(this.current <= this.last)
            return {done: false, value: this.current++};
        else
            return {done: false};
    }
};


Getters, Setters, and Statics

Los getter y setter son descriptores de acceso que establecen la forma de obtener y configurar los valores de las propiedades, los otros tipos de descriptores son los de datos.

Configurando getter y setter en un objeto:

let user = {
    name: "John",
    surname: "Doe",
    get fullname() {
        return `${this.name} ${this.surname}`;
    },
    set fullname(value) {
        [this.name, this.surname] = value.split(" ");
    }
};

user.fullname;
user.fullname = "Valeria Smith";
user.name;
user.surname;
user.fullname;
user;

// → John Doe
// → valeria
// → Smith
// → Valeria Smith
// → {name: 'Valeria, surname: 'Smith', fullname: [Getter/Setter]}


Configurando getter y setter en un objeto mediante defineProperty:

let user = {
    name: "John",
    surname: "Doe"
};

Object.defineProperty(user, "fullname", {
    get() {
        return `${this.name} ${this.surname}`;
    },
    
    set(value) {
        [this.name, this.surname] = value.split(" ");
    }
});

La configuración de getters y setters mediante el método anterior, establece los accesors no visibles, similar al ejemplo posterior.

Configurando getter y setter en una clase:

class Temperature {
    constructor(celsius) {
        this.celsius = celsius;
    }



    get fahrenheit() {
        return this.celsius * 1.8 + 32;
    }
    
    set fahrenheit(value) {
        this.celsius = (value - 32) / 1.8;
    }
    
    static fromFahrenheit(value) {
        return new Temperature((value -32) / 1.8);
    }
}

let temperature = new Temperature(22);

temperature.fahrenheit;
temperature.fahrenheit = 86;
temperature.celsius;

// → 71.6
// → 30


Inheritance

Al igual como sucede con la herencia prototípica, la herencia mediante classes realiza los mismos mecanismos para heredar los métodos de la clase superior, realizar la búsqueda de los métodos o propiedades (inferior → superior) y hasta sobreescribir métodos. En resumen, las clases y la herencia son un sugar syntax de la herencia prototípica.

class Animal {
    constructor(name) {
        this.speed = 0;
        this.name = name;
    }

    run(speed) {
        this.speed = speed;
        return `${this.name} runs with speed ${this.speed} Km/h.`;
    }
    
    stop() {
        this.speed = 0;
        return `${this.name} stands still.`;
    }
}

// Child class:
class Rabbit extends Animal {
    hide() {
        return `${this.name} hides!`;
    }
}

let rabbit = new Rabbit("Black Rabbit");

rabbit.run(90);
rabbit.hide();

// → Black Rabbit runs with speed 90 Km/h.
// → Black Rabbit hides!

Heredando cualquier expresión:

function f(phrase) {
    return class {
        hello() { return `${phrase}`;
    }
}

class User extends f("hellowww!") {}

let user = new User();
user.hello();

// → helloww!

la expresión extends permite casi cualquier otra expresión posterior a su declaración, no necesariamente otra clase de la cual heredar.

Sobreescritura de métodos:

...

class Rabbit extends Animal {
    hide() {
        return `${this.name} hides!`;
    }

    stop() {
        super.stop();                   // Call parent stop
        this.hide();
    }
}

let rabbit = new Rabbit("White Rabbit");

rabbit.run(5);
rabbit.stop();

// → White Rabbit runs with speed 5 Km/h.
// → White Rabbit stands still. White Rabbit hides!

La palabra clave super permite invocar métodos o constructores de la clase superior o superclase. Así como las funciones arrow no tiene this, en este caso tampoco tiene super ni pueden ser llamados con new.

Sobreescritura de constructores:

class Animal {
    constructor(name) {
        this.speed = 0;
        this.name = name;
    }
    ...
}

class Rabbit extends Animal {
    constructor (name, earLength) {
        super(name);
        this.earLength = earLength;
    }
    ...
}


Peculiaridad de JavaScript en la sobreescritura de campos de clase:

class Animal {
    name = "animal";
    constructor() {
        console.log(this.name);
    }
}

class Rabbit extends Animal {
    name = "rabbit";
}

new Animal();
new Rabbit();

// → animal
// → animal


The instanceof Operator

El operador binario instanceof permite saber si un objeto es instancia de una clase:

[1,2,3] instanceof Function;
[1,2,3] instanceof Array;
[1,2,3] instanceof Object;

// → false
// → true
// → true

7. Project: A Robot

→ It will be filled after finished the notes.

8. Bugs and Errors

Stric Mode

JavaScript permite utilizar un modo más estricto que corrige errores que dificultan la optimización del navegador, elimina errores silenciosos y prohibe sintáxis que probablemente se utilicen en futuras versiones de ECMAScript. En algunos casos, puede el código en modo estricto ejecutarse más rápido que el no estricto o sloppy mode.

El modo estricto puede ser aplicado tanto a funciones como al script completo:

function strictMode() {
    "use strict";
    ...
}
"use strict";
...


Types

Debido a que JavaScript realiza una conversión automática de tipos o type coercion y es de tipado débil, puede ser útil comentar el tipo de los parámetros de una función:

//(VillageState, Array) → {direction: string, memory: Array}
function goalOrientedRobot(state, memory) {
...
}

Uno de los dialectos JavaScript que agrega tipos al lenguje es TypeScript, por lo que puede ser una buena alternativa.

Testing

Una manera de comprobar si el programa se está ejecutando como se espera, es crear una función que permita comprobar piezas de código.

Creando una función que permite comprobar si el método toUpperCase funciona correctamente:

function test(label, body) {
    if(!body())
        console.log(`Failed: ${label}`);
}

test("convert Latin text to uppercase", () => {
    return "hello".toUpperCase() == "HELLO";
});

// → 

A pesar de que está opción es más conveniente que verificar el código manualamente, es recomendable utilizar test-runners.

Debugging

Error Propagation

Cuando el programa escrito entra en contacto con el mundo externo, este podría recibir valores antes no previstos, por lo tanto fallar en su ejecución. Es importante anticiparse a estos problemas y hacer que el programa lidie con ellos y continue con su ejecución.

Exceptions

Las excepciones son un mecanismo que permiten lanzar una excepción (error) y luego manejarlos con la sentencia try-catch-finally.

try {
    // code
} catch (err) {
    // error handling
}


Cuando se produce un error, JavaScript genera un objeto con detalles del error:

try {
    ...
} catch (err) {
    err;
    err.name;
    err.message;
    err.stack;
}

Lanzar error personalizados:

let json = {"age": 30};

try {
    let user = JSON.parse(json);

    if(!user.name)
        throw Error("An error occurred booO!");
    
    console.log(user.name);
} catch (err) {
    console.log("JSON Error: " + err.message);
}


Relanzar errores:

let json = '{"age": 30}`;           // Incomplete data

try {
    let user = JSON.parse(json);

    if(!user.name)
        throw new SyntaxError("Incomplete data: no name");
    
    anotherError();                 // Another unexpected error
    console.log(user.name);

} catch (err) {
    if(err instanceOf SyntaxError)
        console.log("JSON Error: " + err.message);
    else
        throw err;                  // Rethrowing "unknown" error
}


Ejecutar a pesar del error:

try {
    ...
} catch(err) {
    ...
} finally {                         // Executes always
    ...
}


9. Regular Expressions

Creating a regular expression

Las expresiones regulares son un tipo de objeto que pueden ser declarados de dos maneras:

let re1 = new RegExp("abc");            // (1) Constructor RegExp
let re2 = /abc/;                        // (2) Valor literal


Testing for Matches

Comprobar una expresión regular:

let reg = /abc/;
reg.test("abxde");

// → false

Set of Characters

Forma corta:

/[0123456789]/.test("in 1990's");
/[0-9]/.test("in 1990's");
/\d/.test("in 1990's");

// → true
// → true
// → true

El orden de los caracteres 0 y 9 están determinandos por su número Unicode correspondiente (48 → 57).

Atajos:

\d          cualquier caracter de dígito.
\w          cualquier caracter alfanumérico.
\s          cualquier espacio en blanco.
\.          cualquier caracter menos salto de línea.

\D          ~dígito.
\W          ~alfanumérico.
\S          ~espacio en blanco.

Ejemplo de verificación básica de una fecha dada:

let dateTime = /\d\d-\d\d\-\d\d\d\d \d\d:\d\d/;         // Básico
dateTime.test("13-05-2021 01:15");

let dateTime = /\d{1,2}-\d{1,2}-\d{4} \d{1,2}:\d{2}/;   // Mejorado
dateTime.test("13-5-2021 1:15");

// → true
// → true


Negando ciertos valores:

let notBinary = /[^01]/;
notBinary.test("10010110110001");
notBinary.test("10010110110009");

// → false
// → true

El símbolo caret ^ también coincide con cualquer caracter al principio de una cadena, a menos que se utilice entre corchetes, donde su comportamiento es de negación.

Repeating Parts of a Pattern

Caracteres especiales:

/ .art/.test("I like cart");                // .    → cualquier caracter
/colou?r/.test("colors");                   // ?    → 0 o 1 vez caracter 'u'
/colou*r/.test("colors");                   // *    → 0 o más veces la 'u'
/colou+r/.test("colors");                   // +    → 1 o más veces la 'u'
/colo{2,}r/.test("coloooors");              // {2,} → 2 o más veces lo 'o'


Grouping Subexpressions

Los caracteres entre paréntesis son tratados como un solo caracter en lo que respecta al operador:

/boo+(hoo+)+/i.test("Boohoohooohoo");
/boo+(hoo+)+/i.test("Bohoohooohoo");

// → true
// → false


Interpretación de una expresión agrupada:

let quotedText = /'([^']*)'/;
quotedText = .exec("she said hello");

// → ["'hello'", 'hello']


Matches and Groups

Similar al método test está exec de execute, que devuelve un objeto con la información de la coincidencia (si es que existe):

let result = /boo+(hoo+)+/i.exec("Monster says: Boohoohooohoo");

result[0];
result[1];
result.index;

// → 'Boohoohooohoo'
// → 'hoo'
// → 14


The Date Class

JavaScript cuenta con una librería estándar Date para representar fechas:

new Date();
new Date(2021, 11, 31);
new Date(year, month, day, hour, minute, second, millisecond);

// → 2021-05-13T17:27:04.012Z
// → 2021-12-31T17:00:00.000Z


Creando un objeto Date a partir de una entrada string:

function getDate(string) {
    [_, month, day, year] = /(\d{1,2})-(\d{1,2})-(\d{4})/.exec(string);
    return new Date(year, month - 1, day);
}

getDate("5-13-2021");

// → Thu May 13 2021 00:00:00 GMT-0500

El binding _ es utilizado para almacenar la parte que no es interés para la función.

Word and String Boundaries

^           → Coincide con el inicio de los caracteres de entrada.
$           → Coincide con el final de los caracteres de entrada.
\b          → Coincide con los límites de una palabra.

/^\W+$/     → Cadena de solo símbolos.
/\bjava\b/  → Cadena 'java'.


Choice Patterns

Elección de patrones:

/\b\d+ (pig|cow|chicken)s?\b/.exec("15 cows");
/\b\d+ (pig|cow|chicken)s?\b/.exec("15 pigs");
/\b\d+ (pig|cow|chicken)s?\b/.exec("1 pig");

// → true
// → true
// → true

The Mechanics of Matching

El motor de expresión regular busca un patrón similar a un diagrama de flujo, empezando por el lado izquierdo hasta llegar al final de la cadena de caracteres:

regexp.svg

El motor de expresión regular va decidiendo la ruta la cual seguir a partir de los caracteres con los que se encuentre.

Backtracking

Representación gráfica de la expresión regular /\b([01]+b|[\da-f]+h|\d+)\b/:

backtracking.svg

Suponiendo que se tiene el string “103”, el matcher inicia su recorrido ingresando a la rama superior (binaria), donde al llegar al caracter 3, este tiene que volver al punto en el que se bifurcan las ramas para tomar otra ruta, dado que la rama era incorrecta. Del mismo modo, ingresa en la rama intermedia para nuevamente iniciar su evaluación; dado que es la rama incorrecta, nuevamente retornar a la última rama, que en este caso si coincide con el patrón.



Es posible escribir expresiones regulares que requieran demasiados backtrackings como con la expresión siguiente: /([01]+)+b/.

backtracking-loop.svg



The Replace Method

Reemplazando caracteres mediante expresiones regulares:

"Borobudur".replace(/[ou]/g, "a");

// → Barabadar

El flag indica que se reemplacen en todas las coincidencias.

Cambiando el orden de dos palabras mediante expresiones regulares:

"Liskov, Lisa\nMcCarthy, John\nWadler, Fray".replace(/(\w+), (\w+)/g, "$2 $1");

// → Lisa Liskov
// → John McCarthy
// → Fray Wadler


Modificando el string mediante replace y una función como argumento:

"the cia and fbi".replace(/\b(fbi|cia)\b/g, str => str.toUpperCase())

// → the CIA and FBI

Dinamically Creating RegExp Objects

Suponiendo que se quiere resaltar un patrón específico (no conocido) dentro de un texto, se tiene que utilizar un objeto regexp dinámico que permita almacenar tal patrón.

let name = "Harry";
let text = "is Harry a suspicious character?";
let regexp = new RegExp("\\b(" + name + ")\\b", "gi");

text.replace(regexp, "_$1_");

// → is _Harry_ a suspicious character?

Dado que se esta declarando la expresión regular dentro de un string, se tienen que escapar los backslashes.

Si el nombre de usuario conteniese caracteres parte de los operadores de la expresión regular, se tendría que escaparlos de manera global:

let name = "dea+hl[]rd";
let text = "This dea+hl[]rd guy is super annoying.";
let escaped = new RegExp(/[\\[.+*?(){|^$]/g, "\\$&");
let regexp = new RegExp("\\b" + escaped + "\\b", "gi");
text.replace(regexp, "_$&_");

l// → This _dea+hl[]rd_ guy is super annoying.

Recurso para visualizar expresiones regulares de manera gráfica: regexper

10. Modules

Los módulos son piezas de código que interactúan entre a sí mediante “conectores”, como si de piezas de lego se tratara. Es ideal que cada uno de ellos puedan ser aislados y analizados por separado, fuera de su contexto o incluso ser capaz de utilizarlo en otro programa. Las interfaces de los módulos es bastante similar al de los objetos.

Packages

Si bien un módulo puede ser reutlizado en otros programas, esto provocaría que se tenga copias del mismo módulo en diferentes programas y que actualizarlo sea complicado. Para lidiar con esto problema, viene bien utilizar paquetes.

Un paquete es un fragmento de código que al ser actualizado, los programas que lo utilizan también se actualicen. Puede contener uno o más módulos y documentar su funcionamiento como también sus dependencias para que cualquier persona pueda utilizarlo.

Muchos de los paquetes pueden ser encontrados en npmjs y descargarse con una herramienta con el mismo nombre (Node package manager). Esto evita la duplicidad y facilita una implementación de código que ha sido probada por muchos usuarios.

Antes del 2015, JavaScript no contaba con un sistema de módulos integrado, por lo que las personas empezaron a utilizar funciones para representar interfaces de módulos que no definían sus dependencias.

Ejemplo del estilo obsoleto para crear módulos:

const weekDay = function() {
    ...
}();

weekDay.returnedFunction(...);


Evaluating Data as Code

JavaScript provee diversas maneras recibir código JS en forma de string y ejecutarlo como parte del programa.

Ejecutar código mediante la función eval:

let x = 1;
let y = 5;

eval("x + y");

// → 6

El uso de eval es un tanto desaconsejado debido a los problemas que puede causar dentro del alcance donde se la invoque.

Ejecutar código mediante el constructor Function:

let plusOne = Function("n", "return n+1;");
plusOne(5);

// → 6

Una alternativa menos peligrosa a eval es el constructor Function que como primer argumento puede recibir una lista de argumentos separados por comas, y en el segundo, el cuerpo de la función.

EcmaScript Modules

El sistema de módulos ES modules es una alternativa de JavaScript implementada para reemplazar muchos de los otros sistemas que se fueron implementando sobre JavaScript.

Ejemplo de la notación import y export para el uso de módulos:

import ordinal from "ordinal";
import {days, months} from "date-names";

// Another file:

export let user = "John";
export function formatDate(...) ... }
export class Foo {...}


Exportando valores por defecto:

export default seasons = ["Winter", "Spring", "Summer", "Autunm"];
import seasons from './seasons.js';

Los archivos que se importan sin las llaves, importarán el binding declarado con la expresión default.

Renombrar bindings importados:

import {days as dayNames} from "date-names";

Building and Building

Si bien dividir el código en muchos scripts pequeños puede tener diferentes ventajas, en la web puede suponer en una tiempo de carga mucho más lento, por lo que se suele utilizar bundlers que agrupan los diferentes scripts o módulos, en un solo gran archivo que permite acelerar el tiempo de carga de la web.

Además de la cantidad de scripts, también el tamaño del archivo influye en la velocidad de transferencia del archivo en la red, por lo que se pueden utilizar minifiers que se encargan de reducir el tamaño del archivo, ya sea removiendo comentarios, reemplazando piezas de código, como renombrando bindings.

Muchos de los paquetes distribuidos con NPM o los que se ejecutan en la web, han pasado por una serie de transformaciones, incluido el sistema de módulos, que ya no son el mismo código que se escribieron.

11. Asynchronous Programming

Un modelo síncrono, es donde las acciones se realizan una después de otra (secuencial) y que es bloqueante para la siguente tarea en espera. A diferencia del modelo asíncrono, donde se crea una línea paralela en la cual se ejecuta la otra tarea requerida que al final notifica al programa principal cuando finaliza su tarea.

Programa que obtiene dos recursos de la red para luego combinarlos:

asynchronous.svg

La línea azul representa el tiempo en ejecución del programa, y la delgada, el tiempo de espera por el recurso de la red.

Callbacks

Un callback es una devolución de llamada o retrollamada, donde una función A recibe como argumento una función B que es ejecutada cuando se invoca a la función A.

Ejemplo básico de un callback:

setTimeout(function(){console.log("Tick");}, 1000);
setTimeout(() => console.log("Tick"), 1000);
setTimeout(tick(), 1000);

// → Tick


Promises

Las promesas son, como su nombre indica, una acción que se realizarán a futuro, o en casos de programación, una acción que posiblemente tome tiempo su ejecución y del que es “incierto” el resultado. Las promesas, al igual que los callbacks, permiten ejecutar código de manera alterna pero con mejoras con respecto a los callbacks, ya que estos permiten una mejor composición y mejora en la lectura de código.

La analogía sería que un cantante pueda realizar la promesa de sacar un disco y notificarlo a sus fans, ya sea que esto suceda o no. Los “fans” se suscriben a tal promesa y obtienen la notificación (sea buena o mala) por parte del cantante de si sacó el disco o no. Lo mismo en JavaScript: se tiene un código que genera la promesa (Promise) y código que espera el resultado de tal promesa para operar con ella. Una mejor versión de la analogía se puede encontrar en javascript.info.

Como parte de las promesas, JavaScript incorpora callbacks como resolve y reject para determinar el estado de la promesa. Para acceder al estado y resultado de la promesa se utiliza then, catch y finally, dependiendo.

Ejemplificando: la función dentro de la promesa es el cantante (executor); el objeto devuelto por new Promise, el intermediario; los métodos para acceder al objeto (.then, .catch y '.finally), los fans.

Estructura básica de una promesa:

let promise = new Promise(function(resolve, reject) {
    ...     // executor code
});

promise.then(...);
promise.catch(...);
promise.finally(...);


Ejemplo de promesa con resolución inmediata:

let promise = Promise.resolve(15);
promise.then(value => console.log(`Got ${value}`));

El método then permite un callback que será invocado cuando la promesa se resuelva o falle, este último es un extra que normalmente se maneja mediante catch.

Ejemplo ilustrativo del uso de promesas (secuencial y asíncrono):

const movies = [
    { id: 1,
      title: "Iron Man",
      year: 2008
    },
    {id: 2,
     title: "Spiderman: Homecomming",
     year: 2017
    },
    {id: 3,
     title: "Avengers: Endgame",
     year: 2019
    }
];

function getMovies() {
    return new Promise(function(resolve, reject) {
        setTimeout(() => resolve(movies), 2000);
    })
}

getMovies().then(e => console.log(e));


Otro ejemplo del uso de promesas:

function storage(nest, name) {
    return new Promise(resolve => {
        nest.readStorage(name, result => resolve(result));
    });
}

storage(bigOak, "enemies")
    .then(value => console.log(`Got ${value}`);

Collections of Promises

Si se desea ejecutar muchas promesas en paralelo, se puede utilizar Promise.all, uno de los 6 métodos estáticos de la clase Promise.

La sintáxis es:

let promise = Promise.all([...promises...]);


Ejemplo práctico de promesas en ejecución paralela:

let urls = [
  'https://api.github.com/users/iliakan',
  'https://api.github.com/users/remy',
  'https://api.github.com/users/jeresig'
];

let requests = urls.map(url => fetch(url));

let promises = Promise.all(requests);

promises
    .then(resps => resps.forEach(r => console.log(`${r.url}: ${r.status}`)));

// → https://api.github.com/users/iliakan   : 200
// → https://api.github.com/users/remy      : 200
// → https://api.github.com/users/jeresig   : 200


Async Functions

Para simplificar la legibilidad de código asíncrono mediante promesas, se tiene una sintáxis especial llamada “async/await”.

Una función definida como async envuelve el código no asíncrono en la función y siempre retorna una promesa. Además de async se tiene la sintáxis await que solo se ejecuta dentro de funciones async y permite esperar hasta que una promesa finalice y retorne su resultado.

Ejemplo simple de una función async:

async function fname() {
    return 1;
}

fname.then(console.log)

// → 1

La declaración de return 1 dentro de la función async, es una manera implícita de return Promise.resolve(1).

Ejemplo de una función async con un await:

async function f() {
    let promise = new Promise((resolve, reject) => {
            setTimeout(() => resolve("done!"), 1000);
        });

    let result = await promise;
    
    console.log(result);
}

f();

// → done!

La impresión por consola solo se ejecuta cuando el valor de result ha sido definido; es decir, que el flujo de ejecución dentro de la función asíncrona, se detiene a esperar que la promesa se resuelva (pausa el flujo), ya luego se ejecuta el console.log(result).

Función async y await con manejo de errores:

async function f() {
    try{
        let response = await fetch("http://wololo");
    }
    catch (err) {
        console.log(err);
    }
}

f();

// → TypeError: failed to fetch

En caso no se tuviera definido el try/catch dentro de la función async este sería rechazado y se podría manejar del bloque de la función: f().catch(console.log).

Ejemplo de async/await con Promise.all:

let result = await Promise.all([fetch(url1), fetch(url2)...]);


Generators

Los generadores o funciones generadoras, a diferencia de las funciones convencionales, pueden retornar muchos valores y, similar a las funciones async, pueden pausar su ejecución y reanudarla posteriormente. Cuando una función generadora es invocada, esta devuelve un objeto generador en lugar de ejecutarse, que puede ser gestionado mediante sus métodos como next().

Definición de una función generadora:

function* numberGenerator() {
    yield 1;
    yield 2;
    yield 3;
    return 4;
}

let generator = numberGenerator();

generator.next();

// → {value: 1, done: false}


The Event Loop

Toda función invocada es cargada en la call stack o pila de llamadas con todo el contexto (bindings locales) de la función. Del mismo modo como las funciones se van apilando, estas se desapilan finalizado su ejecución.

Cuando en medio de la ejecución síncrona, se realiza un callback, este será enviado a una queue o cola especial que se encargará de ejecutarlo posteriormente, mientras tanto, el flujo de ejecución seguirá su transcurso normal hasta que se vacíe el call stack que es cuando se carga la función asíncrona y se ejecuta, de este último paso se encarga el event loop: despachar el callback o mensaje.

El proceso dentro del event loop es síncrono, por lo que un callback se despacha una vez llegado su turno, si es que se tiene varios en espera.

Ejemplo:

setTimeout(() => console.log("callback!"), 1000);   // (1)
console.log("function1!");
console.log("function2!");
console.log("function3!");

// → function1!
// → function2!
// → function3!
// → callback!


12. Project: A Robot

→ It will be filled after finished the notes.

Part II: Browser

13. JavaScript and the Browser

Nothing to add!.

14. The Document Object Model

El DOM (Document Object Model) es una interfaz para los documentos HTML y XML que facilita su interación con los lenguajes de programación mediante una representación estructurada del documento.

Se podría decir que el DOM es una interfaz que abstrae todo el documento (página web) en forma de nodos y objeto que tienen propiedades y métodos a los cuales se puede acceder y modificar mediante un lenguaje de programación.

El DOM es independiente de cualquier lenguaje y hace disponible el acceso a todo su contenido estructurado mediante una API. De acuerdo al DOM, cada etiqueta HTML es un objeto y las anidadas dentro de ese objeto, además de ser hijo de la etiqueta padre, también es un objeto; incluso lo textos dentro de las etiquetas son objetos.



Document Structure

Se puede visualizar un documento HTML como un conjunto de cajas anidadas, para las que existe un objeto que permite su interación:

html-boxes.svg



Trees

La estructura del DOM es un árbol que tiene como raíz el la etiqueta html al que se accede mediante document.documentElement. De acuerdo al DOM, cada etiqueta HTML es un objeto.

html-tree.svg



The Standard (omitible)

De el por qué el DOM utiliza códigos númericos para representar el tipo de los nodos, es porque el DOM está diseñado no solo para representar HTML, sino también XML y ser neutral en cuanto al uso de lenguajes. Por esto mismo, tiene ciertos problemas de diseño como el no poder crear nodes inmediatamente para añadirle nodos hijos y atributos, sino tener que crear primero el nodo para después irle agregando los nodos hijo uno por uno.

Para paliar estas carencias, existen librerías o hasta el mismo JavaScript, que permite crear abstracciones.

Moving Through the Tree

Los nodos del DOM cuentas con enlaces que permiten navegar entre ellos:

html-links.svg

let bodyNodes = document.body.childNodes;
let parent = bodyNodes.item(3).parentNode;
let fchild = bodyNodes.item(3).firstChild;
let lchild = bodyNodes.item(3).lastChild;
let psibling = bodyNodes.item(3).previousSibling;
let lsibling = bodyNodes.item(3).lastSibling;


function talksAbout(node, string) {
    if (node.nodeType == Node.ELEMENT_NODE) {
        for (let child of node.childNodes) {
            if (talksAbout(child, string)) {
                return true;
            }
        }
        return false;

    } else if (node.nodeType == Node.TEXT_NODE)
        return node.nodeValue.indexOf(string) > -1;
    
}

talksAbout(document.body, "some text");

// → true

Finding Elements

document.getElementById('name');
document.getElementsByClassName('name');
document.getElementsByTagName('name');
let container = document.querySelector("#test");                 // First match
let matches   = container.querySelectorAll("div.highlighted > p");


Changing the Document

let newParagraph = document.createElement("p");
newParagraph.appendChild(document.createTextNode("some string"));

document.appendChild(newParagraph);
let paragraphs = document.getElementsByTagName("p");
document.body.insertBefore(paragraphs[2], paragraphs[0]);
...
document.body.replaceChild(newParagraph, paragraphs[0]);
let paragraphs = document.getElementsByTagName("p");
paragraphs[0].remove();
<p>The <img src="cat.png" alt="Cat"> in the <img src="hat.png" alt="Hat">a.</p>
<button onclick="replaceImages()">Replace</button>

// Script:
function replaceImages() {
    let images = document.body.getElementsByTagName("img");
    for(let i=images.length-1; i >= 0; i--) {
        let text = document.createTextNode(images[i].alt);
        images[i].parentNode.replaceChild(text, images[i]);
    }
}


let anchors = document.getElementsByTagName("a");

for(let para of Array.from(anchors)) {
    if(para.getAttribute("href") == "index.html")
        para.setAttribute("href", "post.html");
}

Para acceder al atributo class, se puede también utilizar la variable className.

divs[0].offsetWidth;        // Ancho interno + padding + borders + scrollbar
divs[0].offsetHeight;
divs[0].clientWidth;        // Ancho interno + padding
divs[0].clientHeight;
divs[0].getBoundingClientRect();


document.body.style.backgroundColor;                // Obtiene el estilo (1)
document.body.style["background-color"];            // Obtiene el estilo (2)
document.body.style = "background-color: yellow";   // Modifica el estilo

Debido a que algunos estilos contiene guiones (hyphens) y son raros para trabajar con Javascript, se utilizan la opción (2) o en todo caso la primera.

15. Handling Events

Algunos aplicaciones requieren de la interación del usuario para su propósito: entradas de texto, acciones de mouse o teclado; incluso eventos de su entorno y reaccionar ante estos.

Event Handlers

Una manera de determinar si un evento a ocurrido o no, sería la de verificar constantemente en el elemento de interés a través de código o que el Sistema Operativo los detecte y almacene en una cola (queue) y revisarlo cada cierto tiempo desde el aplicativo. Más allá de los problemas que esto conlleva, se hace un uso muy intensivo de recursos.

Una mejor alternativa es que el sistema (en este caso el navegador) notifique activamente cada vez que un evento ocurre y que permita al aplicativo manejarlo mediante handlers.

<button onClick="handleClick()"> Click me </button>

Events and DOM Nodes

Cada event handler esta registrado en un contexto y los event listener únicamente son llamandos cuando el evento sucede en contexto del objeto en el que estan registrados.

<button> Click me </button>

<script>
let button = document.querySelector("button");
button.addEventListener("click", handleClick);
</script>

Event Objects

Los handlers registrados a un elemento reciben implícitamente un argumento: el event object. Este objeto contiene información y características adicionales acerca del evento y que difiere dependiendo del tipo de este mismo (como “click” o “mousedown”).

<button> Click me </button>

<script>
  let button = document.querySelector("button");
  button.addEventListener("mousedown", event => {
    if (event.button == 0) {
      console.log("Left button");
    } else if (event.button == 1) {
      console.log("Middle button");
    } else if (event.button == 2) {
      console.log("Right button");
    }
  });
</script>

Para hacer referencia al propio elemento se puede utilizar la propiedad target del objeto evento.

Propagation

Tener en cuenta que la propagación de un evento es de adentro hacia fuera. Es decir, que el evento click de un boton dentro de un contendor también desencadenará el handler del contenedor (si es que estos comparten el mismo tipo de evento). Para evitar estos casos se usa stopPropagation.

16. Project: A Platform Game

17. Drawing on Canvas

18. HTTP and Forms

El Hypertext Transfer Protocol (HTTP) es el mecanismo mediante el cual se solicitan y proveen datos en la WWW.

18.1 The Protocol

El protocolo HTTP es un protocolo de tipo stateless, por lo que no guarda ninguna información sobre las conexiones anteriores. Para suplir esta necesidad se hace uso de las cookies.

Para realizar peticiones a un servidor, se hace uso de métodos de petición o “verbos”, tales como POST, GET, PUT, DELETE, etc. seguido de la URL del recurso sobre la que se quiere aplicar la petición y la versión HTTP (1.0, 1.1, 2 o incluso 3) que soporta el cliente. Por el lado del servidor, este retorna la versión HTTP utilizada seguido del código de respuesta (1XX, 2XX, 3XX, 4XX o 5XX) y la frase asociada a dicho retorno.

Ejemplo de una petición:

GET /18_http.html HTTP/1.1
Host: www.eloquentjavascript.net
User-agent: Mozilla/5.0 ...

Ejemplo de respuesta:

HTTP/1.1 200 OK
Server: nginx/1.18.0
Date: Tue, 25 Jan 2022 05:03:19 GMT
Content-Type: text/html
Content-Length: 110321


18.2 Browsers and HTTP

<form method="POST" action="example/message.html">
    <input type="text" name="name">
    <textarea name="message"> </textarea>
    <button type="submit"> Send </button>
</form>

Si el método es GET, la información del formulario es agregada al final de la URL de action en un formato codificado que JavaScript también porporciona: encodeURIComponent("Yes?"). Y si el método es POST, este no es visible en la URL, y en su lugar se agrega al cuerpo de la petición.

18.3 Fetch

Fetch es la interfaz mediante el cual el navegador realiza peticiones HTTP además de usar promesas.

Ejemplo 1:

fetch("example/data.txt").then(response => {
    console.log(response.status);
    console.log(response.headers.get("Content-Type"));
});

Ejemplo 2:

fetch("https://api.github.com/users/jsanxez", {method: 'GET'})
.then(response => response.text())
.then(text => console.log(text));


18.4 HTTP Sandboxing

Por cuestiones de seguridad los navegadores no permiten realizar peticiones HTTP en los scripts de páginas web hacia otros dominios a menos que el servidor lo permita e indique en su respuesta: Access-Control-Allow-Origin.

18.5 Appreciating HTTP

Para poder comunicar Javascript del lado cliente con otro del lado del servidor, se puede utilizar Remote Procedure Calls, que es el mismo concepto detrás de las funciones, solo que estos se ejecutan en otra máquina.

Un enfoque más actual es la de poder utilizar los métodos HTTP junto con documentos JSON.

18.6 Security and HTTPS

El protocolo seguro HTTP (https), encripta la comunicación entre el cliente y el servidor evitando su espionaje o modificación. Pero antes del intercambio de datos, es el cliente quien verifica que el servidor sea quien dice ser mediante una petición de su certificado criptográfico.

Aunque ha habido casos en los que el HTTPS falló, es mucho más conveniente que el HTTP plano.

18.7 Form Fields

Los forms fueron concebidos mucho antes de Javascript para permitir enviar mediante HTTP la informacion enviada por el usuario.

Aunque un campo no necesariamente tiene que aparecer dentro de la etiqueta form, solo aquellos dentro de este, serán enviados como un todo.

18.8 Focus

Solo se puede escribir en aquellos campos que están focused y esto puede ser controlado mediante JavaScript con focus y blur. Para mover el foco mediante el teclado, se usa TAB, al que tambien se puede cambiar el orden en el que recibe el foco mediante tabindex=2, un valor negativo en el tabindex (-1), hace que este se omitible.

<input type="text">
<script>
  document.querySelector("input").focus();
  console.log(document.activeElement.tagName);
  document.querySelector("input").blur();
  console.log(document.activeElement.tagName);

  // → input
  // → body
</script>

18.9 Disable Fields

Para deshabilitar un campo de formulario se usa el atributo disabled. Esto es útil cuando ya se ha iniciado una acción y no se quiere que se genere otra petición.

18.10 The Form as a Whole

La etiqueta form tiene una propiedad elements que contiene una colección tipo arreglo de todos los campos que contiene identificados mediante name.

<form action="example/submit.html">
    Name: <input type="text" name="name"><br>
    Password: <input type="password" name="password"><br>
    <button type="submit">Log in</button>
</form>
<script>
    let form = document.querySelector("form");
    console.log(form.elements[1].type);
    console.log(form.elements.password.type);
    console.log(form.elements.name.form == form);

    // → password
    // → password
    // → true
</script>

Los valores de los campos de un formulario pueden ser enviados mediante el botón submit o con foco en el form; pero antes de que se navegue hacia la pagina indicada en action, se lanza un evento de tipo submit que puede ser manejado con JavaScript. Esto es útil cuando se quiere verificar los datos que se quieren enviar o realizar un envío asíncrono.

<form action="example/submit.html">
  Value: <input type="text" name="value">
  <button type="submit">Save</button>
</form>
<script>
  let form = document.querySelector("form");
  form.addEventListener("submit", event => {
    console.log("Saving value", form.elements.value.value);
    event.preventDefault();
  });
</script>

18.11 Text Fields

18.12 Storing Data Client-Side

Si bien no se puede almacenar datos que persistan entre sesiones, se puede usar el objeto localStorage para almacenar data que persista a los reloads. Estos datos pueden ser sobreescritos o eliminados mediante removeItem o cuando el usuario limpie su local data.

Los sitios de diferentes dominios obtienen diferentes compartimentos de almacenamiento por lo que el localStorage de un sitio dado solo puede, en principio, ser sobreescrito o leído por los scripts del sitio.

19. Project: A Pixel Art Editor

Part III: Node

20. Node.js

20.1 Background

20.2 The Node Command

20.3 Modules

Ejemplo de uso:

// reverse.js
exports.reverse = function(string) {
    return Array.from(string).reverse().join("");
}

// main.js
const {reverse} = require("./reverse");

let argument = process.argv[2];
console.log(reverse(argument));

// Console:
//: node main.js "Hola mundo!"
//→ !odnum aloH

20.4 Installing With NPM

NPM es principalmente utilizado para descargar paquetes, sea mediante: npm install o npm install {paquete}; con el primero se instalan los paquetes listados en el package.json y con el segundo, el paquete especificado. Los paquetes instalados se guardan dentro de su propio directorio en node_modules y se agregan como dependencia del aplicativo al archivo package.json, por lo que es necesario tener tal archivo, sea creándolo manualmente o mediante npm init.

El archivo de dependencias contiene información sobre el proyecto tales como nombre, versión y lista de dependencias.

20.5 Versions

El archivo package.json contiene la versión de la propia aplicación como de sus dependencias y sigue un versionado semántico que consta de 3 dígitos que se incrementan (MAJOR.MINOR.PATCH):

Si al principio de la versión de la dependencia se tiene el carácter ^, como: ^2.3.0, esto significa que se puede instalar una versión sea mayor o igual a 2.3.0 y menor a 3.0.0.

Nota: Para publicar el paquete se utiliza npm publish.

20.6 The File System Module

Uno de los módulos mas importantes de Node es fs (File System).

const {readFile} = require('fs');

readFile("deletme", "utf8", (error, text) => {
    if(error) throw error;
    console.log("The file contains: ", text);
});
const {writeFile} = require('fs');

let contentOfFile = "It's the content of the file.";
writeFile("deletme", contentOfFile, err => {
    if(err) throw err;
    else console.log("The file was written!");
});
const {readFile} = require('fs').promises;

readFile("Apuntes_Javascript.md", "utf8")
    .then(text => console.log("The file contains: ", text));
const {readFileSync} = require('fs');
console.log(readFileSync('Apuntes_Javascript.md', 'utf8'));

Nota: Las operaciones síncronas pueden producir ciertos retrasos, ya que el programa se detiene hasta que la función finalice su ejecución.

20.7 The HTTP Module

Este módulo permite la ejecución de un servidor HTTP y realizar peticiones a este:

const http = require('http');
let server = http.createServer((request, response) => {
    response.writeHead(200, {"content-type": "text/html"})
    response.write(`
        <h1>Hello!</h1>
        <p>You asked for <code>${resquest.url}</code></p>`);
    response.end();
});
server.listen(8000);
console.log("Listening! (port 8000)");

20.8 Streams

20.9 A File Server

20.10 Summary