在coffeescript中构建类时,有没有理由不使用胖箭头作为实例方法?

时间:2012-12-03 12:11:55

标签: memory-management coffeescript arrow-functions

在coffeescript中构建类时,是否有理由使用胖箭头作为实例方法?

编辑: 那好吧!很好的回复! :)总的来说,问题是:
- 需要更多记忆 - 无法修补
- 问题,为什么它用于这种方法?
公约:
- 绑定函数时要明确。
- 在构造函数中声明胖箭头方法。
- 尽可能多地使用,而不是在类声明中。

2 个答案:

答案 0 :(得分:18)

是的,有理由不使用胖箭。事实上我赞成从不使用胖箭方法:)

细箭和胖箭方法在概念上是不同的。前者被编译为预期的基于原型的JS代码;这些方法属于类原型。另一方面,胖箭头方法与构造函数代码中的每个实例相关联。

总是使用胖箭头方法最明显的缺点是它使每个类实例占用更多内存(因为它有更多自己的属性)并且它的初始化更慢(因为它必须创建这些绑定函数并设置它们每个创建实例的时间。)

使用胖箭头方法的另一个缺点是它打破了方法的通常期望:方法不再是类的实例之间共享的函数,但它现在是每个实例的单独函数。例如,如果您想要在类中定义方法后修改方法,则会导致问题:

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

但是我认为最重要的缺点是更主观:引入胖箭头方法会使代码(和语言)不必要地不一致且难以理解。

代码变得更加不一致,因为在引入fat-arrow方法之前,只要我们在类定义中看到<someProp>: <someVal>,我们就知道这意味着“声明一个名为<someProp>的属性,其值为<someVal>在类'prototype'中(除非是<someProp> == 'constructor',这是一个特殊情况),如果<someVal>是数字或函数并不重要,它只是原型中的属性。随着胖箭方法的引入,我们现在又有了一个不必要的特殊情况:如果<someVal>是一个胖箭头函数,它将完全不同于任何其他值。

还有另一个不一致之处:胖箭头bind the this differently在方法定义中使用时比在其他任何地方使用时要长。而不是保留外部this(在class内,this绑定到类构造函数),胖箭头方法中的this是一个对象定义方法时(即类的实例)不存在。

如果你混合使用精简箭头和胖箭头的方法,代码也变得更难以遵循,因为现在每当开发人员看到一个胖箭头的方法时,他们会问自己为什么需要 方法是实例绑定的。方法的声明与其使用位置之间没有直接关联,这是需要fat-arrow方法的地方。


对于这一切,我建议永远不要使用胖箭方法。首选将方法绑定到将要使用它的实例,而不是声明方法的位置。例如:

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

# Instead of:
$someJQueryElement.on 'click', someObject.onClick

或者,如果你真的想在构建时绑定每个实例上的方法,请明确说明:

# 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

我认为在class A的第二个定义中,foo方法发生的事情要比第一个定义更明确。

最后,请注意我根本不反对使用胖箭。这是一个非常有用的结构,我一直用它来进行正常的功能;我只是想避免在class方法定义中使用它:)


编辑:另一个反对使用胖箭头方法的案例:装饰函数:

# 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

答案 1 :(得分:12)

让我添加另一种观点。

@epidemian表达的避免胖箭的精心理由很好,但请考虑以下因素:

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

然后我建议总是使用胖箭,作为一种习惯,无论是方法还是匿名函数。

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

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

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

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

效果

当用作常规(匿名)函数时,胖箭头不会增加任何开销。在方法声明it does add a tiny RAM and CPU overhead中(非常小:每个方法调用几纳秒和几个字节的RAM,后者在尾调用优化的引擎上消失。)

恕我直言,语言清晰度和安全性脂肪箭交换是足以容忍甚至欢迎小额开销的理由。许多其他CoffeeScript习语为生成的代码(for循环等)添加了自己的微小开销,目的是使语言行为更加一致并且不易出错。胖箭没有什么不同。