skip to content

Which JavaScript operations skip symbol-keyed properties on an object, and which ones still expose or copy them?

level: middleimportance: should knowfreq 44%

answer

  1. two surfaces: enumeration vs reflection
  2. Object.keys and JSON never see them
  3. getOwnPropertySymbols and Reflect.ownKeys do
  4. spread copies enumerable symbol keys
  5. hidden is not private

basics

~10 s

Symbol keys are skipped by for...in, Object.keys/values/entries, Object.getOwnPropertyNames and JSON.stringify. They are still returned by Object.getOwnPropertySymbols and Reflect.ownKeys, and copied by Object.assign and object spread. Hidden from loops, not private.

solid answer

~40 s

Symbol-keyed properties are invisible to the ordinary enumeration surface: `for...in`, `Object.keys`, `Object.values`, `Object.entries`, `Object.getOwnPropertyNames` and `JSON.stringify` all skip them. They are fully visible to the reflection surface: `Object.getOwnPropertySymbols(obj)` returns just the symbol keys and `Reflect.ownKeys(obj)` returns string keys followed by symbol keys. The one that trips people up is copying — `Object.assign` and object spread copy own **enumerable** properties, and symbol keys count, so `{ ...obj }` carries your symbol annotation across while `JSON.parse(JSON.stringify(obj))` drops it. `Object.defineProperty` with `enumerable: false` hides a symbol key from the copy path too. The practical rule: symbols keep data out of loops and payloads, but anyone holding the object can still enumerate and read it.

go deeper

for a junior

Know the headline: symbols do not show up in for...in, Object.keys or JSON.stringify, so a symbol key keeps extra data out of ordinary loops and serialised output.

for a middle

Explain the split precisely — enumeration APIs skip symbol keys, Object.getOwnPropertySymbols and Reflect.ownKeys return them, and Object.assign/spread copy the enumerable ones because enumerability is a separate flag from key type.

for a senior

Show you have been bitten: symbol bookkeeping surviving a shallow clone, annotations vanishing across a JSON round trip or structuredClone, and a hand-rolled deep clone silently dropping keys because it iterated Object.keys.

for a principal

Own the boundary decision — symbol keys, a WeakMap side table, or a documented string field differ in discoverability, clone and serialisation behaviour, and how much of a library's internal state leaks into consumers' object graphs and logs.

## Two different surfaces Every own property of an object has a key that is either a string or a symbol, plus an `enumerable` flag. JavaScript's introspection APIs split along both axes, and knowing which API sits where is the whole question. **The enumeration surface — string keys only, enumerable only:** - `for...in` (also walks the prototype chain) - `Object.keys`, `Object.values`, `Object.entries` - `JSON.stringify` **The own-string surface — string keys, enumerable or not:** - `Object.getOwnPropertyNames` **The symbol surface:** - `Object.getOwnPropertySymbols` — own symbol keys, enumerable or not **Everything:** - `Reflect.ownKeys` — own string keys first, then own symbol keys - `Object.getOwnPropertyDescriptors` — descriptors for both kinds ```js const S = Symbol('meta'); const obj = { [S]: 'hidden', visible: 1 }; for (const k in obj) console.log(k); // 'visible' Object.keys(obj); // ['visible'] Object.getOwnPropertyNames(obj); // ['visible'] JSON.stringify(obj); // '{"visible":1}' Object.getOwnPropertySymbols(obj); // [ Symbol(meta) ] Reflect.ownKeys(obj); // ['visible', Symbol(meta)] obj[S]; // 'hidden' ``` Nothing here is access control. The symbol is discoverable from the object itself, and once you have it, the value is one bracket away. A symbol prevents *accidental* collision and *accidental* traversal — nothing more. ## The copy path is the trap Both `Object.assign(target, source)` and object spread `{ ...source }` copy own **enumerable** properties — and "enumerable" is a flag, not a synonym for "string-keyed". Enumerable symbol keys are copied: ```js const S = Symbol('meta'); const source = { [S]: 'carried', visible: 1 }; const clone = { ...source }; clone[S]; // 'carried' Object.assign({}, source)[S]; // 'carried' ``` So a framework's symbol-keyed bookkeeping silently survives a shallow clone that the author assumed would strip it. If you want a symbol key that spread does *not* pick up, define it non-enumerable: ```js Object.defineProperty(source, S, { value: 'meta', enumerable: false }); { ...source }[S]; // undefined Object.getOwnPropertySymbols(source); // still [ Symbol(meta) ] ``` ## JSON drops symbols in three different ways `JSON.stringify` has no representation for symbols at all, and the way it discards them depends on where the symbol sits: ```js const S = Symbol('s'); JSON.stringify({ [S]: 1, a: 2 }); // '{"a":2}' — symbol KEY skipped JSON.stringify({ a: S }); // '{}' — symbol VALUE skipped JSON.stringify([S]); // '[null]' — symbol in an array becomes null JSON.stringify(S); // undefined — the call returns undefined ``` The array case is the sharp one: array elements have to hold their position, so an unrepresentable value becomes `null` instead of disappearing. The same rule applies to `undefined` and functions in those positions. A round trip through JSON therefore never preserves a symbol-keyed annotation — plan for that whenever the annotated object crosses a wire, a `postMessage`, or a cache. ## Why the spec chose this split The design goal was that adding a symbol key to somebody else's object should be **invisible to code that was written before your symbol existed**. Legacy loops over an options bag, `JSON.stringify` on a request body, a `for...in` in a decades-old utility — none of them should suddenly see a new key. Making symbols opaque to the whole enumeration surface achieves that. Reflection is a deliberate escape hatch: `Reflect.ownKeys` exists precisely so that a `Proxy`'s `ownKeys` trap, a deep-clone routine, or a debugger can see every key an object really has. ## Practical consequences - **Metadata that must survive serialisation cannot live under a symbol.** Use a string key, or serialise the annotation explicitly. - **Deep-clone helpers you write yourself must use `Reflect.ownKeys`** if they are to preserve symbol keys; iterating `Object.keys` silently drops them. - **`structuredClone` does not carry symbols either** — symbol-keyed properties are not cloned, and a symbol used as a *value* makes the call throw a `DataCloneError`. - **Class methods with symbol keys** live on the prototype and are non-enumerable like other class methods, so they never appear in any of the enumeration APIs regardless. The one-line summary an interviewer wants back: symbol keys are hidden from *iteration and serialisation*, visible to *reflection*, and copied by *shallow spread*.

  • You write a deep-clone helper. What do you have to do so it preserves symbol-keyed properties?
    Iterate `Reflect.ownKeys(source)` instead of `Object.keys(source)` — it returns string keys and then symbol keys. Better still, copy descriptors with `Object.getOwnPropertyDescriptors` and `Object.defineProperties`, so enumerability, getters and non-writable flags survive too. A clone built on `Object.keys`, `JSON`, or `structuredClone` silently drops every symbol key.
  • How does JSON.stringify treat a symbol used as a value rather than as a key?
    It is unrepresentable, so it is discarded — but positionally. In an object, the whole property is omitted: `JSON.stringify({ a: sym })` gives `'{}'`. In an array the slot must survive, so it becomes `null`: `JSON.stringify([sym])` gives `'[null]'`. And `JSON.stringify(sym)` returns `undefined` rather than a string. `undefined` and functions follow the same three-way rule.
  • Can you keep a symbol-keyed property out of object spread as well?
    Yes — define it as non-enumerable. `Object.defineProperty(obj, S, { value: v, enumerable: false })` keeps it out of `Object.assign` and `{ ...obj }`, because those copy own *enumerable* properties. It is still fully visible to `Object.getOwnPropertySymbols` and `Reflect.ownKeys`; enumerability controls copying and iteration, never reachability.

saying these in an interview costs you the question

  • Says symbol keys are private and cannot be read
  • Assumes JSON.stringify serialises symbol-keyed data
  • Thinks object spread drops symbol keys
  • Believes Object.getOwnPropertyNames returns symbols too
  • Expects structuredClone to carry symbol properties

context