-2

Question: How to extend object 'in place' without creating a new object reference.

Synopsis: JQuery extend does what it says on the tin.

But if using the version of jquery.extend that returns a new object then the new object is, well a new object. All well and good but if I'm doing the extend in a function into which I pass the reference to the object to be extended, the original object is left unaltered. A nice trap waiting for the unwary (me). You do know that objects are passed by reference - right?

Example code: Say I have an object

myObject = {
    fontFamily : 'Tahoma',
    fontSize: '12',
    color : '000000'
}

And Default Options

myDefaults = {
    fontFamily : 'Tahoma',
    fontSize: '15',
    color : 'FF0000',
    weight : 'bold',
    decoration : 'underline'
}

And my expected result is:

myObject = {
    fontFamily : 'Tahoma',
    fontSize: '12',
    color : '000000',
    weight : 'bold',
    decoration : 'underline'
}

So here's the code:

var myObject = {
    fontFamily : 'Tahoma',
    fontSize: '12',
    color : '000000'
}

$('#myObjectIn').html(JSON.stringify(myObject));

extenderoony(myObject);

$('#myObjectOut').html(JSON.stringify(myObject));

function extenderoony(obj){

  var myDefaults = {
        fontFamily : 'Tahoma',
        fontSize: '15',
        color : 'FF0000',
        weight : 'bold',
        decoration : 'underline'
    }

  obj = $.extend({}, obj, myDefaults);  // << Changes the object reference of obj to a new object leaving the original object passed in as the parameter unchanged.

  $('#obj').html(JSON.stringify(obj));


}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>

<div >Before the extend func</div>
<div id='myObjectIn'></div>
<hr>
<div >Inside the extend func</div>
<div id='obj'></div>
<hr>
<div >After  the extend func</div>
<div id='myObjectOut'></div>

My answer feels hacky - I'll post it below.

Vanquished Wombat
  • 9,075
  • 5
  • 28
  • 67
  • err, yeah, that's how it's supposed to work. Only the left most object passed to $.extend is modified. (the first one, in this case a new one you created within the function.) `obj = ` changes the value of `obj`, but it doesn't modify the object you previously stored in it. – Kevin B Oct 17 '19 at 20:50
  • Thanks Kev. Thats kind of my question - how to achieve the merge with the defaults object 'in plcae' without the need for any extra variables. – Vanquished Wombat Oct 17 '19 at 21:09
  • Return the object you want from the function, and store it in the variable you want to hold it on the outside. No additional variables required. – Kevin B Oct 17 '19 at 21:10

2 Answers2

0

Extending the options into the defaults causes the myDefaults to take on the values you explicitly gave. Then extending myDefaults into options causes you to update the original object passed in, rather than assigning it to a completely new object.

var myObject = {
    fontFamily : 'Tahoma',
    fontSize: '12',
    color : '000000'
};

function extend ( options ) {
  let myDefaults = {
    fontFamily : 'Tahoma',
    fontSize: '15',
    color : 'FF0000',
    weight : 'bold',
    decoration : 'underline'
  }
  
  let temp = $.extend( myDefaults, options );
  
  $.extend( options, myDefaults );
}

extend( myObject );

let expectedObject = {
  fontFamily : 'Tahoma'
  , fontSize: '12'
  , color : '000000'
  , weight : 'bold'
  , decoration : 'underline'
};

Object.keys( expectedObject ).forEach( key => {
  console.log( `Does ${key} match expected value?`, myObject[ key ] === expectedObject[ key ] );
} );
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>

If you wanted to skip the double extend, you could copy the keys yourself.

var myObject = {
    fontFamily : 'Tahoma',
    fontSize: '12',
    color : '000000'
};

function extend ( options ) {
  let myDefaults = {
    fontFamily : 'Tahoma',
    fontSize: '15',
    color : 'FF0000',
    weight : 'bold',
    decoration : 'underline'
  }
  
  Object.keys( myDefaults ).forEach( key => {
    if ( options[ key ] === undefined ) options[ key ] = myDefaults[ key ];
  } );
}

extend( myObject );

let expectedObject = {
  fontFamily : 'Tahoma'
  , fontSize: '12'
  , color : '000000'
  , weight : 'bold'
  , decoration : 'underline'
};

Object.keys( expectedObject ).forEach( key => {
  console.log( `Does ${key} match expected value?`, myObject[ key ] === expectedObject[ key ] );
} );
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
Taplar
  • 24,788
  • 4
  • 22
  • 35
  • Thanks for the snippet Taplar. It would appear that there is a new var 'temp' in there - so its not quite an in-place solution ? Plus it needs a double use of extends. Any minute now some purist will chuck in a comment about that taking twice as many cpu cycles to execute. – Vanquished Wombat Oct 17 '19 at 21:14
  • 1
    For my clarification, when you say `in-place` do you mean pre-existing method in jQuery, or do you mean a single operation? – Taplar Oct 17 '19 at 21:16
  • Single operation. So merging the properties of the target object with the defaults object but maintaining any values in the target. In a single operation, without losing the link from the object variable to the original object in memory. Ideally with no other libs - I'm guessing underscore can do it, etc, but I want to keep dependencies to vanilla JS and JQ. – Vanquished Wombat Oct 17 '19 at 21:22
  • 1
    Added a secondary snippet that loops over the default keys, and only copies them to the original object if they are undefined. – Taplar Oct 17 '19 at 21:25
  • Marked your answer as correct. Its not perfect as it's more involved than the simple JQ extend, but then again it's more obvious and indicative about what is happening. Thanks for the prompt response. I'm stuck in ES5 but can convert your code. – Vanquished Wombat Oct 17 '19 at 21:32
  • Well "simple" is a fuzzy concept, because `$.extend()` is not a single operation. If you look in the source code to see what that method does, you'll find it is most likely doing similar stuff, but without the existance check. [jQuery extend source view](https://j11y.io/jquery/#v=2.1.3&fn=jQuery.extend) – Taplar Oct 17 '19 at 21:33
  • All true - I grokked what was going on quite early but decided to post the question with a genuine intent of sticking a flag in the ground for anyone else travelling this path, and of getting a succinct answer, which yours is. Agree the point that jq extend is busy under the covers. The issue for me came about when refactoring code that was in-line into a function when the behaviour changed markedly. – Vanquished Wombat Oct 17 '19 at 21:36
-1

Here's my answer - same basic snippet code as the question with the additions as commented.

var myObject = {
    fontFamily : 'Tahoma',
    fontSize: '12',
    color : '000000'
}

$('#myObjectIn').html(JSON.stringify(myObject));


// Attempt #1
// myObject = extenderoony(myObject); // this fails, I think because the param reference overwrites the returned value 

// Attempt 2
var a = extenderoony(myObject);   // introduce the additional variable to receive the object ref
myObject = a;  // And pass this reference on to the original. Phew, hacky.

$('#myObjectOut').html(JSON.stringify(myObject));

function extenderoony(obj){

  var myDefaults = {
        fontFamily : 'Tahoma',
        fontSize: '15',
        color : 'FF0000',
        weight : 'bold',
        decoration : 'underline'
    }

  obj = $.extend({}, obj, myDefaults);  // << Changes the object reference of obj to a new object leaving the original object passed in as the parameter unchanged.

  $('#obj').html(JSON.stringify(obj));

  return obj;  // have to send back the 'new' object reference cos we used that extend format !

}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>

<div >Before the extend func</div>
<div id='myObjectIn'></div>
<hr>
<div >Inside the extend func</div>
<div id='obj'></div>
<hr>
<div >After  the extend func</div>
<div id='myObjectOut'></div>
Vanquished Wombat
  • 9,075
  • 5
  • 28
  • 67
  • 1
    I mean, yeah? `obj` has a value of the reference to the object in memory. If you `obj.thing = newValue` you use the reference, go to the object place in memory, and change that object in memory, which is reflected outside of the method. However, if you change what obj references, of course it's not going to reflect the original object. You're no longer pointing to it. – Taplar Oct 17 '19 at 20:37
  • Thanks Taplar. Thats kind of my point. I'm looking for an in-place version that achieves the merge of the defaults with the target object without losing the original reference. – Vanquished Wombat Oct 17 '19 at 21:11
  • I'm not sure there is an in-place version of what you're asking for, which is essentially asking for a version of `$.extend(obj, myDefaults)` that only copies the key value from my defaults if it doesn't exist in obj already – Taplar Oct 17 '19 at 21:14