Object.assign() in JavaScript: Merging and Shallow Copies

Hero image for Object.assign() in JavaScript: Merging and Shallow Copies. Image by John Jennings.
Hero image for 'Object.assign() in JavaScript: Merging and Shallow Copies.' Image by John Jennings.

There are a few JavaScript methods that look almost too friendly at first glance. Object.assign() is one of them.

The name suggests something simple enough: copy some properties from one object to another. That is more or less what it does, but the details matter because they affect whether the result is tidy, surprising, or subtly broken.

The biggest source of confusion is this:

  • it can merge objects
  • it can clone objects
  • it mutates its target
  • it only performs a shallow copy

If you understand those four points, most of the awkwardness disappears.


The basic shape of Object.assign()

The method takes a target object followed by one or more source objects:

const result = Object.assign(target, sourceOne, sourceTwo);

JavaScript copies the enumerable own properties from each source onto the target, from left to right, and then returns the target object.

That means later sources overwrite earlier values when the keys collide.

const defaults = {  theme: 'light',  pageSize: 20,};const overrides = {  pageSize: 50,};const settings = Object.assign({}, defaults, overrides);console.log(settings);

Here, settings.pageSize ends up as 50.


It is Useful for Merging Configuration Objects

That kind of example is where Object.assign() feels natural.

We often have:

  • default options
  • environmentspecific options
  • user overrides

Merging those into one final object is a common frontend task.

const defaults = {  retries: 2,  timeout: 3000,  cache: true,};const userOptions = {  timeout: 5000,};const requestOptions = Object.assign({}, defaults, userOptions);

Using an empty object as the target is important here. It means we create a new merged object instead of mutating defaults.


The Target Object is Mutated

This is the behaviour people miss most often.

Consider this:

const defaults = {  theme: 'light',};const userOptions = {  theme: 'dark',};Object.assign(defaults, userOptions);console.log(defaults.theme);

defaults.theme is now 'dark'.

That is because defaults was the target. Object.assign() did not create a fresh object. It updated the one we handed to it.

Sometimes that is exactly what you want. Often it is not.

If you want a new object, use an empty target:

const merged = Object.assign({}, defaults, userOptions);

Copying an Object is Possible, but Only Shallowly

Another common use is cloning:

const user = {  name: 'Ellie',  role: 'Developer',};const copiedUser = Object.assign({}, user);

For flat objects, this works well enough.

But it is only a shallow copy. That means nested objects are still shared:

const user = {  name: 'Ellie',  preferences: {    theme: 'light',  },};const copiedUser = Object.assign({}, user);copiedUser.preferences.theme = 'dark';console.log(user.preferences.theme);

The original user's theme is now 'dark' too.

That catches people because the toplevel object was copied, but the nested preferences object was not duplicated. Both objects still point at the same nested value.


Shallow Copy is Not a Bug, It is the Contract

It helps to think of Object.assign() as copying property values, not recursively rebuilding object trees.

If a property value is:

  • a string
  • a number
  • a boolean

that value is copied in the obvious way.

If a property value is itself an object or array, what gets copied is the reference to that nested structure.

So Object.assign() is not failing. It is doing exactly what it promised. The mistake is assuming it performs a deep clone.


Arrays and Nested State Need the Same Caution

The same shallowcopy rule applies to arrays stored inside objects:

const state = {  filters: ['books', 'sale'],};const nextState = Object.assign({}, state);nextState.filters.push('featured');

Now both objects appear to contain the updated array because the array itself was shared.

This is one reason frontend developers working with stateful UIs need to be very deliberate about what is and is not copied.


Object.assign() is often about intent as much as syntax

When used well, it communicates one of two intentions clearly:

  1. "I am creating a merged object from several sources."
  2. "I am creating a shallow copy of this object."

That clarity is genuinely useful.

It also explains why the empty target pattern became so common:

const merged = Object.assign({}, defaults, overrides);

At a glance, that reads as "make me a new object based on these pieces".


Watch Out for Hidden Mutation in Shared Objects

Imagine a helper like this:

const applyOptions = (  baseOptions: Record<string, unknown>,  overrides: Record<string, unknown>,): Record<string, unknown> => {  return Object.assign(baseOptions, overrides);};

That function looks harmless, but it mutates baseOptions.

If the caller reuses that object elsewhere, the side effect can travel surprisingly far.

This is why many teams prefer the defensive pattern:

return Object.assign({}, baseOptions, overrides);

It is not only about style. It is about avoiding accidental sharedstate bugs.


Property Order and Overwrite Rules are Straightforward

Because sources are applied from left to right, later sources win:

const result = Object.assign(  {},  { status: 'draft' },  { status: 'review' },  { status: 'published' },);

The final value of status is 'published'.

That predictability makes the method useful for layered configuration, where precedence matters.


Object.assign() is still worth knowing even with newer syntax around

Modern JavaScript gives us object spread syntax, which is often nicer to read for simple merging:

const settings = {  ...defaults,  ...overrides,};

Even so, Object.assign() still matters because:

  • you will see it in existing codebases
  • it is explicit about mutating a target
  • it makes the shallowcopy behaviour easier to talk about directly

Understanding it also helps you reason about object copying more generally.


Shallow Copy is Still Shared below the Top Level

Object.assign() is perfectly useful when you know what it does.

Use it when you want to:

  • merge flat objects
  • create a shallow copy
  • layer configuration predictably

Be cautious when:

  • nested objects or arrays are involved
  • the target object might be shared
  • you need true deep cloning

Once that distinction is clear, Object.assign() stops being one of those methods that "usually works until it doesn't" and becomes a predictable part of your everyday JavaScript toolkit.


Categories:

  1. Development
  2. Front‑End Development
  3. Guides
  4. JavaScript