JavaScript中的“x = x || {}”技巧是什么 - 它如何影响这个IIFE?

3

首先,一个伪代码示例:

;(function(foo){

    foo.init = function(baz) { ... }

    foo.other = function() { ... }

    return foo;

}(window.FOO = window.FOO || {}));

这样调用:

FOO.init();

我的问题:

  • window.FOO = window.FOO || {} 的技术名称/描述是什么?

我了解代码的作用...请参见下面我提问的原因。


提问的原因:

我这样调用传递的全局变量:

;(function(foo){
    ... foo vs. FOO, anyone else potentially confused? ...
}(window.FOO = window.FOO || {}));

我不喜欢把小写的"foo"这样称呼它,考虑到全局变量被称为大写的FOO,这似乎会造成混淆。

如果我知道这个技术的技术名称,我可以这样说:

;(function(technicalname){
    ... do something with technicalname, not to be confused with FOO ...
}(window.FOO = window.FOO || {}));

我看到一个最近(很棒的)例子,他们称之为 "exports":

;(function(exports){
    ...
}(window.Lib = window.Lib || {}));

我想我只是想统一我的编码约定... 我想学习专业人士的做法和思考方式(这就是为什么我在这里问的原因)!


你可以将参数称为 FOO。这将隐藏 window.FOO,除非你使用 window.FOO 显式地引用它。 - icktoofay
4
在JavaScript中,"var foo = foo"的意思是声明一个变量foo并将其赋值为undefined。这种写法通常用于在一个作用域中重新定义一个已存在的变量,以确保该变量具有正确的作用域和初始值。 - user166390
1
它们是同一个对象。可以自由地给它们相同的名称或不同的名称。alert(window.FOO === foo); // true 没有技术名称。它取决于它的用途。如果你要暴露一个库,称其为你的库。如果你要导出一个 Foo 构造函数,请称其为 Foo 构造函数。 - I Hate Lazy
感谢大家的评论,我非常感激你们的帮助。@icktoofay我从来没有想过在IIFE内部引用window.FOO;我总是引用它的Alias(感谢zzzzBov)。所以,使用FOO对我来说是可行的。@pst谢谢您提供的链接!很抱歉我错过了那个帖子。@user1689607感谢你的澄清,我非常感谢!感谢大家的帮助......我欠你们几瓶俄勒冈小麦酒。 :) - mhulse
4个回答

4
模式
(function (foo) {
    ...code...
    foo.bar = baz;
    ...more code...
}(window.FOO = window.FOO || {});

您所描述的模式没有正式名称,因为它是三个独立的模式组合而成。每个模式都有多个名称,但在本文中,我将使用以下术语:

  • 闭包
  • 别名
  • 命名空间扩展

闭包

整个模式的基础是闭包。它只是一个用于限定变量和函数的作用域的函数,以使它们不会污染全局命名空间:

无闭包
//these declare window.foo and window.bar respectively
//as such, they pollute the global namespace
var foo;
function bar() {}

在这种情况下,Closure是指立即执行函数表达式(IIFE)
(function () {
    //these declare foo and bar within the function
    //but they are not accessible outside the function
    var foo;
    function bar() {}
}());

闭包内保留变量的优点在于,您不必担心其他人会覆盖您正在使用的变量。对于经常使用的临时变量,如ij,这尤其重要。

别名

此模式的第二个重要部分是别名。别名允许在闭包内定义和使用变量,而无需担心它所属的全局命名空间。 没有别名
(function () {
    ...
    foo = window.SomeFunction(bar, baz);
    ...
}());
使用别名
(function (sf) { //local name
    ...
    foo = sf(bar, baz);
    ...
}(window.SomeFunction)); //global namespace

这一点尤其重要,因为它意味着可以通过在单个位置更改名称,在整个大型JavaScript文件中更改全局命名空间。这是一个好事™。此外,缩小器可以将内部别名缩短为单个字母变量名,例如a,从而在缩小时节省大量字节。

命名空间扩展

命名空间扩展模式依赖于或运算符(||)的合并行为。在许多语言中,&&||返回truefalse,但在JavaScript中,&&返回第一个falsey值(false0''nullundefined),||返回第一个truthy值(任何不是falsey的东西)。对于两个运算符,如果未找到相应类型,则返回最后一个参数。这使得||运算符成为仅在不存在时定义新命名空间的方便方法

没有命名空间扩展
if (typeof window.Foo === 'undefined') {
    window.foo = {};
}
使用命名空间扩展
window.foo = window.foo || {};

这很有用,因为它允许命名空间扩展附加属性和方法,而不必担心这些属性和方法的定义顺序。
在第一个示例中,需要在执行FileB之前执行FileAFileA.js
window.foo = {};
window.foo.bar = 'baz';
FileB.js
window.foo.fizz = 'buzz';

在这个第二个例子中,File1File2可以以任何顺序执行: File1.js
window.foo = window.foo || {};
window.foo.bar = 'baz';
File2.js
window.foo = window.foo || {};
window.foo.fizz = 'buzz';

一起来

将每个模式结合在一起可以创建一个非常强大的模块化脚本:

//use foo internally so that you don't have to worry about
//what the global namespace is called
(function (foo) {
    //declare variables internally that you want to keep local to the script
    var i,
        len,
        internal,
        qux;
    //declare functions/properties on the alias when you want to expose them
    foo.bar = function () {...};
//extend the global namespace so that existing extensions are persistent
}(window.FOO = window.FOO || {}));

谢谢@zzzzBov!你的回答(以及其他所有回答)真的帮了我很多!我可能会在你的回复中添加Pete关于它被称为“Null Coalescing”的信息。再次感谢!我非常感激专业的帮助! - mhulse
我忘了说:感谢指出我正在使用三个模式!我不知道。由于某种原因,我认为我只是在使用“单例”IIFE。(我感觉像个新手!) :) - mhulse
@MickyHulse,从某种程度上说,JavaScript的||运算符类似于C#中的null合并运算符,但它不同之处足以使我不称其为一个null合并运算符。至于使用多个模式,汽车通常符合一个描述模式,但它由许多较小的模式组成,比如车轮轴和车门等,所有这些都汇聚在一起才构成了整个汽车。 - zzzzBov
啊,我现在明白了。谢谢@zzzBov!我真的很感激你(还有其他人)抽出时间帮助像我这样的新手。 :) - mhulse
我不会使用“合并”这个术语 - 它通常被称为“短路求值”。如果您知道||运算符的左操作数是“真实”的,则右操作数的值是无关紧要的。 - Alnitak
@Alnitak,许多使用&&||的语言都存在短路评估行为,但在大多数其他语言中,&&||将返回布尔值,而ECMAScript返回操作数的值。这就是为什么我使用“合并”的原因。 - zzzzBov

2
我一直将其理解为"Null Coalescing"
至于对IIFE的影响,如果已经实例化,则传入window.FOO,否则传入空对象。
你也可以这样理解:
window.FOO = window.FOO || {};
;(function(foo){

    foo.init = function(baz) { ... }

    foo.other = function() { ... }

    return foo;

}(window.FOO));

个人而言,我更喜欢另一种模式:
var FOO;
if (!FOO) {
    FOO = {};
}
(function () {
    "use strict";
    FOO.prop1 = 'bar';
    FOO.bar = function (z) {
        return z + 1;
    };
}());

我认为这样做不会让人感到困惑,并且有助于确保命名空间的清晰。

谢谢 @pete!这是非常有用的信息... "Null Coalescing" 对我来说是一个新术语!我现在正在研究它,感谢您的专业帮助和示例! :) - mhulse

2
为了补充Pete的回答,这是一个替代形式:
;(function() {
  var Foo = window.Foo = window.Foo || {};

  Foo.foo  = 'bar';
  Foo.baz  = function() {
    return "Hello World!";
  };
})();

我通常使用本地变量var,以便让压缩程序有机会节省一些字节。当你在命名空间的几个深度级别上工作时,例如var MyView = window.MyApp.Views.MyView,这将产生更大的影响。


谢谢Renato!非常有帮助。看到多个例子真是太好了...我从你们所有厉害的JS大师那里学到了很多! :) - mhulse

1

正如其他人所指出的那样,您的第一个问题与第二个问题完全独立无关。除了您决定将它们合并成一个语句而不是两个语句之外,它们没有任何关系。

对于您的第一个问题,道格拉斯·克罗克福德称其为默认赋值。也就是说,如果变量存在,则保持不变,否则将其初始化为指定的默认值。当您看到类似以下内容的东西时:

foo = foo || {};

你的心灵之眼应该将其阅读为:

foo defaults to `{}`

技术上来说,它实际上是“如果foo为falsy,则将{}分配给foo”,但我们假设任何falsy的东西(如零、null或undefined)在使用时都是foo的无效值。此外,单词default已经意味着“如果未定义foo”。

但是,说了那么多,知道你的第二个问题后,很明显default不是一个适当的参数名字传递到你的IIFE中。该参数所做的就是将方法和属性简单地附加到传入的对象上。在这种情况下,“exports”是适当的,就像“这些是对象的公共导出成员”。我认为“attach”也是一个适当的名称,例如“将这些内容附加到对象上”。我个人的首选只是将其命名为obj,即对象,这是您希望传入的类型。


谢谢slebetman!非常有帮助!很抱歉我的问题最初有点混乱...我对一些高级技术和概念仍然很陌生。我真的很感激你(以及所有回答我的问题并教授新知识的人)花时间回答我的问题。 :) - mhulse

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