When building classes in coffeescript, is there ever a reason to not use the fat arrow for the instance methods?

前端 未结 2 866
名媛妹妹
名媛妹妹 2020-12-03 05:57

When building classes in coffeescript, is there ever a reason to not use the fat arrow for instance methods?

Edit: Ok then! Great reply! :)

相关标签:
2条回答
  • 2020-12-03 06:23

    Let me add my alternative view.

    The elaborate reasons expressed by @epidemian for avoiding fat arrow are well and good, but consider the following:

    • if you don't care (much, or at all) about the "underlying prototype-based JS code" generated by CoffeeScript, insofar as you are able to write consistent and bug-free CoffeeScript code;
    • if you don't plan to write a gazillion tiny classes ala Java, that will spend 99% of their time calling each other's methods up and down the inheritance tree and get little work done in the process; said another way, if you recognize that performance-sensitive "inner loops" are not a good place to put method calls;
    • if you don't plan on decorating, monkey-patching, or otherwise modifying your classes' methods at runtime;
    • if you state your usage of fat arrow in a header comment, for the benefit of future developers working on your code;

    then I would recommend to always use fat arrow, as a habit, both for methods and for anonymous functions.

    This will make your CoffeeScript code simpler, safer and more intuitive, because you will know that this and @ always refer to the current object whose method you are defining, as in most other programming languages, independently of whoever will be calling your functions and methods at runtime.

    Stated more formally, fat arrow makes the this keyword (and its shorthand @) fully lexically scoped, like any other identifier. Programming language history shows that lexical scoping is the most intuitive and less error-prone way to scope identifiers. That's why it became the standard behaviour for all new languages a long time ago.

    If you choose this path, thin arrow becomes the exception, and a useful one at that. You will use it to prepare those particular callbacks where you need this to refer to something defined at runtime by the caller, instead of your own object. This is a counter-intuitive meaning of this, but some JS libraries need this kind of behavior in user-supplied functions. The thin arrow will then serve to highlight those pieces of code. If I remember correctly, jQuery usually provides everything you need in the function arguments, so you can ignore its artificial this, but other libraries are not as benevolent.

    NB: CoffeeScript 1.6.1 has a bug related to fat arrow methods, so you should avoid that. Previous and later versions should be ok.

    Performance

    When used as a regular (anonymous) function, fat arrow does not add any overhead. In method declarations it does add a tiny RAM and CPU overhead (really tiny: a few nanoseconds and a few bytes of RAM for each method call, and the latter disappears on engines with tail-call optimization.)

    IMHO the language clarity and safety fat arrow gives in exchange is enough reason to tolerate and even welcome the small overhead. Many other CoffeeScript idioms add their own tiny overhead to the generated code (for loops, etc.) with the purpose of making the language behaviour more consistent and less error-prone. Fat arrow is no different.

    0 讨论(0)
  • 2020-12-03 06:24

    Yes, there are reasons to not use the fat arrows always. In fact i'd argue in favour of never using fat-arrowed methods :)

    Thin-arrow and fat-arrow methods are conceptually different things. The former are compiled to the expected prototype-based JS code; the methods belong to the class prototype. Fat-arrowed methods, on the other hand are associated with each instance in the constructor's code.

    The most obvious disadvantage of always using fat-arrowed methods is that it makes each class instance take more memory (because it has more own properties) and its initialization be slower (because it has to create those bound functions and set them each time an instance is created).

    Another disadvantage of using fat-arrow methods is that it breaks the usual expectation of what a method is: a method is no longer a function shared between the instances of a class, but it now is a separate function for each instance. This can cause problems if, for example, you'd want to modify a method after it has been defined in the class:

    class Foo
      # Using fat-arrow method
      bar: (x) => alert x
    
    # I have some Foos
    foos = (new Foo for i in [1..3])
    
    # And i want to path the bar method to add some logging. 
    # This might be in another module or file entirely.
    oldbar = Foo::bar
    Foo::bar = (args...) ->
      console.log "Foo::bar called with", args
      oldbar.apply @, args
    
    # The console.log will never be called here because the bar method 
    # has already been bound to each instance and was not modified by 
    # the above's patch.
    foo.bar(i) for foo, i in foos
    

    But the most important disadvantage in my opinion is more subjective: introducing fat-arrow methods makes the code (and the language) unnecessarily inconsistent and difficult to understand.

    The code becomes more inconsistent because before introducing fat-arrow methods any time we see <someProp>: <someVal> in a class definition we know it means "declare a property named <someProp> with a value <someVal> in the class' prototype" (unless <someProp> == 'constructor', which is a special case), it doesn't matter if <someVal> is a number or a function, it will just be a property in the prototype. With the introduction of fat-arrowed methods we now have another unnecessary special case: if <someVal> is a fat-arrowed function it will do a completely different thing than with any other value.

    And there's another inconsistency: fat arrows bind the this differently when they are used in a method definition than when used anywhere else. Instead of preserving the outer this (which, inside a class, this is bound to the class constructor) the this inside a fat-arrowed method is an object that does not exist when the method is defined (i.e. an instance of the class).

    If you mix thin-arrowed and fat-arrowed methods the code also becomes harder to follow because now every time a developer sees a fat-arrowed method they'll ask themselves why was it needed that for that method to be instance-bound. There's no immediate correlation between the method's declaration and where it's being used, which is where the need for fat-arrow method arises.


    For all this, i'd recommend to never use fat-arrow methods. Prefer binding the method to an instance where it's going to be used instead of where the method is declared. For example:

    # Be explicit about 'onClick' being called on 'someObject':
    $someJQueryElement.on 'click', (e) -> someObject.onClick e
    
    # Instead of:
    $someJQueryElement.on 'click', someObject.onClick
    

    Or, if you really want to bind the method on every instance on construction time, be explicit about that:

    # Instead of fat-arrow methods:
    class A
      constructor: ->
        @bar = 42
      foo: => 
        console.log @bar
    
    # Assing the method in the constructor, just like you would 
    # do with any other own property
    class A
      constructor: ->
        @bar = 42
        @foo = => 
          console.log @bar
    

    I think that in the second definition of class A it's much more explicit what is happening with the foo method than in the first definition.

    Finally, notice that i'm not arguing against using the fat arrow at all. It's a very useful construct and i use it all the time for normal functions; i just prefer to avoid using it inside a class method definition :)


    Edit: Another case against using fat-arrowed methods: decorator functions:

    # A decorator function to profile another function.
    profiled = (fn) ->
      (args...) ->
        console.profile()
        fn.apply @, args
        console.profileEnd()
    
    class A
      bar: 10
    
      # This works as expected
      foo: profiled (baz) ->
        console.log "@bar + baz:", @bar + baz
    
      # This doesn't
      fatArrowedFoo: profiled (baz) =>
        console.log "@bar + baz:", @bar + baz
    
    (new A).foo 5           # -> @bar + baz: 15
    (new A).fatArrowedFoo 5 # -> @bar + baz: NaN
    
    0 讨论(0)
提交回复
热议问题