Search code examples
ecmascript-6javascript-objectsassign

Using Object.assign() to assign a number results in the object reference being undefined


I apologise for the sheer amount of code contained here but I wanted to disclose everything. No matter what I try I cannot get Object.assign() to assign a whole number to the given key, despite the same operation working perfectly to assign a floating point number on the next line.

This code contains all needed references so it's clunky (part of my debugging kit):

let markers = []
let cumulativeTimes = {}

const randomWholeNumber = (min, max) => Math.floor(Math.random() * (max - min + 1) + min)

const time = (marker) => { markers[marker] = process.hrtime() }

const timeEnd = (marker, cumulative = true) => {
  const hrend = process.hrtime(markers[marker])
  const precision = 3
  const seconds = hrend[0] // already an int
  const milliSeconds = +(hrend[1] / 1000000).toFixed(precision) // unary plus converts string result to int

  console.log(`Seconds is ${seconds} with type ${typeof(seconds)}`) // outputs "number" - always!

  if (cumulative) {
    let mark = cumulativeTimes[marker]
    mark ? mark.s += seconds : Object.assign(cumulativeTimes, { [marker]: { s: seconds } } ) // <-- It's a trap!
    mark ? mark.ms += milliSeconds : Object.assign(cumulativeTimes, { [marker]: { ms: milliSeconds } } )
    mark = cumulativeTimes[marker]
    console.log(`${mark.s}s, ${mark.ms}ms -- ${marker}`) // outputs undefineds, then NaNs (for seconds)
  } else {
    console.log(`${seconds}s, ${milliSeconds}ms -- ${marker}`)
  }
}

const someLongOp = () => {
  time('someLongOp')
  return new Promise ( async (resolve) => {
    await setTimeout(timeEnd, randomWholeNumber(1000, 5000), 'someLongOp')
    resolve()
  })
}

const test = async (count) => {
  for (let i = 0; i < count; i++) {
    await someLongOp()
  }
}

test(2)

Sample Output:

Seconds is 2 with type number
undefineds, 993.351ms -- someLongOp
Seconds is 3 with type number
NaNs, 1476.091ms -- someLongOp

Now I understand why the second value is NaN (because on the second run of timeEnd() the "mark.s" key exists however it references the value undefined, and performing any arithmetic on undef results in NaN).

What I don't get is why, when seconds is a simple unsigned integer which is confirmed to be a number, this value isn't properly assigned on the first run of the timeEnd(). Even weirder, as we can see from the milliseconds output, this exact same operation works fine for a floating point number... although they are both technically double-precision 64-bit unsigned ints.

Change the default value of cumulative to false and the seconds are output and displayed as expected, so I'm 95% sure it's something going on during the Object.assign() part.

Will definitely award a bounty to this once it qualifies... looking forward to understanding what on earth is happening!


Solution

  • This has nothing to do with doubles versus integers. It's just that your code

    mark ? mark.s += seconds : Object.assign(cumulativeTimes, { [marker]: { s: seconds } } ) // <-- It's a trap!
    mark ? mark.ms += milliSeconds : Object.assign(cumulativeTimes, { [marker]: { ms: milliSeconds } } )
    

    is wrong. Let's desugar it:

    if (mark)
       mark.s += seconds;
    else
       cumulativeTimes[marker] = { s: seconds };
    if (mark)
       mark.ms += milliSeconds;
    else
       cumulativeTimes[marker] = { ms: milliSeconds };
    

    That's obviously not working, overwriting the object with the s property with the one with the ms property. Remember that Object.assign does not recursively merge!

    What you actually want is

    if (mark) {
        mark.s += seconds;
        mark.ms += milliSeconds;
    } else {
        cumulativeTimes[marker] = {
            s: seconds,
            ms: milliSeconds
        };
    }