JavaScript 中的模块模式
模块是一种类似于单例类的结构。它只有一个实例并暴露其成员,但不具有任何内部状态。
定义模块
模块被创建为 IIFE(立即调用函数表达式),其中包含一个函数:
const SomeModule = (function() {})();
该函数主体内的所有内容都绑定到该模块,并且可以相互访问。模块通过创建前面提到的作用域并仅公开已声明的内容来模拟“公共”和“私有”方法。
私有方法或函数是特定实体的成员,只能在该实体内部访问。公共方法或函数可以从特定实体的外部访问。
让我们尝试创建一个包含私有函数的模块。
const Formatter = (function() {
const log = (message) => console.log(`[${Date.now()}] Logger: ${message}`);
})();
如您所见,有一个简单的log
函数可以记录收到的消息。如何执行它Formatter.log
?
Formatter.log("Hello");
你能猜出它会产生什么吗?Uncaught TypeError: Cannot read property 'log' of undefined
为什么?因为我们的模块没有返回任何东西,所以它实际上是undefined
,即使里面的代码会执行。
const Formatter = (function() {
console.log("Start");
const log = (message) => console.log(`[${Date.now()}] Logger: ${message}`);
})();
这将记录Start
,因为该函数已被触发,并且如您所知,函数并不总是必须返回某些东西。
所以,现在我们知道访问模块实际上就是访问它返回的内容。
该log
函数可以视为私有函数。它可以在模块内部访问,并且模块内部的其他函数可以执行它。我们来试试吧!
const Formatter = (function() {
const log = (message) => console.log(`[${Date.now()}] Logger: ${message}`);
const makeUppercase = (text) => {
log("Making uppercase");
return text.toUpperCase();
};
})();
嘿,等一下,伙计!模块里又有一个函数我无法访问!
暴露模块
是的,这又是一个我们无法访问的函数。但是,了解了我们之前学习过的关于访问模块的知识,我们可以轻松解决这个问题!你已经知道该怎么做了?没错,就是返回这个函数!但是,不要返回单个函数(虽然可以),而是返回一个包含该函数的对象!
const Formatter = (function() {
const log = (message) => console.log(`[${Date.now()}] Logger: ${message}`);
const makeUppercase = (text) => {
log("Making uppercase");
return text.toUpperCase();
};
return {
makeUppercase,
}
})();
现在,我们可以makeUppercase
像平常一样使用该函数:
console.log(Formatter.makeUppercase("tomek"));
结果如何?
> Start
> [1551191285526] Logger: Making uppercase
> TOMEK
模块不仅可以容纳函数,还可以容纳数组、对象和原语。
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,
}
})();
让我们执行它:
console.log(Formatter.makeUppercase("tomek"));
console.log(Formatter.timesRun);
正如预期的那样,0
显示如下。但请注意,这可以从外部覆盖。
Formatter.timesRun = 10;
console.log(Formatter.timesRun);
现在控制台会打印日志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);
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");
它开箱即用(假设我们target
的 DOM 中有一个带有 id 的元素)。听起来很棒,但document
只有当 DOM 可访问时才可用。在服务器上运行这段代码会产生错误。那么,如何确保我们一切顺利呢?
其中一个选项是检查是否document
存在。
const writeToDOM = (selector, message) => {
if (!!document && "querySelector" in document) {
document.querySelector(selector).innerHTML = message;
}
}
这几乎解决了所有问题,但我不喜欢。现在这个模块真的依赖于外部的东西。情况是“只有我朋友也去,我才会去”。一定要这样吗?
不,当然不是。
我们可以声明模块的依赖项并在进行过程中注入它们。
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);
让我们一步一步来。在最顶部,有一个函数的参数。然后,它在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);
我甚至把里面的支票拿掉了makeUppercase
,因为不再需要它了。
—
模块模式非常常见,而且正如你所见,它在这方面非常有效。我经常尝试先写模块,然后再根据需要写类。
文章来源:https://dev.to/tomekbuszewski/module-pattern-in-javascript-56jm