What’s happening in this code with Number objects holding properties and incrementing the number?

A recent tweet contained this snippet of JavaScript.

Can someone please explain what is happening in it step by step?

> function dis() { return this }
undefined
> five = dis.call(5)
Number {[[PrimitiveValue]]: 5}
> five.wtf = 'potato'
"potato"
> five.wtf
"potato"
> five * 5
25
> five.wtf
"potato"
> five++
5
> five.wtf
undefined
> five.wtf = 'potato?'
"potato?"
> five.wtf
undefined
> five
6

In particular, it is not clear to me:

  • why the result of dis.call(5) is a Number with some kind of a [[PrimitiveValue]] property, but the results of five++ and five * 5 appear to just be the plain numbers 5 and 25 (not Numbers)
  • why the five.wtf property disappears after the five++ increment
  • why the five.wtf property is no longer even settable after the five++ increment, despite the five.wtf = 'potato?' assignment apparently setting the value.

Solution 1:

OP here. Funny to see this on Stack Overflow :)

Before stepping through the behaviour, its important to clarify a few things:

  1. Number value and Number object (a = 3 vs a = new Number(3)) are very different. One is a primitive, the other is an object. You cannot assign attributes to primitives, but you can to objects.

  2. Coercion between the two is implicit.

    For example:

    (new Number(3) === 3)  // returns false
    (new Number(3) == 3)   // returns true, as the '==' operator coerces
    (+new Number(3) === 3) // returns true, as the '+' operator coerces
    
  3. Every Expression has a return value. When the REPL reads and executes an expression, this is what it displays. The return values often don't mean what you think and imply things that just aren't true.

Ok, here we go.

Original image of the JavaScript code

The pledge.

> function dis() { return this }
undefined
> five = dis.call(5)
[Number: 5]

Define a function dis and call it with 5. This will execute the function with 5 as the context (this). Here it is coerced from a Number value to a Number object. It is very important to note that were we in strict mode this would not have happened.

> five.wtf = 'potato'
'potato'
> five.wtf
'potato'

Now we set the attribute five.wtf to 'potato', and with five as an object, sure enough it accepts the Simple Assignment.

> five * 5
25
> five.wtf
'potato'

With five as an object, I ensure it can still perform simple arithmetic operations. It can. Do its attributes still stick? Yes.

The turn.

> five++
5
> five.wtf
undefined

Now we check five++. The trick with postfix increment is that the entire expression will evaluate against the original value and then increment the value. It looks like five is still five, but really the expression evaluated to five, then set five to 6.

Not only did five get set to 6, but it was coerced back into a Number value, and all attributes are lost. Since primitives cannot hold attributes, five.wtf is undefined.

> five.wtf = 'potato?'
'potato?'
> five.wtf
undefined

I again attempt to reassign an attribute wtf to five. The return value implies it sticks, but it in fact does not because five is a Number value, not a Number object. The expression evaluates to 'potato?', but when we check we see it was not assigned.

The prestige.

> five
6

Ever since the postfix increment, five has been 6.

Solution 2:

There are two different ways to represent a number:

var a = 5;
var b = new Number(5);

The first is a primitive, the second an object. For all intents and purposes both behave the same, except they look different when printed to the console. One important difference is that, as an object, new Number(5) accepts new properties just like any plain {}, while the primitive 5 does not:

a.foo = 'bar';  // doesn't stick
b.foo = 'bar';  // sticks

As for the initial dis.call(5) part, please see How does the "this" keyword work?. Let's just say that the first argument to call is used as the value of this, and that this operation forces the number into the more complex Number object form.* Later on ++ forces it back into the primitive form, because the addition operation + results in a new primitive.

> five = dis.call(5)  // for all intents and purposes same as new Number(5)
Number {[[PrimitiveValue]]: 5}
> five.wtf = 'potato'
"potato"
> five.wtf
"potato"

A Number object accepts new properties.

> five++

++ results in a new primitive 6 value...

> five.wtf
undefined
> five.wtf = 'potato?'
"potato?"
> five.wtf
undefined

...which does not have and does not accept custom attributes.

* Note that in strict mode the this argument would be treated differently and would not be converted to a Number. See http://es5.github.io/#x10.4.3 for the implementation details.