In JavaScript, a class body is strict-mode code even inside a plain non-module script that never wrote 'use strict'. What concretely changes for code inside the class, and what tends to break when an old constructor function is rewritten as a class?
answer
- no opt-out, and no directive needed
- silent failures turn into throws
- undeclared assignment stops making a global
- lexical, so callees are unaffected
basics
~20 sEvery part of a class definition is strict code with no opt-out, so undeclared assignments throw ReferenceError, previously silent write failures become TypeErrors, and legacy sloppy idioms are rejected outright — even when the surrounding script never opted in.
solid answer
~50 sThe whole class definition — constructor, prototype and static methods, getters and setters, field initializers and static blocks — is strict-mode code, and there is no way to opt out. The changes that bite in practice: assigning to an undeclared identifier throws a `ReferenceError` instead of quietly creating a global; writing to a non-writable or frozen property, or to a getter with no setter, throws a `TypeError` instead of failing silently; an unbound call sees `this` as `undefined` rather than the global object; `arguments` no longer aliases the named parameters; `arguments.callee` and `fn.caller` throw; and duplicate parameter names, octal literals like `0644`, `with`, and `delete someVariable` are outright `SyntaxError`s. Strictness is lexical, not dynamic: a sloppy helper declared elsewhere stays sloppy when a class method calls it, while a callback written inside the class body is strict.
code
javascript · 16 lines// In a classic script with no 'use strict' anywhere.
function legacy() { leaked = 1; return leaked; }
class Service {
run() {
console.log(legacy()); // 1 - still sloppy, creates a global
try { alsoLeaked = 1; } // strict: class bodies always are
catch (e) { console.log(e.name); } // ReferenceError
const cfg = Object.freeze({ n: 1 });
try { cfg.n = 2; }
catch (e) { console.log(e.name); } // TypeError
}
}
new Service().run();go deeper
Know that code written inside a class is always strict, so a typo that would have created a global variable throws an error instead.
List the concrete differences — implicit globals, silent write failures, arguments aliasing, callee and caller, and the parse-time rejections — and say that no directive is needed or possible.
Explain that strictness is lexical, so converting a class hardens only its own body, and walk through what breaks in existing callers when a constructor function becomes a class.
Own the migration strategy: move files to strict or module scope first so sloppy-mode failures are attributed separately, and set a codebase policy that keeps strictness uniform rather than per-construct.
## Where the strictness comes from The language specifies that all parts of a class definition are strict-mode code. That covers more than you might assume: the constructor, every prototype and static method, getters and setters, computed key expressions, field initializers, static initialization blocks, and the heritage clause. There is no directive to turn it off — writing `'use strict'` inside is redundant, and there is no `'use sloppy'`. A class defined in the middle of a legacy script is an island of strict code inside sloppy code. ## What actually changes **Silent failures become throws.** ```js class Counter { bump() { total = (total || 0) + 1; } // ReferenceError: total is not defined } ``` In sloppy code that line would have created a global named `total`. Inside a class it throws — usually the single most valuable difference, because accidental globals are a classic source of cross-module interference. Similarly, writes that used to fail quietly now throw a `TypeError`: assigning to a frozen or non-writable property, adding a property to a non-extensible object, assigning to a getter-only accessor, or assigning to `NaN`/`undefined`. ```js const cfg = Object.freeze({ retries: 3 }); class Runner { setup() { cfg.retries = 5; } } // TypeError in strict code ``` **`this` on unbound calls is `undefined`.** A method pulled off its object and called bare gets `undefined` rather than the global object, so the failure surfaces immediately as a `TypeError` on the first property access instead of silently reading or writing globals. (The binding rules themselves are a separate topic; the strict-mode part is only that there is no substitution of the global object.) **`arguments` stops aliasing parameters.** In sloppy functions, `arguments[0]` and the first named parameter are two views of one storage; writing one changes the other. In strict code they are decoupled. Old code that normalized parameters by writing into `arguments` silently stops working. **Legacy introspection throws.** `arguments.callee`, `fn.caller` and `fn.arguments` throw `TypeError` in strict code — which kills a whole family of older recursion and stack-walking tricks. **Some things become syntax errors**, so they fail at parse time rather than at run time: duplicate parameter names, legacy octal literals (`0644` — use `0o644`), `with`, `delete x` on an unqualified identifier, and using `eval` or `arguments` as an assignment target or binding name. `eval` also no longer leaks declarations into the surrounding scope. ## Strictness is lexical, not dynamic This is the part candidates most often get wrong. Strictness is a property of where code is *written*, not of who calls it: ```js function legacy() { leaked = 1; } // sloppy: defined in sloppy code class Service { run() { legacy(); // still sloppy - creates a global, no throw const inner = () => { alsoLeaked = 1; }; // strict - written inside the class inner(); // ReferenceError } } ``` So converting a class does not retroactively harden the functions it calls, and a callback you write inside a class body is strict even when it is later invoked from sloppy code. ## What breaks when porting a constructor function to a class A rewrite from `function Widget() {}` to `class Widget {}` changes several observable things at once, and the failures land in *callers*, not in the class: - **Sloppy tricks in the old constructor now throw.** Undeclared assignments and frozen-object writes that were silently tolerated become errors on the first call. - **Callers that omitted `new`** get an immediate `TypeError`, and code that borrowed the constructor with `Widget.call(this, ...)` stops working. - **Helpers that copied the prototype** with `for...in` or `Object.assign` now copy nothing, because class methods are non-enumerable. - **`Widget.prototype = { ... }` wholesale replacement** fails: a class's `prototype` property is non-writable, so it throws in strict code and silently does nothing in sloppy code. - **Ordering.** The class must be evaluated before first use; a call that used to run above the old function declaration now hits an uninitialized binding. ## How to approach the migration Convert one constructor at a time and run the code paths that construct and consume it, since none of these failures are visible until execution reaches them. Enabling strict mode across the file *first* — or moving the file to a module, where the whole file is strict — surfaces the sloppy-mode failures separately from the class-specific ones, which makes each error far easier to attribute. Grep for the old call sites: bare `Widget(` without `new`, `.call(` with the constructor, and any prototype-copying utility.
- If a class method calls a helper defined in a sloppy script, is the helper strict?No. Strictness is fixed lexically where the function's source appears, so the helper stays sloppy however it is invoked — it can still create implicit globals and swallow failed writes. Only code written inside the class definition is strict, including arrow functions and callbacks declared in a method body.
- Does putting the file in a module make the class-related differences disappear?It removes the sloppy-versus-strict half: module code is strict, so the file's other functions already behave that way and the class introduces no new strictness. What remains is class-specific — construction without `new` throwing, non-enumerable prototype methods, a non-writable `prototype` property, and the temporal dead zone before the declaration.
- How do you find the sloppy-mode dependencies before converting a file?Add `'use strict'` at the top of the file (or convert it to a module) and run its tests first. Every implicit global, frozen-object write, `arguments` aliasing trick and `arguments.callee` use fails then, separately from the class conversion, so each error points at one cause instead of two overlapping ones.
saying these in an interview costs you the question
- Claims a class body needs its own 'use strict' directive
- Says strictness propagates at run time to called functions
- Thinks strict mode only changes how this is bound
- Expects an undeclared assignment inside a method to create a global
- Assumes a frozen-object write still fails silently inside a class