completefrontend
Open lab My progress
THE JAVASCRIPT FIELD GUIDE

Creational patterns

Learn factory functions, singletons, builders, modules, revealing modules, and cloning choices for production JavaScript architecture.

By the end, you can
  • 01
    Pick the right way to make objectsSay when a factory, a singleton, a builder, a module, or a copy makes your code clearer.
  • 02
    Follow real code step by stepTrace a factory that keeps private data, a singleton made on first use, and a builder that saves choices in steps.
  • 03
    Avoid common pattern mistakesSpot shared data that tests can't swap, copies that share inner objects, lost this, and patterns used where a plain object would do.

Patterns are names for common designs

Programmers solve the same kinds of problems again and again. Over time, the common solutions got names. A design pattern is one of those names. It lets a team say "use a factory here" in three words, instead of explaining the whole design each time. A pattern is a shared word, not a rule you must follow.

The best-known list of patterns comes from a 1994 book by four authors, often called the "Gang of Four" book. It used the languages C++ and Smalltalk. JavaScript has tools those languages lacked, so some of those patterns become very small here:

  • A shared object can be one object exported from a module file.
  • Private data can live in a closure: variables a function keeps after it returns.
  • A long chain of setup steps can often be one object of options with default values.
The main idea

Creational patterns are names for ways to make objects. They answer four questions: how do I create an object, set it up, share it, and copy it? In this lesson you write each one as real JavaScript: factory functions, singletons, builders, modules, and copies.

Some of these ways look alike at first. The table below puts them side by side. Read each row as: "this way makes objects like this, it suits these jobs, and here is what to watch for."

Ways to make objects that are easy to mix up
Way to make objectsHow it makes themWhen it helpsWhat to watch for
Factory functionA plain function makes an object and returns it.When objects differ a little, need private data, or when you want to avoid new and this mistakes.Each call makes a new object and new copies of its methods.
Classnew runs the constructor. The methods are shared on the prototype.When many objects share methods, or you need instanceof or a parent class.Forgetting new, losing this in a saved method, or too many layers of parent classes.
Constructor functionThe older way to write a class: new Fn() sets values on this, and methods go on Fn.prototype.When you read older code, or want to see what a class does inside.Without new, it runs as a normal function, and this is the wrong object.
SingletonOnly one object is made, and every caller gets that same object.Settings, one shared logger, or feature flags.Tests can't easily swap the shared object, so one test can change what the next test sees.
BuilderMethods save choices one at a time. Then build() checks them and returns the result.Queries, test data, and big objects made in steps.Too many steps when one options object with defaults would do.

This lesson uses ideas from object basics, constructors and new, class basics, closures, closure patterns, Object.create, private members, why modules exist, import and export, and immutability.

Factory functions

CHANGE INPUTS

A factory function is a plain function that makes an object and returns it. You call it like any other function, with no new in front. It helps in three ways:

  • It can keep private data in a closure, where outside code can't reach it.
  • It can return a different kind of object depending on what you pass in.
  • It avoids two common class mistakes: forgetting to write new, and losing this, the object a method works on.
Real-life analogyAsking a cook for tea

You ask a cook for tea and say how you like it. The cook makes a fresh cup and hands it to you. The next person gets their own cup, made the way they asked.

In real life: You ask for tea
In JavaScript: You call the factory with an input
In real life: The cook hands you a fresh cup
In JavaScript: The factory returns a new object
In real life: You choose with or without sugar
In JavaScript: The input changes what you get
A tiny factory returns a userPop out in the code editor (opens in a new tab)JavaScript
function createUser(name) {
  return { name, greet: () => "Hi, " + name };
}
const user = createUser("Asha");
console.log(user.greet());

Line 1 starts the factory, createUser. Line 2 returns a new object. Its greet function remembers name, because a function keeps the variables around it. Line 4 asks for Asha's object, and line 5 prints Hi, Asha.

Each counter gets its own private countPop out in the code editor (opens in a new tab)JavaScript
function createCounter(label) {  let count = 0;  return {    label,    next() {      count += 1;      return label + ": " + count;    },    reset() {      count = 0;    },  };} const first = createCounter("factory");const second = createCounter("factory");console.log(first.next());console.log(first.next());console.log(second.next());console.log(first === second);

Read it line by line:

  1. Line 1 starts an ordinary function, createCounter.
  2. Line 2 makes a new count variable, just for this one call.
  3. Lines 5 to 8 are the next method on the returned object. It can still reach count, but nothing outside can.
  4. Lines 15 and 16 call the factory twice. That makes two objects, each with its own count.
  5. Lines 17 to 19 print factory: 1, factory: 2, and factory: 1. The second counter starts from zero.
  6. Line 20 prints false: the two objects are different, even though both have the same label.
One createCounter function makes two carts, first and second. Each cart holds its own next function. First has its own count of 2, second has its own count of 1. first === second is false.createCounter()next()firstown count: 2next()secondown count: 1first === second → false
Each call to createCounter() makes a new object with its own private count. So first and second are two different objects.
A factory, a class, and a constructor function side by sidePop out in the code editor (opens in a new tab)JavaScript
function createUser(name) {  return {    name,    greet() {      return "Hello, " + name;    },  };} class ClassUser {  constructor(name) {    this.name = name;  }  greet() {    return "Hello, " + this.name;  }} function ConstructorUser(name) {  this.name = name;}ConstructorUser.prototype.greet = function () {  return "Hello, " + this.name;}; console.log(createUser("Ada").greet());console.log(new ClassUser("Lin").greet());console.log(new ConstructorUser("Grace").greet());

All three make an object with a greet method. They differ in how:

  • The factory (line 1) returns an object, and greet reads name from the closure.
  • The class (line 10) is called with new and saves name on this.
  • The constructor function (line 19) is the older way to write a class. new ConstructorUser() makes the object, and greet is added to its prototype: a shared object that all its objects look in for methods.

Lines 26 to 28 print Hello, Ada, Hello, Lin, and Hello, Grace.

There is one cost to a factory. Each call makes a new greet function. A class keeps one greet on its prototype, shared by every object. That matters only when you make very many objects.

If you use TypeScript, its types are removed before the code runs. So when a factory gets data from a form or from JSON, check that data in code too.

Pick what the factory makes
Notifier factoryPop out in the code editor (opens in a new tab)JavaScript
function createNotifier(channel) {  if (channel === "email") {    return {      send(message) {        return "email: " + message;      },    };  }  if (channel === "sms") {    return {      send(message) {        return "sms: " + message;      },    };  }  return {    send(message) {      return "toast: " + message;    },  };} console.log(createNotifier("email").send("Build passed"));console.log(createNotifier("sms").send("Deploy now"));
What you getemail
Factory callcreateNotifier("email")
send(message)email: Build passed
Try it yourself
Choose the channel

createNotifier("email") returns an object with the same send method as the others. This one prints email: Build passed.

A factory can pick one of several objects. Each one has the same methods, so the code that uses it doesn't change.

The notifier above is a factory that picks one of three objects: email, SMS, or toast. All three have the same send method, so the code that calls it doesn't change. A bigger version of this idea is called an abstract factory. It makes a whole set of objects that belong together. For example, one UI factory could return a matching createButton and createDialog. Later lessons cover that. For now, remember: one function can hide the choice and return objects with the same methods.

Singletons

STEP THROUGH

A singleton is an object that exists only once. Every part of the program that asks for it gets that same object.

In JavaScript, a module file is the easiest way to make one. A file with import and export runs only once, even if many files import it. So every importer gets the same exported object. This sharing has limits. Each worker, each browser tab, and each Node process runs its own copy of the module.

cart.mjs: the shared objectJavaScript
globalThis.moduleLoads = (globalThis.moduleLoads ?? 0) + 1;
export const cart = { items: [] };

Line 1 counts how many times this file has run. Line 2 exports one cart object.

consumer.mjs: import the same file twiceJavaScript
import { cart as first } from "./cart.mjs";
import { cart as second } from "./cart.mjs";
first.items.push("tea");
console.log(first === second);
console.log(second.items.join(", "));
console.log(globalThis.moduleLoads);

In consumer.mjs, lines 1 and 2 import the cart from the same file, under two names. Line 3 adds tea through first. Lines 4 to 6 print true, tea, and 1:

  • true: both names point at the same object.
  • tea: the item added through first shows up through second.
  • 1: cart.mjs ran only once.

These are two real module files, so they can't run in the page's editor. They have no Run button.

A singleton can also be lazy: it isn't made until the first time someone asks for it. Step through the example below and watch the second call.

Step through a lazy singleton
Step 0 of 13Ready
Your turn: follow the blue line

A lazy singleton makes its one object on the first call. Every later call gets that same object back. Step through both calls and watch the second argument get ignored.

Running in
  1. script
Next: line 1
function getConfig(env) {  if (!configInstance) {    configInstance = Object.freeze({      env,      apiBase: "https://" + env + ".example.com",      retries: 2,    });  }  return configInstance;} const first = getConfig("prod");const second = getConfig("staging");console.log(first === second);console.log(second.env);console.log(Object.isFrozen(first));
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.
Two name tags, first and second. first came from getConfig with prod, second from getConfig with staging. Both point at the same one config cart, which holds env prod. first === second is true.firstgetConfig("prod")secondgetConfig("staging")env: "prod"configfirst === second → true
The first call makes the config object. The second call gets that same object back, so its "staging" is ignored.
New objects from a factory, one object from a singleton
Compare a factory and a singletonPop out in the code editor (opens in a new tab)JavaScript
function createCounter(label) {  let count = 0;  return {    next() {      count += 1;      return label + ": " + count;    },  };} let configInstance;function getConfig(env) {  if (!configInstance) configInstance = { env };  return configInstance;} const firstCounter = createCounter("box");const secondCounter = createCounter("box");const firstConfig = getConfig("prod");const secondConfig = getConfig("staging");console.log(firstCounter === secondCounter);console.log(firstCounter.next());console.log(secondCounter.next());console.log(firstConfig === secondConfig);console.log(secondConfig.env);
What the code printsfactory
firstCounter === secondCounterfalse
firstCounter.next()box: 1
secondCounter.next()box: 1
Try it yourself

Two factory calls make two different objects. Each first next() call prints 1, because each object has its own count.

Checking with === shows whether you got a new object each time or the same shared one.
The cost: data you can't see

Singletons are fine for things that never change while the program runs, like settings, one shared logger, or feature flags. They cause trouble in tests. A test can't easily give the code a different object, because the code reaches for the singleton itself. The fix is to pass the logger or settings into the function as an argument. That is called dependency injection. The mocks and fakes lesson uses the same idea.

Pass a sender in instead of importing onePop out in the code editor (opens in a new tab)JavaScript
function welcomeUser(user, send) {
  return send("Welcome, " + user.name);
}
const messages = [];
welcomeUser({ name: "Asha" }, (message) => messages.push(message));
console.log(messages[0]);

Line 1 takes send as an argument. Line 5 passes a small fake send. It saves the message in an array instead of sending an email. Line 6 prints Welcome, Asha. You don't need any library for this. Real code passes a real sender; a test passes a fake.

Object.freeze stops anyone from changing an object's own properties. That suits a settings object. But it doesn't make code easier to test. It is also shallow: objects inside the frozen object can still change, unless you freeze them too. Use a singleton only when sharing one object is the goal.

Builders

STEP THROUGH

A builder helps when you make something in several steps. It works like this:

  • Each method saves one choice.
  • Each method returns the builder (this), so you can call the next method right after it, like .select().where(). This is called chaining.
  • A last method, build(), checks the saved choices and returns the finished result.
A request needs no builderPop out in the code editor (opens in a new tab)JavaScript
const request = { method: "GET", path: "/users" };
console.log(request.method + " " + request.path);

Line 1 makes the whole request in one go. Line 2 prints GET /users. When an object is this small, a plain object is easier to read than a chain of builder methods.

Step through a builder chain
Step 0 of 12Ready
Your turn: follow the blue line

A builder saves choices over several method calls. Then build() checks them and returns the finished result.

Running in
  1. script
Next: line 1
  const state = {    table,    fields: ["*"],    filters: [],    limit: null,  };  return {    select(...fields) {      state.fields = fields;      return this;    },    where(field, value) {      state.filters.push({ field, value });      return this;    },    limit(count) {      state.limit = count;      return this;    },    build() {      if (!state.table) throw new Error("table is required");      if (state.limit !== null && state.limit <= 0) {        throw new Error("limit must be positive");      }      const fields = state.fields.join(", ");      const where = state.filters.length        ? " WHERE " + state.filters.map((filter) =>            filter.field + " = " + JSON.stringify(filter.value)          ).join(" AND ")        : "";      const limit = state.limit === null ? "" : " LIMIT " + state.limit;      return "SELECT " + fields + " FROM " + state.table + where + limit;    },  };} const query = createQueryBuilder("users")  .select("id", "email")  .where("active", true)  .limit(2)  .build();console.log(query);
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.
Four method cards in a row: select, where, limit, and build. select saves the fields, where saves a filter, limit saves 2. build returns the string SELECT id, email FROM users WHERE active = true LIMIT 2.select()where()limit()build()saves fieldssaves a filtersaves limit 2SELECT id, email FROM usersWHERE active = true LIMIT 2
select(), where(), and limit() each save one choice and return the same builder. Only build() checks the choices and makes the final string.

This query builder is small, but it has every part of a real one. select, where, and limit save choices. build() throws an error if the table is missing or the limit is zero or less. Otherwise it returns a string. A real query builder would also protect against unsafe input. It would not glue SQL together from strings by hand.

One options object with defaults is often enoughPop out in the code editor (opens in a new tab)JavaScript
function createApiClient({  baseUrl = "/api",  timeout = 3000,  headers = {},} = {}) {  return { baseUrl, timeout, headers };} const fastClient = createApiClient({ timeout: 1000 });console.log(fastClient.baseUrl);console.log(fastClient.timeout);

Lines 1 to 5 take one options object and give each option a default value. Line 9 passes only timeout. Line 10 prints the default /api, and line 11 prints 1000.

Many JavaScript functions should stop here. An options object is short, and it works well with parameters and destructuring. Use a builder only when there are real steps to check.

A builder also keeps its choices between calls. Start a new builder for each query. If you reuse one, each where() call adds to the filters already saved there.

Module and revealing module

HISTORICAL

Before module files with import and export worked everywhere, people used an IIFE to keep data private. IIFE stands for "immediately invoked function expression": a function that runs as soon as it is written. Its variables stay private. It returns an object with only the functions other code may use.

A revealing module built with an IIFEPop out in the code editor (opens in a new tab)JavaScript
const cartModule = (() => {  const items = [];  function add(name, price) {    items.push({ name, price });  }  function total() {    return items.reduce((sum, item) => sum + item.price, 0);  }  return { add, total };})(); cartModule.add("Book", 12);cartModule.add("Pen", 3);console.log(cartModule.total());console.log(cartModule.items);

Read it line by line:

  1. Line 1 starts a function and line 10 runs it straight away, with ().
  2. Line 2 makes the items array. It is private, because it is never returned.
  3. Lines 3 to 8 write two private functions, add and total.
  4. Line 9 returns an object with just those two functions.
  5. Lines 12 and 13 add a book and a pen. Line 14 prints 15.
  6. Line 15 prints undefined, because items is not on the returned object.

This is called a revealing module: you write private functions first, then return, or "reveal", the ones you choose.

A module file gives the same privacy for freeJavaScript
let currentUser = null; export function setCurrentUser(user) {  currentUser = user;} export function getCurrentUser() {  return currentUser;}

In a module file, every top-level variable is private unless you export it. Here currentUser (line 1) is private. Only setCurrentUser (line 3) and getCurrentUser (line 7) are exported. And because a module file runs only once, an object made at its top level is JavaScript's usual singleton. Review why modules exist and import and export if this is new to you.

Cloning and copying

REAL COPIES

The prototype pattern makes new objects by copying an existing one. Before you copy, ask what the copy should keep:

  • the object's own properties,
  • objects stored inside it,
  • its methods from a class,
  • its private fields,
  • its prototype, the shared object it looks in for methods.
A prototype object with a clone methodPop out in the code editor (opens in a new tab)JavaScript
const widgetPrototype = {  describe() {    return this.kind + ":" + this.theme;  },  clone(overrides = {}) {    return Object.assign(      Object.create(Object.getPrototypeOf(this)),      this,      overrides    );  },}; const primary = Object.assign(Object.create(widgetPrototype), {  kind: "button",  theme: "primary",});const danger = primary.clone({ theme: "danger" });console.log(primary.describe());console.log(danger.describe());console.log(Object.getPrototypeOf(danger) === widgetPrototype);

widgetPrototype holds two shared methods, describe and clone. Inside clone():

  • Object.create(Object.getPrototypeOf(this)) (line 7) makes a new empty object with the same prototype as the original.
  • Object.assign (line 6) copies the original's own properties onto it, then the changes in overrides.

Line 18 makes danger, a copy of primary with a new theme. Lines 19 to 21 print button:primary, button:danger, and true: the copy kept the same prototype.

A clone() method and a copy constructorPop out in the code editor (opens in a new tab)JavaScript
class Point {  constructor(xOrPoint, y) {    if (typeof xOrPoint === "object") {      this.x = xOrPoint.x;      this.y = xOrPoint.y;    } else {      this.x = xOrPoint;      this.y = y;    }  }  clone() {    return new Point(this.x, this.y);  }  toString() {    return this.x + "," + this.y;  }} const original = new Point(2, 3);const cloned = original.clone();const copied = new Point(original);console.log(cloned.toString());console.log(copied.toString());console.log(copied instanceof Point);

There are two ways to copy here:

  • Line 20 calls clone() on the old point. The method (line 11) makes the copy.
  • Line 21 calls new Point(original). This is a copy constructor: the constructor (line 2) sees an object and copies its x and y.

Both print 2,3, and line 24 prints true: the copy is a real Point. Either way, the class decides how deep the copy goes.

Spread copies only the top levelPop out in the code editor (opens in a new tab)JavaScript
const original = {  user: { name: "Ada" },  tags: ["js"],};const spread = { ...original };spread.user.name = "Lin";spread.tags = [...spread.tags, "patterns"];console.log(original.user.name);console.log(original.tags.join(","));console.log(spread.tags.join(","));

The spread syntax { ...original } (line 5) makes a shallow copy: a new outer object, but the objects inside it are shared, not copied.

  • Line 6 changes spread.user.name. Both objects share one user, so line 8 prints Lin for the original too.
  • Line 7 puts a brand new array in spread.tags. The original array doesn't change, so line 9 prints js and line 10 prints js,patterns.
Two carts, original and spread. Each holds a user card. Arrows from both user cards point at one shared card, name Lin.one shared username: "Lin"useroriginaluserspread
{ ...original } makes a new outer object, but both user properties point at the same inner object. So changing spread.user.name also changes original.user.name.
structuredClone copies the data, not the classPop out in the code editor (opens in a new tab)JavaScript
class Point {  constructor(x, y) {    this.x = x;    this.y = y;  }  distance() {    return Math.hypot(this.x, this.y);  }} const point = new Point(3, 4);const copied = structuredClone(point);console.log(copied instanceof Point);console.log(typeof copied.distance);console.log(JSON.stringify(copied));

structuredClone makes a deep copy: it copies the inner objects too. It works for plain data, like arrays, plain objects, dates, Maps, and Sets. But it doesn't keep a class's prototype. So line 12 copies x and y, but:

  • Line 13 prints false: the copy is not a Point.
  • Line 14 prints undefined: the distance method is gone.
  • Line 15 prints {"x":3,"y":4}: the data is all there.

The table below compares the ways to copy. Read each row as: "this way of copying goes this deep, and it is best for this."

Ways to copy and what each one keeps
Way to copyHow deep does it copy?When to use it
Spread, { ...obj }Only the top level. Inner objects are shared.Plain objects and arrays, when sharing the inner objects is fine.
Object.create(proto)It makes a new object that keeps the same prototype.Objects made by the prototype pattern, with a clone() method and shared methods.
A clone() methodThe object's own method decides.When the object should set its own copy rules, including private fields.
A copy constructorThe class decides.new Point(other) says clearly that it copies, and the result is a real Point.
structuredCloneAll the way down, for plain data.Dates, Maps, Sets, arrays, and plain objects. A class object comes back as a plain object.

Where these appear in real projects

SORT IT

Real code rarely says "this is a pattern". It uses names like createApiClient, logger, queryBuilder, aUser(), or clone(). Knowing the pattern helps you talk about the design and its costs.

The table below shows common jobs. Read each row as: "for this job, people often use this way, for this reason."

Common jobs and the way to make objects for each
The jobThe usual wayWhy it fits
Talking to a server (an API client)A factory or an options objectcreateApiClient({ baseUrl, token }) keeps the server address and key in one place, not in every call.
App settingsA singleton exported from a moduleA frozen settings object is fine when it is made once at startup and never changes.
Database queriesA builderFilters added one step at a time are easy to read, and build() can check them before the query is sent.
Test dataA builder or a factoryaUser().withRole("admin").build() lets a test say only what matters, and still returns a plain object.
Sets of UI partsAn abstract factoryOne factory returns a set of objects that belong together, such as a matching dialog and button.

When a pattern is too much

Pick the smallest thing that works:

  • If one place makes one fixed object, write the object directly, with { }.
  • If the object changes a little with each call, use a factory.
  • If a module already holds one shared service, export it.

Use a named pattern only when it makes the choices, the saved data, the checks, or the copying easier to see.

Object pools: reuse only when it helps

An object pool keeps used objects and hands them out again, instead of making a new one each time. It can help for things that are slow to make or limited in number, like database connections, when the database library doesn't pool them already. For ordinary JavaScript objects, just make new ones.

A tiny pool clears an object before reusing itPop out in the code editor (opens in a new tab)JavaScript
function createPool(make, reset) {
  const available = [];
  return {
    acquire() { return available.pop() ?? make(); },
    release(item) { reset(item); available.push(item); },
  };
}
const pool = createPool(() => ({ value: "" }), (item) => { item.value = ""; });
const first = pool.acquire();
first.value = "old";
pool.release(first);
const reused = pool.acquire();
console.log(first === reused);
console.log(reused.value === "" ? "reset" : reused.value);

Read it line by line:

  1. Lines 1 to 7 make the pool. It keeps returned objects in the available array.
  2. Line 4, acquire(), hands out a kept object, or calls make() if there is none.
  3. Line 5, release(), clears the object with reset and keeps it.
  4. Lines 9 to 12 take an object, write old into it, give it back, and take one again.
  5. Lines 13 and 14 print true and reset: the same object came back, with its old value cleared.

This small pool has gaps. Nothing stops code from giving the same object back twice, and the pool can grow without limit. A real pool needs rules for both. A pool also keeps memory in use, and it can leak old data if the reset misses a property. So measure first, and add a pool only if it clearly helps.

Which creational pattern fits?
  • createNotifier(channel) returns an email, SMS, or toast sender.
  • getConfig() returns the same frozen object after the first call.
  • `query.select("id").where("active", true).build()`
  • `const p2 = new Point(p1);`
  • `createCounter("cart")` returns an object with a private `count`.
  • `userBuilder().withRole("admin").build()`
  • `const copy = structuredClone(settings);`
  • `export const logger = createLogger();`
Try it yourself
0 of 8 correct

Sort each card by the problem it solves. Does it make new objects, share one object, build in steps, or copy?

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Common misconceptions

  • "You must use patterns." Patterns are only names. Use the smallest JavaScript code that solves the problem.
  • "A factory is just a class without the word class." A factory can pick which object to return, and it can keep data private in a closure. A class shares its methods through a prototype.
  • "A frozen singleton is safe." Freezing stops changes to its top-level properties. But it is still one shared object, and tests still can't swap it out.
  • "Every chain of methods is a builder." A builder saves choices and has a build() that checks them. Some chains are just handy helper methods.
  • "structuredClone copies everything." It copies plain data deeply, but a class object comes back without its prototype or its methods.

The table below gives a quick check for each mix-up. Read each row as: "if I believe this, I ask this question, and the better way to say it is this."

Each mix-up and a quick question to check it
The mix-upAsk yourselfA better way to say it
A factory and a class differ only in how they look.Does the code pick which object to return, or need private data in a closure?Factories and classes suit different jobs.
A singleton is the same as a global variable.Is the sharing on purpose, read-only, and easy to swap in tests?A singleton is one shared object that you control, but it is still shared.
A builder always makes code cleaner.Would one options object with defaults say the same thing?Use a builder when there are real steps and checks.
A deep copy keeps class methods.Does the copy still pass instanceof, and does it still have its methods?Copying the data and copying a class object are different things.

Practice exercises

5 EXERCISES
Exercise 1 · Warm-upPredict two factory counters

Type the three printed values in order.

Starter codePop out in the code editor (opens in a new tab)JavaScript
function makeBox(label) {
  let value = 0;
  return {
    add() {
      value += 1;
      return label + ":" + value;
    },
  };
}
const first = makeBox("cart");
const second = makeBox("cart");
first.add();
console.log(first.add());
console.log(second.add());
console.log(first === second);

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upFollow a factory's choice

    Read which object the factory picks, then type the exact text printed.

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    function createNotifier(channel) {
      return channel === "sms"
        ? { send: (message) => "sms: " + message }
        : { send: (message) => "email: " + message };
    }
    console.log(createNotifier("sms").send("Ready"));

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeBuild a URL

      Type the URL the builder prints.

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      function createUrlBuilder(path) {
        const params = [];
        return {
          param(name, value) {
            params.push(name + "=" + encodeURIComponent(value));
            return this;
          },
          build() {
            return path + "?" + params.join("&");
          },
        };
      }
      console.log(createUrlBuilder("/users").param("role", "admin user").build());

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeSpot the shallow spread

        Type the three printed values in order.

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        const original = { nested: { count: 1 }, tags: ["a"] };
        const copy = { ...original };
        copy.nested.count += 1;
        copy.tags = [...copy.tags, "b"];
        console.log(original.nested.count);
        console.log(original.tags.join(""));
        console.log(copy.tags.join(""));

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeChallenge: structuredClone and classes

          Type the two values the code prints.

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          class Widget {
            constructor(name) {
              this.name = name;
            }
            label() {
              return "widget:" + this.name;
            }
          }
          const copied = structuredClone(new Widget("menu"));
          console.log(copied instanceof Widget);
          console.log(typeof copied.label);

          Answer, then press Check. Spacing and letter case don’t matter.

            Check your understanding

            8 QUESTIONS
            Lesson quiz · 8 questionsScore: first tries count
            1. Question 1 of 8What is a design pattern, in this lesson?

              Choose an answer to see the explanation.

            2. Question 2 of 8What does the factory counter print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              function makeCounter() {
                let count = 0;
                return { next: () => ++count };
              }
              const a = makeCounter();
              const b = makeCounter();
              console.log(a.next());
              console.log(b.next());
              console.log(a === b);

              Choose an answer to see the explanation.

            3. Question 3 of 8What does this singleton code print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              let instance;
              function getLogger(name) {
                if (!instance) instance = { name };
                return instance;
              }
              const first = getLogger("app");
              const second = getLogger("test");
              console.log(first === second);
              console.log(second.name);

              Choose an answer to see the explanation.

            4. Question 4 of 8What does this builder print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              function makeBuilder() {
                const parts = [];
                return {
                  add(value) { parts.push(value); return this; },
                  build() { return parts.join("/"); },
                };
              }
              console.log(makeBuilder().add("api").add("users").build());

              Choose an answer to see the explanation.

            5. Question 5 of 8What does this revealing module print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const module = (() => {
                const secret = "inside";
                function reveal() { return secret; }
                return { reveal };
              })();
              console.log(module.reveal());
              console.log(module.secret);

              Choose an answer to see the explanation.

            6. Question 6 of 8What does this print when structuredClone copies a class object?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              class Point {
                constructor(x) { this.x = x; }
                double() { return this.x * 2; }
              }
              const copy = structuredClone(new Point(4));
              console.log(copy instanceof Point);
              console.log(typeof copy.double);

              Choose an answer to see the explanation.

            7. Question 7 of 8What is usually the simpler choice instead of a builder?

              Choose an answer to see the explanation.

            8. Question 8 of 8When should you think about an object pool?

              Choose an answer to see the explanation.

            Key takeaways

            • Patterns are names for common designs, not rules you must follow.
            • A factory returns a new object each time. It can keep private data in a closure, or pick which object to return.
            • A singleton is one shared object. Use it when sharing is the goal, and pass things in as arguments when tests need to swap them.
            • A builder makes something in steps and checks it in build(). An options object with defaults is the simpler choice.
            • Ways of copying differ in how deep they go and whether they keep the prototype. Check with real === and instanceof tests.

            Remember the one-liner.
            Pick the simplest way to make objects that keeps it clear what is new, what is shared, what is checked, and what is copied.

            Up next: Observer and pub/sub shows how one part of an app can react when another part changes, without the two knowing about each other. Later lessons cover more patterns, app state, and how to organize a whole app.

            CompleteFrontend Clear concepts. Working examples.