← JavaScript: el lenguaje

Sesión 3

Funciones y depuración

Tres horas. Media hora para entender qué es una función, qué significa que una función sea un valor y cómo se para un programa para mirar dentro, y dos horas y media convirtiendo tu código en piezas reutilizables y reparando programas rotos.

Hoy · Hoja de ruta

  1. 1. Aprende: Qué son parámetros y retorno, qué es el ámbito, cómo se escribe una función flecha y cuáles son los cuatro tipos de error.
  2. 2. Haz: Convierte el código de la semana pasada en funciones, escribe funciones que reciben funciones y depura tres programas que no puedes reescribir.
  3. 3. Comprueba: Cada función devuelve un valor y no imprime, y encuentras la causa de un fallo antes de tocar el código.

Antes de empezar · 5 minutos, sin apuntes

  1. En tu código de la semana pasada, ¿qué trozo has escrito más de una vez?
  2. Si mañana cambia el porcentaje de IVA, ¿cuántos sitios tendrías que tocar?
  3. ¿Qué haces ahora mismo cuando algo no funciona?

Se explica

25 minutos · conceptos y demostración

Dos ideas grandes y una herramienta. Las ideas: una función devuelve en lugar de imprimir, y una función es un valor que se puede guardar y pasar. La herramienta: parar el programa en una línea y mirar todas las variables a la vez.

Declarar y llamar

function calcularIva(base, tipo) {
  return base * (1 + tipo);
}

const conIva = calcularIva(89.9, 0.21);

Tres piezas: el nombre, que dice qué hace; los parámetros, que son lo que necesita; y el retorno, que es lo que produce.

Una función devuelve; no imprime

Una función que hace console.log en lugar de return solo sirve para lo que estás haciendo hoy. La que devuelve un valor sirve para imprimirlo, para sumarlo, para pintarlo en la UD4 y para enviarlo al servidor en la UD6.

Regla del módulo: las funciones calculan y devuelven; imprimir es cosa de quien las llama.

function calcularIva(base, tipo = 0.21) {
  return base * (1 + tipo);
}

calcularIva(100);        // 121
calcularIva(100, 0.10);  // 110

JavaScript no protesta si llamas con menos argumentos de los declarados: el que falta vale undefined, y el cálculo acaba en NaN. Los valores por defecto evitan la mitad de esos casos.

Ámbito: dónde vive cada variable

const iva = 0.21;                 // global del módulo

function total(base) {
  const impuestos = base * iva;   // ve el de fuera
  return base + impuestos;
}

console.log(impuestos);           // ReferenceError: no existe aquí

Lo declarado dentro de una función solo existe dentro. Lo de fuera se ve desde dentro. Esa restricción es deseable: si todo fuera global, dos funciones que emplean i interferirían entre sí.

Ámbito de bloque

let y const viven dentro de las llaves donde se declaran, incluidas las de un if o un for. Es otra razón para no usar var, que se escapa del bloque.

Las funciones, además, se quieren pequeñas y con un solo cometido:

// Difícil de probar y de reutilizar
function procesar(carrito) { /* 60 líneas */ }

// Tres funciones que se leen y se prueban por separado
function calcularSubtotal(carrito) { }
function aplicarDescuento(subtotal) { }
function calcularEnvio(importe) { }

Una señal fiable: si al describir qué hace una función necesitas la palabra «y» dos veces, probablemente son dos funciones.

La misma función, tres formas

// Declaración
function doble(n) {
  return n * 2;
}

// Expresión guardada en una variable
const doble2 = function (n) {
  return n * 2;
};

// Función flecha
const doble3 = (n) => n * 2;

La flecha con una sola expresión devuelve esa expresión sin escribir return. Con llaves, vuelve a hacer falta:

const etiquetar = (producto) => {
  const estado = producto.stock > 0 ? "Disponible" : "Agotado";
  return `${producto.nombre} · ${estado}`;
};
Forma Úsala para
Declaración function Funciones con nombre del programa, sobre todo si son largas
Flecha Funciones cortas que se pasan como argumento

Una función es un valor

Esta es la idea que abre la sesión 4. Una función puede guardarse:

const operaciones = {
  suma: (a, b) => a + b,
  resta: (a, b) => a - b
};

operaciones.suma(2, 3);    // 5

También puede pasarse como argumento:

function aplicarATodos(lista, transformar) {
  const resultado = [];
  for (const elemento of lista) {
    resultado.push(transformar(elemento));
  }
  return resultado;
}

aplicarATodos([1, 2, 3], (n) => n * 10);   // [10, 20, 30]

Ahí acabas de escribir a mano lo que la semana que viene hará map. Merece la pena haberlo escrito una vez: después, map deja de ser magia.

Callback

Una función que se le pasa a otra para que la llame ella. En la UD4 será «lo que hay que hacer cuando el usuario pulse»; en la UD6, «lo que hay que hacer cuando llegue la respuesta».

Los cuatro tipos de error

Tipo Qué es Ejemplo
Sintaxis El código no se puede leer Falta una llave o un paréntesis
Referencia Usas un nombre que no existe Una variable mal escrita
Tipo Llamas a algo que no es lo que crees productos.filtrar is not a function
Lógico No hay error, pero el resultado está mal El total sale mal, y nadie protesta

Los tres primeros los encuentra el navegador. El cuarto solo lo encuentras tú, y es el que de verdad importa.

Parar el programa y mirar dentro

console.log te dice el valor en un punto. Un punto de interrupción te deja parar el programa y mirarlo todo a la vez.

Depurar en el navegador
  1. Pestaña Sources y abre tu fichero
  2. Clic en el número de línea: punto de interrupción
  3. Recarga: la ejecución se detiene ahí
  4. Mira Scope: todas las variables y sus valores
  5. Avanza paso a paso y observa qué cambia

También puedes escribir debugger; en el código: con DevTools abierto, el programa se detiene en esa línea.

Depurar es reducir el trozo sospechoso, no cambiar cosas

El método es siempre el mismo: qué esperabas, qué ocurre, dónde deja de coincidir. Pon una comprobación a la mitad del recorrido. Si ahí ya está mal, el problema está antes; si está bien, está después. Cada comprobación divide el terreno en dos.

Cambiar líneas a ver si suena la flauta puede arreglar el síntoma de hoy y dejar la causa dentro.

Se trabaja

150 minutos · práctica sobre tu propio proyecto y sobre código ajeno

Los dos primeros pasos convierten tu código en piezas. Los tres últimos lo someten a lo que de verdad pasa en un trabajo: reparar lo que escribió otro, y dejar puesta la comprobación que lo habría avisado.

Paso 1 · Refactoriza en funciones · 35 min

Sobre tu solución de la sesión 2:

  1. Extrae calcularSubtotal, aplicarDescuento y calcularEnvio.
  2. Que ninguna imprima: todas devuelven.
  3. Escribe una función formatearPrecio(valor) que devuelva el texto con dos decimales y el símbolo del euro.
  4. Añade valores por defecto a los parámetros que los admitan.
  5. Comprueba cada función por separado con tres entradas distintas.

Antes de continuar: ninguna de tus funciones contiene un console.log, y cada una se puede probar sola sin ejecutar el resto del programa.

Paso 2 · Funciones que reciben funciones · 40 min

  1. Reescribe tres de tus funciones del paso 1 como flechas y comprueba que siguen dando lo mismo.
  2. Escribe aplicarATodos y úsala para subir un 10 % todos los precios.
  3. Escribe filtrarPor(lista, condicion) y úsala con dos condiciones distintas.
  4. Escribe contarSi(lista, condicion).
  5. Guarda tres reglas de negocio en un objeto de funciones y llámalas por su nombre.
  6. Encadena filtrarPor y aplicarATodos para responder a una pregunta compuesta sobre tu catálogo.

Conserva estas tres funciones: en la sesión 4 las compararás con map, filter y reduce, y la comparación solo funciona si las has escrito antes.

Paso 3 · Tres programas rotos · 40 min

Es el trabajo central de la sesión. Recibirás tres ficheros con un fallo cada uno: uno de referencia, uno de tipo y uno lógico. Para cada uno:

  1. Escribe qué debería hacer el programa.
  2. Ejecútalo y anota qué hace en realidad.
  3. Localiza con un punto de interrupción la primera línea donde el valor deja de ser el esperado.
  4. Explica la causa antes de corregir.
  5. Corrige con el cambio más pequeño posible.

El tercero es el que cuenta: no muestra ningún error y devuelve un número. Ahí no hay mensaje que leer, solo el método.

Consultar · la consola más allá de console.log
console.log(producto);            // lo normal
console.table(productos);         // una lista de objetos, en tabla
console.warn("Stock bajo");       // aviso
console.error("Precio inválido"); // error
console.log({ subtotal, envio, total });   // nombre y valor de cada uno

El último truco vale su peso en oro: envolviendo las variables en llaves, la consola imprime el nombre junto al valor, y se acabaron los seis números sueltos que no sabes de quién son.

Paso 4 · La comprobación que lo habría detectado · 20 min

Vuelve sobre los tres programas del paso anterior. Para cada uno, añade una comprobación que hubiera hecho visible el fallo en el momento de producirse, en lugar de mucho después.

Programa Causa del fallo Comprobación añadida Qué avisa y cuándo

Un criterio para escribirlas: la comprobación debe fallar ruidosamente. Un aviso que se pierde entre cincuenta líneas de consola no sirve; uno que detiene el cálculo y dice qué dato era, sí.

Aplica después la misma idea a tu propio código: pon esa comprobación en la función donde entra el dato, no donde aparece el síntoma.

Paso 5 · Reduce tu programa principal · 15 min

Deja el cuerpo principal de main.js en cinco líneas o menos, que solo llamen a funciones. Todo lo demás vive dentro de ellas.

Si no te caben en cinco, no sigas dividiendo por dividir: mira qué función hace dos cosas y sepárala. Después lee las cinco líneas en voz alta. Deberían sonar como el enunciado del problema.

Ampliación si has completado el trabajo

Primero termina y comprueba los cinco pasos. El primer reto es de depuración y se puede cronometrar; el segundo prepara la sesión 4.

Reto 1 · Rompe tu propio código

Haz una copia de tu programa e introduce tres fallos sutiles, uno de cada tipo:

  • uno de referencia que no salte hasta que se cumple cierta condición;
  • uno de tipo que solo aparezca con un dato concreto;
  • uno lógico que no produzca ningún error y devuelva un número ligeramente equivocado.

Guarda aparte la solución. Intercámbialo con un compañero y cronometrad cuánto tarda cada uno en encontrar los tres. Al terminar:

  1. ¿Cuál costó más? Casi siempre el lógico, y la diferencia de tiempo suele ser de un orden de magnitud.
  2. ¿Encontró la persona la causa, o solo el síntoma? Comprobadlo preguntando qué pasaría con otro dato distinto.
  3. ¿Qué comprobación, puesta en el sitio correcto, habría reducido el tiempo de búsqueda a cero?
  4. Anota qué técnica usó que tú no habías usado.

Reto 2 · Escribe reducir y demuestra que lo demás sobra

En el paso 2 escribiste aplicarATodos, filtrarPor y contarSi. Escribe ahora una cuarta función, más general que las tres:

function reducir(lista, combinar, inicial) {
  // recorre la lista acumulando un resultado
}
  1. Escríbela y pruébala sumando una lista de precios.
  2. Úsala para calcular el máximo de una lista, sin comparar con Math.max.
  3. Ahora la parte interesante: reescribe aplicarATodos usando reducir. La función que combinas devuelve una lista, no un número.
  4. Reescribe también filtrarPor y contarSi con reducir.
  5. Explica en tres líneas qué significa que las tres se puedan escribir con la cuarta, y por qué aun así seguimos usando las tres por separado en código real.

La semana que viene aparecerán map, filter y reduce con esos mismos nombres. Haber escrito las cuatro a mano es la diferencia entre usarlas y entenderlas.

Objetivo mínimoCuatro funciones que devuelven, probadas una a una, y los tres programas rotos localizados con la causa escrita antes del arreglo.
Si lo tienesaplicarATodos, filtrarPor y contarSi funcionando, la tabla de comprobaciones rellenada y el programa principal en cinco líneas.
RetoLos tres fallos sutiles intercambiados y cronometrados, y reducir escrita con las otras tres reconstruidas a partir de ella.

Cierre

5 minutos · comprobación y recuerdo

Lista de verificación de la sesión

  • Tus funciones devuelven valores y no imprimen.
  • Cada función hace una sola cosa, y el programa principal cabe en cinco líneas.
  • Sabes pasar una función como argumento.
  • Has escrito tu propia versión de map y de filter.
  • Usas puntos de interrupción, no solo console.log.
  • Explicas la causa de un fallo antes de corregirlo.

Antes de cerrar · 3 minutos, sin mirar

  1. ¿Qué devuelve una función que no tiene return?
  2. ¿Por qué preferimos devolver a imprimir?
  3. ¿Qué devuelve (n) => n * 2? ¿Y (n) => { n * 2 }?
  4. ¿Qué es un callback?
  5. ¿Cuáles son los cuatro tipos de error, y cuál es el peligroso?
  6. Un total sale mal y no hay ningún error en consola. ¿Cuáles son tus dos primeros pasos?
Ver respuestas

1 · undefined.

2 · Porque el valor devuelto se puede reutilizar: imprimir es solo una de las cosas que se pueden hacer con él.

3 · El doble de n, por el retorno implícito; la segunda devuelve undefined, porque al poner llaves hace falta un return.

4 · Una función que se pasa a otra para que la llame cuando corresponda.

5 · Sintaxis, referencia, tipo y lógico. El lógico es el peligroso porque no avisa.

6 · Escribir qué se esperaba y qué ocurre, y poner una comprobación a la mitad del recorrido para dividir el terreno en dos.

Microprueba semanal 3 · 5–10 minutos

Individual, sin IA y sin apuntes.

  1. Escribe una función que reciba una base y un tipo de IVA y devuelva el total. Sin imprimir nada.
  2. ¿Qué devuelve (n) => n * 2? ¿Y (n) => { n * 2 }?
  3. Un total sale mal y la consola no muestra ningún error. Describe los dos primeros pasos que darías.