Encadenamiento de métodos en JavaScript: ventajas y desventajas
Encadenamiento de métodos en JavaScript: cómo funciona, cuándo mejora la legibilidad y cuándo las cadenas largas complican depuración, async y rendimiento.
El encadenamiento de métodos consiste en llamar varios métodos sobre el mismo objeto en una sola secuencia, y funciona porque cada método devuelve un objeto que todavía tiene métodos que se pueden invocar.
Si alguna vez te has quedado mirando una cadena de seis pasos que devuelve undefined sin decir nada, sin un lugar obvio donde poner un breakpoint, ya conoces la contrapartida. Los puntos son baratos de escribir y caros de desentrañar. En los métodos nativos de arrays y strings, el valor de retorno es el que lleva consigo el siguiente método; en tus propios objetos, cada método termina con return this. El encadenamiento es una herramienta de legibilidad, no una opción por defecto. Se gana su lugar en pipelines cortos y empieza a pasarte factura en los largos. Este artículo cubre el mecanismo, las ventajas reales, los inconvenientes reales (fricción al depurar, trabajo desperdiciado, asincronía confusa), cómo construir correctamente tu propio objeto encadenable y una regla concreta para saber cuándo parar.
Puntos clave
- El encadenamiento de métodos funciona porque cada método devuelve un objeto con más métodos; los métodos nativos devuelven un nuevo valor que lleva consigo el siguiente método, y los objetos personalizados se encadenan terminando cada método con
return this. - El encadenamiento en sí tiene un coste de rendimiento insignificante; el coste real está en las pasadas y asignaciones de memoria adicionales, como cuando
.filter().map()[0]realiza dos pasadas completas por el array mientras que.find()hace una sola y se detiene en la primera coincidencia. - Un método definido como arrow function rompe el encadenamiento porque no tiene un
thispropio, así que nunca apunta a la instancia, ycall,bindyapplyno cambiarán eso. - Una regla práctica: un paso siempre está bien, dos suele estar bien, tres o cuatro deberían hacerte dudar, y cinco o más deberían dividirse en pasos con nombre.
- El encadenamiento optimiza la velocidad de escritura; nombrar los valores intermedios optimiza la lectura y la depuración posteriores, y esos no son el mismo objetivo.
¿Qué es el encadenamiento de métodos y cómo funciona?
El encadenamiento de métodos funciona porque cada método devuelve un objeto que todavía tiene métodos que invocar. Los métodos nativos de arrays y strings devuelven un nuevo valor (un array, un string) que lleva consigo sus propios métodos, así que puedes seguir avanzando:
const topNames = users
.filter(user => user.active)
.map(user => user.name)
.sort();
filter devuelve un array, así que map está disponible; map devuelve un array, así que sort está disponible. En tus propios objetos, reproduces esto devolviendo la instancia desde cada método:
class QueryBuilder {
constructor() { this.parts = []; }
where(clause) { this.parts.push(`WHERE ${clause}`); return this; }
limit(n) { this.parts.push(`LIMIT ${n}`); return this; }
build() { return this.parts.join(" "); }
}
new QueryBuilder().where("active = 1").limit(5).build();
// "WHERE active = 1 LIMIT 5"
Como where y limit devuelven this, el siguiente método se resuelve contra la misma instancia. Este es el mismo principio que impulsa las APIs de tipo builder fluido.
Discover how at OpenReplay.com.
Las ventajas: pipelines legibles y APIs fluidas
El encadenamiento brilla en pipelines cortos donde los pasos forman una transformación clara. Se lee de izquierda a derecha como una secuencia (filtrar, luego mapear, luego ordenar) y evita nombrar variables intermedias desechables que nunca vuelves a referenciar. Para una transformación de dos pasos, una cadena suele ser la expresión más directa de la intención:
const activeNames = users.filter(u => u.active).map(u => u.name);
Las APIs fluidas y de tipo builder se apoyan en el mismo mecanismo para leerse como frases: query.where(...).limit(...).build() o expect(value).to.be.an('array'). Cuando la cadena completa describe una única operación coherente, la sintaxis está haciendo un trabajo real para quien lee.
Los inconvenientes: depuración, trabajo desperdiciado y asincronía confusa
Los costes del encadenamiento aparecen cuando la cadena se alarga, mezcla responsabilidades u oculta cuánto trabajo realiza. Estas son las razones para no recurrir a una cadena por defecto.
Fricción al depurar. Las cadenas más difíciles de depurar son las que producen un valor final incorrecto, porque no hay un lugar natural donde poner un breakpoint o registrar un valor intermedio sin desmontar la cadena. Terminas metiendo un console.log dentro de un callback, mezclando código de depuración con lógica, o partiendo la cadena en pasos de todas formas. En código frontend en producción este es un modo de fallo habitual: puedes ver la salida incorrecta pero no qué paso la produjo. El session replay ayuda aquí: reproducir la interacción que generó el estado erróneo te devuelve las entradas que una cadena colapsada oculta, la misma información que habría quedado expuesta si hubieras dividido la cadena en pasos con nombre.
Trabajo desperdiciado. El encadenamiento te empuja hacia “procesar todo”, incluso cuando eso no es lo que querías decir. .filter().map()[0] hace dos pasadas completas por el array y asigna un array intermedio, para luego descartar todos los elementos menos uno. Cuando solo quieres la primera coincidencia, Array.prototype.find() es la herramienta adecuada. Recorre el array solo hasta que el callback acepta un elemento, lo devuelve y no avanza más:
const name = users.find(u => u.active)?.name;
Opacidad del tipo de retorno. En una cadena larga como data.transform().normalize().validate().save(), tienes que llevar la cuenta de lo que devuelve cada paso sin anotaciones de tipos en tiempo de ejecución. Cuando un paso intermedio devuelve algo inesperado, la cadena completa cambia de forma silenciosamente.
Asincronía confusa. Mezclar flujo de control asíncrono con transformaciones de datos en una sola cadena de .then() difumina la intención. Separar la obtención y el parseo de la transformación se lee con más claridad:
const res = await fetchUsers();
const users = await res.json();
const activeNames = users.filter(u => u.active).map(u => u.name);
Este es un juicio de legibilidad, no de rendimiento. await y .then() hacen el mismo trabajo.
Rendimiento: los puntos son gratis, las pasadas no
El encadenamiento en sí tiene un coste de rendimiento intrínseco insignificante: una llamada a método más un acceso a propiedad por paso es trivial al lado del trabajo de iterar una colección. Lo que realmente te cuesta es hacer más trabajo del necesario. .filter().map()[0] son dos pasadas completas O(n) más un array intermedio; find() es una sola pasada que se detiene pronto. La lección es contar iteraciones y asignaciones, no puntos. Una cadena de cinco métodos que recorre los datos una vez puede ser más rápida que una cadena de dos métodos que los recorre dos veces. Recurre a métodos con cortocircuito como find y some siempre que solo necesites el primer resultado que cumpla la condición.
¿Cómo construyes tu propia API encadenable?
Para hacer un objeto encadenable, devuelve this desde cada método que deba continuar la cadena. El único fallo que lo rompe de forma fiable es escribir un método como arrow function. Una arrow function nunca obtiene un this propio; toma prestado el this que tuviera el código circundante en el momento en que se escribió la arrow function, así que return this devuelve el objeto equivocado, y pasar la función por call, bind o apply no cambiará eso.
const counter = {
count: 0,
// Broken: arrow `this` is the enclosing scope, not `counter`
incArrow: () => { this.count++; return this; },
// Correct: method shorthand binds `this` to the instance
inc() { this.count++; return this; }
};
counter.inc().inc(); // works, count === 2
Tanto las clases como los prototipos admiten el mismo patrón. Si prefieres declarar el estado como campo de clase (parts = []) en lugar de asignarlo dentro del constructor, esa sintaxis es estándar desde ES2022. La versión con prototipo se comporta de forma idéntica:
// Prototype form — identical behavior
function Query() { this.parts = []; }
Query.prototype.where = function (c) { this.parts.push(c); return this; };
Usa una clase para código nuevo; la forma con prototipo merece conocerse para bases de código antiguas y para entender en qué se traduce internamente una clase.
Una regla práctica para la longitud de las cadenas
El encadenamiento optimiza la velocidad de escritura; nombrar los valores intermedios optimiza la lectura y la depuración posteriores, y esos no son el mismo objetivo. Una regla práctica para la longitud:
| Longitud de la cadena | Qué hacer | Por qué |
|---|---|---|
| 1 paso | Encadena sin reservas | No hay nada que desenredar |
| 2 pasos | Normalmente está bien | Sigue siendo una transformación clara |
| 3–4 pasos | Detente; considera nombrar un valor intermedio | La legibilidad y el acceso con breakpoints empiezan a sufrir |
| 5 o más pasos | Divide en pasos con nombre | Los tipos de retorno y las responsabilidades son difíciles de seguir |
Divide una cadena cuando estés depurando activamente, cuando el tipo de retorno de un paso no esté claro o cuando la cadena mezcle flujo de control asíncrono con transformación de datos. Y prefiere un find o some con cortocircuito antes que filtrar y luego indexar siempre que solo quieras un resultado.
Encadena una secuencia cuando los pasos se lean como una única transformación y se mantengan cortos; nombra tus valores intermedios en el momento en que la cadena supere los tres o cuatro pasos o empiece a hacer más trabajo del que pediste. La próxima vez que una cadena cruce esa línea, divídela. Tu yo futuro, al leer el código, pasará menos tiempo descifrando y más tiempo arreglando.
Preguntas frecuentes
¿Es el encadenamiento de métodos más lento que llamar a los métodos por separado?
No. El encadenamiento tiene un coste intrínseco insignificante, porque una llamada a método más un acceso a propiedad por paso es trivial comparado con iterar una colección. El rendimiento depende de cuántas pasadas y asignaciones de memoria haces sobre los datos, no de los puntos. Una cadena que recorre los datos una vez puede ser más rápida que llamadas separadas que los recorren dos veces.
¿Por qué se rompe el encadenamiento cuando un método se escribe como arrow function?
Una arrow function nunca obtiene un 'this' propio. Toma prestado el 'this' del código que la rodea, así que 'return this' devuelve el objeto equivocado y el siguiente método no tiene nada válido contra lo que resolverse. Pasar la función por call, bind o apply tampoco lo arregla, porque esos métodos no pueden darle a una arrow function un nuevo 'this'. Usa la sintaxis abreviada de método o una función normal para que 'this' se vincule a la instancia.
¿Cuándo debería usar find() en lugar de filter().map()[0]?
Usa find() siempre que solo quieras el primer elemento coincidente. Array.prototype.find() recorre el array solo hasta que el callback acepta un elemento, entonces lo devuelve y no avanza más, por lo que hace una única pasada. En cambio, filter().map()[0] hace dos pasadas completas por el array y asigna un array intermedio antes de descartar todo excepto el primer elemento. El método 'some' aplica la misma lógica de cortocircuito cuando solo necesitas un booleano.
¿Encadenar promesas con .then() rinde peor que usar await?
No. Una cadena de '.then()' y 'await' hacen el mismo trabajo subyacente, así que la diferencia está en la legibilidad, no en el rendimiento. Encadenar llamadas a '.then()' tiende a mezclar el flujo de control asíncrono con la transformación de datos en una sola secuencia, lo que difumina la intención. Separar la obtención y el parseo de la transformación con 'await' suele leerse con más claridad, pero ninguno de los dos enfoques es mediblemente más rápido.