Implement custom behavior for Object.assign

Viewed 685

Is there a way to implement custom behaviour for Object.assign - in particular I want array type property values to be merged rather that overwritten - what is the easiest way to achieve it?

Example:

Default behavior:

Object.assign({a:[1]}, {a:[2]}).a // [2]

while the desired result for me is [1,2].

5 Answers

I am creating custom function instead of attaching assign prototype to Object class for merging objects. This the simplest solution I can get without using lodash.

function mergeObject(srcObj, targetObj) {
  // Get all unique keys
  const aSet = new Set([...Object.keys(srcObj), ...Object.keys(targetObj)])
  const mergedObject = {}
  aSet.forEach(k => {
    // Verify if both end of key is an array
    if (Array.isArray(srcObj[k]) && Array.isArray(targetObj[k])) {
      // Concat given array
      mergedObject[k] = srcObj[k].concat(targetObj[k])
    } else {
      // assign target value if present or source value
      mergedObject[k] = targetObj[k] || srcObj[k]
    }
  });
  return mergedObject;
}

Also this solution will work in es6 only.

I don't know about Object.assign but I needed that in the past and implemented it like this (using lodash)

import _f from 'lodash/fp'; // functional lodash interface

export const mergeAllLeaveArrays = _f.mergeAllWith((objValue, srcValue) => {
  if (_f.isArray(srcValue)) return srcValue;
});

// This is what you want
export const mergeAllConcatArray = _f.mergeAllWith((objValue, srcValue) => {
  if (_f.isArray(srcValue) && (objValue == null || _f.isArray(objValue)))
    return _f.concat(objValue || [], srcValue);
});

I copy&pasted this out of my utility library and I'm using the functional lodash interface. It should be straight forward to convert this to the imperative lodash interface if you like that more.

Yes it is possible to override Object.assign, however it is not recommended because when you do it, you change global standard js function which can cause that external JS libraries which use Object.assign can stop work or they will work in wrong way (so this can block you from using external libraries, or block other people to use your code).

Instead just define new function which do what you need e.g. (it's example - not rich deep object merge)

function ObjectAssign(a,b) {
  let o={};
  for(let k of Object.keys(a)) {
    o[k] = a[k];
    o[k] = b[k] ? a[k].concat(b[k]) : b[k];
  }
  return o;
}

result = ObjectAssign({a:[1]}, {a:[2]});
console.log(result);

You don't see it too often in the wild but a clean way of changing the assignment behaviour without modifying the the built-in library, attaching functionality to the object itself or using an alternate API would be to wrap the target in a Proxy. Do note though, that Object.assign will also return the proxy reference here - where you may want to forget about it and continue with the original object.

const arrayMergeProxy = input => new Proxy(input, {
    set(target, prop, newValue) {
        const currentValue = Reflect.get(target, prop)
        return Reflect.set(
            target, 
            prop,
            Array.isArray(currentValue) && Array.isArray(newValue)
                ? currentValue.concat(newValue)
                : newValue
        )
    }
})

const o1 = { a:[1], b: false }
const o2 = { a:[2], b: true }
Object.assign(arrayMergeProxy(o1), o2)

console.log(o1)

Object.defineProperty(Object, "assignMerge", {
  value: function assign(target, varArgs) {
   'use strict';
   if (target == null) {
    throw new TypeError('Cannot convert undefined or null to object');
   }

   var to = Object(target);

   for (var index = 1; index < arguments.length; index++) {
    var nextSource = arguments[index];

    if (nextSource != null) {
     for (var nextKey in nextSource) {
      if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
              if(to.hasOwnProperty(nextKey) && nextSource.hasOwnProperty(nextKey) && Array.isArray(to[nextKey]) && Array.isArray(nextSource[nextKey])){
                to[nextKey] = to[nextKey].concat(nextSource[nextKey])
              }
      }
     }
    }
   }
   return to;
  },
  writable: true,
  configurable: true
});

const mergedObjects = Object.assignMerge({a:[1, 2], b:[2], c:[2]}, {a:[2,5], b:[1]});
console.log(mergedObjects)

I think if you are creating your own method and apply the same principle which you apply that is used for Object.assign polyfill it can look like this.

Related