Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge mutates source object if nested property is a class instance #5823

Open
DeveloperDanjo opened this issue Feb 29, 2024 · 0 comments
Open

Comments

@DeveloperDanjo
Copy link

An original issue #5269 was originally filed about this, but there was no feedback from the issue creator, and it was closed.

My team is experiencing the same situation. However, ours would not be correctly fixed by omitting the original value before merging as was done by a second commenter on that issue.

-- Original issue text starts (as it is still applicable) --

If property of the source objects is created from a class, it will be mutated.

import { merge } from "lodash"

class someClass {
  aa = 0
}

const src1 = { pg: { a: new someClass() } }
const src2 = { pg: { a: { aa: 2 } } }

console.log(JSON.stringify(src1)) // {"pg":{"a":{"aa":0}}}
console.log(JSON.stringify(src2)) // {"pg":{"a":{"aa":2}}}

const dest = merge({}, src1, src2)

console.log(JSON.stringify(src1)) // {"pg":{"a":{"aa":2}}} // However, aa should still be 0
console.log(JSON.stringify(src2)) // {"pg":{"a":{"aa":2}}}

-- Original issue text ends --

Our situation is the same; we are working with an options object for some widget which contains a renderer where the default renderer is an instance of a basic "Renderer" object.

When instantiating a new widget, the author is allowed to provide an object that conforms to the same interface as the "Renderer", but does not have to extend it.

When the new widget is created, the custom render function on the Renderer is attached to the default instance, causing future widgets to use that render function instead:

class Renderer {
  render() { return 'original'; }
}

const DEFAULT_OPTIONS = {
  /** @type {{ render(): string; }} */
  renderer: new Renderer()
};

class Widget {
  #options = undefined;
  constructor(options) {
    this.#options = merge({}, DEFAULT_OPTIONS, options);
  }
}

// ....

const widget = new Widget({
  renderer: { render() { return 'custom'; } }
});

Results in:

widget.#options.renderer:

{
  render() { return 'custom'; },
  <prototype>: {
    constructor: class Renderer,
    render() { return 'default'; }
  }
}

Which is OK, but a little unexpected. I would have expected this to be the same plain object I passed in the options

DEFAULT_OPTIONS.renderer:

{
  render() { return 'custom'; },
  <prototype>: {
    constructor: class Renderer,
    render() { return 'default'; }
  }
}

Definitely unexpected. This should not have the overridden render function as an instance property. Also, this is === to widget.#options.renderer which is definitely wrong.

Now when instantiating a separate widget with no overrides:

const widget2 = new Widget({});
widget2.#options.renderer.render(); // 'custom';

I believe that this behavior is a regression from Lodash 3. You can see the difference in behavior between the two here: https://jsfiddle.net/2y7hkzox/

It seems possible that there is a change between the behavior of baseMergeDeep. (Lodash 3's

: (isPlainObject(value) ? value : {});
and Lodash 4's
else if (typeof objValue === 'function' || !isObject(objValue)) {
) The change between isObject and isPlainObject.

The author of #5269 stated:

But the documentation says that source objects are not mutable.

However, I actually wasn't able to find that in the official documentation: https://lodash.com/docs/4.17.15#merge

But, I do agree with the original author that the behavior seems unintended.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

No branches or pull requests

1 participant