JavaScript (ES2026) Module 11 – Prototypes and classes
Constructor functions and the new operator
The four steps the new operator performs, how constructor functions and prototype methods were written before classes, and what goes wrong without new.
What you will learn
- Explain the four steps the new operator performs
- Read pre-class code that uses constructor functions and prototype methods
- Avoid calling constructors without new, and guard old constructors with new.target
- Identify which functions can be called with new, and how built-in functions treat it
Before you start
What you’ll learn · Before you start
What you will learn
- Explain the four steps the new operator performs
- Read pre-class code that uses constructor functions and prototype methods
- Avoid calling constructors without new, and guard old constructors with new.target
- Identify which functions can be called with new, and how built-in functions treat it
Before you start
On this page
A constructor function is an ordinary function written to be called with new. new Account('Asha') creates a
new object, links it to Account.prototype, runs Account with this set to that object so the function can fill
it in, and hands the object back, unless the function returned a different object instead. Methods are stored once,
on Account.prototype, and every object made this way inherits them through its prototype chain. Before ES2015 added
class, this was how JavaScript made many objects of one kind, and the pattern is still everywhere: in older
libraries, in code compiled for old browsers and in many answers you will find online. Classes perform the same steps
underneath. This lesson takes new apart step by step, measures why methods belong on the prototype, reads the
patterns of pre-class code, and reproduces what happens when someone forgets new.
What new does, in four steps
A function written with function, as a declaration or an expression (not a generator or an async function), gets
a prototype property when it is created: an object with a constructor property that points back to the function.
That object is waiting to become the prototype of whatever new makes. Here a constructor is called with new, and
then the same work is done by hand:
// A constructor function, called with new, and the same four steps written out by hand.
function Booking(passenger, seat) {
this.passenger = passenger;
this.seat = seat;
}
Booking.prototype.label = function () {
return `${this.passenger}, seat ${this.seat}`;
};
const withNew = new Booking('Anil', '14C');
// 1. Create a new, empty object.
// 2. Link it to Booking.prototype (that object becomes its [[Prototype]]).
const obj = Object.create(Booking.prototype);
// 3. Run Booking with this set to the new object.
const returned = Booking.call(obj, 'Anil', '14C');
// 4. The result is the new object, unless Booking returned an object of its own.
const isObject = (typeof returned === 'object' && returned !== null) || typeof returned === 'function';
const byHand = isObject ? returned : obj;
console.log(withNew.label(), '|', byHand.label());
console.log(Object.getPrototypeOf(withNew) === Booking.prototype, Object.getPrototypeOf(byHand) === Booking.prototype);
console.log(withNew instanceof Booking, byHand instanceof Booking);
console.log('Booking returned:', returned);
// Step 4 in action: an object that the constructor returns replaces the new one; a primitive is ignored.
function GivesObject() {
this.made = 'by new';
return { made: 'by the return statement' };
}
function GivesNumber() {
this.made = 'by new';
return 42;
}
console.log(new GivesObject().made, '|', new GivesNumber().made); Output
Anil, seat 14C | Anil, seat 14C true true true true Booking returned: undefined by the return statement | by new
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node what-new-does.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
What new does with a constructor function
Text description of the diagram
The diagram shows what happens when `new Booking('Anil', '14C')` runs, from top to bottom.
1. A new, empty object is created. 2. Its `[[Prototype]]` is set to `Booking.prototype`, so the object inherits the methods stored there. If `Booking.prototype` is not an object, `Object.prototype` is used instead. 3. `Booking` runs with `this` set to the new object and with the arguments `'Anil'` and `'14C'`, so its assignments to `this` fill the new object in. 4. If `Booking` returned an object, that object is the result of the whole `new` expression. If it returned nothing or a primitive value such as a number, the new object from step 1 is the result.
- Create a new, empty object.
- Link it: its
[[Prototype]]becomesBooking.prototype. That is why both objects pass theinstanceof Bookingtest and can calllabel(). IfBooking.prototypehas been replaced by something that is not an object, the new object getsObject.prototypeinstead. - Run
Bookingwiththisset to the new object and with the arguments. The assignments tothisfill the object in. The function itself returned nothing, which is whyBooking returned: undefinedis printed. - Return the new object, unless the function returned an object of its own. The last line shows both cases:
GivesObjectreturned an object, which replaced the new one, while the42fromGivesNumberwas ignored.
The specification describes the same sequence in the [[Construct]] method of a function, with
GetPrototypeFromConstructor for step 2. Returning a different object from a constructor is legal but surprises every
reader, so avoid it; ESLint’s no-constructor-return rule reports it in class constructors.
Methods belong on the prototype
A constructor can also create methods for each object inside its body, this.deposit = function (paise) { … }. That
works, but every call of the constructor then makes new function objects. This run counts what that costs in V8, the
engine of Node.js and Chrome, for 10,000 accounts:
// What it costs when every object gets its own copies of its methods, measured in V8, the engine of Node.js.
// v8.queryObjects() runs a full garbage collection and then counts the functions that are still reachable; the heap
// is read right after it, so both numbers describe live objects only.
import { queryObjects } from 'node:v8';
// Node.js 24 warns that queryObjects() is experimental, with the process number in the message. These two lines
// print the same warning without the number, so that every run prints the same text.
process.removeAllListeners('warning');
process.on('warning', (warning) => console.error(`${warning.name}: ${warning.message}`));
function AccountWithOwnMethods(owner) {
this.owner = owner;
this.balancePaise = 0;
this.deposit = function (paise) {
this.balancePaise += paise;
};
this.statement = function () {
return `${this.owner}: ${this.balancePaise} paise`;
};
}
function Account(owner) {
this.owner = owner;
this.balancePaise = 0;
}
Account.prototype.deposit = function (paise) {
this.balancePaise += paise;
};
Account.prototype.statement = function () {
return `${this.owner}: ${this.balancePaise} paise`;
};
const owners = [];
for (let i = 0; i < 10_000; i++) owners.push(`customer ${i}`);
function measure(label, Constructor) {
const accounts = new Array(owners.length);
const functionsBefore = queryObjects(Function, { format: 'count' });
const heapBefore = process.memoryUsage().heapUsed;
for (let i = 0; i < owners.length; i++) accounts[i] = new Constructor(owners[i]);
const functionsAfter = queryObjects(Function, { format: 'count' });
const bytesPerAccount = (process.memoryUsage().heapUsed - heapBefore) / accounts.length;
console.log(`${label}: ${functionsAfter - functionsBefore} new functions, about ${Math.round(bytesPerAccount)} bytes per account`);
return accounts;
}
const own = measure('methods made in the constructor', AccountWithOwnMethods);
const shared = measure('methods on the prototype ', Account);
console.log('own[0].deposit === own[1].deposit:', own[0].deposit === own[1].deposit);
console.log('shared[0].deposit === shared[1].deposit:', shared[0].deposit === shared[1].deposit); Output
methods made in the constructor: 20000 new functions, about 201 bytes per account methods on the prototype : 0 new functions, about 41 bytes per account own[0].deposit === own[1].deposit: false shared[0].deposit === shared[1].deposit: true
Printed as an error (standard error)
ExperimentalWarning: v8.queryObjects() is an experimental feature and might change at any time
This output changes from run to run: the heap sizes vary by a few bytes per account between runs
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node method-cost.mjs
The version that makes its methods in the constructor created exactly two functions per account, 20,000 in all, and in
the recorded run each of its accounts took about five times the memory of an account that shares its methods through
Account.prototype. The shared version created no functions at all, because deposit and statement exist once and
every account finds them on its prototype. Inside them, this is the account the method was called on, so one
function serves every object. Per-object functions still have their place: a function made inside the constructor can
keep private variables in a closure. For methods that only use this, the prototype is the right home.
Reading code written before classes
Older code puts the pieces next to each other by hand: the constructor, then one assignment per method on its prototype, and “static” helpers as properties of the function itself. Here is one account type written that way and again as a class:
// One account type written twice: as a constructor function (how code was written before ES2015) and as a class.
// --- The constructor-function version ---
function Account(owner, balancePaise) {
this.owner = owner;
this.balancePaise = balancePaise;
}
Account.prototype.deposit = function (paise) {
this.balancePaise += paise;
return this;
};
Account.prototype.toString = function () {
return `${this.owner}: Rs ${(this.balancePaise / 100).toFixed(2)}`;
};
Account.fromRupees = function (owner, rupees) {
return new Account(owner, Math.round(rupees * 100)); // a "static" helper: a property of the function
};
// --- The class version ---
class AccountClass {
constructor(owner, balancePaise) {
this.owner = owner;
this.balancePaise = balancePaise;
}
deposit(paise) {
this.balancePaise += paise;
return this;
}
toString() {
return `${this.owner}: Rs ${(this.balancePaise / 100).toFixed(2)}`;
}
static fromRupees(owner, rupees) {
return new AccountClass(owner, Math.round(rupees * 100));
}
}
const a = Account.fromRupees('Meera', 1250.5).deposit(4950);
const b = AccountClass.fromRupees('Meera', 1250.5).deposit(4950);
console.log(String(a), '|', String(b));
console.log(typeof Account, typeof AccountClass);
console.log(Object.keys(a), Object.keys(b)); // own properties: the same
// Differences that show up in real code:
const forIn = (obj) => {
const keys = [];
for (const key in obj) keys.push(key);
return keys.join(', ');
};
console.log('for...in, function version:', forIn(a)); // methods assigned to the prototype are enumerable
console.log('for...in, class version: ', forIn(b)); // class methods are not
const flags = (C) => Object.getOwnPropertyDescriptor(C, 'prototype').writable;
console.log('prototype can be replaced:', flags(Account), flags(AccountClass)); Output
Meera: Rs 1300.00 | Meera: Rs 1300.00 function function [ 'owner', 'balancePaise' ] [ 'owner', 'balancePaise' ] for...in, function version: owner, balancePaise, deposit, toString for...in, class version: owner, balancePaise prototype can be replaced: true false
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node legacy-account.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
Both versions build the same objects, with the same own properties, and typeof calls both a function. The
differences show up at the edges:
- Methods assigned to
Account.prototypeare enumerable, so afor…inloop over an account listsdepositandtoStringnext to the data. Methods written in a class body are not. - The function’s
prototypeproperty is writable, so old code can, and sometimes does, replace the whole object. A class’sprototypeproperty is read-only.
Replacing the prototype is the pattern that causes the most confusion when you read old code:
// Old code often replaced a constructor's whole prototype with an object literal. Two things change.
function Parcel(weightKg) {
this.weightKg = weightKg;
}
const early = new Parcel(2); // made while Parcel.prototype is still the original object
Parcel.prototype = {
charge() {
return 40 + 15 * Math.ceil(this.weightKg);
},
};
const late = new Parcel(3.2);
console.log('late.charge():', late.charge());
// 1. The literal has no constructor property, so the one inherited from Object.prototype shows through.
console.log('late.constructor === Parcel:', late.constructor === Parcel, '| it is', late.constructor.name);
// 2. Objects made earlier keep the old prototype: they lost the method, and instanceof now says no.
console.log('typeof early.charge:', typeof early.charge, '| early instanceof Parcel:', early instanceof Parcel);
// The usual repair in such code: put constructor back, as a non-enumerable property like the original one.
Object.defineProperty(Parcel.prototype, 'constructor', { value: Parcel, writable: true, configurable: true });
console.log('after the repair:', new Parcel(1).constructor === Parcel); Output
late.charge(): 100 late.constructor === Parcel: false | it is Object typeof early.charge: undefined | early instanceof Parcel: false after the repair: true
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node replaced-prototype.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
The new literal has no constructor property, so late.constructor falls through to the one on Object.prototype.
Worse, early, made before the replacement, still points to the old prototype: it lost charge(), and early instanceof Parcel is now false, because instanceof looks for the current Parcel.prototype in the object’s
chain. Code that replaced prototypes usually put constructor back, as the last lines do, and had to run before any
object was made. The same era also built inheritance by hand, with Parent.call(this, …) inside the child constructor
and Child.prototype = Object.create(Parent.prototype); the lesson on extends in this module puts that pattern next
to a class.
Forgetting new
A constructor function can be called like any other function, and without new nothing creates an object for it.
What happens then depends on the code around it:
// One mistake, three outcomes: a constructor called without new.
// A constructor in sloppy code, the way old classic scripts were written. The Function constructor makes sloppy code
// unless the text starts with "use strict", so it stands in for such a script here.
// eslint-disable-next-line no-new-func -- the example needs a function compiled as sloppy code
const SloppyWallet = new Function('owner', 'this.owner = owner; this.balance = 0;');
const sloppy = SloppyWallet('Asha'); // no new: this is the global object
console.log('sloppy:', sloppy, '| globalThis.owner is now', globalThis.owner);
delete globalThis.owner; // remove the two globals the call created
delete globalThis.balance;
// The same constructor in strict code (every module is strict): this is undefined, so its first line throws.
function StrictWallet(owner) {
this.owner = owner;
this.balance = 0;
}
try {
StrictWallet('Asha');
} catch (err) {
console.log('strict:', `${err.name}: ${err.message}`);
}
// A class refuses the call before its body runs.
class ClassWallet {
constructor(owner) {
this.owner = owner;
this.balance = 0;
}
}
try {
ClassWallet('Asha');
} catch (err) {
console.log('class: ', `${err.name}: ${err.message}`);
}
// new.target is the function that new was used with, and undefined in a plain call: a guard for old constructors.
function GuardedWallet(owner) {
if (!new.target) throw new TypeError('GuardedWallet must be called with new');
this.owner = owner;
this.balance = 0;
}
try {
GuardedWallet('Asha');
} catch (err) {
console.log('guard: ', `${err.name}: ${err.message}`);
}
console.log('with new:', new GuardedWallet('Asha').owner); Output
sloppy: undefined | globalThis.owner is now Asha strict: TypeError: Cannot set properties of undefined (setting 'owner') class: TypeError: Class constructor ClassWallet cannot be invoked without 'new' guard: TypeError: GuardedWallet must be called with new with new: Asha
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node without-new.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
Version note
When you press Run, the QuickJS engine reports the two engine errors in its own words: cannot set property 'owner' of undefined for the strict function and class constructors must be invoked with 'new' for the class.
In your browser, QuickJS (quickjs-emscripten 0.32.0) prints:
sloppy: undefined | globalThis.owner is now Asha strict: TypeError: cannot set property 'owner' of undefined class: TypeError: class constructors must be invoked with 'new' guard: TypeError: GuardedWallet must be called with new with new: Asha
- In sloppy code, the old default of classic scripts, a plain call sets
thisto the global object. The constructor’s assignments quietly created two global variables,ownerandbalance, and the call returnedundefined. In a browser the global object iswindow, so a constructor that setsthis.nameoverwriteswindow.name, the name of the browser window. - In strict code, which includes every module and every class,
thisisundefinedin a plain call, so the first assignment throws aTypeError. That is a better outcome: the bug shows up on the line that has it. - A class constructor refuses a plain call before any of its code runs, with a
TypeError.
new.target, added in ES2015, tells a function how it was called: it holds the function that new was used with,
and undefined in a plain call. Libraries written before it used the test this instanceof Wallet for the same job,
either to throw, as GuardedWallet does, or to repair the call with return new Wallet(owner). Throwing is clearer,
and a class does it for you. The other defence is a naming convention: constructors start with a capital letter and
ordinary functions do not, so a call such as Wallet('Asha') looks wrong at a glance, and ESLint’s new-cap rule
checks the convention.
Which functions can be called with new?
Only functions that have a [[Construct]] method are constructors. Reflect.construct throws when its third argument
is not one, which gives a way to ask without running anything:
// Which functions can be called with new? Reflect.construct(String, [], F) throws a TypeError when F is not a
// constructor, so it answers the question without running F.
function canConstruct(fn) {
try {
Reflect.construct(String, [], fn);
return true;
} catch {
return false;
}
}
const shop = {
method() {},
};
class Basket {}
const kinds = {
'function declaration': function plain() {},
'arrow function': () => {},
'method in an object literal': shop.method,
'class': Basket,
'async function': async function load() {},
'generator function': function* ids() {},
'bound function': function plain() {}.bind(null),
};
for (const [kind, fn] of Object.entries(kinds)) {
console.log(`${kind.padEnd(28)} ${canConstruct(fn) ? 'yes' : 'no'}`);
}
// Built-in functions decide for themselves what a call with and without new means.
console.log(typeof Date(), typeof new Date(0)); // a string, then a Date object
console.log(typeof String(42), typeof new String(42)); // a primitive, then a wrapper object
try {
// eslint-disable-next-line no-new-native-nonconstructor -- the TypeError is what the line shows
new Symbol('id');
} catch (err) {
console.log('new Symbol:', err.name);
} Output
function declaration yes arrow function no method in an object literal no class yes async function no generator function no bound function yes string object string object new Symbol: TypeError
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node constructible.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
Function declarations and expressions, classes, and functions made from them with bind() are constructors. Arrow
functions, methods written with the shorthand syntax, async functions and generators are not: new with one of them
throws TypeError: … is not a constructor. Arrow functions, shorthand methods and async functions do not even have
a prototype property; a generator function has one, but it serves the generator objects the function returns, not
new.
Built-in functions decide for themselves what a call with or without new means. Date() returns the current time
as a string, while new Date() makes a Date object. String(42) converts to a primitive string, while
new String(42) makes a wrapper object, whose typeof is 'object' and which is truthy even when it wraps '', so
avoid it; ESLint’s no-new-wrappers rule reports it. Symbol and BigInt refuse new with a TypeError.
Constructor function or class?
| Criterion | Constructor function | Class |
|---|---|---|
| Called without new | Runs: sloppy code creates globals, strict code throws | Always a TypeError |
| Methods added to the prototype | Enumerable when assigned, so for…in lists them | Not enumerable |
| The prototype property | Writable: it can be replaced | Read-only |
| Used before its declaration | Works: function declarations are hoisted | ReferenceError until the class has been defined |
| Body | Sloppy unless the code is strict | Always strict |
| When to choose | Reading and maintaining older code, or code that must run unchanged in very old engines | New code: the same objects and prototype methods, with fewer traps, plus fields, private members and extends |
Exercise: write new yourself
Exercise · Medium · JavaScript
Write your own new as a function
Write myNew(Constructor, ...args), a function that does what new Constructor(...args) does for an ordinary constructor function, without using new or Reflect.construct.
- The result is linked to
Constructor.prototype:Object.getPrototypeOf(myNew(F)) === F.prototype, soinstanceofand inherited methods work. WhenConstructor.prototypeis not an object (someone replaced it with a number, say), link the result toObject.prototypeinstead, asnewdoes. Constructorruns once, withthisset to the new object and with every argument passed on.- If
Constructorreturns an object, and functions count as objects here, that object is the result. Any other return value (a number, a string,undefinedornull) is ignored, and the new object is the result. - If
Constructoris not a function, throw aTypeError.
function Ticket(from, to) {
this.route = `${from}-${to}`;
}
Ticket.prototype.label = function () {
return `Ticket ${this.route}`;
};
const t = myNew(Ticket, 'Pune', 'Goa');
t.label(); // 'Ticket Pune-Goa'
t instanceof Ticket; // true
The starter code runs the constructor on a plain object and returns that object. Run the sample tests to see what it misses.
Starter code · my-new.mjs
// myNew(Constructor, ...args): what new Constructor(...args) does, written as a function.
// This version runs the constructor on a plain object, so the result inherits nothing from Constructor.prototype.
export function myNew(Constructor, ...args) {
const obj = {};
Constructor.apply(obj, args);
return obj;
} The sample tests · my-new.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { myNew } from './my-new.mjs';
function Ticket(from, to) {
this.route = `${from}-${to}`;
}
Ticket.prototype.label = function () {
return `Ticket ${this.route}`;
};
test('the result is linked to Constructor.prototype', () => {
const ticket = myNew(Ticket, 'Pune', 'Goa');
assert.equal(Object.getPrototypeOf(ticket), Ticket.prototype);
assert.ok(ticket instanceof Ticket);
assert.equal(ticket.label(), 'Ticket Pune-Goa');
});
test('the constructor runs once, with this and every argument', () => {
let calls = 0;
let seen;
function Probe(...args) {
calls += 1;
seen = { self: this, args };
}
const result = myNew(Probe, 1, 'two', [3]);
assert.equal(calls, 1);
assert.equal(seen.self, result);
assert.deepEqual(seen.args, [1, 'two', [3]]);
});
test('an object returned by the constructor replaces the new one', () => {
const shared = { kind: 'cached' };
function Cached() {
this.kind = 'new';
return shared;
}
assert.equal(myNew(Cached), shared);
});
test('a primitive or null returned by the constructor is ignored', () => {
function GivesNumber() {
this.kind = 'new';
return 42;
}
function GivesNull() {
this.kind = 'new';
return null;
}
assert.equal(myNew(GivesNumber).kind, 'new');
assert.equal(myNew(GivesNull).kind, 'new');
});
test('something that is not a function throws a TypeError', () => {
assert.throws(() => myNew({}), TypeError);
assert.throws(() => myNew('Ticket'), TypeError);
}); A hint
Make the object with Object.create(proto), where proto is Constructor.prototype when that is an object or a function and Object.prototype otherwise. Run the constructor with Constructor.apply(obj, args) and keep what it returns. Remember that typeof null is 'object': check for null before you decide that the constructor returned an object.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (QuickJS): nothing is sent to mysmartcopilot.com. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Self-check
Check yourself
5 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
Interview questions
Fresher · What does the new operator do?
new F(...args) performs four steps. It creates a new, empty object; it sets that object’s prototype to
F.prototype, or to Object.prototype when F.prototype is not an object; it calls F with this bound to the
new object and with the arguments; and it returns the new object, unless F returned an object of its own, which then
replaces it, while a primitive return value is ignored. Because of the second step, methods stored on F.prototype
are shared by every object that new F() creates, and obj instanceof F is true. Only constructors can be used with
new: ordinary functions and classes can, while arrow functions, methods, async functions and generators throw a
TypeError. Inside the function, new.target is F; in a plain call it is undefined.
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Fresher to mid
A classic script on a shop page defines function Tracker(name) { this.name = name; this.events = []; }. One call site reads const t = Tracker('checkout'). That line throws nothing, but the next, t.events.push(e), throws a TypeError, and window.name is now 'checkout'. Explain both symptoms, fix the code and stop it happening again.
-
Mid to senior
A data grid makes one Row object per record with a constructor that defines six methods inside its body, such as this.format = function () { … }. With 200,000 records, a heap snapshot shows about 1.2 million function objects, and the tab uses far more memory than the data needs. Explain why, estimate the cost, and fix it.
Key takeaways
new F(args)creates an object, links it toF.prototype, runsFwiththisset to it, and returns it, unlessFreturns another object. A primitive return value is ignored.- A function declared with
functionhas aprototypeobject with aconstructorproperty; methods stored there are shared by every instance, while methods made inside the constructor cost new functions for every object. - Pre-class code assigns methods to
F.prototype(enumerable), adds helpers as properties ofF, and sometimes replacesF.prototype, which losesconstructorand breaksinstanceoffor objects made earlier. - Without
new, a constructor in sloppy code writes to the global object; in strict code it throws aTypeError, and a class always throws.new.targetand capitalised names help catch the mistake. - Arrow functions, shorthand methods, async functions and generators cannot be called with
new. Built-ins such asDateandStringgive different results with and without it; avoid wrapper objects.
References
- ECMAScript 2026 Language Specification: the new operator (Ecma International)
- ECMAScript 2026 Language Specification: [[Construct]] of ECMAScript function objects (Ecma International)
- ECMAScript 2026 Language Specification: GetPrototypeFromConstructor (Ecma International)
- ECMAScript 2026 Language Specification: MakeConstructor (Ecma International)
- ECMAScript 2026 Language Specification: [[Call]] of ECMAScript function objects (Ecma International)
- ECMAScript 2026 Language Specification: OrdinaryCallBindThis (Ecma International)
- ECMAScript 2026 Language Specification: meta properties (new.target) (Ecma International)
- new (MDN Web Docs)
- new.target (MDN Web Docs)
- Node.js v24 documentation: v8.queryObjects() (OpenJS Foundation (Node.js))
- ESLint rule: new-cap (OpenJS Foundation (ESLint))
- ESLint rule: no-new-wrappers (OpenJS Foundation (ESLint))
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress