Nivel inicial16 min de lectura · Publicado el

Koans y katas de ES6: JavaScript moderno con tests

Katas de ES6: con const la etiqueta queda pegada a su caja, cuyo contenido cambia; con let la etiqueta pasa de una caja a otra. const no congela el objetorubentejera.com constletcarritounidades: 3dentro, sí cambia01contadorla etiqueta se mueve
La variable es la etiqueta y su valor, la caja. Con const la etiqueta no se despega (total = 1 lanza TypeError), pero lo de dentro sí cambia (carrito.unidades = 3). Con let, la etiqueta pasa a otra caja (++contador).

Lees código JavaScript actual y te encuentras (...args) => ({ ...base, [clave]: valor }). Todo eso es ES6, y si no conoces las piezas, la línea parece un jeroglífico.

La forma más rápida de aprenderlas es con katas: ejercicios cortos que repites hasta que te salen solos, como las escalas de un músico. Si no tienes claro qué es una kata o un koan, empieza por ¿Qué es una kata y un koan?.

Índice

Tema Qué aprendes Katas de es6katas
let y const Variables que viven solo dentro de su bloque 7 y 8
Arrow functions Funciones cortas y su this 5 y 6
Parámetros por defecto Valores que se usan si no pasas nada 57
Rest Recoger lo que sobra en un array 18 y 19
Spread Repartir un array, un string o un objeto 20 y 21
Objetos literales Atajos para escribir objetos 9
Propiedades calculadas Claves que salen de una variable 16
Getters Propiedades que se calculan al leerlas 66
Setters Propiedades que ejecutan código al asignarlas 67

Cómo practicar: un test en rojo que pones en verde

Cada kata es un test que falla. Tu trabajo es cambiar el código hasta que pase, sin tocar la línea del assert. Es el mismo ciclo de TDD: rojo, verde y otra vez.

Puedes hacerlas en el navegador, en jskatas.org, o en tu ordenador con Node, que ya trae su propio sistema de tests (node:test). Crea un fichero que termine en .test.mjs y empieza siempre con estas dos líneas:

import { test } from 'node:test';
import assert from 'node:assert/strict';

assert/strict compara de forma estricta: '2' y 2 no son iguales. Eso es bueno, porque te obliga a entender qué tipo de dato tienes. Mira este test en rojo:

test('los caracteres siguen siendo strings', () => {
  const [a, b] = [...'12'];
  assert.equal(b, 2);
});

Lo ejecutas con node --test y Node te dice exactamente qué falla:

✖ los caracteres siguen siendo strings (1.995146ms)
  AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:

  '2' !== 2

b es el texto '2', no el número 2. Ya sabes qué arreglar. Salvo los ejemplos de error, todos los bloques de código de esta guía son tests que pasan: copia las dos líneas de arriba, pega debajo el bloque y ejecútalo.

let y const: variables con ámbito de bloque

Un bloque es todo lo que va entre llaves { }: un if, un for o unas llaves sueltas. Con var, la variable se escapa del bloque. Con let y const, se queda dentro.

Es como una nota pegada en la puerta de una habitación: solo la ve quien está dentro. var es una nota en el tablón del pasillo, que ve toda la casa.

test('var se escapa del bloque; let, no', () => {
  if (true) {
    var conVar = 'fuera';
    let conLet = 'dentro';
  }
  assert.equal(conVar, 'fuera');
  assert.equal(typeof conLet, 'undefined'); // fuera del bloque no existe
});

test('let en un for vive solo dentro del bucle', () => {
  for (let clave in { x: 1 }) {}
  assert.throws(() => clave, ReferenceError);
});

test('antes de declararla, una variable let no se puede usar', () => {
  assert.throws(() => {
    hora; // zona muerta temporal
    let hora = 9;
  }, ReferenceError);
});

El último test muestra la zona muerta temporal: desde que empieza el bloque hasta la línea del let, la variable existe pero no se puede tocar. Con var obtendrías undefined sin ningún aviso.

const no es «constante» del todo

const es como let, pero no deja reasignar la variable. Piensa en una etiqueta pegada a una caja: no puedes mover la etiqueta a otra caja, pero sí cambiar lo que hay dentro.

test('const no deja reasignar', () => {
  const total = 0;
  assert.throws(() => { total = 1; }, TypeError);
});

test('pero el objeto de dentro sí se puede cambiar', () => {
  const carrito = { unidades: 1 };
  carrito.unidades = 3;
  assert.equal(carrito.unidades, 3);
});

test('Object.freeze sí lo bloquea', () => {
  const carrito = Object.freeze({ unidades: 1 });
  assert.throws(() => { carrito.unidades = 3; }, TypeError);
});

La regla práctica: usa const por defecto y let solo cuando necesites reasignar. var ya no hace falta.

Si quieres la explicación completa, con el hoisting y la zona muerta temporal, la tienes en var, let y const en JavaScript.

Arrow functions y this

Una arrow function (función flecha) es una forma corta de escribir una función con =>:

test('formas cortas', () => {
  const saludar = () => 'hola';
  const restarUno = n => n - 1;
  const sumar = (a, b) => a + b;
  const crearPunto = () => ({ x: 0, y: 0 }); // paréntesis para devolver un objeto
  assert.equal(saludar(), 'hola');
  assert.equal(restarUno(25), 24);
  assert.equal(sumar(23, 42), 65);
  assert.deepEqual(crearPunto(), { x: 0, y: 0 });
});
Caso Cómo se escribe
Sin parámetros () => 'hola'
Un parámetro n => n - 1 (paréntesis opcionales)
Varios parámetros (a, b) => a + b
Devolver un objeto () => ({ x: 0 }), con paréntesis: sin ellos, las llaves se leen como el cuerpo de la función
Varias líneas (a) => { const b = a * 2; return b; }, con return explícito

El this de una arrow function

this es «el objeto que está usando la función». En una función normal, depende de cómo la llames. En una arrow function, depende de dónde la escribiste, y ya no cambia. Es como una foto: guarda quién estaba delante cuando se hizo, aunque luego la enseñes en otro sitio.

class Temporizador {
  constructor() { this.segundos = 0; }
  conArrow() { return () => this; }
  conFunction() { return function () { return this; }; }
}

test('una arrow function usa el this de donde se escribió', () => {
  const temporizador = new Temporizador();
  const fn = temporizador.conArrow();
  assert.equal(fn(), temporizador);
  assert.equal(fn.call({ otro: true }), temporizador); // call no lo cambia
});

test('una function normal pierde el this al sacarla del objeto', () => {
  const fn = new Temporizador().conFunction();
  assert.equal(fn(), undefined); // en modo estricto, this es undefined
});

test('las arrow functions no tienen arguments propio', () => {
  function exterior() {
    const interior = () => arguments.length;
    return interior(1, 2, 3);
  }
  assert.equal(exterior('a'), 1); // cuenta los de exterior, no los de interior
});

Por eso las arrow functions son ideales para callbacks dentro de un método (un setTimeout, un map), y una mala idea como métodos de un objeto: ahí querrás el this del objeto, no el de fuera. Los demás casos en que no conviene una flecha están en arrow functions en JavaScript.

Parámetros por defecto

Puedes dar un valor a un parámetro por si no te lo pasan, como el idioma que una web elige por ti si no escoges ninguno.

const pedir = (metodo = 'GET') => metodo;

test('se usa el valor por defecto si no pasas nada o pasas undefined', () => {
  assert.equal(pedir(), 'GET');
  assert.equal(pedir(undefined), 'GET');
});

test('null u otros valores no lo activan', () => {
  assert.equal(pedir('POST'), 'POST');
  assert.equal(pedir(null), null);
});

test('se calcula en cada llamada', () => {
  let contador = 0;
  const siguiente = (id = ++contador) => id;
  assert.equal(siguiente(), 1);
  assert.equal(siguiente(), 2);
  assert.equal(siguiente(99), 99);
  assert.equal(contador, 2); // con un valor dado, no se evalúa
});

test('puede usar parámetros anteriores', () => {
  const rectangulo = (ancho, alto = ancho) => ancho * alto;
  assert.equal(rectangulo(3), 9);
});

Dos ideas que conviene recordar:

  • Solo undefined activa el valor por defecto. null, 0 o '' son valores y se respetan.
  • El valor por defecto se calcula en cada llamada, no al definir la función. Por eso puede ser una llamada a otra función o usar un parámetro anterior.

Rest: recoger lo que sobra

Los tres puntos ... hacen dos cosas opuestas según dónde los pongas. Cuando reciben valores, son rest: meten en un array todo lo que sobra. Es la bolsa donde echas lo que no cabe en las manos.

test('rest recoge los argumentos sobrantes en un array', () => {
  const pedido = (cliente, ...productos) => productos;
  assert.deepEqual(pedido('Ana', 'pan', 'leche'), ['pan', 'leche']);
  assert.deepEqual(pedido('Ana'), []);
});

test('es un array de verdad, a diferencia de arguments', () => {
  const sumar = (...numeros) => numeros.reduce((a, b) => a + b, 0);
  assert.equal(sumar(1, 2, 3), 6);
});

test('con desestructuración separa el primero del resto', () => {
  const [primero, ...resto] = [1, 2, 3, 4];
  assert.equal(primero, 1);
  assert.deepEqual(resto, [2, 3, 4]);
});

test('también con objetos', () => {
  const { password, ...publico } = { nombre: 'Ana', password: '1234' };
  assert.deepEqual(publico, { nombre: 'Ana' });
});

El parámetro rest tiene que ir el último. Si no, Node ni siquiera arranca:

const fn = (...rest, ultimo) => ultimo;
SyntaxError: Rest parameter must be last formal parameter

Rest sustituye al antiguo arguments: es un array normal, con map, reduce y compañía, y además funciona en las arrow functions, que no tienen arguments propio.

Spread: repartir arrays, strings y objetos

Cuando los ... entregan valores, son spread: sacan los elementos uno a uno. Es vaciar la bolsa de la compra sobre la mesa.

test('spread copia y une arrays', () => {
  const inicio = [1, 2];
  const todo = [...inicio, 3, 4];
  assert.deepEqual(todo, [1, 2, 3, 4]);
  assert.deepEqual(inicio, [1, 2]); // el original no cambia
});

test('pasa los elementos de un array como argumentos', () => {
  assert.equal(Math.max(...[23, 0, 42, 41]), 42);
});

test('también sirve para constructores', () => {
  const fecha = new Date(...[2015, 1, 1]);
  assert.equal(fecha.getMonth(), 1); // los meses empiezan en 0: es febrero
});

Ojo con la fecha: new Date(2015, 1, 1) es el 1 de febrero, porque los meses de Date empiezan en 0.

Spread con strings

Un string se reparte carácter a carácter:

test('con un string, cada carácter es un elemento', () => {
  assert.deepEqual([...'hola'], ['h', 'o', 'l', 'a']);
  assert.deepEqual(['a', ...'bcd', 'e'], ['a', 'b', 'c', 'd', 'e']);
});

test('los caracteres siguen siendo strings', () => {
  const [a, b] = [...'12'];
  assert.equal(a, '1');
  assert.notEqual(b, 2); // con la comparación estricta, '2' no es 2
});

Spread con objetos

También copia objetos. Si repites una propiedad, gana la última, lo que viene muy bien para sobrescribir ajustes:

test('copia objetos', () => {
  const base = { idioma: 'es', tema: 'claro' };
  const ajustes = { ...base, tema: 'oscuro' };
  assert.deepEqual(ajustes, { idioma: 'es', tema: 'oscuro' });
});

test('la copia es superficial', () => {
  const original = { lista: [1] };
  const copia = { ...original };
  copia.lista.push(2);
  assert.deepEqual(original.lista, [1, 2]); // comparten el array de dentro
});

La copia es superficial: copia el primer nivel. Si dentro hay otro array u objeto, la copia y el original lo comparten. Para una copia completa existe structuredClone().

Rest: recibe

  • En los parámetros: (...args) =>
  • A la izquierda del =: const [a, ...resto] = lista
  • Junta varios valores en un array u objeto

Spread: entrega

  • En la llamada: fn(...lista)
  • Dentro de [ ] o { }: [...a, ...b]
  • Reparte un array, string u objeto en sus piezas
Rest recoge; spread reparte.

Objetos literales: los atajos de ES6

Un objeto literal es un objeto escrito directamente con llaves: { x: 1 }. ES6 añadió atajos para escribir menos:

test('atajos: propiedad y método', () => {
  const x = 1;
  const y = 2;
  const punto = {
    x,
    y,
    describir() { return `(${this.x}, ${this.y})`; },
  };
  assert.deepEqual({ x: punto.x, y: punto.y }, { x: 1, y: 2 });
  assert.equal(punto.describir(), '(1, 2)');
});
  • { x } es lo mismo que { x: x }, cuando la variable y la propiedad se llaman igual.
  • describir() { … } es lo mismo que describir: function () { … }.
  • Si quieres otro nombre para la clave, lo escribes como siempre: { otraClave: x }.

Propiedades calculadas

Si pones la clave entre corchetes, JavaScript la calcula: puede ser una variable, una suma de textos o el resultado de una función.

test('propiedades calculadas con []', () => {
  const campo = 'email';
  const prefijo = 'proper';
  const formulario = {
    [campo]: '[email protected]',
    [prefijo + 'tyName']: true,
    [`validar_${campo}`]: () => 'ok',
  };
  assert.equal(formulario.email, '[email protected]');
  assert.equal('propertyName' in formulario, true);
  assert.equal(formulario.validar_email(), 'ok');
});

Getters

Un getter es una propiedad que se calcula cada vez que la lees. Se escribe con get delante y sin parámetros, y se usa sin paréntesis. Es como el indicador de batería del móvil: lo miras como un dato, pero detrás hay un cálculo.

test('getter: se lee como una propiedad, pero se calcula', () => {
  const persona = {
    nombre: 'Ada',
    apellido: 'Lovelace',
    get completo() { return `${this.nombre} ${this.apellido}`; },
  };
  assert.equal(persona.completo, 'Ada Lovelace'); // sin paréntesis
});

Un getter debe tener cero parámetros. Si le pones uno, el código ni arranca:

SyntaxError: Getter must not have any formal parameters.

Setters

Un setter es lo contrario: parece que asignas un valor, pero se ejecuta una función. Se escribe con set y lleva exactamente un parámetro: el valor que asignas.

test('setter: se asigna como una propiedad, pero ejecuta código', () => {
  const termostato = {
    celsius: 20,
    set fahrenheit(valor) { this.celsius = Math.round((valor - 32) * 5 / 9); },
  };
  termostato.fahrenheit = 212;
  assert.equal(termostato.celsius, 100);
});

Con cero o con dos parámetros, error:

SyntaxError: Setter must have exactly one formal parameter.

Getters y setters: lo que conviene saber

test('getters y setters con nombre calculado', () => {
  const nombre = 'x';
  const obj = {
    _x: null,
    get [nombre]() { return this._x; },
    set [nombre](valor) { this._x = valor; },
  };
  obj.x = 'axe';
  assert.equal(obj._x, 'axe');
  assert.equal(obj.x, 'axe');
});

test('delete borra la propiedad con su getter o setter', () => {
  let llamado = false;
  const obj = { set x(v) { llamado = true; } };
  delete obj.x;
  obj.x = true;
  assert.equal(llamado, false);
  assert.equal(obj.x, true); // ahora x es una propiedad normal
});

test('si repites una clave, gana la última', () => {
  const obj = {
    x: 1,
    get x() { return 'ax'; },
  };
  assert.equal(obj.x, 'ax');
});

Siguiente paso

  1. Abre jskatas.org y haz las katas de ES6 de esta guía en orden: cada tema tiene su página.
  2. Cuando las tengas, repítelas en tu ordenador con node --test, sin mirar la solución.
  3. Pasa a problemas más largos, como los de Katas para pruebas. Si te falta soltura con map, filter o reduce, antes tienes unos ejercicios de métodos de arrays en JavaScript.

Si prefieres JavaScript clásico (ES5), Carlos Ble tiene sus JavaScript koans en GitHub.

Tema Kata en jskatas.org
let y const let · const
Arrow functions basics · binding
Parámetros por defecto basics
Rest as parameter · with destructuring
Spread with arrays · with strings
Objetos literales basics · computed properties · getter · setter

Fuentes