I always assumed that <var> += 1
and <var> = <var> + 1
have the same semantics in JS.
Now, this CoffeeScript code compiles to different JavaScript when applied to the global variable e
:
a: ->
e = e + 1
b: ->
e += 1
Note that b
uses the global variable, whereas a
defines a local variable:
({
a: function() {
var e;
return e = e + 1;
},
b: function() {
return e += 1;
}
});
Try it yourself.
Is this a bug or is there a reason why this is so?
I think I would call this a bug or at least an undocumented edge case or ambiguity. I don't see anything in the docs that explicitly specifies when a new local variable is created in CoffeeScript so it boils down to the usual
We do X when the current implementation does X and that happens because the current implementation does it that way.
sort of thing.
The condition that seems to trigger the creation of a new variable is assignment: it looks like CoffeeScript decides to create a new variable when you try to give it a value. So this:
a = ->
e = e + 1
becomes
var a;
a = function() {
var e;
return e = e + 1;
};
with a local e
variable because you are explicitly assigning e
a value. If you simply refer to e
in an expression:
b = ->
e += 1
then CoffeeScript won't create a new variable because it doesn't recognize that there's an assignment to e
in there. CS recognizes an expression but isn't smart enough to see e +=1
as equivalent to e = e + 1
.
Interestingly enough, CS does recognize a problem when you use an op=
form that is part of CoffeeScript but not JavaScript; for example:
c = ->
e ||= 11
yields an error that:
the variable "e" can't be assigned with ||= because it has not been defined
I think making a similar complaint about e += 1
would be sensible and consistent. Or all a op= b
expressions should expand to a = a op b
and be treated equally.
If we look at the CoffeeScript source, we can see what's going on. If you poke around a bit you'll find that all the op=
constructs end up going through Assign#compileNode
:
compileNode: (o) ->
if isValue = @variable instanceof Value
return @compilePatternMatch o if @variable.isArray() or @variable.isObject()
return @compileSplice o if @variable.isSplice()
return @compileConditional o if @context in ['||=', '&&=', '?=']
#...
so there is special handling for the CoffeeScript-specific op=
conditional constructs as expected. A quick review suggests that a op= b
for non-conditional op
(i.e. op
s other than ||
, &&
, and ?
) pass straight on through to the JavaScript. So what's going on with compileCondtional
? Well, as expected, it checks that you're not using undeclared variables:
compileConditional: (o) ->
[left, right] = @variable.cacheReference o
# Disallow conditional assignment of undefined variables.
if not left.properties.length and left.base instanceof Literal and
left.base.value != "this" and not o.scope.check left.base.value
throw new Error "the variable \"#{left.base.value}\" can't be assigned with #{@context} because it has not been defined."
#...
There's the error message that we see from -> a ||= 11
and a comment noting that you're not allowed to a ||= b
when a
isn't defined somewhere.