Koans y katas de ES6: JavaScript moderno con tests
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
undefinedactiva el valor por defecto.null,0o''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 quedescribir: 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
- Abre jskatas.org y haz las katas de ES6 de esta guía en orden: cada tema tiene su página.
- Cuando las tengas, repítelas en tu ordenador con
node --test, sin mirar la solución. - Pasa a problemas más largos, como los de Katas para pruebas. Si te falta soltura con
map,filteroreduce, 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
- Comprobado en noviembre de 2026 en MDN Web Docs: let, const, arrow functions, parámetros por defecto, rest, spread, objetos literales, get, set, Date() y Object.freeze().
- Katas originales de Wolfram Kriesing: es6katas.org redirige ahora a jskatas.org, donde siguen disponibles (comprobado el 7 de noviembre de 2026).
- Todos los tests de la guía se han ejecutado con
node --testen Node.js 24.21.0, connode:testynode:assert/strict. Las salidas que se muestran son reales.