CoffeeScript模块的设计模式

29

在查看 Github 上的 CoffeeScript 源代码 时,我注意到大多数(如果不是全部)模块都定义如下:

(function() {
    ...
}).call(this);

这个模式看起来像是将整个模块包装在一个匿名函数中并调用它本身。

这种方法的优点(和缺点)是什么?有没有其他实现相同目标的方法?


5
请注意,您正在查看生成的 JavaScript 而不是编写的 CoffeeScript。 - Dykam
3个回答

78

Harmen的回答非常好,但让我稍微详细解释一下为什么这是由CoffeeScript编译器完成的。

当你使用coffee -c foo.coffee编译某个代码时,你会得到一个foo.js文件,其代码看起来像这样:

(function() {
  ...
}).call(this);

为什么呢?好吧,假设你进行了如下的赋值操作:

x = 'stringy string'

foo.coffee中,编译器会这样问:在当前作用域或者外部作用域是否已经存在x?如果不存在,它会在 JavaScript 输出的顶部放置一个var x声明。

现在假设你写下:

x = 42

bar.coffee 中编译两个文件,然后在部署时将 foo.jsbar.js 进行合并。你会得到:

(function() {
  var x;
  x = 'stringy string';
  ...
}).call(this);
(function() {
  var x;
  x = 42;
  ...
}).call(this);
因此,在foo.coffee中的xbar.coffee中的x是完全独立的。这是 CoffeeScript 的一个重要特点:变量永远不会从一个 .coffee 文件泄漏到另一个文件中,除非显式地导出(通过附加到共享全局或附加到 Node.js 中的 exports)。
您可以使用 -b("裸")标志覆盖此行为,但这只应在非常特殊的情况下使用。 如果您在上面的示例中使用它,则会得到以下输出。
var x;
x = 'stringy string';
...
var x;
x = 42;
...

这可能会带来严重的后果。想要自行测试,请尝试在foo.coffee中添加setTimeout(-> alert x), 1。请注意,您不必手动连接这两个JS文件 - 如果您使用两个单独的<script>标签将它们包含在页面中,则它们仍然有效地运行为一个文件。

通过隔离不同模块的作用域,CoffeeScript编译器使您免于担心项目中的不同文件可能使用相同的局部变量名称而感到头痛。这是JavaScript世界的常见做法(例如,查看jQuery源代码或几乎任何jQuery插件),但是CoffeeScript已经为您处理好了。


4
你的回答写得很好,真正帮助我更好地理解了CoffeeScript(和JavaScript)...谢谢! - James Sun
1
谁会像那样玩全局变量?我不会想做任何事情而没有类或闭包。必须明确导出是令人讨厌的。 - Duke
我在开发AngularJS时开始使用Coffeescript。AngularJS的策略是消除所有全局变量,因此在我的情况下,-b标记已经足够好了 :) - Roman M. Koss

19

这种方法的好处是它创建了私有变量,因此不会与变量名称发生冲突:

(function() {
  var privateVar = 'test';
  alert(privateVar); // test
})();

alert(typeof privateVar); // undefined

添加.call(this)使得this关键字引用外部函数中的相同值。如果不添加,则this关键字将自动引用全局对象。

下面是一个小例子来展示不同之处:

function coffee(){
  this.val = 'test';
  this.module = (function(){
    return this.val;
  }).call(this);
}

var instance = new coffee();
alert(instance.module); // test

function coffee(){
  this.val = 'test';
  this.module = (function(){
    return this.val;
  })();
}

var instance = new coffee();
alert(typeof instance.module); // undefined

1

这与此类似的语法:

(function() {

}());

这被称为立即函数。该函数被定义并立即执行。这样做的好处是,您可以将所有代码放在此块内,并将函数分配给单个全局变量,从而减少全局命名空间污染。它在函数内提供了一个良好的封闭作用域。

这是我编写模块时使用的典型模式:

var MY_MODULE = (function() {
    //local variables
    var variable1,
        variable2,
        _self = {},
        etc

    // public API
    _self = {
       someMethod: function () {

       }
    }

    return _self;
}());

不确定可能存在哪些缺点,如果有人知道任何缺点,我很乐意了解。


我认为缺点有两个:1)你需要更加努力地在模块之间共享状态(至少在CoffeeScript中是这样的——在JavaScript中,你可以省略“var”,但通常不建议这样做,例如通过jsLint),2)额外的函数调用和字节会带来一点点开销。但99%的情况下,额外的安全性是值得的。 - Trevor Burnham
不,这不是一个相关的答案。他特别在问(function(){}).call(this),而不是你举例的(function(){})()。Harmen的回答非常直接。 - Özgür

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接