martes, 3 de octubre de 2023

Clean JavaScript (Resumen I)

 

Portada del libro Clean JavaScript

VAR, LET Y CONST

/*
Lo ideal es tratar de evitar a toda costa el uso de var, ya que no permite
definir variables con ámbito de bloque, lo cual puede derivar en comportamientos
inesperados.
 
La diferencia entre let y const radica en que a const no se le puede reasignar
su valor, aunque si modificarlo.
*/
 
// var___________________________________________________
var variable = 5;
{
    console.log('inside', variable); // 5
    var variable = 10;
}
 
console.log('outside', variable); // 10
variable = variable * 2;
console.log('changed', variable); // 20
 
// let___________________________________________________
let variable2 = 5;
 
{
    console.log('inside', variable2);
     // error Cannot access 'variable' before initialization
     // La variable2 vive dentro de este scope, por lo que no
     // Pede ser invocada anes de ser inicializada.
    let variable2 = 10;
}
 
console.log('outside', variable2); // 5
variable2 = variable2 * 2;
console.log('changed', variable); // 10
 
// const_________________________________________________
const variable3 = 5;
variable3 = variable3 * 2; //error

 

Ámbito global

 Cualquier variable que no esté dentro de un bloque de una función, estará dentro del ámbito global. Dichas variables serán accesibles desde cualquier parte de la aplicación:

let greeting = 'hello world';
 
function greet()
{
    console.log(greeting);
}
greet
();

 

Ámbito de bloque

Los bloques en Javascript se delimitan mediante llaves, una de apertura ‘{‘, y otra de cierre ‘}’. Como comentamos en el apartado de “Uso correcto de var, let y const”, para definir variables con alcance de bloque debemos hacer uso de let o const:

{
    let greeting = "Hello universe";
    var lang = "English";
    console.log(greeting); // Helo universe
}
 
console.log(lang);//”English”
console.log(greeting);//// Uncaught ReferenceError: greeting is not defined

 

como puedes ver, si invoco greeting desde fuera del bloque me dice que la variable no está definida, porque esa variable solo existe dentro del scope de las llaves, sin embargo si invoco lang si ya que al haver usado la palabra reservada var y no let, esa variable vive fuera del scope.

Ámbito estático vs. Dinámico

El ámbito de las variables en JavaScript tiene un comportamiento de naturaleza estática. Esto quiere decir que se determina en tiempo de compilación en lugar de en tiempo de ejecución. Esto también se suele denominar ámbito léxico (lexical scope). Veamos un ejemplo:

const number = 10;
 
function printNumber()
{
    console.log( number );
}
 
function app()
{
    const number = 5;
    printNumber();
}
 
app(); // 10

En el ejemplo, console.log (number) siempre imprimirá el número 10 sin importar desde dónde se llame la función printNumber(). Si JavaScript fuera un lenguaje con el ámbito dinámico, console.log(number) imprimiría un valor diferente dependiendo de dónde se ejecutará la función printNumber().

Hoisting

En JavaScript las declaraciones de las variables y funciones se asignan en memoria en tiempo de compilación; a nivel práctico es como si el intérprete moviera dichas declaraciones al principio de su ámbito. Este comportamiento es conocido como hoisting. Gracias al hoisting podríamos ejecutar una función antes de su declaración:

greet(); //”Hello world”;
 
function greet(){
    let greeting = "Hello world";
    console.log(greeting);
}

 

Al asignar la declaración en memoria es como si “subiera” la función al principio de su ámbito. En el caso de las variables, el hoisting puede generar comportamientos inesperados, ya que como hemos dicho solo aplica a la declaración y no a su asignación:

var grret = "Hi";
 
(function () {
    console.log(greet); // Undefined
    var greet = "Hello";
    console.log(greet); // Hello
})();

 

En el primer console.log del ejemplo, lo esperado es que escriba “Hi”, pero como hemos comentado, el intérprete “eleva” la declaración de la variable a la parte superior de su scope. Por lo tanto, el comportamiento del ejemplo anterior sería equivalente a escribir el siguiente código:

var grret = "Hi";
 
(function () {
  var greet;
  console.log(greet);// "undefined"
  greet = "Hello";
  console.log(greet); //”Hello”
})();

 

He usado este ejemplo porque creo que es muy ilustrativo para explicar el concepto de hoisting, pero volver a declarar una variable con el mismo nombre y además usar var para definirlas es muy mala idea.

Funciones

“Sabemos que estamos desarrollando código limpio cuando cada función hace exactamente lo que su nombre indica”. – Ward Cunningham

Las funciones son la entidad organizativa más básica en cualquier programa. Por ello, deben resultar sencillas de leer y de entender, además de transmitir claramente su intención. Antes de profundizar en cómo deberían ser, exploraremos las diferentes maneras en las que se pueden definir: declaración, expresiones y funciones arrow. Además, en esta última explicaremos el funcionamiento del objeto this, del cual podemos adelantar que tiene un comportamiento poco intuitivo en JavaScript.

 

 

 

Declaración de una función

function doSomething()
{
    return "Doing something";
}
 
doSomething()  //"Doing something"

 

Expresión de una función

Una expresión de una función tiene una sintaxis similar a la declaración de una función, con la salvedad de que asignamos la función a una variable:

const  doSomething = function()
{
    return "Doing something";
}
 
doSomething()  //"Doing something"

 

Expresiones con funciones flecha (arrow functions)

Con la aparición de ES6, se añadió al lenguaje la sintaxis de la función flecha, una forma de definir funciones mucho más legible y concisa.

 const doSomething =() => "Doing something";
 
 //El return está implícito si no añadimos las llaves.

 

Las arrow functions son ideales para declarar expresiones lambda1 (funciones en línea), puesto que se reduce el ruido en la sintaxis y se mejora la expresividad e intencionalidad del código.

 //Sin arrows functions
 [1, 2, 3].map(function(n){return n * 2})
 //Con arrows functions
  [1, 2, 3].map(n => n * 2
)

 

Las arrow functions también son muy útiles a la hora de escribirfunciones currificadas. Como sabrás, una función de este tipo toma un argumento, devuelve una función que toma el siguiente argumento, y así sucesivamente. Con las arrow functions, este proceso puede acortarse, lo que permitirá obtener un código mucho más legible.

 

 

 

// Sin arrow functions
function add(x)
{
    return function(y)
    {
        return x + y;
    }
}
 
// Con arrow functions
const add = x => y => x + y;
 
const addTwo = add(2);
console.log(addTwo(5)) // 7

 

Funcionamiento del objeto this en arrow functions

Otra característica interesante de las arrow functions es que cambian el comportamiento por defecto del objeto this en JavaScript. Cuando creamos una arrow function, su valor de this queda asociado permanentemente al valor de this de su ámbito externo inmediato; window en el caso del ámbito global del navegador o global en el caso del ámbito global de NodeJS:

const isWindow = () => this === window;

isWindow(); // true

En el caso de encontrarse en el ámbito local de un método, el valor de this sería el valor del ámbito de la función. Veamos un ejemplo:

 
const counter = {
    number: 0,
 
    increase() {
        setInterval(()=> ++this.number, 1000);
    }
};
 
counter.increase
();

 

Funciones autoejecutadas IIFE

Una forma de definir funciones menos conocida es a través de las IIFE. Las IIFE (Immediately-Invoked Function Expressions), o funciones autoejecutadas en castellano, son funciones que se ejecutan al momento de definirse:

(function(){
    // do something
})()

 

Este patrón era muy utilizado para crear un ámbito de bloque antes de que se introdujeran let y const. Desde la aparición de ES6 esto no tiene demasiado sentido, pero es interesante conocerlo:

(function(){
    var number = 42;
}());
 
console.log(number); // Reference error

 

Cómo hemos visto en la sección de ámbito de bloque, el uso de let y const para definirlo es mucho más intuitivo y conciso:

{
    let number = 42;
}
 
console.log(number); // Reference Error

 

Parámetros y argumentos

Los argumentos son los valores con los que llamamos a las funciones, mientras que los parámetros son las variables nombradas que reciben estos valores dentro de nuestra función:

1 const double = x => x * 2; // x es el parámetro de nuestra función

2 double(2); // 2 es el argumento con el que llamamos a nuestra función

Limita el número de argumentos

Una recomendación importante es la de limitar el número de argumentos que recibe una función. En general deberíamos limitarnos a tres parámetros como máximo. En el caso de tener que exceder este número, podría ser una buena idea añadir un nivel más de indirección a través de un objeto:

 

 

Parámetros por defecto

Desde ES6, JavaScript permite que los parámetros de la función se inicialicen con valores por defecto.

// Con ES6
 
function greet(text = 'world')
{
    console.log('Hello' + text);
}
 
greet(); // sin parámetro. Hello world
greet(undefined); // undefined. Hello world
greet('crafter') // con parámetro. Hello crafter

 

En JavaScript clásico, para hacer algo tan sencillo como esto teníamos que comprobar si el valor está sin definir y asignarle el valor por defecto deseado:

// Antes de ES6
function greet(text)
{
    if(typeof text === 'undefined')
    text = 'world';
 
    console.log('Hello ' + text);
}

 

Aunque no debemos abusar de los parámetros por defecto, esta sintaxis nos puede ayudar a ser más concisos en algunos contextos.

Parámetro rest y operador spread

El operador … (tres puntos) es conocido como el parámetro rest o como operador spread (propagación en español), dependiendo de dónde se emplee. El parámetro rest unifica los argumentos restantes en la llamada de una función cuando el número de argumentos excede el número de parámetros declarados en esta. En cierta manera, el parámetro rest actúa de forma contraria a spread (operador propagación en español): mientras que spread “expande” los elementos de un array u objeto dado, rest unifica un conjunto de elementos en un array.

//ESTAS DOS NUEVAS CARACTERÍSTICAS DE ES6 SON MUY ÚTILES PARA EL TRABAJO CON FAMEWORKS REACTIVOS JS
//COMO ANGULAR, VUE Y REACT. EL PARÁMETRO REST Y EL OPERADOR DE PROPAGACIÓN SPREAD.
 
/*- PARÁMETROS REST:
    Los parámetros rest son una forma de ir agregando virtualmente parámetros infinitos
    a una función o dentro de una variable. Por ejemplo tenemos un arreglo pero no sabemos
    cuantas posiciones va a tener, aqui definiremos un parámetro REST, para ello le anteponemos
    tres puntos suspensivos. antes de la variable u opbjeto donde van a estar guardados esos
    posibles valores infinitos.
*/
 
//Imaginemos una función que va a recibir valores y los va a sumar pero
//no sabemos cuantos numeros vamos a recibir para sumar.
 
function sumar (a,b, ...c){
  let resultado = a + b;
 
 //c lo convertimos en un sumatorio mediante un forEach y guardamos el resultado en una variable
  c.forEach(function(n){  
      resultado += n
  });
 
//devolvemos esa variable
 return resultado;  
}
 
//console.log(sumar(1,2));
console.log(sumar(1,2,3));
 
//a partir del tercer numero se detecta el spread y devuelve la suma de todos los números
console.log(sumar(1,2,3,4));  
 
//SPREAD OPERATOR [OPERADOR DE PROPAGACIÓN]
/*
Este operador nos va a permitir expandir una expresión
en situaciones donde tengamos que guardar múltiples
elementos o arguemntos.
ejemplo: Imaginemos que tenemos un arreglo con cierto número
de elementos, pero en un momento dedo recibimos nuevos
parámetros. En casos así en vez de estar haciendo
concatenaciones o push a nuestro arreglo, podemos
agregarlos con el SPREAD operator.
 */
 
 const arr1 = [1,2,3,4,5],
       arr2 = [6,7,8,9,0]
 
 //console.log(arr1, arr2);
 
  //AHORA IMAGINEMOS QUE QUEREMOS CREAR UN ARREGLO 3
  //A PARTIR DE DE ESTOS DOS ANTERIORES. YO PODRÍA HACER:
  //const arr3 =[arr1, arr2]; //<--SI YO HAGO ESTO ESTOY CREANDO UN ARREGLO DE DOS POSICIONES QUE INTERNAMENTE TIENENDOS ARREGLOS DE 5 POSICIONES
                             //NO ES LO QUE QUERÍAMOS, QUEREMOS CREAR UN ARRAY CON TANTAS POSICIONES COMO LA SUMA DE LOS ANTERIORES Y SUS DATOS
 
 // console.log(arr3);
                             //PARA ELLO ANTEPONGO LOS 3 PUNTOS SUSPENSIVOS SPREAD OPERATOR
 
     const arr3 =[...arr1, ...arr2];
     console.log(arr3);  //AHORA SI TENGO UN ARRAY CON 10 POSICIONES Y TODOS LOS VALORES DE LOS ANTERIORES

 

Cláusulas de guarda

Las cláusulas de guarda, también conocidas como aserciones o precondiciones, son una pieza de código que comprueba una serie de condiciones antes de continuar con la ejecución de la función

En el siguiente ejemplo como puedes observar, tenemos demasiados condicionales anidados.

const getPayAmount = () => {
    let result;
    if (isDead){
         result = deadAmount();
         }
         else {
          if (isSeparated){
        result = separatedAmount();
        }
         else {
          if (isRetired){
         result = retiredAmount();
         }
         else{
         result = normalPayAmount();
         }
        }
    }
       return result;
}

 

 Para resolverlo podríamos sustituir los casos edge por cláusulas de guarda

const getPayAmount = () => {
    if (isDead)
    return deadAmount();
 
    if(isSeparated)
    return separatedAmount();
 
    if (isRetired)
    return retiredAmount();
 
    return normalPayAmount();
}

 

Evita el uso de else

Otra estrategia que suelo utilizar para evitar la anidación es no emplear la palabra clave else. En mis proyectos intento evitarlo siempre que sea posible; de hecho, he trabajado en proyectos con miles de líneas de código sin utilizar un solo else. Para lograrlo, suelo priorizar el estilo declarativo, hacer uso de las cláusulas de guarda cuando uso estructuras condicionales o reemplazo las estructurasif/else por el operador ternario, lo que da lugar a un código mucho más comprensible y expresivo:

 //if/else
 const isRunning = true;
 if(isRunning){
    stop()
  }
  else{
    run()
 }
 
//operador ternario
 isRunning ? stop() : run
()

 

Prioriza las condiciones asertivas

La evidencia nos dice que las frases afirmativas suelen ser más fáciles de entender que las negativas, por esta razón deberíamos invertir, siempre que sea posible, las condiciones negativas para convertirlas en afirmativas:

// Negative
if(!canNotFormat)
{
    format()
}
 
// Positive
if(canFormat)
{
    format()
}

 


 

GLOSARIO

1 Expersiones Lambda en JavaScript

Las expresiones lambda son conceptos que permiten tratar una función como datos. Todo en JavaScript puede manejarse comoun objeto, lo que implica que una función puede enviarse como un parámetro a otra función y recibirse como un valor de retorno de la función llanada.

Ejemplo:

function learnLambda(array, funct)
{
    let output = "";
    for (const value of array) {
        output += funct(value) + " ";
    }
    console.log (output);
}
 
const array = [5, 6, 7, 8, 9];
const sqqr = (value) => value ** 2;
 
learnLambda(array, sqqr);  //25 36 49 64 81

 

La diferencia más notable entre Lambda y las funciones anónimas en JavaScript es que las funciones Lambda se pueden nombrar.

En el código anterior tenemos una función learnLambda(array, funct), que recibe como parámetros un array y el resultado de una función, en este caso la función felcha expresada y guardada en la constante sqqr.

 

2 Método Map

El método map() de las instancias de Matriz crea una nueva matriz poblada con los resultados de llamar a una función proporcionada en cada elemento de la matriz llamada

// Método Map
const array1 = [1, 4, 9, 16];
 
// Pass a function to map
const map1 = array1.map((x) => x * 2);
 
console.log(map1); // [2, 8, 18, 32]
 

Sintaxis

map(callbackFn)

map(callbackFn, thisArg)

 

3 Método filter

El método filter() de las instancias de Matriz crea una copia superficial de una parte de una matriz dada, filtrada a sólo los elementos de la matriz dada que pasan la prueba implementada por la función proporcionada.

// Método filter
const words = ['spray', 'limit', 'elite', 'exuberant', 'destruction', 'present'];
const result = words.filter((word) => word.length > 6);
console.log(result);
// ["exuberant", "destruction", "present"]


No hay comentarios:

Publicar un comentario