Here are some of the "gotchas" I've encountered when I converted all the javascript code into coffeescript. (BTW, this is gathered while coding for YourMechanic.com, a very cool mobile auto repair service.)
Disclaimer: Coffeescript is a great productivity booster for me personally. Even with all the things outlined below, it is still way better than javascript. None of these should discourage you from jumping into it right away.
(Things that generate wrong code silently)
For example, these two lines:
# Really need to convert lines like this
candy = good() ? 1 : 0
# to this before you rename the .js to .js.coffee
candy = if good() then 1 else 0
Generates code like these:
// Unexpected!
candy = (_ref = good()) != null ? _ref : {
1: 0
};
// Expected
candy = good() ? 1 : 0;
Coffeescript always returns the last expression (like in ruby). This can be fairly unexpected for
javascript programmers, and easily overlooked (even with people fluent in ruby).
I end up almost always terminate a void function with empty return
statement. For example:
with_return = ->
doStuff()
return
without_return = ->
doStuff()
get translated to this:
with_return = function() {
doStuff();
};
without_return = function() {
return doStuff();
};
Returning the last value may not seem like a big deal until it hits you in event handlers (last expression evaluated to false).
(completely understandable, but still):
# No space: Good
'Time:'+hour+' hours'
# Irregular spaces: Bad
'Time:' +hour +' hours'
# All spaced: Good again
'Time:' + hour + ' hours'
here is what these statements get translated to:
'Time:' + hour + ' hours';
'Time:' + hour(+' hours');
'Time:' + hour + ' hours';
Coffeescript range ..
/...
works in both directioins. In case the compiler does not know which
direction to take, it generates dynamic code to do both. This is usually not intened, and there is
no good way to specify direction of range. For example:
Original javascript code:
var sum = 0;
for(var i=5;i<arr.length;i++)
{
sum += arr[i];
}
Naively, I would translate it to these coffee script code:
sum = 0
for i in [5..arr.length]
sum += arr[i]
and be surprised to see that actual javascript generated to be this (wrong logic for arr.length
less than 5)
var i, sum, _ref;
sum = 0;
for (i = 5, _ref = arr.length; 5 <= _ref ? i <= _ref : i >= _ref; 5 <= _ref ? i++ : i--) {
sum += arr[i];
}
Even in the cases where logic is not wrong, it is still inefficient to do two compares for each iteration.
(Things that are understandable and relatively easily fixable)
Sometimes this
is used together with my
(e.g. in event handlers this used to refer to element)
In many cases I need to convert code like this:
var my = this;
$('.tab').click(function()
{
my.switchTab($(this).data('tab'));
})
to the code below with alternate way to get to the event target.
$('.tab').click (ev)=>
@switchTab $(ev.target).data('tab')
return
(Lately I would use _.bindAll
and switch from =>
to ->
, to avoid =>
as much as I can)
(Made worse by this effect: http://www.hiddentao.com/archives/2011/11/09/coffeescript-function-binding-gotcha-when-using-cloned-spine-models/)
Function b()
disappears in below code. Would be a good candidate for a "coffeescript lint" tool.
class Test
a: ->
doStuffA()
return
b= ->
doStuffB()
return
Block comment style ### sometimes mysteriously interfere with "pound lines" like "#####################" in the original javascript code. Syntax highlighting saves the day here. (Why mysterious? It only treat code block as comment if number of pounds in your line satisfies (nPounds/3)%2==1)
For example
#############
doStuff1()
###############
doStuff2()
#########
var count;
function Show() { count += 1; ActualShow(); }
$(function()
{
count = 0;
Show();
setInterval(1000, Show)
});
Naive translation wouldn't work. A dummy assignment is required to get the proper forward declaration
count = null
Show = ->
count += 1
ActualShow()
$ ->
count = 0
Show()
setInterval(1000, Show)
Not immediately apparent how to do one-liner fluent jquery-style bindings (to put parenthesis right before the arrow)
$('#el').focus(function(){ DoFocus(); }).blur(function(){ DoBlur(); })
Took me a while to realize that I can write like this
$('#el').focus( -> DoFocus()).blur( -> DoBlur())
Then again, this is not quite correct due to implicty return problem mentioned above, so I end-up have to write something like this:
$('#el').focus ->
DoFocus()
return
.blur ->
DoBlur()
return
Much less fluent than the original javascript.
I have to write:
idx = words.length
while i > 0
i -= 1
words.splice(i,1) if (words[i].length < 2)
instead of
reverse_for word, i in words
words.splice(i,1) if (word.length < 2)
NOTE: I used to do something shown below, which is actually incorrect. Can you discover the bug? Again it is related to the special behavior of coffeescript for-loop construct.
for word, i in words
if (word.length < 2)
words.splice(i,1)
i-- # works, but scary
# Good coffeescript ...
if (cc)
a()
b()
# turns bad if your coffee cup hits the space bar
if (cc)
a()
b()
translates to these:
if (cc) {
a();
b();
}
if (cc) a();
b();
Could've warned me of this because I have the own
keyword there.
# I wanted to do this:
for own id of list
doStuff(id)
# But end up doing this
for own id in list
doStuff(id)
Coffeescript just blissfully generate code for both lines:
var id, _i, _len,
__hasProp = Object.prototype.hasOwnProperty;
for (id in list) {
if (!__hasProp.call(list, id)) continue;
doStuff(id);
}
for (_i = 0, _len = list.length; _i < _len; _i++) {
id = list[_i];
doStuff(id);
}
This does not work, generates PARSE ERROR ON LINE 2: UNEXPECTED 'INDENT'
FB.login
(response) -> doStuff(); return
scope: 'email'
This works:
FB.login(
(response) -> doStuff(); return
scope: 'email')
for(n=head; n!=null; n=n.next)
dostuff()
And there is no do-while loops - See: http://stackoverflow.com/questions/6052946/were-do-while-loops-left-out-of-coffeescript
https://gist.github.com/alanhogan/8b2f67f4bae9646e88e3/revisions