Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Intermediate
  • 25 minutes
  • Examples run with Node.js 24.21.0, jQuery 4.0.0 and quickjs 0.32.0
  • By MySmartCoPilot
Free lesson, Pro answers: 25 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

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:

The page and its check

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);
What each build still has JavaScript · presence.mjs
// 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

  1. Question 1 of 2 Besides the 18 removed APIs and the Ajax and effects that every slim build leaves out, which three APIs does the slim build of jQuery 4.0.0 lack?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: $.Deferred, $.Callbacks and .queue()

    jQuery 4's slim build also drops the Callbacks, Deferred and queue modules; the upgrade guide suggests native Promises where Deferreds were used, or the full build.

  2. Question 2 of 2 Old code calls $.unique(rows.toArray()). What replaces it on jQuery 4?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: $.uniqueSort(rows.toArray()), the same function under the name jQuery kept

    $.uniqueSort is the renamed $.unique and is still in jQuery 4, so this one is a rename. TypeScript would not catch the old name, because the jQuery 4 types still declare it.

$.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:

The page and its table

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);
16 values through $.isNumeric and four replacements JavaScript · isnumeric.mjs
// 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:

Interactive · answer and compare Which test replaces $.isNumeric in your field? Answer three questions about one field of your own. The table marks the tests that fit it and says why the others do not.
Does the value come from a form field or a URL, as text?
Must every answer stay as $.isNumeric gave it, for now?
Is the value a quantity or a code?

Answer the questions above: the table then marks what fits your case and says why the rest does not.

Four replacements for $.isNumeric, against four kinds of input from the recorded table
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.

Regex Tester Try a pattern for one of your fields, such as /^[0-9]+$/, on the values your users really type.

Checkpoint

Check: Replacing $.isNumeric

  1. Question 1 of 2 After the upgrade, if (Number.isFinite($('#qty').val())) never passes, whatever number is typed. Why?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: .val() returns a string, and Number.isFinite() is false for every string

    The guide's suggestion fits values that are already numbers. Values from a form or a URL are text, so they need a test that converts them, and that decides what to do with blanks, hex and exponents.

  2. Question 2 of 2 A team replaces $.isNumeric(v) with Number.isFinite(Number(v)). Which inputs now pass that did not before?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: An empty or blank field, and null, true and [3], because Number() turns them into numbers

    An empty field that passes as numeric is how a form saves a quantity of 0 that nobody typed. Add the blank test, as numeric(v) does, and check the result on real values.

$.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:

The page and its script

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);
What $.type, typeof and the type tag say JavaScript · type.mjs
// 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

  1. Question 1 of 2 A plugin runs if ($.type(callback) === 'function') callback(). On 3.7.1, an async callback is silently skipped. What replaces the test on jQuery 4?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: typeof callback === 'function', which is true for every kind of function

    The guide replaces $.isFunction and the function case of $.type with typeof. The upgrade also fixes an old bug: async and generator callbacks now pass.

  2. Question 2 of 2 Which test names a date as a date, even when the date was made in another frame?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Object.prototype.toString.call(value) === '[object Date]'

    For the old names, build on the type tag: [object Date], [object RegExp], [object Error] and so on, as the table's toString column shows.

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:

The cart page and its script

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);
push, sort and splice, and what replaces them JavaScript · collections.mjs
// 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

  1. Question 1 of 2 The total row comes before the item rows in the page. What does rows.add(total) return?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The total row first, then the items, because .add() returns its set in document order

    .add() is the documented replacement, but its order is the page's, not the order of the calls. Use [].push.call(rows, total) when code depends on the new element coming last.

  2. Question 2 of 2 The fix for rows.push(total) was rows.add(total); on a line of its own. How many rows does rows hold afterwards?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Still three, because .add() returned a new set and the line threw it away

    Assign the result, rows = rows.add(total), or use [].push.call(rows, total) when the element must go last in the same set.

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:

hover.js: the help tip JavaScript · check/hover.js
/* 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'); });
});
The help tip before and after eslint --fix JavaScript · autofix.mjs
// 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

  1. Question 1 of 2 eslint --fix rewrote $('#help').hover(over, out) as $('#help').on("hover", over, out). What does the help tip do on jQuery 4?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Never opens, because no event is called hover; since 1.9 the name no longer means mouseenter and mouseleave

    The right rewrite is .on('mouseenter', over).on('mouseleave', out). Read every autofixed line and test the pages it touches: after this fix ESLint reported nothing, and the tip was broken.

  2. Question 2 of 2 Code binds $('#x').on('click', $.proxy(save, form)) and later calls $('#x').off('click', save). After replacing $.proxy with save.bind(form), what happens to the .off() call?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It no longer removes the handler, because .off() matches a bound function only by itself; keep a reference to it

    Store the bound function, const onSave = save.bind(form), and pass the same onSave to .on() and .off(), or use a namespace such as 'click.save'.

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:

The helpers before and after, and the test that calls them

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"]
}
Results, Migrate, ESLint and TypeScript, before and after JavaScript · check.mjs
// 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

  1. Question 1 of 2 ESLint found 11 removed calls in utils-old.js, TypeScript only 10. Which one did TypeScript let pass, and why?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: $.unique, because @types/jquery 4.0.1 still declares it, marked as deprecated

    Each tool has its own blind spot: here the types, elsewhere the variable names of ESLint's rules, and for Migrate the code that never runs. That is why a fix needs all three.

  2. Question 2 of 2 On jQuery 4.0.0 with Migrate, utils-old.js gave the same 28 results as on 3.7.1. Is the file ready for jQuery 4?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: No, its 11 WARN entries mean it works only because Migrate patched the calls back

    Done means both: the same results as the old code, and nothing found by ESLint, TypeScript and Migrate. utils-new.js met both; utils-old.js met only the first.

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.

  1. Replace $.isNumeric without changing a single answer To do
  2. 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 when Number() 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' and 7 are, while '12px', 'Infinity' and NaN are 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)).

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 as new 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, {}, a Map or 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'.

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.

JavaScript & TypeScript Online Compiler Paste one of your own helpers with its replacement and compare the two on your real values.

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.

  1. Question 1 of 12 Old code sets $.fx.interval = 20 to slow animations down. What do you do with that line on jQuery 4?

    Choose one answer.

    Show the answer: Question 1 of 12

    Answer: Delete it, because jQuery 4 no longer reads the setting

    Two settings went with no replacement, $.fx.interval and $.event.global: jQuery 4 does not read them, so the code that touches them can simply go.

  2. Question 2 of 12 A debugging helper sets $.Deferred.getStackHook to log where an error in a Deferred came from. On jQuery 4 it logs nothing. What is the fix?

    Choose one answer.

    Show the answer: Question 2 of 12

    Answer: Assign the function to $.Deferred.getErrorHook, the name since 3.7.0, and return an Error object

    A rename, not a loss: getErrorHook, from 3.7.0. Code that must still run on older 3.x can set both names during the move.

  3. Question 3 of 12 What does the 4.0 upgrade guide say to use instead of $.isNumeric?

    Choose one answer.

    Show the answer: Question 3 of 12

    Answer: Number.isFinite or a custom implementation

    The guide offers a starting point, not a drop-in replacement: Number.isFinite fits real numbers, and form text needs a test of its own.

  4. Question 4 of 12 A field holds order codes such as "0123". Which replacement for its $.isNumeric test fits best?

    Choose one answer.

    Show the answer: Question 4 of 12

    Answer: /^[0-9]+$/ on the text, keeping the code as a string

    A code is text that happens to hold digits: test the characters, keep the string. A quantity is a number: trim, convert and check its range.

  5. Question 5 of 12 Where code truly needs to tell a window apart, which test does the upgrade guide give for the removed $.isWindow?

    Choose one answer.

    Show the answer: Question 5 of 12

    Answer: obj != null && obj === obj.window

    The guide calls window detection unreliable and discouraged; remove the call where you can, and use its expression where you cannot.

  6. Question 6 of 12 A helper replaces $.camelCase with name.replace(/-([a-z])/g, (_, c) => c.toUpperCase()). What does it make of '-ms-transform', and what did $.camelCase make?

    Choose one answer.

    Show the answer: Question 6 of 12

    Answer: 'MsTransform', while $.camelCase made 'msTransform': the old function turned the -ms- prefix into ms first

    An exact replacement copies the old function's quirks: name.replace(/^-ms-/, 'ms-') first, then the general rule, as the lesson's helper does.

  7. Question 7 of 12 Code pushes a summary row onto a jQuery set and relies on it being the last element. What keeps that on jQuery 4?

    Choose one answer.

    Show the answer: Question 7 of 12

    Answer: [].push.call(rows, summary), which adds it at the end of the same set

    Choose by what the code needs: .add() for a set in page order, [].push.call() for the old in-place append.

  8. Question 8 of 12 What replaces rows.sort(byPrice) on jQuery 4?

    Choose one answer.

    Show the answer: Question 8 of 12

    Answer: $(rows.toArray().sort(byPrice))

    Anything array-like that jQuery objects no longer offer is done on .toArray() and wrapped with $() again.

  9. Question 9 of 12 What replaces the deprecated $('#save').click(onSave)?

    Choose one answer.

    Show the answer: Question 9 of 12

    Answer: $('#save').on('click', onSave)

    The event shorthands still work in 4.0 and log INFO entries with Migrate; .on() and .trigger() are the forms that will stay.

  10. Question 10 of 12 Which of these still work on jQuery 4.0.0 without jQuery Migrate? Choose every one.

    Choose every answer that is right.

    Show the answer: Question 10 of 12

    Answer:

    • .hover(over, out)
    • $.proxy(fn, obj)
    • .bind('click', fn)

    Deprecated means present but on its way out; removed means gone. Fix the removed ones before the upgrade, and the deprecated ones while the code is open.

  11. Question 11 of 12 Why does the lesson run test.js on 3.7.1 with the old helpers before judging the new ones?

    Choose one answer.

    Show the answer: Question 11 of 12

    Answer: Its results are the reference that the new helpers must match, line by line

    Characterise first: record what the old code does, then require the new code to do the same. The tools find calls; only the comparison proves behaviour.

  12. Question 12 of 12 Which gaps did this module's runs show? Choose every one.

    Choose every answer that is right.

    Show the answer: Question 12 of 12

    Answer:

    • ESLint's autofix turned .hover() into a handler that never runs
    • jQuery Migrate restored the old results, so the old file looked fine
    • TypeScript let $.unique pass, because the jQuery 4 types still declare it

    Each pass has a blind spot the others cover: that is the reason for running all three, and for comparing results as well.

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.

  1. 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.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. 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?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • jQuery 4 removed 18 APIs, most of them utilities with a one-line native replacement, and the slim build also lacks $.Deferred, $.Callbacks and .queue().
  • $.isNumeric has no drop-in replacement: Number.isFinite(v) rejects every string, so choose a test for each field after checking it on real values.
  • Replace $.type with typeof, Array.isArray() or the type tag of Object.prototype.toString, depending on what the code asks; async functions show why typeof is the safer test for functions.
  • .add() replaces push() with a new set in document order, $(set.toArray().sort(fn)) replaces sort(), and .not(set.eq(i)) replaces splice().
  • 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

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.