12

在咖啡脚本中构建类时,是否有理由使用粗箭头作为实例方法?

编辑:那好吧!很好的回复!:)
总而言之,问题是:
- 占用更多内存
- 无法修补
- 求问,为什么要使用这种方法?
约定:
- 绑定函数时要明确。
- 在构造函数中声明粗箭头方法。
- 尽可能多地使用,而不是在类声明中。

4

2 回答 2

18

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
于 2012-12-03T16:12:55.287 回答
12

让我添加我的另一种观点。

@epidemian 表达的避免胖箭头的详细原因很好,但请考虑以下几点:

  • 如果您不关心(非常或根本不关心)由 CoffeeScript 生成的“底层基于原型的 JS 代码”,只要您能够编写一致且无错误的 CoffeeScript 代码;
  • 如果你不打算像 Java 那样编写大量的小类,那将花费 99% 的时间在继承树上上下调用彼此的方法,并且在此过程中完成的工作很少;换句话说,如果您认识到性能敏感的“内部循环”不是放置方法调用的好地方;
  • 如果您不打算在运行时装饰、猴子修补或以其他方式修改类的方法;
  • 如果您在标题注释中说明您对胖箭头的使用,为了将来开发您的代码的开发人员的利益;

那么我建议始终将 fat arrow用作方法和匿名函数的习惯。

这将使您的 CoffeeScript 代码更简单、更安全和更直观,因为您将知道这一点this@始终引用您正在定义其方法的当前对象,就像在大多数其他编程语言中一样,与在运行时调用您的函数和方法的人无关.

更正式地说,胖箭头使this关键字(及其简写@)完全在词法范围内,就像任何其他标识符一样。编程语言历史表明,词法作用域是确定标识符作用域的最直观且不易出错的方法。这就是为什么它在很久以前成为所有新语言的标准行为。

如果您选择此路径,细箭头将成为例外,并且是一个有用的路径。您将使用它来准备那些需要this引用调用者在运行时定义的东西而不是您自己的对象的特定回调。这是一个反直觉的含义this,但是一些 JS 库在用户提供的函数中需要这种行为。然后细箭头将用于突出显示这些代码。如果我没记错的话,jQuery 通常会在函数参数中提供你需要的一切,所以你可以忽略它的人工this,但其他库就没有那么仁慈了。

注意:CoffeeScript 1.6.1 有一个与粗箭头方法相关的错误,因此您应该避免这种情况。以前和以后的版本应该没问题。

表现

当用作常规(匿名)函数时,胖箭头不会增加任何开销。在方法声明中,它确实增加了很小的RAM 和 CPU 开销(真的很小:每个方法调用几纳秒和几个字节的 RAM,而后者在具有尾调用优化的引擎上消失了。)

恕我直言,作为交换的语言清晰度和安全性胖箭头足以容忍甚至欢迎小额开销。许多其他 CoffeeScript 习惯用法在生成的代码(for 循环等)中添加了自己的微小开销,目的是使语言行为更加一致且不易出错。胖箭也不例外。

于 2013-07-02T17:16:57.103 回答