【问题标题】:Why is Coffeescript of the opinion that shadowing is a bad idea为什么 Coffeescript 认为阴影是一个坏主意
【发布时间】:2015-11-09 13:10:59
【问题描述】:

我想切换到 Coffeescript 有一段时间了,昨天我以为我终于被卖掉了,但后来我偶然发现了 Armin Ronachers article 关于 Coffeescript 的影子。

Coffeescript 现在确实放弃了阴影,如果您对嵌套循环使用相同的迭代器,该问题的一个示例就是。

var arr, hab, i;

arr = [[1, 2], [1, 2, 3], [1, 2, 3]];

for(var i = 0; i < arr.length; i++){
  var subArr = arr[i];
  (function(){
      for(var i = 0; i < subArr.length; i++){
        console.log(subArr[i]);
      }
  })();
}

因为 cs 仅在我无法在咖啡脚本中执行此操作时才声明变量

阴影已被有意移除,我想了解为什么 cs 的作者想要摆脱这样的功能?

更新:这里是 better example,说明为什么影子很重要,源自与此问题有关的问题 on github

PS:我不是在寻找一个答案,告诉我我可以插入带有反引号的纯 Javascript。

【问题讨论】:

  • 这是一个功能吗?应始终避免使程序员感到困惑或更容易出错的事情。没有?
  • 没有明确声明,解释器怎么知道你的两个i变量是否应该是同一个?
  • @Alnitak 这正是我的观点,在 cs 之前,您可以通过 while i := 0 隐藏变量
  • 更令人困惑的是,这种没有阴影甚至不一致。如果您有多个文件,变量将被文件隐藏(除非明确设置为全局范围)。在这种情况下,函数参数会隐藏其他变量,但是在这个函数内部使用另一个变量不会隐藏任何其他同名变量。总的来说,CoffeeScript 反对影子,因为他们不知道自己在做什么。当 Ashkenas 说这是“一个巨大的概念简化”时,我听到“很难正确实施”。
  • 尽管使用了超过 50,000 行的咖啡脚本,但我从未被您描述的问题所困扰。我建议,如果这引起了问题,那么它表明存在更深层次的架构问题。

标签: javascript coffeescript


【解决方案1】:

如果您阅读 this ticket 上的讨论,您会看到 CoffeeScript 的创建者 Jeremy Ashkenas 解释了禁止显式阴影之间的一些推理:

我们都知道动态作用域与词法作用域相比是不好的,因为它很难推断变量的值。使用动态范围,您无法通过读取周围的源代码来确定变量的值,因为该值完全取决于调用函数时的环境。如果允许并鼓励变量隐藏,则如果不向后跟踪源中最接近的 var 变量,则无法确定变量的值,因为完全相同的局部变量标识符在相邻范围内可能具有完全不同的值。在所有情况下,当您想要隐藏变量时,只需选择更合适的名称即可完成相同的操作。如果局部变量名在整个词法范围内只有一个值,那么推理代码会容易得多,并且禁止使用阴影。

因此对于 CoffeeScript 来说,一块石头杀死两只鸟是一个非常慎重的选择——通过删除“var”概念来简化语言,并禁止将阴影变量作为自然结果。

如果您在 CoffeeScript 问题中搜索“范围”或“阴影”,您会发现它一直都在出现。我不会在这里发表意见,但要点是 CoffeeScript 创建者相信它会导致更简单的代码更不容易出错。

好的,我会说一点:阴影并不重要。你可以想出一些人为的例子来说明为什么这两种方法都更好。事实是,无论是否有阴影,您都需要“向上”搜索范围链以了解变量的生命周期。如果您显式声明变量 ala JavaScript,您可能能够更快地短路。但这没关系。如果您不确定给定函数的范围内有哪些变量,那您就错了。

Shadowing is 在 CoffeeScript 中是可能的,不包括 JavaScript。如果你实际上需要一个你知道是本地范围的变量,你可以得到它:

x = 15
do (x = 10) ->
  console.log x
console.log x

因此,如果在实践中出现这种情况的可能性很小,那么有一个相当简单的解决方法。

就个人而言,我更喜欢显式声明每个变量的方法,并将提供以下内容作为我的“论据”:

doSomething = ->
  ...
  someCallback = ->
    ...
      whatever = ->
        ...
        x = 10
        ...

这很好用。然后突然一个实习生走过来并添加了这行:

x = 20
doSomething = ->
  ...
  someCallback = ->
    ...
      whatever = ->
        ...
        x = 10
        ...

然后 bam,代码被破坏了,但是直到很久以后才出现破坏。哎呀!使用var,就不会发生这种情况。但是对于“通常是隐式范围,除非您另有说明”,它会。所以。无论如何。

我在一家在客户端和服务器上使用 CoffeeScript 的公司工作,我从未听说过这种情况在实践中发生。我认为不必在任何地方输入单词 var 所节省的时间比范围错误(永远不会出现)所浪费的时间要多。

编辑:

自从写下这个答案以来,我已经看到这个错误在实际代码中发生了两次。每次发生这种情况,都非常烦人且难以调试。我的感觉已经改变,认为 CoffeeScript 的选择是糟糕的时期。

一些类似于 CoffeeScript 的 JS 替代品,例如 LiveScript 和 coco,为此使用两种不同的赋值运算符:= 用于声明变量,:= 用于修改外部范围内的变量。这似乎是一个比仅仅保留 var 关键字更复杂的解决方案,而且一旦 let 被广泛使用,它也不能很好地支持。

【讨论】:

  • 谢谢!有时我对您在 SO 上获得的答案质量感到惊讶。
【解决方案2】:

这里的主要问题不是阴影,它的 CoffeeScript 将变量初始化和变量重新分配混为一谈,并且不允许程序员准确地指定他们的意图

当咖啡脚本编译器看到x = 1时,它不知道你的意思是不是

我想要一个新变量,但我忘记了我已经在上层范围内使用了该名称

我想为我最初在文件顶部创建的变量重新分配一个值

这不是您在语言中禁止影子的方式。这就是你如何创建一种语言来惩罚那些不小心重用变量名的用户,这些用户带有微妙且难以检测的错误。

CoffeeScript 本来可以设计为禁止阴影,但通过保留 var 将声明和赋值分开。编译器只会抱怨这段代码:

var x = blah()

var test = -> 
  var x = 0

带有“变量 x 已经存在(第 4 行)”

但它也会抱怨这段代码:

x = blah()

test = ->
  x = 0;

带有“变量 x 不存在(第 1 行)”

但是,由于 var 已被删除,编译器不知道您的意思是“声明”还是“重新分配”,因此无法提供帮助。

对两个不同的事物使用相同的语法并不“更简单”,即使它看起来很简单。我推荐Rich Hickey's talk, Simple made easy 他深入了解为什么会这样。

【讨论】:

  • 关于这个问题的大部分争论都是重述偏好差异。这个答案构成了讨论的实际进展。谢谢。
【解决方案3】:

因为 cs 只会在循环无法按预期工作时声明变量。

这些循环的预期工作方式是什么?如果arr 不为空,while i = 0 &lt; arr.length 中的条件将始终为真,因此它将是一个无限循环。即使只有一个 while 循环无法按预期工作(假设无限循环不是您要寻找的):

# This is an infinite loop; don't run it.
while i = 0 < arr.length
  console.log arr[i]
  i++

顺序迭代数组的正确方法是使用for ... in 构造:

arr = [[1,2], [1,2,3], [1,2,3]]

for hab in arr
  # IDK what "hab" means.
  for habElement in hab
    console.log habElement

我知道这个答案听起来像是在切线;重点是为什么 CS 不鼓励可变阴影。但是,如果要用例子来论证支持或反对某事,那么例子应该是好的。这个例子无助于鼓励变量阴影的想法。

更新(实际答案)

关于变量阴影问题,值得澄清的一件事是讨论是否应该在不同的函数作用域之间允许变量阴影,而不是块。在同一个函数范围内,变量将提升整个范围,无论它们首先被分配到哪里;这个语义继承自 JS:

->
  console.log a # No ReferenceError is thrown, as "a" exists in this scope.
  a = 5

->
  if someCondition()
    a = something()
  console.log a # "a" will refer to the same variable as above, as the if 
                # statement does not introduce a new scope.

有时被问到的问题是为什么不添加一种方法来显式声明变量的范围,例如 let 关键字(从而在封闭范围中隐藏其他同名变量),或者使 = 始终在该范围内引入一个新变量,并使用:= 之类的东西从封闭范围内分配变量,而无需在当前范围内声明一个。这样做的动机是避免这种错误:

user = ... # The current user of the application; very important!

# ...
# Many lines after that...
# ...

notifyUsers = (users) ->
  for user in users # HO NO! The current user gets overridden by this loop that has nothing to do with it!
    user.notify something

CoffeeScript 的论点是没有用于隐藏变量的特殊语法是您根本不应该做这种事情。清楚地命名你的变量。因为即使允许使用阴影,如果有两个具有两种不同含义的变量具有相同的名称,一个在内部范围内,一个在封闭范围内,这将是非常令人困惑的。

根据你有多少上下文使用适当的变量名:如果你没有多少上下文,例如一个顶级变量,您可能需要一个非常具体的名称来描述它,例如currentGameState尤其是如果它不是一个常量并且它的值会随时间变化);如果您有更多上下文,则可以使用描述性较低的名称(因为上下文已经存在),例如循环变量:killedEnemies.forEach (e) -&gt; e.die()

如果您想了解有关此设计决策的更多信息,您可能有兴趣阅读以下 HackerNews 线程中的 Jeremy Ashkenas 意见:linklink;或在讨论此主题的许多 CoffeeScript 问题中:#1121#2697 等。

【讨论】:

  • 感谢您指出这一点,我的示例是错误的,我使用this 转换我的 javascript 并且没有正确检查输出。我已经更新了示例,但是,您的回答并没有真正回答我的问题,我认为它应该是真正的评论。
  • @nimrod,评论太长了。您的新示例仍然不起作用:即使 var i 为每个 JS for 循环出现一次,也只有一个变量 i (将在两个循环之间共享),因为 JS 中的变量具有功能范围。你可以试试[这里]:它只会打印第一个子数组。此外,如果问题本身无效,则很难提供有效的答案;既然你已经更新了你的问题,也许我可以更新我的答案:D
  • 我傻了,又更新了,现在可以正常工作了:)
  • 酷;我也更新了我的答案。我希望现在可以更好地回答您的问题。此外,您可能对 do 关键字感兴趣,它可以在需要时有效地创建新的函数作用域(对于创建稍后执行的函数的循环非常有用)。
  • 感谢解答,很全面!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2015-09-11
  • 1970-01-01
  • 2017-01-31
  • 2011-06-22
  • 2011-11-29
  • 2019-04-07
  • 1970-01-01
相关资源
最近更新 更多