jQuery 4 (legacy code and migration to DOM APIs) Module 1 – Start here: jQuery in 2026
Loading jQuery: CDN, SRI, npm, slim and noConflict
Load jQuery 4 from a CDN with Subresource Integrity, from npm or your own server, pick the full or slim build, order the scripts and use noConflict.
What you will learn
- Load jQuery 4 from a CDN with Subresource Integrity, from npm or from your own server
- Compute an integrity value for a file and explain why one changed byte stops the script
- Choose between the full and slim builds by checking which parts of jQuery the code calls
- Order the script tags of jQuery, jQuery Migrate, plugins and page code, and explain defer and async
- Run jQuery beside another library that uses $, or beside a second copy of jQuery, with noConflict()
Before you start
What you’ll learn · Before you start
What you will learn
- Load jQuery 4 from a CDN with Subresource Integrity, from npm or from your own server
- Compute an integrity value for a file and explain why one changed byte stops the script
- Choose between the full and slim builds by checking which parts of jQuery the code calls
- Order the script tags of jQuery, jQuery Migrate, plugins and page code, and explain defer and async
- Run jQuery beside another library that uses $, or beside a second copy of jQuery, with noConflict()
Before you start
On this page
jquery-4.0.0.min.js is 78,748 bytes, and a browser runs whatever arrives under that name unless the page says what
to expect. Loading jQuery takes five decisions: where the file comes from, how the browser can check it, which build
to load, where its tag goes among the other scripts, and what happens when something else on the page wants the
name $.
This lesson makes each decision with the official files: it computes their integrity values and sizes, then runs
pages in jsdom, a DOM implementation for Node.js, that load the slim build, put a plugin in the wrong place, share
$ with another library and hold two copies of jQuery.
Each place to load jQuery 4 from fits a different site
A page can take jQuery from a CDN: A content delivery network: servers in many places that keep copies of files such as jquery-4.0.0.min.js and send each visitor the copy from a server near them., from the npm package through a build step, or from its own server. Each comes with its own rules:
| Where from | How the page gets it | It fits when |
|---|---|---|
| A CDN | A script tag that names the file on code.jquery.com, with its integrity value | The site has no build step, and its rules allow another site’s files |
| npm | npm install jquery, then import $ from 'jquery' in code that a bundler builds |
The site already builds its JavaScript |
| Your own server | The same file, copied to your site and loaded from there | A strict Content Security Policy, or no third party in the way |
code.jquery.com publishes every release with its integrity value (releases.jquery.com),
and the copy that jsDelivr serves from the npm package has the same hash, OaVG6p…, by jsDelivr’s own
file list. Google Hosted Libraries, by contrast, stops at
3.7.1 (Hosted Libraries).
The npm route behaves differently from a script tag. The package sends a bundler’s import to jQuery’s ES module
build (npm registry), and that build ends with jQueryFactory( window, true ) and
export default jQuery (jquery-4.0.0.module.js): the true tells
jQuery not to create the globals jQuery and $. Your modules get jQuery from the import, and an old plugin that
looks for window.jQuery finds nothing.
Two things count against a public CDN today. Its old advantage, that visitors already had the file from other sites,
is gone: since version 86, Chrome keys its HTTP cache by the site of the page as well as by the URL
(Chrome for Developers), so each site downloads its own
copy. And rules may forbid it: a site whose Content Security Policy says script-src 'self' runs scripts from its own
origin only (CSP Level 3), and WordPress.org’s plugin rules forbid calling scripts from
remote servers and bundling libraries that WordPress already ships
(common issues).
Checkpoint
Check: CDN, npm or your own server
Subresource Integrity lets the browser check the file
A script tag for a CDN copy carries two more attributes. integrity holds the hash of the file you expect, and
crossorigin="anonymous" asks for the file with CORS: Cross-Origin Resource Sharing: the HTTP headers with which a server lets pages from other sites read its responses. A script from another site with an integrity value is fetched with CORS, which crossorigin="anonymous" asks for. and without cookies, which the check
needs for a file from another site. That pair is Subresource Integrity (SRI): A browser check: a script or style tag carries the hash of the file it expects in its integrity attribute, and the browser refuses to use a downloaded file whose hash is different.. The
specification requires browsers to support SHA-256, SHA-384
and SHA-512, calls SHA-384 “a good baseline”, and says that a browser refuses a file whose hash does not match. Step
through one check:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- The tag. The script tag names jquery-4.0.0.min.js and carries its integrity value, sha256-OaVG6p…, with crossorigin="anonymous".
- A CORS request. Because of crossorigin, the browser asks for the file with CORS and without cookies. The check needs CORS for a file from another site.
- The file arrives. The CDN answers with the 78,748 bytes of the file and a header that lets other sites read it.
- The hash. The browser computes the SHA-256 hash of the bytes it received and compares it with the value in the tag. Here they are equal.
- The script runs. Only now does the browser run the file. A copy with even one changed byte would have another hash.
- When a byte differs. If the hashes differ, the browser refuses the file, as if the request had failed, and fires an error event on the tag. jQuery is then not on the page at all.
The integrity value is nothing secret: it is the hash of the file’s bytes, written in base64 after the algorithm’s name. This program computes it for the official file, compares it with the value releases.jquery.com publishes, then changes one byte, as a tampered or damaged copy would:
// sri.mjs: the integrity value of jquery-4.0.0.min.js, worked out from its bytes, and what one changed byte does to it.
import { createHash } from 'node:crypto';
import { readFileSync } from 'node:fs';
// The value releases.jquery.com publishes for this file.
const PUBLISHED = 'sha256-OaVG6prZf4v69dPg6PhVattBXkcOWQB62pdZ3ORyrao=';
const integrity = (algorithm, bytes) => `${algorithm}-${createHash(algorithm).update(bytes).digest('base64')}`;
const file = readFileSync(new URL('./vendor/jquery-4.0.0.min.js', import.meta.url));
console.log(`jquery-4.0.0.min.js: ${file.length} bytes`);
for (const algorithm of ['sha256', 'sha384', 'sha512']) console.log(integrity(algorithm, file));
console.log(`sha256 value equals the published one: ${integrity('sha256', file) === PUBLISHED}`);
// One changed byte, as in a tampered or damaged copy: the "j" of "jQuery" in the licence comment becomes "J".
const changed = Buffer.from(file);
const at = changed.indexOf('jQuery');
changed[at] = 'J'.charCodeAt(0);
console.log(`byte ${at} changed: ${integrity('sha256', changed)}`);
console.log(`equals the published one: ${integrity('sha256', changed) === PUBLISHED}`); Output
jquery-4.0.0.min.js: 78748 bytes sha256-OaVG6prZf4v69dPg6PhVattBXkcOWQB62pdZ3ORyrao= sha384-fgGyf7Mo7DURSOMnOy7ed+dkq5Job205Gnzu6QIg0BOHKaqt4D76Dt8VlDCzcMHV sha512-8LENNbXmzI/Gbj+OwXmqR6V4QaUAw0/porPzy1+dQoJqC0JPHedWoe0DDOTL2uHA5XXJyIsPtiMHH86pVlay6A== sha256 value equals the published one: true byte 4 changed: sha256-JscA6SnPEzBScjFQkf/gLUOohHN9C3eSgY5g34MV7s8= equals the published one: false
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node sri.mjs
The SHA-256 value equals the published one, so a tag with it runs this exact file and nothing else. Changing a single
byte, byte 4, the j of jQuery in the licence comment, gave a completely different hash, so a browser would refuse
that copy. The SHA-384 and SHA-512 values check the same file with longer hashes; any one of them works in the tag.
A new version with the old integrity value
Wrong: changing jquery-3.7.1.min.js to jquery-4.0.0.min.js in a tag and keeping its integrity value, or leaving
out crossorigin: the browser blocks the script either way, and the page loses jQuery. Right: change the address
and the value together, copied from releases.jquery.com or computed from the file, and keep
crossorigin="anonymous".
Checkpoint
Check: Subresource Integrity
Full or slim: check what the code calls first
jQuery comes in two builds. The slim build: A smaller build of jQuery without its Ajax and effects modules; in jQuery 4 it also leaves out Deferreds, Callbacks and queues. Its version string is 4.0.0+slim. leaves out modules that many pages never use; the 4.0 upgrade guide lists what it lacks from 4.0 onwards (upgrade guide). First, what it saves, measured on the official files:
// sizes.mjs: each build's size in bytes on disk, and compressed with gzip (level 9) and Brotli (quality 11), the two
// ways a server or a CDN usually sends it.
import { readFileSync } from 'node:fs';
import { brotliCompressSync, constants, gzipSync } from 'node:zlib';
const files = ['jquery-4.0.0.min.js', 'jquery-4.0.0.slim.min.js', 'jquery-3.7.1.min.js'];
const bytes = (n) => n.toLocaleString('en-US').padStart(8);
console.log(`${'file'.padEnd(25)} ${'on disk'.padStart(8)} ${'gzip'.padStart(8)} ${'Brotli'.padStart(8)}`);
for (const name of files) {
const file = readFileSync(new URL(`./vendor/${name}`, import.meta.url));
const gzip = gzipSync(file, { level: 9 }).length;
const brotli = brotliCompressSync(file, { params: { [constants.BROTLI_PARAM_QUALITY]: 11 } }).length;
console.log(`${name.padEnd(25)} ${bytes(file.length)} ${bytes(gzip)} ${bytes(brotli)}`);
} Output
file on disk gzip Brotli jquery-4.0.0.min.js 78,748 27,530 24,992 jquery-4.0.0.slim.min.js 56,032 19,461 17,691 jquery-3.7.1.min.js 87,533 30,342 27,445
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node sizes.mjs
Compressed the way a server sends it, the slim build saves 8,069 bytes with gzip and 7,301 with Brotli, about 8 KB once per visitor, since the browser caches the file; the release post puts the slim build at around 19.5k bytes gzipped, and the measurement gives 19,461. 4.0.0 is also 2,812 bytes smaller than 3.7.1 gzipped. Second, what the saving costs. This script looks up twelve parts of jQuery, then calls one that the slim build lacks; it ran on the slim build, on the slim build of 3.7.1 in its place, and on the full build:
builds/slim.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>The slim build of jQuery 4.0.0</title>
</head>
<body>
<p id="cart">Cart</p>
<script src="https://code.jquery.com/jquery-4.0.0.slim.min.js" integrity="sha256-8DGpv13HIm+5iDNWw1XqxgFB4mj+yOKFNb+tHBZOowc=" crossorigin="anonymous"></script>
<script src="parts.js"></script>
</body>
</html> builds/full.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>The full build of jQuery 4.0.0</title>
</head>
<body>
<p id="cart">Cart</p>
<script src="https://code.jquery.com/jquery-4.0.0.min.js" integrity="sha256-OaVG6prZf4v69dPg6PhVattBXkcOWQB62pdZ3ORyrao=" crossorigin="anonymous"></script>
<script src="parts.js"></script>
</body>
</html> builds/parts.js
// parts.js: which parts of jQuery does the loaded build have? Each name is only looked up, so nothing is sent anywhere.
console.log(`jQuery.fn.jquery: ${jQuery.fn.jquery}`);
const parts = {
'$.ajax': $.ajax,
'$.getJSON': $.getJSON,
'.load()': $.fn.load,
'.animate()': $.fn.animate,
'.fadeIn()': $.fn.fadeIn,
'$.Deferred': $.Deferred,
'$.when': $.when,
'$.Callbacks': $.Callbacks,
'.delay()': $.fn.delay,
'.queue()': $.fn.queue,
'.show()': $.fn.show,
'.serialize()': $.fn.serialize,
};
for (const [name, value] of Object.entries(parts)) {
console.log(`${name.padEnd(12)} ${typeof value === 'function' ? 'present' : 'missing'}`);
}
// What code that calls a missing part sees:
$('#cart').fadeIn();
console.log('fadeIn() ran'); // Prints what builds/parts.js logged in jsdom on the slim build (then on 3.7.1's slim build) and on the full build.
import { show } from './show.mjs';
for (const page of ['builds/slim.html', 'builds/full.html']) {
console.log(`${page}:`);
show(page);
} Output
builds/slim.html: jQuery.fn.jquery: 4.0.0+slim $.ajax missing $.getJSON missing .load() missing .animate() missing .fadeIn() missing $.Deferred missing $.when missing $.Callbacks missing .delay() missing .queue() missing .show() present .serialize() present Uncaught TypeError: $(...).fadeIn is not a function (jQuery 3.7.1 logged other lines:) line 1: jQuery.fn.jquery: 3.7.1 -ajax,-ajax/jsonp,-ajax/load,-ajax/script,-ajax/var/location,-ajax/var/nonce,-ajax/var/rquery,-ajax/xhr,-manipulation/_evalUrl,-deprecated/ajax-event-alias,-effects,-effects/animatedSelector,-effects/Tween line 7: $.Deferred present line 8: $.when present line 9: $.Callbacks present line 10: .delay() present line 11: .queue() present builds/full.html: jQuery.fn.jquery: 4.0.0 $.ajax present $.getJSON present .load() present .animate() present .fadeIn() present $.Deferred present $.when present $.Callbacks present .delay() present .queue() present .show() present .serialize() present fadeIn() ran
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node builds.mjs
The slim build of 4.0.0 has no Ajax ($.ajax, $.getJSON, .load()), no effects (.animate(), .fadeIn()), no
Deferreds or Callbacks and no queues, and calling .fadeIn() threw TypeError: $(...).fadeIn is not a function.
.show() and .serialize() stayed. The slim build of 3.7.1 still had $.Deferred, $.when, $.Callbacks,
.delay() and .queue(), so a page that ran fine on 3.x’s slim build can break on 4.0’s. The version string names
the build: 4.0.0+slim, against 4.0.0 for the full build.
Search before you switch
Before you load the slim build, search the page’s code and every plugin it loads for .ajax(, .getJSON(,
.post(, .load(, .animate(, .fade, .slide, Deferred, .when(, Callbacks, .delay( and .queue(, and
for $.get( and jQuery.get(. One match in code you do not control is a reason to keep the full build.
Checkpoint
Check: The full and slim builds
Scripts run in order: jQuery, Migrate, plugins, your code
A classic script, one without defer or async, runs as soon as the browser reaches its tag, before the rest of the
HTML is read (HTML Standard). So the tags of
a page run in their order, and each script sees only what the scripts before it created. jQuery comes first,
jQuery Migrate: A plugin from the jQuery team, loaded right after jQuery, that brings back APIs a newer jQuery removed or changed and, in its development build, logs a warning each time code uses one, so the code can be fixed before the upgrade. More in “jQuery in 2026: versions, support and where it runs” straight after it, then the plugins, then the page’s own code:
The drawing shows the whole scene; its 7 steps are listed below.
Every step as text
- In document order. A classic script without defer or async runs as soon as the browser reaches it, before the HTML after it is read, so the scripts run in the order of the page.
- jQuery first. jQuery runs first and creates the names jQuery and $ on the window.
- Then Migrate. jQuery Migrate, if the page uses it, comes straight after jQuery: it patches the jQuery that exists when it runs.
- Then the plugins. A plugin adds its methods to jQuery.fn, the prototype of the jQuery that exists when it runs.
- Your code last. The page's own code runs last, when jQuery, Migrate and the plugin are all there: it logged badge: 2.
- One script too early. With the plugin above jQuery, it ran first and threw ReferenceError: jQuery is not defined, and the later call failed with $(...).cartBadge is not a function.
- defer and async. defer keeps this order and runs the scripts once the HTML is read; async runs each one as soon as it arrives, in any order, which breaks plugins.
The same three scripts in the wrong order and in the right one:
order/wrong.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Scripts in the wrong order</title>
</head>
<body>
<p id="cart">Cart</p>
<!-- The plugin comes before jQuery. -->
<script src="cart-badge.js"></script>
<script src="https://code.jquery.com/jquery-4.0.0.min.js" integrity="sha256-OaVG6prZf4v69dPg6PhVattBXkcOWQB62pdZ3ORyrao=" crossorigin="anonymous"></script>
<script src="page.js"></script>
</body>
</html> order/right.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Scripts in the right order</title>
</head>
<body>
<p id="cart">Cart</p>
<!-- jQuery first, then the plugin, then the code of the page. -->
<script src="https://code.jquery.com/jquery-4.0.0.min.js" integrity="sha256-OaVG6prZf4v69dPg6PhVattBXkcOWQB62pdZ3ORyrao=" crossorigin="anonymous"></script>
<script src="cart-badge.js"></script>
<script src="page.js"></script>
</body>
</html> order/cart-badge.js
// cart-badge.js: a small plugin. It adds .cartBadge() to the copy of jQuery that is loaded when it runs.
(function ($) {
$.fn.cartBadge = function (count) {
return this.each(function () {
$(this).attr('data-count', String(count));
});
};
})(jQuery); order/page.js
// page.js: the page's own code, which needs jQuery and the plugin.
$('#cart').cartBadge(2);
console.log(`badge: ${$('#cart').attr('data-count')}`); // Prints what the two pages of order/ logged in jsdom: the plugin before jQuery, then jQuery first.
import { show } from './show.mjs';
for (const page of ['order/wrong.html', 'order/right.html']) {
console.log(`${page}:`);
show(page);
} Output
order/wrong.html: Uncaught ReferenceError: jQuery is not defined Uncaught TypeError: $(...).cartBadge is not a function order/right.html: badge: 2
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node order.mjs
On the wrong page the plugin ran first and threw ReferenceError: jQuery is not defined; it never attached
.cartBadge(), so the page’s code failed too. Read such errors from the first one: the second is only its
consequence. On the right page the code logged badge: 2.
defer and async change when a script runs. The HTML Standard runs a defer script once the page has been read,
and defer scripts keep the order of their tags; an async script runs as soon as it has arrived, whenever that is.
So put defer on jQuery, its plugins and your code alike, or on none of them, and never async: a plugin that
arrives before jQuery fails exactly as on the wrong page.
Checkpoint
Check: The order of the scripts
noConflict() makes room for another library, but two copies remain a bug
When jQuery loads, it remembers what window.jQuery and window.$ held and then takes both names.
jQuery.noConflict() gives $ back to its old value, and jQuery.noConflict(true) gives back jQuery as well;
both return jQuery, so you can keep it under any name (jQuery.noConflict()).
WordPress uses this on every site: the copy of jQuery it builds ends with jQuery.noConflict();
(Gruntfile.js), so on a WordPress page $ is
not jQuery. Here a page loads an older library that owns $, then jQuery:
share/other.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>jQuery beside another library that uses $</title>
</head>
<body>
<p id="cart">Cart</p>
<!-- other-lib.js defines $ first; jQuery takes the name over until noConflict() gives it back. -->
<script src="other-lib.js"></script>
<script src="https://code.jquery.com/jquery-4.0.0.min.js" integrity="sha256-OaVG6prZf4v69dPg6PhVattBXkcOWQB62pdZ3ORyrao=" crossorigin="anonymous"></script>
<script src="other.js"></script>
</body>
</html> share/other-lib.js
// other-lib.js: an older library that also names its main function $: $('cart') finds the element whose id is cart.
window.$ = function (id) {
return document.getElementById(id);
}; share/other.js
// other.js: give $ back to the other library, then use jQuery by its own name, or as $ inside a function.
jQuery.noConflict();
console.log(`$('cart') is the other library's again: ${$('cart').id === 'cart'}`);
(function ($) {
// Code that runs at once: the wrapper's parameter $ is jQuery.
console.log(`inside the wrapper, $ is jQuery ${$.fn.jquery}`);
})(jQuery);
jQuery(function ($) {
// Code that waits for the page: jQuery passes itself to the function as $.
$('#cart').text('Cart: 3 items');
console.log(`ready: ${$('#cart').text()}`);
}); // Prints what share/other.js logged in jsdom, beside a library that also uses $.
import { show } from './show.mjs';
show('share/other.html'); Output
$('cart') is the other library's again: true
inside the wrapper, $ is jQuery 4.0.0
ready: Cart: 3 items
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node other.mjs
After noConflict(), $('cart') was the other library’s lookup by id again. jQuery kept working through two
wrappers that give code a $ of its own, as the jQuery Learning Center
recommends: (function ($) { … })(jQuery); runs at once, and jQuery(function ($) { … }) waits for the page,
because jQuery passes itself to a ready function. Neither touches the global $.
A second copy of jQuery is a different problem. When a theme loads jQuery 3.7.1 and a plugin for it, and another plugin brings jQuery 4.0.0, the second copy takes both names. Step through what each script does to them:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- The first copy. jQuery 3.7.1 loads and sets both names, window.jQuery and window.$, to itself.
- A plugin attaches. cart-badge.js adds .cartBadge() to jQuery.fn of the copy that window.jQuery names at that moment: 3.7.1.
- The second copy. jQuery 4.0.0 loads, remembers what the two names held, and sets both to itself. The 3.7.1 copy and its plugin are still in memory, but no name reaches them.
- The plugin is gone. two.js finds jQuery 4.0.0 under both names, and typeof jQuery.fn.cartBadge is undefined: a call to .cartBadge() now fails.
- noConflict(true). jQuery.noConflict(true) hands both names back to the values 4.0.0 remembered, the 3.7.1 copy, and returns 4.0.0, which the script keeps as jq4.
- Two separate copies. jQuery('#cart').cartBadge(3) works again. Each copy keeps its own data: .data('items') is 3 through 3.7.1 and undefined through jq4.
share/two-copies.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Two copies of jQuery on one page</title>
</head>
<body>
<p id="cart">Cart</p>
<!-- A theme loads jQuery 3.7.1 and a plugin for it; another plugin brings its own jQuery 4.0.0. -->
<script src="https://code.jquery.com/jquery-3.7.1.min.js" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script>
<script src="cart-badge.js"></script>
<script src="https://code.jquery.com/jquery-4.0.0.min.js" integrity="sha256-OaVG6prZf4v69dPg6PhVattBXkcOWQB62pdZ3ORyrao=" crossorigin="anonymous"></script>
<script src="two.js"></script>
</body>
</html> share/two.js
// two.js: two copies of jQuery on one page. window.jQuery and window.$ name the one that was loaded last.
console.log(`jQuery is ${jQuery.fn.jquery}, and $ is ${$.fn.jquery}`);
console.log(`cartBadge on this copy: ${typeof jQuery.fn.cartBadge}`);
// noConflict(true) hands both names back to the copy loaded before, and returns this one.
const jq4 = jQuery.noConflict(true);
console.log(`after noConflict(true): jQuery is ${jQuery.fn.jquery}, jq4 is ${jq4.fn.jquery}`);
jQuery('#cart').cartBadge(3);
console.log(`badge: ${jQuery('#cart').attr('data-count')}`);
// Each copy keeps its own data and event handlers.
jQuery('#cart').data('items', 3);
console.log(`data('items') through 3.7.1: ${jQuery('#cart').data('items')}, through 4.0.0: ${jq4('#cart').data('items')}`); // Prints what share/two.js logged in jsdom, on a page with two copies of jQuery.
import { show } from './show.mjs';
show('share/two-copies.html'); Output
jQuery is 4.0.0, and $ is 4.0.0
cartBadge on this copy: undefined
after noConflict(true): jQuery is 3.7.1, jq4 is 4.0.0
badge: 3
data('items') through 3.7.1: 3, through 4.0.0: undefined
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node two-copies.mjs
The 4.0.0 copy had no .cartBadge(), because the plugin had attached itself to 3.7.1, and the two copies kept
separate data: .data('items') was 3 through one and undefined through the other. noConflict(true) gave both
names back to 3.7.1 and made the plugin reachable again, but the page still downloaded and ran jQuery twice. The real
fix is one copy: the platform’s on a platform, your own everywhere else.
Key idea
Load exactly one jQuery, before everything that uses it, and give your own code a local $ with a wrapper instead of
taking the global name.
Checkpoint
Check: noConflict() and second copies
Practise: code that works when $ is not jQuery
A shop page loads another library that owns $, then jQuery, then calls noConflict(), as WordPress does. Fix the
function that shows the number of items in the cart, so that it waits for the page, uses jQuery without the global
$ and shows 99+ for a count above 99. The sample tests run in your browser:
Exercise · Easy · JavaScript
Show the cart count on a page where $ is not jQuery
A shop page loads an older library that owns the name $, then jQuery, then calls jQuery.noConflict(), as WordPress does with its own copy. Fix showCount(win, count) in count.mjs so that it works on that page:
- When the page is ready, it writes
countinto the element whose id iscart-count, as text, with jQuery's.text(). A count above 99 is shown as99+, so the badge keeps its size. - It must not use the global
$, which belongs to the other library, and it must not change it either: nowindow.$ = …and no globalvar $. winis the page'swindow; reach jQuery throughwin.jQuery.
The sample tests run without a browser. They give showCount a stand-in for the window whose jQuery behaves like jQuery for this task: called with a function, it runs it once the page is ready and passes itself as the first argument; called with '#cart-count', it returns a collection whose .text(value) records the value. Meanwhile the global $ is a stand-in for the other library, which returns null for anything it does not know.
Starter code · count.mjs
/**
* Writes the number of items in the cart into #cart-count when the page is ready, on a page whose $ belongs to
* another library (jQuery.noConflict() was called). A count above 99 is shown as 99+.
* @param {object} win the page's window
* @param {number} count
*/
export function showCount(win, count) {
win.jQuery(function () {
$('#cart-count').text(count);
});
} The sample tests · count.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { showCount } from './count.mjs';
// A stand-in for the page: the other library owns the global $, and win.jQuery behaves like jQuery for this task.
// jQuery(fn) runs fn when the page is ready and passes jQuery to it; jQuery('#cart-count') returns a collection whose
// text(value) records the text it would show (jQuery turns a number into text itself). html() is there too, and
// records that it was used.
function shopPage() {
const writes = [];
const collection = {
length: 1,
text(value) {
writes.push(['text', String(value)]);
return collection;
},
html(value) {
writes.push(['html', String(value)]);
return collection;
},
};
const ready = [];
function jQuery(arg) {
if (typeof arg === 'function') {
ready.push(arg);
return jQuery;
}
return arg === '#cart-count' ? collection : { length: 0, text: () => collection, html: () => collection };
}
const otherLibrary = function $(id) {
return null;
};
globalThis.$ = otherLibrary;
const win = { jQuery, $: otherLibrary };
const makeReady = () => ready.splice(0).forEach((fn) => fn(jQuery));
return { win, writes, otherLibrary, makeReady };
}
test('writes the count as text once the page is ready', () => {
const page = shopPage();
showCount(page.win, 3);
assert.deepEqual(page.writes, [], 'nothing is written before the page is ready');
page.makeReady();
assert.deepEqual(page.writes, [['text', '3']]);
});
test('writes 0 as well', () => {
const page = shopPage();
showCount(page.win, 0);
page.makeReady();
assert.deepEqual(page.writes, [['text', '0']]);
});
test('shows 99+ above 99', () => {
for (const [count, shown] of [[99, '99'], [100, '99+'], [250, '99+']]) {
const page = shopPage();
showCount(page.win, count);
page.makeReady();
assert.deepEqual(page.writes, [['text', shown]], `a count of ${count}`);
}
});
test("leaves the other library's $ alone", () => {
const page = shopPage();
showCount(page.win, 5);
page.makeReady();
assert.equal(globalThis.$, page.otherLibrary, 'the global $ is still the other library');
assert.equal(page.win.$, page.otherLibrary, 'window.$ is still the other library');
}); A hint
jQuery passes itself to a ready function: win.jQuery(function ($) { … }) gives the code inside a $ of its own, which is jQuery, while the global $ stays with the other library. Then $('#cart-count').text(count).
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (QuickJS): nothing is sent to mysmartcopilot.com. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Finish with a round of questions: those on the ideas you missed come first, and each choice is explained when you pick it:
Practice that adapts to you
Practise this lesson
10 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up (fresher to 2–5 years): what do the integrity and crossorigin attributes do on a script tag that loads jQuery from a CDN?
integrity holds the hash of the file the page expects, such as
sha256-OaVG6prZf4v69dPg6PhVattBXkcOWQB62pdZ3ORyrao= for jquery-4.0.0.min.js. The browser computes the same hash of
the file it downloads and refuses to run it if the two differ, so a CDN that serves a changed file cannot run code on
your page. crossorigin="anonymous" asks for the file with CORS and without cookies, which the check needs for a file
from another site. Browsers must support SHA-256, SHA-384 and SHA-512, and the value must change whenever the file
does, so it pins one exact release.
Two harder questions follow: a page that broke on the slim build, and “$ is not a function” on a WordPress theme. Sketch your answer to each before you read its first step:
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Mid to senior
To save bytes, a team switched a page to the slim build of jQuery 4. Now it throws "$.ajax is not a function". How do you decide which build the page should use, and how much does the slim build actually save?
-
Fresher to senior
On a WordPress site, a theme script that starts with $(document).ready(...) throws "$ is not a function". Explain the cause, give two correct fixes, and name one "fix" you would reject.
Key takeaways
- jQuery 4 comes from a CDN with an integrity value, from npm through a bundler, whose ES module build creates no globals, or from your own server, which a strict CSP and platform rules may require.
- An integrity value is the base64 hash of one exact file, and with
crossorigin="anonymous"the browser refuses any copy whose hash differs, even by one byte. - The slim build of jQuery 4 saves about 8 KB gzipped but has no Ajax, effects, Deferreds, Callbacks or queues, so search the code before you switch.
- Classic scripts run in the order of their tags: jQuery, then Migrate, then plugins, then your code;
deferkeeps that order andasyncdoes not. noConflict()gives$back, wrappers give your code a local$, and a second copy of jQuery takes over both names and leaves the first copy’s plugins and data behind.
Terms in this lesson
- CDN
- A content delivery network: servers in many places that keep copies of files such as jquery-4.0.0.min.js and send each visitor the copy from a server near them.
- CORS
- Cross-Origin Resource Sharing: the HTTP headers with which a server lets pages from other sites read its responses. A script from another site with an integrity value is fetched with CORS, which crossorigin="anonymous" asks for.
- slim build
- A smaller build of jQuery without its Ajax and effects modules; in jQuery 4 it also leaves out Deferreds, Callbacks and queues. Its version string is 4.0.0+slim.
- Subresource Integrity (SRI)
- A browser check: a script or style tag carries the hash of the file it expects in its integrity attribute, and the browser refuses to use a downloaded file whose hash is different.
References
- Download jQuery (OpenJS Foundation (jQuery))
- jQuery CDN: jQuery Core releases and their integrity values (OpenJS Foundation (jQuery))
- jsDelivr data API: the files of jquery 4.0.0 and their hashes (jsDelivr)
- npm registry: the jquery package (its exports) (npm)
- jQuery 4.0.0, ES module build (jquery-4.0.0.module.js) (OpenJS Foundation (jQuery))
- Gaining security and privacy by partitioning the cache (Chrome for Developers)
- Content Security Policy Level 3 (W3C)
- WordPress Plugin Handbook: Common issues (WordPress)
- Hosted Libraries (Google for Developers)
- Subresource Integrity (W3C)
- jQuery 4.0.0 Released (OpenJS Foundation (jQuery blog))
- jQuery Core 4.0 Upgrade Guide: the slim build (OpenJS Foundation (jQuery))
- HTML Standard: the script element (async and defer) (WHATWG)
- jQuery.noConflict() (OpenJS Foundation (jQuery API documentation))
- Avoiding conflicts with other libraries (OpenJS Foundation (jQuery Learning Center))
- WordPress 7.1: Gruntfile.js (the copy of jQuery it builds) (WordPress)
- jsdom (jsdom project)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress