Pregunta

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?

¿Fue útil?

Solución

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. ops 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.

Otros consejos

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.

This issue has very recently been discussed on CoffeeScript's Github Issues. It seems the current behaviour of the compiler was was agreed upon, or at least discussed, on this previous issue.

Basically, in JavaScript the expressions e = e + 1 and e += 1 are always equivalent, as they never introduce a new variable: they will always add 1 to the (local or global) e variable, or they will fail if typeof e === 'undefined'. Now, the expression var e = e + 1 is valid in JavaScript and will declare the e variable and assign it to the value of adding undefined and 1 (NaN, obviously =P), while var e += 1 is syntactically invalid.

In CoffeeScript, e = e + 1 can be a variable declaration in case e was not declared before, or just an assignment statement if e is defined in the current scope, while e += 1 never introduces a new variable (a somewhat reasonable behaviour, as it doesn't make sense to increment a previously undeclared variable).

This is the current behaviour as i understand it. I think it's kind of unfortunate that e = e + 1 and e += 1 can mean different things, but i understand that it's a consequence of the combination of implicit variable declarations and JavaScript's scoping rules (this this comment for a, probably quite biased, explanation).

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top