Definindo Funções em Javascript
Algo bem comum das linguagens de programação é o uso de funções, cada linguagem tem suas particularidades e maneiras específicas de como definir as mesmas. Nesse post vamos entender as diferentes definições e tipos de funções em JavaScript.
Atualmente até o momento em que esse post está sendo escrito, existem cinco tipos de definições, sendo:
- Functions declaration (Função de declaração)
- Functions expression (Função de expressão)
- Arrow Functions (Função de flecha)
- Functions constructor (Função construtora)
- Generator Functions (Função geradora)
As definições mais comuns são declaration e expression (veremos ambas mais à frente). Essas duas definições são bem similiar, geralmente não fazemos distinção entre elas, mas, nesse post eu irei explicar de maneira separada.
Então vamos começar a dar uma olhada com mais detalhes em cada uma delas.
Functions declaration
O jeito mais básico de definir funções em JavaScript é através da function declaration, toda função de declaração começa com a palavra reservada e obrigatória function
, seguida pelo nome da função (também obrigatório) e uma lista de parâmetros (opcionais) separados por vírgula e encapsulados em parenteses (obrigatórios), o último passo é definir as chaves (obrigatórias) que será o corpo da função.
A estrutura seria mais ou menos assim:
Essa estrutura é a mais simples, porém, obrigatória para as functions declaration. Como mencionado anteriormente, também podemos definir parâmetros opcionais separados por vírgula:
Dentro de uma função de declaração, também podemos ter outras funções, que só irão ser visíveis dentro do bloco (chaves) onde a mesma foi declarada.
Vamos ver alguns exemplos:
function ola() {
console.log('Olá')
}
ola()
function ola() {
function mensagem() {
return 'Olá'
}
console.log(mensagem())
}
ola()
function ola() {
function mensagem() {
return 'Olá'
}
console.log(mensagem())
}
ola()
console.log(mensagem()) // a função mensagem não irá existir nesse trecho de código, ela somente existe dentro da função ola
function ola(nome) {
console.log('Olá', nome)
}
ola('Matheus')
Dado os exemplos acima, vamos destacar algumas curiosidades:
- Em JavaScript podemos declarar funções dentro de funções.
- Uma função declarada dentro de outra, apenas irá viver durante o escopo da função pai, ou seja, a função
mensagem
apenas existe no escopo/bloco da funçãoola
. - Para invocar uma função utilizamos o seu nome seguido por parenteses
()
. - Para alimentar algum parâmetro, adicionamos o valor dentro dos parenteses
('Matheus')
, onde a ordem dos parâmetros irá influenciar, ou seja, se uma função recebe dois parâmetros:function ola(nome, sobrenome)
, ao chamá-la precisamos tomar cuidado com a ordem dos mesmos:ola('Matheus', 'Castiglioni')
é diferente deola('Castiglioni', 'Matheus')
.
Functions expression
Como mencionado anteriormente, as expression e declaration são muito parecidas, a diferença é que uma função de expressão pode ser lidada como uma qualquer expressão em JavaScript, por exemplo:
const nome = 'Matheus'
Nesse exemplo, estamos criando uma expressão onde definimos uma variável chamada nome
e atribuímos uma String
para ela.
Com as funções de expressão, podemos fazer algo muito semelhante:
const ola = function() {
console.log('Olá')
}
ola()
Repare que é bem parecido com as funções de declaração, uma das sútis diferenças é que ela está sendo atribuída para uma variável, onde não definimos o nome da função e sim o nome da variável que irá referenciar a mesma. Tornando a estrutura mais ou menos assim:
Mas, você deve estar se perguntando:
Porque eu iria querer atribuir uma função à uma variável?
Atribuir uma função à uma variável pode ser muito útil, por exemplo: Assim podemos definir a função exatamente onde ela precisa ser chamada, ou seja, definimos a função apenas onde precisamos dela, isso em alguns momentos pode tornar nosso código mais simples de entender.
Saiba mais
Uma expressão em JavaScript onde definimos variáveis, também pode ser chamada de assignment expression.
Arrow Functions
Como usamos bastante funções, nada melhor do que criar atalhos e sintaxes menos verbosas, não é? Pois é, um dos motivos da criação das funções de flecha é facilitar a criação e utilização de funções em JavaScript, ou seja, elas permitem a criação de funções de maneira resumida.
Em outras palavras, as arrow functions são simplificações para as functions expression:
Imagine um exemplo onde temos a seguinte função de expressão:
const numeroAleatorio = function() {
return Math.random()
}
Tivemos que escrever bastante coisa, para apenas criar uma função que devolve um número aleatório. Agora vamos transformar essa função de expressão em uma função de flecha.
const numeroAleatorio = () => {
return Math.random()
}
Até então nada diferente, apenas removemos a palavra reservada function
e adicionamos uma seta (sinal de =
seguido por >
).
Vamos começar a fazer algumas melhorias e refatorações, o primeiro passo será no return
, repare que a função possuí apenas uma linha como conteúdo, será que não podemos deixar o retorno implicito? Sim:
const numeroAleatorio = () => Math.random()
Repare que não só removemos a palavra reservada return
, mas, como também as chaves, ou seja:
- Caso o corpo da arrow function tenha apenas uma linha, podemos omitir a declaração das chaves
- Seguindo o principio que o corpo tem apenas um linha, também não precisamos utilizar o
return
, podemos removê-lo, pois a primeira linha será executada e retornada automaticamente.
Show, o que mais podemos fazer? Vamos imaginar uma segunda função:
function nomeCompleto(nome, sobrenome) {
return `${nome} ${sobrenome}`
}
Como vimos anteriormente, quando trata-se de apenas uma linha e já precisamos fazer o retorno, podemos omitir as chaves e a palavra return
:
const nomeCompleto = (nome, sobrenome) => `${nome} ${sobrenome}`
Agora, imagine que não precisamos mais do sobrenome, vamos apenas criar uma função para dizer Olá, NOME
:
function ola(nome) {
return `Olá ${nome}`
}
Seguindo o que já sabemos:
const ola = (nome) => `Olá ${nome}`
Mas, quando uma arrow function possuí apenas um parâmetro, também podemos omitir os parenteses, ficando:
const ola = nome => `Olá ${nome}`
Sendo assim, podemos definir uma arrow function:
Functions constructor
As funções construtoras são declaradas e definidas como qualquer outra expression ou declaration, o jeito de se usar é o mesmo, a diferença está mais no caso de uso e o que ela retorna.
Uma pequena observação é que normalmente o nome de funções construtoras começa com a primeira letra maiúscula, por exemplo:
function Pessoa() {}
Nesse exemplo, estamos criando uma função construtora que irá criar um objeto Pessoa
.
A principal diferença entre a função construtora está na maneira como ela é invocada, enquanto as demais apenas precisam ser nomeadas e utilizar os parenteses:
function ola() {}
ola()
const ola = function() {}
ola()
As funções construtoras precisam ser invocadas com a palavra reservada new
:
const p = new Pessoa()
Utilizar funções construtoras pode ser uma funcionalidade muito útil, pois, podemos criar objetos de uma maneira simplificada:
function Pessoa(nome) {
this.nome = nome
}
const p = new Pessoa('Matheus') // { nome: 'Matheus' }
Veja que estamos criando um novo objeto Pessoa
com a propriedade nome
.
Quando utilizamos a palavra reservada new
para invocar uma função, o JavaScript por baixo dos panos cria automaticamente um novo objeto para nós, esse objeto pode ser referenciado através do this
.
Quando realizamos this.nome = nome
, estamos adicionando uma nova propriedade chamada nome
para o objeto recém criado, onde o valor da propriedade será o valor informado no parâmetro da função. Seria algo similar à:
const p = {}
p.nome = 'Matheus'
E por fim, funções construtoras por padrão retornam esse objeto recém criado de maneira implícita.
Generator Functions
Por último, não menos importante, vamos falar das funções geradoras, a definição e declaração da mesma é muito semelhante as funções de expressão e declaração, uma pequena diferença está na adição de um *
na palavra reservada function
, ou seja, function*
:
function* ola(p1, p2) {}
As demais regras se aplicam para a mesma, onde os parâmetros são opcionais e separados por vírgula e o corpo da função fica dentro das chaves.
Mas, porque utilizá-las? Diferente de funções normais em JavaScript, ou seja:
function ola() {
console.log('Olá')
console.log('Matheus')
console.log('Castiglioni')
}
ola()
A execução dessa função não pode ser controlada, em outras palavras, a função será executada e processada de maneira completa, nós não temos nenhum controle do tipo:
- Execute o primeiro log
- Pause
- Faça alguma coisa
- Execute o segundo log
- Pause
- Faça alguma coisa
- etc…
O resultado da execução seria:
ola()
// 'Olá'
// 'Matheus'
// 'Castiglioni'
Porém, as funções geradoras podem ser interrompidas durante a invocação e posteriormente podemos dar continuidade em sua execução, eu sei que deve ter ficado um pouco complexo, mas, calma que iremos exemplificar a explicação.
function* ola() {
yield 'Olá'
yield 'Matheus'
yield 'Castiglioni'
}
Repare que temos uma nova palavra reservada, a yield
, essa palavra indica quais são os passos e onde a função deve ir parando sua execução, ou seja, cada yield
é um ponto de interrupção da função.
Vamos executar nossa função e atribuir seu retorno para uma variável:
const nome = ola()
console.log(nome)
O valor da variável nome
será um objeto do tipo Generator
, que nada mais é do que um Iterator
. Mas, o que isso quer dizer? Um Iterator
pode ser iterado de uma maneira diferente a array
, ele possuí uma função next
que irá passar por cada ponto de parada:
const nome = ola()
const n1 = nome.next()
console.log(n1)
A função next
vai retornar um objeto com duas propriedades:
value
: O valor informado para cadayield
.done
: Um booleano que vai indicar se oIterator
percorreu todos os pontos de interropção, dessa maneira, quando o valor fortrue
a iteração terminou.
Como nossa função possuí três pontos de parada, ou seja, três yield
, podemos chamar a função next
três vezes o obter o valor de cada yield
:
// n = next
const nome = ola()
const n1 = nome.next()
console.log(n1.value) // 'Olá'
console.log(n1.done) // false
const n2 = nome.next()
console.log(n2.value) // 'Matheus'
console.log(n2.done) // false
const n3 = nome.next()
console.log(n3.value) // 'Castiglioni'
console.log(n3.done) // false
const n4 = nome.next()
console.log(n4.value) // undefined
console.log(n4.done) // true
Veja que percorremos todos os passos da função de maneira manual, ou seja, nos estamos controlando quando a mesma deve ser executada ou não.
Na quarta chamada da função next
não havia mais pontos de parada, o value
então é undefined
e por fim o done
está marcado como true
.
Entre cada declaração do yield
podemos adicionar códigos JavaScript que serão processados normalmente:
function* inc() {
let n = 0
console.log('Estou incrementando uma vez')
n++
yield n
console.log('Estou incrementando pela segunda vez')
n++
yield n
}
Logo, sua execução pode ser feita:
const interador = inc()
const n1 = interador.next()
// 'Estou incrementando uma vez'
console.log(n1.value) // 1
console.log(n1.done) // false
const n2 = interador.next()
// 'Estou incrementando pela segunda vez'
console.log(n2.value) // 2
console.log(n2.done) // false
const n3 = interador.next()
console.log(n3.value) // undefined
console.log(n3.done) // true
Veja que os logs dentro da função foram executadas, a variável foi incrementada e cada yield
também foi chamado. Dessa maneira, temos controle total sobre quando e quais passos devem ser executados em nossa função geradora.
Também podemos atribuir as funções geradores para variáveis:
const ola = function* () {}
Saiba mais
Caso você não precise controlar o fluxo de execução e queira chamar todos os yield
de maneira automática, pode utilizar o for of
(utilizado para percorrer iteradores):
function* ola() {
yield 'Olá'
yield 'Matheus'
yield 'Castiglioni'
}
for (const n of ola()) {
console.log(n)
}
Repare que não precisamos nos preocuper em chamar o next
, verificar se tem value
e o done
não está true
, o próprio for of
faz tudo isso para a gente.
Conclusão
Nesse post vimos como podemos definir e declarar diferentes tipos de função em JavaScript, entendemos a motivação de uso para cada uma, as principais diferenças entre elas e como podemos fazer para além de declará-las, também as invocá-las.
E aí, você já conhecia os diferentes tipos de função e cada motivação para usá-las? Não deixe de comentar.
Abraços até a próxima.