2

This is a question about how JavaScript might add a reference to existing rather than creating new.

Here are some examples that are hopefully illustrative-enough, in the context of a Redux reducer because it is a familiar place for spread operator or Object.assign():

See here we are just returning an object literal with a string, so there is nothing that could drag in a reference to something that is existing elsewhere.

export default (state = {}, action) => {
    switch (action.type) {
        case SOME_ACTION:
            return {
                props: 'something arbitray'
            }
    }
}

This one is the suspect problem:

We are returning an object literal but we have included reference to args[type]. First, I need to know for certain, is this returning an object that maintains a link to whatever args[type] is currently set to? If args[type] were to get mutated after, would that be reflected in this returned object?

export default (state = {}, action) => {
    switch (action.type) {
        case SOME_ACTION:
            return {
                props: args[type]
            }
    }
}

Here are two examples I suspect would not have this problem:

Do I understand that correctly? Does JavaScript copy just the property and not maintain any reference to args[type]?

export default (state = {}, action) => {
    switch (action.type) {
        case SOME_ACTION:
            return Object.assign({}, state, { props: args[type] })
    }
}

Here is another example that I recently learned might be syntactically identical to the Object.assign() syntax:

export default (state = {}, action) => {
    switch (action.type) {
        case SOME_ACTION:
            return { ...state, props: args[type] }
    }
}

The Questions:

  1. Does the spread operator do the exact same thing as Object.assign() in this context and create an entirely new object without risk of illegal-mutability due to maintaining a reference to args[type]? I need to be able to rely on the immutable state of the object after it is created.

  2. Would the second example I showed maintain a live reference to args[type]?

I have some code that normally spreads in something, and I have a use case that omits that spread, so I am curious if that could be a problem. How can I guarantee random alterations of args[type] would not affect this returned object?

Would this be the correct answer?:

export default (state = {}, action) => {
    switch (action.type) {
        case SOME_ACTION:
            return Object.assign({}, { props: args[type] })
    }
}

[edit] I am able to reproduce the issue by doing this:

const arr = ['one', 'two', 'three']

const args = {
  type: arr
}

const something = {
  props: args.type
}

arr.push('four') // Notice how this appears in something.props

console.log(something)

And this fixes it (so it seems to have something to do with primitives vs. maintaining an object reference):

const arr = ['one', 'two', 'three']

const args = {
  type: arr[2]
}

const something = {
  props: args.type
}

arr[2] = 'what' // Notice how this doesn't appear in something.props

console.log(something)

Updated Question

Is there a way to copy a non-primitive (ie: object/array) so that it breaks this reference?

I am noticing it doesn't work with Object.assign()

agm1984
  • 15,500
  • 6
  • 89
  • 113

2 Answers2

3

var a = 5; var b = a;

Does b maintains a reference to a ? No, it does not. You are passing a value reference/value and not the parent object.

  • It's because a is a primitive. try `var a = [5]; var b = a; a.push(2); console.log(b);` – agm1984 Oct 27 '17 at 22:25
  • I want to know how I can ensure this behaviour does not occur. – agm1984 Oct 27 '17 at 22:33
  • In your example b still has no reference to a. they both point to the same object but not to each other. ie. if you assign something else to 'a'. and then do any operation on 'a'. That will not affect 'b'. – Marcin Malinowski Oct 29 '17 at 16:26
  • 1
    to understand you can imagine that [5] can exist (but only for a moment) even without any reference to it. Garbage collector will clean it up at next cycle but the thing to understand is that variable can hold primitive values and but only references objects. if you expect args[type] to be an object you can clone it using `Object.assign({}, args[type])`. Note object assign is doing shallow cloning. – Marcin Malinowski Oct 29 '17 at 16:31
0

The answer seems to be that it's not an issue if the value is a primitive, because JavaScript does not store references to primitives.

The solution here is to spread in the object or array as a property:

const arr = ['one', 'two', 'three']

const args = {
  type: arr
}

const something = {
  props: [...args.type]
}

arr.push('four') // notice how this isn't included

console.log(something)

This is helpful: Is JavaScript a pass-by-reference or pass-by-value language?

Reading about shallow vs deep copying is also helpful, Hard Copy vs Shallow copy javascript

This solves the issue if the target is an object:

const obj = {
  one: 'uno',
  two: 'dos',
  three: 'tres'
}

const args = {
  type: obj
}

const something = {
  props: Object.assign({}, args.type)
}

obj.four = 'four' // notice how this isn't included

console.log(something)

I recall this is described in a book by Eric Elliot also. Object.assign() is key for abandoning that reference. (and now in 2017, spread)

agm1984
  • 15,500
  • 6
  • 89
  • 113
  • 2
    The real answer might be to not mutate the value, as suggested by the immutability tag on your question :-) – Bergi Oct 27 '17 at 22:55
  • That is what we are doing, but I need to understand it enough to properly reason about it! – agm1984 Oct 27 '17 at 22:57
  • 1
    You should probably know the difference between reassignments and mutations too. Javascript never stores references but always values, even for object types. When you assign an object type to a variable, a copy of the reference to this object is stored, not the reference itself. This subtle difference is important to understand Javascript's evaluation strategy. –  Oct 29 '17 at 10:36