Build a jQuery-style library
Build a safe chainable $ library with an IIFE, prototypes, plugins, delegated events, and modern DOM alternatives you can compare on a real page.
- 01Keep code privateUse an IIFE and strict mode so the library's helpers stay private and only
$becomes a global. - 02Build a small chainable libraryWrite a
$function that needs nonew, share methods through one prototype, and make setters returnthis. - 03Add to libraries and read themAdd plugins with
$.fn, keep text safe, and read a real library's code step by step.
What this library is
jQuery is a JavaScript library from 2006. A library is a file of ready-made code that you add to your page. With jQuery you wrote $(".item") to find elements. Then you called a method such as addClass, and it changed all of them at once.
Back then, browsers did not agree with each other. The same job needed different code in different browsers:
- Finding elements with a CSS selector worked differently.
- Listening for clicks and key presses worked differently.
- Asking a server for data (called Ajax) needed special code for each browser.
- Common page changes took many lines of code.
jQuery hid those differences behind one short set of methods. That is why so many sites used it.
Today browsers agree much more, and they have these tools built in. But building a small jQuery-style library is still a great way to practise ideas from earlier lessons:
- closures and closure patterns,
- the prototype chain and function prototypes,
- DOM navigation and delegation.
A jQuery-style mini library is a function, usually named $. You give it a CSS selector, and it returns a wrapper: an object that holds the matching elements. The wrapper has methods that change those elements, read values from them, and listen for events. A method that changes things gives back the same wrapper, so you can call one method right after another.
Picture a waiter in a café. She carries several cups on one tray. She takes the tray to the coffee machine, and the machine fills every cup. Then she walks to the milk jug with the same tray in her hands. The café doesn't need one coffee machine per tray. One machine serves every tray.
- In real life: The tray holds several cups
- In JavaScript: The wrapper holds several elements
- In real life: One coffee machine fills the cups on any tray
- In JavaScript: One shared method works on any wrapper
- In real life: After filling, you still hold the same tray
- In JavaScript: A method that changes things returns
this, the same wrapper - In real life: The café can add a new machine
- In JavaScript: A plugin adds a new method through
$.fn
Keep the library's helpers private
FULL SOURCEOld libraries were added with a plain <script> tag, before JavaScript had modules. Any name a script made at its top level became a global: a name that every script on the page shares. Two scripts could then fight over the same name.
To avoid that, the library puts all its code inside an IIFE. IIFE is short for "Immediately Invoked Function Expression". It is a function that runs right where it is written. Names made inside it stay inside it. The IIFE here does three more things:
- It takes
windowanddocumentas parameters. So the first line shows which browser objects the library uses. - It turns on
"use strict". In strict mode, a typo that would quietly make a new global throws an error instead. - It makes exactly one global on purpose, on the last line:
window.$ = $.
(function (window, document) {
"use strict";
const previousDollar = window.$;
const events = new WeakMap();
const slice = Array.prototype.slice;
function isElement(value) {
return value && (value.nodeType === 1 || value === window || value === document);
}
function isListLike(value) {
return value && typeof value !== "string" && typeof value.length === "number";
}
function normalize(input, context) {
if (input == null) return [];
if (input instanceof Collection) return input.toArray();
if (typeof input === "string") {
try {
return slice.call((context || document).querySelectorAll(input));
} catch (error) {
throw new SyntaxError('Invalid selector "' + input + '": ' + error.message);
}
}
if (isElement(input)) return [input];
if (isListLike(input)) {
const elements = slice.call(input);
if (!elements.every(isElement)) {
throw new TypeError("List input must contain only DOM elements.");
}
return elements;
}
throw new TypeError("Expected a selector string, DOM element, or array-like collection.");
}
function classes(names) {
return String(names).trim().split(/\s+/).filter(Boolean);
}
function remember(element, record) {
const list = events.get(element) || [];
list.push(record);
events.set(element, list);
}
function Collection(input, context) {
const elements = normalize(input, context);
this.length = elements.length;
for (let index = 0; index < elements.length; index += 1) {
this[index] = elements[index];
}
}
function $(input, options) {
if (options !== undefined && (!options || typeof options !== "object" || Array.isArray(options))) {
throw new TypeError("Options must be an object.");
}
const settings = Object.assign({ root: document }, options);
if (!settings.root || typeof settings.root.querySelectorAll !== "function") {
throw new TypeError("options.root must support querySelectorAll().");
}
return new Collection(input, settings.root);
}
Collection.prototype.toArray = function () {
return slice.call(this, 0, this.length);
};
Collection.prototype.each = function (callback) {
for (let index = 0; index < this.length; index += 1) {
callback.call(this[index], this[index], index);
}
return this;
};
Collection.prototype.map = function (callback) {
return $(this.toArray().map(function (element, index) {
return callback.call(element, element, index);
}).filter(Boolean));
};
Collection.prototype.filter = function (test) {
const predicate = typeof test === "string"
? function (element) { return element.matches(test); }
: test;
return $(this.toArray().filter(function (element, index) {
return predicate.call(element, element, index);
}));
};
Collection.prototype.find = function (selector) {
const found = [];
this.each(function (element) {
found.push.apply(found, element.querySelectorAll(selector));
});
return $(found);
};
Collection.prototype.addClass = function (names) {
return this.each(function (element) {
element.classList.add.apply(element.classList, classes(names));
});
};
Collection.prototype.removeClass = function (names) {
return this.each(function (element) {
element.classList.remove.apply(element.classList, classes(names));
});
};
Collection.prototype.toggleClass = function (name, force) {
return this.each(function (element) {
element.classList.toggle(name, force);
});
};
Collection.prototype.css = function (name, value) {
if (typeof name === "string" && value === undefined) {
return this[0] ? window.getComputedStyle(this[0])[name] : undefined;
}
const styles = typeof name === "object" ? name : { [name]: value };
return this.each(function (element) {
Object.keys(styles).forEach(function (property) {
element.style[property] = styles[property];
});
});
};
Collection.prototype.text = function (value) {
if (value === undefined) return this[0] ? this[0].textContent : "";
return this.each(function (element) {
element.textContent = value;
});
};
Collection.prototype.attr = function (name, value) {
if (value === undefined) return this[0] ? this[0].getAttribute(name) : undefined;
return this.each(function (element) {
if (value === null) element.removeAttribute(name);
else element.setAttribute(name, String(value));
});
};
Collection.prototype.on = function (type, selector, handler) {
const delegated = typeof selector === "string";
const listener = delegated ? handler : selector;
if (typeof listener !== "function") throw new TypeError("on() expects a handler function.");
return this.each(function (element) {
const wrapped = function (event) {
if (!delegated) {
listener.call(element, event, element);
return;
}
const match = event.target.closest(selector);
if (match && element.contains(match)) listener.call(match, event, match);
};
remember(element, { type: type, selector: delegated ? selector : null, listener: listener, wrapped: wrapped });
element.addEventListener(type, wrapped);
});
};
Collection.prototype.off = function (type, selector, handler) {
const delegated = typeof selector === "string";
const listener = delegated ? handler : selector;
return this.each(function (element) {
const list = events.get(element) || [];
const kept = [];
list.forEach(function (record) {
const sameType = !type || record.type === type;
const sameSelector = !delegated || record.selector === selector;
const sameListener = !listener || record.listener === listener;
if (sameType && sameSelector && sameListener) element.removeEventListener(record.type, record.wrapped);
else kept.push(record);
});
events.set(element, kept);
});
};
$.fn = Collection.prototype;
$.version = "1.0.0";
$.extend = function (target) {
const sources = slice.call(arguments, 1);
if (sources.length === 0) {
sources.push(target);
target = $;
}
sources.forEach(function (source) {
Object.keys(source || {}).forEach(function (key) {
target[key] = source[key];
});
});
return target;
};
$.noConflict = function () {
if (window.$ === $) window.$ = previousDollar;
return $;
};
window.$ = $;
})(window, document);The code is long. You don't need to read it all now. Each part is explained in its own section below.
A library's public API is the list of names other people are meant to use. This library puts three of them on $ itself, so you can check them:
console.log($.version);
console.log(typeof $.fn.text);
console.log(typeof $.extend);It prints 1.0.0, then function twice:
$.versionis the version number of the library.$.fnholds the methods every wrapper can call, such astext.$.extendis a function that copies extra names onto an object.
These names are a promise to users. The helpers inside the IIFE are not part of that promise.
Now look at the whole library from the outside in. The only global it makes on purpose is window.$ = $. Everything else stays inside the IIFE: events, normalize, classes, and Collection. Code outside can't reach them. This is the same privacy idea as in closure patterns, used for a browser library.
Each row of this table names a risk, what the library does about it, and why that helps.
| What could go wrong | What the library does | Why it helps |
|---|---|---|
| A user's text could run as HTML. | text(value) writes to textContent. | The text is shown as plain characters, so no HTML is built and no code runs. |
| Two scripts could fight over one global name. | Only $ becomes global, and noConflict() can give it back. | Other scripts keep their names. |
| Changing built-in prototypes changes every array or element. | Methods go on Collection.prototype, never on Array.prototype. | Built-in objects work the same for every other script. |
| A selector could match nothing. | Setters loop zero times. Getters return undefined or "". | Code can run before the elements exist, without crashing. |
Patching means adding your own methods to a built-in prototype, such as Array.prototype. This library never does that. It adds methods only to its own Collection.prototype. It never changes Array.prototype, NodeList.prototype, or Element.prototype. So arrays, lists, and elements work the same for every other script on the page. The native prototypes lesson explains this rule in full.
One $ function, shared methods
STEP THROUGHA factory is a function that makes objects for you, so you don't write new. In this library, $ is the factory. You write $(selector), not new Collection(selector). $ does three jobs:
- It makes the
new Collection(...)call for you. - It turns many kinds of input into one plain list of elements.
- It returns a wrapper with a
lengthand numbered slots0,1, and so on, like an array. An object shaped like this is called array-like.
It accepts any of these inputs:
- a selector string, such as
".item", - a single element,
- a
NodeList, the list thatquerySelectorAllreturns, - an array of elements,
- another wrapper.
$ also takes an optional second argument: an options object. Its root option says where to search.
const root = document.querySelector(".cards");
const cards = $(".card", { root });
console.log(cards.length);
console.log($(".missing", { root }).length);Without options, $ searches the whole document. Here root is the .cards area, so $ searches only there:
- Line 2 finds one
.card, so line 3 prints1. - Line 4 searches for
.missing, finds nothing, and prints0.
A selector that matches nothing is not an error. You simply get an empty wrapper.
$ also checks its input. When the input is wrong, it throws an error with a clear message.
try {
$(42);
} catch (error) {
console.log(error.name);
console.log(error.message);
}
try {
$("li[");
} catch (error) {
console.log(error.name);
console.log(error.message.startsWith("Invalid selector"));
console.log(error instanceof SyntaxError);
}This code has two try blocks. Each one catches an error and prints parts of it:
$(42)passes a number.$can't use a number, so it throws aTypeError. The message lists the inputs it accepts.$("li[")passes a broken selector.$throws aSyntaxError. Its message starts withInvalid selector, and it really is aSyntaxError, so the last two lines printtrue.
A clear error tells the user what went wrong, so the bug is quick to find.
Step through the factory below. Watch it make two wrappers, then compare their methods.
Step through how $ makes wrappers that hold elements but share one set of methods.
script
const b = $(document.querySelectorAll(".item"));console.log(a.length);console.log(a.addClass === b.addClass);const items = $(".item");
const button = $(document.querySelector(".save"));
const fromList = $(document.querySelectorAll(".item"));
console.log(items.length);
console.log(button.length);
console.log(fromList[0].textContent.trim());It prints 2, 1, and Alpha. The selector found two items. The single button became a wrapper of one. The NodeList kept its order, so slot 0 holds the Alpha item.
The methods live on the prototype: one shared object that every wrapper can reach. When you ask a wrapper for addClass, JavaScript doesn't find it on the wrapper itself. So it looks in the prototype and finds it there.
If a page makes 100 wrappers, there is still only one addClass function. You can check that:
const a = $(".item");
const b = $(".save");
console.log(a.addClass === b.addClass);It prints true. Both wrappers point to the very same function.
addClass in one shared prototype, so a.addClass === b.addClass is true.Real jQuery used a hidden constructor called init behind $. A constructor is a function you call with new to build an object. jQuery then set jQuery.fn.init.prototype = jQuery.fn. So objects built by init still found every method in jQuery.fn, and users never wrote new. This lesson uses the simpler return new Collection(...), so the idea is easier to see.
function Dollar(selector) {
return new Dollar.fn.init(selector);
}
Dollar.fn = Dollar.prototype = {
constructor: Dollar,
addClass(name) {
this[0].classList.add(name);
return this;
},
};
Dollar.fn.init = function (selector) {
this[0] = document.querySelector(selector);
this.length = this[0] ? 1 : 0;
};
Dollar.fn.init.prototype = Dollar.fn;Line 2 calls the hidden init with new. Line 11 is init itself. Line 15 points init's prototype at Dollar.fn, so every object it builds shares the same methods.
Calling methods one after another
INTERACTIVEA chain is several method calls joined by dots, such as $(".item").addClass("active").text("Hi"). You read it from left to right. It works because of one rule:
- A setter is a method that changes something. It does its work, then returns
this, the wrapper it was called on. The next method in the chain runs on that same wrapper. - A getter is a method that reads something.
text(),attr(name), andcss(name)return a value from the first element. A value is not a wrapper, so the chain ends there.
Step through a chain below. Watch each setter hand back the same wrapper.
Follow $(".item").addClass("active").text("Hi"): make the wrapper, call two setters, and get the same wrapper back each time.
script
const result = items .addClass("active") .text("Hi");console.log(result === items);$(".item")
.addClass("active")
.attr("data-state", "ready")
.text("Hi");
console.log($(".item").attr("class"));
console.log($(".item").attr("data-state"));
console.log($(".item").text());Lines 1 to 4 are one chain with three setters. Lines 5 to 7 read the results back. It prints item active, ready, and Hi.
Notice that text does two jobs. text("Hi") writes. text() reads. Using one name for two jobs is called overloading. It keeps the library short. But you can't tell what a call returns until you count its arguments. A modern API design might use two names instead, such as getText and setText.
text("Hi") writes and hands back the wrapper, so you can call another method. With no value, text() reads and hands back the text, so the chain stops there.console.log($(".item").css("color"));
$(".item").css("color", "crimson");
$(".item").css({ backgroundColor: "lemonchiffon" });
console.log($(".item").attr("data-name"));
$(".item").attr("data-name", "renamed");Line 1 passes one argument, so css reads the color. Lines 2 and 3 write: one with a name and a value, one with an object. Line 4 reads data-name, and line 5 writes it.
const items = $(".item");items.addClass("active");items.text("Safe text");items.find("span").addClass("label");$.fn.flash = function () { return this.addClass("highlighted");};items.flash();Reset builds a real list. Press one button at a time and watch the list change.
$ here only searches inside this box, so the rest of the page is not touched. Try typing HTML into the text field: it shows up as plain text.Changing elements and listening for clicks
The methods that change elements are short on purpose. Each one hands the work to a tool the browser already has:
addClass,removeClass, andtoggleClassuseclassList, the element's list of CSS classes.csswrites style properties, such ascolor.attrreads and writes attributes, such asdata-name.findsearches inside each wrapped element.
These build on modifying the DOM and styles and classes.
The text(value) setter writes to textContent, not innerHTML. The two are different:
textContentshows a string as plain text.innerHTMLreads a string as HTML and builds elements from it.
So with text, a string like <img src=x onerror=alert(1)> shows up as those characters on the page. No image is made, and no code runs. This matters for text that came from a user. The later XSS lesson explains why turning a user's text into HTML is dangerous.
Delegation means one listener on a parent element handles events for its children. It works because a click on a child also reaches its parents. This is called bubbling. The library's on(type, selector, handler) uses delegation:
- It adds one listener to the parent.
- When a click arrives, it calls
event.target.closest(selector).closeststarts at the clicked element and walks up through its parents until it finds a match. - If it finds one, it calls your handler with
thisset to the matching child.
const list = $(".todo-list");
let heard = "";
function rememberClick(event) {
heard = this.dataset.name;
}
list.on("click", ".item", rememberClick);
document.querySelector('[data-name="Beta"]').click();
list.off("click", ".item", rememberClick);
console.log(heard);Line 6 adds one listener to the list. Line 7 clicks the Beta item. Line 8 removes the listener. Inside rememberClick, this is the Beta item, so heard becomes "Beta". Line 9 prints Beta.
closest(".item") finds Beta, so heard becomes "Beta".How does off on line 8 remove the right listener? on doesn't add your handler directly. It adds a small function of its own around it. So on saves that function in a private WeakMap, a map from each element to its saved listeners. off looks there and removes the exact function. This is the same idea as the delegation lesson, packed into one library method.
on also checks that you gave it a function:
try {
$(".item").on("click", ".item", null);
} catch (error) {
console.log(error.name);
console.log(error.message);
}Passing null instead of a function throws a TypeError with the message on() expects a handler function. It is better to fail now than to add a listener that can never run.
Adding your own methods with plugins
EXTENDA plugin is extra code that adds a method to a library without editing the library's file. Teams use plugins to add the methods they need.
In jQuery, $.fn is another name for the wrapper prototype. This library does the same, with $.fn = Collection.prototype. Put a function on $.fn, and every wrapper can call it. If the plugin changes things, it is a setter, so it must return this. Otherwise the next method in a chain fails.
$.fn.highlight = function (color = "gold") {
return this.css("backgroundColor", color).addClass("highlighted");
};
$(".item").highlight("gold").text("Plugin");
console.log($(".item").attr("class"));
console.log($(".item").text());Lines 1 to 3 add highlight to $.fn. Line 2 returns what addClass returns, which is the wrapper. So line 4 can chain .text("Plugin") after it. It prints item highlighted and Plugin.
$.fn is another name for the wrapper prototype. Putting highlight there gives it to every wrapper, next to the library's own methods.Plugins often take options. A plugin's defaults are the values it uses when the caller gives none. Copy the defaults into a new object first, then put the caller's options on top. That way one caller can't change the defaults for everyone after them.
$.fn.markReady = function (options = {}) {
const settings = Object.assign({}, $.fn.markReady.defaults, options);
return this.addClass(settings.className);
};
$.fn.markReady.defaults = { className: "ready" };
const options = Object.freeze({ className: "active" });
$(".item").markReady(options);
console.log($(".item").attr("class"));
console.log(options.className);
console.log($.fn.markReady.defaults.className);Line 2 uses Object.assign. It copies the defaults into a new empty object, {}, then copies the caller's options over them. It prints item active, active, and ready:
- The caller asked for
active, so the items get that class. - The caller's own options object is unchanged.
- The plugin's default is still
ready.
This copy is shallow: it copies only the top-level properties. An object nested inside the options would still be shared, so it needs its own copy.
Some names belong on $ itself, not on each wrapper. $.extend copies properties for that:
$.extend(target, source)copies the properties ofsourceontotarget.$.extend(source), with one argument, copies them onto$.
$.extend({ libraryName: "Mini" });
const settings = $.extend({}, { root: ".todo-list" });
console.log($.libraryName);
console.log(settings.root);Line 1 adds libraryName to $. Line 2 copies root onto a new empty object. It prints Mini and .todo-list. Like Object.assign, this copy is shallow.
Some pages load two scripts that both want the name $. noConflict() solves that. It gives window.$ back to whatever it was before. It also returns the library, so you can keep it under another name.
const localDollar = $.noConflict();
console.log(window.$ === undefined ? "released" : "still global");
window.$ = localDollar;
console.log(localDollar(".item").length);Line 1 gives $ back. Nothing had $ before, so window.$ is now undefined and line 2 prints released. Line 3 puts the library back for the next examples. Line 4 uses the saved name and prints 2.
- Use a regular
functionwhen the plugin needsthis. An arrow function doesn't get its ownthis. - Return
thisfrom a plugin that changes things. - Keep the plugin's data private, in a closure or a
WeakMap, not in new globals. - Write down whether the plugin reads, writes, or does both.
A version such as 1.0.0 has three numbers: major, minor, and patch. This system is called semantic versioning.
- A patch release, such as
1.0.1, fixes a bug. Documented behavior stays the same. - A minor release, such as
1.1.0, adds a new method or option. Old code keeps working. - A major release, such as
2.0.0, changes or removes documented behavior, such as what a setter returns.
Before you call a change "breaking", ask: could users depend on the old behavior? Your tests and examples count as part of what you promised.
How to read a library's source code
Real libraries are much bigger than this one. But you can read them the same way. Don't start by reading every helper from top to bottom. Follow these steps instead:
- Start: find where the code begins, such as the exported function or the IIFE.
- Public API: list the methods users are meant to call, such as
$.fn.text. - One call: follow one real call from start to end, such as
$(".item").addClass("active").text("Hi"). - Tests: search the tests for the method name. They show what the method should do.
- History: when a line looks strange, use
git blameor the commit history to see why it was written.
Ask Tuto for the source tour. It walks through this library in order: the IIFE, the $ factory, the shared methods, the saved event listeners, the plugin slot $.fn, and noConflict. You can follow the same path in any open-source library.
Good docs show a call the way a user will write it:
/** Select matching items inside one root element. */
const list = $(".item", { root: document.querySelector(".todo-list") });
list.addClass("ready");
console.log(list.length);
console.log(list.attr("class"));Line 1 is a short comment that says what the call does. Line 2 calls $ exactly as a user would, inside the .todo-list root. It prints 2 and item ready. Run examples like this in your tests too. Then you notice as soon as the docs stop matching the code.
What people use today
COMPAREOld libraries were not silly. They solved real problems. But browsers got better, and many jQuery jobs are now built in. Today:
- Small widgets often use the browser's own DOM methods directly.
- Larger apps often use a framework.
- Shared helpers live in ES modules. A module exports names, and other files import them, so no global is needed.
Each row of this table asks one question. The columns answer it for the mini $ library, the built-in DOM methods, and ES modules.
| Question | Mini $ library | Built-in DOM methods | ES modules |
|---|---|---|---|
| What is the main idea? | A wrapper object holds elements and has methods you can chain. | You call methods on elements and lists directly, such as classList.add. | You export named functions and import them where you need them. |
| What becomes global? | Only $. The helpers stay private inside the IIFE. | Nothing new. You use document, elements, and other built-in objects. | Nothing. Each file imports what it uses by name. |
| How do you add a feature? | Put a method on $.fn, or copy a helper onto $ with $.extend. | Write a plain function. Never add methods to built-in prototypes. | Export another function, or build it from smaller modules. |
| When is it a good fit? | For learning, for old pages, and for small extras on a page. | For most work with the page today. | For larger apps split across many files. |
document.querySelectorAll(".item").forEach((element) => {
element.classList.add("active");
element.textContent = "Hi";
});
document.querySelector(".todo-list")?.addEventListener("click", (event) => {
const item = event.target.closest(".item");
if (item) console.log(item.dataset.name);
});Line 1 finds every item and loops over them. Lines 2 and 3 add the class and set the text. Lines 5 and 6 add one listener to the list and use closest to find the clicked item.
export function selectAll(selector, root = document) {
return [...root.querySelectorAll(selector)];
}
export function addClass(elements, name) {
for (const element of elements) element.classList.add(name);
return elements;
}Each helper is a named export. A file that needs one imports it by name, so there is no global $.
`$.fn.highlight = function () { ... }``$(".item").addClass("active").text("Hi")``event.target.closest(".item")``element.classList.add("active")``export function addClass(elements, name) { ... }``document.querySelectorAll(".item")`
Sort each card: is it part of the mini $ library, a built-in DOM method, or the ES module style?
Common misconceptions
- "An IIFE makes code secure." It only keeps names private. It doesn't make risky page changes or network calls safe.
- "Array-like means it is an array." The wrapper has numbered slots and a
length, but it is the library's own object with its own prototype.Array.isArraysaysfalsefor it. - "Every method should chain." Getters return values. Setters usually return
this. - "Plugins are always safe." A plugin can still make globals, turn a user's text into HTML, or break chains by forgetting to return
this. - "jQuery is old, so its code teaches nothing." Its code still teaches API design, working across browsers, testing, and moving to new tools.
- "Small helpers should go on built-in prototypes." Use your own wrapper or module functions instead.
Each row of this table names an idea, what people often think it is, and what it really is.
| Idea | What people often think | What it really is |
|---|---|---|
$.fn | It is $ itself. | It is the prototype that holds the methods every wrapper can call. |
$.extend | It is a plugin. | It is a helper that copies properties onto $ or onto another object. |
| One method that reads and writes | It always chains. | Reading returns a value. Writing returns the wrapper. |
| Delegation | It adds a listener to every child. | It adds one listener to a parent, which finds the matching child. |
Practice exercises
5 EXERCISESRun the code. Do the two wrappers have the very same addClass function? Type true or false.
const a = $(".item");
const b = $(".save");
console.log(a.addClass === b.addClass);It prints true. Both wrappers find addClass on Collection.prototype, so it is the very same function.
These two methods should work in a chain, like $(".item").hide().show(). What should both methods return?
$.fn.hide = function () {
return this.css("display", "none");
};
$.fn.show = function () {
return this.css("display", "");
};
$(".item").hide().show();$.fn.hide = function () {
return this.css("display", "none");
};
$.fn.show = function () {
return this.css("display", "");
};
$(".item").hide();
console.log($(".item").css("display"));
$(".item").show();
console.log($(".item").css("display") === "none");hide and show both return this, the wrapper. So $(".item").hide().show() keeps chaining. The code prints none after hide. After show, it prints false, because the display is no longer none.
Where should you put a method so that every wrapper can call it? Type the name of that place.
// Replace this object with the real extension point.
const extensionPoint = {};
extensionPoint.highlight = function () {
return this.addClass("highlighted");
};$.fn.highlight = function () {
return this.addClass("highlighted");
};
console.log($(".item").highlight().attr("class"));$.fn is Collection.prototype. Adding highlight there gives it to every wrapper, so the code prints item highlighted.
This method adds a class, but the next call in the chain crashes. Which line fixes it?
$.fn.mark = function () {
this.addClass("marked");
};
// Uncomment after you fix the method:
// $(".item").mark().text("Broken");$.fn.mark = function () {
this.addClass("marked");
};
try {
$(".item").mark().text("Broken");
} catch (error) {
console.log(error.name);
}
$.fn.mark = function () {
this.addClass("marked");
return this;
};
console.log($(".item").mark().text("Fixed").text());Without a return, mark returns undefined. Calling .text on undefined throws a TypeError. Adding return this; keeps the chain going, so the fixed version prints Fixed.
Which DOM property shows a user's text as plain text, instead of reading it as HTML?
$.fn.text = function (value) {
return this.each(function (element) {
// Which property should receive value?
});
};$(".item").text("<img src=x onerror=alert(1)>");
console.log(document.querySelector(".item").children.length);
console.log($(".item").text());textContent treats the string as plain text. The code prints 0 children, so no <img> element was made. Then it prints the string itself.
Check your understanding
8 QUESTIONSQuestion 1 of 8Why did jQuery become popular in the first place?
Choose an answer to see the explanation.
Question 2 of 8What does the IIFE keep private?
Choose an answer to see the explanation.
Question 3 of 8What does this print?
Read the code, then predictconst a = $(".item"); const b = $(".save"); console.log(a.addClass === b.addClass);Choose an answer to see the explanation.
Question 4 of 8Why can setter methods chain? What does this print?
Read the code, then predictconst items = $(".item"); console.log(items.addClass("active") === items);Choose an answer to see the explanation.
Question 5 of 8Why is
text(value)safer than anhtml(value)method for a user's text? What does this print?Read the code, then predict$(".item").text("<strong>Hi</strong>"); console.log(document.querySelector(".item").children.length);Choose an answer to see the explanation.
Question 6 of 8What does the delegation example print after clicking Beta?
Read the code, then predictconst list = $(".todo-list"); let heard = ""; function rememberClick(event) { heard = this.dataset.name; } list.on("click", ".item", rememberClick); document.querySelector('[data-name="Beta"]').click(); list.off("click", ".item", rememberClick); console.log(heard);Choose an answer to see the explanation.
Question 7 of 8Where should a plugin put a method that every wrapper can call?
Choose an answer to see the explanation.
Question 8 of 8In a new app today, what would you usually use instead of a global
$?Choose an answer to see the explanation.
Key takeaways
- An IIFE keeps the library's helpers private and makes just one global on purpose:
$. - The
$factory hidesnew. It accepts a selector, an element, aNodeList, an array, or another wrapper. - Methods live on one shared prototype. Setters return
this; getters return values. $.fnadds methods for every wrapper,$.extendcopies names onto$, andnoConflictgives the name$back.- Today, built-in DOM methods and ES modules do most of these jobs. Reading old library code still teaches good API design.
Final definition: a mini DOM library is a $ function that wraps the elements you select, shares its methods through one prototype, lets setters chain, and gives clear places to add new methods.
Next: build a tiny reactive UI with the same DOM and API design ideas.