使用一个函数来改变条件内的东西,使条件依赖于顺序,这是不好的做法吗?

Is it bad practice to use a function that changes stuff inside a condition, making the condition order-dependent?

var a = 1;

function myFunction() {
    ++a;
    return true;
}

// Alert pops up.
if (myFunction() && a === 2) {
    alert("Hello, world!");
}

// Alert does not pop up.
if (a === 3 && myFunction()) {
    alert("Hello, universe!");
}

https://jsfiddle.net/3oda22e4/6/

myFunction 递增一个变量和 returns 一些东西。如果我在包含它递增的变量的 if 语句中使用这样的函数,则条件将取决于顺序。

这样做是好是坏,为什么?

这是不好的做法,原因如下:

  • 代码的可读性远不如结构良好的代码。如果代码稍后由第三方检查,这一点非常重要。

  • 如果以后更改 myfunction,代码流将完全不可预测,可能需要进行重大文档更新。

  • 小而简单的更改会对代码的执行产生巨大影响。

  • 看起来很业余

如果你不得不问,那可不是什么好习惯。是的,正是出于您提到的原因,这是一种不好的做法:更改逻辑运算的操作数顺序不应影响结果,因此通常应避免条件中的副作用。特别是当它们隐藏在函数中时。

该函数是纯函数(只读取状态并执行一些逻辑)还是改变状态从它的名字中应该很明显。您有多种选择来修复此代码:

  • 将函数调用放在 if:

    之前
    function tryChangeA() {
        a++;
        return true;
    }
    
    var ok = tryChangeA();
    if (ok && a == 2) … // alternatively: if (a == 2 && ok)
    
  • if:

    中明确突变
    function testSomething(val) {
        return true;
    }
    
    if (testSomething(++a) && a == 2) …
    
  • 将逻辑放在被调用的函数中:

    function changeAndTest() {
        a++;
        return a == 2;
    }
    
    if (changeAndTest()) …
    

无论您是否更改条件中使用的变量,条件都取决于顺序。您用作示例的两个 if 语句是不同的,并且无论您是否使用 myFunction() 都会有所不同。它们相当于:

if (myFunction()) {
   if (a === 2) {
     alert("Hello, world!")
   }
}

// Alert does not pop up.
if (a === 3) {
   if (myFunction()) {
     alert("Hello, universe!")
   }
}

在我看来,您代码中的错误做法不是您在条件内更改条件的操作数值,而是您的应用程序状态在甚至不接受 this 的函数内公开和操作状态变化变量作为参数。我们通常会尝试将函数与其范围之外的代码隔离开来,并使用它们的 return 值来影响其余代码。全局变量在 90% 的情况下都是一个坏主意,并且随着您的代码库变得越来越大,它们往往会产生难以跟踪、调试和解决的问题。

该代码实际上提出了不止一种不良做法:

var a = 1;
function myFunction() {
    ++a; // 1
    return true;
}

if (myFunction() && a === 2) { // 2, 3, 4
    alert("Hello, world!")
}

if (a === 3 && myFunction()) { // 2, 3, 4
    alert("Hello, universe!")
}
  1. 在不同的范围内改变一个变量。这可能是也可能不是问题,但通常是。

  2. if 语句条件内调用函数。 这本身不会引起问题,但它并不真正干净。 更好的做法是将该函数的结果分配给一个变量,可能具有描述性名称。这将帮助阅读代码的人了解您想要在 if 语句中检查的确切内容。顺便说一下,函数总是 return true.

  3. 使用一些幻数。 想象一下其他人正在阅读该代码,并且它是大型代码库的一部分。这些数字是什么意思?更好的解决方案是用命名良好的常量替换它们。

  4. 如果要支持更多的消息,需要添加更多的条件。 更好的方法是使其可配置。

我会重写代码如下:

const ALERT_CONDITIONS = { // 4
  WORLD_MENACE: 2,
  UNIVERSE_MENACE: 3,
};

const alertsList = [
  {
    message: 'Hello world',
    condition: ALERT_CONDITIONS.WORLD_MENACE,
  },
  {
    message: 'Hello universe',
    condition: ALERT_CONDITIONS.UNIVERSE_MENACE,
  },
];


class AlertManager {
  constructor(config, defaultMessage) {
    this.counter = 0; // 1
    this.config = config; // 2
    this.defaultMessage = defaultMessage;
  }

  incrementCounter() {
    this.counter++;
  }

  showAlert() {
    this.incrementCounter();
    let customMessageBroadcasted = false;
    this.config.forEach(entry => { //2
      if (entry.condition === this.counter) {
        console.log(entry.message);
        customMessageBroadcasted = true; // 3
      }
    });

    if (!customMessageBroadcasted) {
      console.log(this.defaultMessage)
    }
  }
}

const alertManager = new AlertManager(alertsList, 'Nothing to alert');
alertManager.showAlert();
alertManager.showAlert();
alertManager.showAlert();
alertManager.showAlert();
  1. 一个 class 具有精确的函数,它使用自己的内部状态,而不是一堆依赖于可能位于任何地方的变量的函数。是否使用 class 是一个选择问题。它可以用不同的方式来完成。

  2. 使用配置。这意味着如果您想添加更多消息,您根本不必触摸代码。例如,假设该配置来自数据库。

  3. 您可能会注意到,这会在函数的外部范围内改变一个变量,但在这种情况下它不会导致任何问题。

  4. 使用名称清晰的常量。 (好吧,它可能会更好,但请耐心等待我举个例子)。

MyFunction违反了一个叫做Tell, Don't Ask的原则。

MyFunction 改变某物的状态,从而使它成为一个命令。如果 MyFunction 成功或以某种方式未能增加 a,它不应该 return 真或假。它被赋予了一份工作,它必须尝试成功,或者如果它发现目前无法完成工作,它应该抛出异常。

在 if 语句的谓词中,MyFunction 用作查询。

一般来说,查询不应该表现出副作用(即不改变可以观察到的东西)。一个好的查询可以被视为一个计算,因为对于相同的输入,它应该产生相同的输出(有时被描述为 "idempotent")。

知道这些是帮助您和其他人推理代码的指导方针也很重要。 可以导致混淆的代码,。代码混乱是错误的温床。

有一些很好的模式,例如 Trier-Doer 模式,可以像您的代码示例一样使用,但阅读它的每个人都必须通过名称和结构了解发生了什么。

一个改变东西的函数。什么世界也来了?此函数必须在每次调用时更改内容和 return 不同的值。

考虑一副扑克牌的 dealCard 函数。它发牌 1-52。每次调用它都应该 return 不同的值。

function dealCard() {
    ++a;
    return cards(a);
}

/* 我们假设阵列卡已洗牌 */

/* 为了简洁起见,我们假设牌组是无限的并且不会在 52 处循环*/