В моём экземпляре «Вы не знаете JS: Типы и грамматические конструкции» есть раздел про порядок выполнения логических операторов. Здесь есть неточность: результаты примеров выше обусловлены тем, что интерпретатор отбрасывает части выражения, которые не требуются вычислять.
Действуют принципы Ленивых вычислений (Lazy evaluation) и Вычислений по короткой схеме (Short-circuit evaluation):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | // Хелперы const log = (...args) => console.log(...args) // Результат функции — последнее выражение после оператора "запятая" const getTrue = num => (log('get true', num), true) const getFalse = num => (log('get false', num), false) // &&: Последовательность выражений выполняется до первого ложного результата: getTrue(1) && getFalse(2) && getTrue(3) && getTrue(4) // get true 1 // get false 2 // ||: Выполняется и возвращается только первый операнд, вернувший true getTrue(1) || getFalse(2) || getFalse(3) || getTrue(4) // get true 1 (getTrue(1) || getFalse(2)) && getFalse(3) // get true 1 // get false 3 // Даже если оба операнда верные, выполняется и возвращается только первый из них getTrue(1) || getTrue(2) // get true 1 |
Short-circuit evaluation: отбрасывает операнды, которые не изменят результата выражения.
Lazy evaluation: не выполняет отброшенные операнды (то есть до того, как они использованы в выражении), даже если это вызовы функции.
Short-circuit evaluation поддерживается только для логических операторов, для побитовых операторов все операнды вычисляются.
1 2 3 4 | getTrue(1) | getTrue(2) | getTrue(3) // get true 1 // get true 2 // get true 3 |
Применение
Любители Clean code для улучшения читаемости кода могут написать что-то вроде:
1 2 3 4 5 6 7 | const heavyOperationResult = callHeavyOperation(args) const anotherHeavyOperationResult = callHeavyOperation2(args) // Обе операции выполнены, даже если первый вернул false if (heavyOperationResult && anotherHeavyOperationResult) { doLogic() } |
В каком-нибудь компилируемом языке компилятор, возможно, сделает оптимизацию и встроит вызов функции вместо переменной. JS интерпретатор выполнит то, что написано разработчиком, и вычислит оба выражения перед использованием.
Разумнее встроить вызовы тяжёлых операций и написать:
1 2 3 4 | // Тяжёлая операция callHeavyOperation2 не выполняется, если первый операнд вернул false if (callHeavyOperation(args) && callHeavyOperation2(args)) { doLogic() } |
Примечание: принципы Clean code я люблю и уважаю, а пример, конечно, выдуман. Результаты функций, сохранённые в переменной, могут пригодиться далее по коду программы, но всегда стоит подумать, нельзя ли в подобной ситуации сэкономить немного ресурсов.
Ещё один пример с React
Иногда я вижу результаты прилежного причёсывания кода по принципу «Собрать все переменные в одном месте. Ч — Читаемость!»:
1 2 3 4 5 6 7 8 9 10 11 12 | const derivedProps = getDerivedProps(props) const reducedProps = reduceProps(derivedProps) const headerComponentProps = getHeaderComponentProps(reducedProps) // ... const childComponentProps = getChildComponentProps(reducedProps) return <Fragment> <h1 {...headerComponentProps} /> {props.children && ( <Wrapper props={childComponentProps}>{props.children}</Wrapper> )} </Fragment> |
childComponentProps
вычисляются в любом случае, даже если Wrapper
не выводится.
Как и в прошлом примере, стоит встроить вызов метода, и вычислять свойства компонента только при необходимости:
1 2 3 4 5 | {props.children && ( <Wrapper props={getChildComponentProps(reducedProps)}> {props.children} </Wrapper> )} |