Valores, Tipos e Operadores

Abaixo da superfície da máquina, o programa se move. Sem esforço, ele se expande e se contrai. Em grande harmonia, elétrons se dispersam e se reagrupam. As formas no monitor são apenas ondulações na água. A essência permanece invisível abaixo.

Master Yuan-Ma, The Book of Programming
Ilustração de um mar de pontos escuros e claros (bits) com ilhas nele

No mundo do computador, existe apenas dado. Você pode ler dados, modificar dados, criar novos dados—mas aquilo que não é dado não pode ser mencionado. Todos esses dados são armazenados como longas sequências de bits e, portanto, são fundamentalmente iguais.

Bits são qualquer tipo de coisa com dois valores, geralmente descritos como zeros e uns. Dentro do computador, eles assumem formas como uma carga elétrica alta ou baixa, um sinal forte ou fraco, ou um ponto brilhante ou opaco na superfície de um CD. Qualquer pedaço de informação discreta pode ser reduzido a uma sequência de zeros e uns e, assim, representado em bits.

Por exemplo, podemos expressar o número 13 em bits. Isso funciona da mesma forma que um número decimal, mas em vez de 10 dígitos diferentes, temos apenas 2, e o peso de cada um aumenta por um fator de 2 da direita para a esquerda. Aqui estão os bits que compõem o número 13, com os pesos dos dígitos mostrados abaixo deles:

   0   0   0   0   1   1   0   1
 128  64  32  16   8   4   2   1

Esse é o número binário 00001101. Seus dígitos não zero representam 8, 4 e 1, e somam 13.

Valores

Imagine um mar de bits—um oceano deles. Um computador moderno típico tem mais de 100 bilhões de bits em seu armazenamento volátil de dados (memória de trabalho). O armazenamento não volátil (o disco rígido ou equivalente) tende a ter ainda algumas ordens de magnitude a mais.

Para conseguir trabalhar com quantidades tão grandes de bits sem se perder, nós os separamos em blocos que representam partes de informação. Em um ambiente JavaScript, esses blocos são chamados de valores. Embora todos os valores sejam feitos de bits, eles desempenham papéis diferentes. Todo valor tem um tipo que determina seu papel. Alguns valores são números, alguns são pedaços de texto, alguns são funções, e assim por diante.

Para criar um valor, basta invocar seu nome. Isso é conveniente. Você não precisa reunir material de construção para seus valores nem pagar por eles. Você simplesmente chama por um, e whoosh, ele aparece. Claro, valores não são realmente criados do nada. Cada um precisa ser armazenado em algum lugar, e se você quiser usar uma quantidade gigantesca deles ao mesmo tempo, pode acabar ficando sem memória do computador. Felizmente, isso só é um problema se você precisar de todos simultaneamente. Assim que você não usa mais um valor, ele se dissipa, deixando seus bits para serem reciclados como material para a próxima geração de valores.

O restante deste capítulo apresenta os elementos atômicos dos programas JavaScript, ou seja, os tipos simples de valores e os operadores que podem agir sobre esses valores.

Números

Valores do tipo número (number) são, como era de se esperar, valores numéricos. Em um programa JavaScript, eles são escritos assim:

13

Usar isso em um programa fará com que o padrão de bits para o número 13 passe a existir dentro da memória do computador.

JavaScript usa um número fixo de bits, 64 deles, para armazenar um único valor numérico. Há apenas um certo número de padrões possíveis com 64 bits, o que limita a quantidade de números diferentes que podem ser representados. Com N dígitos decimais, você pode representar 10N números. De forma semelhante, com 64 dígitos binários, você pode representar 264 números diferentes, o que dá cerca de 18 quintilhões (um 18 com 18 zeros depois). É bastante coisa.

A memória dos computadores costumava ser muito menor, e as pessoas tendiam a usar grupos de 8 ou 16 bits para representar seus números. Era fácil ocorrer um overflow nesses números pequenos—ou seja, acabar com um número que não cabia na quantidade de bits disponível. Hoje, até mesmo computadores que cabem no bolso têm bastante memória, então você pode usar blocos de 64 bits livremente e só precisa se preocupar com overflow ao lidar com números realmente astronômicos.

Nem todos os números inteiros menores que 18 quintilhões cabem em um número do JavaScript, porém. Esses bits também armazenam números negativos, então um bit indica o sinal do número. Um problema maior é representar números não inteiros. Para isso, alguns dos bits são usados para armazenar a posição da vírgula decimal. O maior número inteiro que pode ser armazenado na prática está mais na faixa de 9 quadrilhões (15 zeros)—o que ainda é confortavelmente enorme.

Números fracionários são escritos usando um ponto:

9.81

Para números muito grandes ou muito pequenos, você também pode usar notação científica adicionando um e (de exponent), seguido pelo expoente do número.

2.998e8

Isso é 2.998 × 108 = 299.800.000.

Cálculos com números inteiros (também chamados de inteiros) menores que os 9 quadrilhões mencionados são garantidamente precisos. Infelizmente, cálculos com números fracionários geralmente não são. Assim como π (pi) não pode ser expresso com precisão por um número finito de dígitos decimais, muitos números perdem um pouco de precisão quando apenas 64 bits estão disponíveis para armazená-los. Isso é uma pena, mas causa problemas práticos apenas em situações específicas. O importante é estar ciente disso e tratar números fracionários digitais como aproximações, não como valores exatos.

Aritmética

A principal coisa a fazer com números é aritmética. Operações aritméticas como adição ou multiplicação recebem dois valores numéricos e produzem um novo número a partir deles. Veja como isso aparece em JavaScript:

100 + 4 * 11

Os símbolos + e * são chamados de operadores. O primeiro representa adição e o segundo representa multiplicação. Colocar um operador entre dois valores fará com que ele seja aplicado a esses valores e produza um novo valor.

Esse exemplo significa “Some 4 e 100 e multiplique o resultado por 11”, ou a multiplicação é feita antes da soma? Como você pode imaginar, a multiplicação acontece primeiro. Como na matemática, você pode mudar isso envolvendo a soma em parênteses.

(100 + 4) * 11

Para subtração, há o operador -. A divisão pode ser feita com o operador /.

Quando operadores aparecem juntos sem parênteses, a ordem em que são aplicados é determinada pela precedência dos operadores. O exemplo mostra que a multiplicação vem antes da adição. O operador / tem a mesma precedência que *. Da mesma forma, + e - têm a mesma precedência. Quando vários operadores com a mesma precedência aparecem lado a lado, como em 1 - 2 + 1, eles são aplicados da esquerda para a direita: (1 - 2) + 1.

Não se preocupe muito com essas regras de precedência. Em caso de dúvida, basta adicionar parênteses.

Há mais um operador aritmético que você talvez não reconheça de imediato. O símbolo % é usado para representar a operação de resto. X % Y é o resto da divisão de X por Y. Por exemplo, 314 % 100 produz 14, e 144 % 12 resulta em 0. A precedência do operador de resto é a mesma da multiplicação e divisão. Você também verá esse operador frequentemente chamado de modulo.

Números especiais

Existem três valores especiais em JavaScript que são considerados números, mas não se comportam como números normais. Os dois primeiros são Infinity e -Infinity, que representam os infinitos positivo e negativo. Infinity - 1 ainda é Infinity, e assim por diante. Mas não confie demais em cálculos com infinito. Eles não são matematicamente sólidos e rapidamente levam ao próximo número especial: NaN.

NaN significa “not a number” (não é um número), embora seja um valor do tipo número. Você obterá esse resultado quando, por exemplo, tentar calcular 0 / 0 (zero dividido por zero), Infinity - Infinity ou várias outras operações numéricas que não produzem um resultado significativo.

Strings

O próximo tipo básico de dado é a string. Strings são usadas para representar texto. Elas são escritas colocando seu conteúdo entre aspas.

`Down on the sea`
"Lie on the ocean"
'Float on the ocean'

Você pode usar aspas simples, aspas duplas ou crases para delimitar strings, desde que o tipo de aspas no início e no fim seja o mesmo.

Você pode colocar praticamente qualquer coisa entre aspas para que o JavaScript crie um valor string a partir disso. Mas alguns caracteres são mais complicados. Dá para imaginar como colocar aspas dentro de aspas pode ser difícil, já que elas parecerão marcar o fim da string. Quebras de linha (os caracteres que você obtém ao pressionar enter) só podem ser incluídas quando a string está entre crases (`).

Para permitir incluir esses caracteres em uma string, usa-se a seguinte notação: uma barra invertida (\) dentro de um texto entre aspas indica que o caractere seguinte tem um significado especial. Isso é chamado de escape do caractere. Uma aspa precedida por uma barra invertida não encerra a string, mas passa a fazer parte dela. Quando um caractere n aparece após uma barra invertida, ele é interpretado como uma quebra de linha. Da mesma forma, um t após uma barra invertida significa um caractere de tabulação. Veja a seguinte string:

"This is the first line\nAnd this is the second"

Este é o texto real dentro dessa string:

This is the first line
And this is the second

Há, claro, situações em que você quer que uma barra invertida em uma string seja apenas uma barra invertida, não um código especial. Se duas barras invertidas aparecem em sequência, elas se reduzem a uma só no valor final da string. É assim que a stringA newline character is written like "\n".” pode ser expressa:

"A newline character is written like \"\\n\"."

Strings também precisam ser modeladas como uma série de bits para existirem dentro do computador. A forma como o JavaScript faz isso é baseada no padrão Unicode. Esse padrão atribui um número a praticamente todo caractere que você possa precisar, incluindo caracteres do grego, árabe, japonês, armênio e assim por diante. Se temos um número para cada caractere, uma string pode ser descrita por uma sequência de números. E é isso que o JavaScript faz.

Há uma complicação, porém: a representação do JavaScript usa 16 bits por elemento de string, o que permite descrever até 216 caracteres diferentes. No entanto, o Unicode define mais caracteres do que isso—aproximadamente o dobro, atualmente. Assim, alguns caracteres, como muitos emoji, ocupam duas “posições de caractere” em strings do JavaScript. Voltaremos a isso no Capítulo 5.

Strings não podem ser divididas, multiplicadas ou subtraídas. O operador + pode ser usado com elas, não para somar, mas para concatenar—ou seja, colar duas strings. A linha a seguir produzirá a string "concatenate":

"con" + "cat" + "e" + "nate"

Valores string têm várias funções associadas (methods) que podem ser usadas para realizar outras operações sobre eles. Falarei mais sobre isso no Capítulo 4.

Strings escritas com aspas simples ou duplas se comportam praticamente da mesma forma—a única diferença é qual tipo de aspas você precisa escapar dentro delas. Strings entre crases, geralmente chamadas de template literals, podem fazer alguns truques a mais. Além de poderem abranger várias linhas, elas também podem incorporar outros valores.

`half of 100 is ${100 / 2}`

Quando você escreve algo dentro de ${} em um template literal, seu resultado será calculado, convertido para string e incluído naquela posição. Este exemplo produz a string "half of 100 is 50".

Operadores unários

Nem todos os operadores são símbolos. Alguns são escritos como palavras. Um exemplo é o operador typeof, que produz um valor string indicando o tipo do valor que você fornece.

console.log(typeof 4.5)
// → number
console.log(typeof "x")
// → string

Usaremos console.log em exemplos de código para indicar que queremos ver o resultado da avaliação de algo. (Mais sobre isso no próximo capítulo.)

Os outros operadores mostrados até agora neste capítulo operam sobre dois valores, mas typeof usa apenas um. Operadores que usam dois valores são chamados de operadores binários, enquanto aqueles que usam apenas um são chamados de operadores unários. O operador de menos (-) pode ser usado tanto como operador binário quanto como operador unário.

console.log(- (10 - 2))
// → -8

Valores Booleanos

Muitas vezes é útil ter um valor que diferencie apenas duas possibilidades, como “sim” e “não” ou “ligado” e “desligado”. Para isso, o JavaScript tem o tipo Booleano (Boolean), que possui apenas dois valores, true e false, escritos exatamente assim.

Comparação

Aqui está uma maneira de produzir valores booleanos:

console.log(3 > 2)
// → true
console.log(3 < 2)
// → false

Os sinais > e < são os símbolos tradicionais para “maior que” e “menor que”, respectivamente. Eles são operadores binários. Aplicá-los resulta em um valor booleano que indica se a relação é verdadeira nesse caso.

Strings podem ser comparadas da mesma forma.

console.log("Aardvark" < "Zoroaster")
// → true

A forma como strings são ordenadas é aproximadamente alfabética, mas não exatamente como em um dicionário: letras maiúsculas são sempre “menores” que minúsculas, então "Z" < "a", e caracteres não alfabéticos (!, -, e assim por diante) também entram na ordenação. Ao comparar strings, o JavaScript percorre os caracteres da esquerda para a direita, comparando os códigos Unicode um a um.

Outros operadores semelhantes são >= (maior ou igual), <= (menor ou igual), == (igual) e != (diferente).

console.log("Garnet" != "Ruby")
// → true
console.log("Pearl" == "Amethyst")
// → false

Há apenas um valor em JavaScript que não é igual a si mesmo: NaN (“not a number”).

console.log(NaN == NaN)
// → false

NaN representa o resultado de um cálculo sem sentido e, como tal, não é igual ao resultado de nenhum outro cálculo sem sentido.

Operadores lógicos

Também existem operações que podem ser aplicadas aos próprios valores booleanos. O JavaScript suporta três operadores lógicos: and, or e not. Eles podem ser usados para “raciocinar” sobre valores booleanos.

O operador && representa o and lógico. É um operador binário, e seu resultado é true apenas se ambos os valores fornecidos forem true.

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

O operador || representa o or lógico. Ele produz true se pelo menos um dos valores fornecidos for true.

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

Not é escrito como um ponto de exclamação (!). É um operador unário que inverte o valor fornecido—!true produz false e !false produz true.

Ao misturar esses operadores booleanos com operadores aritméticos e outros, nem sempre é óbvio quando parênteses são necessários. Na prática, você pode se virar sabendo que, entre os operadores que vimos até agora, || tem a menor precedência, depois vem &&, depois os operadores de comparação (>, == e assim por diante), e então o restante. Essa ordem foi escolhida de forma que, em expressões típicas como a seguinte, sejam necessários o mínimo possível de parênteses:

1 + 1 == 2 && 10 * 10 > 50

O último operador lógico que veremos não é unário nem binário, mas ternário, operando sobre três valores. Ele é escrito com um ponto de interrogação e dois pontos, assim:

console.log(true ? 1 : 2);
// → 1
console.log(false ? 1 : 2);
// → 2

Esse é chamado de operador condicional (ou às vezes apenas operador ternário, já que é o único desse tipo na linguagem). O operador usa o valor à esquerda do ponto de interrogação para decidir qual dos outros dois valores “escolher”. Se você escrever a ? b : c, o resultado será b quando a for true e c caso contrário.

Valores vazios

Existem dois valores especiais, escritos como null e undefined, que são usados para indicar a ausência de um valor significativo. Eles são valores em si, mas não carregam informação.

Muitas operações na linguagem que não produzem um valor significativo retornam undefined simplesmente porque precisam retornar algum valor.

A diferença de significado entre undefined e null é um acidente do design do JavaScript e, na maior parte do tempo, não importa. Nos casos em que você realmente precisa se preocupar com esses valores, recomendo tratá-los como praticamente intercambiáveis.

Conversão automática de tipos

Na introdução, mencionei que o JavaScript faz o possível para aceitar praticamente qualquer programa que você forneça, até mesmo programas que fazem coisas estranhas. Isso é bem demonstrado pelas seguintes expressões:

console.log(8 * null)
// → 0
console.log("5" - 1)
// → 4
console.log("5" + 1)
// → 51
console.log("five" * 2)
// → NaN
console.log(false == 0)
// → true

Quando um operador é aplicado ao tipo “errado” de valor, o JavaScript converte silenciosamente esse valor para o tipo necessário, usando um conjunto de regras que frequentemente não são o que você espera. Isso é chamado de coerção de tipo. O null na primeira expressão se torna 0, e "5" na segunda expressão se torna 5 (de string para número). Já na terceira expressão, o + tenta a concatenação de strings antes da adição numérica, então o 1 é convertido para "1" (de número para string).

Quando algo que não corresponde claramente a um número (como "five" ou undefined) é convertido para número, você obtém o valor NaN. Operações aritméticas subsequentes com NaN continuam produzindo NaN, então, se você se deparar com esse valor em um lugar inesperado, procure por conversões de tipo acidentais.

Ao comparar valores do mesmo tipo usando o operador ==, o resultado é fácil de prever: você deve obter true quando ambos os valores são iguais, exceto no caso de NaN. Mas quando os tipos diferem, o JavaScript usa um conjunto de regras complicado e confuso para decidir o que fazer. Na maioria dos casos, ele simplesmente tenta converter um dos valores para o tipo do outro. No entanto, quando null ou undefined aparece em qualquer lado do operador, o resultado será true apenas se ambos os lados forem um desses dois valores.

console.log(null == undefined);
// → true
console.log(null == 0);
// → false

Esse comportamento costuma ser útil. Quando você quer testar se um valor tem um valor real em vez de null ou undefined, pode compará-lo com null usando == ou !=.

E se você quiser testar se algo é exatamente o valor false? Expressões como 0 == false e "" == false também são true por causa da conversão automática de tipo. Quando você não quer que nenhuma conversão de tipo aconteça, há dois operadores adicionais: === e !==. O primeiro testa se um valor é estritamente igual ao outro, e o segundo testa se não é estritamente igual. Assim, "" === false é false, como esperado.

Recomendo usar os operadores de comparação com três caracteres de forma defensiva para evitar que conversões inesperadas de tipo causem problemas. Mas quando você tem certeza de que os tipos em ambos os lados são iguais, não há problema em usar os operadores mais curtos.

Curto-circuito dos operadores lógicos

Os operadores lógicos && e || lidam com valores de tipos diferentes de uma maneira peculiar. Eles convertem o valor à esquerda para o tipo Booleano para decidir o que fazer, mas, dependendo do operador e do resultado dessa conversão, retornam o valor original da esquerda ou o valor da direita.

O operador ||, por exemplo, retorna o valor à esquerda quando esse valor pode ser convertido para true e retorna o valor à direita caso contrário. Isso tem o efeito esperado quando os valores são booleanos e faz algo análogo para valores de outros tipos.

console.log(null || "user")
// → user
console.log("Agnes" || "user")
// → Agnes

Podemos usar essa funcionalidade como uma forma de definir um valor padrão. Se você tem um valor que pode estar vazio, pode colocar || depois dele com um valor substituto. Se o valor inicial puder ser convertido para false, você obterá o substituto. As regras para converter strings e números em valores booleanos dizem que 0, NaN e a string vazia ("") contam como false, enquanto todos os outros valores contam como true. Isso significa que 0 || -1 produz -1, e "" || "!?" resulta em "!?".

O operador ?? é semelhante ao ||, mas retorna o valor da direita apenas se o valor da esquerda for null ou undefined, e não se for outro valor que possa ser convertido para false. Muitas vezes, isso é preferível ao comportamento de ||.

console.log(0 || 100);
// → 100
console.log(0 ?? 100);
// → 0
console.log(null ?? 100);
// → 100

O operador && funciona de forma semelhante, mas ao contrário. Quando o valor à esquerda é algo que pode ser convertido para false, ele retorna esse valor; caso contrário, retorna o valor da direita.

Outra propriedade importante desses dois operadores é que a parte à direita só é avaliada quando necessário. No caso de true || X, não importa o que seja X—mesmo que seja um trecho de programa que faça algo terrível—o resultado será true, e X nunca será avaliado. O mesmo vale para false && X, que é false e ignora X. Isso é chamado de avaliação de curto-circuito.

O operador condicional funciona de forma semelhante. Dos segundo e terceiro valores, apenas aquele que for selecionado é avaliado.

Resumo

Vimos quatro tipos de valores em JavaScript neste capítulo: números, strings, booleanos e valores indefinidos (undefined). Esses valores são criados ao digitar seu nome (true, null) ou seu conteúdo (13, "abc").

Você pode combinar e transformar valores com operadores. Vimos operadores binários para aritmética (+, -, *, / e %), concatenação de strings (+), comparação (==, !=, ===, !==, <, >, <=, >=) e lógica (&&, ||, ??), além de vários operadores unários (- para negar um número, ! para negação lógica e typeof para descobrir o tipo de um valor) e um operador ternário (?:) para escolher entre dois valores com base em um terceiro.

Isso já é suficiente para usar o JavaScript como uma calculadora de bolso, mas não muito mais. O próximo capítulo começará a unir essas expressões em programas básicos.