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

Posted

技术标签:

【中文标题】在咖啡脚本中构建类时,是不是有理由不使用实例方法的粗箭头?【英文标题】:When building classes in coffeescript, is there ever a reason to not use the fat arrow for the instance methods?在咖啡脚本中构建类时,是否有理由不使用实例方法的粗箭头? 【发布时间】:2012-11-20 21:40:36 【问题描述】:

在 coffeescript 中构建类时,是否有理由将粗箭头用于实例方法?

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

【问题讨论】:

也许有帮助:***.com/questions/8965855/… 在不需要的时候不使用粗箭头,它更轻。不过,不确定这是否很重要。 所以这只是性能问题? 是的。它还避免了意想不到的后果。您应该始终知道为什么要向默认行为添加某些内容。稍后在您的项目中,您将不会记得所有内容都使用粗箭头绑定,并且您将追逐否则永远不会发生的错误。 有时将实例用作闭包对象并将方法作为回调给另一个方法可能很有用。 【参考方案1】:

是的,有理由不总是使用粗箭头。事实上,我赞成 never 使用粗箭头方法:)

细箭头和粗箭头方法在概念上是不同的东西。前者编译成预期的基于原型的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

但在我看来,最重要的缺点更加主观:引入粗箭头方法会使代码(和语言)不必要地不一致且难以理解。

代码变得更加不一致,因为在引入胖箭头方法之前,任何时候我们在类定义中看到<someProp>: <someVal>,我们都知道这意味着“在类的原型中声明一个名为<someProp> 的属性,其值为<someVal>” (除非<someProp> == 'constructor',这是一个特例),<someVal> 是数字还是函数都没有关系,它只是原型中的一个属性。随着粗箭头方法的引入,我们现在有了另一个不必要的特殊情况:如果 <someVal> 是一个粗箭头函数,它将与任何其他值完全不同。

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

如果你混合使用细箭头和粗箭头方法,代码也会变得更难理解,因为现在每次开发人员看到粗箭头方法时,他们都会问自己为什么需要它 em> 方法被实例绑定。方法的声明和它的使用位置之间没有直接的关联,这就是需要使用胖箭头方法的地方。


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

# 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

【讨论】:

至于装饰器的问题是不是和无法打补丁本质上是一样的问题? @SimonLandeholm,我认为它们是不同的问题。在装饰器的情况下,胖箭头没有做预期的事情:不是将this绑定到构造后的实例,而是将其绑定到类构造函数(如果你只是add parens around the fat arrow function,也会发生同样的情况)。无法修补是因为打破了方法的 this 是惰性绑定的假设(例如,[].slice.call(arguments) 是依赖于该假设的常见 JS 构造) @epidemian 很好的答案!不过,有没有办法从胖箭头函数内部访问类属性?我看到 @bar 在您的 fatArrowedFoo 函数中未定义。我想我是在想办法让我的蛋糕也吃起来.. :) @Goran_Mandic,如果你 bind them on the constructor,你可以在方法上使用装饰器。在构造函数中,this 将是一个A 实例,而不是A 本身。但是在类定义中,thisA,Coffee 无法知道某个任意表达式(即profiled (baz) -> ...)中的粗箭头函数实际上是绑定到A 实例而不是绑定到A 本身。希望这是有道理的:P 它确实如此,实际上.. 现在我意识到,你的回答也包含了我的问题的答案,一直以来。我想它只是有点消化一次。非常感谢!【参考方案2】:

让我添加我的替代视图。

@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 循环等)中添加了自己的微小开销,目的是使语言行为更加一致且不易出错。胖箭头也不例外。

【讨论】:

嗯,我不确定我是否真的会推荐您的方法 - 从表面上看它似乎是正确的 - 绝对如此。但是,我认为您指定的有关猴子修补等的前提条件是现实中无法实现的,这会造成麻烦。除此之外 - 一个非常有趣的方法。如果你已经链接到我的问题和答案,也许会很好;-) 我相信这在很大程度上取决于项目的类型。在一个大中型项目(10k 到 Ms 的代码行),或者一个旨在成为这样的项目,具有这种一致性是一件好事。知道在谁的类中的每个@ 总是意味着“正在定义的类的当前实例的属性”是无价的。毫不奇怪,猴子补丁和其他动态代码更改通常在此类项目中不受欢迎,原因相同:它使您更难(更慢)理解您未编写的代码,因此最终成本更高。 (赞成,对此感到抱歉) 感谢您的回答。实际上,我正在编写的 Web 应用程序中几乎就是这样做的。特别是,express 需要一个将 (req, res, next) 作为参数的函数,然后由您在处理时保留这些对象。鉴于我想做的大部分事情都涉及到数据库或网络的 I/O,我迷失在许多嵌套回调中。这是一种将reqres 绑定到我需要的任何回调的方法;非常有用。 @owensmartin 不客气。事实上,我通常将我的回调定义为带有描述性名称的胖箭头方法,以便我可以将它们作为值传递给库:someLibrary.doSomething 1, 2, @somethingWasDone 这将我的代码变成了方法调用的线性序列,没有嵌套的金字塔@ 987654335@ 和相关的变量范围问题。 即使是 javascript 创建者也说在今天使用原型继承并不是一个好主意。 youtube.com/watch?v=bo36MrBfTk4#t=1896

以上是关于在咖啡脚本中构建类时,是不是有理由不使用实例方法的粗箭头?的主要内容,如果未能解决你的问题,请参考以下文章

构建咖啡脚本代码?

咖啡脚本检查是不是不在数组中

将咖啡脚本合并到您的节点项目中?

是否有任何理由使用一个 DataContext 实例,而不是几个?

使用咖啡脚本的正确时间

是否可以在咖啡脚本评论中添加哈希