Object static methods

Use JavaScript Object methods precisely for own-property enumeration, shallow copying, descriptors, and integrity levels.

level intermediate time 11 min at Standard depth
version Node 24
what

Object static methods select own properties, transform key-value pairs, control property descriptors, and restrict object structure.

trap

Different methods select different keys, while both Object.assign() and Object.freeze() operate only one level deep.

fix

Decide among own or inherited, enumerable or non-enumerable, and string or Symbol before choosing a method; then test nested object identity.

What it is and why it exists

Object static methods are object operations attached to the Object constructor, such as Object.keys(value), Object.assign(target, source), and Object.freeze(value). They aren’t instance methods that every object can safely call; the first argument names the object to inspect or change.

JavaScript objects store data under property keys. A key is either a string or a Symbol, and a property may belong to the object itself or come from its prototype chain , with metadata such as whether it is enumerable. “Get every field” is therefore incomplete until the caller defines which property categories count.

These methods solve four recurring problems: projecting an object into a key-value list, constructing an object from key-value pairs, reading or defining exact property behavior, and restricting whether an object can gain, lose, or rewrite properties. You meet them in configuration merging, API projections, library boundaries, and diagnostic tools.

The APIs don’t share one universal meaning of “copy an object.” Copying plain data, retaining accessors, retaining a prototype, and duplicating an entire object graph are different contracts. Pick the wrong method and a new-looking object can lose metadata, invoke a getter, or continue sharing nested objects with its source.

Property removal still uses the delete operator, prototype-chain membership uses in, and a uniform reflection interface comes from Reflect. These features sit beside Object methods but don’t replace one another.

How it works

Three selection axes

Enumeration APIs mainly filter properties along three axes: whether a property is own or inherited, whether it is enumerable, and whether its key is a string or Symbol. Write down those conditions before guessing from a method name.

OperationOwn string keysOwn Symbol keysInherited keysNon-enumerable keys
Object.keys()YesNoNoNo
Object.values()Corresponding valuesNoNoNo
Object.entries()Corresponding pairsNoNoNo
Object.getOwnPropertyNames()YesNoNoYes
Object.getOwnPropertySymbols()NoYesNoYes
Reflect.ownKeys()YesYesNoYes
for...inYesNoYesNo

“Non-enumerable keys” in the table means that the operation can include that category. Object.values() and Object.entries() select the same keys as Object.keys() and only change the result shape. Even an enumerable Symbol property is omitted by all three methods.

Object.hasOwn(object, key) answers whether one key is an own property, regardless of enumerability, and it accepts Symbols. It is safer than object.hasOwnProperty(key) because the target may override that method or have no Object.prototype at all.

Property descriptors control behavior

Every own property has a property descriptor . A data descriptor uses value and writable; an accessor descriptor uses get and set. Both forms can use enumerable and configurable, but one descriptor cannot specify data and accessor fields together.

Properties created by an object literal or ordinary assignment are normally writable, enumerable, and configurable. Omitted fields in Object.defineProperty() have very different defaults: unspecified Boolean flags are false. Writing only { value: 1 } creates a non-writable, non-enumerable, non-configurable property.

Object.getOwnPropertyDescriptor() returns a descriptor snapshot; changing the returned object does not alter the property. Object.getOwnPropertyDescriptors() returns descriptors for every own string and Symbol key. It can be combined with Object.create() to build a new outer object with the same own-property behavior and prototype.

Copies and transformations stay shallow

Object.assign(target, ...sources) reads each source’s enumerable own string and Symbol keys from left to right, then writes their values to the target. A later source overwrites an earlier source’s same-named key. The target is changed in place and is also the return value.

This operation produces a shallow copy . When a property value is an object, only the reference to that same object is copied; nested objects aren’t merged recursively. If a source property is a getter, copying executes it and puts the resulting value in the target instead of preserving its descriptor.

Object.entries() projects an object into an array of string-keyed pairs for filtering or mapping with array methods. Object.fromEntries(iterable) constructs an ordinary object from pairs. The combination is useful for data transformations, but it isn’t a lossless round trip: Object.entries() omits Symbols, non-enumerable properties, descriptors, and the prototype.

Integrity levels constrain one layer

Object.preventExtensions(), Object.seal(), and Object.freeze() all modify and return the supplied object. They progressively restrict the object’s own structure but don’t recurse into objects referenced by property values.

Object after operationAdd propertyDelete propertyReconfigure propertyWrite an originally writable data property
preventExtensions()NoYesYesYes
seal()NoNoNoYes
freeze()NoNoNoNo

seal() makes every own property non-configurable; freeze() also makes own data properties non-writable. Accessor properties don’t have a writable flag, so getters still run on a frozen object and an existing setter may still change other state. “Frozen” is therefore an integrity state for one object, not immutability for an arbitrary object graph.

When direct assignment or delete conflicts with an integrity restriction, strict mode throws TypeError while a non-strict script may fail silently. Reflect.set() and Reflect.deleteProperty() report these ordinary failures as Booleans, making their result explicit in examples.

Examples

These four examples successively verify property selection, key-value transformation, descriptor copying, and integrity levels. Every output shown here came from local Node 24.14.0.

Selecting the exact property set

This object has an inherited property, a non-enumerable property, and a Symbol property. Putting every category in one sample makes the boundaries of the common APIs visible.

inspect-profile.js
const token = Symbol('token');
const baseProfile = { inheritedRole: 'reader' };
const profile = Object.create(baseProfile);

Object.defineProperties(profile, {
  name: { value: 'Ada', enumerable: true },
  internalId: { value: 17, enumerable: false },
  [token]: { value: 'secret', enumerable: true },
});

console.log(JSON.stringify(Object.keys(profile)));
console.log(JSON.stringify(Object.values(profile)));
console.log(JSON.stringify(Object.entries(profile)));
console.log(Reflect.ownKeys(profile).map(String).join(','));
console.log(Object.hasOwn(profile, 'inheritedRole'));
console.log('inheritedRole' in profile);
["name"]
["Ada"]
[["name","Ada"]]
name,internalId,Symbol(token)
false
true

Object.keys() returns only name, not because the other properties don’t exist but because they don’t satisfy all three conditions: own, enumerable, and string-keyed. in follows the prototype chain, which is why the last two lines give different answers for the same key.

At a boundary with an allowlist, don’t begin with “every key” and delete a list of known dangerous fields. Read the permitted names directly and use Object.hasOwn() to distinguish missing fields from inherited ones; that contract remains stable as the input grows.

Transforming and merging plain data

The first transformation removes an internal field. The configuration merge then demonstrates two independent facts: later sources overwrite earlier keys, and the nested limits object is replaced as a unit rather than merged recursively.

transform-config.js
const flags = {
  checkout: true,
  internalNote: 'remove',
  retries: 0,
};

const publicFlags = Object.fromEntries(
  Object.entries(flags).filter(([key]) => !key.startsWith('internal')),
);
console.log(JSON.stringify(publicFlags));

const defaults = { theme: 'light', limits: { requests: 100 } };
const input = { theme: 'dark', limits: { burst: 10 } };
const merged = Object.assign({}, defaults, input);

console.log(JSON.stringify(merged));
console.log(merged === defaults, merged.limits === input.limits);

const nestedMerge = {
  ...defaults,
  ...input,
  limits: { ...defaults.limits, ...input.limits },
};
console.log(JSON.stringify(nestedMerge));
{"checkout":true,"retries":0}
{"theme":"dark","limits":{"burst":10}}
false true
{"theme":"dark","limits":{"requests":100,"burst":10}}

Using an empty object as the Object.assign() target avoids changing defaults, but it doesn’t make nested values independent. The true on the third output line proves that merged.limits and input.limits still refer to the same object.

Explicitly spreading limits works for configuration with a known shape. A generic “deep merge” needs separate policies for arrays, cycles, accessors, dangerous keys, and different object types; those policies don’t follow naturally from Object.assign().

Defining and retaining property descriptors

This example first exposes the defaults used by defineProperty(), then compares value copying with descriptor copying. A source getter’s read count makes the side effect visible.

copy-descriptors.js
'use strict';

const account = {};
Object.defineProperty(account, 'id', {
  value: 'A-17',
  enumerable: true,
});

const idDescriptor = Object.getOwnPropertyDescriptor(account, 'id');
console.log(JSON.stringify({
  writable: idDescriptor.writable,
  enumerable: idDescriptor.enumerable,
  configurable: idDescriptor.configurable,
}));

let reads = 0;
Object.defineProperty(account, 'balance', {
  get() {
    reads += 1;
    return 42;
  },
  enumerable: true,
  configurable: true,
});

const assigned = Object.assign({}, account);
const exact = Object.create(
  Object.getPrototypeOf(account),
  Object.getOwnPropertyDescriptors(account),
);

console.log(assigned.balance, reads);
console.log(typeof Object.getOwnPropertyDescriptor(assigned, 'balance').get);
console.log(typeof Object.getOwnPropertyDescriptor(exact, 'balance').get);
{"writable":false,"enumerable":true,"configurable":false}
42 1
undefined
function

Object.assign() runs the balance getter once to obtain its value and creates an ordinary data property on the target. Object.getOwnPropertyDescriptors() reads the accessor function itself, so creating exact does not run the getter again.

Here exact only means that the outer prototype and own property descriptors are retained. Property values still share references, and built-ins or class instances with internal slots or private fields cannot be turned into universal exact clones with this combination.

Comparing the three integrity levels

Using Reflect exposes whether each operation succeeds without depending on strict-mode error text. The last two lines also verify that freezing affects only the outer object.

integrity-levels.js
const extensible = { count: 1 };
Object.preventExtensions(extensible);
console.log(
  Reflect.set(extensible, 'count', 2),
  Reflect.set(extensible, 'extra', true),
  Reflect.deleteProperty(extensible, 'count'),
);

const sealed = Object.seal({ count: 1 });
console.log(
  Reflect.set(sealed, 'count', 2),
  Reflect.set(sealed, 'extra', true),
  Reflect.deleteProperty(sealed, 'count'),
);

const preferences = { theme: 'light' };
const frozen = Object.freeze({ count: 1, preferences });
console.log(
  Reflect.set(frozen, 'count', 2),
  Reflect.deleteProperty(frozen, 'count'),
  Object.isFrozen(frozen),
);

preferences.theme = 'dark';
console.log(frozen.preferences.theme);
console.log(Object.isFrozen(frozen.preferences));
true false true
true false false
false false true
dark
false

A non-extensible object still permits changes to and deletion of existing properties. A sealed object still permits writes to originally writable values; a frozen object refuses those writes too. None of the three operations follows the preferences reference, so the nested object remains mutable.

Object.isFrozen() answers only whether its argument is frozen. If a contract claims that an entire input graph is immutable, tests must continue through every relevant nested object or the design must establish explicit ownership of immutable data.

Pitfalls

Treating Object.keys() as every property

Fix: choose a key set from the requirement. Use Reflect.ownKeys() for every own key, Object.hasOwn() for one own-key test, and an explicit, filtered for...in only when prototype-chain enumeration is intentional.

Letting Object.assign() mutate shared defaults

Fix: merge plain data into a fresh target, as in Object.assign({}, defaults, input), and handle nested fields separately. For untrusted input, construct an allowlisted result instead of writing every enumerable key into an ordinary object.

Mistaking a shallow copy for an independent copy

Fix: state which levels require independent identity and copy only those levels. When a general object graph must be copied, use a mechanism with a contract matching the data types, then test shared references, cycles, and unsupported values instead of applying a JSON round trip.

Forgetting descriptor defaults

Fix: spell out every flag the business behavior depends on and assert the result with Object.getOwnPropertyDescriptor(). Making a property non-configurable is a strong one-way decision; confirm that later code won’t need to redefine or delete it.

Assuming freeze recurses or disables accessors

Fix: describe freezing as an outer integrity constraint and test nested identity and accessor side effects. Before implementing recursive freezing, define how to handle cycles, Symbol keys, accessors, proxies, typed arrays, and instances with private state.

Deep Property order and round-trip boundaries

Property order and round-trip boundaries

Ordinary own keys follow a defined order. Array-index string keys come first in ascending numeric order, other string keys follow creation order, and Symbol keys follow in creation order at the end. Object.keys(), Object.values(), Object.entries(), and Reflect.ownKeys() preserve this order within their selected categories.

“Array-index string” is narrower than “looks numeric.” '2' and '10' participate in index ordering, while '01', '-1', and ordinary names stay in string creation order. Tests for order-sensitive code should include values that expose these boundaries.

Object.fromEntries() accepts any iterable of key-value pairs and converts non-Symbol keys to strings. Repeated keys overwrite in iteration order, leaving the final value; a Symbol key can become a property key directly.

This explains why Object.fromEntries(Object.entries(object)) is not a general inverse operation. Object.entries() has already removed Symbols, non-enumerable properties, inherited properties, descriptors, and prototype information, so the second step cannot recreate them. “Data projection” is a better name for this combination than “object clone.”

Object.keys() selects keys without reading their property values, so it doesn’t itself execute getters. Object.values(), Object.entries(), object spread, and Object.assign() must read selected values, which runs getters. A Proxy can also intercept the relevant internal operations, so its result needs separate verification against the proxy contract.

Observable behavior of copy operations

The differences among copying operations go beyond syntax. Source reads, target writes, key selection, and the property-creation mechanism can all be observed by a program.

OperationSource keysSource getterTarget setterPreserves descriptors
Object.assign(target, source)Enumerable own strings and SymbolsRunsMay runNo
{ ...source }Enumerable own strings and SymbolsRunsDoesn’t run an existing target setterNo
Object.fromEntries(entries)Keys supplied by iteratorDepends on iteratorNoNo
Object.create(proto, descriptors)Keys supplied by descriptorsDoesn’t read source property valuesNoYes

Object spread creates own data properties on a new object literal rather than writing by ordinary assignment to an existing target as Object.assign() does. Both still read source properties, so spread does not avoid side effects from source getters.

Descriptor copying preserves getters, setters, and flags instead of saving a getter’s current result as a data property. It still reuses object values and accessor functions from those descriptors, and it doesn’t copy closure state, private fields, or the internal slots of built-ins.

Object.create(Object.getPrototypeOf(source), Object.getOwnPropertyDescriptors(source)) therefore implements one narrow contract: retain the outer prototype and every own property descriptor. Calling it cloneWithDescriptors can be accurate; calling it deepClone without qualification misleads callers.

Object.assign() ignores a source whose value is null or undefined, but either value as the target causes a TypeError. Other primitives are first converted to wrapper objects; except for string characters, they usually have no enumerable own properties to copy. A boundary API that expects plain records should validate that requirement rather than depend on coercion details.

Integrity boundaries and recursive freezing

Integrity methods operate on the supplied object itself and attempt to adjust its extensibility and own property descriptors. They aren’t copy operations, and variables continue to point to the same object. Consequently, Object.freeze(value) === value is always true.

Freezing an array prevents element writes, element deletion, and changes to length because those behaviors are represented by the array’s own properties. Objects referenced by its elements remain mutable. The same shallow boundary applies to ordinary objects: freezing a container doesn’t freeze its members.

A reliable recursive freezer must at least record visited objects to handle cycles and use Reflect.ownKeys() to include Symbol keys. It must also inspect descriptors to avoid executing a getter merely to traverse an accessor property. Proxies can intercept inspection and freezing operations, while different built-ins may add their own restrictions.

That is why the short Object.values(value).forEach(deepFreeze) recipe isn’t a general utility. It skips non-enumerable and Symbol values, fails on cycles, and executes user code when it reads an accessor. With a fixed data model, implement and test a recursion policy for that model; otherwise narrow the meaning of “deeply immutable” first.

Freezing is not a confidentiality or authorization boundary either. Read access remains, and state in closures, private fields, a WeakMap, or an external service is outside the object’s own descriptors. Security review must trace data exposure and permission checks instead of treating isFrozen() as a trust marker.

Null-prototype dictionaries and prototype choice

Object.create(null) creates an object without Object.prototype. It inherits no toString, constructor, or hasOwnProperty, so code that assumes those instance methods exist on every object fails.

Object.keys(), Object.entries(), Reflect.ownKeys(), and Object.hasOwn() all work directly with null-prototype objects. When string keys come from external input, such a dictionary also avoids ordinary inherited-name collisions with Object.prototype.

A null-prototype object still isn’t a complete substitute for Map. When you need keys of any type, direct size, an explicit insertion and deletion interface, or no string-key coercion, Map often fits the contract better. Base the choice on key types and interface rather than a blanket claim that one container is faster.

Object.create(prototype) sets an intended prototype at creation. Object.setPrototypeOf() changes an existing object’s property-lookup relationships and can violate callers’ assumptions about its type. Business code should usually create the object with its intended prototype or use a class to express the public interface instead of swapping prototypes after the object is in circulation.

Further reading

checkpoint

4 questions · 1 predict-the-output · 1 spot-the-bug

Copy as Markdown Interview bank Edit on GitHub Report an error Was this clear?