讨论闭包传入参数:window & undefined

引言

最常见的闭包 (Closure) 范式大家都很熟悉了:

(function() {
// ...
})();

很简单,大家都在用。但是,我们需要了解更多。
首先,闭包是一个匿名函数 (Anonymous function), 即是 (function() {}) 这部分。之所以要给 function 添加括弧是为了让它形成一个表达式 (expression), 有了表达式,并且确定它的类型是个函数 (Function 实例), 就可以直接调用它。所以,后面的一对括弧是可以工作的,它的意义是:我要调用 (call) 这个函数。

既然是函数调用,那就可以像一般的函数那样,在调用时传入参数。这就是本次讨论的话题。

传入 window 参数

(function(win) {
// ...
})(window);

这样做最直观的好处是书写便利:少写几个字。你可以在闭包内任何地方使用 win, 它都会指向 window 对象。另外,它有利于压缩减少最终代码的体积,经过压缩后 (如 Google Closure Complier), 所有的 win 都会被替换成形如 a 这样的简单变量。win 用得越多,减少的字节数也越多。

不过,便利的同时也会带来陷阱。在 IE 上,window 总是指向当前窗口对象,这个没有问题,但是在某些场景下,使用闭包内的 win 变量会导致拒绝访问错误 (Access denied). 重现方式大致是这样的:当页面引用其他域名的脚本,并且该脚本调用了闭包内的 window.document, 而且这个闭包代码是来自另一个域名的脚本。在这种情况下,使用 win 会保持对 window 最早的引用,通过另一个域的脚本访问 win 会导致 IE 认为脚本产生了跨越冲突,从而拒绝了对 win.document 的访问。解决办法是不使用形参 win, 而是直接使用 window. 需要说明的是,给闭包传入 document 也会导致 IE 出现同样的问题。

传入 undefined

其实把 undefined 作为形参就,实参就可以不用传了,因为 JavaScript 中访问未传入的参数就会得到 undefined. 因此,你可以这样写:

(function(undefined) {
// ...
})();

和上面的讨论一样,你可以在闭包内任何地方使用 undefined, 可以少写几个字(如果把 undefined 换成更短的名字),也可以在减少压缩后体积。

另一个的优势是,你可以认为它是个变量,把它当变量来使用,它的值恒等于 (===) 真正的 undefined. 当外部代码意外地定义了 undefined 的时候——不常见,但确实可能会发生——你可以正常地使用真正的 undefined, 而不会被外部的 undefined 意外影响. 这是由 JavaScript 作用域规则决定的。
无论是否使用这个 undefined 参数,都应该避免使用 undefined 的字符串常量,如:

if(typeof myVar === 'undefined') {
// bad part...
}

因为如果你把字符串写错了,机器不会告诉你,而且会产生一个难以检查出来的bug. 幸运的是,对于 JavaScript 来说,JsLint 可以帮你做这个校验。当 myVar 已定义的时候(通过形参或 var 声明),上面的代码改成这样会更易于调试:

if(myVar === undefined) {
// good part...
}

结论

从上面两个例子来看,我们建议不要传入 window, 但是可以安全地使用第二种方式 (写 undefined 形参);我们还要尽量避免使用字符串常量。
最后,最重要的是,这只是两个特定对象和类型的讨论,举一反三,你会更了解 JavaScript :-)

已有16条评论
  • catge 2010-05-17 12:01:06
    每次听大伯的课都会有收获~
    回复
  • sofish 2010-05-17 12:13:38
    顶大伯~
    回复
  • Ethan 2010-05-17 13:00:32
    赞大伯,细节讲得如此详细
    回复
  • lavi 2010-05-18 02:44:10
    http://www.f1play.com.cn/blog/post/145.html 修正了没有?
    回复
  • 太伯 2010-05-18 03:00:50
    @lavi: 谢谢你的反馈,请问你是从哪里点进去的?
    回复
  • Bernie 2010-05-18 15:12:19
    对第一点不是很明白 难道script不是加载到当前浏览器然后执行的马? 如果脚本来自其他域,当当前浏览器load完脚本运行时传入的参数难道不是当前浏览器的window对象?
    回复
  • 风速 2010-05-19 04:22:58
    学习了,谢谢
    回复
  • 太伯 2010-05-19 06:20:27
    @Bernie: 这是我们在一个JSONP应用中发现的IE bug. 主要原因是IE持有了最初通过闭包传入的 window 对象,由于未知原因,它被判断跨域冲突,直接使用 window 是没有问题的。
    回复
  • argb 2010-05-21 17:11:03
    if(myVar === undefined) { // good part... } 也有缺点,如果myVar 没有declare将会报错。 而if(typeof myVar === 'undefined') { }不会~
    回复
  • 太伯 2010-05-23 02:33:20
    @argb: 是的,所以文中强调了变量须“通过形参或 var 声明”。
    回复
  • chencheng 2010-05-24 13:47:29
    第一个问题能搞个demo吗?
    回复
  • HTC论坛 2010-05-30 08:27:05
    js真伟大。
    回复
  • afc163 2010-06-18 11:46:17
    我是今天您面的那个学生,看到你的文章了,很兴奋,学习ing
    回复
  • black 2011-04-18 08:46:31
    第一个问题能提供个demo吗?
    回复
我要评论