Wyrażenia logiczne w JavaScript

Cześć! To jest fragment książki JavaScript od podstaw, która ma pomóc w nauce programowania od zera.

Do tej pory widzieliśmy zaledwie bardzo proste warunki sprawdzające wartość jednej zmiennej. Jak zachować się w sytuacji, gdy chcielibyśmy wykonać akcję, w której jednocześnie dwa warunki są spełnione? Na przykład:

  • czy tweet jest promowany i pochodzi od osoby obserwowanej,
  • czy tweet jest własny lub pochodzi od osoby obserwowanej.

Aby uzyskiwać odpowiedzi na takie pytania potrzebujemy operatorów logicznych, czyli stawianych między wartościami logicznymi.

Operator i &&

Aby dobrze zrozumieć operatory logiczne, przytoczę historię małego Jasia. Chciał on pograć na komputerze, więc poszedł do mamy poprosić o pozwolenie. Ta jednak powiedziała: "Będziesz mógł pograć, gdy odrobisz lekcje i posprzątasz pokój".

Ten warunek w programowaniu mógłby być wyrażony następująco:

const canPlayGames = finishedHomework && cleanedRoom;

Widzimy tam zmienne finishedHomeworkcleanedRoom, reprezentujące kolejno ukończenie zadań domowych i posprzątanie pokoju (zakładamy, że są to wartości logiczne, czyli true lub false). Pomiędzy nimi widzimy natomiast operator i && 1, który sprawia, że całe wyrażenie jest true tylko wtedy, gdy obydwie jego strony są true2.

aba && b
truetruetrue
truefalsefalse
falsetruefalse
falsefalsefalse
console.log(true && true); // true
console.log(true && false); // false
console.log(false && true); // false
console.log(false && false); // false

// A czy Jasio może mieć szczeniaka?
const haveTime = true;
const isResponsible = false;
const canHavePuppy = isResponsible && haveTime;
console.log(canHavePuppy); // false

Za przykładowe częste wykorzystanie tego operatora uznaje się sprawdzenie, czy liczba znajduje się w jakimś zakresie: czy jest większa od minimum i mniejsza niż maksimum. Dla przykładu moglibyśmy sprawdzić, czy zmienna percent ma poprawną wartość, czyli czy znajduje się między 0 a 100.

if (percent >= 0 && percent <= 100) {
  console.log("Poprawna wartość");
}

Operator lub ||

Niestety Jasio nie odrobił lekcji, zatem nie mógł pograć na komputerze. Wtedy jednak zadzwonili do niego znajomi i zaprosili go do kina. Jasio poszedł do mamy i poprosił o pieniądze na wyjście, a ta powiedziała: "Dostaniesz pieniądze, jeśli posprzątasz garaż lub umyjesz samochód". Ten warunek można przedstawić następująco:

const willGoToCinema = cleanGarage || washedCar;

Operator ||3 reprezentuje alternatywę. Całe wyrażenie zwraca true, gdy spełnione jest albo cleanGarage, albo washedCar, albo oba te warunki jednocześnie.

aba || b
truetruetrue
truefalsetrue
falsetruetrue
falsefalsefalse
console.log(true || true); // true
console.log(true || false); // true
console.log(false || true); // true
console.log(false || false); // false

// Czy Jasio dostanie czekoladę?
const behavedWell = true;
const cleanedRoom = false;
const canEatChocolate = behavedWell || cleanedRoom;
console.log(canEatChocolate); // true

Jasio tak się zapalił do pracy, że zarówno posprzątał garaż, jak i umył samochód. Zdziwiona mama powiedziała: "Nie chciałam byś robił i jedno i drugie", a Jasio odpowiedział: "Użyłaś łącznika lub, który akceptuje zrobienie zarówno jednego, jak i drugiego, podobnie jak || w programowaniu". Mama Jasia zaakceptowała to wyjaśnienie, więc poszedł on z kolegami do kina.

Operator negacji !

Ostatnim operatorem logicznym, który powinniśmy poznać jest zaprzeczenie, czyli pojedynczy wykrzyknik ! przed wartością logiczną. Podobnie jak - (minus) przed liczbą zamienia jej znak na odwrotny, tak ! (operator negacji) przed wartością logiczną zamienia ją na odwrotną. Operator negacji ! zamienia true na false, a false na true. Dlatego często tłumaczy się go jako "nie".

cond!cond
truefalse
falsetrue
console.log(!true); // false
console.log(!false); // true

const isAmazing = true;
console.log(!isAmazing); // false

const isBoring = false;
console.log(!isBoring); // true

// Podobnie jak przy liczbach
const positive = 1;
console.log(-positive); // -1

const negative = -1;
console.log(-negative); // 1

Programowanie akceptuje podwójne zaprzeczenie, co oznacza, że każdy kolejny znak zaprzeczenia odwraca wartość logiczną4.

console.log(true); // true
console.log(!true); // false
console.log(!!true); // true
console.log(!!!true); // false
console.log(!!!!true); // true

Operator negacji ! stawia się często w bardziej złożonym wyrażeniu przed pojedynczą zmienną. Jasio zrozumiał to wtedy, gdy jego mama powiedziała: "Będziesz mógł grać na komputerze, jeśli nie zawalisz sprawdzianu z matematyki i posprzątasz pokój". Wykorzystał on zaprzeczenie, by przełożyć to zdanie na język programistyczny.

const canPlayGames = !failedMathTest && cleanedRoom;

Zwróć uwagę, że negacja odnosi się tutaj tylko do niezawalenia sprawdzianu. !true && false zwraca false, podobnie jak -10 + 11 równe jest 1, a nie -21. Jeśli chcielibyśmy, aby zaprzeczenie negowało większy obszar wyrażenia, powinniśmy cały ten fragment otoczyć nawiasami. !(true && false) zwraca true.

// Sprawdzian zaliczony
const failedMathTest = false;

// Pokój nieposprzątany
const cleanedRoom = false;

// Nie zawaliłeś sprawdzianu i posprzątałeś pokój
console.log(!failedMathTest && cleanedRoom); // false

// Nie jest tak, że 
// zawaliłeś sprawdzian i posprzątałeś pokój
console.log(!(failedMathTest && cleanedRoom)); // true

Czytanie wyrażeń logicznych

Warto poćwiczyć czytanie wyrażeń logicznych na głos. Ja zwykle robię to następująco:

  • && czytam jako i.
  • || czytam jako lub.
  • ! przed zmienną czytam jako nie, a przed nawiasem jako nie jest tak, że.

Poćwiczmy to na poniższych przykładach, dla zmiennych:

  • isGrounded jako "ma szlaban"
  • cleanedRoom jako "posprzątał pokój"
  • passedTest jako "zaliczył sprawdzian"

Przeczytaj poniższe zdania:

  • cleanedRoom && passedTest - posprzątał pokój i zaliczył sprawdzian.
  • !isGrounded || passedTest - nie ma szlabanu lub zaliczył sprawdzian.
  • cleanedRoom && !passedTest - posprzątał pokój i nie zaliczył sprawdzianu.
  • !(isGrounded || !passedTest) - nie jest tak, że ma szlaban lub nie zaliczył sprawdzianu.
  • !(!cleanedRoom && !passedTest) - nie jest tak, że nie posprzątał pokoju i nie zaliczył sprawdzianu. Jest to równoznaczne z cleanedRoom || passedTest.

a || b jest zawsze równoznaczne z !(!a && !b), a a && b jest równoznaczne z !(!a || !b). Takie obrócenia stosuje się, aby pozbyć się ciężkiego do przeczytania zaprzeczenia. Przykładowo, jeśli mielibyśmy warunek !(hasComputer && !isGrounded) to moglibyśmy to zamienić na !hasComputer || isGrounded. Wykrzyknik przy isGrounded zniknął, bo !!a jest równoznaczne z a.

Ćwiczenie: Złożone wyrażenia logiczne

Określ, co wypisze do konsoli poniższy kod:

const hasComputer = true;
const passedTest = false;
let isGrounded = false;

console.log(hasComputer && passedTest);
console.log(passedTest || isGrounded);
console.log(hasComputer && !passedTest);

let canPlayGames = hasComputer && !isGrounded;
console.log(canPlayGames);

let playGames = hasComputer && canPlayGames;

if (!passedTest) {
  isGrounded = true;
}

console.log(playGames);
console.log(passedTest || !isGrounded);
console.log(!(!hasComputer || !passedTest));

Odpowiedzi na końcu książki.

Wartości falsy i truthy

JavaScript posiada specjalną funkcjonalność sprawiającą, że jeśli w warunku umieścimy wartość, która nie jest wartością logiczną (true albo false), to mimo wszystko zostanie zinterpretowana jako wartość logiczna. W JavaScript używa się jej powszechnie najczęściej jako alternatywy dla sprawdzenia, czy obiekt nie jest null albo undefined. Dzieje się tak dlatego, że obydwie wartości są traktowane jako false (mówi się na nie falsy) w czasie, gdy obiekty są traktowane jako true (są truthy). W przypadku gdy wiemy, że zmienna zawiera obiekt, null albo undefined, użycie jej w warunku if można traktować jak sprawdzenie, czy dana wartość jest obiektem.

if (user) {
  // Robimy coś z user
}

// Ekwiwalent, zakładając że user to obiekt

if (user !== null && user !== undefined) {
  // Robimy coś z user
}

W całym JavaScript falsy (a więc interpretowane jako false) są następujące wartości5:

  • false,
  • null,
  • undefined,
  • 0 (oraz -0 i 0n),
  • NaN,
  • "".

Wszystkie inne wartości interpretowane są jako true i mówimy o nich, że są truthy.

Zwróć uwagę na pułapkę powyższego sprawdzenia dla tekstów i liczb, ponieważ zostaną one odrzucone także w przypadku "" (pustego stringa) albo 0.

Zwracanie wartości przez &&||

Uwaga: Ta sekcja jest bardziej zaawansowana i nie będzie potrzebna w dalszej części książki. Jeśli nie czujesz się pewnie z dotychczasową wiedzą, pomiń ją.

W JavaScript nie tylko wartości logiczne mogą być użyte jako warunek w instrukcji if. Podobnie operatory logiczne &&|| akceptują inne wartości, niż tylko truefalse. W takiej sytuacji wartość zwracana przez te operatory może nie być wartością logiczną.

Operator && zwraca lewą stronę, jeśli była falsy lub prawą, jeśli lewa była truthy.

console.log(true && true) // true
console.log(true && false) // false
console.log(false && true) // false
console.log(false && false) // false

console.log(true && "Prawa") // Prawa
console.log(false && "Prawa") // false
console.log(true && null) // null
console.log("Lewa" && "Prawa") // Prawa

Operator || zwraca lewą stronę, jeśli była truthy lub prawą, jeśli lewa była falsy.

console.log(true || true) // true
console.log(true || false) // true
console.log(false || true) // true
console.log(false || false) // false

console.log(true || "Prawa") // true
console.log(false || "Prawa") // "Prawa"
console.log("Lewa" || "Prawa") // "Lewa"

Ta cecha języka JavaScript jest często wykorzystywana jako krótsza alternatywa dla operatora warunkowego.

Ćwiczenie: Wartości zwracane przez operator warunkowy i operatory logiczne

  1. Dla jakiej wartości name poniższy fragment kodu wyświetli "Ma na imię Jasio", a dla jakich tylko pustego stringa?
const nameLabel = name ? "Ma na imię " + name : ""
console.log(nameLabel)
  1. Dla jakiej wartości age poniższy fragment kodu wyświetli "Ma 10 lat", a dla jakich null?
const nameLabel = age ? "Ma " + age + " lat" : null
console.log(nameLabel)
  1. Dla jakiej wartości name poniższy fragment kodu wyświetli "Ma na imię Janek"? Co zostanie wyświetlone dla name będącego falsy?
const nameLabel = name && "Ma na imię " + name
console.log(nameLabel)
  1. Dla jakiej wartości age poniższy fragment kodu wyświetli wartość age, a dla jakich 0?
const displayAge =
  (age && typeof age === "number" && age > 0) ? age : 0
console.log(displayAge)

Odpowiedzi na końcu książki.

1:

&& to podwójny znak et (ang. ampersand), po angielsku oznaczający "and", czyli "i".

2:

Precyzyjniej mówiąc, całe wyrażenie jest truthy, gdy obydwie strony są truthy.

3:

|| to podwójna kreska pionowa, określana czasem jako pipe. W programowaniu często jest utożsamiana z alternatywą.

4:

Bawiąc się językiem, możemy powiedzieć: nie jest tak, że programowanie nie akceptuje podwójnego zaprzeczenia. A nawet (nie nie nie nie) akceptuje wielokrotne zaprzeczenie.

5:

Umieściłem pełną listę, ale wartości NaN0n nie były omawiane i nie będą nam potrzebne w dalszej części książki. Dla zainteresowanych NaN to skrót od Not-A-Number i powstaje, gdy na przykład próbujemy odczytać liczbę z czegoś, co liczbą nie jest parseInt("whatever"). Jeśli chodzi o n po liczbie, to jest wykorzystywane, by przechowywać potencjalnie bardzo duże liczby (typ "bigint").