早期解决/拒绝后我需要返回吗?

405

假设我有以下的代码。

function divide(numerator, denominator) {
 return new Promise((resolve, reject) => {

  if(denominator === 0){
   reject("Cannot divide by 0");
   return; //superfluous?
  }

  resolve(numerator / denominator);

 });
}
如果我的目的是使用 reject 提前退出,那么我是否应该养成紧接着使用 return 的习惯呢?

如果我的目的是使用 reject 早早退出,那么我是否应该立即使用 return 来养成习惯呢?


14
是的,由于运行至完成 - user6445533
6个回答

577

return语句的作用是在拒绝后终止函数的执行,以防止其后面的代码继续执行。

function divide(numerator, denominator) {
  return new Promise((resolve, reject) => {

    if (denominator === 0) {
      reject("Cannot divide by 0");
      return; // The function execution ends here 
    }

    resolve(numerator / denominator);
  });
}

在这种情况下,它防止了resolve(numerator / denominator); 的执行,这并不是绝对必要的。但是,为了防止未来可能出现的陷阱,终止执行仍然是更可取的。此外,防止不必要地运行代码是一个好习惯。

背景

Promise 可以处于以下三种状态之一:

  1. pending - 初始状态。从 pending 状态可以转移到其他状态之一
  2. fulfilled - 操作成功
  3. rejected - 操作失败

当 Promise 被 fulfilled 或 rejected 时,它将无限期地保持在此状态(settled)。因此,拒绝已经 fulfilled 的 Promise 或履行已经 rejected 的 Promise 将没有任何效果。

此示例片段显示,尽管 Promise 在被拒绝后被履行,但它仍保持着被拒绝的状态。

function divide(numerator, denominator) {
  return new Promise((resolve, reject) => {
    if (denominator === 0) {
      reject("Cannot divide by 0");
    }

    resolve(numerator / denominator);
  });
}

divide(5,0)
  .then((result) => console.log('result: ', result))
  .catch((error) => console.log('error: ', error));

那么为什么我们需要返回呢?

虽然我们无法改变一个已经确定的promise状态,但是拒绝或解决并不会停止函数其余部分的执行。该函数可能包含会导致混乱结果的代码。例如:

function divide(numerator, denominator) {
  return new Promise((resolve, reject) => {
    if (denominator === 0) {
      reject("Cannot divide by 0");
    }
    
    console.log('operation succeeded');

    resolve(numerator / denominator);
  });
}

divide(5, 0)
  .then((result) => console.log('result: ', result))
  .catch((error) => console.log('error: ', error));

尽管当前函数中没有这样的代码,但这会造成未来潜在的陷阱。未来的重构可能忽略了在Promise被拒绝后仍然执行的事实,并且将难以调试。
在resolve/reject之后停止执行:
这是标准的JS控制流程内容。
- 在resolve/reject之后返回:

function divide(numerator, denominator) {
  return new Promise((resolve, reject) => {
    if (denominator === 0) {
      reject("Cannot divide by 0");
      return;
    }

    console.log('operation succeeded');

    resolve(numerator / denominator);
  });
}

divide(5, 0)
  .then((result) => console.log('result: ', result))
  .catch((error) => console.log('error: ', error));

  • 通过返回resolve/reject来结束函数 - 由于回调函数的返回值被忽略,我们可以通过返回resolve/reject语句来少写一行代码:

function divide(numerator, denominator) {
  return new Promise((resolve, reject) => {
    if (denominator === 0) {
      return reject("Cannot divide by 0");
    }

    console.log('operation succeeded');

    resolve(numerator / denominator);
  });
}

divide(5, 0)
  .then((result) => console.log('result: ', result))
  .catch((error) => console.log('error: ', error));

  • 使用if/else块:

function divide(numerator, denominator) {
  return new Promise((resolve, reject) => {
    if (denominator === 0) {
      reject("Cannot divide by 0");
    } else {
      console.log('operation succeeded');
      resolve(numerator / denominator);
    }
  });
}

divide(5, 0)
  .then((result) => console.log('result: ', result))
  .catch((error) => console.log('error: ', error));

我倾向于使用return选项之一,因为代码更加扁平化。


36
值得注意的是,如果有或没有return,代码实际上不会有任何不同,因为一旦Promise状态被设置了,它就不能被更改,所以在调用reject()后调用resolve()除了使用一些额外的CPU周期外,不会产生任何作用。我个人会从代码清晰度和效率的角度考虑使用return,但在这个特定的例子中并不需要。 - jfriend00
2
尝试使用Promise.try(() => { })而不是新的Promise,并避免使用resolve/reject调用。相反,您可以编写return denominator === 0 ? throw 'Cannot divide by zero' : numerator / denominator;我使用Promise.try作为启动Promise的手段,以及消除在try/catch块中包装的Promises带来的问题。 - kingdango
2
很好了解,我喜欢这个模式。然而,目前 Promise.try 是一个 0 阶段的建议,所以你只能通过 shim 或使用像 bluebird 或 Q 这样的 promise 库来使用它。 - Ori Drori
14
@jfriend00 显然在这个简单的例子中,代码不会有任何不同的行为。但是,如果您在 reject 之后有昂贵的操作,比如连接到数据库或 API 端点,那么这些操作都是不必要的,会浪费您的金钱和资源,特别是当您连接到像 AWS 数据库或 API Gateway 端点这样的东西时。在那种情况下,您肯定会使用 return 以避免执行不必要的代码。 - Jake Wilson
5
当然,这只是JavaScript中正常的代码流程,与Promise完全无关。如果你已经处理完函数,并且不希望在当前代码路径中执行任何更多的代码,可以插入一个return - jfriend00
显示剩余7条评论

48

一种常见的惯用语(可能适合你,也可能不适合),是将returnreject结合使用,以同时拒绝 Promise 并退出函数,以便不执行包括resolve在内的函数的其余部分。如果你喜欢这种风格,它可以使你的代码更加紧凑。

function divide(numerator, denominator) {
  return new Promise((resolve, reject) => {
    if (denominator === 0) return reject("Cannot divide by 0");
                           ^^^^^^^^^^^^^^
    resolve(numerator / denominator);
  });
}

这段代码能够正常运行的原因是Promise构造函数不会对任何返回值进行操作,并且无论如何resolvereject都不返回任何内容。

另外一个答案中展示了回调函数,也可以使用相同的惯用法。

function divide(nom, denom, cb){
  if(denom === 0) return cb(Error("Cannot divide by zero"));
                  ^^^^^^^^^
  cb(null, nom / denom);
} 

这样做没问题,因为调用divide的人并不期望它返回任何东西,并且不会处理返回值。


20
我不喜欢这个。这会让人误以为你正在退回某些东西,但事实上并不是。你正在调用"拒绝"函数,然后使用"返回"来结束函数执行。请将它们放在不同的行上,你现在的做法只会让人感到困惑。代码可读性至关重要。 - basickarl
11
@KarlMorrison实际上返回了“某物”,一种被拒绝的承诺。我认为你所说的“概念”非常个人化。返回reject状态并没有什么问题。 - Frondor
7
@Frondor,我认为你没有理解我的写作意图。当然,你和我都明白在同一行返回拒绝时不会发生任何事情。但是对于那些不太熟悉JavaScript的开发人员来说呢?这种编程方式会降低代码的可读性。如今JavaScript生态系统已经够混乱了,而推广这种做法只会让情况变得更糟。这是不好的编程实践。 - basickarl
3
@KarlMorrison 个人观点不等于坏习惯。解释一下返回值的含义可能会有助于新的JavaScript开发者理解代码内部的运作过程。 - Toby Caulk
3
如果一个人需要学习return的含义,那么他们就不应该随意使用Promise,而是应该先学习基础编程知识。 - basickarl
显示剩余4条评论

28

如果在完成resolve/reject后没有"return",可能会发生一些不好的事情(例如页面重定向),而你本意是想要它停止的。来源:我曾遇到过这种情况。


9
感谢示例。我曾遇到一个问题,我的程序会发出100多个无效的数据库查询请求,而我无法找出原因。后来发现是在拒绝之后没有“返回”。这是一个小错误,但我吸取了教训。 - AdamInTheOculus
另一个个人经历哈哈:拒绝无效的ID并不允许其被“收藏”(在“收藏列表”中),但不返回它却使其“既被拒绝又被添加到收藏列表中”!一开始它甚至没有反映出真相,因为有一个onError回调手动更新了缓存(),在下一次渲染时(例如当请求添加另一个项目到收藏列表时)它突然揭示了[悲伤]的真相! - aderchox

14

从技术上讲,这里并不需要1 - 因为 Promise 只能被解决 拒绝,并且只有一次机会。第一个 Promise 结果胜出,每个后续结果都将被忽略。这与 Node 样式的回调不同

话虽如此,在实践中,确保仅调用一个 Promise 是好的清洁实践,特别是在没有进一步异步/延迟处理的情况下,像本例一样。 "提前返回"的决定与结束任何函数时其工作已完成的方式相同 - 与继续无关或不必要的处理相比。

在适当的时间返回(或以其他方式使用条件语句避免执行“其他”情况)可以减少意外运行处于无效状态的代码或执行不必要的副作用的机会;因此,这使得代码更少容易“突然断开”


1 从技术上讲,这个答案也取决于在这种情况下,“return”之后的代码是否会导致副作用。JavaScript 可以愉快地除以零并返回 +Infinity/-Infinity 或 NaN。


很好的脚注! - HankCa

9

Ori已经解释了不必要使用return, 但这是一个好的习惯。请注意,promise构造函数是throw safe,因此它将忽略后面传递的抛出异常,本质上你有一些难以观察的副作用。

请注意,在回调中早期return也非常常见:

function divide(nom, denom, cb){
     if(denom === 0){
         cb(Error("Cannot divide by zero");
         return; // unlike with promises, missing the return here is a mistake
     }
     cb(null, nom / denom); // this will divide by zero. Since it's a callback.
} 

因此,在承诺中这是一个良好的实践,但在回调函数中则是必需的。以下是有关您代码的一些注意事项:

  • 您的用例是假设性的,请勿实际使用具有同步操作的承诺。
  • 承诺构造函数忽略返回值。某些库会警告您如果您返回非undefined值,则会发出警告以免犯下该错误。大多数库并不那么聪明。
  • 承诺构造函数是安全的,它将异常转换为拒绝,但正如其他人指出的那样 - 承诺只解决一次。

6

在许多情况下,可以分别验证参数并立即使用Promise.reject(reason)返回一个拒绝的 promise。

function divide2(numerator, denominator) {
  if (denominator === 0) {
    return Promise.reject("Cannot divide by 0");
  }
  
  return new Promise((resolve, reject) => {
    resolve(numerator / denominator);
  });
}


divide2(4, 0).then((result) => console.log(result), (error) => console.log(error));


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