108

我最近看到了这篇关于如何在 Node.js 中编写单例的文章。我知道以下require 国家的文件:

模块在第一次加载后被缓存。多次调用require('foo')可能不会导致模块代码被多次执行。

因此,似乎每个必需的模块都可以轻松地用作单例,而无需单例样板代码。

问题:

上面的文章是否提供了关于创建单例的解决方案?

4

10 回答 10

144

以上所有内容都过于复杂。有一种观点认为设计模式显示出实际语言的缺陷。

具有基于原型的 OOP(无类)的语言根本不需要单例模式。您只需动态创建一个(吨)对象,然后使用它。

至于 node 中的模块,是的,默认情况下它们是缓存的,但是如果你想热加载模块更改,可以对其进行调整。

但是,是的,如果你想全部使用共享对象,把它放在一个模块导出中就可以了。只是不要将它与“单例模式”复杂化,在 JavaScript 中不需要它。

于 2012-11-01T14:35:58.343 回答
67

这基本上与 nodejs 缓存有关。干净利落。

https://nodejs.org/api/modules.html#modules_caching

(v 6.3.1)

缓存

模块在第一次加载后被缓存。这意味着(除其他外)每次调用 require('foo') 都将返回完全相同的对象,如果它会解析为同一个文件。

多次调用 require('foo') 可能不会导致模块代码被多次执行。这是一个重要的特点。有了它,可以返回“部分完成”的对象,从而允许加载传递依赖项,即使它们会导致循环。

如果你想让一个模块多次执行代码,那么导出一个函数,然后调用那个函数。

模块缓存注意事项

模块根据其解析的文件名进行缓存。由于模块可能会根据调用模块的位置(从 node_modules 文件夹加载)解析为不同的文件名,因此不能保证 require('foo') 将始终返回完全相同的对象,如果它会解析为不同的文件.

此外,在不区分大小写的文件系统或操作系统上,不同的解析文件名可以指向同一个文件,但缓存仍会将它们视为不同的模块,并会多次重新加载文件。例如, require('./foo') 和 require('./FOO') 返回两个不同的对象,无论 ./foo 和 ./FOO 是否是同一个文件。

所以简单来说。

如果你想要一个单身人士;导出一个对象

如果您不想要单例;导出一个函数(并在该函数中做东西/返回东西/任何东西)。

要非常清楚,如果您正确执行此操作,它应该可以工作,请查看https://stackoverflow.com/a/33746703/1137669(Allen Luce 的回答)。它在代码中解释了由于不同解析的文件名而导致缓存失败时会发生什么。但是,如果您总是解析为相同的文件名,它应该可以工作。

2016 年更新

使用 es6 符号在 node.js 中创建一个真正的单例 另一个解决方案在此链接中

2020 年更新

这个答案是指CommonJS(Node.js 自己的导入/导出模块的方式)。Node.js 很可能会切换到ECMAScript 模块https ://nodejs.org/api/esm.html (如果您不知道,ECMAScript 是 JavaScript 的真名)

迁移到 ECMAScript 时,请先阅读以下内容:https ://nodejs.org/api/esm.html#esm_writing_dual_packages_while_avoiding_or_minimizing_hazards

于 2016-08-10T13:31:36.837 回答
30

不会。 当 Node 的模块缓存失败时,单例模式就会失败。我修改了示例以在 OSX 上有意义地运行:

var sg = require("./singleton.js");
var sg2 = require("./singleton.js");
sg.add(1, "test");
sg2.add(2, "test2");

console.log(sg.getSocketList(), sg2.getSocketList());

这给出了作者预期的输出:

{ '1': 'test', '2': 'test2' } { '1': 'test', '2': 'test2' }

但是一个小的修改会破坏缓存。在 OSX 上,执行以下操作:

var sg = require("./singleton.js");
var sg2 = require("./SINGLETON.js");
sg.add(1, "test");
sg2.add(2, "test2");

console.log(sg.getSocketList(), sg2.getSocketList());

或者,在 Linux 上:

% ln singleton.js singleton2.js

然后将sg2要求行更改为:

var sg2 = require("./singleton2.js");

bam,单例被击败:

{ '1': 'test' } { '2': 'test2' }

我不知道有一种可以接受的方法来解决这个问题。如果你真的觉得有必要做一些类似单例的东西并且可以污染全局命名空间(以及可能导致的许多问题),你可以将作者getInstance()exports行更改为:

singleton.getInstance = function(){
  if(global.singleton_instance === undefined)
    global.singleton_instance = new singleton();
  return global.singleton_instance;
}

module.exports = singleton.getInstance();

也就是说,我从来没有在生产系统上遇到过需要做这样的事情的情况。我也从来没有觉得需要在 Javascript 中使用单例模式。

于 2015-11-16T23:45:43.327 回答
22

进一步查看模块文档中的模块缓存警告:

模块根据其解析的文件名进行缓存。由于模块可能会根据调用模块的位置(从 node_modules 文件夹加载)解析为不同的文件名,因此不能保证require('foo') 将始终返回完全相同的对象,如果它会解析为不同的文件.

因此,根据您在需要模块时所处的位置,可以获得模块的不同实例。

听起来模块不是创建单例的简单解决方案。

编辑:或者他们可能. 像@mkoryak 一样,我无法提出单个文件可能解析为不同文件名(不使用符号链接)的情况。但是(正如@JohnnyHK 评论),不同目录中文件的多个副本node_modules将分别加载和存储。

于 2012-11-01T16:43:36.437 回答
19

像这样的 node.js(或浏览器 JS)中的单例是完全没有必要的。

由于模块是缓存和有状态的,因此您提供的链接上给出的示例可以更简单地重写:

var socketList = {};

exports.add = function (userId, socket) {
    if (!socketList[userId]) {
        socketList[userId] = socket;
    }
};

exports.remove = function (userId) {
    delete socketList[userId];
};

exports.getSocketList = function () {
    return socketList;
};
// or
// exports.socketList = socketList
于 2012-11-01T14:32:36.123 回答
12

在 js 中做单例不需要什么特别的东西,文章中的代码也可以是:

var socketList = {};

module.exports = {
      add: function() {

      },

      ...
};

在 node.js 之外(例如,在浏览器 js 中),您需要手动添加 wrapper 函数(在 node.js 中自动完成):

var singleton = function() {
    var socketList = {};
    return {
        add: function() {},
        ...
    };
}();
于 2012-11-01T14:31:08.827 回答
12

这里唯一使用 ES6 类的答案

// SummaryModule.js
class Summary {

  init(summary) {
    this.summary = summary
  }

  anotherMethod() {
    // do something
  }
}

module.exports = new Summary()

需要这个单例:

const summary = require('./SummaryModule')
summary.init(true)
summary.anotherMethod()

这里唯一的问题是您不能将参数传递给类构造函数,但可以通过手动调用init方法来规避。

于 2017-08-05T12:50:56.723 回答
7

单例在 JS 中很好,它们只是不需要那么冗长。

在节点中,如果您需要一个单例,例如在服务器层的各种文件中使用相同的 ORM/DB 实例,您可以将引用填充到全局变量中。

只需编写一个模块,如果它不存在则创建全局变量,然后返回对其的引用。

@allen-luce 在这里复制了他的脚注代码示例:

singleton.getInstance = function(){
  if(global.singleton_instance === undefined)
    global.singleton_instance = new singleton();
  return global.singleton_instance;
};

module.exports = singleton.getInstance();

但重要的是要注意,不需要new使用关键字。任何旧的对象、函数、iife 等都可以工作——这里没有发生 OOP 巫术。

如果您在一个返回对它的引用的函数中关闭某个 obj 并将该函数设为全局函数,那么即使重新分配全局变量也不会破坏已经从它创建的实例,尽管这可能很有用。

于 2016-06-06T15:33:36.143 回答
1

保持简单。

foo.js

function foo() {

  bar: {
    doSomething: function(arg, callback) {
      return callback('Echo ' + arg);
    };
  }

  return bar;
};

module.exports = foo();

然后就

var foo = require(__dirname + 'foo');
foo.doSomething('Hello', function(result){ console.log(result); });
于 2017-01-15T15:53:35.763 回答
0

如果要使用类,它是最短和最漂亮的

module.exports = new class foo {...}
于 2021-11-10T09:25:56.420 回答