JavaScript identifier resolution is normally static — you can tell from the source which binding a name refers to. Which two legacy constructs break that guarantee, how does each one do it, and what do engines and minifiers do in response?
answer
- names normally decidable from source
- one splices an object into the chain
- one can declare into the caller
- strict mode removes both powers
- tools stop renaming and deoptimize
basics
~20 sThe with statement and non-strict direct eval break it. with pushes an object onto the scope chain so lookups depend on that object's properties at runtime, and direct eval can add bindings to the calling scope. Both defeat static analysis, so engines deoptimize and minifiers stop renaming.
solid answer
~50 sNormally every free identifier can be matched to a binding by reading the source, which is what lets engines pre-resolve variable accesses and lets minifiers rename locals. Two constructs remove that certainty. `with (obj) { ... }` pushes an *object* environment record onto the scope chain, so whether `x` inside the block means `obj.x` or an outer variable depends on `obj`'s properties — including inherited ones — at the moment the lookup runs. Direct `eval(src)` in non-strict code runs `src` in the calling context and can introduce new `var` bindings into that function's variable environment. In both cases the set of names in scope is unknown until runtime, so engines fall back to slow dynamic lookup for the affected function and minifiers refuse to rename anything in it. Strict code bans `with` outright and gives direct `eval` its own environment for its declarations. Indirect eval — `(0, eval)(src)` — always evaluates in global scope, so it never touches local bindings.
code
javascript · 11 lines// non-strict script: `with` is a SyntaxError in strict code and modules
const x = 'from scope';
const obj = { x: 'from object' };
with (obj) {
console.log(x); // "from object" — resolved against obj at runtime
}
delete obj.x;
with (obj) {
console.log(x); // "from scope" — same source, different binding
}go deeper
Know that with is banned in modern code and that eval is avoided; you are not expected to explain the environment-record mechanics at this level, but do not claim they are merely style preferences.
Explain the mechanism for each: with splices an object environment record into the scope chain, so lookups depend on runtime properties; non-strict direct eval can declare into the calling function's environment. Name what strict code does to each.
Connect the language rule to the toolchain: pre-resolved variable slots and minifier renaming both rest on static resolution, so these constructs cost performance and bundle size, not just taste. Distinguish direct from indirect eval and know which one leaves scopes intact.
Own the policy. Decide where runtime code evaluation is permitted at all, given both the deoptimization and the injection surface, and what the sanctioned alternative is — indirect eval, the Function constructor, or a data-driven interpreter — plus how you enforce that with lint and CSP-level controls.
## What "static resolution" buys Because JavaScript scoping is lexical, the binding every free identifier refers to can normally be determined from the source text alone. That property is worth real money: - **Engines** can resolve a variable access to a fixed slot in an environment record — or keep the value in a register entirely — instead of doing a name lookup along a chain at every access. - **Minifiers** can rename a function's locals to single letters, because they can prove which occurrences of a name refer to that binding. - **Humans and linters** can tell whether a name is a typo, an import, or a shadowed outer variable without running anything. Two constructs in the language take that certainty away. ## `with`: an object on the scope chain `with (obj) { ... }` creates an **object environment record** whose "bindings" are the properties of `obj`, and splices it into the scope chain in front of the enclosing environment for the duration of the block: ```js // non-strict script const x = 'from scope'; const obj = { x: 'from object' }; with (obj) { console.log(x); // "from object" } ``` What makes this unanalysable is that an object's property set is a runtime fact. Add or delete a property on `obj` — or on something in `obj`'s prototype chain, because inherited properties count too — and the same source line resolves to a different binding. There is no way to decide statically what `x` means inside that block. The language later added a narrow escape hatch: `Symbol.unscopables`. An object may carry a `[Symbol.unscopables]` object whose truthy-valued keys are hidden from `with` lookups. `Array.prototype[Symbol.unscopables]` uses this so that newer array methods do not shadow same-named variables inside `with (someArray)` in old code. That the committee needed such a mechanism at all is the clearest evidence of how disruptive `with` is. Strict code bans `with` as a **SyntaxError**, so it cannot appear in an ES module or a class body at all. ## Direct `eval`: new bindings in your scope A *direct* eval — a call written literally as `eval(...)` where `eval` resolves to the built-in — runs its argument in the caller's execution context. In non-strict code that means the evaluated source can both read the caller's locals and *declare into* the caller's variable environment: ```js // non-strict script function f() { var local = 'inner'; console.log(eval('local')); // "inner" — sees f's scope eval('var added = 1;'); console.log(added); // 1 — eval declared into f } f(); ``` So a static analyser reading `f` cannot even enumerate `f`'s variables, let alone rename them. Any function containing a direct eval is treated as opaque. Strict code softens this: a direct eval in strict code still *reads* and *writes* existing outer bindings, but its own declarations go into a fresh environment that disappears when the eval returns, so it cannot inject new names into the caller. ## Indirect eval is a different function Calling eval through any expression that is not the literal `eval` reference — `(0, eval)(src)`, `const e = eval; e(src)`, `globalThis.eval(src)` — is an **indirect** eval, and it is specified to evaluate the source as *global* code: ```js var top = 'global'; function f() { var local = 'inner'; console.log(eval('local')); // "inner" — direct const g = eval; try { g('local'); } catch (e) { console.log(e.constructor.name); // "ReferenceError" — indirect, no access to local } console.log(g('top')); // "global" } f(); ``` Because indirect eval never touches local scope, it does not deoptimize the surrounding function the way direct eval does. If you truly must evaluate source at runtime, the indirect form (or a `Function` constructor, which likewise only closes over global scope) is the version that leaves your scopes intact. ## The cost, concretely For a function containing `with` or a direct eval, an engine cannot pre-bind variable accesses; it keeps a real, inspectable environment and performs dynamic name lookup, and it typically declines the optimisations that assume a known variable set. Minifiers detect the same constructs and bail out of renaming for that scope — some tools warn about exactly this, because one stray `eval` can noticeably inflate a bundle by preventing name mangling in a large function. There is a second, separate cost worth naming in an interview: evaluating source built from input is a code-injection vector. But the scope argument stands on its own even for source you fully control. ## What to say The strong answer names both constructs, explains the *mechanism* for each (object environment record spliced into the chain; declarations injected into the caller's variable environment), states what strict code does to each, distinguishes direct from indirect eval, and closes with the consequence: the language's static-resolution guarantee is what engines and tools monetise, and these two constructs are the only places it is surrendered.
- Does the `Function` constructor have the same scope problem as direct eval?No. `new Function('a', 'return a + shared;')` creates a function whose scope is the global environment only — it cannot see the locals of the code that built it, and it cannot declare into them. That makes it analysable and non-deoptimizing, which is why it is preferred over eval when runtime code generation is genuinely required, though the code-injection risk is unchanged.
- Why did the committee add `Symbol.unscopables` rather than simply removing `with`?Because `with` could not be removed from non-strict code without breaking existing pages. `Symbol.unscopables` was a targeted fix: adding new methods to `Array.prototype` would otherwise shadow same-named variables inside `with (arr)` blocks in old scripts, so the prototype lists those method names as unscopable and `with` skips them during lookup.
- How does an engine detect that it must deoptimize a function's variable access?It sees the constructs at parse time. A `with` statement is syntax, and a direct `eval` is a call whose callee is literally the identifier `eval`, so the parser flags the enclosing scope as requiring a real, dynamically-searchable environment. Nothing about the runtime values matters — the presence of the construct is enough, which is why one unused direct eval taxes the whole function.
saying these in an interview costs you the question
- Says eval is slow but cannot explain the scope effect
- Treats (0, eval)(src) as identical to eval(src)
- Claims strict mode makes direct eval harmless
- Says with only changes this, not name lookup
- Thinks minifiers can still rename locals around eval