Why compare?
jQuery was created when browsers disagreed about basic things. Today they agree, and many jQuery methods have a direct built-in equivalent. Knowing both lets you:
- maintain jQuery sites confidently,
- read modern tutorials and code that use plain JavaScript,
- decide, project by project, whether jQuery is worth loading.
Each example below gives the same output both ways.
Selecting
<ul id="menu"><li class="item">Home</li><li class="item active">Shop</li><li class="item">Blog</li></ul>
<script src="https://code.jquery.com/jquery-4.0.0.min.js"></script>
<script>
// jQuery
console.log("jQuery:", $(".item").length, $(".item.active").text(), $("#menu").find("li").first().text());
// Plain JavaScript
const items = document.querySelectorAll(".item");
console.log("plain: ", items.length, document.querySelector(".item.active").textContent, document.querySelector("#menu li").textContent);
</script>jQuery: 3 Shop Home
plain: 3 Shop Home| jQuery | Plain JavaScript |
|---|---|
$("#id") |
document.getElementById("id") or querySelector("#id") |
$(".item") |
document.querySelectorAll(".item") |
$(el).find(".x") |
el.querySelectorAll(".x") |
$(el).closest(".card") |
el.closest(".card") |
$(el).parent() / .children() |
el.parentElement / el.children |
$(el).next() / .prev() |
el.nextElementSibling / el.previousElementSibling |
One important difference: when nothing matches, querySelector returns null, and calling a method on null throws an error. jQuery returns an empty object and quietly does nothing.
Changing every element
jQuery acts on every element in a selection. Plain JavaScript does not: you loop yourself.
<p class="note">One</p><p class="note">Two</p>
<script src="https://code.jquery.com/jquery-4.0.0.min.js"></script>
<script>
// jQuery: implicit iteration
$(".note").addClass("seen").attr("title", "Read");
// Plain JavaScript: loop over the NodeList
document.querySelectorAll(".note").forEach(p => {
p.classList.add("checked");
p.setAttribute("data-read", "yes");
});
console.log([...document.querySelectorAll(".note")].map(p => p.className + " / " + p.title + " / " + p.dataset.read).join(" | "));
</script>note seen checked / Read / yes | note seen checked / Read / yes| jQuery | Plain JavaScript (one element) |
|---|---|
.addClass / .removeClass / .toggleClass / .hasClass |
el.classList.add / .remove / .toggle / .contains |
.text() / .text(v) |
el.textContent |
.html() / .html(v) |
el.innerHTML (same XSS warning) |
.attr(n) / .attr(n, v) / .removeAttr(n) |
el.getAttribute(n) / setAttribute / removeAttribute |
.prop("checked") / .val() |
el.checked / el.value |
.data("userId") |
el.dataset.userId (always a string) |
.css("color", "red") |
el.style.color = "red" |
.css("color") (reading) |
getComputedStyle(el).color |
Creating and inserting
<ul id="list"><li>First</li></ul>
<script src="https://code.jquery.com/jquery-4.0.0.min.js"></script>
<script>
// jQuery
$("<li>", { text: "Added by jQuery", "class": "new" }).appendTo("#list");
// Plain JavaScript
const li = document.createElement("li");
li.textContent = "Added by plain JS";
li.className = "new";
document.querySelector("#list").append(li);
// Both remove the same way
document.querySelector("#list li").remove();
console.log($("#list li").map((i, el) => el.textContent).get().join(" | "));
</script>Added by jQuery | Added by plain JSModern elements have append, prepend, before, after, replaceWith and remove built in, with the same meanings as jQuery's.
Events
<ul id="list"><li>Apple</li><li>Banana</li></ul>
<script src="https://code.jquery.com/jquery-4.0.0.min.js"></script>
<script>
// jQuery delegation
$("#list").on("click", "li", function () {
console.log("jQuery:", $(this).text());
});
// Plain JavaScript delegation
document.querySelector("#list").addEventListener("click", event => {
const li = event.target.closest("li");
if (li) console.log("plain: ", li.textContent);
});
</script>jQuery: Banana
plain: BananaaddEventListener has no selector argument, so delegation in plain JavaScript uses event.target.closest(). There are no namespaces either; to remove a handler you keep a reference to the function, or pass an AbortController signal and abort it.
Ajax versus fetch
<script src="https://code.jquery.com/jquery-4.0.0.min.js"></script>
<script>
const url = "https://jsonplaceholder.typicode.com/users/1";
$.getJSON(url).then(user => {
console.log("jQuery:", user.name);
return fetch(url);
}).then(response => {
if (!response.ok) throw new Error("HTTP " + response.status);
return response.json();
}).then(user => {
console.log("fetch: ", user.name);
return fetch("https://jsonplaceholder.typicode.com/users/99999");
}).then(response => {
console.log("fetch 404 still resolved: ok =", response.ok, "| status =", response.status);
});
</script>jQuery: Leanne Graham
fetch: Leanne Graham
fetch 404 still resolved: ok = false | status = 404The biggest difference: fetch() does not reject on HTTP errors. A 404 or 500 still resolves, so you must check response.ok. $.ajax() sends them to .fail(). fetch also has no built-in timeout (use AbortSignal.timeout(ms)), and you send JSON with JSON.stringify and a Content-Type header yourself, just as with $.ajax.
Effects
Plain JavaScript has no .fadeIn() or .slideDown(). The modern replacements are CSS transitions toggled with classList (see the CSS transitions lesson), or the Web Animations API:
element.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 300, fill: "forwards" });Document ready
// jQuery
$(function () { /* ... */ });
// Plain JavaScript: put scripts at the end of body, or use defer, or:
document.addEventListener("DOMContentLoaded", () => { /* ... */ });So which should you use?
Keep jQuery when a site already uses it, especially with jQuery plugins or WordPress themes that depend on it. Rewriting working code costs time and risks new bugs; upgrade it to jQuery 4 instead (next lesson).
Consider plain JavaScript for new, small scripts. You save a download, and the code runs anywhere without dependencies.
Use a framework (React, Vue, Svelte) for large interactive applications, where keeping many parts of the page in step with data is the main problem. Do not mix a framework and jQuery on the same elements.
Whatever you choose, the ideas you learned in this course, selecting, traversing, events, delegation, safe text, Ajax and promises, are exactly the same in every tool.
Common mistakes
- Calling methods on
querySelector'snullwhen nothing matches. - Expecting
querySelectorAllresults to have jQuery methods, or implicit iteration. - Forgetting
response.okwithfetch(). - Rewriting a working jQuery site just because jQuery is old.
Exercise
Translate this jQuery into plain JavaScript, keeping the same behaviour:
$("#toggle").on("click", function () {
$("#panel").toggleClass("open");
$(this).attr("aria-expanded", $("#panel").hasClass("open"));
});Show one possible answer
<button id="toggle" aria-expanded="false">Menu</button>
<div id="panel">Panel</div>
<script>
const toggle = document.querySelector("#toggle");
const panel = document.querySelector("#panel");
toggle.addEventListener("click", () => {
const open = panel.classList.toggle("open");
toggle.setAttribute("aria-expanded", open);
console.log("aria-expanded:", toggle.getAttribute("aria-expanded"));
});
</script>aria-expanded: trueclassList.toggle() returns true when it added the class, which saves the separate hasClass check.
Key points
- document.querySelector and querySelectorAll select elements like $(), but return elements or a NodeList, not a jQuery object.
- Plain JavaScript has no implicit iteration: loop over a NodeList with forEach to change every element.
- classList, textContent, dataset, addEventListener, closest and fetch cover most everyday jQuery methods.
- jQuery remains useful for existing code, quick scripts and its plugins; new projects often do not need it.