CoffeeScript, When to use fat arrow (=>) over arrow (->) and vice versa
When building a class in CoffeeScript, should all the instance method be defined using the =>
("fat arrow") operator and all the static methods being defined using the ->
operator?
Solution 1:
No, that's not the rule I would use.
The major use-case I've found for the fat-arrow in defining methods is when you want to use a method as a callback and that method references instance fields:
class A
constructor: (@msg) ->
thin: -> alert @msg
fat: => alert @msg
x = new A("yo")
x.thin() #alerts "yo"
x.fat() #alerts "yo"
fn = (callback) -> callback()
fn(x.thin) #alerts "undefined"
fn(x.fat) #alerts "yo"
fn(-> x.thin()) #alerts "yo"
As you see, you may run into problems passing a reference to an instance's method as a callback if you don't use the fat-arrow. This is because the fat-arrow binds the instance of the object to this
whereas the thin-arrow doesn't, so thin-arrow methods called as callbacks as above can't access the instance's fields like @msg
or call other instance methods. The last line there is a workaround for cases where the thin-arrow has been used.
Solution 2:
A point not mentioned in other answers that is important to note is that binding functions with fat arrow when it is not necessary can lead to unintended results such as in this example with a class we'll just call DummyClass.
class DummyClass
constructor : () ->
some_function : () ->
return "some_function"
other_function : () =>
return "other_function"
dummy = new DummyClass()
dummy.some_function() == "some_function" # true
dummy.other_function() == "other_function" # true
In this case the functions do exactly what one might expect and there seems to be no loss to using fat arrow, but what happens when we modify the DummyClass prototype after it's already been defined (e.g. changing some alert or changing the output of a log):
DummyClass::some_function = ->
return "some_new_function"
DummyClass::other_function = ->
return "other_new_function"
dummy.some_function() == "some_new_function" # true
dummy.other_function() == "other_new_function" # false
dummy.other_function() == "other_function" # true
As we can see overriding our previously defined function of the prototype causes some_function to be correctly overwritten but other_function remains the same on instances as fat arrow has caused other_function from the class to be bound to all instances so instances won't refer back to their class to find a function
DummyClass::other_function = =>
return "new_other_new_function"
dummy.other_function() == "new_other_new_function" # false
second_dummy = new DummyClass()
second_dummy.other_function() == "new_other_new_function" # true
Even fat arrow won't work as fat arrow only causes the function to be bound to new instances (which do gain the new functions as one would expect).
However this leads to some problems, what if we need a function (e.g. in the case of switching a logging function to an output box or something) that will work on all existing instances (including event handlers) [as such we can't use fat arrows in original definition] but we still need access to internal attributes in an event handler [the exact reason we used fat arrows not thin arrows].
Well the simplest way to accomplish this is to merely include two functions in the original class definition, one defined with a thin arrow which does the operations that you wish to execute, and another defined with a fat arrow that does nothing but call the first function for example:
class SomeClass
constructor : () ->
@data = 0
_do_something : () ->
return @data
do_something : () =>
@_do_something()
something = new SomeClass()
something.do_something() == 0 # true
event_handler = something.do_something
event_handler() == 0 # true
SomeClass::_do_something = -> return @data + 1
something.do_something() == 1 # true
event_handler() == 1 # true
So when to use thin/fat arrows can be summed up fairly easy in four ways:
-
Thin arrow alone functions should be used when the both conditions are mett:
- The method will never be passed by reference including event_handlers e.g. you never have a case such as: some_reference = some_instance.some_method; some_reference()
- AND the method should be universal over all instances so if the prototype function changes so does the method over all instances
-
Fat arrow alone functions should be used when the following condition is met:
- The method should be bound precisely to the instance at instance creation and remain permanently bound even if the function definition changes for the prototype, this includes all cases where the function should be an event handler and the event handler behaviour should be consistent
-
Fat arrow function which directly calls a thin arrow function should be used when the following conditions are met:
- The method is required to be called by reference such as an event handler
- AND the functionality may change in future affecting existing instances by replacing the thin arrow function
-
Thin arrow function which directly calls a fat arrow (not demonstrated) function should be used when the following conditions are met:
- The fat arrow function must be always attached to the instance
- BUT the thin arrow function may change (even to a new function which doesn't use the original fat arrow function)
- AND the thin arrow function is never needed to be passed by reference
In all approaches it must be considered in the case where the prototype functions might be changed whether or not behaviour for specific instances will behave correctly for example although a function is defined with a fat arrow its behaviour may not be consistent within an instance if it calls a method that is changed within the prototype
Solution 3:
Usually, ->
is fine.
class Foo
@static: -> this
instance: -> this
alert Foo.static() == Foo # true
obj = new Foo()
alert obj.instance() == obj # true
Note how the static method return the class object for this
and the instance returns the instance object for this
.
What's happening is that the invocation syntax is providing the value of this
. In this code:
foo.bar()
foo
will be the context of the bar()
function by default. So it just sorta works how you want. You only need the fat arrow when you call these function in some other way that does not use the dot syntax for invocation.
# Pass in a function reference to be called later
# Then later, its called without the dot syntax, causing `this` to be lost
setTimeout foo.bar, 1000
# Breaking off a function reference will lose it's `this` too.
fn = foo.bar
fn()
In both of those cases, using a fat arrow to declare that function would allow those to work. But unless you are doing something odd, you usually don't need to.
So use ->
until you really need =>
and never use =>
by default.