使用RequireJS,我如何传递全局对象或单例?

54

假设我正在编写主页面级别的代码,有两个依赖项需要同一个对象实例,并将其作为依赖项。应该如何处理?

基本上我想说,“如果这个依赖项没有加载...那么加载它。否则,使用已经加载的相同实例并传递给它。”

6个回答

58

你可以将它作为模块级变量。例如:

// In foo.js
define(function () {
    var theFoo = {};

    return {
        getTheFoo: function () { return theFoo; }
    };
});

// In bar.js
define(["./foo"], function (foo) {
    var theFoo = foo.getTheFoo(); // save in convenience variable

    return {
        setBarOnFoo: function () { theFoo.bar = "hello"; }
    };
}

// In baz.js
define(["./foo"], function (foo) {
    // Or use directly.
    return {
        setBazOnFoo: function () { foo.getTheFoo().baz = "goodbye"; }
    };
}

// In any other file
define(["./foo", "./bar", "./baz"], function (foo, bar, baz) {
    bar.setBarOnFoo();
    baz.setBazOnFoo();

    assert(foo.getTheFoo().bar === "hello");
    assert(foo.getTheFoo().baz === "goodbye");
};

1
这就是在没有全局变量的情况下实现全局变量。现在你可以随意在任何地方写入全局 foo 对象了。 - Raynos
3
@Raynos说的是错误的;它实现了单例模式,这是面向对象编程中解决类似于过程式语言中全局变量问题的一种方法。 - Domenic
8
此外,它不会污染全局命名空间。只有那些显式声明 foo 为依赖项的模块才能使用它。 - Domenic
1
@Raynos,听起来你遇到了RequireJS的模块系统和JavaScript的扩展属性特性的问题,而不是我在这里提供的特定解决方案。OP明确要求barbaz都能够访问“同一个对象实例”。 - Domenic
1
@Juliusz Gonera:这就是为什么在Java或C#中不会暴露公共字段,而是使用getter的原因:封装。如果我直接返回theFoo,其他模块可能会执行foo.theFoo =“不再是对象了,笨蛋!”,这将破坏一切。 - Domenic
显示剩余12条评论

8
只需像平常一样为您的单例提供一个API即可。确保它是懒加载的。最简单的方法是使用一个抽象库,比如underscore,它提供了跨浏览器的帮助程序。其他选项是ES5 Object.defineProperty或自定义getter/setters。在这种情况下,_.once 确保构造函数的结果在第一次调用后被缓存,它基本上是惰性加载它。
define(function() {
    var constructor = _.once(function() { 
        ...
    });

    return {
        doStuffWithSingleton: function() {
            constructor().doStuff();
        }
    };

});

来自underscore的_.once函数。


@Domenic 如果你使用 _.once,它会起作用。如果你不能使用 ES5,我实际上建议使用 _.once 或自己的惰性加载 getters。 - Raynos
我明白你的意思。但是在baz模块中,您仍然可以执行foo.lazyloaded.bar = "not me"; 使用构造函数而不是theFoo = {}并不能解决任何问题,尽管如果OP确实需要以那种方式构建对象,它当然更强大。 - Domenic
@Domenic 我意识到这是个问题。所以我改变了我的答案,不会真正给予对懒加载对象的访问权限,因为那样会使其过于公开。 - Raynos
非常好:)。现在我认为我们已经涵盖了所有内容......您的答案提供了更好的最佳实践解决方案,而我的则涵盖了OP对“同一对象实例”的具体(也许是误导性的)请求。 - Domenic
使用整个库来实现单例模式似乎有些过度了。被接受的答案也不完全是一个单例模式 - 只是一种变量工厂。这里有一个简单的JS单例模式示例:http://www.dofactory.com/javascript/singleton-design-pattern - Hal50000

6

结合 Raynos 对封装的关注和 OP 的澄清,即他想在消息服务中公开一些方法,我认为以下是正确的做法:

// In messagingServiceSingleton.js
define(function () {
    var messagingService = new MessagingService();

    return {
        notify: messagingService.listen.bind(messagingService),
        listen: messagingService.notify.bind(messagingService)
    };
});

// In bar.js
define(["./messagingServiceSingleton"], function (messagingServiceSingleton) {
    messagingServiceSingleton.listen(/* whatever */);
}

// In baz.js
define(["./messagingServiceSingleton"], function (messagingServiceSingleton) {
    messagingServiceSingleton.notify(/* whatever */);
}

Function.prototype.bind不会在所有浏览器中都存在,因此您需要包含类似Mozilla提供的填充程序。

另一种(我个人认为可能更好的)方法是将消息服务对象本身作为模块。这将看起来像:

// In messagingService.js
define(function () {
    var listenerMap = {};

    function listen(/* params */) {
        // Modify listenerMap as appropriate according to params.
    }
    function notify(/* params */) {
        // Use listenerMap as appropriate according to params.
    }

    return {
        notify: notify
        listen: listen
    };
});

由于您向所有使用您的模块的人公开了相同的“notify”和“listen”方法,并且这些方法始终引用相同的“listenerMap”变量(它是私有的),因此这应该符合您的要求。这还消除了对Function.prototype.bind的需要,并消除了消息服务本身与强制其单例使用的模块之间的相当不必要的区别。

有什么理由更喜欢 obj.f.bind(obj) 而不是 function() { obj.f(); }。另外,像 {_.bindAll(obj)](http://documentcloud.github.com/underscore/#bindAll) 这样的抽象对于这些情况非常有用。 - Raynos
@Raynos:没有什么特别的原因,只是如果你正在使用它的本地版本,那么中间函数在调试时不会显示在堆栈跟踪中。我猜它可能会稍微快一点,但当然这只是微小的优化。 - Domenic
在 messagingServiceSingleton.js 中,你有 'define(function () {' 但是 {它引用的 MessagingService 在哪里呢?} 因为 http//google.com/search?q=MessagingService+Javascript 找不到任何结果,所以该定义是否应该开始为 'define(["./messagingService"], function (MessagingService) {' 来引用你自己的 messagingService.js?如果需要更正,请进行更改。 - Destiny Architect

1
这是一个版本,其中模块本身是共享变量,而不是该模块内的变量。
define('foo', [], {bar: "this text will be overwritten"});

define('bar', ["foo"], function (foo) {
    return {
        setBarOnFoo: function () { foo.bar = "hello"; }
    };
});

define('baz', ["foo"], function (foo) {
    return {
        setBazOnFoo: function () { foo.baz = "goodbye"; }
    };
});

require(["foo", "bar", "baz"], function (foo, bar, baz) {
    bar.setBarOnFoo();
    baz.setBazOnFoo();

    $('#results').append(foo.bar + ' ' + foo.baz);
});​​​

// reads: hello goodbye

0
作为 Domenic 答案的变体,您可以使用 'exports' 魔术模块 来自动生成模块的引用 -- "添加到 exports 对象的属性将成为模块公共接口的一部分,无需返回任何值。" 这样就避免了调用 getTheFoo() 函数来获取引用的情况。
// In foo.js
define(['exports'], function (foo) {
   foo.thereCanBeOnlyOne = true; 
});

// In bar.js
define(["exports", "./foo"], function (bar, foo) {
  bar.setBarOnFoo = function () { foo.bar = "hello"; };
});

// in baz.js
define(["exports", "./foo"], function (baz, foo) {
  baz.setBazOnFoo = function () { foo.baz = "goodbye"; };
});

// In any other file
define(["./foo", "./bar", "./baz"], function (foo, bar, baz) {
  bar.setBarOnFoo();
  baz.setBazOnFoo();

  assert(foo.bar === "hello");
  assert(foo.baz === "goodbye");
  assert(foo.thereCanBeOnlyeOne);
});

针对下面的评论,我个人发现上述约定很有用。你的情况可能不同,但如果你认为这个约定有用,可以自由采用。这个约定归结为以下两条规则:

  • 在定义数组中将“exports”声明为第一个依赖项。
  • 在函数中以JavaScript文件命名参数。

使用文件名,例如对于foo.js,将变量命名为“foo”,可以增加代码的可读性,因为大多数开发人员会将“foo”定义为foo.js依赖项的参数。扫描代码或使用grep时,很容易找到所有内外模块中使用“foo”的引用,并且它使得轻松地挑选出模块向公众公开的内容。例如,如果bar.js模块中的声明与其他文件中的使用相匹配,则将bar.setBarOnFoo重命名为bar.setFooBar要容易得多。在所有文件中搜索和替换bar.setBarOnFoo到bar.setFooBar就可以完成任务。


1
如果您使用exports模块,应该在匿名函数的参数中将其命名为exports。为什么?因为这是惯例。99.998%使用exports机制的代码都遵循惯例。反对惯例会使您的代码更难阅读。实际上,exports模块存在的唯一目的是支持惯例:即CommonJS模块习语。除非您要使用此习语(在示例代码中未使用),否则没有强制使用exports的理由。 - Louis
关于“在定义数组中将'exports'声明为第一个依赖项。[然后]将函数中的参数命名为JavaScript文件的名称”:(1)对于“file”,不意味着“module”,因为后者可以捆绑,(2)不期望也没有看到这会使我混淆,在阅读您的代码时,所以我+1 @Louis cmt,包括作为(3)如果在任何地方打破约定,则(聪明且受人赞赏的)约定是在理想情况下{就在此之前-尚未完成},警告破坏“ALERT:我使用模块名称命名“exports”的名称”,最好附带完整说明的链接,(4)…(我的下一个评论) - Destiny Architect
(我的上一个评论)(4)我看了你的反驳:在我看来,这是个可爱的想法,但我仍然不认同,因为(a)它复制了{something: file name},而这很容易改变,(b)你的全局重命名技巧很可爱,但是{通常是一个大操作,所以在模块定义中进行搜索和替换也不会很昂贵},而且{模块使用有时与其文件名不匹配,有很好的理由},{处理这些问题及更多,我发明并执行:每个非本地名称都有一个“_”+{全局唯一的永久短后缀,特别是KCGUID}的后缀,并且自动完成使得输入变得容易,因此没有冲突,尤其是在所有用途的不完整重命名之后,自动查找所有用途}。 - Destiny Architect

-1
我遇到了这种情况:
由于不同的原因,我需要调用一个在requirejs模块中的函数,但是触发该调用的点击事件不在require范围内。
我解决这个问题的方法是创建一个requirejs模块,覆盖window对象。
define("one", [], function() {
    window.popupManager = (function () {
            console.log ('aca');

        var popUpManager = function () {
            self = this;

            self.CallMe = function ()
            {
                alert ('someone calls');
            };
        };
        return new popUpManager();
    })();
});
require(['one']);

window.popupManager.CallMe();

这样,如果任何超出所需范围的代码(我知道不应该这样)调用了此要求的函数,则可以覆盖窗口对象。

我真的知道这不是一个“优雅”的解决方案,但在紧急情况下可能会对您有所帮助。


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