使用 (function(window, document, undefined) { ... })(window, document) 有哪些优势?

64

我猜这种模式是最新的流行趋势,但我不理解其中的优点以及作用域的影响。

该模式:

(function(window, document, undefined){
  window.MyObject = {
    methodA: function() { ... },
    methodB: function() { ... }
  };
})(window, document)

关于这个问题,我有几个疑问。

封装对象是否有特殊优势?
为什么要传入 windowdocument ,而不是直接访问它们?
为什么要传入 undefined
将我们创建的对象直接附加到 window 上是一个特别好的主意吗?

我习惯使用我称之为 Crockford 风格的 JavaScript 封装(因为我从 Douglas Crockford 的 JavaScript 视频中得到了它)。

NameSpace.MyObject = function() {
  // Private methods
  // These methods are available in the closure
  // but are not exposed outside the object we'll be returning.
  var methodA = function() { ... };

  // Public methods
  // We return an object that uses our private functions,
  // but only exposes the interface we want to be available.
  return {

    methodB: function() {
      var a = methodA();
    },
    methodC: function() { ... }

  }
// Note that we're executing the function here.
}();

这两种模式中,有一种功能上比另一种更好吗?第一种是第二种的进化版本吗?


缓存 + 沙箱化。详情见下文 ;) - Christoph
4个回答

51

为什么要将window和document作为参数传入,而不是正常访问它们?

一般来说,将它们作为本地变量可以加速标识符解析过程(尽管我个人认为性能改进可能微不足道)。

在非浏览器环境中,传递全局对象也是一种广泛使用的技术,因为你无法在全局范围内使用window标识符,例如:

(function (global) {
  //..
})(this); // this on the global execution context is 
          // the global object itself

为什么会传递未定义的东西呢?

这是因为在ECMAScript 3中,全局属性undefined是可变的,意味着某些人可以更改它的值从而影响你的代码,例如:

undefined = true; // mutable
(function (undefined) {
  alert(typeof undefined); // "undefined", the local identifier
})(); // <-- no value passed, undefined by default

仔细观察会发现undefined实际上并没有被传递(函数调用中没有参数),这是一种获得undefined值的可靠方法,而不使用window.undefined属性。

在JavaScript中,名称undefined并没有什么特殊含义,它不像truefalse等关键字,只是一个标识符。

值得一提的是,在ECMAScript 5中,该属性被设置为不可写入...

我们直接将正在创建的对象附加到window上,这是一个特别好的做法吗?

当你在另一个函数范围内时,这是一种常用的声明全局属性的方式。


1
为什么要同时传递window和document?难道document不是window的属性吗? - Shaun Luttin
2
@ShaunLuttin 这样做只是使引用变得更快。不仅如此,这样你可以像大多数人一样使用它。你通常不会使用它作为: window.document.whatever(),而是使用它作为: document.whatever()。如果你不这样做而是使用"document.whatever()",实际上你正在使用全局window对象而不是scoped对象,即使它不够清晰。最后,它允许更好的最小化。Window可以成为"w",而document可以成为"d"。仅凭这一点就可以大幅减少脚本中的字符数。 - brunoais

27

这种风格相比于"Crockford"风格确实有一些好处。主要是传递windowdocument可以使脚本更有效地被压缩。一个压缩器可以将这些参数重命名为单字符名称,每个引用可以节省5和7字节。这可能会累加:jQuery引用window 33次和document 91次。将每个标记压缩到一个字符可以节省802字节。

此外,您确实可以获得执行速度的好处。当我第一次阅读@joekarl的说法时,它提供了性能优势,我想:"那似乎相当牵强"。所以我使用测试页面实际测试了性能。在Firefox 3.6中,引用window 一亿次,局部变量引用提供了适度的20%的速度增加(从4200毫秒到3400毫秒),在Chrome 9中则提供了惊人的31,000%的速度增加(从13秒到400毫秒)。

当然,您永远不会在实践中引用window 100,000,000次,即使直接引用10,000次在Chrome中也只需要1毫秒,因此这里的实际性能增益几乎可以忽略不计。

为什么要传递undefined

由于(正如@CMS所提到的),标记undefined实际上是未定义的。与null不同,它没有特殊含义,您可以像分配任何其他变量名一样自由地为此标识符赋值。(但是请注意,在ECMAScript 5中,不再成立。)

将我们创建的对象直接附加到window上是一个特别好的主意吗?

window对象是全局作用域,因此无论您是否明确编写“window”,在某些时候都会这样做。window.Namespace = {};Namespace = {};是等价的。

3
压缩的优势加一。 - Sprintstar
1
不要忘记 undefined -- 压缩器也会将此参数重命名为短名称,并重命名脚本中的所有引用。 - Roy Tinker

1

我认为这主要是针对需要在多个窗口上下文中运行的代码。比如说你有一个复杂的应用程序,其中有很多iframe和/或子窗口。它们都需要运行MyObject中的代码,但你只想加载一次。因此,你可以在任何你选择的窗口/框架中加载它,但你需要为每个窗口/框架创建一个MyObject,并引用正确的窗口和文档。

接受未定义的参数是为了防止未定义被更改:

undefined = 3;
alert(undefined);

请参阅CMS关于如何提高安全性的答案。


2
它并没有传递未定义的内容,而只是声明了一个名为undefined的参数,并且没有为该参数传递任何内容,这使得它实际上是未定义的。虽然不确定为什么在函数中优先使用这种方式而不是“var undefined”。 - Mark Bessey

1

我认同使用Crockford的风格。

回答你的问题:

1.封装对象有什么特别的优点吗?

我唯一能看到的优点是将窗口和文档作为局部变量而不是全局变量,可以通过不能直接覆盖它们来获得一些附加的安全性,并且它们都是局部的,从而获得一些性能上的优势。

2.为什么要传入窗口和文档而不是正常访问?

如上所述。本地变量往往更快,但是随着现在的jit编译,这变得微不足道了。

3.为什么会传入未定义的变量?

毫无头绪....

4.将我们创建的对象直接附加到窗口上是一个特别好的想法吗?

可能不是,但我仍然会坚持Crockford的模式,因为将函数附加到窗口对象将其通过窗口对象暴露给整个全局堆栈,而不是通过非标准命名空间公开它。


3
我认为传递undefined的原因是因为在某些实现中undefined理论上是可以被覆盖的(也就是说它并不是一个关键字,而只是一个最初未定义的全局变量)。所以你提供了第三个参数叫做"undefined",然后什么也不传入(这样它就真的是未定义的),这样你就避免了某些疯子定义全局undefined的情况。 - Platinum Azure
-1 是因为建议窗口和“全局堆栈”(全局作用域?)是不同的。 - Andy Balaam

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