即时函数表达式的应用案例
首先,我想以一个坦白作为本文的开篇:
每当我遇到一个编程概念,我都会立刻开始思考它有哪些实际应用场景。如果我找不到实际应用,我或许在技术上“理解”了它,但我永远无法真正掌握它。
这对我来说有点像个思维障碍,因为有人会说:“嘿,你应该考虑用[此处插入概念]”,我点点头,然后读几篇关于这个概念的基础文章。但除非我能设想出一个合乎逻辑的场景来运用这个概念,否则它就只会躺在我的脑海深处,积灰,对我的日常编程工作几乎毫无帮助。
多年来,立即调用函数表达式 (IIFE) 一直都是这种概念。当然,我理解 IIFE是什么。但我始终没能真正理解它,因为我从来没有想过,“哦,等等,我应该在这里用 IIFE!”
不知何故,我最近开始比较频繁地使用即时函数表达式(IIFE)。所以我想写这篇文章,希望能帮助那些一直没能真正理解其用途的人。
什么是IIFE?
即时函数表达式 (IIFE) 的完整名称基本上已经说明了它的本质。它是一个会被立即调用的函数表达式。它与匿名函数的区别在于,匿名函数仍然可以被多次调用,例如:
[1,2,3].forEach(item => console.log(item))
在上面的例子中,这是匿名函数:
item => console.log(item)
之所以称之为“匿名”,是因为它没有存储在变量中。因此,它没有“句柄”,我们无法在代码的其他位置手动调用该函数。但它也不是立即执行的函数表达式,因为它不会立即运行。相反,它会针对传递给数组原型函数的每个元素运行一次.forEach()。
即时函数表达式 (IIFE) 具有特殊的语法,它告诉 JavaScript 引擎在定义后立即执行其中的逻辑。该语法如下所示:
const item = 1;
(() => console.log(item))()
在上面的例子中,IIFE 会将item( 1) 的值输出到控制台。这本来没什么问题,但是……当我看到像上面这样的例子时,IIFE 感觉完全……毫无意义。我的意思是,如果你只想输出 ( ) 的值item,为什么不直接这样做呢?
const item = 1;
console.log(item);
当然,对于这个(极其简单的)例子来说,使用立即执行函数表达式(IIFE)确实没有任何逻辑上的理由。正是这种基本的常识让我一直认为IIFE是一种几乎毫无意义的语言结构。
需要说明的是,我偶尔会遇到其他开发者编写的立即执行函数表达式(IIFE)。但是很多时候,当我看到实际应用中的 IIFE 时,我仍然百思不得其解,原开发者当初为什么要选择使用它。我的想法是这样的:
-
IIFE(立即执行函数表达式)只是一段立即执行的代码块。
-
所以,如果我希望这段代码立即执行,那我为什么不直接写代码,而要把它放在立即执行函数表达式(IIFE)里呢?
但正如我上面提到的,我发现它们在很多情况下确实很有用。我希望以下例子能帮助你理解它们的用途,就像它们帮助我理解它们一样。
图书馆
假设你有一个包含实用函数“库”的单个文件。它可能看起来像这样:
// conversionUtilities.js
const convertUserDBToUI = userDB => {
return {
id: userDB.user_id,
firstName: userDB.first_name,
lastName: userDB.last_name,
}
}
const convertUserUIToDB = userUI => {
return {
user_id: userUI.id,
first_name: userUI.firstName,
last_name: userUI.lastName,
}
}
上述函数只是将为 UI 格式化的对象转换为为 DB 格式化的对象(反之亦然)。
当然,如果这些函数确实是“实用”函数(意味着您可能需要在应用程序的各个地方调用它们),那么您需要将export这些函数声明为可以在应用程序其他地方导入的函数。代码如下所示:
// conversionUtilities.js
export const convertUserDBToUI = userDB => {
return {
id: userDB.user_id,
firstName: userDB.first_name,
lastName: userDB.last_name,
}
}
export const convertUserUIToDB = userUI => {
return {
user_id: userUI.id,
first_name: userUI.firstName,
last_name: userUI.lastName,
}
}
上面的代码可以正常运行。但是,它可能会导致import代码中出现一些多余的语句,因为当你在同一个文件中使用两个或多个这样的函数时,就需要import在这些文件中包含两条或多条语句。当你创建包含许多相关实用函数的“库”文件时,将它们放在一个单独的文件中通常会很有帮助export。
实现此目的的一种方法是class:
// conversionUtilities.js
export const Convert = class {
userDBToUI(userDB) {
return {
id: userDB.user_id,
firstName: userDB.first_name,
lastName: userDB.last_name,
}
}
userUIToDB(userUI) {
return {
user_id: userUI.id,
first_name: userUI.firstName,
last_name: userUI.lastName,
}
}
}
以上代码运行正常。以下是上述代码的一些优点:
-
我们不再需要
convert在每个方法名中重复,因为它已经隐含在类名中了。 -
我们所有的实用函数都“打包”在一个单独的实体(即
Convert类)中,因此不需要单独导入。
但也有一些“缺点”:
-
有些(不……很多)JS/TS开发者简直厌恶使用
class关键字。 -
每次需要使用这些实用函数时,都需要先调用类似`.`
const convert = new Convert();这样的函数,然后再使用这些实用函数const userUI = convert.userDBToUI(userDB);。这可能会变得……很麻烦。
你可以通过导出类的实例而不是导出类本身来解决第二个“问题” 。代码如下:
// conversionUtilities.js
const Convert = class {
userDBToUI(userDB) {
return {
id: userDB.user_id,
firstName: userDB.first_name,
lastName: userDB.last_name,
}
}
userUIToDB(userUI) {
return {
user_id: userUI.id,
first_name: userUI.firstName,
last_name: userUI.lastName,
}
}
}
export const convert = new Convert();
这样可以节省我们导入Convert类时的一些击键次数。但这仍然会让一些开发者感到不舒服,因为我们依赖于类。不过没关系。因为我们可以用函数来实现同样的功能:
// conversionUtilities.js
export const Convert = () => {
const userDBToUI = userDB => {
return {
id: userDB.user_id,
firstName: userDB.first_name,
lastName: userDB.last_name,
}
}
const userUIToDB = userUI => {
return {
user_id: userUI.id,
first_name: userUI.firstName,
last_name: userUI.lastName,
}
}
return {
userDBToUI,
userUIToDB,
}
}
这种方法的优点如下:
-
不再使用“令人厌恶”的
class关键词。 -
同样,我们不再需要
convert在每个函数名中重复,因为它已经隐含在导出的函数名中。 -
我们所有的实用函数都“捆绑”在一个单独的实体(即
convert函数)中,因此不需要单独导入。
但至少还有一个“缺点”:
- 每次需要使用这些实用函数时,都需要先调用类似`.`
const convert = Convert();这样的函数,然后再使用这些实用函数const userUI = convert.userDBToUI(userDB);。这可能会变得……很麻烦。
你可以通过导出父函数的调用而不是导出父函数本身来解决这个问题。代码如下:
// conversionUtilities.js
const Convert = () => {
const userDBToUI = userDB => {
return {
id: userDB.user_id,
firstName: userDB.first_name,
lastName: userDB.last_name,
}
}
const userUIToDB = userUI => {
return {
user_id: userUI.id,
first_name: userUI.firstName,
last_name: userUI.lastName,
}
}
return {
userDBToUI,
userUIToDB,
}
}
export const convert = Convert();
说实话,这通常是我见到的主要实现方式。但还有另一种方法——使用立即执行函数表达式 (IIFE)——无需先 1) 定义函数Convert,然后再 2) 导出该函数的调用。代码如下:
// conversionUtilities.js
export const convert = (() => {
const userDBToUI = userDB => {
return {
id: userDB.user_id,
firstName: userDB.first_name,
lastName: userDB.last_name,
}
}
const userUIToDB = userUI => {
return {
user_id: userUI.id,
first_name: userUI.firstName,
last_name: userUI.lastName,
}
}
return {
userDBToUI,
userUIToDB,
}
})()
请注意,我们无需先定义父函数,然后再导出该函数的调用。相反,我们使用了一个立即执行函数表达式 (IIFE),用一条语句就完成了所有操作。现在,无论何时有人导入该模块convert,他们都会得到一个已经包含所有实用函数的对象convert。
这是即时函数表达式(IIFE)的绝对可靠、无需思考的用例吗?并非如此。如上所述,您可以通过以下方式实现同样的功能:
-
逐个导出每个实用程序函数。
-
将实用函数包含在一个类中,然后导出该类的一个实例。
-
将实用函数包含在父函数中,然后导出该函数的调用。
不过,我觉得 IIFE 方法更简洁一些。而且,这至少是IIFE 的一个合理应用场景。
吞下承诺
在处理数据调用和其他异步操作时,Async/await 是非常强大的工具。但它们也可能给你的代码带来一些连锁反应,尤其是在严格的 TypeScript 代码中。
await只能在函数内部使用async。假设你有三个级联函数,它们分别执行以下操作:
-
FunctionA 处理用户操作的结果(例如点击“提交”按钮)。
-
如果满足正确的条件,FunctionA 将调用 FunctionB 中的一些验证逻辑。
-
根据验证结果,FunctionB 可以调用 FunctionC,FunctionC 通过异步 REST 调用将数据传输到服务器。
你想使用awaitFunctionC 中的语法,这意味着你需要将其定义为一个async函数。
但这意味着 FunctionB 现在会期望FunctionC 返回一个 Promise。所以……你需要将 FunctionB 改成一个async函数。
但这意味着 FunctionA 现在需要FunctionB 提供一个 Promise 对象。所以……你需要将 FunctionA 改成一个async函数。
但现在,最初调用 FunctionA 的事件处理程序也需要一个 Promise 对象。而根据你的 TypeScript 环境配置的严格程度,这可能根本行不通。
其“级联”效应async/await大致如下:
export const App = () => {
const submit = () => {
const result = await someApiCall(values);
// handle API result
return result;
}
const doValidation = () => {
// check some validation
if (isValid) {
return submit();
}
}
const handleSubmit = () => {
// set some state vars
if (someCondition) {
return doValidation();
}
}
return <>
<button onClick={handleSubmit}>
Submit
</button>
</>
}
在上面的例子中,TypeScript 编译器会报错,因为callApi()`is` 不是一个async函数。所以你把 `is` 设置callApi()为async`is`,但是……这也要求 ` doValidation()is`是 `is`。async所以你把 `is` 设置doValidation()为 `is`,async但是……这也要求handleSubmit()`is` 是 `is` async。所以你把 `is` 设置handleSubmit()为 `is`,async但是……TypeScript 可能仍然会报错,因为onClick事件处理程序没有配置为处理生成的 Promise。
到了这一步,你已经把代码塞进async了很多你原本不想塞进去的地方。更糟糕的是,TypeScript仍然会报错,提示你的onClick处理程序没有处理生成的 Promise。
【注:在纯 JavaScript 中,你可以直接忽略生成的 Promise。但你不能仅仅因为不想处理这些层叠的 Promise 调用就将整个项目从 TypeScript 转换为 JavaScript async/await。】
幸运的是,即时财务执行(IIFE)在这里可以发挥很大的作用。具体来说,它的作用如下:
export const App = () => {
const callApi = () => {
(async () => {
const result = await someApiCall(values);
// handle API result
return result;
})()
}
const doValidation = () => {
// check some validation
if (isValid) {
return callApi();
}
}
const handleSubmit = () => {
// set some state vars
if (someCondition) {
return doValidation();
}
}
return <>
<button onClick={handleSubmit}>
Submit
</button>
</>
}
在上面的代码中,我们现在可以直接async/await在内部使用 `inside` callApi() ,而无需将其声明callApi()为async函数。这是因为异步调用发生在函数内部async。而这个函数恰好async是……函数内部的callApi()一个立即执行函数表达式 (IIFE) 。
这实际上是我经常使用的即时函数表达式(IIFE)的一个用例。
就地逻辑
最后,我想举例说明一个场景,在这个场景中,就地逻辑(即由立即执行函数表达式 (IIFE) 提供的那种逻辑)会很有意义。
我最近一直在帮朋友处理一些 EDI 数据转换工作。表面上看,这项工作很简单。你会得到一个大型数据对象,它应该采用特定的格式——可能像这样:
const rawData = {
fee: 'fee',
einz: '1',
fie: 'fie',
zwei: '2',
foe: 'foe',
drei: '3',
fum: 'fum',
}
然后,您需要编写一些转换例程,用于提取(并“处理”)某些值,并以不同的格式返回新的数据对象,如下所示:
const getGiantSpeak = data => {
return {
fee: data.fee,
fie: data.fie,
foe: data.foe,
fum: data.fum,
}
}
问题在于,你会发现许多数据供应商会以各种不同的方式格式化数据。例如,只要你假设输入的值rawData都是简单的标量值,那么上述简单的逻辑就能正常工作。
但随后你会发现数据转换会间歇性地失败。当你调查这些间歇性问题时,你会发现供应商有时rawData.fie提交的是一个简单的字符串,而有时则提交的是一个字符串数组getGiantSpeak()。因此,要解决这个问题,你需要在返回值的地方插入一些逻辑fie。
在这种情况下,我发现一个简单的立即执行函数表达式(IIFE)就能起到很好的效果:
const rawData = {
fee: 'fee',
einz: '1',
fie: ['fie', 'fiddler'],
zwei: '2',
foe: 'foe',
drei: '3',
fum: 'fum',
}
const getGiantSpeak = data => {
return {
fee: data.fee,
fie: (() => {
if (Array.isArray(data.fie))
return data.fie.join(',');
else
return data.fie;
})(),
foe: data.foe,
fum: data.fum,
}
}
【注:我知道用简单的三元运算符也能实现上述逻辑。但“现实生活”中的例子通常需要更“微妙”的逻辑,而三元运算符并不适合。】
在这种情况下,构建新对象所需的逻辑实际上是一次性的,将其包含在立即执行函数表达式 (IIFE) 中会更加简洁。
文章来源:https://dev.to/bytebodger/use-cases-for-iifes-5gdg



