Monday, March 25, 2019

Deep Copy version of Javascript Object.assign

I was working on some Redux work and needed a reducer that would merge in some sparse updates to the current state of an object.

If you're learning Redux you may be familiar with the tutorial example of a TODO list item where it's changing one attribute of the TODO list:

return Object.assign({}, state, {visibilityFilter: action.filter});

Now, instead of changing a single top-level attribute like the visibility filter, assume you have some data that needs to be merged into the existing object both at the top level and in some nested attributes.  For example, presume your current state looks like:

{ a : "a", deep: { a: "a" }

and you get new data for that state that need to be merged in that looks like:

{ b : "b", deep: { b: "b" } }

The Object.assign function will do a shallow copy merge and the result will be:

> Object.assign({}, { a : "a", deep: { a: "a" } }, { b : "b", deep: { b: "b" } })
{ a: 'a', deep: { b: 'b' }, b: 'b' }

The new value of deep simply replaced the first value.  The nested value of deep wasn't merged together.  The method below will correctly merge nested values as follows:

> deepAssign({}, { a : "a", deep: { a: "a" } }, { b : "b", deep: { b: "b" } })
{ a: 'a', deep: { a: 'a', b: 'b' }, b: 'b' }

Here's the code:

function deepAssign(...objs) {
    let target = objs.shift();
    let source = objs.shift();
    
    if (source) {
        if (source instanceof Array) {
            for (let element of source) {
                if (element instanceof Array) {
                    target.push(deepAssign([], element));
                } else if (element instanceof Object) {
                    target.push(deepAssign({}, element));
                } else {
                    target.push(element);
                }
            }
        } else {
            for(const attribute in source) {
                if (source.hasOwnProperty(attribute) && source[attribute]) {
                    if (source[attribute] instanceof Array) {
                        target[attribute] = target[attribute] || [];
                        for (let element of source[attribute]) {
                            if (element instanceof Array) {
                                target[attribute].push(deepAssign([], element));
                            } else if (element instanceof Object) {
                                target[attribute].push(deepAssign({}, element));
                            } else {
                                target[attribute].push(element);
                            }
                        }
                    } else if (source[attribute] instanceof Object) {
                        target[attribute] = deepAssign(target[attribute] || {}, source[attribute]);
                    } else {
                        target[attribute] = source[attribute];
                    }
                }
            }
        }
    }
    if (objs.length > 0) {
        return deepAssign(target, ...objs);
    } else {
        return target;
    }
};


Update 7/15/19 : Original version didn't handle Arrays correctly.  They become objects.  They now copy correctly including nested array and object in the arrays.  You can't just use the spread operator (newArray = [...oldArray]) to copy it or it wouldn't copy the nested objects as new objects

No comments: