jQuery 4 (legacy code and migration to DOM APIs) Module 5 – Upgrading to jQuery 4
APIs removed in jQuery 4 and their replacements
Every API jQuery 4 removed, with the native code or jQuery method that replaces it, the removals that need a decision, and three ways to check each fix.
What you will learn
- Replace every API that jQuery 4 removed with native code or a jQuery method that remains
- Choose a replacement for $.isNumeric and $.type that fits what the calling code needs
- Rewrite code that used push, sort and splice on jQuery objects without changing the result
- Check a file of fixes with ESLint, TypeScript and jQuery Migrate, and compare its results with the old file's
Before you start
What you’ll learn · Before you start
What you will learn
- Replace every API that jQuery 4 removed with native code or a jQuery method that remains
- Choose a replacement for $.isNumeric and $.type that fits what the calling code needs
- Rewrite code that used push, sort and splice on jQuery objects without changing the result
- Check a file of fixes with ESLint, TypeScript and jQuery Migrate, and compare its results with the old file's
Before you start
On this page
On the day a shop moved to jQuery 4, its quantity field stopped accepting any number. The fix for the removed
$.isNumeric(v) had been Number.isFinite(v), the replacement the upgrade guide names, and the value of a form field
is always a string, which Number.isFinite never accepts.
jQuery 4 removed 18 APIs from the full build. Most have a replacement of one line; a few need a decision about what the
old code meant. Here you will see each removal next to what replaces it, run the decisions for $.isNumeric and
$.type on recorded values, rewrite code that used push(), sort() and splice(), and check a whole file of fixes
three ways before you trust it.
Eighteen APIs are gone, and most have a one-line replacement
The upgrade guide and the release post
list the removals: utilities that jQuery 3 had deprecated one release after another, three array methods that jQuery
objects carried without documenting them, and a setting that jQuery no longer reads, $.fx.interval. jQuery Migrate’s
warnings page adds a second one, $.event.global. This page asks its own jQuery
which of them it has; it ran in A DOM implementation for Node.js: it builds a page's document from HTML and runs the page's scripts outside a browser, so a recording can load any jQuery file in a fresh window.
with the official 4.0.0, 3.7.1 and slim 4.0.0 files:
pages/presence.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>What this jQuery has</title>
</head>
<body>
<script src="https://code.jquery.com/jquery-4.0.0.min.js" integrity="sha256-OaVG6prZf4v69dPg6PhVattBXkcOWQB62pdZ3ORyrao=" crossorigin="anonymous"></script>
<script src="presence.js"></script>
</body>
</html> pages/presence.js
// presence.js: which of the APIs that jQuery 4 removed this page's jQuery still has.
(function ($) {
var UTILITIES = ['trim', 'isArray', 'isFunction', 'isNumeric', 'isWindow', 'camelCase', 'nodeName', 'type', 'now',
'parseJSON', 'unique', 'cssNumber', 'cssProps'];
var METHODS = ['push', 'sort', 'splice'];
var gone = UTILITIES.filter(function (name) { return !(name in $); }).map(function (name) { return '$.' + name; });
gone = gone.concat(METHODS.filter(function (name) { return !(name in $.fn); }).map(function (name) { return '.' + name + '()'; }));
if (!$.fx || $.fx.interval === undefined) gone.push('$.fx.interval');
if ($.event.global === undefined) gone.push('$.event.global');
['Deferred', 'Callbacks'].forEach(function (name) { if (!(name in $)) gone.push('$.' + name); });
if (!('queue' in $.fn)) gone.push('.queue()');
console.log('jQuery ' + $.fn.jquery + ': ' + gone.length + ' gone');
console.log(' ' + (gone.join(', ') || 'none'));
console.log(' still there: $.uniqueSort ' + ('uniqueSort' in $) + ', $.proxy ' + ('proxy' in $) +
', $.holdReady ' + ('holdReady' in $) + ', .bind() ' + ('bind' in $.fn) + ', .hover() ' + ('hover' in $.fn));
})(jQuery); // Prints what presence.js logged in jsdom (recorded.mjs) on jQuery 4.0.0, 3.7.1 and the slim build of 4.0.0.
import { line, runsOf } from './show.mjs';
for (const lines of Object.values(runsOf('pages/presence.html'))) for (const l of lines) console.log(line(l)); Output
jQuery 4.0.0: 18 gone $.trim, $.isArray, $.isFunction, $.isNumeric, $.isWindow, $.camelCase, $.nodeName, $.type, $.now, $.parseJSON, $.unique, $.cssNumber, $.cssProps, .push(), .sort(), .splice(), $.fx.interval, $.event.global still there: $.uniqueSort true, $.proxy true, $.holdReady true, .bind() true, .hover() true jQuery 3.7.1: 0 gone none still there: $.uniqueSort true, $.proxy true, $.holdReady true, .bind() true, .hover() true jQuery 4.0.0+slim: 21 gone $.trim, $.isArray, $.isFunction, $.isNumeric, $.isWindow, $.camelCase, $.nodeName, $.type, $.now, $.parseJSON, $.unique, $.cssNumber, $.cssProps, .push(), .sort(), .splice(), $.fx.interval, $.event.global, $.Deferred, $.Callbacks, .queue() still there: $.uniqueSort true, $.proxy true, $.holdReady true, .bind() true, .hover() true
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node presence.mjs
jQuery 3.7.1 has all 18, and 4.0.0 none. The slim build of 4.0.0 also lacks $.Deferred, $.Callbacks and .queue(),
besides the Ajax and effects that a slim build always leaves out, and the APIs that are only deprecated are still in
every build. This table pairs each removal with its
replacement, from the guide and from jQuery Migrate’s
warnings page:
| Removed in 4.0 | Replace it with |
|---|---|
$.trim(s) |
s.trim(), or String.prototype.trim.call(s == null ? '' : s) when s may be null |
$.isArray(x), $.parseJSON(s), $.now() |
Array.isArray(x), JSON.parse(s), Date.now() |
$.isFunction(x) |
typeof x === 'function' |
$.nodeName(el, 'div') |
el.nodeName.toLowerCase() === 'div' |
$.unique(list) |
$.uniqueSort(list), the same function under its newer name |
$.isNumeric(x), $.type(x) |
A test chosen for each use: the next two sections |
$.isWindow(x), $.camelCase(s) |
Usually nothing; where needed, an expression or a helper of your own |
$.cssNumber, $.cssProps |
Nothing: give .css() values with their units, under standard property names |
.push(), .sort(), .splice() |
.add(), .toArray() and .not(): the fourth section |
$.fx.interval, $.event.global |
Nothing: jQuery no longer reads them, so delete the code |
Two more changes in the guide are not names you can test for. $.Deferred.getStackHook was renamed
$.Deferred.getErrorHook in 3.7.0, and jQuery 4 calls only the new name. And .toggleClass() no longer takes a
boolean alone, a change in behaviour that the next lesson covers with the others. The one-line replacements, with
their edge cases such as $.trim(null), are worked through in the lesson on jQuery’s utilities.
Checkpoint
Check: What jQuery 4 removed
$.isNumeric needs a decision about what numeric means
The guide replaces $.isNumeric with “Number.isFinite or a custom implementation”. The two are far apart, because
Number.isFinite() never turns a string into a number (ECMAScript), and almost every
value code tests comes from a form or a URL, as a string. This page puts 16 values through jQuery 3.7.1’s
$.isNumeric and four replacements:
pages/isnumeric.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>What counts as numeric</title>
</head>
<body>
<script src="https://code.jquery.com/jquery-3.7.1.min.js" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script>
<script src="isnumeric.js"></script>
</body>
</html> pages/isnumeric.js
// isnumeric.js: 16 values through jQuery 3.7.1's $.isNumeric and four replacements, side by side.
(function ($) {
var VALUES = ['42', '-3.5', ' 12 ', '1e5', '0x1A', '0123', '12px', '', ' ', 'Infinity',
'1234567890123456789012345', 7, NaN, null, true, [3]];
// The test the upgrade guide names, a looser one, the closest match to $.isNumeric, and a test for one kind of value.
var TESTS = {
'Number.isFinite(v)': function (v) { return Number.isFinite(v); },
'Number.isFinite(Number(v))': function (v) { return Number.isFinite(Number(v)); },
'numeric(v)': function (v) {
return (typeof v === 'number' || typeof v === 'string') && String(v).trim() !== '' && Number.isFinite(Number(v));
},
'/^[0-9]+$/': function (v) { return typeof v === 'string' && /^[0-9]+$/.test(v); },
};
var names = Object.keys(TESTS);
var show = function (v) { return typeof v === 'string' ? JSON.stringify(v) : Array.isArray(v) ? '[' + v + ']' : String(v); };
var mark = function (b) { return b ? 'yes' : 'no'; };
var same = names.map(function () { return 0; });
console.log(['value', '$.isNumeric'].concat(names).join(' | '));
VALUES.forEach(function (v) {
var old = $.isNumeric(v);
var row = names.map(function (name, i) {
var r = TESTS[name](v);
if (r === old) same[i] += 1;
return mark(r);
});
console.log([show(v), mark(old)].concat(row).join(' | '));
});
console.log(['same as $.isNumeric', VALUES.length + ' of ' + VALUES.length].concat(same.map(function (n) { return n + ' of ' + VALUES.length; })).join(' | '));
})(jQuery); // Prints the table isnumeric.js logged in jsdom on jQuery 3.7.1 (recorded.mjs).
import { line, runsOf } from './show.mjs';
for (const l of runsOf('pages/isnumeric.html')['3.7.1']) console.log(line(l)); Output
value | $.isNumeric | Number.isFinite(v) | Number.isFinite(Number(v)) | numeric(v) | /^[0-9]+$/ "42" | yes | no | yes | yes | yes "-3.5" | yes | no | yes | yes | no " 12 " | yes | no | yes | yes | no "1e5" | yes | no | yes | yes | no "0x1A" | yes | no | yes | yes | no "0123" | yes | no | yes | yes | yes "12px" | no | no | no | no | no "" | no | no | yes | no | no " " | no | no | yes | no | no "Infinity" | no | no | no | no | no "1234567890123456789012345" | yes | no | yes | yes | yes 7 | yes | yes | yes | yes | no NaN | no | no | no | no | no null | no | no | yes | no | no true | no | no | yes | no | no [3] | no | no | yes | no | no same as $.isNumeric | 16 of 16 | 9 of 16 | 11 of 16 | 16 of 16 | 11 of 16
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node isnumeric.mjs
Read the last line first. Number.isFinite(v) gave the old answer for 9 of the 16 values: it says no to "42".
Number.isFinite(Number(v)) gave 11, because Number("") and Number(" ") are 0, and null, true and [3]
convert too. Only numeric(v), which accepts numbers and strings that are not blank and then converts, matched all 16.
But matching is not the same as fitting: $.isNumeric itself called "0x1A" and "1e5" numeric, and accepted
"0123", whose value as a number is 123. A quantity field may want none of that, and an order code is text that only happens to hold digits.
Migrate’s warnings page suggests two tests for this case, but neither runs as printed: it calls isNan, which does
not exist (the function is isNaN), and calls .test() on the string instead of on the regular expression. Write
your own, and choose it by the field. Answer three questions about one field of your code, and the table marks the
tests that fit:
Answer the questions above: the table then marks what fits your case and says why the rest does not.
| Test | Form text, like "42" | Empty fields | Hex and exponents | Codes, like "0123" |
|---|---|---|---|---|
| Number.isFinite(v) Takes: the value as it is | Poor "42" is no: it never turns a string into a number | Good "" and " " are no | Good "0x1A" and "1e5" are no, as every string is | Poor "0123" is no |
| Number.isFinite(Number(v)) Takes: the value turned into a number | Good "42", " 12 " and "-3.5" are yes | Poor "" and " " are yes, because Number("") is 0 | Mixed "0x1A" and "1e5" are yes, as with $.isNumeric | Mixed "0123" is yes, read as the number 123 |
| numeric(v) Takes: a number, or a string that is not blank | Good the same answer as $.isNumeric on all 16 values | Good "" and " " are no | Mixed "0x1A" and "1e5" are yes, as with $.isNumeric | Mixed "0123" is yes, but as a number its 0 is lost |
| /^[0-9]+$/ Takes: a string of digits only | Mixed "42" is yes; "-3.5" and " 12 " are no | Good "" and " " are no | Good "0x1A" and "1e5" are no | Good "0123" is yes, and stays text |
How the answers decide
- “Does the value come from a form field or a URL, as text?” Yes, it is text: rules out Number.isFinite(v). Number.isFinite never turns a string into a number, so every value of a form would fail, as "42" did in the table.
- “Must every answer stay as $.isNumeric gave it, for now?” Yes, nothing may change yet: rules out Number.isFinite(Number(v)) and /^[0-9]+$/. Only numeric(v) gave the same answer as $.isNumeric on all 16 values; the other two differ on 5 of them.
- “Must every answer stay as $.isNumeric gave it, for now?” Yes, nothing may change yet: adds a caveat to numeric(v). It keeps the old answers, hex and exponents included: decide later what the field should accept.
- “Is the value a quantity or a code?” A code, like an order number: adds a caveat to numeric(v) and Number.isFinite(Number(v)). A code is text: as a number, "0123" becomes 123. Test its digits and keep it as text.
- “Is the value a quantity or a code?” A quantity, like 3 or 2.5: adds a caveat to /^[0-9]+$/. Digits alone refuse "-3.5" and " 12 ": trim the text first, and decide whether decimals and signs are allowed.
In short: numeric(v) while nothing may change, then a test written for each field, digits for codes and a trimmed,
checked number for quantities.
Checkpoint
Check: Replacing $.isNumeric
$.type, $.isWindow and $.camelCase need code of your own
$.type() returned a lower-case name for a value: 'array', 'date', 'regexp' and so on. Code that only checks
for functions or arrays needs typeof or Array.isArray(); code that switches on all the old names needs a
replacement that returns the same strings. This page shows what $.type, typeof and the
type tag: The text Object.prototype.toString.call(value) returns for a value, such as [object Date] or [object Array]. It names the kind of object even when it was made in another frame, where instanceof fails. of Object.prototype.toString say about 15 values, then the replacements of
$.isWindow and $.camelCase next to the functions they replace:
pages/type.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>What $.type said</title>
</head>
<body>
<script src="https://code.jquery.com/jquery-3.7.1.min.js" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script>
<script src="type.js"></script>
</body>
</html> pages/type.js
// type.js: what jQuery 3.7.1's $.type, typeof and Object.prototype.toString say about 15 values, then the replacements
// of $.isWindow and $.camelCase next to the functions they replace.
(function ($) {
var VALUES = [
['null', null], ['undefined', undefined], ['true', true], ['3', 3], ['NaN', NaN], ["'text'", 'text'], ['10n', BigInt(10)],
['function () {}', function () {}], ['async function () {}', async function () {}], ['[1, 2]', [1, 2]], ['{}', {}],
['new Date(0)', new Date(0)], ['/re/', /re/], ["new String('a')", new String('a')], ['new Map()', new Map()],
];
VALUES.forEach(function (pair) {
var v = pair[1];
console.log(pair[0] + ': $.type ' + $.type(v) + ', typeof ' + typeof v + ', toString ' + Object.prototype.toString.call(v));
});
var isWindow = function (obj) { return obj != null && obj === obj.window; };
console.log('isWindow: window ' + $.isWindow(window) + '/' + isWindow(window) + ', document ' + $.isWindow(document) + '/' + isWindow(document) + ', null ' + $.isWindow(null) + '/' + isWindow(null));
var camelCase = function (name) {
return name.replace(/^-ms-/, 'ms-').replace(/-([a-z])/g, function (_, letter) { return letter.toUpperCase(); });
};
['font-size', 'border-top-width', '-webkit-transform', '-ms-transform', 'data-delivery-slot'].forEach(function (name) {
console.log('camelCase ' + name + ': ' + $.camelCase(name) + ' / ' + camelCase(name));
});
})(jQuery); // Prints what type.js logged in jsdom on jQuery 3.7.1 (recorded.mjs).
import { line, runsOf } from './show.mjs';
for (const l of runsOf('pages/type.html')['3.7.1']) console.log(line(l)); Output
null: $.type null, typeof object, toString [object Null]
undefined: $.type undefined, typeof undefined, toString [object Undefined]
true: $.type boolean, typeof boolean, toString [object Boolean]
3: $.type number, typeof number, toString [object Number]
NaN: $.type number, typeof number, toString [object Number]
'text': $.type string, typeof string, toString [object String]
10n: $.type bigint, typeof bigint, toString [object BigInt]
function () {}: $.type function, typeof function, toString [object Function]
async function () {}: $.type object, typeof function, toString [object AsyncFunction]
[1, 2]: $.type array, typeof object, toString [object Array]
{}: $.type object, typeof object, toString [object Object]
new Date(0): $.type date, typeof object, toString [object Date]
/re/: $.type regexp, typeof object, toString [object RegExp]
new String('a'): $.type string, typeof object, toString [object String]
new Map(): $.type object, typeof object, toString [object Map]
isWindow: window true/true, document false/false, null false/false
camelCase font-size: fontSize / fontSize
camelCase border-top-width: borderTopWidth / borderTopWidth
camelCase -webkit-transform: WebkitTransform / WebkitTransform
camelCase -ms-transform: msTransform / msTransform
camelCase data-delivery-slot: dataDeliverySlot / dataDeliverySlot
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node type.mjs
The table shows a gap in the old function: $.type(async function () {}) is 'object' on 3.7.1, because its list of
tags knew only [object Function]. The guide’s replacement, typeof x === 'function', is right for every
kind of function. The tag column is the one to build on when the old strings matter: it names arrays, dates, regular
expressions and errors, and the boxed primitive: An object that wraps a primitive value, such as new String('a') or new Number(4). typeof calls it 'object', while its type tag, [object String], names the value inside. new String('a'), which typeof
calls 'object' and $.type called 'string'.
The last lines show the other two. $.isWindow(x) is best removed; where a window must be told apart, the guide’s
obj != null && obj === obj.window gave the same answers for window, document and null. $.camelCase was never documented, and a helper of your
own needs one detail to match it: -ms-transform becomes msTransform with a small m, while -webkit-transform
becomes WebkitTransform.
Key idea
Replace $.type by what the code asks: typeof for primitives and functions, Array.isArray() for arrays, and the
type tag of Object.prototype.toString only where the old names are part of the code’s logic.
Checkpoint
Check: Replacing $.type, $.isWindow and $.camelCase
add, toArray and not replace push, sort and splice
jQuery objects carried the array methods push, sort and splice for internal use, and jQuery 4 took them away.
Each has a replacement, but not every replacement does the same thing. This cart page tries each one on its rows, on
jQuery 3.7.1 and on 4.0.0:
pages/collections.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Cart</title>
</head>
<body>
<table id="cart">
<tr id="total"><td>Total</td><td class="p">400</td></tr>
<tr class="item" id="tea"><td>Tea</td><td class="p">120</td></tr>
<tr class="item" id="rice"><td>Rice</td><td class="p">80</td></tr>
<tr class="item" id="oil"><td>Oil</td><td class="p">200</td></tr>
</table>
<script src="https://code.jquery.com/jquery-4.0.0.min.js" integrity="sha256-OaVG6prZf4v69dPg6PhVattBXkcOWQB62pdZ3ORyrao=" crossorigin="anonymous"></script>
<script src="collections.js"></script>
</body>
</html> pages/collections.js
// collections.js: the cart's rows through push(), sort() and splice(), where the jQuery has them, and through the
// replacements that work on every release.
(function ($) {
var ids = function ($set) { return $set.map(function () { return this.id; }).get().join(', '); };
var price = function (row) { return Number($(row).find('.p').text()); };
var total = document.getElementById('total');
var tryIt = function (label, call) {
try { console.log(label + ': ' + call()); } catch (error) { console.log(label + ': ' + error.name + ': ' + error.message); }
};
console.log('jQuery ' + $.fn.jquery);
tryIt(' rows.push(total)', function () { var rows = $('#cart tr.item'); rows.push(total); return ids(rows); });
tryIt(' rows.add(total)', function () { var rows = $('#cart tr.item'); var more = rows.add(total); return ids(more) + ' (rows still ' + ids(rows) + ')'; });
tryIt(' [].push.call(rows, total)', function () { var rows = $('#cart tr.item'); [].push.call(rows, total); return ids(rows); });
tryIt(' rows.sort(byPrice)', function () { return ids($('#cart tr.item').sort(function (a, b) { return price(a) - price(b); })); });
tryIt(' $(rows.toArray().sort(byPrice))', function () { return ids($($('#cart tr.item').toArray().sort(function (a, b) { return price(a) - price(b); }))); });
tryIt(' rows.splice(1, 1)', function () { var rows = $('#cart tr.item'); rows.splice(1, 1); return ids(rows); });
tryIt(' rows.not(rows.eq(1))', function () { var rows = $('#cart tr.item'); return ids(rows.not(rows.eq(1))); });
})(jQuery); // Prints what collections.js logged in jsdom on jQuery 3.7.1 and 4.0.0 (recorded.mjs).
import { line, runsOf } from './show.mjs';
for (const lines of Object.values(runsOf('pages/collections.html'))) for (const l of lines) console.log(line(l)); Output
jQuery 3.7.1 rows.push(total): tea, rice, oil, total rows.add(total): total, tea, rice, oil (rows still tea, rice, oil) [].push.call(rows, total): tea, rice, oil, total rows.sort(byPrice): rice, tea, oil $(rows.toArray().sort(byPrice)): rice, tea, oil rows.splice(1, 1): tea, oil rows.not(rows.eq(1)): tea, oil jQuery 4.0.0 rows.push(total): TypeError: rows.push is not a function rows.add(total): total, tea, rice, oil (rows still tea, rice, oil) [].push.call(rows, total): tea, rice, oil, total rows.sort(byPrice): TypeError: $(...).sort is not a function $(rows.toArray().sort(byPrice)): rice, tea, oil rows.splice(1, 1): TypeError: rows.splice is not a function rows.not(rows.eq(1)): tea, oil
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node collections.mjs
On 4.0.0, the three old calls throw a TypeError. Of the replacements, only .add() changes the result:
rows.add(total) returned the total row first, because .add() returns a new set in
document order: The order in which elements appear in the page's HTML, from top to bottom: a parent before its children, an earlier sibling before a later one. Selectors and add() return elements in this order; prevAll() and parents() return the reverse. More in “Traversing with jQuery: find, closest, filter, end”, and rows itself kept its three rows
(.add()). Code that relied on push() putting the new element last, and changing the
set in place, needs [].push.call(rows, total), which the release post offers for exactly this case. Sorting works on
a plain array, $(rows.toArray().sort(byPrice)), and .not(rows.eq(1)) removes one row by its position.
Calling .add() as if it were push()
Wrong: rows.add(total); on a line of its own, which builds a new set and throws it away, so rows never gets the
total row. Right: rows = rows.add(total) when document order is fine, or [].push.call(rows, total) when the new
element must come last.
Checkpoint
Check: Replacing push, sort and splice
Deprecated, not removed, and the autofix that breaks .hover()
Some old APIs are still in jQuery 4 but deprecated, and they are the likely removals of a later major version (deprecated). Each has a replacement that works on 3.x and 4.x alike, so replace them while the file is open for the removals:
| Still in 4.0, deprecated | Replace it with |
|---|---|
.bind(), .unbind(), .delegate(), .undelegate() |
.on() and .off() |
.click(fn) and the other event shorthands |
.on('click', fn), and .trigger('click') for .click() |
.hover(over, out) |
.on('mouseenter', over).on('mouseleave', out) |
$.proxy(fn, obj) |
fn.bind(obj), but .off() no longer finds the handler by the original function |
$.holdReady() |
Code that does not need the whole page to wait |
$.expr[':'] |
$.expr.pseudos |
:first, :eq(1) and the other positional selectors |
.first(), .eq(1) |
Linters can rewrite some of these by themselves, and an autofix: A change a linter makes to the source by itself, when it runs with --fix, for a rule that knows how to rewrite what it reports. It is only as right as the rule, so every rewritten line still needs a reading and a test. is only as good as its
rule. eslint-plugin-no-jquery’s rule for event shorthands rewrites .hover(over, out) as .on("hover", over, out),
but since jQuery 1.9 the name "hover" no longer stands for mouseenter mouseleave
(1.9 upgrade guide), so the new handler waits for an event that never comes.
The help tip below ran on 4.0.0 as written and after eslint --fix, with the pointer moving over its icon and away:
/* global jQuery */
// hover.js: a help tip that opens while the pointer is over its icon.
jQuery(function ($) {
$('#help').hover(function () { $('#tip').addClass('open'); }, function () { $('#tip').removeClass('open'); });
}); // Prints what eslint --fix changed in check/hover.js and what the help tip did on jQuery 4.0.0 before and after it
// (recorded.mjs).
import { line, recording, unchanged } from './show.mjs';
const { files, fix, hover } = recording.tools;
unchanged(files);
unchanged(hover.files);
const wrote = fix.fixed.split('\n').find((l) => l.includes('#help'));
console.log(`eslint --fix wrote:\n ${wrote.trim()}`);
console.log(fix.out ? `and then reported:\n${fix.out}` : 'and then reported nothing');
for (const [label, lines] of Object.entries(hover.runs)) {
console.log(`${label}, on 4.0.0:`);
for (const l of lines) console.log(` ${line(l)}`);
} Output
eslint --fix wrote:
$('#help').on("hover", function () { $('#tip').addClass('open'); }, function () { $('#tip').removeClass('open'); });
and then reported nothing
hover.js as written, on 4.0.0:
pointer over the icon: tip open
pointer away: tip closed
hover.js after --fix, on 4.0.0:
pointer over the icon: tip closed
pointer away: tip closed
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node autofix.mjs
As written, the tip opened. After the fix it never opened, and ESLint had nothing left to report. Read every autofixed line before you commit it, and test the pages it touches.
Checkpoint
Check: Deprecated APIs and autofixes
Check every fix three ways
A file of fixes is done when it gives the same results as the old file and no check finds anything left. This theme’s helpers ran on jQuery 3.7.1 as they were, then on 4.0.0 with the development build: The uncompressed file of a library, such as jquery-migrate-4.0.2.js, made for working on code. jQuery Migrate's development build logs a message for each kind of old call it sees; its compressed build logs only that it is installed. More in “The upgrade path with jQuery Migrate 3 and 4” of jQuery Migrate, before and after the fixes, and both files went through ESLint and TypeScript:
check/utils-old.js
/* global jQuery */
// utils-old.js: a theme's helpers, written on jQuery's utilities.
var shopUtils = (function ($) {
return {
cleanCode: function (s) { return $.trim(s).toUpperCase(); },
isQty: function (v) { return $.isNumeric(v) && v > 0; },
asList: function (v) { return $.isArray(v) ? v : [v]; },
read: function (json) { return $.parseJSON(json); },
kind: function (v) { return $.type(v); },
isFn: function (f) { return $.isFunction(f); },
stamp: function () { return $.now(); },
isWin: function (o) { return $.isWindow(o); },
prop: function (name) { return $.camelCase(name); },
tagIs: function (el, name) { return $.nodeName(el, name); },
uniq: function (els) { return $.unique(els); },
};
})(jQuery); check/utils-new.js
/* global jQuery */
// utils-new.js: the same helpers, with the replacements of this lesson.
var shopUtils = (function ($) {
var TYPES = {
'[object Array]': 'array', '[object Date]': 'date', '[object RegExp]': 'regexp', '[object Error]': 'error',
'[object Boolean]': 'boolean', '[object Number]': 'number', '[object String]': 'string', '[object Symbol]': 'symbol',
};
return {
cleanCode: function (s) { return String(s == null ? '' : s).trim().toUpperCase(); },
isQty: function (v) {
return (typeof v === 'number' || typeof v === 'string') && String(v).trim() !== '' && Number.isFinite(Number(v)) && Number(v) > 0;
},
asList: function (v) { return Array.isArray(v) ? v : [v]; },
read: function (json) { return JSON.parse(json); },
kind: function (v) {
if (v == null) return String(v);
if (typeof v === 'function') return 'function';
if (typeof v !== 'object') return typeof v;
return TYPES[Object.prototype.toString.call(v)] || 'object';
},
isFn: function (f) { return typeof f === 'function'; },
stamp: function () { return Date.now(); },
isWin: function (o) { return o != null && o === o.window; },
prop: function (name) {
return name.replace(/^-ms-/, 'ms-').replace(/-([a-z])/g, function (_, letter) { return letter.toUpperCase(); });
},
tagIs: function (el, name) { return el.nodeName.toLowerCase() === name.toLowerCase(); },
uniq: function (els) { return $.uniqueSort(els); },
};
})(jQuery); check/test.js
// test.js: runs every helper of shopUtils on the same inputs and logs the results, so two runs can be compared line
// by line.
(function (u) {
var log = function (label, value) { console.log(label + ' -> ' + JSON.stringify(value)); };
log('cleanCode(" save10 ")', u.cleanCode(' save10 '));
log('cleanCode(null)', u.cleanCode(null));
['2', ' 3 ', '', 'two', '0x10', 0].forEach(function (v) { log('isQty(' + JSON.stringify(v) + ')', u.isQty(v)); });
log('asList("tea")', u.asList('tea'));
log('read("[1,2]")', u.read('[1,2]'));
[null, [1], new Date(0), /x/, new Error('e'), 'a', 1, true, {}, function () {}].forEach(function (v, i) {
log('kind(value ' + i + ')', u.kind(v));
});
log('isFn(alert)', u.isFn(window.alert));
log('typeof stamp()', typeof u.stamp());
log('isWin(window)', u.isWin(window));
log('isWin(document)', u.isWin(document));
log('prop("-ms-transform")', u.prop('-ms-transform'));
log('prop("font-size")', u.prop('font-size'));
log('tagIs(body, "BODY")', u.tagIs(document.body, 'BODY'));
log('uniq length', u.uniq([document.body, document.head, document.body]).length);
})(shopUtils); check/tsconfig.json
{
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"noEmit": true,
"strict": false,
"target": "es2022",
"lib": ["es2022", "dom"],
"types": ["jquery"]
},
"files": ["utils-old.js"]
} // Prints the three checks of the helpers in check/ (recorded.mjs): whether test.js logged the same results before and
// after the fix, what jQuery Migrate 4.0.2 counted, and what ESLint and TypeScript reported for each file.
import { recording, runsOf, unchanged } from './show.mjs';
const old = runsOf('check/old.html');
const neu = runsOf('check/new.html');
const results = (lines) => lines.filter(([kind, text]) => kind === 'log' && text.includes(' -> ')).map(([, text]) => text);
const before = results(old['utils-old.js on 3.7.1']);
for (const [label, lines] of [...Object.entries(old).slice(1), ...Object.entries(neu)]) {
const now = results(lines);
const differ = before.filter((t, i) => t !== now[i]);
const migrate = lines.find(([, text]) => text.startsWith('Migrate '));
console.log(`${label}: ${now.length - differ.length} of ${before.length} results as on 3.7.1${migrate ? `; ${migrate[1]}` : ''}`);
for (const t of differ) console.log(` differs: ${t}`);
}
const { files, eslint, plugin, typescript, typesJquery, lint, types } = recording.tools;
unchanged(files);
console.log(`\nESLint ${eslint} with eslint-plugin-no-jquery ${plugin}, deprecated-4.0 rules:`);
for (const [file, { out }] of Object.entries(lint)) {
const count = (out.match(/✖ (\d+) problems?/) ?? [null, '0'])[1];
console.log(` ${file}: ${count} problems`);
}
console.log(`TypeScript ${typescript} with @types/jquery ${typesJquery}, checkJs:`);
for (const [file, { out }] of Object.entries(types)) {
const errors = out ? out.split('\n').filter((l) => / error TS\d+:/.test(l)) : [];
console.log(` ${file}: ${errors.length} errors`);
for (const l of errors) console.log(` ${l.replace(/^[^(]+/, '')}`);
} Output
utils-old.js on 4.0.0 + Migrate 4.0.2: 28 of 28 results as on 3.7.1; Migrate 4.0.2: 11 WARN, 0 INFO
utils-new.js on 4.0.0 + Migrate 4.0.2: 28 of 28 results as on 3.7.1; Migrate 4.0.2: 0 WARN, 0 INFO
utils-new.js on 3.7.1: 28 of 28 results as on 3.7.1
ESLint 10.13.0 with eslint-plugin-no-jquery 6.0.1, deprecated-4.0 rules:
utils-old.js: 11 problems
utils-new.js: 0 problems
TypeScript 6.0.3 with @types/jquery 4.0.1, checkJs:
utils-old.js: 10 errors
(5,40): error TS2339: Property 'trim' does not exist on type 'JQueryStatic'.
(6,36): error TS2339: Property 'isNumeric' does not exist on type 'JQueryStatic'.
(7,37): error TS2551: Property 'isArray' does not exist on type 'JQueryStatic'. Did you mean 'inArray'?
(8,38): error TS2339: Property 'parseJSON' does not exist on type 'JQueryStatic'.
(9,35): error TS2339: Property 'type' does not exist on type 'JQueryStatic'.
(10,35): error TS2339: Property 'isFunction' does not exist on type 'JQueryStatic'.
(11,35): error TS2339: Property 'now' does not exist on type 'JQueryStatic'.
(12,36): error TS2339: Property 'isWindow' does not exist on type 'JQueryStatic'.
(13,38): error TS2339: Property 'camelCase' does not exist on type 'JQueryStatic'.
(14,43): error TS2339: Property 'nodeName' does not exist on type 'JQueryStatic'.
utils-new.js: 0 errors
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node check.mjs
Every run gave the same 28 results as the old file on 3.7.1, so the fixes changed nothing a caller can see. On 4.0.0
with Migrate, the old file still worked, because Migrate patched all 11 removed calls back, and left 11 WARN entries;
the new file left none. ESLint found 11 problems in the old file and none in the new one. TypeScript found 10: the
@types/jquery 4.0.1 package still declares $.unique, marked as deprecated, so the type check let that one call
pass. Each tool misses something, which is why a fix counts as done only when all three agree.
Key idea
A fix is finished when the results match the old code’s and ESLint, TypeScript and jQuery Migrate all find nothing: each of the three passed something the others caught.
Checkpoint
Check: Checking fixes three ways
Practise: replace the two that need a decision
Two helpers, one for each removal that has no one-line replacement. The first keeps every answer of $.isNumeric, the
second keeps the strings of $.type and corrects its mistake about async functions; the sample tests hold jQuery
3.7.1’s own answers, for the values of the tables above and a few more edge cases:
Code lab · 2 steps
Replace $.isNumeric and $.type
Write looksNumeric() and typeName(), which give old code the answers jQuery 3.7.1 gave, without jQuery.
- Replace $.isNumeric without changing a single answer To do
- Replace $.type for code that switches on its strings To do
Exercise · Easy · JavaScript
Replace $.isNumeric without changing a single answer
Write looksNumeric(value) in numeric.mjs: the replacement for $.isNumeric(value) in code that must keep every answer jQuery 3.7.1 gave, until someone decides what "numeric" should mean for each field.
It returns true exactly when jQuery 3.7.1 did; this lesson's table recorded 16 of its answers:
- only a number or a string can be numeric;
null,true, arrays and objects never are, even whenNumber()turns them into a number; - a string of spaces only, or an empty one, is not numeric;
- the value is numeric when
Number(value)is a finite number:'42',' 12 ','1e5','0x1A'and7are, while'12px','Infinity'andNaNare not.
Starter code · numeric.mjs
/**
* True exactly when jQuery 3.7.1's $.isNumeric(value) was true.
* @param {unknown} value
* @returns {boolean}
*/
export function looksNumeric(value) {
return Number.isFinite(value);
} The sample tests · numeric.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { looksNumeric } from './numeric.mjs';
// The answers jQuery 3.7.1's $.isNumeric gives: the values of this lesson's table, and a few more edge cases.
test('strings that jQuery 3 called numeric', () => {
for (const v of ['42', '-3.5', ' 12 ', '1e5', '0x1A', '0123', '1234567890123456789012345', '.5', '+1']) {
assert.equal(looksNumeric(v), true, JSON.stringify(v));
}
});
test('strings that it did not', () => {
for (const v of ['12px', 'Infinity', 'abc', '1_000', ' -0x1A', '1e999']) assert.equal(looksNumeric(v), false, JSON.stringify(v));
});
test('empty and blank strings', () => {
assert.equal(looksNumeric(''), false);
assert.equal(looksNumeric(' '), false);
assert.equal(looksNumeric('\n 3 \t'), true, 'spaces around a number are allowed');
});
test('numbers', () => {
assert.equal(looksNumeric(7), true);
assert.equal(looksNumeric(0), true);
assert.equal(looksNumeric(-5), true);
assert.equal(looksNumeric(NaN), false);
assert.equal(looksNumeric(Infinity), false);
});
test('values that are neither numbers nor strings', () => {
assert.equal(looksNumeric(null), false, 'Number(null) is 0, but null is not numeric');
assert.equal(looksNumeric(true), false);
assert.equal(looksNumeric([3]), false);
assert.equal(looksNumeric(undefined), false);
}); A hint
Test the type first, with typeof value === 'number' || typeof value === 'string', then the blank string with String(value).trim() !== '', and last Number.isFinite(Number(value)).
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.
Exercise · Medium · JavaScript
Replace $.type for code that switches on its strings
An old plugin decides what to do with an option by a switch on $.type(option). Write typeName(value) in type.mjs so the switch keeps working on jQuery 4. It returns the strings jQuery 3.7.1's $.type returned; this lesson's table recorded 15 of them:
'null'and'undefined'for those two values;'boolean','number','string'and'symbol'for primitives, and also for the objects that wrap them, such asnew String('a'), and'bigint'for a BigInt;'array','date','regexp'and'error'for arrays, dates, regular expressions and errors;'function'for functions, and'object'for everything else,{}, aMapor a DOM element.
One answer changes on purpose: jQuery 3 called an async function and a generator function 'object'. Return 'function' for them, as typeof does, so a plugin that switches on 'function' accepts them.
Starter code · type.mjs
/**
* The string jQuery 3.7.1's $.type(value) returned, except 'function' for async and generator functions.
* @param {unknown} value
* @returns {string}
*/
export function typeName(value) {
return typeof value;
} The sample tests · type.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { typeName } from './type.mjs';
// The strings jQuery 3.7.1's $.type returns: the values of this lesson's table, and a few more edge cases.
test('null and undefined', () => {
assert.equal(typeName(null), 'null');
assert.equal(typeName(undefined), 'undefined');
});
test('primitives', () => {
assert.equal(typeName(true), 'boolean');
assert.equal(typeName(3), 'number');
assert.equal(typeName(NaN), 'number');
assert.equal(typeName('text'), 'string');
assert.equal(typeName(Symbol('s')), 'symbol');
assert.equal(typeName(BigInt(10)), 'bigint');
});
test('objects that wrap a primitive', () => {
assert.equal(typeName(new String('a')), 'string');
assert.equal(typeName(new Number(4)), 'number');
assert.equal(typeName(new Boolean(false)), 'boolean');
});
test('arrays, dates, regular expressions and errors', () => {
assert.equal(typeName([1, 2]), 'array');
assert.equal(typeName(new Date(0)), 'date');
assert.equal(typeName(/re/), 'regexp');
assert.equal(typeName(new TypeError('x')), 'error');
});
test('functions, async and generator ones too', () => {
assert.equal(typeName(function () {}), 'function');
assert.equal(typeName(class A {}), 'function');
assert.equal(typeName(async function () {}), 'function', 'jQuery 3 said object here');
assert.equal(typeName(function* () {}), 'function', 'jQuery 3 said object here');
});
test('everything else is an object', () => {
assert.equal(typeName({}), 'object');
assert.equal(typeName(new Map()), 'object');
assert.equal(typeName(Object.create(null)), 'object');
assert.equal(typeName(Promise.resolve()), 'object');
}); A hint
Handle value == null first. Then typeof value === 'function' covers every kind of function, and for other objects Object.prototype.toString.call(value) gives a tag such as [object Date] that a small table turns into 'date'.
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.
Last, test yourself on new cases of every replacement here, from $.unique to the three-way check. The weakest idea
comes first, and a wrong choice names the bug it would ship:
Practice that adapts to you
Practise this lesson
12 questions about 6 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): which methods did jQuery 4 remove from jQuery objects, and what replaces them?
push(), sort() and splice(), array methods that jQuery objects carried for internal use without documenting them.
push(el) becomes .add(el), which returns a new set in document order and leaves the old one as it was, or
[].push.call($set, el) when the element must go last in the same set. sort(fn) becomes
$($set.toArray().sort(fn)), sorting a plain array and wrapping it again. splice(i, 1) becomes .not($set.eq(i))
or a .slice(), and anything more complex is done on .toArray() and wrapped with $(). On jQuery 4 the old calls
throw “is not a function”, and jQuery Migrate 4 restores them with a WARN entry until they are fixed.
Two harder questions follow, about a quantity field broken by a fix and an autofix run across a whole codebase. Decide what you would say before you open each first step:
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Mid to senior
After the move to jQuery 4, a cart's quantity field rejects every number. The change shows $.isNumeric(qty) replaced by Number.isFinite(qty), as the upgrade guide suggests. Explain the bug, and fix the test so the field accepts what it should.
-
Mid to staff
A colleague proposes running eslint --fix with eslint-plugin-no-jquery's deprecated-4.0 rules over the whole codebase before the jQuery 4 upgrade, in one commit. What do you say, and how would you run it safely?
Key takeaways
- jQuery 4 removed 18 APIs, most of them utilities with a one-line native replacement, and the slim build also lacks
$.Deferred,$.Callbacksand.queue(). $.isNumerichas no drop-in replacement:Number.isFinite(v)rejects every string, so choose a test for each field after checking it on real values.- Replace
$.typewithtypeof,Array.isArray()or the type tag ofObject.prototype.toString, depending on what the code asks; async functions show whytypeofis the safer test for functions. .add()replacespush()with a new set in document order,$(set.toArray().sort(fn))replacessort(), and.not(set.eq(i))replacessplice().- Replace deprecated APIs such as
.bind(),.hover()and$.proxy()too, but read every autofix: the rewrite of.hover()binds an event that never fires. - Treat a fix as done when results match the old code and ESLint, TypeScript and jQuery Migrate all find nothing.
Terms in this lesson
- autofix
- A change a linter makes to the source by itself, when it runs with --fix, for a rule that knows how to rewrite what it reports. It is only as right as the rule, so every rewritten line still needs a reading and a test.
- boxed primitive
- An object that wraps a primitive value, such as new String('a') or new Number(4). typeof calls it 'object', while its type tag, [object String], names the value inside.
- type tag
- The text Object.prototype.toString.call(value) returns for a value, such as [object Date] or [object Array]. It names the kind of object even when it was made in another frame, where instanceof fails.
References
- jQuery Core 4.0 Upgrade Guide (OpenJS Foundation (jQuery))
- jQuery Core 1.9 Upgrade Guide (OpenJS Foundation (jQuery))
- jQuery 4.0.0 Released (OpenJS Foundation (jQuery blog))
- jQuery Migrate 4.x: warnings and their meaning (OpenJS Foundation (jQuery))
- Deprecated: the deprecated API categories (OpenJS Foundation (jQuery API documentation))
- jQuery.uniqueSort() (OpenJS Foundation (jQuery API documentation))
- .add() (OpenJS Foundation (jQuery API documentation))
- ECMAScript Language Specification: Number.isFinite and Object.prototype.toString (Ecma International (TC39))
- eslint-plugin-no-jquery (Wikimedia Foundation)
- @types/jquery (npm (DefinitelyTyped))
- jsdom (jsdom project)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress