16

我想切换到 Coffeescript 有一段时间了,昨天我以为我终于被卖掉了,但后来我偶然发现了Armin Ronachers关于 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-authors 想要摆脱这样的功能?

更新:这是一个更好的例子,说明为什么影子很重要,源自github 上有关此问题的问题

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

4

3 回答 3

27

如果您阅读了有关此票证的讨论,您可以看到 CoffeeScript 的创建者 Jeremy Ashkenas 解释了禁止显式阴影之间的一些推理:

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

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

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

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

在 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
        ...

并且砰,代码被破坏了,但是直到很久以后才出现破坏。哎呀!有了var,就不会发生这种情况。但是对于“通常是隐式范围,除非您另有说明”,它会。所以。反正。

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

编辑:

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

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

于 2013-03-05T16:08:50.447 回答
7

这里的主要问题不是阴影,它的 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 的演讲,Simple made easy,他深入探讨了为什么会这样。

于 2013-12-11T00:23:15.027 回答
2

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

这些循环的预期工作方式是什么?如果 不为空,则in 的条件while i = 0 < arr.length将始终为真arr,因此它将是一个无限循环。即使只有一个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) -> e.die().

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

于 2013-03-05T14:02:37.417 回答