Objects & Properties Interview Questions & Answers
30 questions Updated 2026-06-18
JavaScript object interview questions — creating objects, property descriptors, getters and setters, Object methods, enumeration, freezing and copying.
Read the in-depth guideJavaScript Objects & Properties — Creation, Descriptors, Getters and EnumerationSeveral, each with different use cases:
const a = {} // object literal (most common)
const b = new Object() // constructor (rarely used)
const c = Object.create(proto) // with an explicit prototype
function Point(x) { this.x = x } // constructor function
const d = new Point(1)
class P {} // class
const e = new P()
The object literal {} is by far the most common. Object.create(proto)
is the only one that lets you set the prototype directly (or create a
prototype-less object with Object.create(null)). Constructor functions and
classes are for producing many similar objects.
Dot notation (obj.name) is concise but only works for fixed, valid
identifier keys. Bracket notation (obj['name']) accepts any string or a
computed expression — required for dynamic keys, keys with spaces/special
characters, or numeric-like keys.
obj.firstName // static key
obj['first-name'] // key with a hyphen — dot won't work
const key = 'age'
obj[key] // dynamic key from a variable
Use dot notation by default; reach for brackets when the key is dynamic or not a valid identifier.
Shorthand lets you omit the value when a variable has the same name as the key. Computed names let you use an expression as a key inside a literal.
const name = 'Ada', age = 36
const user = { name, age } // { name: 'Ada', age: 36 }
const field = 'email'
const obj = { [field]: 'a@b.com', [`is${field}`]: true }
// { email: 'a@b.com', isemail: true }
const methods = { greet() { return 'hi' } } // method shorthand
These ES6 features make object construction terser and enable dynamic keys without a separate assignment.
Every property has a descriptor describing its attributes. Data properties
have value, writable, enumerable, and configurable; accessor properties
have get, set, enumerable, configurable.
const obj = {}
Object.defineProperty(obj, 'id', {
value: 1,
writable: false, // can't reassign
enumerable: false, // hidden from for...in / Object.keys
configurable: false, // can't delete or redefine
})
Object.getOwnPropertyDescriptor(obj, 'id')
Properties created normally default all flags to true; defineProperty
defaults them to false. Descriptors let you create read-only or
non-enumerable properties — used heavily by the language internals.
Accessor properties that run a function on read (get) or write (set),
letting a property be computed or validated while still being accessed like a
normal property.
const temp = {
celsius: 20,
get fahrenheit() { return this.celsius * 1.8 + 32 },
set fahrenheit(f) { this.celsius = (f - 32) / 1.8 },
}
temp.fahrenheit // 68 (computed)
temp.fahrenheit = 212
temp.celsius // 100
They're useful for derived values, validation, and encapsulation. Define them
with get/set syntax in a literal/class, or via Object.defineProperty.
They return arrays of an object's own enumerable string-keyed properties:
const o = { a: 1, b: 2 }
Object.keys(o) // ['a', 'b']
Object.values(o) // [1, 2]
Object.entries(o) // [['a', 1], ['b', 2]]
for (const [k, v] of Object.entries(o)) console.log(k, v)
They ignore inherited and non-enumerable properties (and symbol keys). They're
the standard way to iterate objects, and Object.fromEntries reverses
entries back into an object.
Object.assign(target, ...sources) copies own enumerable properties from
the sources onto the target (a shallow copy) and returns the target.
const merged = Object.assign({}, defaults, overrides) // merge into a new object
const clone = Object.assign({}, original) // shallow clone
It only copies one level deep — nested objects are shared by reference. The
spread operator { ...a, ...b } does the same shallow merge more concisely and
is usually preferred. Object.assign also triggers setters on the target.
{ ...obj } copies an object's own enumerable properties into a new object — a
concise shallow copy/merge. Later spreads override earlier keys.
const updated = { ...user, name: 'Grace' } // copy + override one field
const merged = { ...defaults, ...options } // options win on conflicts
Like Object.assign, it's shallow — nested objects are shared. It's the
idiomatic way to do immutable updates (especially in React). Spread copies
values, not getters (it evaluates them).
The delete operator removes a property entirely (so 'key' in obj becomes
false), returning true on success.
const obj = { a: 1, b: 2 }
delete obj.a
'a' in obj // false
Caveats: delete only removes own properties (not inherited ones), can't
delete configurable: false properties, and can deoptimize objects in hot
paths. For immutable removal, use destructuring: const { a, ...rest } = obj.
Setting to undefined keeps the key; delete removes it.
Three common approaches, each subtly different:
'key' in obj // true for own AND inherited properties
obj.hasOwnProperty('key') // own properties only
Object.hasOwn(obj, 'key') // own only (modern, safer)
obj.key !== undefined // fails if the value IS undefined
Use in when inherited properties count; use Object.hasOwn (ES2022) for own
properties — it's safer than hasOwnProperty, which can be shadowed or missing
on Object.create(null) objects. Checking === undefined is unreliable.
Enumerable properties show up in for...in loops, Object.keys, and spread;
non-enumerable ones are hidden from them (but still accessible directly).
const obj = { visible: 1 }
Object.defineProperty(obj, 'hidden', { value: 2, enumerable: false })
Object.keys(obj) // ['visible']
obj.hidden // 2 (still readable)
Object.getOwnPropertyNames(obj) // ['visible', 'hidden'] — includes non-enumerable
Built-in methods (like array methods on the prototype) are non-enumerable, so
they don't appear when you iterate. getOwnPropertyNames lists all own
properties regardless of enumerability.
for...in iterates enumerable string keys, including inherited ones
from the prototype chain — which is its main pitfall.
for (const key in obj) {
if (Object.hasOwn(obj, key)) { // guard against inherited keys
console.log(key, obj[key])
}
}
Always guard with Object.hasOwn/hasOwnProperty unless you want inherited
keys. For arrays, prefer for...of (values) or a normal loop — for...in
iterates indices as strings and includes non-index enumerable properties.
Three levels of locking down an object:
preventExtensions— can't add new properties; existing ones stay writable/deletable.seal— preventExtensions + can't delete or reconfigure properties; values still writable.freeze— seal + can't change values. Fully immutable (shallowly).
const obj = Object.freeze({ a: 1, nested: { b: 2 } })
obj.a = 9 // ignored (throws in strict mode)
obj.nested.b = 9 // still works — freeze is SHALLOW
All three are shallow. Check with Object.isFrozen/isSealed. For deep
immutability, recursively freeze.
Shallow copies ({ ...obj }, Object.assign) share nested references. For a
fully independent copy, use structuredClone (modern, handles cycles,
Dates, Maps), or a library.
const deep = structuredClone(original) // best built-in option
const viaJson = JSON.parse(JSON.stringify(original)) // loses functions,
// undefined, Symbols; breaks on Dates/Maps/cycles
structuredClone is the standard now; the JSON trick works only for plain
JSON-safe data and silently mangles everything else.
By reference. Two distinct objects with identical contents are not equal; only two references to the same object are.
{ a: 1 } === { a: 1 } // false (different objects)
const x = { a: 1 }
const y = x
x === y // true (same reference)
y.a = 2; x.a // 2 — they share the object
Assigning an object copies the reference, not the object. This underlies
shallow-copy bugs and why you need structuredClone for true independence.
?. short-circuits to undefined if a reference is null/undefined, instead
of throwing — safe access into deep structures.
user?.address?.city // undefined if user or address is missing
user?.getName?.() // call only if it exists
data?.items?.[0] // safe index access
const city = user?.address?.city ?? 'unknown'
It stops evaluating at the first nullish link. Pair it with ?? for defaults.
Don't overuse it to mask data that should exist — it can hide bugs.
In a regular-function method called as obj.method(), this is the object the
method was called on. Extract the method, and this is lost.
const counter = {
count: 0,
inc() { this.count++ },
}
counter.inc() // this === counter
const fn = counter.inc
fn() // this is undefined -> TypeError
Use method shorthand (a regular function) for methods that need this; avoid
arrow methods (their this is the outer scope, not the object).
It creates an object with no prototype — a "pure dictionary" with no
inherited properties or methods (toString, hasOwnProperty, etc.).
const dict = Object.create(null)
dict.key = 'value'
dict.toString // undefined — no Object.prototype
'toString' in dict // false
Useful as a safe map where user-controlled keys won't collide with inherited
property names (e.g. a key literally named "hasOwnProperty"). The trade-off:
object methods aren't available, so use Object.keys(dict) etc. (A Map is
often a cleaner choice.)
Use a Map when keys are dynamic or non-string, when you need ordering and
easy size, or when you frequently add/remove entries.
| Object | Map | |
|---|---|---|
| Keys | strings/symbols | any type |
| Order | mostly insertion (quirky for integer keys) | guaranteed insertion |
| Size | Object.keys(o).length |
map.size |
| Iteration | needs Object.entries |
directly iterable |
| Prototype keys | risk of collisions | none |
Use a plain object for fixed, known-shape records and when you need JSON
serialization (Maps don't JSON.stringify directly).
Object.keys misses non-enumerable and symbol keys. Use the fuller reflection
methods:
Object.getOwnPropertyNames(obj) // all string keys (incl. non-enumerable)
Object.getOwnPropertySymbols(obj) // symbol keys
Reflect.ownKeys(obj) // EVERYTHING: strings + symbols
Reflect.ownKeys is the complete list of own keys. These are needed for deep
cloning, serialization, and metaprogramming where you must see every property.
Modern engines follow a spec order: integer-like keys first, in ascending numeric order, then string keys in insertion order, then symbol keys in insertion order.
const obj = { b: 1, 2: 1, a: 1, 1: 1 }
Object.keys(obj) // ['1', '2', 'b', 'a'] — numbers sorted, then strings as added
So integer-like keys are not in insertion order — a surprise if you rely on
order. If you need strict insertion order for all keys, use a Map.
A property holds a value; a method is a property whose value is a function, so it can be called.
const obj = {
name: 'Ada', // property
greet() { return 'hi' }, // method (function-valued property)
}
obj.name // 'Ada'
obj.greet() // 'hi'
Methods are just properties that happen to be functions — typeof obj.greet
is 'function'. Defining a method with shorthand also makes it non-arrow, so
this works correctly.
Override toString/valueOf, or implement Symbol.toPrimitive for full
control over coercion.
const money = {
amount: 100,
[Symbol.toPrimitive](hint) {
return hint === 'string' ? `$${this.amount}` : this.amount
},
toString() { return `$${this.amount}` },
}
`${money}` // '$100'
money + 1 // 101
Without these, objects coerce to [object Object] (string) via toString.
Symbol.toPrimitive takes precedence and receives a hint ('string',
'number', 'default').
It serializes own enumerable properties, but silently drops undefined,
functions, and symbols, converts NaN/Infinity to null, and calls a
toJSON() method if present.
JSON.stringify({ a: 1, b: undefined, c: () => {}, d: Symbol() })
// '{"a":1}' — b, c, d dropped
JSON.stringify(obj, ['a', 'b']) // replacer array: only these keys
JSON.stringify(obj, null, 2) // pretty-print with 2-space indent
BigInt and circular references throw. The optional replacer and space
arguments control filtering and formatting.
They assign only when the left side is falsy (||=) or nullish (??=) — handy
for defaulting object properties.
const config = { timeout: 0 }
config.timeout ||= 1000 // 1000 overwrites valid 0
config.timeout ??= 1000 // 0 only fills null/undefined
config.retries ??= 3 // adds retries: 3
??= is the safe choice when 0/''/false are valid values. They're
shorthand for x = x ?? value and short-circuit (the right side isn't
evaluated if no assignment happens).
=== only checks reference identity, so you must compare contents yourself for
"same data" equality.
const shallowEqual = (a, b) => {
const ak = Object.keys(a), bk = Object.keys(b)
return ak.length === bk.length && ak.every((k) => a[k] === b[k])
}
For nested structures you need a recursive deep equality (lodash isEqual).
The JSON.stringify(a) === JSON.stringify(b) trick works only for simple data
and is sensitive to key order and undefined.
Plain assignment (obj.x = 1) creates an enumerable, writable, configurable
data property — or triggers an inherited setter if one exists.
Object.defineProperty creates the property with explicit (default false)
flags and bypasses setters.
obj.x = 1 // enumerable: true, writable: true, configurable: true
Object.defineProperty(obj, 'y', { value: 2 })
// enumerable: false, writable: false, configurable: false
Use assignment for normal data; use defineProperty for read-only, hidden, or
accessor properties, or to define multiple at once with defineProperties.
Provide defaults in the destructuring pattern; they apply only when the value
is undefined (not null or other falsy values).
const { timeout = 1000, retries = 3, mode = 'auto' } = options
const { a: alias = 5 } = obj // rename + default
function f({ x = 0, y = 0 } = {}) {} // default for the whole object too
The = {} default on the parameter prevents a crash when the argument is
omitted entirely. Defaults trigger only on undefined, so an explicit null
overrides them.
Use destructuring with a rest pattern — the named keys are excluded, the rest collected into a new object.
const { password, ...safe } = user // `safe` has everything except password
const { [dynamicKey]: _, ...rest } = obj // omit a dynamic key
This is the idiomatic immutable "remove a property" — it produces a new object
without mutating the original (unlike delete). Great for stripping sensitive
fields before sending data.
Convert to entries, transform, and convert back with Object.fromEntries:
// double every value
const doubled = Object.fromEntries(
Object.entries(obj).map(([k, v]) => [k, v * 2])
)
// filter keys
const filtered = Object.fromEntries(
Object.entries(obj).filter(([, v]) => v > 10)
)
Object.entries + array methods + Object.fromEntries is the functional
pattern for mapping/filtering objects (which have no native map/filter).
Object.groupBy (ES2024) groups an array into an object by a key function.
Practice tests are coming soon
Get notified when interactive mock interviews and quizzes launch.