Desarrollo web y móvil
jQuery en 2026: leer, mantener y retirar un código legacy
Principiante9 min de lecturaActualizado
Esta página se tradujo del inglés con ayuda de IA y se cotejó con el original. Leer la versión en inglés
jQuery sigue funcionando en buena parte de la web, así que la mayoría de los desarrolladores se lo encuentran en tareas de mantenimiento y no en proyectos nuevos. Esta guía explica el modelo que hay detrás de la función dólar, recorre la selección, la manipulación, los eventos, Ajax y los plugins con ejemplos reales, y después relaciona cada modismo con la API de la plataforma que lo sustituyó y propone un plan por etapas para eliminar la dependencia sin reescribirlo todo.
Lo que aprenderás
- Explicar qué problemas resolvía jQuery y por qué en su mayoría ya no existen
- Leer código jQuery con soltura: el objeto jQuery, el encadenamiento, el recorrido del DOM, los eventos y Ajax
- Reconocer los patrones de plugin y de espacio de nombres que aparecen en código antiguo
- Traducir los modismos habituales de jQuery a las API estándar del DOM equivalentes
- Planificar una retirada incremental que mantenga la aplicación legacy desplegable en todo momento
Durante una década, más o menos, añadir jQuery era la primera línea de casi cualquier proyecto web. La biblioteca no ofrecía una arquitectura nueva ni un modelo de componentes; ofrecía alivio. Los navegadores de aquella época discrepaban en casi todo —el registro de eventos, la opacidad, XMLHttpRequest, si existía o no getElementsByClassName— y jQuery ocultaba esas diferencias tras una única API concisa que funcionaba igual en todas partes. Eso bastó para convertirla en la biblioteca JavaScript más desplegada de la historia.
Hoy la mayoría de los desarrolladores se la encuentran al revés: heredando un código que depende de ella para funcionar. Esta guía está escrita para esa situación. Explica el modelo lo suficiente como para que leas jQuery con confianza, cubre las partes con las que de verdad te vas a topar —selección, manipulación del DOM, eventos, Ajax, plugins— y después relaciona cada modismo con la API estándar que lo reemplazó, con un plan por etapas para sacar la dependencia sin detener el desarrollo de funcionalidades.
Los problemas que resolvía, y qué fue de ellos
Tres cosas hacían imprescindible a jQuery, y la plataforma ha absorbido las tres.
Normalización entre navegadores. Registrar un evento exigía antes elegir entre addEventListener y attachEvent, y el propio objeto del evento tenía nombres de propiedades distintos. jQuery limaba esas diferencias. Los navegadores modernos, incluidos los evergreen que sustituyeron a la era de versiones fijas, implementan los mismos estándares.
Selección concisa. Antes de querySelectorAll, encontrar elementos con un selector CSS significaba escribir tu propio motor o incluir uno. jQuery traía Sizzle, y la función dólar se convirtió en la forma abreviada de decir «encuéntrame estos elementos». El DOM incorporó la misma capacidad de forma nativa en 2009 y hoy está disponible en todas partes.
Una capa de Ajax usable. XMLHttpRequest a pelo era verboso y estaba lleno de callbacks. $.ajax lo envolvía, añadía manejo de JSON y, más adelante, una interfaz parecida a las promesas. fetch con async/await es más corto y está mejor especificado.
Lo que jQuery nunca ofreció es lo que la mayoría de las aplicaciones necesitan hoy: una forma de mantener el DOM sincronizado con el estado de la aplicación. Esa carencia explica por qué la comunidad se pasó a los frameworks de componentes en lugar de buscar un jQuery mejor.
El objeto jQuery, y por qué todo se encadena
Casi todo el código jQuery se apoya en una sola idea. Llamar a $(selector) devuelve un objeto jQuery (el wrapped set): un objeto parecido a un array que contiene cero o más elementos del DOM y que lleva encima todos los métodos de jQuery. Los métodos actúan sobre todo el conjunto a la vez —sin necesidad de bucles— y la mayoría vuelven a devolver el conjunto, que es lo que permite encadenarlos.
$(".row.expired")
.addClass("dimmed")
.attr("aria-hidden", "true")
.find("button")
.prop("disabled", true);
La asimetría clave: los setters devuelven el conjunto; los getters, no. $(".row").text("hi") establece el texto de todas las filas encontradas y devuelve el conjunto; $(".row").text() lee el texto solo del primer elemento y devuelve un string, con lo que la cadena termina. Leer jQuery ajeno consiste en gran parte en detectar el punto en que una cadena se convierte, sin avisar, en un valor.
Un objeto jQuery sin coincidencias no es un error. $(".typo").hide() no hace nada y no avisa de nada, lo cual es cómodo y a la vez explica por qué una errata en un selector puede sobrevivir años. Comprueba .length cuando la coincidencia sea obligatoria.
La función dólar tiene cuatro sobrecargas: un string con un selector, un elemento o conjunto del DOM que envolver, un string de HTML que se analiza para crear nodos nuevos y —la que despista a la gente— una función, que queda registrada para ejecutarse cuando el documento esté listo. $(function () { ... }) equivale a $(document).ready(...).
Seleccionar, recorrer y filtrar
Los selectores son CSS, más un conjunto de extensiones exclusivas de jQuery como :visible, :contains() y :first. Esas extensiones no se pueden delegar al motor del navegador, así que son bastante más lentas y no tienen equivalente estándar; conviene tenerlo en cuenta al planificar una migración.
const $rows = $("#orders tbody tr");
$rows.first(); // first element, still wrapped
$rows.filter(".pending"); // narrow the set
$rows.not(".archived"); // exclude
$rows.eq(2); // by index
$rows.find("td.amount"); // descendants of the set
$row.closest("form"); // nearest matching ancestor
$row.siblings().addBack(); // siblings plus the original
$el.parent().children(".error");
Los métodos de recorrido son la parte de jQuery cuya expresividad el DOM estándar todavía no iguala del todo; closest ya es nativo, pero no hay siblings() ni addBack() integrados. Son helpers de tres líneas cuando los necesitas.
Seleccionar una y otra vez lo mismo es el error de rendimiento clásico. $(".item") vuelve a ejecutar la consulta cada vez, así que guarda el resultado en una variable; la convención casi universal de poner un dólar delante del nombre de esas variables existe precisamente para indicar «esto es un objeto jQuery, no un elemento».
Modificar el documento
$el.addClass("open").removeClass("closed").toggleClass("pinned", isPinned);
$el.hasClass("open");
$el.text("Plain text, escaped");
$el.html("<em>parsed as markup</em>");
$el.val(); // form control value
$el.attr("data-state", "ready"); // HTML attribute
$el.prop("checked", true); // live DOM property
$el.data("orderId"); // reads data-order-id, with a cache
$list.append($row); // insert last inside
$row.before($banner); // insert as previous sibling
$row.remove(); // detach and clean up handlers/data
Hay dos distinciones que importan al depurar. attr lee y escribe el atributo HTML; prop lee y escribe la propiedad viva del DOM. En una casilla de verificación, attr("checked") refleja el marcado tal como se escribió, mientras que prop("checked") refleja lo que el usuario ha hecho de verdad; confundirlos produce formularios que envían valores desfasados. Y .data() no se limita a leer el atributo data-*: se inicializa a partir del atributo una sola vez y luego guarda los valores en una caché interna, de modo que los cambios posteriores del atributo le resultan invisibles.
.html() analiza e inserta marcado, lo que lo convierte en un vector de cross-site scripting en cuanto el string contiene algo que haya aportado un usuario. Usa .text() salvo que controles cada byte.
Eventos y delegación
$("#save").on("click", function (event) {
event.preventDefault();
console.log($(this).data("orderId")); // this is the raw DOM element
});
// Delegated: one handler on a stable ancestor, works for rows added later.
$("#orders").on("click", ".row .delete", function () {
$(this).closest(".row").remove();
});
$("#save").off("click");
$("#form").trigger("submit");
$el.one("transitionend", finish); // fires at most once
Dentro de un handler, this es el elemento en bruto, no un objeto jQuery, así que $(this) es la primera línea de rigor. Eso también significa que las funciones flecha rompen el modismo: un peligro real al modernizar un archivo, porque convertir los handlers en funciones flecha hace que this pase a apuntar, sin avisar, al ámbito que las rodea.
La delegación es la funcionalidad de eventos más valiosa de jQuery y se traslada tal cual al código moderno: el handler vive en un contenedor que siempre existe y el selector se comprueba mientras el evento sube por el árbol. Por eso las filas insertadas dinámicamente siguen funcionando sin volver a enlazar nada.
La biblioteca también normaliza event.which, pageX/pageY y propiedades similares, y admite eventos personalizados con trigger y handlers con espacio de nombres como "click.editor", que permiten a un plugin quitar solo sus propios listeners con .off(".editor").
Ajax
$.ajax({
url: "/api/orders",
method: "POST",
contentType: "application/json",
data: JSON.stringify({ sku: "A-12", qty: 2 }),
dataType: "json",
})
.done((data) => renderOrder(data))
.fail((jqXHR, textStatus) => showError(jqXHR.status, textStatus))
.always(() => hideSpinner());
$.getJSON("/api/health", render); // shorthand
$("#results").load("/partials/results"); // fetch HTML into an element
Hay dos comportamientos que difieren de fetch y que importan al portar código. jQuery trata cualquier respuesta que no sea 2xx como un fallo y llama a fail, mientras que fetch resuelve la promesa y te deja a ti comprobar response.ok. Además, jQuery serializa por defecto un objeto data plano como datos de formulario; para enviar JSON tienes que fijar contentType y hacer tú el stringify. Cuando estés inspeccionando lo que devuelve realmente un endpoint legacy, el formateador de JSON y el codificador/decodificador de URL te ahorran un buen rato entornando los ojos delante de las devtools.
Los objetos $.Deferred son anteriores a las promesas nativas y no son del todo conformes con ellas, aunque el jQuery moderno los hace lo bastante interoperables como para usar await con ellos. En código más antiguo también verás $.when(a, b).then(...), el precursor de Promise.all.
El patrón de plugin y otros modismos de la época
Los plugins extienden $.fn, el prototipo que comparten todos los objetos jQuery. Su forma convencional es esta:
(function ($) {
"use strict";
$.fn.tooltip = function (options) {
const settings = $.extend({ delay: 200, position: "top" }, options);
return this.each(function () { // preserve chaining
const $el = $(this);
if ($el.data("tooltip-bound")) return; // idempotent
$el.data("tooltip-bound", true).on("mouseenter.tooltip", () => show($el, settings));
});
};
})(jQuery);
La expresión de función invocada inmediatamente (IIFE) da al plugin un ámbito privado y garantiza que $ signifique jQuery incluso en una página que ejecuta noConflict(). return this.each(...) es lo que mantiene viva la cadena. $.extend fusiona los valores por defecto con las opciones de quien llama: el precursor del spread de objetos.
Junto a los plugins verás otros modismos de la época: un único objeto global como espacio de nombres para todas las funciones, envoltorios $(document).ready anidados en varios niveles, feature flags leídos de atributos data-* y handlers registrados en línea en el HTML. Nada de esto está mal, pero es anterior a los módulos, así que entender el orden de carga suele ser lo más difícil al leer un código así. La guía de fundamentos de JavaScript cubre los módulos y el manejo del ámbito que sustituyeron a estas convenciones.
Modismo por modismo, en DOM estándar
| jQuery | DOM estándar |
|---|---|
$(".item") | document.querySelectorAll(".item") |
$("#id") | document.getElementById("id") |
$el.find(".x") | el.querySelectorAll(".x") |
$el.closest("form") | el.closest("form") |
$el.addClass("a").removeClass("b") | el.classList.add("a"); el.classList.remove("b") |
$el.attr("x", v) / $el.prop("checked", v) | el.setAttribute("x", v) / el.checked = v |
$el.text(s) / $el.html(s) | el.textContent = s / el.innerHTML = s |
$el.data("orderId") | el.dataset.orderId |
$parent.append($child) | parent.append(child) |
$el.remove() | el.remove() |
$el.on("click", fn) | el.addEventListener("click", fn) |
$parent.on("click", ".row", fn) | listener en el padre más event.target.closest(".row") |
$.ajax(...) / $.getJSON(url) | fetch(url) y luego await response.json() |
$.extend({}, a, b) | spread de objetos |
$.each(arr, fn) / $.map | arr.forEach(fn) / arr.map(fn) |
$(fn) | document.addEventListener("DOMContentLoaded", fn) o un script con defer |
$el.hide() / $el.show() | alternar una clase CSS |
$el.fadeIn() | una transición CSS, o la Web Animations API |
La única fila sin un equivalente directo es la delegación, y es lo bastante corta como para escribirla una vez:
function delegate(root, type, selector, handler) {
root.addEventListener(type, (event) => {
const match = event.target.closest(selector);
if (match && root.contains(match)) handler.call(match, event);
});
}
Quitarlo sin reescribirlo todo
En un código grande, una retirada de golpe rara vez está justificada. Un enfoque por etapas mantiene la aplicación desplegable en cada paso.
- Primero, el inventario. Busca con grep
$(y$.por separado, y haz una lista de los plugins. Suelen ser los plugins los que marcan el calendario: un tooltip hecho a medida es una tarde; un date picker o una tabla de datos usados en cuarenta pantallas son un proyecto en sí mismos. - Elimina el peso muerto. Los bundles antiguos suelen incluir jQuery UI, jQuery Migrate o varios plugins para funciones que ya nadie usa. Quitar código sin uso es el ahorro de bytes más barato que existe y reduce la superficie que tienes que portar.
- Establece una frontera. El trabajo nuevo usa API estándar y módulos; los archivos existentes se quedan como están. Añade una regla de lint que prohíba
$en los directorios nuevos para que la línea no se difumine. - Convierte por archivo, no por modismo. Portar todas las llamadas a
.addClass()en cincuenta archivos produce un diff imposible de revisar. Toma una pantalla, conviértela de principio a fin y publícala. - Sustituye los plugins por funciones de la plataforma. El
dialognativo reemplaza a los plugins de modales,detailsa los acordeones, las transiciones CSS a la cola de animaciones y el Intersection Observer a los plugins basados en la posición del scroll. - Quita la etiqueta script al final, cuando el inventario esté vacío, y confírmalo buscando
jQueryy$en la salida del build. Mide antes y después: el ahorro de bytes y el menor coste de análisis suelen notarse en las métricas que se tratan en rendimiento de aplicaciones.
Si la aplicación es de AngularJS con jQuery mezclado, ordena el trabajo con cuidado: la migración del framework y la retirada de la biblioteca se afectan mutuamente, y las ventajas e inconvenientes se explican en migrar de AngularJS a Angular.
Ideas clave
- jQuery resolvió la normalización entre navegadores, la selección concisa y la ergonomía de Ajax; la plataforma ya hace las tres cosas, así que un proyecto nuevo no gana nada con él, mientras que los existentes siguen dependiendo mucho de él.
- Todo gira en torno al objeto jQuery: los métodos actúan sobre cada coincidencia, los setters devuelven el conjunto para permitir el encadenamiento y los getters solo leen el primer elemento.
- Conoce las parejas que causan bugs:
attrfrente aprop,.text()frente a.html(), y.data(), que lee de una caché en lugar del atributo. - Los handlers delegados en un ancestro estable son el patrón que merece la pena conservar; dentro de un handler,
thises el elemento en bruto, así que las funciones flecha cambian el significado del código. - Casi cada modismo tiene un equivalente estándar de una línea; la delegación es la única laguna, y un helper de diez líneas la cubre.
- Retira la dependencia archivo por archivo tras una frontera de lint, ocúpate primero de los plugins porque son los que marcan el calendario, y quita la etiqueta script solo cuando el inventario esté vacío.
Fuentes y lecturas recomendadas
Preguntas frecuentes
- ¿Está muerto jQuery?
- No está muerto, pero ha dejado de ser una opción por defecto. La biblioteca sigue mantenida y todavía se carga en una cantidad enorme de sitios en producción, así que conocerla es una habilidad práctica de mantenimiento. Lo que ha cambiado es que sus tres argumentos originales —la normalización entre navegadores, una selección concisa y un envoltorio de Ajax usable— los resuelve ya la propia plataforma, de modo que un proyecto nuevo no gana nada añadiéndola.
- ¿Puedo usar jQuery junto a un framework moderno?
- Puedes, y las páginas híbridas existen, pero trátalo como un estado temporal. Los frameworks son dueños del DOM que renderizan, y si jQuery modifica esos mismos nodos a sus espaldas acabas con un estado que el framework desconoce y con re-renderizados que descartan tus cambios sin avisar. Si ambos tienen que convivir, limita jQuery a zonas que el framework nunca toque y elimínalo zona por zona.
- ¿Por qué jQuery devuelve el mismo objeto en casi todos sus métodos?
- La mayoría de los métodos de jQuery devuelven el conjunto de elementos sobre el que se llamaron, y eso es lo que hace posible el encadenamiento. Los setters devuelven el conjunto para que la siguiente llamada siga trabajando sobre los mismos elementos; los getters rompen la cadena y devuelven un valor simple leído solo del primer elemento. Esa asimetría es, con diferencia, la causa más habitual de confusión al leer código jQuery ajeno.
- ¿Cuánto más ligera es una página sin jQuery?
- La biblioteca minificada y comprimida ocupa unos 30 KB, a lo que hay que sumar el tiempo de análisis y ejecución en cada carga de página y los plugins que van encima, que a menudo pesan más que la propia biblioteca. En un sitio de contenidos con un puñado de interacciones, toda la dependencia suele sustituir a unas cincuenta líneas de DOM estándar, así que quitarla casi siempre es una mejora clara tanto en tiempo de carga como en superficie de dependencias.