OP here. Funny to see this on Stack Overflow :)
Before stepping through the behaviour, its important to clarify a few things:
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.
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
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.
> 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.
> 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.
> five
6
Ever since the postfix increment, five
has been 6
.
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.
There's Coercion in the JavaScript World - A Detective Story
Nathan, you have no idea what you've uncovered.
I've been investigating this for weeks now. It all started on a stormy night last October. I accidentally stumbled upon the Number
class - I mean, why in the world did JavaScript have a Number
class?
I wasn't prepared for what I was going to find out next.
It turns out that JavaScript, without telling you, has been changing your numbers to objects and your objects to numbers right under your nose.
JavaScript was hoping no one would catch on, but people have been reporting strange unexpected behavior, and now thanks to you and your question I have the evidence I need to blow this thing wide open.
This is what we've found out so far. I don't know if I should even be telling you this - you might want to turn off your JavaScript.
> function dis() { return this }
undefined
When you created that function, you probably had no idea what was going to happen next. Everything looked fine, and everything was fine - for now.
No error messages, just the word "undefined" in console output, exactly what you would expect. After all, this was a function declaration - it isn't supposed to return anything.
But this was just the beginning. What happened next, no one could have predicted.
> five = dis.call(5)
Number {[[PrimitiveValue]]: 5}
Yeah I know, you expected a 5
, but that's not what you got, was it - you got something else - something different.
The same thing happened to me.
I didn't know what to make of it. It drove me nuts. I couldn't sleep, I couldn't eat, I tried to drink it away, but no amount of Mountain Dew would make me forget. It just didn't make any sense!
That's when I found out what was really going on - it was coercion, and it was happening right there in front of my eyes, but I was too blind to see it.
Mozilla tried to bury it by putting it where they knew nobody would look - their documentation.
After hours of recursively reading and re-reading and re-re-reading I found this:
"... and primitive values will be converted to objects."
It was right there plain as can be spelled out in Open Sans font. It was the call()
function - how could I be so stupid?!
My number was no longer a number at all. The moment I passed it into call()
, it became something else. It became... an object.
I couldn't believe it at first. How could this be true? But I couldn't ignore the evidence that was mounting up around me. It's right there if you just look:
> five.wtf = 'potato'
"potato"
> five.wtf
"potato"
wtf
was right. Numbers can't have custom properties - we all know that! It's the first thing they teach you at the academy.
We should have known the moment we saw the console output - this was not the number we thought it was. This was an impostor - an object passing itself off as our sweet innocent number.
This was... new Number(5)
.
Of course! It made perfect sense. call()
had a job to do, he had to invoke a function, and to do that he needed to populate this
, he knew he couldn't do that with a number - he needed an object and he was willing to do anything to get it, even if that meant coercing our number. When call()
saw the number 5
, he saw an opportunity.
It was the perfect plan: wait until no one was looking and swap out our number for an object that looks just like it. We get a number, the function gets invoked, and no one would be the wiser.
It really was the perfect plan, but like all plans, even perfect ones, there was a hole in it, and we were about to fall right into it.
See, what call()
didn't understand was that he wasn't the only one in town who could coerce numbers. This was JavaScript after all - coercion was everywhere.
call()
took my number, and I wasn't going to stop until I pulled the mask off of his little impostor and expose him to the whole Stack Overflow community.
But how? I needed a plan. Sure it looks like a number, but I know it's not, there's gotta be a way to prove that. That's it! It looks like a number, but can it act like one?
I told five
I need him to become 5 times larger - he didn't ask why and I didn't explain. I then did what any good programmer would do: I multiplied. Surely there was no way he could fake his way out of this.
> five * 5
25
> five.wtf
'potato'
Damn it! Not only did five
multiply just fine wtf
was still there. Damn this guy and his potato.
What the hell was going on? Was I wrong about this whole thing? Is five
really a number? No, I must be missing something, I know it, there's something I must be forgetting, something so simple and basic that I'm completely overlooking it.
This was not looking good, I had been writing this answer for hours and I was still no closer to making my point. I couldn't keep this up, eventually people would stop reading, I had to think of something and I had to think of it fast.
Wait that's it! five
wasn't 25, 25 was the result, 25 was a completely different number. Of course, how could I forget? Numbers are immutable. When you multiply 5 * 5
nothing gets assigned to anything you just create a new number 25
.
That must be what's happening here. Somehow when I multiply five * 5
, five
must be getting coerced into a number and that number must be the one used for the multiplication. It's the results of that multiplication that gets printed to the console, not the value of five
itself. five
never gets assigned anything - so of course it doesn't change.
So then how do I get five
to assign himself the result of an operation. I got it. Before five
even had a chance to think, I yelled "++".
> five++
5
Aha! I had him! Everybody knows 5 + 1
is 6
, this was the evidence I needed to expose that five
was not a number! It was an impostor! A bad impostor that didn't know how to count. And I could prove it. Here's how a real number acts:
> num = 5
5
> num++
5
Wait? What was going on here? sigh I got so caught up in busting five
that I forget how post operators work. When I use the ++
at the end of five
I'm saying return the current value, then increment five
. It's the value before the operation occurs that gets printed to the console. num
was in fact 6
and I could prove it:
>num
6
Time to see what five
really was:
>five
6
...it was exactly what it should be. five
was good - but I was better. If five
were still an object that would mean it would still have the property wtf
and I was willing to bet everything it didn't.
> five.wtf
undefined
Aha! I was right. I had him! five
was a number now - it wasn't an object anymore. I knew the multiplication trick wouldn't save it this time. See five++
is really five = five + 1
. Unlike the multiplication, the ++
operator assigns a value to five
. More specifically, it assigns it the results of five + 1
which just like in the case of multiplication returns a new immutable number.
I knew I had him, and just to make sure he couldn't squirm his way out of it. I had one more test up my sleeve. If I was right, and five
was really a number now, then this wouldn't work:
> five.wtf = 'potato?'
'potato?'
He wasn't going to fool me this time. I knew potato?
was going to be printed to the console because that's output of the assignment. The real question is, will wtf
still be there?
> five.wtf
undefined
Just as I suspected - nothing - because numbers can't be assigned properties. We learned that the first year at the academy ;)
Thanks Nathan. Thanks to your courage in asking this question I can finally put all this behind me and move on to a new case.
Like this one about the function toValue()
. Oh dear god. Nooo!
01 > function dis() { return this }
02 undefined
03 > five = dis.call(5)
04 Number {[[PrimitiveValue]]: 5}
05 > five.wtf = 'potato'
06 "potato"
07 > five.wtf
08 "potato"
09 > five * 5
10 25
11 > five.wtf
12 "potato"
13 > five++
14 5
15 > five.wtf
16 undefined
17 > five.wtf = 'potato?'
18 "potato?"
19 > five.wtf
20 undefined
21 > five
22 6
01
declares a function dis
that returns the context object. What this
represents changes depending on whether you're using strict mode or not. The entire example has different results if the function were declared as:
> function dis() { "use strict"; return this }
This is detailed in section 10.4.3 in the ES5 specification
- If the function code is strict code, set the ThisBinding to thisArg.
- Else if thisArg is null or undefined, set the ThisBinding to the global object.
- Else if Type(thisArg) is not Object, set the ThisBinding to ToObject(thisArg).
02
is the return value of the function declaration. undefined
should be self explanatory here.
03
the variable five
is initialized with the return value of dis
when called in the context of the primitive value 5
. Because dis
is not in strict mode, this line is identical to calling five = Object(5)
.
04
The odd Number {[[PrimitiveValue]]: 5}
return value is the representation of the object that wraps the primitive value 5
05
the five
object's wtf
property is assigned a string value of 'potato'
06
is the return value of the assignment and should be self explanatory.
07
the five
object's wtf
property is being examined
08
as five.wtf
was previously set to 'potato'
it returns 'potato'
here
09
the five
object is being multiplied by the primitive value 5
. This is no different from any other object being multiplied and is explained in section 11.5 of the ES5 specification. Of particular note is how objects are cast to numeric values, which is covered in a few sections.
9.3 ToNumber:
- Let primValue be ToPrimitive(input argument, hint Number).
- Return ToNumber(primValue).
9.1 ToPrimitive:
Return a default value for the Object. The default value of an object is retrieved by calling the [[DefaultValue]] internal method of the object, passing the optional hint PreferredType. The behaviour of the [[DefaultValue]] internal method is defined by this specification for all native ECMAScript objects in 8.12.8.
8.12.8 [[DefaultValue]]:
Let valueOf be the result of calling the [[Get]] internal method of object O with argument "valueOf".
If IsCallable(valueOf) is true then,
- Let val be the result of calling the [[Call]] internal method of valueOf, with O as the this value and an empty argument list.
- If val is a primitive value, return val.
This is all a roundabout way of saying that the object's valueOf
function gets called and the return value from that function is used in the equation. If you were to change the valueOf
function you could change the results of the operation:
> five.valueOf = function () { return 10 }
undefined
> five * 5
50
10
as five
s valueOf
function was unchanged, it returns the wrapped primitive value 5
so that five * 5
evaluates to 5 * 5
which results in 25
11
the five
object's wtf
property is evaluated again despite having been unchanged from when it was assigned on 05
.
12
'potato'
13
the Postfix Increment Operator is called on five
, which gets the numeric value (5
, we covered how earlier), stores the value so that it can be returned, adds 1
to the value (6
), assigns the value to five
, and returns the stored value (5
)
14
as before, the returned value is the value before it was incremented
15
the wtf
property of the primitive value (6
) stored at the variable five
is accessed. Section 15.7.5 of the ES5 specification defines this behavior. Numbers get the properties from Number.prototype
.
16
Number.prototype
doesn't have a wtf
property, so undefined
is returned
17
five.wtf
is assigned a value of 'potato?'
. Assignment is defined in 11.13.1 of the ES5 specification. Basically the value assigned is returned but not stored.
18
'potato?'
was returned by the assignment operator
19
again five
, which has a value of 6
is accessed, and again Number.prototype
doesn't have a wtf
property
20
undefined
as explained above
21
five
is accessed
22
6
is returned as explained in 13
It's pretty simple.
function dis () { return this; }
This returns the this
context. So, if you do call(5)
you're passing the number as an object.
The call
function doesn't supply arguments, the first argument you give is the context of this
. Usually if you want it on it's on context, you give it {}
so dis.call({})
, which means this
in the function is an empty this
. However, if you pass 5
it seems it will be converted to an object. See .call
So the return is object
When you do five * 5
, JavaScript sees the object five
as the primitive type, so is equivalent to 5 * 5
. Interestingly, do '5' * 5
, it still equals 25
, so JavaScript is clearly casting under the hood. No changes to the underlying five
type is done on this line
But when you do ++
it will convert the object to the primitive number
type thus removing the .wtf
property. Because you are affecting the underlying type
The primitive values can't have property. But when you try to access to a property on primitive value, it transparently transtype to a temporary Number object.
So:
> function dis() { return this }
undefined
// Like five.dis(), so dis return the temporaty Number object and
// reference it in five
> five = dis.call(5)
Number {[[PrimitiveValue]]: 5}
// Write the wtf attribut on the Number object referenced by five
> five.wtf = 'potato'
"potato"
// Read the wtf attribut on the Number object referenced by five
> five.wtf
"potato"
// Return 5*5 but dont change the reference of five
> five * 5
25
// Read the same wtf attribut on the Number object referenced by five
> five.wtf
"potato"
// Change the five reference to a new primitive value (5+1). Five
// reference a primitive now.
> five++
5
// Read the wtf attribut on a new temporary Number object construct from
// the primitive referenced by five. So wtf does not exist.
> five.wtf
undefined
// Write the wtf attribut on a new temporary Number object construct from
// the primitive referenced by five. But this object not referenced by
// five. It will be lost.
> five.wtf = 'potato?'
"potato?"
// Read the wtf attribut on a new temporary Number object construct from
// the primitive referenced by five. So wtf does not exist.
> five.wtf
undefined
> five
6
Declare function dis
. Function returns its context
function dis() { return this }
undefined
Call dis
with context 5
. Primitive values are boxed when passed as context in strict mode (MDN). So five
now is object (boxed number).
five = dis.call(5)
Number {[[PrimitiveValue]]: 5}
Declare wtf
property on five
variable
five.wtf = 'potato'
"potato"
Value of five.wtf
five.wtf
"potato"
five
is boxed 5
, so it's number and object at the same time (5 * 5 = 25). It doesn't changes five
.
five * 5
25
Value of five.wtf
five.wtf
"potato"
Unboxing five
here. five
now is just primitive number
. It prints 5
, and then add 1
to five
.
five++
5
five
is primitive number 6
now, there are no properties in it.
five.wtf
undefined
primitives cannot have properties, you can't set this
five.wtf = 'potato?'
"potato?"
you can't read this, because it was not set
five.wtf
undefined
five
is 6
because of post incrementing above
five
6
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With