48

Javascript's Object doesn't have any native merge operation. If you have two objects, say

{a:1, b:2}
{c:3, d:4}

And want to get

{a:1, b:2, c:3, d:4}

As far as I know, you have to iterate through the objects. That is to say that you decide on either a merge left or merge right strategy and then you do something like (simplified)

for (key in object2) {
  object1[key] = object2[key];
}

This is fine. However, Javascript has the call and prototype feature. For instance, turning arguments into an Array can be done with

Array.prototype.slice.call(arguments)

This approach exploits existing native code, and so therefore is less susceptible to programmer folly and should run faster than a non-native implementation.

The question

Is there a trick to use this prototype/call pattern on perhaps the Attribute or Node traversal features of the DOM, or perhaps some of the generic String functions in order to do a native object merge?

The code would look something like this:

var merged = somethingrandom.obscuremethod.call(object1, object2)

And as a result, you'd get a native merge without a traversal.

A possible, sub-optimal solution

If you could use the constructor property of an Object and then coerce one object to have a constructor of another object and then run new over the composite object, you may get a merge for free. But I don't have a firm grasp of the full implications of the constructor feature in javascript to make this call.

Lemma

The same question holds true for Arrays. A common problem is to take, say 7 arrays of numbers, then try to find out the intersection of those arrays. That is to say, which numbers exist in all 7 arrays.

You could concat them together, then do a sort, and then do a traversal, surely. But it would be nice if there is a generic intersect tucked away somewhere that we can coerce an array to doing natively.

Any thoughts?

edit:

Getting half way there

For the array problem, you could do the following:

array.concat(a, b, c).sort().join(':') and then use some tricky RegExp capture and repeat patterns in order to traverse. RegExp implementations, if you don't know, run on a very simple stack-based virtual machine. When you initialize your regular expression that's really a program that gets compiled (RegExp.compile is a deprecated JS method). Then the native runs over the string in a blisteringly fast way. Perhaps you could exploit that for membership thresholds and get better performance...

It still doesn't go all the way though.

TylerH
  • 20,799
  • 66
  • 75
  • 101
kristopolous
  • 1,768
  • 2
  • 16
  • 24
  • 4
    This is an awesome question. Alas i doubt the answer is anything but a big fat no... but here's to hoping, it would sure come in handy :) – Martin Jespersen Jan 19 '11 at 21:15
  • 1
    Awesome question--I also think the answer is no, but the relevant places to look are here: https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array#Methods_2 here: https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object#Methods and here: http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-262.pdf (starting at section 15.4.4) – draeton Jan 19 '11 at 21:17
  • I also hope there is a way to do this ;) Sadly, if there is, I doubt it's cross-browser. – Brian Donovan Jan 19 '11 at 21:22
  • CSS Classes are a merge. If we can exploit this, then we have it. – kristopolous Jan 19 '11 at 22:03
  • It's worth noting that I searched through v8 and spidermonkey for merge like programming patterns and tried to traverse up the stack to see what was calling them ... nothing yet. – kristopolous Jan 19 '11 at 22:19
  • JSON.parse(JSON.stringify(obj1).slice(0,-1) + ',' + JSON.stringify(obj2).slice(1)) is about 3.5 times slower then the traditional merge – kristopolous Jan 19 '11 at 23:11
  • Why on earth is there not something like `Object.merge` in ES6. :/ – Samuel Katz Jul 19 '12 at 12:16
  • 1
    possible duplicate of [How can I merge properties of two JavaScript objects dynamically?](http://stackoverflow.com/questions/171251/how-can-i-merge-properties-of-two-javascript-objects-dynamically) – givanse Mar 09 '14 at 21:24

8 Answers8

24

My answer to this will be disappointing, but still:

no

The reason for this is simple: Mr Resig's implementation of merge (or "extend" as it's called for objects) in jQuery is doing a loop, just like the one in your question. You can look at it here. And I dare say that if John Resig hasn't found a clever build-in way to do it, then the mere mortals of stackoverflow won't either :)

ComFreek
  • 29,044
  • 18
  • 104
  • 156
Jakob
  • 24,154
  • 8
  • 46
  • 57
  • 6
    I'm just full of off topic comments today. :o) Just FYI, you can click a line number at github to create a direct url to that line. Shift-click to highlight a range. – user113716 Jan 19 '11 at 21:28
  • 1
    Jquery extend works for functions and objects; I would be happy with a shallow merge that works just for strings or numbers. It's a smaller, and thus different problem domain. – kristopolous Jan 19 '11 at 21:28
  • 1
    Two very useful comments, that's not something I see every day :) – Jakob Jan 19 '11 at 21:29
  • Function instanceof Object === true; extend works for all objects – draeton Jan 19 '11 at 21:30
8

Using ES6 (ES2015) you can use Object.assign method:

var x = {a:1, b:2};
var y = {c:3, d:4};
var z = Object.assign({},x,y);

Using ES7 (ES2016, Chrome 60+ or Babel) you can use Object spread operator:

var x = {a:1, b:2};
var y = {c:3, d:4}; 
var z = {...x, ...y};
DanSavoy
  • 106
  • 1
  • 2
3

The million dollar question! I've tried doing this numerous ways, and the loop way described above always seemed the dirtiest. ES6's Object.setPrototypeOf() allows you to delegate a "property override" object to a "default properties" object, pretty much accomplishing what you're trying to do, but using Object.setPrototypeOf() has some serious implications, like disabling the browser's compiler optimizations for the whole script.

Also, in both the loop solution and the Object.setPrototypeOf() solution, you are left with a situation where the "property override" object can mutate the "default properties" object:

defaultObj = {
    a: [1, 2]
}
...
overrideObj = {
    b: 3
}
Object.setPrototypeOf(overrideObj, defaultObj);
console.log(overrideObj); // {a: [1, 2], b: 3}
// Great!
...
overrideObj.a.push(4);
console.log(defaultObj); // {a: [1, 2, 4]}
// Uh-oh.

You might think this is not a problem, but let's say you're using this object as configuration for a 3rd party lib. You are now handing the control of your default object and everything referenced in it to the 3rd party lib.

A better solution might be to use JSON.stringify and JSON.parse to copy and combine the objects. Here's a Gist with the example: https://gist.github.com/spikesagal/6f7822466887f19b9c65

HTH

Spike Sagal
  • 191
  • 1
  • 4
2

Not that I know of, no. Also, you'll want to write your merge method like this:

function mergeInto(o1, o2) {
  if (o1 == null || o2 == null)
    return o1;

  for (var key in o2)
    if (o2.hasOwnProperty(key))
      o1[key] = o2[key];

  return o1;
}
Brian Donovan
  • 8,274
  • 1
  • 26
  • 25
  • 3
    A little off topic, but you can test for `null` or `undefined` at the same time if you use `==` instead of `===`, as in `if(o1==null || o2==null) return o1;` This is because `null==undefined`. – user113716 Jan 19 '11 at 21:22
  • 1
    @patrick dw: true. For some reason I thought that `0 == null` would be true, but I just tried it and it's not. Thanks. – Brian Donovan Jan 19 '11 at 21:24
  • Actually this is wrong ... it doesn't check for types. You should do if (o1.prototype !== Object.prototype || o2.prototype !== Object.prototype) otherwise you could get numbers or strings passed in. – kristopolous Jan 19 '11 at 21:30
  • ^^ my comment above has incorrect code. Please excuse me. ^^ You still need to check for the type though. – kristopolous Jan 19 '11 at 21:36
  • 3
    Well, you could add a type check, but I'm not sure it should be this method's job to make sure you didn't pass in a `Date` or something. If you did want to, though, you should do `Object.prototype.toString.call(o1) == "[object Object]"`. – Brian Donovan Jan 19 '11 at 22:20
  • That's an argument of design truly. MDC's suggested alternatives to newer JS features usually include very strong type checking as scaffolding. e.g. Array.indexof: https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/indexOf . I would certainly argue that the type checking is the professional proper way to do things; but at the same time; don't think the programmer should expect to be formally notified for screwing up like that and view it as an unnecessary added cost. Mixed; truly. – kristopolous Jan 19 '11 at 22:34
1

You can do the following using native JS 1.7, without the need of a framework. See example on fiddle (example only intended for simple objects - not complex nested objects)

var obj1 = {a: "a", b: "b"};
var obj2 = {c: "c", d: "d"};

// The magic: ugly but works perfectly
var value = (JSON.stringify(obj1).concat(JSON.stringify(obj2))).replace("}{", ",");

document.getElementById("lbl1").setAttribute("value", value);

// back to object
var obj3 = JSON.parse(value);
document.getElementById("lbl2").setAttribute("value", obj3.a + " " + obj3.b + " " + obj3.c + " " + obj3.d);
Ron Anon
  • 19
  • 1
  • Yes, ugly! :-) I do recommend everyone use jQuery, but if you are tempted to use the above, change it not to choke on `"}{"` appearing inside the JSON strings: `var obj1Json = JSON.stringify(obj1);` `var value = (obj1Json.substr(0,obj1Json.length-1).concat(JSON.stringify(obj2).substr(1)));` – Scarabeetle Sep 12 '13 at 09:40
  • Sorry, that should have been: `var value = (obj1Json.substr(0,obj1Json.length-1) + "," + JSON.stringify(obj2).substr(1);` – Scarabeetle Sep 12 '13 at 09:48
  • Ron, you're a real Honey Badger! Awesome native approach... (+1) – Cody Dec 14 '14 at 21:57
1

You can combine the spread operator (...) and the Object.assign approach to get a good solution for the case where you have a bunch of objects in an array and want to merge them all into one super object.

const obj1 = {a:1, b:2}
const obj2 = {c:3, d:4}
const objArr = [obj1, obj2]
const mergedObj = Object.assign({}, ...objArr)

> { a: 1, b: 2, c: 3, d: 4 }
darkhipo
  • 1,384
  • 1
  • 14
  • 19
0

No native ways in ECMA-Script, use:

function merge(o1,o2) {
 if (typeof(o1)!=='object') o1={};
 if (typeof(o2)!=='object') o2={};
 for (var k in o2) {
 if (o1[k]!==undefined)
  alert ('Collision Error'); // TODO
 else
   o1[k]=o2[k];
 }
 return o1;
}
Informate.it
  • 243
  • 3
  • 2
-2

Below I've included a deep-merge function I wrote. It will not deep-merge Arrays, only Objects. It will take two objects, and return a third, new object.

var merge = function(o1, o2) {
    var o_new = {};
    for(p in o1) {
        if(o1[p]) {
            if(typeof o1[p] == 'object' && !(o1[p] instanceof Array) && o2.hasOwnProperty(p)) {
                o_new[p] = merge(o1[p], o2[p]);
            }
            else {
                o_new[p] = o1[p];
            }
        }
    }
    for(p in o2) {
        if(typeof o2[p] == 'object' && !(o2[p] instanceof Array) && o1.hasOwnProperty(p)) {
            o_new[p] = merge(o1[p], o2[p]);
        }
        else {
            o_new[p] = o2[p];
        }
    }
    return o_new;
}
cpreid
  • 521
  • 3
  • 12