In an ES module, `import * as ns from './m.js'` gives you `ns`. What kind of object is `ns` — can you add, delete or overwrite properties on it, and what is its prototype?
answer
- not a plain object you could build
- the engine owns its shape
- nothing inherited from Object.prototype
- structural changes are refused
- values move, keys never do
basics
~20 sns is a module namespace object: non-extensible, with one non-configurable property per export whose value is a live view of the exporter's variable. Adding, deleting or overwriting properties is rejected, and its prototype is null.
solid answer
~40 s`ns` is a module namespace object — an exotic object the engine creates for the module, not a plain object you could have built. It has exactly one own property per exported name, plus `default` if the module has a default export, and each property reads the exporter's current variable, so it is live in the same way a named import is. It is not extensible and its properties are non-configurable, so `ns.extra = 1`, `ns.foo = 2` and `delete ns.foo` all fail — and because module code is strict, they throw `TypeError` rather than failing quietly. Its `[[Prototype]]` is `null`, so it inherits nothing from `Object.prototype`; `ns.hasOwnProperty` is `undefined` and you need `Object.hasOwn(ns, 'foo')` instead. It also carries a `Symbol.toStringTag` of `"Module"`, so `Object.prototype.toString.call(ns)` returns `"[object Module]"`.
go deeper
Know that import * as ns gives one object with a property per export, including default, and that you cannot add anything to it. Say that reading ns.foo reads the module's current value.
Explain the specifics: an exotic object, non-extensible with non-configurable properties, null prototype so Object.prototype helpers are unavailable, Symbol.toStringTag of "Module", and live property reads.
Show you know the consequences in real code — namespace objects cannot be monkey-patched, so test seams must be designed in; Object.hasOwn replaces hasOwnProperty; and spreading a namespace gives a copy that is mutable but no longer live.
Frame the sealing and null prototype as an integrity guarantee: the public surface of a module is fixed by its source text and cannot be tampered with at runtime, which is what lets tooling and other modules reason about the graph statically.
## What you actually получили `import * as ns from './m.js'` does not build a plain object out of the module's exports. It hands you the **module namespace object**, an exotic object the specification defines with custom internal behaviour. One namespace object exists per module per realm, so two different files doing `import * as ns` from the same module receive the identical object. ## Its shape - **One own property per export**, named exactly as exported. A default export appears as the property `default`. - **Own keys are the exported names sorted in code-unit order**, followed by the symbol keys. Unlike a plain object, the order does not reflect declaration or insertion order. - **`Symbol.toStringTag` is `"Module"`**, so `Object.prototype.toString.call(ns)` gives `"[object Module]"` — a reliable way to tell a namespace object from a plain one. - **`typeof ns` is `"object"`**, and it is not callable. ## Its prototype is null The namespace object's `[[Prototype]]` is `null`, which trips people up: ```js import * as ns from './m.js'; Object.getPrototypeOf(ns); // null ns.hasOwnProperty('foo'); // TypeError: ns.hasOwnProperty is not a function Object.hasOwn(ns, 'foo'); // works Object.prototype.hasOwnProperty.call(ns, 'foo'); // also works ``` A null prototype means no inherited `toString`, `valueOf` or `hasOwnProperty`, and also means no export name can ever be shadowed by an `Object.prototype` member. ## It is sealed, and writes throw The object is not extensible, and the properties for exported names are non-configurable, so every structural change fails: ```js ns.extra = 1; // TypeError — not extensible ns.foo = 2; // TypeError — writes to exported names are rejected delete ns.foo; // TypeError — the property is non-configurable Object.isExtensible(ns); // false ``` Module code is always strict, so these are thrown errors rather than silent no-ops. Reading a name that is *not* exported is different: `ns.missing` simply evaluates to `undefined`, because it is a plain property read of an absent property. (A static `import { missing }` is a different story entirely — that fails when the graph is linked.) ## The one odd detail Although writes are always rejected, the property descriptors report `writable: true` (with `enumerable: true, configurable: false`). That looks contradictory until you see the reason: a non-writable, non-configurable data property is supposed to be immutable forever, and the namespace's values are *live* — they change when the exporter reassigns the variable. Reporting the property as writable keeps the object honest about the fact that its values can change, while the object's own `[[Set]]` behaviour still refuses every write. A practical consequence: `Object.isSealed(ns)` is `true` but `Object.isFrozen(ns)` is `false` for a module with exports, even though you cannot change anything through it. ## Liveness Each property read on the namespace goes to the exporter's variable at that moment, exactly like a named import: ```js // m.js export let count = 0; export function bump() { count++; } ``` ```js import * as ns from './m.js'; ns.bump(); console.log(ns.count); // 1 ``` So `ns` is a *view* of the module, not a snapshot of it. What it is not is dynamic in shape: the export list is fixed by the module's source text, so no property ever appears or disappears at runtime. ## Why the design is like this Sealing and the null prototype make the namespace a trustworthy description of the module's public surface: nobody can monkey-patch an export onto another module's namespace, tools can rely on the key set matching the source text, and there is no prototype chain through which a name could be faked. That rigidity is also why namespace objects are awkward as ad-hoc containers — if you need a mutable bag of the module's values, build your own object from it (`{ ...ns }`), which copies the current values and is, of course, no longer live. ## Interview shape The crisp answer is three facts: it is an exotic object created by the engine, it is sealed with a null prototype so you can neither extend nor patch it, and its properties are live reads of the exporter's variables.
- If everything about it is fixed, why does `Object.isFrozen(ns)` return false?Because frozen requires every data property to report `writable: false`, and the namespace reports its export properties as writable. That flag exists precisely because the values are live: a non-writable, non-configurable property is supposed to never change, which would contradict the exporter reassigning its variable. `Object.isSealed(ns)` is `true`, and writes still throw.
- Do two modules that both do `import * as ns` from the same file get the same object?Yes — one namespace object exists per module record per realm, created lazily and cached. Reference equality holds across importers, which is why nobody being able to patch it matters: a mutation would be visible program-wide. Across realms, such as a separate worker or frame, the module is a different record and gets its own namespace object.
- How do you get a plain, mutable copy of a namespace object?Spread it: `const copy = { ...ns }`. That reads every enumerable own property once, producing an ordinary object with `Object.prototype` as its prototype. The copy is extensible and writable — and no longer live, so later reassignments inside the exporting module will not be reflected in it.
- What does accessing a name the module does not export return?`undefined`, because it is just a property read on an object that lacks that property — no error. That is a real difference from a static named import of a missing export, which is detected when the module graph is linked. Dynamic access through the namespace trades that early checking for a silent `undefined`.
saying these in an interview costs you the question
- Calls it a plain object built from the exports
- Thinks you can attach helpers to a namespace object
- Assumes it inherits Object.prototype methods
- Says the namespace is a one-time snapshot of values
- Believes new exports can appear on it at runtime