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.
This can be pieced together from the documentation:
=
: Assignment in Lexical scope
The CoffeeScript compiler takes care to make sure that all of your variables are properly declared within lexical scope — you never need to write
var
yourself.
inner
within the function, on the other hand, should not be able to change the value of the external variable of the same name, and therefore has a declaration of its own.
The example given in this section is precisely the same as your case.
+=
and ||=
This is not a declaration, so the above does not apply. In its absence, +=
takes on its usual meaning, as does ||=
.
In fact, since these are not redefined by CoffeeScript, they take their meaning from ECMA-262 — the underlying target language — which yields the results you've observed.
Unfortunately, this "fall-through" doesn't seem to be explicitly documented.
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