Object static methods select own properties, transform key-value pairs, control property descriptors, and restrict object structure.
Different methods select different keys, while both Object.assign() and Object.freeze() operate only one level deep.
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.
| Operation | Own string keys | Own Symbol keys | Inherited keys | Non-enumerable keys |
|---|---|---|---|---|
Object.keys() | Yes | No | No | No |
Object.values() | Corresponding values | No | No | No |
Object.entries() | Corresponding pairs | No | No | No |
Object.getOwnPropertyNames() | Yes | No | No | Yes |
Object.getOwnPropertySymbols() | No | Yes | No | Yes |
Reflect.ownKeys() | Yes | Yes | No | Yes |
for...in | Yes | No | Yes | No |
“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 operation | Add property | Delete property | Reconfigure property | Write an originally writable data property |
|---|---|---|---|---|
preventExtensions() | No | Yes | Yes | Yes |
seal() | No | No | No | Yes |
freeze() | No | No | No | No |
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.
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
trueObject.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.
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.
'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
functionObject.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.
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
falseA 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.
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.
| Operation | Source keys | Source getter | Target setter | Preserves descriptors |
|---|---|---|---|---|
Object.assign(target, source) | Enumerable own strings and Symbols | Runs | May run | No |
{ ...source } | Enumerable own strings and Symbols | Runs | Doesn’t run an existing target setter | No |
Object.fromEntries(entries) | Keys supplied by iterator | Depends on iterator | No | No |
Object.create(proto, descriptors) | Keys supplied by descriptors | Doesn’t read source property values | No | Yes |
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
4 questions · 1 predict-the-output · 1 spot-the-bug