Funções

As pessoas pensam que ciência da computação é a arte de gênios, mas a realidade é o oposto: muitas pessoas fazendo coisas que se constroem umas sobre as outras, como uma parede de pedrinhas.

Donald Knuth
Ilustração de folhas de samambaia com formato fractal, abelhas ao fundo

Funções são uma das ferramentas mais centrais na programação em JavaScript. O conceito de encapsular um pedaço do programa em um valor tem muitos usos. Ele nos dá uma forma de estruturar programas maiores, reduzir repetição, associar nomes a subprogramas e isolar esses subprogramas uns dos outros.

A aplicação mais óbvia das funções é definir novo vocabulário. Criar novas palavras em prosa geralmente é um mau estilo, mas em programação é indispensável.

Falantes adultos de inglês normalmente têm cerca de 20.000 palavras em seu vocabulário. Poucas linguagens de programação vêm com 20.000 comandos embutidos. E o vocabulário que existe tende a ser mais precisamente definido e, portanto, menos flexível do que na linguagem humana. Consequentemente, nós precisamos introduzir novas palavras para evitar verbosidade excessiva.

Definindo uma função

Uma definição de função é uma ligação comum em que o valor da ligação é uma função. Por exemplo, este código define square para se referir a uma função que produz o quadrado de um número dado:

const square = function(x) {
  return x * x;
};

console.log(square(12));
// → 144

Uma função é criada com uma expressão que começa com a palavra-chave function. Funções têm um conjunto de parâmetros (neste caso, apenas x) e um corpo, que contém as instruções que devem ser executadas quando a função é chamada. O corpo de uma função criada dessa forma deve sempre ser envolvido por chaves, mesmo quando consiste em apenas uma única instrução.

Uma função pode ter múltiplos parâmetros ou nenhum parâmetro. No exemplo a seguir, makeNoise não lista nenhum nome de parâmetro, enquanto roundTo (que arredonda n para o múltiplo mais próximo de step) lista dois:

const makeNoise = function() {
  console.log("Pling!");
};

makeNoise();
// → Pling!

const roundTo = function(n, step) {
  let remainder = n % step;
  return n - remainder + (remainder < step / 2 ? 0 : step);
};

console.log(roundTo(23, 10));
// → 20

Algumas funções, como roundTo e square, produzem um valor, e outras não, como makeNoise, cujo único resultado é um efeito colateral. Uma instrução return determina o valor que a função retorna. Quando a execução encontra essa instrução, ela sai imediatamente da função atual e entrega o valor retornado ao código que chamou a função. Uma palavra-chave return sem uma expressão após ela fará a função retornar undefined. Funções que não têm nenhuma instrução return, como makeNoise, também retornam undefined.

Parâmetros de uma função se comportam como ligações comuns, mas seus valores iniciais são fornecidos por quem chama a função, não pelo código dentro da própria função.

Ligações e escopos

Cada ligação tem um escopo, que é a parte do programa na qual a ligação é visível. Para ligações definidas fora de qualquer função, bloco ou módulo (veja Capítulo 10), o escopo é o programa inteiro — você pode se referir a essas ligações em qualquer lugar. Eles são chamados de globais.

Ligações criadas para parâmetros de função ou declarados dentro de uma função só podem ser referenciados dentro dessa função, então são conhecidos como ligações locais. Cada vez que a função é chamada, novas instâncias dessas ligações são criadas. Isso fornece algum isolamento entre funções — cada chamada de função atua em seu próprio pequeno mundo (seu ambiente local) e frequentemente pode ser entendida sem saber muito sobre o que está acontecendo no ambiente global.

Ligações declaradas com let e const são, na verdade, locais ao bloco em que são declaradas, então, se você criar uma deles dentro de um loop, o código antes e depois do loop não pode “vê-la”. No JavaScript anterior a 2015, apenas funções criavam novos escopos, então ligações no estilo antigo, criadas com a palavra-chave var, são visíveis em toda a função em que aparecem — ou em todo o escopo global, se não estiverem dentro de uma função.

let x = 10;   // global
if (true) {
  let y = 20; // local ao bloco
  var z = 30; // também global
}

Cada escopo pode “enxergar para fora” no escopo ao seu redor, então x é visível dentro do bloco no exemplo. A exceção é quando múltiplas ligações têm o mesmo nome — nesse caso, o código só pode ver o mais interno. Por exemplo, quando o código dentro da função halve se refere a n, ele está vendo o seu próprio n, não o n global.

const halve = function(n) {
  return n / 2;
};

let n = 10;
console.log(halve(100));
// → 50
console.log(n);
// → 10

Escopo aninhado

JavaScript distingue não apenas ligações globais e locais. Blocos e funções podem ser criados dentro de outros blocos e funções, produzindo múltiplos graus de localidade.

Por exemplo, esta função — que imprime os ingredientes necessários para fazer uma porção de homus — tem outra função dentro dela:

const hummus = function(factor) {
  const ingredient = function(amount, unit, name) {
    let ingredientAmount = amount * factor;
    if (ingredientAmount > 1) {
      unit += "s";
    }
    console.log(`${ingredientAmount} ${unit} ${name}`);
  };
  ingredient(1, "can", "chickpeas");
  ingredient(0.25, "cup", "tahini");
  ingredient(0.25, "cup", "lemon juice");
  ingredient(1, "clove", "garlic");
  ingredient(2, "tablespoon", "olive oil");
  ingredient(0.5, "teaspoon", "cumin");
};

O código dentro da função ingredient pode ver a ligação factor da função externa, mas suas ligações locais, como unit ou ingredientAmount, não são visíveis na função externa.

O conjunto de ligações visíveis dentro de um bloco é determinado pela posição desse bloco no texto do programa. Cada escopo local também pode ver todos os escopos locais que o contêm, e todos os escopos podem ver o escopo global. Essa abordagem para visibilidade de ligações é chamada de escopo léxico.

Funções como valores

Uma ligação de função geralmente atua simplesmente como um nome para uma parte específica do programa. Essa ligação é definida uma vez e nunca é alterado. Isso facilita confundir a função com seu nome.

Mas os dois são diferentes. Um valor de função pode fazer todas as coisas que outros valores fazem — você pode usá-lo em expressãos arbitrárias, não apenas chamá-lo. É possível armazenar um valor de função em um nova ligação, passá-lo como argumento para uma função e assim por diante. Da mesma forma, uma ligação que contém uma função ainda é apenas uma ligação comum e pode, se não for constante, receber um novo valor, assim:

let launchMissiles = function() {
  missileSystem.launch("now");
};
if (safeMode) {
  launchMissiles = function() {/* do nothing */};
}

No Capítulo 5, discutiremos coisas interessantes que podemos fazer ao passar valores de função para outras funções.

Notação de declaração

Há uma forma um pouco mais curta de criar uma ligação de função. Quando a palavra-chave function é usada no início de uma instrução, ela funciona de maneira diferente:

function square(x) {
  return x * x;
}

Isso é uma declaração de função. A instrução define a ligação square e a aponta para a função dada. É um pouco mais fácil de escrever e não requer ponto e vírgula após a função.

Há uma sutileza nessa forma de definição de função.

console.log("The future says:", future());

function future() {
  return "You'll never have flying cars";
}

O código acima funciona, mesmo que a função seja definida abaixo do código que a usa. Declarações de função não fazem parte do fluxo normal de execução de cima para baixo. Elas são conceitualmente movidas para o topo de seu escopo e podem ser usadas por todo o código nesse escopo. Isso às vezes é útil porque oferece a liberdade de organizar o código da forma que parecer mais clara, sem se preocupar em definir todas as funções antes de usá-las.

Arrow functions

Há uma terceira notação para funções, que parece bem diferente das outras. Em vez da palavra-chave function, ela usa uma seta (=>) formada por um sinal de igual e um sinal de maior (não confundir com o operador maior ou igual, que é escrito >=):

const roundTo = (n, step) => {
  let remainder = n % step;
  return n - remainder + (remainder < step / 2 ? 0 : step);
};

A seta vem depois da lista de parâmetros e é seguida pelo corpo da função. Ela expressa algo como “esta entrada (os parâmetros) produz este resultado (o corpo)”.

Quando há apenas um nome de parâmetro, você pode omitir os parênteses ao redor da lista de parâmetros. Se o corpo for uma única expressão, em vez de um bloco entre chaves, essa expressão será retornada pela função. Assim, estas duas definições de square fazem a mesma coisa:

const square1 = (x) => { return x * x; };
const square2 = x => x * x;

Quando uma arrow function não tem parâmetros, sua lista de parâmetros é apenas um conjunto vazio de parênteses.

const horn = () => {
  console.log("Toot");
};

Não há um motivo profundo para termos tanto arrow functions quanto expressões com function na linguagem. Exceto por um pequeno detalhe, que veremos no Capítulo 6, elas fazem a mesma coisa. Arrow functions foram adicionadas em 2015, principalmente para permitir escrever pequenas expressões de função de forma menos verbosa. Vamos usá-las com frequência no Capítulo 5.

A pilha de chamadas

A forma como o controle flui através das funções é um pouco complexa. Vamos olhar mais de perto. Aqui está um programa simples que faz algumas chamadas de função:

function greet(who) {
  console.log("Hello " + who);
}
greet("Harry");
console.log("Bye");

Uma execução desse programa acontece mais ou menos assim: a chamada de greet faz o controle pular para o início dessa função (linha 2). A função chama console.log, que assume o controle, faz seu trabalho e então devolve o controle à linha 2. Lá, ela chega ao fim da função greet, então retorna ao lugar que a chamou — linha 4. A linha seguinte chama console.log novamente. Depois que isso retorna, o programa chega ao fim.

Podemos mostrar o fluxo de controle esquematicamente assim:

not in function
  in greet
    in console.log
  in greet
not in function
  in console.log
not in function

Como uma função precisa voltar ao lugar que a chamou quando retorna, o computador precisa lembrar o contexto de onde a chamada aconteceu. Em um caso, console.log precisa retornar para a função greet quando termina. No outro, ele retorna para o fim do programa.

O lugar onde o computador armazena esse contexto é a pilha de chamadas. Cada vez que uma função é chamada, o contexto atual é armazenado no topo dessa pilha. Quando uma função retorna, ela remove o contexto do topo da pilha e usa esse contexto para continuar a execução.

Armazenar essa pilha requer espaço na memória do computador. Quando a pilha cresce demais, o computador falha com uma mensagem como “out of stack space” ou “too much recursion”. O código a seguir ilustra isso ao fazer ao computador uma pergunta muito difícil que causa um vai-e-vem infinito entre duas funções. Ou melhor, seria infinito, se o computador tivesse uma pilha infinita. Como não tem, ficaremos sem espaço, ou “estouraremos a pilha”.

function chicken() {
  return egg();
}
function egg() {
  return chicken();
}
console.log(chicken() + " came first.");
// → ??

Argumentos opcionais

O código a seguir é permitido e executa sem problemas:

function square(x) { return x * x; }
console.log(square(4, true, "hedgehog"));
// → 16

Definimos square com apenas um parâmetro. Ainda assim, quando a chamamos com três, a linguagem não reclama. Ela ignora os argumentos extras e calcula o quadrado do primeiro.

JavaScript é extremamente flexível quanto ao número de argumentos que você pode passar para uma função. Se você passar argumentos demais, os extras são ignorados. Se passar de menos, os parâmetros faltantes recebem o valor undefined.

A desvantagem disso é que é possível — até provável — que você passe acidentalmente o número errado de argumentos para funções. E ninguém vai te avisar. A vantagem é que você pode usar esse comportamento para permitir que uma função seja chamada com diferentes números de argumentos. Por exemplo, esta função minus tenta imitar o operador - agindo sobre um ou dois argumentos:

function minus(a, b) {
  if (b === undefined) return -a;
  else return a - b;
}

console.log(minus(10));
// → -10
console.log(minus(10, 5));
// → 5

Se você escrever um operador = após um parâmetro, seguido por uma expressão, o valor dessa expressão substituirá o argumento quando ele não for fornecido. Por exemplo, esta versão de roundTo torna seu segundo argumento opcional. Se você não o fornecer ou passar undefined, ele terá valor padrão 1:

function roundTo(n, step = 1) {
  let remainder = n % step;
  return n - remainder + (remainder < step / 2 ? 0 : step);
};

console.log(roundTo(4.5));
// → 5
console.log(roundTo(4.5, 2));
// → 4

O próximo capítulo apresentará uma forma de o corpo de uma função acessar a lista completa de argumentos recebidos. Isso é útil porque permite que uma função aceite qualquer número de argumentos. Por exemplo, console.log faz isso, exibindo todos os valores que recebe:

console.log("C", "O", 2);
// → C O 2

Closure

A capacidade de tratar funções como valores, combinada com o fato de que ligações locais são recriados toda vez que uma função é chamada, levanta uma questão interessante: o que acontece com ligações locais quando a chamada de função que os criou não está mais ativa?

O código a seguir mostra um exemplo disso. Ele define uma função, wrapValue, que cria uma ligação local. Em seguida, retorna uma função que acessa e retorna essa ligação local.

function wrapValue(n) {
  let local = n;
  return () => local;
}

let wrap1 = wrapValue(1);
let wrap2 = wrapValue(2);
console.log(wrap1());
// → 1
console.log(wrap2());
// → 2

Isso é permitido e funciona como esperado — ambas as instâncias da ligação ainda podem ser acessadas. Essa situação demonstra bem o fato de que ligações locais são criadas novamente a cada chamada, e chamadas diferentes não afetam as ligações locais umas das outras.

Esse recurso — ser capaz de referenciar uma instância específica de uma ligação local em um escopo externo — é chamado de closure. Uma função que referencia ligações de escopos locais ao seu redor é chamada de closure. Esse comportamento não apenas livra você de se preocupar com o tempo de vida das ligações, como também possibilita usar valores de função de formas criativas.

Com uma pequena mudança, podemos transformar o exemplo anterior em uma forma de criar funções que multiplicam por uma quantidade arbitrária.

function multiplier(factor) {
  return number => number * factor;
}

let twice = multiplier(2);
console.log(twice(5));
// → 10

A ligação explícita local do exemplo wrapValue não é realmente necessário, já que um parâmetro já é uma ligação local.

Pensar em programas assim exige alguma prática. Um bom modelo mental é pensar nos valores de função como contendo tanto o código em seu corpo quanto o ambiente em que foram criados. Quando chamada, a função enxerga o ambiente em que foi criada, não o ambiente em que é chamada.

No exemplo anterior, multiplier é chamada e cria um ambiente em que seu parâmetro factor está ligado a 2. O valor de função que ela retorna, armazenado em twice, lembra desse ambiente, de modo que quando é chamado, multiplica seu argumento por 2.

Recursão

É perfeitamente aceitável que uma função chame a si mesma, desde que não faça isso tantas vezes a ponto de estourar a pilha. Uma função que chama a si mesma é chamada de recursiva. A recursão permite que algumas funções sejam escritas em um estilo diferente. Veja, por exemplo, esta função power, que faz o mesmo que o operador ** (exponenciação):

function power(base, exponent) {
  if (exponent == 0) {
    return 1;
  } else {
    return base * power(base, exponent - 1);
  }
}

console.log(power(2, 3));
// → 8

Isso é bastante próximo da forma como matemáticos definem exponenciação e, possivelmente, descreve o conceito com mais clareza do que o loop que usamos no Capítulo 2. A função chama a si mesma várias vezes com expoentes cada vez menores para realizar a multiplicação repetida.

No entanto, essa implementação tem um problema: em implementações típicas de JavaScript, ela é cerca de três vezes mais lenta do que uma versão usando um loop for. Executar um loop simples geralmente é mais barato do que chamar uma função várias vezes.

O dilema entre velocidade e elegância é interessante. Você pode vê-lo como algo que varia entre facilidade para humanos e facilidade para máquinas. Quase qualquer programa pode ser tornado mais rápido tornando-o maior e mais complicado. O programador precisa encontrar um equilíbrio adequado.

No caso da função power, uma versão menos elegante (com loop) ainda é bastante simples e fácil de ler. Não faz muito sentido substituí-la por uma função recursiva. Muitas vezes, porém, um programa lida com conceitos tão complexos que abrir mão de alguma eficiência para torná-lo mais direto é útil.

Preocupar-se com eficiência pode ser uma distração. É mais um fator que complica o design de programas, e quando você já está fazendo algo difícil, essa preocupação extra pode ser paralisante.

Portanto, geralmente você deve começar escrevendo algo que seja correto e fácil de entender. Se estiver preocupado com a lentidão — o que geralmente não é o caso, já que a maior parte do código simplesmente não é executada com frequência suficiente para consumir muito tempo — você pode medir depois e melhorar, se necessário.

Recursão nem sempre é apenas uma alternativa ineficiente a loops. Alguns problemas realmente são mais fáceis de resolver com recursão do que com loops. Na maioria das vezes, são problemas que exigem explorar ou processar vários “ramos”, cada um dos quais pode se ramificar ainda mais.

Considere este quebra-cabeça: começando pelo número 1 e repetidamente somando 5 ou multiplicando por 3, pode-se produzir um conjunto infinito de números. Como você escreveria uma função que, dado um número, tenta encontrar uma sequência dessas somas e multiplicações que produza esse número? Por exemplo, o número 13 pode ser alcançado multiplicando por 3 e depois somando 5 duas vezes, enquanto o número 15 não pode ser alcançado.

Aqui está uma solução recursiva:

function findSolution(target) {
  function find(current, history) {
    if (current == target) {
      return history;
    } else if (current > target) {
      return null;
    } else {
      return find(current + 5, `(${history} + 5)`) ??
             find(current * 3, `(${history} * 3)`);
    }
  }
  return find(1, "1");
}

console.log(findSolution(24));
// → (((1 * 3) + 5) * 3)

Note que este programa não necessariamente encontra a sequência mais curta de operações. Ele fica satisfeito ao encontrar qualquer sequência.

Tudo bem se você não entender imediatamente como esse código funciona. Vamos analisá-lo, pois é um ótimo exercício de pensamento recursivo.

A função interna find faz a recursão de fato. Ela recebe dois argumentos: o número atual e uma string que registra como chegamos a esse número. Se encontrar uma solução, retorna uma string que mostra como chegar ao alvo. Se não puder encontrar solução a partir desse número, retorna null.

Para isso, a função executa uma de três ações. Se o número atual for o número alvo, o histórico atual é uma forma de chegar lá, então ele é retornado. Se o número atual for maior que o alvo, não faz sentido continuar explorando esse ramo, pois tanto somar quanto multiplicar só aumentariam o número, então retorna null. Por fim, se ainda estivermos abaixo do alvo, a função tenta ambos os caminhos possíveis a partir do número atual chamando a si mesma duas vezes, uma para soma e outra para multiplicação. Se a primeira chamada retornar algo diferente de null, esse valor é retornado. Caso contrário, retorna-se o resultado da segunda chamada, seja ele uma string ou null.

Para entender melhor como essa função produz o efeito desejado, vejamos todas as chamadas a find ao buscar uma solução para o número 13:

find(1, "1")
  find(6, "(1 + 5)")
    find(11, "((1 + 5) + 5)")
      find(16, "(((1 + 5) + 5) + 5)")
        too big
      find(33, "(((1 + 5) + 5) * 3)")
        too big
    find(18, "((1 + 5) * 3)")
      too big
  find(3, "(1 * 3)")
    find(8, "((1 * 3) + 5)")
      find(13, "(((1 * 3) + 5) + 5)")
        found!

A indentação indica a profundidade da pilha de chamadas. Na primeira vez que find é chamada, a função começa chamando a si mesma para explorar a solução que começa com (1 + 5). Essa chamada continuará recursivamente para explorar todas as continuações possíveis que resultem em um número menor ou igual ao alvo. Como não encontra uma que atinja o alvo, retorna null para a primeira chamada. Lá, o operador ?? faz com que a chamada que explora (1 * 3) aconteça. Essa busca tem mais sorte — sua primeira chamada recursiva, por meio de outra chamada recursiva, chega ao número alvo. Essa chamada mais interna retorna uma string, e cada um dos operadores ?? nas chamadas intermediárias repassa essa string, retornando por fim a solução.

Fazendo funções crescerem

Existem duas maneiras mais ou menos naturais de introduzir funções em programas.

A primeira ocorre quando você se pega escrevendo código semelhante várias vezes. Você preferiria não fazer isso, já que mais código significa mais espaço para erros se esconderem e mais material para quem tenta entender o programa. Então você pega a funcionalidade repetida, encontra um bom nome para ela e a coloca em uma função.

A segunda maneira é quando você percebe que precisa de alguma funcionalidade que ainda não escreveu e que parece merecer sua própria função. Você começa nomeando a função e depois escreve seu corpo. Você pode até começar a escrever código que usa a função antes mesmo de defini-la.

O quão difícil é encontrar um bom nome para uma função é um bom indicativo de quão claro é o conceito que você está tentando encapsular. Vamos ver um exemplo.

Queremos escrever um programa que imprima dois números: o número de vacas e de galinhas em uma fazenda, com as palavras Cows e Chickens depois deles e zeros preenchidos antes dos números para que tenham sempre três dígitos:

007 Cows
011 Chickens

Isso pede uma função de dois argumentos — o número de vacas e o número de galinhas. Vamos programar.

function printFarmInventory(cows, chickens) {
  let cowString = String(cows);
  while (cowString.length < 3) {
    cowString = "0" + cowString;
  }
  console.log(`${cowString} Cows`);
  let chickenString = String(chickens);
  while (chickenString.length < 3) {
    chickenString = "0" + chickenString;
  }
  console.log(`${chickenString} Chickens`);
}
printFarmInventory(7, 11);

Escrever .length após uma string nos dá o comprimento dessa string. Assim, os loops while continuam adicionando zeros à frente até que tenham pelo menos três caracteres.

Missão cumprida! Mas, quando estamos prestes a enviar o código ao fazendeiro (junto com uma bela fatura), ela liga dizendo que também começou a criar porcos e pergunta se podemos estender o software para imprimi-los também.

Claro que podemos. Mas, no momento em que estamos prestes a copiar e colar aquelas quatro linhas mais uma vez, paramos e reconsideramos. Deve haver uma forma melhor. Aqui vai uma primeira tentativa:

function printZeroPaddedWithLabel(number, label) {
  let numberString = String(number);
  while (numberString.length < 3) {
    numberString = "0" + numberString;
  }
  console.log(`${numberString} ${label}`);
}

function printFarmInventory(cows, chickens, pigs) {
  printZeroPaddedWithLabel(cows, "Cows");
  printZeroPaddedWithLabel(chickens, "Chickens");
  printZeroPaddedWithLabel(pigs, "Pigs");
}

printFarmInventory(7, 11, 3);

Funciona! Mas esse nome, printZeroPaddedWithLabel, é meio estranho. Ele mistura três coisas — imprimir, preencher com zeros e adicionar um rótulo — em uma única função.

Em vez de extrair a parte repetida do programa como um todo, vamos tentar identificar um único conceito:

function zeroPad(number, width) {
  let string = String(number);
  while (string.length < width) {
    string = "0" + string;
  }
  return string;
}

function printFarmInventory(cows, chickens, pigs) {
  console.log(`${zeroPad(cows, 3)} Cows`);
  console.log(`${zeroPad(chickens, 3)} Chickens`);
  console.log(`${zeroPad(pigs, 3)} Pigs`);
}

printFarmInventory(7, 16, 3);

Uma função com um nome claro como zeroPad facilita para quem lê o código entender o que ela faz. Essa função também é útil em mais situações do que apenas neste programa específico. Por exemplo, você pode usá-la para ajudar a imprimir tabelas de números bem alinhadas.

Quão inteligente e versátil deve ser nossa função? Podemos escrever qualquer coisa, desde uma função muito simples que apenas preenche um número até três caracteres até um sistema complexo de formatação numérica que lide com números fracionários, negativos, alinhamento de casas decimais, preenchimento com diferentes caracteres e assim por diante.

Um princípio útil é evitar adicionar complexidade a menos que você tenha certeza de que vai precisar. Pode ser tentador escrever “frameworks” gerais para cada pedacinho de funcionalidade. Resista a isso. Você não vai fazer trabalho real — vai ficar ocupado escrevendo código que nunca usa.

Funções e efeitos colaterais

Funções podem ser divididas, de forma geral, entre aquelas chamadas por seus efeitos colaterais e aquelas chamadas por seu valor de retorno (embora também seja possível ter ambos).

A primeira função auxiliar no exemplo da fazenda, printZeroPaddedWithLabel, é chamada por seu efeito colateral: ela imprime uma linha. A segunda versão, zeroPad, é chamada por seu valor de retorno. Não é coincidência que a segunda seja útil em mais situações do que a primeira. Funções que criam valores são mais fáceis de combinar de novas maneiras do que funções que executam efeitos colaterais diretamente.

Uma função pura é um tipo específico de função que produz valores e que não apenas não tem efeitos colaterais, mas também não depende de efeitos colaterais de outros códigos — por exemplo, não lê ligações globais cujo valor pode mudar. Uma função pura tem a propriedade agradável de que, quando chamada com os mesmos argumentos, sempre produz o mesmo valor (e não faz mais nada). Uma chamada a essa função pode ser substituída por seu valor de retorno sem alterar o significado do código. Quando você não tem certeza de que uma função pura está funcionando corretamente, pode testá-la simplesmente chamando-a e saber que, se funciona nesse contexto, funcionará em qualquer contexto. Funções impuras tendem a exigir mais estrutura para serem testadas.

Ainda assim, não há motivo para se sentir mal ao escrever funções que não são puras. Efeitos colaterais costumam ser úteis. Não há como escrever uma versão pura de console.log, por exemplo, e console.log é algo bom de se ter. Algumas operações também são mais fáceis de expressar de forma eficiente quando usamos efeitos colaterais.

Resumo

Este capítulo ensinou como escrever suas próprias funções. A palavra-chave function, quando usada como expressão, pode criar um valor de função. Quando usada como instrução, pode declarar uma ligação e atribuir a ele uma função como valor. Arrow functions são outra forma de criar funções.

// Define f para conter um valor de função
const f = function(a) {
  console.log(a + 2);
};

// Declara g como uma função
function g(a, b) {
  return a * b * 3.5;
}

// Um valor de função menos verboso
let h = a => a % 3;

Uma parte fundamental para entender funções é compreender escopos. Cada bloco cria um novo escopo. Parâmetros e ligações declaradas em um determinado escopo são locais e não visíveis de fora. Ligações declaradas com var se comportam de forma diferente — eles acabam no escopo da função mais próxima ou no escopo global.

Separar as tarefas que seu programa realiza em diferentes funções é útil. Você não precisará se repetir tanto, e as funções podem ajudar a organizar um programa agrupando o código em partes que fazem coisas específicas.

Exercícios

Mínimo

O capítulo anterior apresentou a função padrão Math.min, que retorna o menor argumento. Agora podemos escrever uma função assim por conta própria. Defina a função min que recebe dois argumentos e retorna o menor deles.

// Seu código aqui.

console.log(min(0, 10));
// → 0
console.log(min(0, -10));
// → -10
Mostrar dicas...

Se tiver dificuldade em colocar chaves e parênteses nos lugares certos para obter uma definição de função válida, comece copiando um dos exemplos deste capítulo e modificando-o.

Uma função pode conter várias instruções return.

Recursão

Vimos que podemos usar % (o operador de resto) para testar se um número é par ou ímpar usando % 2 para ver se ele é divisível por dois. Aqui vai outra forma de definir se um número inteiro positivo é par ou ímpar:

Defina uma função recursiva isEven correspondente a essa descrição. A função deve aceitar um único parâmetro (um número inteiro positivo) e retornar um Boolean.

Teste-a com 50 e 75. Veja como ela se comporta com -1. Por quê? Consegue pensar em uma forma de corrigir isso?

// Seu código aqui.

console.log(isEven(50));
// → true
console.log(isEven(75));
// → false
console.log(isEven(-1));
// → ??
Mostrar dicas...

Sua função provavelmente se parecerá um pouco com a função interna find no exemplo recursivo findSolution exemplo deste capítulo, com uma cadeia if/else if/else que testa qual dos três casos se aplica. O else final, correspondente ao terceiro caso, faz a chamada recursiva. Cada um dos ramos deve conter uma instrução return ou, de alguma outra forma, garantir que um valor específico seja retornado.

Quando recebe um número negativo, a função continuará recursivamente, passando para si mesma números cada vez mais negativos, afastando-se cada vez mais de um resultado. Eventualmente, ela ficará sem espaço na pilha e abortará.

Contagem de letras

Você pode obter o N-ésimo caractere, ou letra, de uma string escrevendo [N] após a string (por exemplo, string[2]). O valor resultante será uma string contendo apenas um caractere (por exemplo, "b"). O primeiro caractere tem posição 0, o que faz com que o último esteja na posição string.length - 1. Em outras palavras, uma string de dois caracteres tem comprimento 2, e seus caracteres têm posições 0 e 1.

Escreva uma função chamada countBs que recebe uma string como único argumento e retorna um número que indica quantos caracteres “B” maiúsculos existem na string.

Em seguida, escreva uma função chamada countChar que se comporta como countBs, exceto que recebe um segundo argumento que indica qual caractere deve ser contado (em vez de contar apenas “B” maiúsculos). Reescreva countBs para usar essa nova função.

// Seu código aqui.

console.log(countBs("BOB"));
// → 2
console.log(countChar("kakkerlak", "k"));
// → 4
Mostrar dicas...

Sua função precisará de um loop que examine cada caractere da string. Ele pode usar um índice de zero até um a menos que seu comprimento (< string.length). Se o caractere na posição atual for igual ao que a função procura, ela adiciona 1 a uma variável contadora. Quando o loop terminar, o contador pode ser retornado.

Certifique-se de tornar todas as ligações usadas na função locais à função, declarando-os corretamente com let ou const.