Pegue uma bebida ou algo para comer, vamos falar de assunto profundo e com pouco material, mas que é uma das bases do Ecmascript, muito usado antes das especificações ES5/ES6 e que auxilia muito com problemas comum que vemos.

Esse artigo tem uma novidade em teste, as palavras destacadas em verdes tem uma tradução mais explicativa dos termos, só clicar para abrir/fechar. Com o tempo libero o código.

Acredito que você já ouviu falar de problemas como callback hellchamadas de retorno de função infernais, promises hellfunções de promessas infernais ou async/await hellchamadas assincronas infernais, não precisamos ser super especialistas em Javascript para ver que a culpa não é da tecnologia ou da linguagem como alguns gostam de falar, mas na forma que vemos o código e escrevemos, já que a adição ou mudança de novos recursos da linguagem não diminuem o problema.

Não é fácil pensar de forma assincrona, após aprender lógica de programação, isso gera problemas e acrescente não lembrar que tudo é referência no javascript.

Ao olharmos para alguns dados como, desde quanto o termo callback hell tem sido pesquisado no Google, percebemos que é algo recente dado o tempo de existencia do Javascript.

Então o que aconteceu para isso ser algo tão recorrente, especificamente de 2012, não existia callback hell antes de 2012?

jQuery, NodeJS e AngularJS

O ano de 2009 foi intenso para o Javascript, apesar dos efeitos terem sido sentidos somente por volta de 2013, jQuery se popularizou e junto houve o surgimento do NodeJS e AngularJS.

Isso fez com que o desenvolvimento de aplicações em Javascript aumentasse vertiginosamente, mas conceitos consolidados em outras linguagens como de sempre pensar no código de cima para baixo, junto com a falta de aprofundamento em mecanismos da linguagem resultou no surgimento do callback hell.

Olhando novamente para o Google Trends, especificamente as buscas por passagens de valores por referência, vemos como a busca sobre o assunto caiu quase na mesma época, mas é um assunto fundamental para entender Javascript e evitar problemas.

(Lembrando que correlação não implica em casualidade, mas é um padrão no minimo curioso de se observar.) [1][2]

O Callback Hell

Vamos começar com um detalhe simples, imagine que temos uma função e precisamos usar ela após o evento de click como no exemplo abaixo:

//(Os exemplos serão em Javascript com padrões antigos, mas vamos ter um pouco de ES6)
window.addEventListener('click', function () { 
  console.log('fui clicado'); 
});

O lidarmos com eventos quase sempre passamos o callbackchamada de retorno como uma função anônima, afinal só teria uma única execução quando ocorre o evento e isso otimiza processamento.

É nas funções anônimas onde começa o callback hell, muitos viram o surgimento de arrow functionsfunções de setas como a salvação ou resolução do problema, mas não demorou muito para vermos diversos códigos assim...

fs.readdir(source, function (err, files) {
  if (err) {
    return console.log('Error finding files: ' + err)
  } 
  
  files.forEach(function (filename, fileIndex) {
    gm(source + filename).size(function (err, values) {

      if (err) {
        return console.log('Error identifying file size: ' + err)
      }
      aspect = (values.width / values.height)

      widths.forEach(function (width, widthIndex) {
        height = Math.round(width / aspect)

        this.resize(width, height).write(dest + 'w' + width + '_' + filename, function(err) {
          if (err) console.log('Error writing file: ' + err)
        })
      }.bind(this))
    })
  })
});

...se transformando em códigos assim:

fs.readdir(source, (err, files) => {
  if (err) {
    return console.log('Error finding files: ' + err)
  } 
  
  files.forEach((filename, fileIndex) => {
    gm(source + filename).size((err, values) => {

      if (err) {
        return console.log('Error identifying file size: ' + err)
      }
      aspect = (values.width / values.height)

      widths.forEach((width, widthIndex) => {
        height = Math.round(width / aspect)

        this.resize(width, height).write(dest + 'w' + width + '_' + filename, (err) => {
          if (err) console.log('Error writing file: ' + err)
        })
      }.bind(this))
    })
  })
});
// O inferno ainda está aqui, vc só não percebe ele ;)

Já que o problema começa aqui, vamos observar o que acontece com algumas funções além das anônimas.

Window, global e ambiente léxico

Toda vez que seu código for utilizado, ou mesmo que não tenha código seja só aquele terminal ou console do navegador é criado um Lexical Environmentlit. Ambiente Léxico (uma identificação das termos, tipo var, const etc.) que vai criar automáticamente um objeto global.

Após a criação desse ambiente é criado um objeto ecmascript global, caso exista declarações de funções e de variáveis elas são adicionadas como propriedades desse objeto global. E com isso uma variavel declarada podeser acessada por outras funções ter um comportamento totalmente esquisito. Só copiar e colar o código abaixo e ver o resultado de a;

a = 10;
function modifyA() { a = 30 }
modifyA()
console.log(a); // será 30;

Mas ainda existe outro detalhe interessante, a criação do lexical environment cria referências das funções e variaveis declaradas e com isso podemos acessar a função do nosso exemplo da seguinte forma window.modifyA() ou global.modifyA() se estiver no NodeJS e também a variavel window.a ou global.a. Além disso, o objeto criado, seja window ou global, vai conter as funções nativas declaradas e que podem ser acessadas através de suas referências.

Dando um pincelada sobre escopo, apesar de modifyA acessar uma variável, tratada como global, se ela tivesse sido declarada dentro da função a variável teria outro comportamento, isso porque para cada função é criado o seu próprio escopo. Para fins de exemplo vamos sempre tratar do escopo global nesse artigo, mas falaremos bem sobre escopos em um artigo futuro, talvez dessa série mesmo.

Quando declaramos uma variável e atribuímos uma função é como atribuir essa função ao objeto global. E se você programa Javascript já por algum tempo pode ter percebido que nem sempre precisa chamar a função quando ela é uma propriedade.

Dentro do Javascript é possivel chamar algumas funções através de propriedades em objetos para serem executados depois, algo como no seguinte exemplo, onde o then e catch se torna uma propriedade da Promise instanciada.

const promise = new Promise((resolve) => resolve('oi'));
promise.then();
promise.catch();

Já que podemos fazer isso nos leva para outro ponto, uma função só é chamada quando existem os paratenses na sequência da declaração, caso isso não aconteça ele interpreta a declaração ou como uma expressão ou como uma propriedade do tipo referência.

No contexto de execução a função é uma propriedade do objeto, seja ele global ou algo declarado e podemos passar essa referência de uma função para dentro de uma outra função que executará a referência. Usando ainda o exemplo acima da promise, veja o que poderíamos fazer para tratar o resultado da promise

const promise = new Promise((resolve) => resolve('oi'));
promise.then(console.log);
promise.catch(console.error);

Com isso não precisei declarar nenhuma função anônima ou arrow functions para ter o meu callback. Internamente o próprio then vai executar meu console.log com os parametros corretos. O que nos leva na resolução de problemas dos callbacks hell de forma elegante. Para concluir o artigo vamos ver aquele exemplo usando fs?

function directoryHandler(err, files) {
  if (err) {
    return console.log('Error finding files: ' + err)
  }
  files.forEach((filename) => gm(source + filename).size(IdentifierFileSize));
}

function IdentifierFileSize(err, values) {
  if (err) {
    return console.log('Error identifying file size: ' + err)
  }
  this.aspect = (values.width / values.height)
  widths.forEach(ResizeAndWrite.bind(this))
}

function ResizeAndWrite(width) {
  const height = Math.round(width / this.aspect)
  this.resize(width, height).write(source + 'w' + '_' + filename, (err) => {
    if (err) console.log('Error writing file: ' + err)
  })
}

Para quem está acostumado com linguagens estruturadas e que a sequência importam o código acima não faz muito sentido, mas de novo as declarações de funções serão associadas ao objeto global antes da execução de código.

Mais artigos sobre o assunto virão e também como são executados essas funções.


Para se aprofundar

Os artigos abaixo foram usados como referencia e pesquisa e mudaram muito o texto e alguns outros acabou não sendo usado, mas moldou a ideia central do artigo

[1] https://stackoverflow.com/questions/7744611/pass-variables-by-reference-in-javascript

[2] http://whatsthepointy.blogspot.com/2013/11/javascript-does-not-have-pass-by.html (Tanto a pergunta 1 e esse texto, me fez ver que eu estava errado sobre uma abordagem e me fez mudar um pouco a abordagem do artigo, talvez esteja diferente da versão que alguns outros viram.)

https://stackoverflow.com/questions/13276475/javascript-top-level-this-expression

(Os três próximos links estão mais por conta de uma curiosidade que foi surgindo ao longo do artigo e houve pouca influencia, mas vale a pena já que ajudou a corrigir alguns pontos)
https://www.quora.com/How-does-a-JavaScript-engine-such-as-Rhino-V8-or-SpiderMonkey-work-What-are-some-useful-resources-for-understanding-them

https://developer.telerik.com/featured/a-guide-to-javascript-engines-for-idiots/

https://www.slideserve.com/oriana/v8-an-open-source-high-performance-javascript-engine

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Functions

https://gist.github.com/maxogden/4bed247d9852de93c94c

https://blogs.msdn.microsoft.com/ie/2006/08/28/ie-javascript-performance-recommendations-part-1/

http://ecma-international.org/ecma-262/5.1/#sec-8.7
http://ecma-international.org/ecma-262/5.1/#sec-11.1.2

http://dmitrysoshnikov.com/ecmascript/chapter-8-evaluation-strategy/