JavaScript 中的模块模式

2025-05-28

JavaScript 中的模块模式

模块是一种类似于单例类的结构。它只有一个实例并暴露其成员,但不具有任何内部状态。

定义模块

模块被创建为 IIFE(立即调用函数表达式),其中包含一个函数:

const SomeModule = (function() {})();
Enter fullscreen mode Exit fullscreen mode

该函数主体内的所有内容都绑定到该模块,并且可以相互访问。模块通过创建前面提到的作用域并仅公开已声明的内容来模拟“公共”和“私有”方法。

私有方法或函数是特定实体的成员,只能在该实体内部访问。公共方法或函数可以从特定实体的外部访问。

让我们尝试创建一个包含私有函数的模块。

const Formatter = (function() {
  const log = (message) => console.log(`[${Date.now()}] Logger: ${message}`);
})();
Enter fullscreen mode Exit fullscreen mode

如您所见,有一个简单的log函数可以记录收到的消息。如何执行它Formatter.log

Formatter.log("Hello");
Enter fullscreen mode Exit fullscreen mode

你能猜出它会产生什么吗?Uncaught TypeError: Cannot read property 'log' of undefined为什么?因为我们的模块没有返回任何东西,所以它实际上是undefined,即使里面的代码会执行。

const Formatter = (function() {
  console.log("Start");
  const log = (message) => console.log(`[${Date.now()}] Logger: ${message}`);
})();
Enter fullscreen mode Exit fullscreen mode

这将记录Start,因为该函数已被触发,并且如您所知,函数并不总是必须返回某些东西。

所以,现在我们知道访问模块实际上就是访问它返回的内容

log函数可以视为私有函数。它可以在模块内部访问,并且模块内部的其他函数可以执行它。我们来试试吧!

const Formatter = (function() {
  const log = (message) => console.log(`[${Date.now()}] Logger: ${message}`);

  const makeUppercase = (text) => {
    log("Making uppercase");
    return text.toUpperCase();
  };
})();
Enter fullscreen mode Exit fullscreen mode

嘿,等一下,伙计!模块里又有一个函数我无法访问!

暴露模块

是的,这又是一个我们无法访问的函数。但是,了解了我们之前学习过的关于访问模块的知识,我们可以轻松解决这个问题!你已经知道该怎么做了?没错,就是返回这个函数!但是,不要返回单个函数(虽然可以),而是返回一个包含该函数的对象!

const Formatter = (function() {
  const log = (message) => console.log(`[${Date.now()}] Logger: ${message}`);

  const makeUppercase = (text) => {
    log("Making uppercase");
    return text.toUpperCase();
  };  

  return {
    makeUppercase,
  }
})();
Enter fullscreen mode Exit fullscreen mode

现在,我们可以makeUppercase像平常一样使用该函数:

console.log(Formatter.makeUppercase("tomek"));
Enter fullscreen mode Exit fullscreen mode

结果如何?

> Start
> [1551191285526] Logger: Making uppercase
> TOMEK
Enter fullscreen mode Exit fullscreen mode

模块不仅可以容纳函数,还可以容纳数组、对象和原语。

const Formatter = (function() {
  let timesRun = 0;

  const log = (message) => console.log(`[${Date.now()}] Logger: ${message}`);
  const setTimesRun = () => { 
    log("Setting times run");
    ++timesRun;
  }

  const makeUppercase = (text) => {
    log("Making uppercase");
    setTimesRun();
    return text.toUpperCase();
  };

  return {
    makeUppercase,
    timesRun,
  }
})();
Enter fullscreen mode Exit fullscreen mode

让我们执行它:

console.log(Formatter.makeUppercase("tomek"));
console.log(Formatter.timesRun);
Enter fullscreen mode Exit fullscreen mode

正如预期的那样,0显示如下。但请注意,这可以从外部覆盖。

Formatter.timesRun = 10;
console.log(Formatter.timesRun);
Enter fullscreen mode Exit fullscreen mode

现在控制台会打印日志10。这表明所有公开暴露的内容都可以从外部更改。这是模块模式最大的缺点之一。

引用类型的工作方式有所不同。在这里,您可以定义它,它会随着您的操作自动填充。

const Formatter = (function() {
  const log = (message) => console.log(`[${Date.now()}] Logger: ${message}`);
  const timesRun = [];

  const makeUppercase = (text) => {
    log("Making uppercase");
    timesRun.push(null);
    return text.toUpperCase();
  };

  return {
    makeUppercase,
    timesRun,
  }
})();

console.log(Formatter.makeUppercase("tomek"));
console.log(Formatter.makeUppercase("tomek"));
console.log(Formatter.makeUppercase("tomek"));
console.log(Formatter.timesRun.length);
Enter fullscreen mode Exit fullscreen mode

3在用大写字母说完我的名字三次后,它就会记录下来。

声明模块依赖关系

我喜欢将模块视为封闭的实体。这意味着它们存在于自身内部,不需要任何其他东西来维持它们的存在。但有时你可能需要使用 DOM 或window全局对象等。

为了实现这一点,模块可能具有依赖关系。让我们尝试编写一个函数,将消息写入我们请求的 HTML 元素。

const Formatter = (function() {
  const log = (message) => console.log(`[${Date.now()}] Logger: ${message}`);

  const makeUppercase = (text) => {
    log("Making uppercase");
    return text.toUpperCase();
  };

  const writeToDOM = (selector, message) => {
    document.querySelector(selector).innerHTML = message;
  }

  return {
    makeUppercase,
    writeToDOM,
  }
})();

Formatter.writeToDOM("#target", "Hi there");
Enter fullscreen mode Exit fullscreen mode

它开箱即用(假设我们target的 DOM 中有一个带有 id 的元素)。听起来很棒,但document只有当 DOM 可访问时才可用。在服务器上运行这段代码会产生错误。那么,如何确保我们一切顺利呢?

其中一个选项是检查是否document存在。

const writeToDOM = (selector, message) => {
  if (!!document && "querySelector" in document) {
    document.querySelector(selector).innerHTML = message;
  }
}
Enter fullscreen mode Exit fullscreen mode

这几乎解决了所有问题,但我不喜欢。现在这个模块真的依赖于外部的东西。情况是“只有我朋友也去,我才会去”。一定要这样吗?

不,当然不是。

我们可以声明模块的依赖项并在进行过程中注入它们。

const Formatter = (function(doc) {
  const log = (message) => console.log(`[${Date.now()}] Logger: ${message}`);

  const makeUppercase = (text) => {
    log("Making uppercase");
    return text.toUpperCase();
  };

  const writeToDOM = (selector, message) => {
    if (!!doc && "querySelector" in doc) {
      doc.querySelector(selector).innerHTML = message;
    }
  }

  return {
    makeUppercase,
    writeToDOM,
  }
})(document);
Enter fullscreen mode Exit fullscreen mode

让我们一步一步来。在最顶部,有一个函数的参数。然后,它在writeToDOM方法中被使用,而不是我们的document。最后,在最后一行,我们添加了document。为什么?这些是我们的模块将被调用的参数。为什么我在模块中更改了参数名称?我不喜欢隐藏变量。

当然,这是一个很好的测试机会。现在,我们不必依赖测试工具是否支持 DOM 模拟器或类似功能,而是可以插入一个模拟代码。但我们需要在定义过程中插入,而不是事后再插入。这相当简单,你只需要编写一个模拟代码并将其作为“备用”代码即可:

const documentMock = (() => ({
  querySelector: (selector) => ({
    innerHTML: null,
  }),
}))();

const Formatter = (function(doc) {
  const log = (message) => console.log(`[${Date.now()}] Logger: ${message}`);

  const makeUppercase = (text) => {
    log("Making uppercase");
    return text.toUpperCase();
  };

  const writeToDOM = (selector, message) => {
    doc.querySelector(selector).innerHTML = message;
  }

  return {
    makeUppercase,
    writeToDOM,
  }
})(document || documentMock);
Enter fullscreen mode Exit fullscreen mode

我甚至把里面的支票拿掉了makeUppercase,因为不再需要它了。

模块模式非常常见,而且正如你所见,它在这方面非常有效。我经常尝试先写模块,然后再根据需要写类。

文章来源:https://dev.to/tomekbuszewski/module-pattern-in-javascript-56jm
PREV
棘手的 JavaScript 问题
NEXT
通过清除“if”语句来保持代码整洁