Alternative to nested ternary operator in JS

2020-06-06 17:38发布

问题:

I personally love ternary operators, and in my humble opinion, they make complicated expressions very easy to digest. Take this one:

  word = (res.distance === 0) ? 'a'
    : (res.distance === 1 && res.difference > 3) ? 'b'
    : (res.distance === 2 && res.difference > 5 && String(res.key).length > 5) ? 'c'
    : 'd';

However in our project's ESLINT rules nested ternary operators are forbidden, so I have to get rid of the above.

I'm trying to find out alternatives to this approach. I really don't want to turn it into a huge if / else statement, but don't know if there's any other options.

回答1:

Your alternatives here are basically:

  1. That if/else you don't want to do
  2. A switch combined with if/else

I tried to come up with a reasonable lookup map option, but it got unreasonable fairly quickly.

I'd go for #1, it's not that big:

if (res.distance == 0) {
    word = 'a';
} else if (res.distance == 1 && res.difference > 3) {
    word = 'b';
} else if (res.distance == 2 && res.difference > 5 && String(res.key).length > 5) {
    word = 'c';
} else {
    word = 'd';
}

If all the braces and vertical size bother you, without them it's almost as concise as the conditional operator version:

if (res.distance == 0) word = 'a';
else if (res.distance == 1 && res.difference > 3) word = 'b';
else if (res.distance == 2 && res.difference > 5 && String(res.key).length > 5) word = 'c';
else word = 'd';

(I'm not advocating that, I never advocate leaving off braces or putting the statement following an if on the same line, but others have different style perspectives.)

#2 is, to my mind, more clunky but that's probably more a style comment than anything else:

word = 'd';
switch (res.distance) {
    case 0:
        word = 'a';
        break;
    case 1:
        if (res.difference > 3) {
            word = 'b';
        }
        break;
    case 2:
        if (res.difference > 5 && String(res.key).length > 5) {
            word = 'c';
        }
        break;
}

And finally, and I am not advocating this, you can take advantage of the fact that JavaScript's switch is unusual in the B-syntax language family: The case statements can be expressions, and are matched against the switch value in source code order:

switch (true) {
    case res.distance == 0:
        word = 'a';
        break;
    case res.distance == 1 && res.difference > 3:
        word = 'b';
        break;
    case res.distance == 2 && res.difference > 5 && String(res.key).length > 5:
        word = 'c';
        break;
    default:
        word = 'd';
        break;
}

How ugly is that? :-)



回答2:

To my taste, a carefully structured nested ternary beats all those messy ifs and switches:

const isFoo = res.distance === 0;
const isBar = res.distance === 1 && res.difference > 3;
const isBaz = res.distance === 2 && res.difference > 5 && String(res.key).length > 5;

const word =
  isFoo ? 'a' :
  isBar ? 'b' :
  isBaz ? 'c' :
          'd' ;


回答3:

You could write an immediately invoked function expression to make it a little more readable:

const word = (() =>  {
  if (res.distance === 0) return 'a';
  if (res.distance === 1 && res.difference > 3) return 'b';
  if (res.distance === 2 && res.difference > 5 && String(res.key).length > 5) return 'c';
  return 'd';
})();

Link to repl



回答4:

We can simplify it using basic operators like && and ||

let obj = {}

function checkWord (res) {
      return (res.distance === 0)   && 'a'
             || (res.distance === 1 && res.difference > 3) && 'b' 
             || (res.distance === 2 && res.difference > 5  && String(res.key).length > 5) && 'c'
             || 'd';
           
}

// case 1 pass
obj.distance = 0
console.log(checkWord(obj))

// case 2 pass
obj.distance = 1
obj.difference = 4
console.log(checkWord(obj))

// case 3 pass
obj.distance = 2
obj.difference = 6
obj.key = [1,2,3,4,5,6]
console.log(checkWord(obj))

// case 4 fail all cases
obj.distance = -1
console.log(checkWord(obj))



回答5:

If all your truthy conditions evaluate to truthy values (so the value between the question mark and the semicolon evaluates to true if coerced to boolean...) you could make your ternary expressions return false as the falsy expression. Then you could chain them with the bitwise or (||) operator to test the next condition, until the last one where you return the default value.

In the example below, the "condsXXX" array represent the result of evaluating the conditions. "conds3rd" simulates the 3rd condition is true and "condsNone" simulates no condition is true. In a real life code, you'd have the conditions "inlined" in the assignment expression:

var conds3rd = [false, false, true];
var condsNone = [false, false, false];

var val3rd = (conds3rd[0] ? 1 : false) ||
  (conds3rd[1] ? 2 : false) ||
  (conds3rd[2] ? 3 : 4);

var valNone = (condsNone[0] ? 1 : false) ||
  (condsNone[1] ? 2 : false) ||
  (condsNone[2] ? 3 : 4);

alert(val3rd);
alert(valNone);

Your example could end up like below:

word = ((res.distance === 0) ? 'a' : false) ||
    ((res.distance === 1 && res.difference > 3) ? 'b' : false) ||
    ((res.distance === 2 && res.difference > 5 && String(res.key).length > 5) ? 'c' : 'd';

As a side note, I don't feel it's a good looking code, but it is quite close to using the pure ternary operator like you aspire to do...



回答6:

word = (res.distance === 0) ? 'a'
: (res.distance === 1 && res.difference > 3) ? 'b'
: (res.distance === 2 && res.difference > 5 && String(res.key).length > 5) ? 'c'
: 'd';

This is an older question, but this is how I would do it... I would start with the default case and then change the variable or pass it unchanged as desired.

var word = 'd';
word = (res.distance === 0) ? 'a' : word;
word = (res.distance === 1 && res.difference > 3) ? 'b' : word
word = (res.distance === 2 && res.difference > 5 && String(res.key).length > 5) ? 'c' : word;


回答7:

If you are looking to use const with a nested ternary expression, you can replace the ternary with a function expression.

const res = { distance: 1, difference: 5 };

const branch = (condition, ifTrue, ifFalse) => condition?ifTrue:ifFalse;
const word = branch(
  res.distance === 0,    // if
  'a',                   // then
  branch(                // else
    res.distance === 1 && res.difference > 3,   // if
    'b',                                        // then
    branch(                                     // else
      res.distance === 2 && res.difference > 5,   // if
      'c',                                        // then
      'd'                                         // else
    )
  )
);

console.log(word);

or using named parameters via destructuring...

const branch2 = function(branch) {
  return branch.if ? branch.then : branch.else;
}

const fizzbuzz = function(num) {
  return branch2({
    if: num % 3 === 0 && num % 5 === 0,
    then: 'fizzbuzz',
    else: branch2({
        if: num % 3 === 0,
        then: 'fizz',
        else: branch2({
          if: num % 5 === 0,
          then: 'buzz',
          else: num
        })
      })
  });
}

console.log(
  [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16].map(
    cv => fizzbuzz(cv)
  )
);

... edit: It may be clearer to model it after the python if expression like this:

const res = { distance: 1, difference: 5 };

const maybe = def => ({
  if: expr => {
    if (expr) {
      return { else: () => def };
    } else {
      return { else: els => els };
    }
  }
});
const word = maybe('a').if(res.distance === 0).else(
  maybe('b').if(res.distance === 1 && res.difference > 3).else(
    maybe('c').if(res.distance === 2 && res.difference > 5).else('d')
  )
);
console.log(word);



回答8:

I faced this too recently and a google search led me here, and I want to share something I discovered recently regarding this:

a && b || c

is almost the same thing as

a ? b : c

as long as b is truthy. If b isn't truthy, you can work around it by using

!a && c || b

if c is truthy.

The first expression is evaluated as (a && b) || c as && has more priority than ||.

If a is truthy then a && b would evaluate to b if b is truthy, so the expression becomes b || c which evaluates to b if it is truthy, just like a ? b : c would if a is truthy, and if a is not truthy then the expression would evaluate to c as required.

Alternating between the && and || trick and ? and || in the layers of the statement tricks the no-nested-ternary eslint rule, which is pretty neat (although I would not recommend doing so unless there is no other way out).

A quick demonstration:

true ? false ? true : true ? false : true ? true ? true : false : true : true
// which is interpreted as
true ? (false ? true : (true ? false : (true ? (true ? true : false) : true))) : true
// now with the trick in alternate levels
true ? (false && true || (true ? false : (true && (true ? true : false) || true))) : true
// all of these evaluate to false btw

I actually cheated a bit by choosing an example where b is always truthy, but if you are just setting strings then this should work fine as even '0' is ironically truthy.



回答9:

I've been using a switch(true) statement for these cases. In my opinion this syntax feels slightly more elegant than nested if/else operators

switch (true) {
  case condition === true :
    //do it
    break;
  case otherCondition === true && soOn < 100 :
    // do that
    break;
}