Você está na página 1de 99

Capítulo 7.

Refatorando Funções e Objetos

No capítulo anterior, iniciamos um projeto de criação de um classificador Naive Bayes


(ou NBC, abreviado).Fizemos melhorias, mas nada até agora chegou ao cerne da refato-
ração de nossas funções e objetos.

É disso que trata este capítulo.

A PROGRAMAÇÃO ORIENTADA A OBJETOS AINDA É RELEVANTE EM JAVASCRIPT?

Neste e nos próximos dois capítulos, estamos trabalhando com POO.Em alguns estilos Ja-
vaScript,OOP não é enfatizado em favor da programação funcional. Mas além da razão
prática de entender OOP em JavaScript para dar suporte a projetos legados, os recursos
de OOP do JavaScript ainda estão sendo expandidos por meio do comitê TC39 que decide
sobre recursos para JavaScript.

É razoável ter uma preferência pessoal por sua própria codificação, mas se seu objetivo
é entender o JavaScript moderno, no momento da redação deste artigo, tanto FP quanto
OOP ainda estão sendo desenvolvidos ativamente. Sem um “vencedor” claro neste mo-
mento, faz sentido aprender sobre ambos.

O Código (Aprimorado)

Caso você tenha passado o último capítuloou perdeu um passo em algum lugar,aqui está
a versão que acabamos com:

function fileName(){

  var theError = new Error("here I am");

  return theError.stack.match(/\/(\w+\.js)\:/)[1];

};

console.log(`Welcome to ${fileName()}!`);

var easy = 'easy';

var medium = 'medium';

var hard = 'hard';

imagine = ['c', 'cmaj7', 'f', 'am', 'dm', 'g', 'e7'];

somewhereOverTheRainbow = ['c', 'em', 'f', 'g', 'am'];

tooManyCooks = ['c', 'g', 'f'];

iWillFollowYouIntoTheDark = ['f', 'dm', 'bb', 'c', 'a', 'bbm'];

babyOneMoreTime = ['cm', 'g', 'bb', 'eb', 'fm', 'ab'];

creep = ['g', 'gsus4', 'b', 'bsus4', 'c', 'cmsus4', 'cm6'];

paperBag = ['bm7', 'e', 'c', 'g', 'b7', 'f', 'em', 'a', 'cmaj7',

            'em7', 'a7', 'f7', 'b'];

toxic = ['cm', 'eb', 'g', 'cdim', 'eb7', 'd7', 'db7', 'ab', 'gmaj7',

         'g7'];

bulletproof = ['d#m', 'g#', 'b', 'f#', 'g#m', 'c#'];

var songs = [];

var labels = [];

var allChords = [];


var labelCounts = [];

var labelProbabilities = [];

var chordCountsInLabels = {};

var probabilityOfChordsInLabels = {};

function train(chords, label){

  songs.push([label, chords]);

  labels.push(label);

  chords.forEach(chord => {

    if(!allChords.includes(chord)){

      allChords.push(chord);

    }

  });

  if(Object.keys(labelCounts).includes(label)){

    labelCounts[label] = labelCounts[label] + 1;

  } else {

    labelCounts[label] = 1;

  }

};

function setLabelProbabilities(){

  Object.keys(labelCounts).forEach(function(label){

    labelProbabilities[label] = labelCounts[label] / songs.length;

  });

};

function setChordCountsInLabels(){

  songs.forEach(function(song){

    if(chordCountsInLabels[song[0]] === undefined){

      chordCountsInLabels[song[0]] = {};

    }

    song[1].forEach(function(chord){

      if(chordCountsInLabels[song[0]][chord] > 0){

        chordCountsInLabels[song[0]][chord] += 1;

      } else {

        chordCountsInLabels[song[0]][chord] = 1;

      }

    });

  });

function setProbabilityOfChordsInLabels(){

  probabilityOfChordsInLabels = chordCountsInLabels;

  Object.keys(probabilityOfChordsInLabels).forEach(

function(difficulty){

    Object.keys(probabilityOfChordsInLabels[difficulty]).forEach(

function(chord){

      probabilityOfChordsInLabels[difficulty][chord] /= songs.length;

    });

  });

train(imagine, easy);

train(somewhereOverTheRainbow, easy);

train(tooManyCooks, easy);

train(iWillFollowYouIntoTheDark, medium);

train(babyOneMoreTime, medium);

train(creep, medium);

train(paperBag, hard);

train(toxic, hard);

train(bulletproof, hard);

setLabelProbabilities();

setChordCountsInLabels();

setProbabilityOfChordsInLabels();

function classify(chords){

  var smoothing = 1.01;

  console.log(labelProbabilities);

  var classified = {};

  Object.keys(labelProbabilities).forEach(function(difficulty){

    var first = labelProbabilities[difficulty] + smoothing;

    chords.forEach(function(chord){

      var probabilityOfChordInLabel =

probabilityOfChordsInLabels[difficulty][chord];

      if(probabilityOfChordInLabel){

        first = first * (probabilityOfChordInLabel + smoothing);

      }

    });

    classified[difficulty] = first;

  });

  console.log(classified);

};

classify(['d', 'g', 'e', 'dm']);

classify(['f#m7', 'a', 'dadd9', 'dmaj7', 'bm', 'bm7', 'd', 'f#m']);

Como foi o caso antes, salve isso em um arquivo chamado nb.js e você pode executá-lo
com este comando:

node nb.js

Não teremos testes para mais algumas seções. Ainda estamos mancando verificando se
nossa saída não mudou. Você deve obter o seguinte:

Bem-vindo ao nb.js!

[fácil: 0,3333333333333333,

  médio: 0,3333333333333333,

  difícil: 0,3333333333333333 ]

{fácil: 2,023094827160494,

  médio: 1,855758613168724,

  difícil: 1.855758613168724 }

[fácil: 0.3333333333333333,

  médio: 0,3333333333333333,

  difícil: 0,3333333333333333 ]

{fácil: 1,3433333333333333,

  médio: 1.5060259259259259,

  difícil: 1.6884223991769547}

Alternativas de matriz e objeto

Recipientes(especialmente arrays e objetos) eiterandoatravés deles são conceitos funda-


mentais na programação JavaScript.Nesta seção, exploraremos mais opções diferencia-
das que o JavaScript tem disponível. Aqui estão os tópicos que abordaremos:

Alternativa de matriz: conjuntos


Alternativa de matriz: objetos
Alternativa de objeto: mapas
Alternativa de matriz: campos de bits

Alternativa de matriz: conjuntos


Um conjunto é como um array, mas pode conter apenas um de um determinado valor
(por exemplo, [1, 2, 3] , mas não [1, 1, 2, 2, 3, 3] ).Embora conjuntossão ite-
ráveis, como arrays, sua interface é muito diferente. Como eles só podem conter um de
algo, podemos tentar adicionar elementos a eles sem verificar se eles já estão lá.

Então, com um conjunto em vez de um array, podemos transformar este código:

var allChords = []; //this is outside the train function

// this is inside the train function

chords.forEach(chord => {

  if(!allChords.includes(chord)){

    allChords.push(chord);

  }

});

neste código:

var allChords = new Set(); // this is outside the train function

// this is inside the train function

chords.forEach(chord => allChords.add(chord));

Ele salva quatro linhas e uma verificação condicional. Ganha-ganha. Faça essa mudança
agora.

C O N J U N TO S E M A PA S N Ã O P O S S U E M TO D A S A S F U N Ç Õ E S D E A R R AY Ú T E I S

Por exemplo, até o momento em que escrevo, nem Set nem Map tem uma map função.Isso seria
bom, mas há algumas razões teóricas que estão fora do escopo aqui (tendo a ver com as leis dos
functors , que discutiremos muito brevemente no Capítulo 11 ) para você ficar preso sem imple-
mentações nativas. Portanto, no que diz respeito às opções convenientes, você fica preso à conver-
são para uma matriz e vice-versa ou ao uso de forEach .

Alternativa de matriz: objetos

Outra alternativa possível para um array é apenas um objeto. Este é especialmente o


caso se você tiver dados em que:

Não se importe com a ordem


Quer misturar tipos na mesma estrutura
Quer rótulos significativos em vez de índices numéricos

Nosso código já tem um caso como este. Apesar de termos inicializado labelCounts e
labelProbabilities como arrays, nós os usamos como objetos o tempo todo. Para ser
mais específico, devemos alterar estas linhas:

var labelCounts = [];

var labelProbabilities = [];

a estes:

var labelCounts = {};

var labelProbabilities = {};

Salve/execute/verifique/commit, e ainda estamos bem. A alteração para


labelProbabilities altera um pouco metade da saída ( {} em vez de [] ), mas isso é
realmente mais correto. Alguém poderia argumentar que agora estamos mudando o
comportamento e indo além do escopo da refatoração ao fazer essas mudanças. Por ou-
tro lado, nossa saída é apenas para inspeção visual neste momento. Ainda parece correto
e, se alguma coisa, melhorou um pouco.

A R R AY S N Ã O S Ã O TO TA L M E N T E D I S T I N TO S D E O B J E TO S

Como conseguimos usar uma matriz em vez de um objeto? Infelizmente, o JavaScript permite que
você declare um array e atribua elementos a chaves baseadas em string, como se fosse um objeto.

Temos duas matrizes restantes, songs e labels . Se observarmos como eles são usados,
ambos têm elementos enviados a eles e songs são iterados e têm seu comprimento refe-
renciado. Ambos são matrizes justificadas, mas há dois pontos de interesse aqui. Pri-
meiro, labels é apenas “usado” no sentido de que os elementos são empurrados para
ele, mas não é realmente referenciado de outra forma. É código morto, então essas li-
nhas podem ser removidas:

var labels = []; // near the top

labels.push(label); // inside the train function

(Salvar/executar/verificar/confirmar.)
O segundo ponto a ser observado songs é que “arrays” são inseridos nele, mas esses
“arrays” têm apenas dois elementos, nenhum dos quais é do mesmo tipo. Um é um ró-
tulo (uma classificação de dificuldade como “fácil”) e o outro é uma matriz de acordes.
Isso soa como um ajuste melhor para um objeto do que uma matriz. Para ser claro,
songs continua sendo um array, mas as coisas colocadas nele devem ser objetos. Isso
envolve algumas mudanças.

Dentro da train função, o que enviaremos será diferente:

// get rid of this line

songs.push([label, chords]);

// and replace it with this

songs.push({label: label, chords: chords});

Agora estamos usando um objeto em vez de um array. Isso vai quebrar muitas coisas.
Antes de corrigirmos isso, porém, podemos fazer uma pequena alteração usando a abre-
viação da propriedade object :

songs.push({label, chords});

Felizmente, como não mudamos songs a si mesmo, as chamadas dele ( length e


forEach ) ainda funcionarão bem. Dentro da função anônima para forEach   (dentro
de setChordCountsInLabels ), no entanto, nossas referências a song agora apresen-
tam alguns problemas .

Especificamente, cada referência a song[0] ou song[1] deve ser alterada, respectiva-


mente, para song.label e song.chords . Você pode apenas pesquisar e substituir,
mas aqui está outra visão sobre como alterar o arquivo:

-    if(chordCountsInLabels[song[0]] === undefined){

-      chordCountsInLabels[song[0]] = {}

+    if(chordCountsInLabels[song.label] === undefined){

+      chordCountsInLabels[song.label] = {}

     }

-    song[1].forEach(function(chord){

-      if(chordCountsInLabels[song[0]][chord] > 0){

-        chordCountsInLabels[song[0]][chord] += 1;

+    song.chords.forEach(function(chord){

+      if(chordCountsInLabels[song.label][chord] > 0){

+        chordCountsInLabels[song.label][chord] += 1;

       } else {

-        chordCountsInLabels[song[0]][chord] = 1;
+        chordCountsInLabels[song.label][chord] = 1;

Nós não usamos isso antes, mas é assim que git diff representa as mudanças.Se você
usa o Git, verá isso com frequência. Cada linha com a - é uma linha a ser excluída e
substituída pela seguinte linha prefixada com um + . Linhas sem mais ou menos são for-
necidas apenas para contexto.

Nesse ponto, você poderá salvar, executar o código e verificar se a saída não foi alterada.

Alternativa de objeto: mapas

Agora parece que todos os nossos arrays são arrays apropriados, em vez de conjuntos ou
objetos disfarçados.Mas e esses objetos? Nós realmente queremos objetos?

Se você ainda não ouviu falar Map (do objeto, não da função), você pode estar se pergun-
tando qual seria a alternativa para um objeto. Mas depois de ler a última frase, você
pode estar adivinhando que é um mapa.

Por que você usaria um mapa sobreum objeto? (Vamos nos referir a ambos como
“contêineres”.)

Você quer saber facilmente o tamanho do recipiente.


Você não quer a bagagem hierárquica que pode vir com os objetos.
Você deseja um contêiner para elementos semelhantes entre si.
Geralmente, você deseja iterar pelo contêiner.

Na maioria das linguagens orientadas a objetos, existe um container tipo mapaacessível.


Às vezes é chamado de dicionário ou hash , responsável por chaves e valores . E isso ge-
ralmentecontrastado por um sistema de classes mais pesado (com classes, instâncias, he-
rança, etc.) que se destina a armazenar estado (atributos) e comportamento
(funções/métodos).

Em JavaScript, os objetos tradicionalmente preencheram esses dois papéis, mas o sis-


tema de classes (sim, “pseudo” de acordo com alguns), junto com os módulos, está assu-
mindo as tarefas arquiteturais maiores, enquanto os mapas destinam-se a cumprir as
“chaves e chaves mais leves”. valores” papel.

Em termos práticos, isso significa que se a maioria de suas interações com objetos con-
sistem em fazer um loop através deles, e eles tendem a armazenar valores do mesmo
tipo (ou pelo menos valores que podem ser usados ​da mesma maneira—por exemplo,
endereçados com funções semelhantes ), você provavelmente quer um mapa.

O que tudo isso significa para nós e nossos objetos dentro de nossa NBC?

Todos deveriam ser mapas.

C O N T R A P O N TO : O S M A PA S S Ã O T E R R Í V E I S . . . M A S S Ó P O R A G O R A ?

Enquanto os dados do nosso programa sugerem fortemente que devemos usar mapas em vez de
objetos, hátambém são razões pelas quais podemos querer evitá-los:

A notação .get and .set não é tão conveniente quanto a


object.property.property.etc sintaxe para lidar com estruturas profundamente
aninhadas.
A API (em comparação com a dos objetos) pode não ser familiar para você ou outros membros
de sua equipe
Suportes JSONpara “Notação de Objeto JavaScript”. Quando você obtém dados JSON (de uma
API remota, por exemplo), pode ser simplesmente mais fácil trabalhar com eles como um ob-
jeto. Converter objetos em mapas pode não valer a pena.
O suporte interno (nativo) e externo (biblioteca) não é tão conveniente para mapas quanto
para outros contêineres (por exemplo, até o momento Map não tem map função).

Por esses motivos (especialmente o primeiro), acabaremos deixando alguns objetos internos como
estão, mas ainda usaremos mapas para os contêineres externos.

O objeto mais fácil de converter em um mapa é o classified objeto dentro da


classify função, porque tem o menor número de linhas.Aqui está o diferencial:

-  var classified = {};

+  var classified = new Map();

-  classified[difficulty] = first;

+  classified.set(difficulty, first);

Quando salvamos e executamos isso, a saída parece um pouco diferente, mas os núme-
ros são todos iguais. Comprometer-se.

Em seguida, vamos ver o que seria necessário para converter labelCounts de um ob-
jeto para um mapa:

-var labelCounts = {};

+var labelCounts = new Map();

-  if(Object.keys(labelCounts).includes(label)){

-    labelCounts[label] = labelCounts[label] + 1;

+  if(Array.from(labelCounts.keys()).includes(label)){

+    labelCounts.set(label, labelCounts.get(label) + 1);

-    labelCounts[label] = 1;

+    labelCounts.set(label, 1);

-  Object.keys(labelCounts).forEach(function(label){

-    labelProbabilities[label] = labelCounts[label] / songs.length;

+  labelCounts.forEach(function(_count, label){

+    labelProbabilities[label] = labelCounts.get(label) / songs.length;

Lembre-se de nossa git diff notação, onde + é uma linha adicionada e uma linha -
excluída.

A primeira mudança obviamente usa apenas um Map construtor em vez de atribuir um


literal de objeto vazio, {} . A segunda mudança é a mais complicada. Neste, o significado
de toda a primeira linha é ver se o rótulo ainda não foi incluído.

Para obter o array de rótulos com nosso objeto antigo, usamos a Object.keys função,
que tem o labelCounts objeto como parâmetro. Para obter a mesma matriz do nosso
mapa,temos que primeiro obter um iterador com labelCounts.keys() . Ao contrário
de um array, este objeto iterador não tem uma includes função, então convertemos de
um iterador para um array por meio da Array.from função.

Outra parte um pouco confusa está na mudança 4, onde forEach percorremos o


mapa.A parte estranha é que nossa função anônima está usando dois parâmetros em vez
de um: _count e label .

O label é a chave do nosso mapa, e o valor é o _count . O sublinhado está aí para sig-
nificar isso, embora devamos incluir algo no primeiro ponto da lista de parâmetros para
rotular e acessar o segundo; o primeiro não é usado. Alguns usariam apenas um _ , mas
não há uma boa razão para não nomear a variável como algo útil. Alguém mais tarde
usando isso precisa procurar a definição da função para perceber o que o primeiro parâ-
metro significa ou usar console.log para encontrar seu valor? Além disso, se o pri-
meiro parâmetro for útil, é conveniente apenas excluir o sublinhado que precede um
nome de variável perfeitamente útil e descritivo.
POR QUE ELES FIZERAM ISSO?

A ordenação doparâmetros na forEach função de Map está para trás em que as pessoasusual-
mentedescreva hashes/dicionários como “pares chave/valor”, e aqui o valor é listado primeiro.

Poderia ser melhor se fosse (key, value) , em vez de (value, key) , mas suponho que a supo-
sição é que as pessoas estarão mais interessadas em estritamente o value , o que significa que ten-
deriam a chamá-lo com um parâmetro: (value) .

De qualquer forma, fique seguro lá fora.

As outras mudanças refletem apenas diferenças deobtendo atributos, versus , e na confi-


guração de atributos, versus .  .get(thing) [thing] .set(thing, newValue)
[thing] = newValue

Através da mesma abordagem, você também pode converter


labelProbabilities para um mapa. Faça as seguintes alterações:

- var labelProbabilities = {};

+ var labelProbabilities = new Map();

- labelProbabilities[label] = labelCounts.get(label) / songs.length;

+ labelProbabilities.set(label, labelCounts.get(label) / songs.length);

- Object.keys(labelProbabilities).forEach(function(difficulty){

-   var first = labelProbabilities[difficulty] + smoothing;


+ labelProbabilities.forEach(function(_probabilities, difficulty){

+   var first = labelProbabilities.get(difficulty) + smoothing;

Neste ponto, você ainda deve ter um código que seja executado e forneça os números
esperados.

Os outros objetos de nível superior ( chordCountsInLabels e


probabilityOfChordsInLabels ) são um pouco mais complicados de converter em
mapas. Isso ocorre principalmente porque seu estado é global e mutável. Eles também
são um pouco mais resistentes à mudança porque o último é inicialmente atribuído ao
primeiro.

Podemos aplicar a mesma abordagem de antes, embora seja mais meticuloso desta vez.
Vamos precisar das seguintes alterações:
-var chordCountsInLabels = {};

-var probabilityOfChordsInLabels = {};

+var chordCountsInLabels = new Map();

+var probabilityOfChordsInLabels = new Map();

-if(chordCountsInLabels[song.label] === undefined){

-  chordCountsInLabels[song.label] = {};

+if(chordCountsInLabels.get(song.label) === undefined){

+  chordCountsInLabels.set(song.label, {});

-if(chordCountsInLabels[song.label][chord] > 0){

-  chordCountsInLabels[song.label][chord] += 1;

+if(chordCountsInLabels.get(song.label)[chord] > 0){

+  chordCountsInLabels.get(song.label)[chord] += 1;

-chordCountsInLabels[song.label][chord] = 1;
+chordCountsInLabels.get(song.label)[chord] = 1;

- Object.keys(probabilityOfChordsInLabels).forEach(

-function(difficulty){

-  Object.keys(probabilityOfChordsInLabels[difficulty]).forEach(

-    probabilityOfChordsInLabels[difficulty][chord] /= songs.length;

+probabilityOfChordsInLabels.forEach(function(_chords, difficulty){

+ Object.keys(probabilityOfChordsInLabels.get(difficulty)).forEach(

+  probabilityOfChordsInLabels.get(difficulty)[chord] /= songs.length;

-var probabilityOfChordInLabel =

-probabilityOfChordsInLabels[difficulty][chord];

+var probabilityOfChordInLabel =

+probabilityOfChordsInLabels.get(difficulty)[chord];

Salvar/executar/verificar/commit.
VERSÕES “FRACAS” DE SET E MAP

Antes de deixarmos nossa discussão sobre conjuntos e mapas, vocêdeve notarque tam-
bém existem WeakSet e WeakMap . As principais diferenças entre eles e suas contrapar-
tes “fortes” (força normal?) são:

Eles não podem ser iterados (sem forEach função).


Eles não têm uma referência ao seu tamanho.
WeakSet não pode armazenar primitivos.
Eles seguram suas chaves “fracamente”; ou seja, as chaves estão disponíveis para co-
leta de lixo quando não possuem referências.

Basicamente, com as formas fracas, você abre mão da capacidade de saber facilmente o
que está dentro ou de aplicar funções a todo o conjunto. E o que você ganha é o controle
sobre vazamentos de memória e privacidade.

Alternativa de matriz: campos de bits

Mais um candidato para substituir arrays merece uma menção: campos de bits.Se você
tiver uma matriz que armazena booleanos, talvez queira campos de bits em alguns ca-
sos. Não há implementação nativa de campos de bits em JavaScript. No entanto, você
tem acesso a números e aritmética bit a bit, e isso é tudo que você precisa.

Imagine que você tenha as seguintes condicionais:

states = [true,

true,

true,

true,

true,

true,

false,

true]

Você também pode representá-los em binário como 0b11111101 .

Se você tivesse uma condicional que fosse válida apenas nessas condições, você poderia
fazer algo assim:

if(state[0] && state[1] && state[2] && state[3] && state[4]

&& state[5] && !state[6] && state[7]){

// something something

Como esses estados têm pouco significado por si mesmos, uma refatoração em potencial
seria mover essas condições para uma função:

if(stateIsOk()){

// something something

...

stateIsOk = function(state){

return state[0] && state[1] && state[2] && state[3] && state[4] &&

state[5] && !state[6] && state[7]

Mas se você armazenou seu estado em um campo de bits, você pode fazer isso:

if(state===0b11111101){

// something something

// or you can give this state a more specific name

if(stateIsOk()){

  // something something

...

stateIsOk = function(state){

  return state===0b11111101;

Isso tem mais potencial para otimização de desempenho do que refatoração, porque a
aritmética bit a bit é super rápida, mas é um pouco difícil de trabalhar em muitos aplica-
tivos. Se você estiver fazendo algo que é graficamente intensivo e/ou precisa ser rápido
(como um jogo), lembre-se dessa representação tipo array.

Extrair esta função é bom porque podemos descrever facilmente (através do nome da
função) o que 0b11111101 realmente significa. Falando em extrair funções, vamos fa-
zer um monte disso na próxima seção.

Testando o que temos


Para nossas mudanças até agora,nós poderíamos mancar juntosem precisarpara puxar
em uma estrutura de teste, em vez de testar manualmente com console.log . Todo
mundo tem uma tolerância diferente para quão grande e complexo um projeto deve ser
antes de desenvolver certos aspectos, incluindo testes.Para alguns, inspecionar visual-
mente a saída correta pode ter sido um incômodo suficiente para motivar os testes desde
o início. Outros podem ter sentido falta de confiança por não testar funções de baixo ní-
vel desde o início.

Por outro lado, há um caso para ficar com testes de alto nível (mesmo manuais como o
nosso) até que seu código comece a tomar um pouco mais de forma.Muitos testes que co-
brem refatorações de baixo nível podem fornecer trabalho extra sem confiança extra.

Por exemplo, se você sabe que tem código morto, deseja testá-lo antes de excluí-lo? Se
você tem uma função que sabe que deseja renomear, deseja realmente testá-la antes?
MAIS QUATRO FILOSOFIAS DE TESTE

A combinação de testes unitários e de alto nível em JavaScript (incluindo testar funções


privadas quando quiser e usar TDD quando funciona bem para você e sua equipe) é bas-
tante comum, embora este livro enfatize os testes de caracterização porque eles são ter-
rivelmente subestimados.

É uma boa abordagem para se sentir confortável e tende a combinar bem com a maioria
das equipes. No entanto, existem algumas outras filosofias de teste que valem a pena
considerar:

Testes de unidade são uma perda de tempo (apenas testes/métricas de alto nível
importam)
Eu nunca ouvi isso de um engenheiro competente que atualmente está traba-
lhando em tarefas de codificação, em vez de um gerente ou consultor focado mais
em metas de alto nível.

TATFT (Testar todo o tempo [redigido] )


Nesta abordagem, você testa tudo. Você testa suas dependências. Você testa seus
testes. Você testa tudo . Eu nunca vi pessoas adaptarem isso na prática, mas defini-
tivamente vi isso inspirar as pessoas a escrever mais testes.

Se compilar (ou digitar verifica ), funciona


Para linguagens que se preocupam com segurança de tipo (como Haskell ou a va-
riante compila para JS, PureScript), muitos erros simplesmente não acontecem,
porque os erros aparecerão em tempo de compilação. No entanto, mesmo no caso
de funções puras, idempotentes e de tipo seguro, erros de lógica ainda são possí-
veis, portanto, usar uma linguagem funcional não o deixa fora do gancho para
testes.

UUM (Usa Até Modificação)


Esta é uma visão diferenciada em testes que sugere o grau em que você testa o có-
digo deve ser proporcional ao quanto ele será usado antes de ser modificado. Se
estiver em seu editor, sendo trabalhado ativamente, será usado zero vezes antes
de ser modificado, portanto, nenhum teste é necessário. Se for para o mundo onde
milhões de pessoas irão usá-lo (seja uma biblioteca de desenvolvedores ou algo
voltado para o público em geral), então deve ter mais testes. O post original tem
apenas alguns pontos de bala e vale a pena dar uma olhada.

Neste capítulo, nossas mudanças são muito mais agressivas. Para abordar essas mudan-
ças com confiança, precisaremos de mais testes. Antes de fazermos mais alterações, va-
mos testar o que temos agora.

Caso não seja óbvio o que queremos testar, a premissa é que queremos converter nosso
fluxo de trabalho manual em automatizado. Isso significa que qualquer coisa que esteja-
mos procurando como saída deve estar dentro de um teste.

CÓDIGO QUERENDO SER MELHOR

Se você olhar para o código de uma certa maneira, poderá ver como ele quer ser melhor. Arquivos
longos querem ser mais curtos. Comentários explicando algumas linhas de código querem ser fun-
ções, nomeadas com o comentário, que encerram essas poucas linhas. Como se aplica a nós agora,
essas instruções de registro querem ser funções.

Nosso teste de configuração

Antes de começarmos a testar o comportamento real do nosso código, é útil fazer um


teste para garantir que tudo está funcionando corretamente. 

Adicione isto ao final do seu arquivo:

var wish = require('wish');

describe('the file', function() {

  it('works', function(){

wish(true);

});

});

Observe que você também pode precisar executar esses comandos na linha de comando:

npm install -g mocha

npm install wish

Agora, se você correr mocha nb.js , você deveveja um teste de aprovação, bem como
nossas informações de registro.

N Ã O S E E S Q U E Ç A D O O B S E RVA D O R

Não se esqueça de usar mocha -w nb.js para executar o watcher em um terminaljanela para
atualizar os resultados quando você fizer alterações.
Testes de caracterização para classificar

Em vez de usar console.log no final das funções, podemos retornar algo delas que po-
demos testar.Adicione isso dentro do describe bloco (a classify linha é a mesma de
antes):

it('classifies', function(){

  classify(['f#m7', 'a', 'dadd9', 'dmaj7', 'bm', 'bm7', 'd', 'f#m']);

});

Em seguida, no final da classify função, faça com que ela retorne além do log:

function classify(chords){

...

});

  console.log(classified);

  return classified; // this line is new

};

Voltando ao teste: sabemos que, por falta de cobertura, queremos escrever um teste de
caracterização. Vamos usar o modo de caracterização de desejo fornecendo um segundo
parâmetro de true , e deixar a saída de teste nos dizer qual é a saída.

Faça o "classifies" teste ficar assim:

it('classifies', function(){

  var classified = classify(['f#m7', 'a', 'dadd9',

'dmaj7', 'bm', 'bm7', 'd', 'f#m']);

  wish(classified.get('easy'), true);

  wish(classified.get('medium'), true);

  wish(classified.get('hard'), true);

});

Depois de executar mocha , isso leva a:

WishCharacterization: classified.get('easy')

evaluated to 1.3433333333333333

E então podemos simplesmente colocar esse valor em nosso teste:


// replace this

wish(classified.get('easy'), true);

// with this

wish(classified.get('easy') === 1.3433333333333333);

Execute mocha novamente e teremos:

WishCharacterization: classified.get('medium')

  evaluated to 1.5060259259259259

Perfeito. Novamente, substituímos o , true pelo valor de saída:

wish(classified.get('medium') === 1.5060259259259259);

Salve e execute mocha . Mais uma vez, obtemos um erro de caracterização:

WishCharacterization: classified.get('hard')

  evaluated to 1.6884223991769547

Agora temos as informações necessárias para o bloco de teste completo:

it('classifies', function(){

  var classified = classify(['f#m7', 'a', 'dadd9',

'dmaj7', 'bm', 'bm7', 'd', 'f#m']);

  wish(classified.get('easy') === 1.3433333333333333);

  wish(classified.get('medium') === 1.5060259259259259);

  wish(classified.get('hard') === 1.6884223991769547);

});

Seguindo esse mesmo processo novamente, podemos escrever um teste semelhante para
a outra música que estamos classificando:

it('classifies again', function(){

  var classified = classify(['d', 'g', 'e', 'dm']);

  wish(classified.get('easy') === 2.023094827160494);

  wish(classified.get('medium') === 1.855758613168724);

  wish(classified.get('hard') === 1.855758613168724);

});

Agora podemos remover três coisas:

Nossa instrução de registro ( console.log(classified); ) da classify função


As chamadas para classify isso estão fora dos testes
O "works" teste que adicionamos para garantir que nossa configuração estava
correta

Você pode estar se perguntandopor que não apenas copiamos os valores da


console.log instrução e os colocamos no teste. A razão é para que possamos ter uma
falha de teste primeiro. Se não vemos uma falha, não podemos ter certeza absoluta de
que a ação específica que tomamos é realmente o que fez o teste passar. “As coisas pare-
cem estar funcionando corretamente” não é tão confiável em uma afirmação quanto “eu
mudei a expectativa do teste para corresponder ao resultado do teste de caracterização
reprovado, e isso mudou o teste de vermelho para verde”. Isso pode parecer sutil, mas
pequenos passos são mais fáceis de desfazer.

Testando a WelcomeMessage

Agora vamos adicionar um novo teste para a mensagem de boas-vindas (dentro do


describe bloco). Desta vez, em vez de deixar o nosso logging em vigor, vamos movê-la
para um teste:

// delete this line from the file (near the top)

console.log(`Welcome to ${fileName()}!`);

// add this test inside of the describe block

it('sets welcome message', function(){

  console.log(`Welcome to ${fileName()}!`);

});

Neste momento, isso passa, pois não há nenhuma afirmação feita. Mas ainda vemos a
saída da instrução de log, então sabemos que o código ainda funciona. Vamos adicionar
a afirmação agora:

it('sets welcome message', function(){

  console.log(`Welcome to ${fileName()}!`);

wish(welcomeMessage() === 'Welcome to nb.js!') // this line is new

});

Observe que pretendemos adicionar uma função aqui. Este não é um teste de caracteri-
zação. Este é um teste de unidade para uma função que ainda não foi escrita.
Recebemos um erro:

ReferenceError: welcomeMessage não está definido

Excelente. Vamos definir essa função no topo do arquivo:

function welcomeMessage(){

  return `Welcome to ${fileName()}!`;

};

O teste agora passa. Isso significa que não precisamos mais confiar na declaração de re-
gistro de mensagens de boas-vindas em nenhum lugar, inclusive em nosso teste, para
que possamos ter apenas o seguinte:

it('sets welcome message', function(){

  wish(welcomeMessage() === 'Welcome to nb.js!')

});

Testando para labelProbabilities

Aqui, usaremos a mesma abordagem de mover a instrução de registro para um teste e,


em seguida, transformá-la em uma asserção adequada.Para começar, exclua esta linha
da classify função:

console.log(labelProbabilities);

E adicione este código dentro do describe bloco:

it('label probabilities', function(){

  console.log(labelProbabilities);

});

Podemos mudar isso para um teste de caracterização como este:

it('label probabilities', function(){ 

wish(labelProbabilities, true);

});

Quando executamos o teste, nossa saída nos diz o que precisamos fazer a seguir:
WishCharacterization: labelProbabilities

evaluated to [["easy",0.3333333333333333],

              ["medium",0.3333333333333333],

              ["hard",0.3333333333333333]]

Desde que nóssabemos que estamos lidando labelProbabilities como um conjunto,


podemos testar componentes individuais como este:

it('label probabilities', function(){

  wish(labelProbabilities.get('easy') === 0.3333333333333333);

  wish(labelProbabilities.get('medium') === 0.3333333333333333);

  wish(labelProbabilities.get('hard') === 0.3333333333333333);

});

Agora que temos testes de alto nível, podemos refatorar agressivamente e com confi-
ança. Além disso, estamos completamente livres de console.log declarações.

Extraindo Funções

Aqui vamos nós!Esta seção é sobre o que, como dissemos no capítulo anterior, é prova-
velmente a técnica de refatoração mais útil e subutilizada: extrair funções.

Fugindo do Código de Processo

Se examinarmos o código neste ponto, podemos notar que temos dados (músicas e rótu-
los) misturados com nossas funções.No geral, nosso programa parece ter estas etapas:

1. Configurar dados (músicas e rótulos).


2. Configure objetos, conjuntos e mapas.
3. Treine nosso classificador no conjunto de músicas.
4. Defina contagens e probabilidades.
5. Classifique com novas músicas (tratadas pelos testes).

No momento, as primeiras quatro etapas ainda são explicadas de maneira desajeitada


de maneira processual e não estruturada.

Queremos deixar de executar o arquivo e começar a pensar em executar funções . Isso


significa que qualquer coisa que não esteja dentro de uma função ou do código de teste é
um problema .
As seguintes funções são sempre executadas em conjunto:

setLabelProbabilities();

setChordCountsInLabels();

setProbabilityOfChordsInLabels();

Isso significa que podemos envolvê-los em outrofunction e execute isso imediatamente,


assim:

function setLabelsAndProbabilities(){

  setLabelProbabilities();

  setChordCountsInLabels();

  setProbabilityOfChordsInLabels();

};

setLabelsAndProbabilities();

Observe que também podemos executar isso como umIIFE anônimo (expressão de fun-
ção imediatamente invocada; consulte “Chamadas de função e literais de função” ),
assim:

(function(){

  setLabelProbabilities();

  setChordCountsInLabels();

  setProbabilityOfChordsInLabels();

})();

Mas ao fazer isso, perdemos algum controle. Se alguma vez decidíssemos chamar isso
mais de uma vez, teríamos que nomeá-lo ou encontrar uma maneira de executar o có-
digo que o contém a cada vez.

Por enquanto, não importa quando essa função é chamada, desde que seja chamada
apenas uma vez: depois que o classificador for treinado, mas antes de executarmos
classify .

Vamos adicionar uma trainAll função e chamar setLabelsAndProbabilities no


final dela.Não vamos chamá-lo em nenhum outro lugar.Além disso, vamos adicionar
uma chamada trainAll logo após sua declaração:

function trainAll(){

  train(imagine, easy);

  train(somewhereOverTheRainbow, easy);

  train(tooManyCooks, easy);

  train(iWillFollowYouIntoTheDark, medium);

  train(babyOneMoreTime, medium);

  train(creep, medium);

  train(paperBag, hard);

  train(toxic, hard);

  train(bulletproof, hard);

  setLabelsAndProbabilities();

};

trainAll();

function setLabelsAndProbabilities(){

  setLabelProbabilities();

  setChordCountsInLabels();

  setProbabilityOfChordsInLabels();

};

Queremos trainAll fazer partenossa interface pública (uma função que executamos,
em vez de parte de um arquivo que executamos) como qualquer outra instrução, por-
tanto, devemos movê-la para ser chamada dentro do describe bloco de nossos testes:

describe('the file', function() {

  trainAll();

Em seguida, podemos extrair uma função para definir nossas músicas e (por enquanto)
chamá-la imediatamente:

function setSongs(){

  imagine = ['c', 'cmaj7', 'f', 'am', 'dm', 'g', 'e7'];

  somewhereOverTheRainbow = ['c', 'em', 'f', 'g', 'am'];

  tooManyCooks = ['c', 'g', 'f'];

  iWillFollowYouIntoTheDark = ['f', 'dm', 'bb', 'c', 'a', 'bbm'];

  babyOneMoreTime = ['cm', 'g', 'bb', 'eb', 'fm', 'ab'];

  creep = ['g', 'gsus4', 'b', 'bsus4', 'c', 'cmsus4', 'cm6'];

  paperBag = ['bm7', 'e', 'c', 'g',

              'b7', 'f', 'em', 'a',

              'cmaj7', 'em7', 'a7', 'f7',

              'b'];

  toxic = ['cm', 'eb', 'g', 'cdim', 'eb7', 'd7', 'db7', 'ab',

'gmaj7', 'g7'];

  bulletproof = ['d#m', 'g#', 'b', 'f#', 'g#m', 'c#'];

};

setSongs();

Sabemos que queremos que issoacontecer uma vez antestraining, para que possamos
mover nossa setSongs chamada de função (a última linha do snippet anterior) para
dentro de trainAll :

function trainAll(){

  setSongs();

...

Como sempre, após cada uma dessas alterações, você deve observar o conjunto de testes
aprovado e confirmar os resultados.

Queremos fazer o mesmo com a configuração de variáveis ​de dificuldade e nossos con-
têineres (array, conjuntos e mapas) que usamos em todo o programa, mas se adotarmos
a mesma abordagem de extrair uma função, nosso conjunto de testes apresentará um
erro. Tente isto:

function setDifficulties(){

  var easy = 'easy';

  var medium = 'medium';

  var hard = 'hard';

};

setDifficulties();

Se fizermos isso, nossas variáveis ​ficarão presas no escopo dessa função e não poderão
mais ser lidas por outras funções. Mais tarde, abordaremos como restringir os escopos
dessas variáveis, mas, por enquanto,podemos pegar o caminho mais fácil e deixá-los
como variáveis ​globaisomitindo a var palavra-chave:

function setDifficulties(){

  easy = 'easy';

  medium = 'medium';

  hard = 'hard';

};

setDifficulties();

E podemos fazer o mesmo para nossas outras variáveis ​globais (movê-las para uma fun-
ção, remover a declaração de escopo e chamar a função):

function setup(){

  songs = [];

  allChords = new Set();


  labelCounts = new Map();

  labelProbabilities = new Map();

  chordCountsInLabels = new Map();

  probabilityOfChordsInLabels = new Map();

};

setup();

Em seguida, podemos mover a funçãochamadas em nossa trainAll função:

function trainAll(){

  setDifficulties();

  setup();

  setSongs();

...

Certifique-se de remover essas três chamadas ( setDifficulties , setup e


setSongs ) de outros locais no programa. Supondo que você tenha feito isso, agora tudo
está dentro de uma função ou do código de teste.

AH NÃO! VAR FOI EXCLUÍDO! TUDO ESTÁ PIOR AGORA!

E nós nem tínhamos var declarações para nossas músicas para começar! Código ruim!

Certo?

Falaremos sobre var e outras declarações de escopo mais tarde.Por enquanto, reco-
nheça que nosso programa dependia delas como variáveis ​globais antes. Não estamos
mais usando a var palavra-chave para fingir que não são globais.

Uma maneira de corrigir isso é definir o escopo e passá-los especificamente para cada
função que os exija. Feito ingenuamente, esse caminho exigiria muitas mudanças, in-
cluindo assinaturas de métodos mais complexas (mais parâmetros) e mais valores de
retorno.

Em algum momento, vamos querer anexar essas variáveis ​a algum objeto diferente do
global, mas, por enquanto, é uma vitória ter a interface bem definida pelos testes: eles
apenas executam trainAll e classify , o que significa que estamos mais preocupa-
dos com o conteúdo do arquivo em oposição ao pedido .

Excelente. Então agora tudo está dentro de uma função e nossa interface para os testes
está bem definida. Poderíamos extrair mais funções se quiséssemos ser mais específicos
em alguns lugares. Por exemplo, poderíamos mudar nossa setup função para fazer algo
assim (mas não faça isso):

function setSongsVariable(){

songs = [];

};

function setup(){

  setSongsVariable();

  allChords = new Set();


  labelCounts = new Map();

  labelProbabilities = new Map();

  chordCountsInLabels = new Map();

  probabilityOfChordsInLabels = new Map();

};

E poderíamos fazer isso com o resto das linhas setup também. Ou podemos usá-lo para
agrupar arbitrariamente algumas linhas dentro (não faça isso também):

function setSome(){

  songs = [];

  allChords = new Set();


  labelCounts = new Map();

};

function setOthers(){

  labelProbabilities = new Map();

  chordCountsInLabels = new Map();

  probabilityOfChordsInLabels = new Map();

};

function setup(){

  setSome();

  setOthers();

};

Podemos adicionar o máximo de indireção que quisermos extraindo funções e usar essa
técnica para agrupar coisas onde fizer sentido. Mas essas mudanças não ajudam nosso
código a ficar mais claro. Estamos apenas agrupando linhas arbitrárias neste momento.

O oposto de extrair funções é inlinhá -las.Quando uma função extraída não faz nada, é
sensato colocá-la embutida.Se você extraiu essas funções, insira-as agora e restaure a
setup função como era:

function setup(){

  songs = [];

  allChords = new Set();


  labelCounts = new Map();

  labelProbabilities = new Map();

  chordCountsInLabels = new Map();

  probabilityOfChordsInLabels = new Map();

};

Se você está se perguntando qual era o objetivo disso, é que extrair e inserir funções de-
vem vir tão naturalmente para você quanto introduzir e inserir variáveis.

Extraindo e nomeando funções anônimas

Além de extrair funções que ajudam a agrupar o comportamento,também é útil para no-
mear funções anônimas. Temos algumas funções anônimas em nosso programa. Para
obter exemplos, observe qualquer código que segue um forEach , ou o segundo parâ-
metro para as chamadas de função describe e no teste. it

Poderíamos extrair algumas dessas funções, mas para demonstrar os benefícios de


forma um pouco mais simples, vamos dar uma olhada em algum código jQuery pedestre
que visita uma URL quando um botão é clicado:

$('.my-button').on('click', function(){

window.location = "http://refactoringjs.com";

});

$('.other-button').on('click', function(){

  window.location = "http://refactoringjs.com";

});

Algumas pessoas identificariam corretamente essa duplicação como um problema de


manutenção que merece algum tipo de correção.Infelizmente, muitos deles pulariam
para essa solução, simplesmente extraindo uma variável, e parariam por aí:

var siteUrl = "http://refactoringjs.com";

$('.my-button').on('click', function(){

  window.location = siteUrl;

});

$('.other-button').on('click', function(){

  window.location = siteUrl;

});

Isso facilita a mudança siteUrl no futuro (supondo que seja usado em outros lugares),
mas podemos reduzir ainda mais a duplicação extraindo uma função:
var siteUrl = "http://refactoringjs.com";

function visitSite(){

window.location = siteUrl;

$('.my-button').on('click', function(){

  visitSite();

});

$('.other-button').on('click', function(){

  visitSite();

});

Agora nossa implementação será mais fácil de mudar no futuro, mas de que adianta en-
volver nossa chamada de função em uma função? Nada! Tente isso em vez disso:

var siteUrl = "http://refactoringjs.com";

function visitSite(){

  window.location = siteUrl;

$('.my-button').on('click', visitSite);

$('.other-button').on('click', visitSite);

Agora podemos manter nossos manipuladores de cliques juntos.Esta organização é


muito melhor.

Chamadas de Função e Literais de Função

Para programadoresque são novos em JavaScript ou aqueles que trabalham principal-


mente em alguma outra linguagem no backend, muitas vezes há alguma confusão sobre
a sintaxe da função que torna esse tipo de refatoração difícil para eles.

Este é um anônimofunção literal:

function(){};

Este é um literal de função nomeado:

function visitSite(){};

Este é um literal de função anônimaatribuído a uma variável:


var visitSite = function(){};

Isto é umchamada de função:

visitSite();

A única maneira de chamar uma função anônima (a primeira, quando não está atri-
buída a uma variável como no terceiro snippet) é executando o código que a contém:

(function(){})();

// or

(function(){}());

Isso é chamado de IIFE (“iffy”), ou expressão de função imediatamente invocada . E é uma


chamada de função (ou seja, invocação ).

A confusão surge quando as pessoas não percebem que uma declaração de função anô-
nima, quandosubstituído por uma função nomeada para reutilização, pode ser usado da
mesma maneiracomo uma referência de função :

$('.my-button').on('click', visitSite);

e não com uma chamada de função como esta:

$('.my-button').on('click', visitSite());

Você deve passar uma referência à visitSite função na on função, não uma chamada
de função.

Uma confusão adicional surge quando um parâmetro é usado pela função que está
sendo referenciada. Se nossa função ficou assim:

function visitSite(siteUrl){

  window.location = siteUrl;

};

algumas pessoas ficariam tentadas a escrever o manipulador de cliques assim:


$('.my-button').on('click', visitSite("http://refactoringjs.com"));

Mas isso não vai funcionar. Nesse caso, a solução mais óbvia seria usar uma função anô-
nima para envolver a chamada como antes:

$('.my-button').on('click', function(){

  visitSite("http://refactoringjs.com");

});

Outra solução, que se aplica não apenas à on função do jQuery, mas também às funções
nativas do JavaScript, como forEach e map , é passar outro argumento junto com os ou-
tros. Em muitos casos, o que você passar vai acabar sendo o parâmetro “implícito” (o
this ) da funçãoque você está chamando, mas se você estiver definindo uma função
que recebe uma função como argumento, você também pode projetar a assinatura da
função para receber argumentos que se tornarão argumentos explícitos para a função
passada.

De qualquer forma, quando você está passando funções dentro de funções, você deve ter
duas coisas em mente. Primeiro, consulte a API da função que recebe uma função como
argumento para quaisquer argumentos opcionais que possam ser passados ​e saiba o que
acontece quando eles não são. Segundo, reconheça que as chamadas de função JavaS-
cript funcionarão bem com muitos ou poucos argumentos. Você receberá um erro dentro
da função se tentar chamá-la e usar um parâmetro que não foi definido pela assinatura.
Além disso, quaisquer parâmetros não atendidospela funçãochamada será definida
como undefined . 

Simplificando a API com um objeto global

Neste ponto, temos tudo embrulhado em funções, mas ainda temos muitas variáveis ​glo-
bais flutuando (isso inclui nossas funções, bem como variáveis ​definidas sem
var ).Nesta seção, temos dois objetivos. A primeira é reduzir o escopo de todas as variá-
veis ​globais que temos. A segunda é projetar nossa API – em outras palavras, tomar deci-
sões sobre como nosso código será usado.

Para o estilo de código que estamos explorando aqui, queremos um objeto principal, que
chamaremos classifier , e ele conterá todas as funções e variáveis ​incidentais criadas
ao longo do caminho. Exploraremos sua criação por meio de vários mecanismos (função
de fábrica, função construtora e classe) e, em todos os casos, trabalharemos com ela de
maneira OOP. Isso significa que estaremos usando construções de linguagem como
this e permitindo que nosso classifier objeto mude durante a execução de nosso
programa.

Se você preferir trabalhar em direção a uma abordagem funcional, em vez de uma OOP,
parte do que se segue não seguirá o caminho que você deseja seguir. Para esse caminho,
eu recomendaria ainda ler o restante deste capítulo e depois examinar o Capítulo 11 .
Mas o tempo que gastamos “ this -ficando” para o estilo OOP aqui precisará de trabalho
extra para converter para o estilo FP.A razão é que a OOP nos levará a ter um objeto mu-
tável principal (embora possa ser razoavelmente dividido em objetos menores), en-
quanto em FP, queremos tratar nosso objeto delimitador ( classifier ) como mais um
namespace estúpido para funções.

Uma versão funcional do nosso NBC é fornecida no Capítulo 11 . Se você estiver interes-
sado em chegar a esse ponto, tente começar pelo código que temos aqui, bem como pelo
código que você tem no final deste capítulo. Suspeito que você encontrará código divi-
dido em funções (mas não tão fortemente investido em POO quanto o código no final
deste capítulo), como temos neste ponto, mais fácil de trabalhar. Por todos os meios,
tente ambas as abordagens se isso lhe interessar.

Para o resto do capítulo, vamos ignorar as funções welcomeMessage e fileName , bem


como o teste que o acompanha. Caso você esteja perdido neste ponto, ou queira cons-
truir uma versão FP a partir do que temos agora, aqui está o código:

function setDifficulties(){

  easy = 'easy';

  medium = 'medium';

  hard = 'hard';

};

function setSongs(){

  imagine = ['c', 'cmaj7', 'f', 'am', 'dm', 'g', 'e7'];

  somewhereOverTheRainbow = ['c', 'em', 'f', 'g', 'am'];

  tooManyCooks = ['c', 'g', 'f'];

  iWillFollowYouIntoTheDark = ['f', 'dm', 'bb', 'c', 'a', 'bbm'];

  babyOneMoreTime = ['cm', 'g', 'bb', 'eb', 'fm', 'ab'];

  creep = ['g', 'gsus4', 'b', 'bsus4', 'c', 'cmsus4', 'cm6'];

  paperBag = ['bm7', 'e', 'c', 'g',

              'b7', 'f', 'em', 'a',

              'cmaj7', 'em7', 'a7', 'f7',

              'b'];

  toxic = ['cm', 'eb', 'g', 'cdim', 'eb7', 'd7', 'db7', 'ab',

           'gmaj7', 'g7'];

  bulletproof = ['d#m', 'g#', 'b', 'f#', 'g#m', 'c#'];

};

function setup(){

  songs = [];

  allChords = new Set();


  labelCounts = new Map();

  labelProbabilities = new Map();

  chordCountsInLabels = new Map();

  probabilityOfChordsInLabels = new Map();

};

function train(chords, label){

  songs.push({label, chords});

  chords.forEach(chord => allChords.add(chord));

  if(Array.from(labelCounts.keys()).includes(label)){

    labelCounts.set(label, labelCounts.get(label) + 1);

  } else {

    labelCounts.set(label, 1);

  }

};

function setLabelProbabilities(){

  labelCounts.forEach(function(_count, label){

    labelProbabilities.set(label,

labelCounts.get(label) / songs.length);

  });

};

function setChordCountsInLabels(){

  songs.forEach(function(song){

    if(chordCountsInLabels.get(song.label) === undefined){

      chordCountsInLabels.set(song.label, {});

    }

    song.chords.forEach(function(chord){

      if(chordCountsInLabels.get(song.label)[chord] > 0){

        chordCountsInLabels.get(song.label)[chord] += 1;

      } else {

        chordCountsInLabels.get(song.label)[chord] = 1;

      }

    });

  });

function setProbabilityOfChordsInLabels(){

  probabilityOfChordsInLabels = chordCountsInLabels;

  probabilityOfChordsInLabels.forEach(function(_chords, difficulty){

    Object.keys(probabilityOfChordsInLabels.get(difficulty)).forEach(

function(chord){

      probabilityOfChordsInLabels.get(difficulty)[chord]

/= songs.length;

    });

  });

function trainAll(){

  setDifficulties();

  setup();

  setSongs();

  train(imagine, easy);

  train(somewhereOverTheRainbow, easy);

  train(tooManyCooks, easy);

  train(iWillFollowYouIntoTheDark, medium);

  train(babyOneMoreTime, medium);

  train(creep, medium);

  train(paperBag, hard);

  train(toxic, hard);

  train(bulletproof, hard);

  setLabelsAndProbabilities();

};

function setLabelsAndProbabilities(){

  setLabelProbabilities();

  setChordCountsInLabels();

  setProbabilityOfChordsInLabels();

};

function classify(chords){

  var smoothing = 1.01;

  var classified = new Map();


  labelProbabilities.forEach(function(_probabilities, difficulty){

    var first = labelProbabilities.get(difficulty) + smoothing;

    chords.forEach(function(chord){

      var probabilityOfChordInLabel =

probabilityOfChordsInLabels.get(difficulty)[chord];

      if(probabilityOfChordInLabel){

        first = first * (probabilityOfChordInLabel + smoothing);

      }

    });

    classified.set(difficulty, first);

  });

  return classified;

};

var wish = require('wish');

describe('the file', function() {

  trainAll();

  it('classifies', function(){

    var classified = classify(['f#m7', 'a', 'dadd9',

                               'dmaj7', 'bm', 'bm7', 'd', 'f#m']);

    wish(classified.get('easy') === 1.3433333333333333);

    wish(classified.get('medium') === 1.5060259259259259);

    wish(classified.get('hard') === 1.6884223991769547);

  });

  it('classifies again', function(){

    var classified = classify(['d', 'g', 'e', 'dm']);

    wish(classified.get('easy') === 2.023094827160494);

    wish(classified.get('medium') === 1.855758613168724);

    wish(classified.get('hard') === 1.855758613168724);

  });

  it('label probabilities', function(){

    wish(labelProbabilities.get('easy') === 0.3333333333333333);

    wish(labelProbabilities.get('medium') === 0.3333333333333333);

    wish(labelProbabilities.get('hard') === 0.3333333333333333);

  });

});

Extraindo o objeto classificador  

Para nossoglobalvariáveis ​(e funções) criaremos um objeto chamado classifier para


armazená-las, para que não sejam anexadas diretamente ao objeto global. Coloque isso
no topo do seu arquivo:

var classifier = {};

Então, dentro de trainAll , em vez de setup ser executado como uma função do ob-
jeto global, chame-o como parte disso classifier :

function trainAll(){

  classifier.setup(); // this line is new

  setDifficulties();

  setup(); // get rid of this one

...

Agora precisamos mover nossa setup função para nosso classifier objeto,no topo do
arquivo. Você também pode excluir a setup função antiga:

var classifier = {

  setup: function(){

    this.songs = [];

    this.allChords = new Set();

    this.labelCounts = new Map();

    this.labelProbabilities = new Map();

    this.chordCountsInLabels = new Map();

    this.probabilityOfChordsInLabels = new Map();

  };

};

Queremos que essas variáveis ​façam parte de classifier , então precisamos adicionar
um this. na frente de cada uma delas.Observe que setup é uma propriedade com o
rótulo ( setup ) seguido por um literal de função que contém o corpo da função.

Seria ótimo se os testes passassem neste ponto, mas eles não passam. Felizmente, a exe-
cução dos testes relatará um erro útil:

ReferenceError: as músicas não estão definidas

E aqui temos algumas mudanças muito chatas e repetitivas, mas relativamente fáceis de
fazer. Precisamos adicionar a classifier. na frente de cada referência às seguintes
variáveis ​(que não começam com this. ): songs , allChords , labelCounts ,
labelProbabilities , chordCountsInLabels e probabilityOfChordsInLabels .
Faça essas alterações agora.

Os testes devem passar novamente. Salve e confirme essas alterações.

Inlining a função de configuração

Agora que olhamos para nossa setup função anexada a classifier , no entanto, po-
demos notar que ela não está fazendo muito trabalho.Podemos atribuir essas variáveis ​
diretamente ao objeto sem usar uma função de encapsulamento:

var classifier = {

  songs: [],

  allChords: new Set(),

  labelCounts: new Map(),

  labelProbabilities: new Map(),

  chordCountsInLabels: new Map(),

  probabilityOfChordsInLabels: new Map()

};

Observe que o encapsulamento da função desapareceu, mas a sintaxe interna também


foi ajustada. Por exemplo, em vez disso:

this.songs = [];

agora temos isso:

songs: [],

A vírgula versus ponto e vírgula é fácil de perder. E também podemos excluir a chamada
para setup in trainAll :

function trainAll(){

  classifier.setup(); // delete this line

...

Isso é ótimo, não apenas porque temos uma linha a menos, mas também porque nosso
código de treinamento realmente não tem nada a ver com a configuração geral do nosso
classifier .

Salve/teste/commit para ter certeza de que tudo ainda parece bom.

Extraindo o objeto songList

Em seguida, vamos considerar como estamos adicionando músicas para serem treina-
das. setSongs define variáveis ​globais para cada nome de música e, em seguida,
cadadesses é referenciado na trainAll função. Esse acoplamento é propenso a erros e
não é realmente escalável. Claramente, se alterarmos nossa lista de músicas, teremos
que fazê-lo em dois lugares. Mas pior do que isso, não estamos preparados para um caso
provável em que teríamos um banco de dados de músicas para treinar, em vez de um
punhado que codificamos em um arquivo .

Vamos fazer um songList objeto que contenha as músicas em um array e tenha uma
função para adicionar músicas a ele. Isso pode ir no topo do seu arquivo:

var songList = {

  songs: [],

  addSong: function(name, chords, difficulty){

    this.songs.push({name: name,

                    chords: chords,

                    difficulty: difficulty});

  }

};

Agora vamos mudar nossa setSongs função para fazer uso deste songList objeto:

function setSongs(){

  songList.addSong('imagine',

['c', 'cmaj7', 'f', 'am', 'dm', 'g', 'e7'], easy)

  songList.addSong('somewhereOverTheRainbow',

['c', 'em', 'f', 'g', 'am'], easy)

  songList.addSong('tooManyCooks', ['c', 'g', 'f'], easy)

  songList.addSong('iWillFollowYouIntoTheDark',

['f', 'dm', 'bb', 'c', 'a', 'bbm'], medium);

  songList.addSong('babyOneMoreTime',

['cm', 'g', 'bb', 'eb', 'fm', 'ab'], medium);

  songList.addSong('creep',

['g', 'gsus4', 'b', 'bsus4', 'c', 'cmsus4', 'cm6'], medium);

  songList.addSong('paperBag',

['bm7', 'e', 'c', 'g', 'b7', 'f', 'em',

'a', 'cmaj7', 'em7', 'a7', 'f7',

'b'], hard);

  songList.addSong('toxic',

['cm', 'eb', 'g', 'cdim', 'eb7',

'd7', 'db7', 'ab', 'gmaj7', 'g7'], hard);

  songList.addSong('bulletproof',

['d#m', 'g#', 'b', 'f#', 'g#m', 'c#'], hard);

};

Se executarmos os testes agora, obteremos erros porque nossas variáveis ​nomeadas pe-
los nomes das músicas não estão mais disponíveis.Precisamos alterar nossa
trainAll função para adicionar as músicas do songList :

function trainAll(){

  setDifficulties();

  setSongs();

  songList.songs.forEach(function(song){

    train(song.chords, song.difficulty);

  });

  setLabelsAndProbabilities();

};

E agora nossos testes voltaram a funcionar.


Lidando com as Variáveis ​Globais Restantes

Ainda ficamos com aquelestrês variáveis ​globais irritantes para representar a


dificuldade.

Como primeiro esforço, uma vez que eles são apenas referenciados dentro de
setSongs , podemos simplesmente inline as variáveis ​lá:

function setSongs(){

  var easy = 'easy';

  var medium = 'medium';

  var hard = 'hard';

...

E agora podemos excluir a função setDifficulties e nossa chamada para ela dentro
de trainAll . Os testes devem estar passando neste momento.

Depois disso, pode parecer um desperdício complicar nossa setSongs função com essas
variáveis. E se nós apenas usássemos um array para difficulties , e deixássemos
songList lidar com as strings?

var songList = {

  difficulties: ['easy', 'medium', 'hard'],

  songs: [],

  addSong: function(name, chords, difficulty){

    this.songs.push({name: name,

                    chords: chords,

                    difficulty: this.difficulties[difficulty]})

  }

};

Aqui, criamos um difficulties atributo songList como um array com os rótulos de-
sejados. Então, quando adicionamos uma música, esperamos que o índice do array apa-
reça e atribua conforme necessário com this.difficulties[difficulty] .

Isso significa que podemos nos livrar do ruído da descrição do rótulo setSongs e usar
números em vez das variáveis ​que estávamos usando antes. Observe os números como o
terceiro parâmetro das addSong chamadas de função:

function setSongs(){

  songList.addSong('imagine',

['c', 'cmaj7', 'f', 'am', 'dm', 'g', 'e7'], 0);

  songList.addSong('somewhereOverTheRainbow',

['c', 'em', 'f', 'g', 'am'], 0);

  songList.addSong('tooManyCooks', ['c', 'g', 'f'], 0);

  songList.addSong('iWillFollowYouIntoTheDark',

['f', 'dm', 'bb', 'c', 'a', 'bbm'], 1);

  songList.addSong('babyOneMoreTime',

['cm', 'g', 'bb', 'eb', 'fm', 'ab'], 1);

  songList.addSong('creep',

['g', 'gsus4', 'b', 'bsus4', 'c', 'cmsus4', 'cm6'], 1);

  songList.addSong('paperBag',

['bm7', 'e', 'c', 'g', 'b7', 'f', 'em',

 'a', 'cmaj7', 'em7', 'a7', 'f7',

 'b'], 2);

  songList.addSong('toxic',

['cm', 'eb', 'g', 'cdim', 'eb7', 'd7', 'db7', 'ab', 'gmaj7', 'g7'], 2);

  songList.addSong('bulletproof',

['d#m', 'g#', 'b', 'f#', 'g#m', 'c#'], 2);

};

Salvar/testar/comprometer.

Tornando os dados independentes do programa

Vamos pensar sobre essa trainAll função por um minuto.O que a configuração de mú-
sicas tem a ver com o treinamento do nosso classificador? Absolutamente nada, real-
mente.E se movermos a chamada para setSongs em nosso teste?

describe('the file', function() {

  setSongs(); // moved and deleted from inside of trainAll

  trainAll();

E enquanto estamos nisso, definir as músicas tem alguma coisa a ver com a estrutura do
nosso programa? Não. São apenas dados que estamos configurando e usando. Isso signi-
fica que faz parte da execução de uma possibilidade do nosso programa, não do pro-
grama em si. Isso implica que a função em setSongs si pertence aos testes. Vamos em-
butir o corpo da função nos testes e remover completamente a função do programa:

describe('the file', function() {

  songList.addSong('imagine',

['c', 'cmaj7', 'f', 'am', 'dm', 'g', 'e7'], 0);

  songList.addSong('somewhereOverTheRainbow',

['c', 'em', 'f', 'g', 'am'], 0)

  songList.addSong('tooManyCooks', ['c', 'g', 'f'], 0);

...

  songList.addSong('bulletproof',

['d#m', 'g#', 'b', 'f#', 'g#m', 'c#'], 2);

  trainAll();

...

Agora estamos livres para executar o programa independentemente dos dados forneci-
dos. Este é um grande passo e abre nosso programa para mais testes e colocá-lo em pro-
dução como um módulo.

Declarações de escopo: var, let e const

Passando para outro escopopreocupações, vamos falar sobredeclarações de escopo:


var , let , e const . var existe há mais tempo, então é mais provável que você o en-
contre em bases de código mais antigas ou código escrito por pessoas que estão presas
em uma mentalidade mais antiga.Embora var seja melhor do que nada (uma declara-
ção de variável, e necessariamente uma atribuição, sem qualquer declaração de escopo
criará uma variável global) let e const criará escopos mais restritos (escopo de bloco
em vez de apenas escopo funcional). A diferença entre let e const é que const não
permitirá que uma variável seja reatribuída.

Temos nove instâncias de var em nosso programa agora. Devemos mudar algum deles?
Em caso afirmativo, eles devem mudar para let ou const ?

C O N S T N Ã O S I G N I F I C A I M U TA B I L I D A D E !

Se você está pensando que const fornece um caminho fácil para a imutabilidade, lamento infor-
mar algumas notícias tristes.Isso impede que uma variável seja reatribuída (ou seja, por meio do
= operador), mas o conteúdo da variável ainda pode ser alterado. Os índices de matriz podem ser
atualizados. O mesmo vale para atributos de objetos e membros de conjuntos e mapas.

Object.freeze para o resgate!?Quase. Se você congelar um objeto com mais de um nível de pro-
fundidade, ainda poderá atualizar essas propriedades internas. Além disso, mesmo se você conge-
lar um objeto, se ele foi declarado com var ou let , você ainda pode reatribuí-lo.

Para garantir a imutabilidade, você precisará ir além e pode achar mais fácil usar uma solução em
pacote como Immutable.js ou mori, que fornece versões imutáveis ​de matrizes, mapas, conjuntos e
assim por diante. Mesmo se você não estiver usando nada que o imponha, é melhor tentar criar
novas variáveis ​em vez de reaproveitá-las.
Em geral, é preferível usar const . Quanto menos suas variáveis ​forem reatribuídas,
melhor.

Uma vez que temos testes em vigor, podemos fazer a suposição ousada de que todas as
nossas var declarações deveriam ser const em vez disso. Pesquise e substitua-os e exe-
cute os testes. Você receberá um erro:

TypeError: Atribuição à variável constante.

A descrição do erro “variável constante” vale a pena notar como um estranho paradoxo,
mas vamos seguir em frente. Sem surpresa, nossa suposição ousada está errada, mas
apenas em um caso. Nossa first variável dentro da classify função requer reatri-
buição por enquanto, então a linha a seguir deve ser modificada para usar let :

const first = classifier.labelProbabilities.get(difficulty) +

smoothing;

Deveria ser:

let first = classifier.labelProbabilities.get(difficulty) +

smoothing;

Todas as outras const declarações estão corretas, e estamos livres para seguir em
frente.

Trazendo classificar para o classificador

Com algumas alterações, a variável first não precisa ser reatribuída, no entanto. Inici-
almente, destina-se a refletir a probabilidade de uma dificuldade aparecer em relação a
outras dificuldades.Mais tarde, é multiplicado pela probabilidade de um acorde existir
em uma música de determinada dificuldade.

E se, em vez de reatribuir essa variável, introduzíssemos um likelihoods array que


capturasse todos os valores que precisamos para multiplicar e depois os
multiplicássemos?

function classify(chords){

  const smoothing = 1.01;

  const classified = new Map();

  classifier.labelProbabilities.forEach(

function(_probabilities, difficulty){

    const likelihoods = [classifier.labelProbabilities.get(difficulty)

+ smoothing];

    chords.forEach(function(chord){

      const probabilityOfChordInLabel =

classifier.probabilityOfChordsInLabels.get(difficulty)[chord]

      if(probabilityOfChordInLabel){

        likelihoods.push(probabilityOfChordInLabel + smoothing)

      }

    })

    const totalLikelihood = likelihoods.reduce(function(total, index) {

      return total * index;

    });

    classified.set(difficulty, totalLikelihood);

  });

  return classified;

};

Mencionamos, mas não nos aprofundamos, a reduce função. Ele nos permite percorrer
um array e aplicar alguma função a ele, enquanto trabalhamos com um valor retornado,
junto com cada elemento.

Mas espere! Já temos um array pelo qual estamos percorrendo ( chords ). Por que cria-
ríamos um novo para percorrer? Nós realmente precisamos fazer um loop duas vezes
(uma vez para acumular e uma vez para multiplicar)? Vamos tentar usar reduce isso
em forEach vez disso:

function classify(chords){

  const smoothing = 1.01;

  const classified = new Map();

  classifier.labelProbabilities.forEach(

function(_probabilities, difficulty){

// reduce starts

    const totalLikelihood = chords.reduce(function(total, chord){

      const probabilityOfChordInLabel =

classifier.probabilityOfChordsInLabels.get(difficulty)[chord]

      if(probabilityOfChordInLabel){

        return total * (probabilityOfChordInLabel + smoothing)

      }else{

        return total;

      }

    }, classifier.labelProbabilities.get(difficulty) + smoothing)

// reduce ends

    classified.set(difficulty, totalLikelihood);

  });

  return classified;

};

Isso é um pouco mais complexo, por dois motivos. Primeiro, nossa probabilidade inicial
está agora no final, logo acima desta linha: //reduce ends . Isso fornece o “valor ini-
cial” ao reduce total. Em segundo lugar, o else ramo da nossa if declaração está de
volta. A razão é que em cada passo da reduce função, se nada for retornado (o que seria
o caso se a if condição fosse false e não tivéssemos else ramificação), então
undefined seria retornado. Retornar o total tem o mesmo efeito que apenas passar
para o próximo elemento.

Se isso é confuso, pense dessa maneira. Digamos que você tenha uma reduce função
como esta:

[2, 3, 4].reduce(function(result, element){ return result }, 10)

Esta função simplesmente retornaria 10 . Se nenhum valor for fornecido como segundo
parâmetro (se o , 10 não estiver lá), ele retornará o primeiro elemento: 2 . Isso ocorre
porque result é realimentado na função para cada elemento. Compare isso com outro
uso simples de reduce , que soma os valores de uma matriz:

[2, 3, 4].reduce(function(result, element){return result + element })

Isso vai voltar 9 . Se tivesse um elemento inicial de 10 , adicionaria isso também, retor-
nando 19 .

De volta ao nosso exemplo, um padrão que é especialmente comum em JavaScript é per-


mitir que condições não atendidas retornem algo fora de um arquivo else . Por
exemplo:

if(probabilityOfChordInLabel){

return total * (probabilityOfChordInLabel + smoothing);

return total;

Se a true ramificação da if instrução não for executada, o código continuará além


dela. Mas, pessoalmente, sinto que esse estilo convida a tipos de retorno incompatíveis e
muitas vezes privilegia visualmente as possibilidades menos prováveis ​no topo de uma
função.  Além disso, ele sinaliza menos fortemente dois caminhos de código do que um
if / explícito else , que (mesmo que levemente) disfarça a complexidade do código.
Para quem está interessado em refatorar e assim eliminar a complexidade, pode masca-
rar lugares que podem ser bons candidatos à mudança.

De qualquer forma, você verá frequentemente esse estilo usado paratratamento de erros
assim:

function callback(error, response){

if(error){

return new Error(error);

// do something with the response

Agora vamos escolher a classify função um pouco mais. Em primeiro lugar, não deve-
mos ter problemas em vinculá-lo mais de perto ao classifier objeto. Se há uma coisa
óbvia que um classificador faz, é classificar.

Para isso, vamos mover a classify função para o classifier :

const classifier = {

  songs: [],

  allChords: new Set(),

  labelCounts: new Map(),

  labelProbabilities: new Map(),

  chordCountsInLabels: new Map(),

  probabilityOfChordsInLabels: new Map(),

  classify: function(chords){

    const smoothing = 1.01;

    const classified = new Map();

    classifier.labelProbabilities.forEach(

function(_probabilities, difficulty){

      const totalLikelihood = chords.reduce(function(total, chord){

        const probabilityOfChordInLabel =

classifier.probabilityOfChordsInLabels.get(difficulty)[chord]

        if(probabilityOfChordInLabel){

          return total * (probabilityOfChordInLabel + smoothing);

        }else{

          return total;

        }

      }, classifier.labelProbabilities.get(difficulty) + smoothing);

      classified.set(difficulty, totalLikelihood);

    });

    return classified;

  }

};

Nossos testes serão interrompidos neste ponto, porque precisamos substituir instâncias
de classify com classifier.classify nos testes. Altere estas linhas:

const classified = classify(['f#m7', 'a', 'dadd9', 'dmaj7',

'bm', 'bm7', 'd', 'f#m']);

...

const classified = classify(['d', 'g', 'e', 'dm']);

a estes:

const classified = classifier.classify(['f#m7', 'a', 'dadd9',

'dmaj7', 'bm', 'bm7',

'd', 'f#m']);

...

const classified = classifier.classify(['d', 'g', 'e', 'dm']);

Agora que nossos testes estão funcionando novamente (save/test/commit), ainda temos
algum trabalho a fazer com esta função. Ele se refere desajeitadamente a si mesmo na
terceira pessoa, em certo sentido. Ou seja, em vez de usar this , ele diz seu próprio
nome: classifier .Há três instâncias disso. Vamos alterá-los, tornando a função o
seguinte:

classify: function(chords){

  const smoothing = 1.01;

  const classified = new Map();

  this.labelProbabilities.forEach(

function(_probabilities, difficulty){

    const totalLikelihood = chords.reduce(function(total, chord){

      const probabilityOfChordInLabel =

this.probabilityOfChordsInLabels.get(difficulty)[chord]

      if(probabilityOfChordInLabel){

        return total * (probabilityOfChordInLabel + smoothing);

      }else{

        return total;

      }

    }, this.labelProbabilities.get(difficulty) + smoothing);

    classified.set(difficulty, totalLikelihood);

  });

  return classified;

A primeira instância de this ficará bem, mas a segunda e a terceira causarão


problemas:

const probabilityOfChordInLabel =

this.probabilityOfChordsInLabels.get(difficulty)[chord]

// and

}, this.labelProbabilities.get(difficulty) + smoothing)

Isso ocorre porque nosso this (parâmetro implícito) é definido no contexto da função
que contém essas instruções. A correção mais comum e oafish é esta:

classify: function(chords){

  const smoothing = 1.01;

  const classified = new Map();

  const self = this;

  this.labelProbabilities.forEach(

function(_probabilities, difficulty){

    const totalLikelihood = chords.reduce(function(total, chord){

      const probabilityOfChordInLabel =

self.probabilityOfChordsInLabels.get(difficulty)[chord]

      if(probabilityOfChordInLabel){

        return total * (probabilityOfChordInLabel + smoothing);

      }else{

        return total;

      }

    }, self.labelProbabilities.get(difficulty) + smoothing);

    classified.set(difficulty, totalLikelihood);

  });

  return classified;

Definindouma self variável (no Capítulo 5 , nósusado that em vez disso), você pode
garantir que tenha acesso ao objeto desejado, mesmo dentro das funções internas. No
Capítulo 5 , aprendemossobre usar call , apply , ou bind para realizar a mesma coisa
com um pouco mais de elegância. Nesse caso, como não estamos chamando essas fun-
ções anônimas diretamente, apenas declarando que são chamadas por forEach and
reduce , não podemos usar call ou apply para definir o argumento implícito, this .
Para este caso, devemos usar bind ou encontrar outra maneira.
Quanto a forEach , podemos confiarna capacidade dessa função de aceitar a
thisArg como parâmetro. Isso nos permite definir o que queremos this que seja na
função anônima:

this.labelProbabilities.forEach(

function(_probabilities, difficulty){

    const totalLikelihood = chords.reduce(function(total, chord){

      const probabilityOfChordInLabel =

self.probabilityOfChordsInLabels.get(difficulty)[chord];

      if(probabilityOfChordInLabel){

        return total * (probabilityOfChordInLabel + smoothing);

      }else{

        return total;

      }

    }, this.labelProbabilities.get(difficulty) + smoothing);

    classified.set(difficulty, totalLikelihood);

}, this);

Agora que adicionamos this como um segundo parâmetro forEach (na última linha),
estamos livres para usar this em vez self da terceira da última linha. Mas a chamada
restante para self ainda não pode ser alterada para this , porque seu valor é encapsu-
lado pela função anônima dentro de reduce .

Pode-se razoavelmente esperar que a assinatura da função para reduce o retorno de


chamada de 's também permitiria que thisArg a fosse aceito como um parâmetro opci-
onal. Infelizmente, este não é o caso, então neste estilo, o this interior dessa função
deve ser definido com bind :

  const totalLikelihood = chords.reduce(function(total, chord){

...

  }.bind(this), this.labelProbabilities.get(difficulty) + smoothing);

Agora nossa função pode estar livre da variável classify estranha : self

classify: function(chords){

  const smoothing = 1.01;

  const classified = new Map();

  this.labelProbabilities.forEach(

function(_probabilities, difficulty){

    const totalLikelihood = chords.reduce(function(total, chord){

      const probabilityOfChordInLabel =

this.probabilityOfChordsInLabels.get(difficulty)[chord];

      if(probabilityOfChordInLabel){

        return total * (probabilityOfChordInLabel + smoothing);

      }else{

        return total;

      }

    }.bind(this),

this.labelProbabilities.get(difficulty) + smoothing);

    classified.set(difficulty, totalLikelihood);

  }, this);

  return classified;

Mas antes de deixarmos este exemplo, há outra ferramenta que podemos usar para lidar
com a passagem this : funções de seta.Através deles, nossa função pode ser simplifi-
cada assim:

classify: function(chords){

  const smoothing = 1.01;

  const classified = new Map();

  this.labelProbabilities.forEach((_probabilities, difficulty) => {

    const totalLikelihood = chords.reduce((total, chord) => {

      const probabilityOfChordInLabel =

this.probabilityOfChordsInLabels.get(difficulty)[chord];

      if(probabilityOfChordInLabel){

        return total * (probabilityOfChordInLabel + smoothing);

      }else{

        return total;

      }

    }, this.labelProbabilities.get(difficulty) + smoothing);

    classified.set(difficulty, totalLikelihood);

  });

  return classified;

Agora podemos nos livrar de nosso this como segundo parâmetro para forEach e
nosso bind(this) in reduce . As funções de seta estão nas seguintes linhas:

this.labelProbabilities.forEach((_probabilities, difficulty) => {

...

const totalLikelihood = chords.reduce((total, chord) => {

As partes com => são o que as tornam funções de seta . A sintaxe pode parecer um
pouco estranha (e tem muita variação), mas o que é ótimo é a this passagem da função
externa.
Para simplificar um pouco mais esta função, smoothing pode ser retiradoda função e
adicionado como um novo atributo do classifier :

smoothing: 1.01,

classify: function(chords){

  const classified = new Map();

  this.labelProbabilities.forEach((_probabilities, difficulty) => {

    const totalLikelihood = chords.reduce((total, chord) => {

      const probabilityOfChordInLabel =

this.probabilityOfChordsInLabels.get(difficulty)[chord];

      if(probabilityOfChordInLabel){

        return total * (probabilityOfChordInLabel + this.smoothing);

      }else{

        return total;

      }

    }, this.labelProbabilities.get(difficulty) + this.smoothing);

    classified.set(difficulty, totalLikelihood);

  });

  return classified;

Por um lado, isso é bom porque smoothing sai da nossa função. No entanto, aumenta o
escopo de onde está disponível e precisa ser adicionado this. para precedê-lo dentro
da função. Extrair isso const para um atributo também o torna atribuível novamente.
Infelizmente, obter algo como um const dentro de nosso classifier literal de objeto
dá um pouco de trabalho.Existe uma defineProperty função que podemos usar para
definir atributos que não são apenas não graváveis, mas até imutáveis ​por padrão. Não
entraremos em detalhes aqui, mas defineProperty é uma boa ferramenta para anali-
sar se você quiser ter muito controle sobre como as propriedades são usadas.

Normalmente, defineProperty seria chamado após a criação do objeto, o que adicio-


naria complexidade à nossa execução em algum ponto e moveria a criação do atributo
para algum outro local físico no arquivo. Por outro lado, poderíamos chamar isso dentro
da classify função assim:

classify: function(chords){

  Object.defineProperty(this, 'smoothing', {value: 1.01});

Mas isso devolve a complexidade à nossa função, que é o que estávamos tentando evitar,
tornando-a um objeto para começar. Mais tarde, discutiremos alternativas para criar ob-
jetos com literais de objeto, o que pode nos dar mais controle sobre nossas propriedades
à medida que os objetos são criados. Mas, por enquanto, vamos reverter essa mudança e
permitir smoothing que seja um atributo como era.

Outra mudança que podemos fazer na classify função é inline a variável classificada
usando a map função em vez de forEach .Sempre que você estiver configurando um
contêiner (geralmente um array, mas no nosso caso um Map objeto), usando um loop de
algum tipo para alterar essa variável e, em seguida, retornando a variável, você pode ser
mais bem servido usando uma map função em vez de um ciclo:

classify: function(chords){

  const classified = new Map();

  Array.from(this.labelProbabilities.entries()).map(

(labelWithProbability) => {

    const totalLikelihood = chords.reduce((total, chord) => {

      const probabilityOfChordInLabel =

this.probabilityOfChordsInLabels.get(labelWithProbability[0])[chord];

      if(probabilityOfChordInLabel){

        return total * (probabilityOfChordInLabel + this.smoothing);

      }else{

        return total;

      }

    }, this.labelProbabilities.get(labelWithProbability[0]) +

this.smoothing);

    classified.set(labelWithProbability[0], totalLikelihood);

  });

  return classified;

Infelizmente, o Map objeto não tem uma map função (sim, isso também me incomoda.)
Portanto, nosso primeiro passo envolve extrair um array das entradas no Map para que
possamos usá map -lo. A segunda mudança é que acabamos com um objeto um pouco
menos conveniente ( labelWithProbability em vez de difficulty ), que exige que
peguemos o primeiro índice de [0] . Em outras palavras, difficulty é substituído por
labelWithProbability[0] . Mas podemos inicializar difficulty como um novo
const :

classify: function(chords){

  const classified = new Map();

  Array.from(this.labelProbabilities.entries()).map(

(labelWithProbability) => {

    const difficulty = labelWithProbability[0];

    const totalLikelihood = chords.reduce((total, chord) => {

      const probabilityOfChordInLabel =

this.probabilityOfChordsInLabels.get(difficulty)[chord];

      if(probabilityOfChordInLabel){

        return total * (probabilityOfChordInLabel + this.smoothing);

      }else{

        return total;

      }

    }, this.labelProbabilities.get(difficulty) + this.smoothing);

    classified.set(difficulty, totalLikelihood);

  });

  return classified;

Agora podemos quase nos livrar de nossa classified variável. Em vez de inicializá-lo,
atualizá-lo no loop e retorná-lo depois, podemos map retornar um array multidimensio-
nal para definir o Map :

classify: function(chords){

  const classified = new Map(Array.from(

    this.labelProbabilities.entries()).map((labelWithProbability) => {

    const difficulty = labelWithProbability[0];

    const totalLikelihood = chords.reduce((total, chord) => {

      const probabilityOfChordInLabel =

this.probabilityOfChordsInLabels.get(difficulty)[chord];

      if(probabilityOfChordInLabel){

        return total * (probabilityOfChordInLabel + this.smoothing);

      }else{

        return total;

      }

    }, this.labelProbabilities.get(difficulty) + this.smoothing);

    return [difficulty, totalLikelihood];

  }));

  return classified;

A segunda linha agorainclui atribuir diretamente um new Map objeto a classified .


Além disso, nosso return , quatro linhas a partir da parte inferior, agora retorna um
array com difficulty e totalLikelihood . A última (muito pequena) alteração a ser
observada é que três linhas da parte inferior desta amostra, agora temos um parêntese
de fechamento extra na linha:

})); // just above "return classified;"

E agora estamos prontos para nos livrarmos da classified variável. Simplesmente ex-
clua a linha com a última return instrução e, em vez disso, retorne o resultado da cha-
mada para o new Map construtor :

classify: function(chords){

  return new Map(Array.from(

this.labelProbabilities.entries()).map((labelWithProbability) => {

...

  }));

Agora temos uma variável desnecessária semelhante em totalLikelihood :

const totalLikelihood = chords.reduce((total, chord) => {

const probabilityOfChordInLabel =

this.probabilityOfChordsInLabels.get(difficulty)[chord];

  if(probabilityOfChordInLabel){

    return total * (probabilityOfChordInLabel + this.smoothing);

  }else{

    return total;

  }

}, this.labelProbabilities.get(difficulty) + this.smoothing);

return [difficulty, totalLikelihood];

Em vez de atribuir uma variável ao resultado da reduce chamada e retorná-la no array,


podemos inline isso e retornar o array diretamente:

classify: function(chords){

  return new Map(Array.from(

    this.labelProbabilities.entries()).map((labelWithProbability) => {

    const difficulty = labelWithProbability[0];

    return [difficulty, chords.reduce((total, chord) => {

      const probabilityOfChordInLabel =

this.probabilityOfChordsInLabels.get(difficulty)[chord];

      if(probabilityOfChordInLabel){

        return total * (probabilityOfChordInLabel + this.smoothing);

      }else{

        return total;

      }

    }, this.labelProbabilities.get(difficulty) + this.smoothing)];

  }));

Isso pode parecer confuso, mas ainda estamos retornando um array de dois elementos.
O primeiro elemento é difficulty e o segundo é o resultado da reduce chamada.

Se quisermos simplificar ainda mais, podemos extrair uma função para eliminar nossa
atribuição e nossa condicional dentro deste código:

const probabilityOfChordInLabel =

this.probabilityOfChordsInLabels.get(difficulty)[chord]

if(probabilityOfChordInLabel){

  return total * (probabilityOfChordInLabel + this.smoothing)

}else{

  return total;

Temos que excluir essa atribuição e substituir as outras duas referências (uma no teste
da condicional e outra na if ramificação da condicional) pelo lado direito da atribuição.
Isso deve nos deixar com isso:

if(this.probabilityOfChordsInLabels.get(difficulty)[chord]){

  return total *

(this.probabilityOfChordsInLabels.get(

difficulty)[chord] + this.smoothing);

}else{

  return total;

Em seguida, extraímos uma função para substituir completamente essas linhas por isso:

return total * this.valueForChordDifficulty(difficulty, chord);

E podemos adicionar uma nova função acima classify :

valueForChordDifficulty(difficulty, chord){

  if(this.probabilityOfChordsInLabels.get(difficulty)[chord]){

    return this.probabilityOfChordsInLabels.get(difficulty)[chord] +

this.smoothing;

  }else{

    return 1;

  }

},

Essa função produzirá um valor para multiplicar pelo total em execução. Se quisermos
uma sintaxe um pouco mais densa para essa função, podemos usar a sintaxe ternária:

valueForChordDifficulty(difficulty, chord){

  const value =

this.probabilityOfChordsInLabels.get(difficulty)[chord];

  return value ? value + this.smoothing : 1;

},

Observe que isso também funcionaria como a primeira linha se você não gostar da sin-
taxe abreviada da função:

valueForChordDifficulty: function(difficulty, chord){

Ao contrário do que temos agora, que é:

valueForChordDifficulty(difficulty, chord){

DENSIDADE E ABSTRAÇÃO

Neste ponto, você deve estar se perguntando se essa refatoração realmente vale a pena.
Ao inserir variáveis, tornamos o código mais denso.Há também alguma sobrecarga para
usar map com um Map objeto. Para algumas pessoas, embutir variáveis ​e usar funções
como bind vai tornar o código mais difícil de ler. Para outros, a sintaxe da seta, com to-
das as suas variações (que abordaremos mais adiante), pode ser esmagadora. É impor-
tante estar ciente de como seu estilo pode afetar outros membros de sua equipe.

Então, acabamos de piorar o código? Não necessariamente.A vantagem das variáveis ​


embutidas é dupla.Primeiro, menos estado interno significa menos para acompanhar.
Em segundo lugar, as variáveis ​embutidas podem facilitar a extração de funções, que são
mais flexíveis e testáveis ​do que variáveis ​simples.

Para variáveis ​e funções, a parte importante é estar ciente de inlining e extração como
escolhas.
O C Ó D I G O N Ã O T E M E S TA D O F I N A L , P E R F E I TO , “ R E FATO R A D O ”

Uma coisa importante a notar sobre o nosso trabalho na classify função éque não há como sa-
ber quando você “termina” a refatoração. Algumas refatorações (como embutir e extrair) são pro-
cessos inversos, então você pode acabar desfazendo o trabalho de refatoração anterior com esfor-
ços subsequentes. Você poderia criar um loop infinito de refatoração apenas devido a esse fato.
Misture as opiniões dos outros sobre o que é um código “bom”, e você realmente pode “melhorar”
o código para sempre. É por isso que é importante desenvolver seus próprios padrões de quali-
dade e calibrá-los com as pessoas com quem você trabalha.

Desembaraçando Valores Acoplados

Passando da nossa classify função,nóster um problemano código que precisa ser en-
dereçado. Você verá algo interessante se adicionar instruções de log aos testes (depois
trainAll(); de ) como o seguinte:

console.log(classifier.probabilityOfChordsInLabels);

console.log(classifier.chordCountsInLabels);

Têm os mesmos valores. Não só isso, eles realmente estão referenciando o mesmo
Set objeto. 

A razão para isso está na setProbabilityOfChordsInLabels função—especifica-


mente, a segunda linha (que se espalha em duas linhas):

function setProbabilityOfChordsInLabels(){

  classifier.probabilityOfChordsInLabels =

classifier.chordCountsInLabels;

  classifier.probabilityOfChordsInLabels.forEach(

function(_chords, difficulty){

    Object.keys(

classifier.probabilityOfChordsInLabels.get(difficulty)).forEach(

function(chord){

      classifier.probabilityOfChordsInLabels.get(difficulty)[chord]

/= classifier.songs.length;

    });

  });

O problema é que quando atribuímos um conjunto a outro,não estamos apenas copi-


ando os valores do Set objeto do lado direito da atribuição para o Set objeto do lado
esquerdo. Ambos classifier.probabilityOfChordsInLabels e
classifier.chordCountsInLabels são apenas dedos apontando para o mesmo ob-
jeto. Se você alterar os valores do conjunto referenciando qualquer nome, ambos serão
afetados.

Aqui está um pequeno exemplo para demonstrar isso:

x = {a: 2};

// returns { a: 2 }

y = x;

// returns { a: 2 }

x['b'] = 3;

// returns 3

y;

// returns { a: 2, b: 3 }

y['c'] = 5;

// returns 5

x;

// returns { a: 2, b: 3, c: 5 }

No entanto, tudo isso envolve o que acontece quando você atualiza um objeto. Se você
reatribuir o objeto novamente, ele não alterará os dois rótulos para apontar para o novo
objeto:

x = {a: 2};

// returns { a: 2 }

y = x;

// returns { a: 2 }

x = {b: 5};

console.log(y);

// prints { a: 2 }

// because y still ponts at the original object

// x has been assigned to a new object

De volta ao nosso código, se probabilityOfChordsInLabels e


chordCountsInLabels fizermos a mesma coisa, podemos apenas substituir as referên-
cias ao primeiro pelo último:

function setProbabilityOfChordsInLabels(){

  classifier.chordCountsInLabels = classifier.chordCountsInLabels;

  classifier.chordCountsInLabels.forEach(function(_chords, difficulty){

    Object.keys(classifier.chordCountsInLabels.get(difficulty))

.forEach(function(chord){

      classifier.chordCountsInLabels.get(difficulty)[chord]

/= classifier.songs.length;

    });

  });

Agora a linha dois parece mais obviamente redundante, o que é ótimo, porque sabemos
que podemos excluí-la:

function setProbabilityOfChordsInLabels(){

  classifier.chordCountsInLabels.forEach(function(_chords, difficulty){

    Object.keys(classifier.chordCountsInLabels.get(difficulty))

      .forEach(function(chord){

        classifier.chordCountsInLabels.get(difficulty)[chord]

/= classifier.songs.length;

    });

  });

Também temos mais duas referências probabilityOfChordsInLabels para alterar


dentro de classifier . Uma é esta linha, que podemos deletar:

probabilityOfChordsInLabels: new Map(),

A outra é a última linha do trecho a seguir:

const classifier = {

...

const value =
this.probabilityOfChordsInLabels.get(difficulty)[chord];

A atribuição para value (dentro da valueForChordDifficulty função de


classifier ) deve ser atualizada para ser usada chordCountsInLabels .

Isso nos deixa com o seguinte:

const classifier = {

...

const value = this.chordCountsInLabels.get(difficulty)[chord];

E os testes devem estar passando neste momento. Em alguns casos, a refatoração correta
aqui seria copiar verdadeiramente o Set objeto em um novo. Então, ambos seriam obje-
tos independentes. Na verdade, não precisamos de dois objetos, então tentaremos uma
abordagem diferente aqui.

OBJETOS “COPIANDO”

Se você estiver interessado em copiar objetos emJavaScript, procureos termos cópia pro-
funda versus cópia superficial , bem como clonagem e as Object funções freeze ,
assign , e seal . Eles podem fazer o que você quer. Ou talvez você queira criar um
novo objeto vinculado ao antigounsprotótipo com Object.create . Talvez você queira
um objeto totalmente novo com Object.assign . Talvez você queira fazer uma função
construtora ou uma classe? Talvez você queira apenas uma função de fábrica (veja o Ca-
pítulo 8 ). Talvez você queira um contêiner novo e imutável para seus dados.

A propósito, há duas coisas a serem observadas Object.create . Primeiro, ele não pos-
sui as propriedades do objeto protótipo (seu primeiro parâmetro), então elas podem ser
sobrescritas. Segundo, ele apenas “copia” propriedades enumeráveis, portanto, não es-
pere que ele copie tudo .

Existem cerca de um milhão e cinco maneiras de fazerherança e cópia de objetos em Ja-


vaScript. Isso torna difícil escolher um. O ponto aqui é lembrar que atribuir com não = é
um deles. 

Como o último exemplo de código contém nossa única referência ao valor do novo ob-
jeto, tudo o que temos agora é um objeto que muda. Mas talvez não precisemos atualizar
o objeto. Vamos tentar usar uma função como atributo, em vez de um conjunto que ar-
mazena todos os valores.

Primeiro, vamos mover a setProbabilityOfChordsInLabels função para o


classifier objeto:

const classifier = {

...

  setProbabilityOfChordsInLabels: function(){

    classifier.chordCountsInLabels

.forEach(function(_chords, difficulty){

      Object.keys(classifier.chordCountsInLabels.get(difficulty))

        .forEach(function(chord){

          classifier.chordCountsInLabels.get(difficulty)[chord]

  /= classifier.songs.length;

      });

    });

  },

  valueForChordDifficulty(difficulty, chord){

...

E também precisaremos atualizar nossa setLabelsAndProbabilities função para


usar classifier. antes da chamada da função:

function setLabelsAndProbabilities(){

  setLabelProbabilities();

  setChordCountsInLabels();

  classifier.setProbabilityOfChordsInLabels();

};

A execução do conjunto de testes mostra que tudo está funcionando corretamente. Agora
vamos substituir as referências a classifier por this :

  setProbabilityOfChordsInLabels: function(){

    this.chordCountsInLabels.forEach(function(_chords, difficulty){

      Object.keys(this.chordCountsInLabels.get(difficulty))

        .forEach(function(chord){

          this.chordCountsInLabels.get(difficulty)[chord]

  /= this.songs.length;

      }, this);

    }, this);

  },

Lembre-se quealém de substituir classifier por this , também precisamos passar


this como o segundo argumento opcional para ambas as forEach chamadas. Caso
contrário, nosso this contexto se perderia. Desde antes, sabemos que poderíamos fazer
a mesma coisa com funções de seta, mas na verdade estaremos removendo essa função
em breve, então evitaremos essa refatoração por enquanto.

Agora chegamos ao cerne do problema: não queremos funções que definam valores por
meio de efeitos colaterais.Queremos funções que retornem valores. Eles são muito mais
fáceis de trabalhar e acompanhar. Não chegaremos até aqui neste capítulo e, sem dú-
vida, OOP luta contra esse ethos, enquanto o FP o encoraja (consulte o Capítulo 11 ).

Quanto a setProbabilityOfChordsInLabels , todo esse looping equivale a muito


pouco. Realmente, tudo o que queremos fazer é dividir o número de vezes que o acorde
aparece em uma determinada dificuldade pelo número de músicas.
Você pode excluir a  setProbabilityOfChordsInLabels função do objeto, bem como
sua chamada (de dentro de setLabelsAndProbabilities ). Em seu lugar, usaremos
uma função simples para verificar um determinado acorde e combinação de dificul-
dade. Adicione a seguinte likelihoodFromChord função e atualize a
valueForChordDifficulty função da classifier seguinte forma:

const classifier = {

...

  likelihoodFromChord: function(difficulty, chord){

    return this.chordCountsInLabels

.get(difficulty)[chord] / this.songs.length;

  },

  valueForChordDifficulty(difficulty, chord){

    const value = this.likelihoodFromChord(difficulty, chord);

    return value ? value + this.smoothing : 1;

  },

...

Agora setLabelsAndProbabilities deve ficar assim:

function setLabelsAndProbabilities(){

  setLabelProbabilities();

  setChordCountsInLabels();

};

Com essas alterações, simplificamos o código e paramos de depender de tantas reatribui-


ções .

Neste ponto, todos os testes devem estar funcionando novamente. Apenas para recapitu-
lar o que aconteceu nesta seção, tivemos dois nomes de variáveis ​que foram atribuídos
ao mesmo objeto. Primeiro, alteramos todas as instâncias do nome da segunda variável
para o nome da primeira variável, deixando-nos com uma variável que foi atualizada.
Então, em vez de depender das atualizações dessa variável, alteramos as referências a
ela para usar um cálculo para obter as informações (em vez de apenas acessar onde as
informações foram armazenadas).
SOBRE A REATRIBUIÇÃO DE VARIÁVEIS

Entre os desenvolvimentos recentes mais importantes em JavaScript e além está o au-


mento da importância da programação funcional.Exploraremos mais os benefícios no
Capítulo 11 , mas uma das melhores coisas sobre ele é como ele permite que os valores
sejam facilmente confiáveis.

Na outra extremidade do espectro está a reatribuição de valores. Nada torna um pro-


grama mais difícil de depurar, escrever recursos, refatorar ou entender do que variáveis
​de escopo amplo que são atribuídas e reatribuídas. No entanto, mesmo variáveis ​com es-
copo bastante limitado podem criar dificuldades de manutenção quando são reatribuí-
das várias vezes (por exemplo, uma variável que muda algumas vezes em apenas uma
função de 20 linhas).

Se há uma coisa que todos poderiam parar de fazer para beneficiar seu código, seria rea-
tribuir variáveis.

Atualizar valores (adicionar/excluir/alterar elementos em um objeto ou uma matriz, por


exemplo) pode ser tão ruim quanto, e é melhor feito como uma operação de uma etapa
(principalmente simplesmente por meio map de filter , ou reduce ) atrás de uma
função e atribuindo a um nova variável quando possível. No geral, tornar o escopo da
maioria das variáveis ​pequeno deve ser uma prioridade.

Algo a ser considerado é a ideia de que a POO encoraja a reatribuição de variáveis ​por
meio de objetos que permanecem e permitem que as propriedades sejam alteradas.

Objetos com informações duplicadas

Na última seção, tratamos de duas referências a um objeto que foi atualizado. Agora, va-
mos lidar com dois objetos independentes que são um pouco parecidos demais.
classifier.songs e songList.songs parecem ter dados quase idênticos. Realmente,
a única diferença é a propriedade extra ( name ) em songList.songs . Vamos nos li-
vrar da classifier propriedade songs de :

const classifier = {

...

likelihoodFromChord: function(difficulty, chord){

  return this.chordCountsInLabels

.get(difficulty)[chord] / songList.songs.length;

},

Precisamos fazer duas alterações no classifier . Exclua o songs atributo e altere


this para songList na likelihoodFromChord função.

Em seguida, temos que excluir a segunda linha de nossa train função:

function train(chords, label){

  classifier.songs.push({label: label, chords: chords}); // this one

Além disso, setLabelProbabilities deve usar songList.songs.length em vez de


classifier.songs.length :

function setLabelProbabilities(){

  classifier.labelCounts.forEach(function(_count, label){

    classifier.labelProbabilities.set(label,

classifier.labelCounts.get(label) / songList.songs.length);

  })

};

Por último, nossa setChordCountsInLabels função precisa ser alterada para usar
songList.songs em vez de classifier.songs , e também para usar
song.difficulty em vez de song.label :

function setChordCountsInLabels(){

  songList.songs.forEach(function(song){

    if(classifier.chordCountsInLabels.get(song.difficulty)

=== undefined){

      classifier.chordCountsInLabels.set(song.difficulty, {});

    }

    song.chords.forEach(function(chord){

      if(classifier.chordCountsInLabels.get(song.difficulty)[chord] >

0){

        classifier.chordCountsInLabels.get(song.difficulty)[chord] +=

1;

      } else {

        classifier.chordCountsInLabels.get(song.difficulty)[chord] =

1;

      }

    });

  });

Neste ponto, todos os testes devem funcionar novamente.


Trazendo as outras funções e variáveis ​para o classificador

Agora vamosmudar chordCountsInLabels paranosso classifier objeto:

const classifier = {

...

  setChordCountsInLabels = function(){

    songList.songs.forEach(function(song){

      if(classifier.chordCountsInLabels.get(song.difficulty)

  === undefined){

        classifier.chordCountsInLabels.set(song.difficulty, {});

      }

      song.chords.forEach(function(chord){

        if(classifier.chordCountsInLabels

.get(song.difficulty)[chord] > 0){


          classifier.chordCountsInLabels

.get(song.difficulty)[chord] += 1;
        } else {

          classifier.chordCountsInLabels

.get(song.difficulty)[chord] = 1;

        }

      });

    });

  },

...

E então precisamos alterar a chamada setLabelsAndProbabilities para referenciá-


laatravés do classifier :

function setLabelsAndProbabilities(){

  setLabelProbabilities();

  classifier.setChordCountsInLabels();

};

Em seguida, “ this -ificamos” a função alterando as referências para classifier e


this adicionando o thisArg às forEach funções:

const classifier = {

...

  setChordCountsInLabels: function(){

    songList.songs.forEach(function(song){

      if(this.chordCountsInLabels.get(song.difficulty) === undefined){

        this.chordCountsInLabels.set(song.difficulty, {});

      }

      song.chords.forEach(function(chord){

        if(this.chordCountsInLabels.get(song.difficulty)[chord] > 0){

          this.chordCountsInLabels.get(song.difficulty)[chord] += 1;

        } else {

          this.chordCountsInLabels.get(song.difficulty)[chord] = 1;

        }

      }, this);

    }, this);

  },

Todos os testes ainda passam. A seguir, como comtransformando nosso


likelihoodFromChord em uma consulta em vez de definir e depois recuperar os valo-
res do  probabilityOfChordsInLabels mapa, agora podemos fazer a mesma coisa
para eliminar o chordCountsInLabels mapa.

Dê uma olhada novamente na setChordCountsInLabels função no último trecho.


Tudo o que ele faz é percorrer cada acorde em cada música e adicionar 1 para cada ins-
tância do acorde.

Se tudo o que temos a fazer écontar o número de vezes que um acorde aparece em uma
determinada dificuldade, podemos montar loops semelhantes e um contador, e basta so-
mar 1 quando houver correspondência. Adicione isso depois
setChordCountsInLabels no classify objeto:

chordCountForDifficulty: function(difficulty, testChord){

  let counter = 0;

  songList.songs.forEach(function(song){

    if(song.difficulty === difficulty){

      song.chords.forEach(function(chord){

        if(chord === testChord){

          counter = counter + 1;

        }

      });

    }

  });

  return counter;

},

Agora podemos usar essa função em vez de definir e recuperar as contagens de acordes.
Observe que não precisamos thisArg de s para nossas forEach funções porque não
temos mais referências a this .
Precisamos de algumas mudanças adicionais. Primeiro, a likelihoodFromChord fun-
ção pode ser alterada para:

likelihoodFromChord: function(difficulty, chord){

  return this.chordCountForDifficulty(difficulty, chord) /

songList.songs.length;

},

Em seguida, podemos excluir nossa setChordCountsInLabels função de


classifier , bem como a chamada para ela em setLabelsAndProbabilities , o que
nos deixa com isso:

function setLabelsAndProbabilities(){

  setLabelProbabilities();

};

Esta função agora só existe para chamar outra função. Isso significa que podemos ex-
cluir setLabelsAndProbabilities e apenas chamar setLabelProbabilities , o
queacontece dentro da trainAll função. Mude isso:

function trainAll(){

  songList.songs.forEach(function(song){

    train(song.chords, song.difficulty);

  });

  setLabelsAndProbabilities();

};

para isso:

function trainAll(){

  songList.songs.forEach(function(song){

    train(song.chords, song.difficulty);

  });

  setLabelProbabilities();

};

Todos os testes ainda passam. Antes de prosseguir, vamosdeve ter outro olhar para a
chordCountForDifficulty função:

chordCountForDifficulty: function(difficulty, testChord){

  let counter = 0;

  songList.songs.forEach(function(song){

    if(song.difficulty === difficulty){

      song.chords.forEach(function(chord){

        if(chord === testChord){

          counter = counter + 1;

        }

      });

    }

  });

  return counter;

},

Como fizemos na classify função, quando temos um código que usa um loop para
aplicar alguma função a uma coleção enquanto altera uma variávelpor toda parte, é um
bom candidato para reduce . Pelocaminho, observe queusamos let aqui, em vez de
const , porque temos uma variável que atualiza genuinamente. Vamos alterá-lo para
usar reduce :

chordCountForDifficulty: function(difficulty, testChord){

  return songList.songs.reduce(function(counter, song){

    if(song.difficulty === difficulty){

      song.chords.forEach(function(chord){

        if(chord === testChord){

          counter = counter + 1;

        }

      });

    }

    return counter;

  }, 0);

},

Temos algumas mudanças aqui:

Retornamos o resultado de reduce diretamente.


Substituímos nossa counter variável   anteriorcom um parâmetro para reduce a
função de retorno de chamada de .
Retornamos o counter interior da reduce função. Reconheça que isso não está re-
tornando da chordCountForDifficulty função, mas é usado dentro de
reduce para definir o counter valor à medida que percorre o arquivo songList .

Também podemos usar a filter , em vez de a forEach , para ajudarcontamos os ele-


mentos que atendem a uma condição. A filter função retorna uma nova matriz com-
posta por elementos que corresponderam à condicional:
chordCountForDifficulty: function(difficulty, testChord){

  return songList.songs.reduce(function(counter, song){

    if(song.difficulty === difficulty){

      counter += song.chords.filter(function(chord){

        return chord === testChord;

      }).length;

    }

    return counter;

  }, 0);

},

Obtemos o length deste array e o adicionamos ao counter . Isso significa menos atua-
lizações para o counter . Também raspamos duas linhas da
chordCountForDifficulty função. Poderíamos ter usado um segundo reduce aqui
em vez de filter se quiséssemos focar mais na contagem do que na condicional, mas
isso significaria uma innerCount variável, que parece um pouco mais desajeitada.

IMPACTOS NO DESEMPENHO

As alterações que fizemos para consultar conforme a necessidade, em vez de criar estru-
turas mais específicas para necessidades posteriores, são para fins de refatoração, in-
cluindo a redução do tamanho do código.

Dependendo dos padrões de acesso a dados de seu programa, essa estratégia pode tornar
seu programa mais lento ou mais rápido. Transformar estruturas em outras que você
nunca usará seria um desperdício. Transformá-los em outros mais simples (mais rasos)
que serão acessados ​com frequência pode produzir algum benefício de desempenho.
Tudo isso vem com a ressalva de que uma determinada plataforma JavaScript pode oti-
mizar seu código de maneiras que você não espera. 

Discutiremos a memoização junto com a programação funcional no Capítulo 11 . Ela, as-


sim como outras técnicas de armazenamento em cache, ajudará a superar um impacto
no desempenho que você pode obter com o tipo de refatoração que fizemos aqui.

De qualquer forma, a abordagem deste livro é escrever primeiro para humanos e se pre-
ocupar com o desempenho depois do fato.

Seguindo em frente, temos apenas três funções restantes no escopo global: train ,
trainAll , e setLabelProbabilities . Esses parecem que se encaixam perfeita-
mente como parte do classifier . Vamos movê-los agora:
const classifier = {

...

trainAll: function(){

  songList.songs.forEach(function(song){

    classifier.train(song.chords, song.difficulty);

  });

  classifier.setLabelProbabilities();

},

train: function(chords, label){

  chords.forEach(chord => {

    classifier.allChords.add(chord);

  });

  if(Array.from(classifier.labelCounts.keys()).includes(label)){

    classifier.labelCounts.set(

label, classifier.labelCounts.get(label) + 1);

  } else {

    classifier.labelCounts.set(label, 1);

  }

},

setLabelProbabilities: function(){

  classifier.labelCounts.forEach(function(_count, label){

    classifier.labelProbabilities.set(

label, classifier.labelCounts.get(label) /

songList.songs.length);

  });

...

A maior mudança é que precisamos colocar essas funções na sintaxe do atributo do ob-
jeto. Além disso, trainAll precisa preceder suas chamadas para train e
setLabelProbabilities com classifier. .

Nossa chamada trainAll nos testes também precisa de uma novidade


classifier. no início:

classifier.trainAll();

Os testes devem estar funcionando neste momento.

Agora, vamos “ this -ificar” nossas funções.Isso significa alterar as instâncias de


classifier to this , bem como adicionar o thisArg como segundo argumento aos
retornos de chamada das forEach funções dentro de trainAll e
setLabelProbabilities :

trainAll: function(){

  songList.songs.forEach(function(song){

    this.train(song.chords, song.difficulty);

  }, this);

  this.setLabelProbabilities();

},

train: function(chords, label){

  chords.forEach(chord => { this.allChords.add(chord) });

  if(Array.from(this.labelCounts.keys()).includes(label)){

    this.labelCounts.set(label, this.labelCounts.get(label) + 1);

  } else {

    this.labelCounts.set(label, 1);

  }

},

setLabelProbabilities: function(){

  this.labelCounts.forEach(function(_count, label){

    this.labelProbabilities.set(label, this.labelCounts.get(label) /

songList.songs.length);

  }, this);

Observe que nossa forEach função dentro de train não precisa de um thisArg , pois
usa a sintaxe de seta.

Agora, tudo faz parte de um classifier ou songList . Estamos reduzidos a duas va-
riáveis ​globais. Agora podemos começar a pensar um pouco mais sobre o que pertence a
onde. Um atributo de classifier se destaca como sendo mais apropriado como parte
de songList : allChords .

Podemos simplesmente mover o atributo:

const songList = {

  allChords: new Set(),

Agora podemos excluí-lo de classifier e mudar this para songList na chamada de


função dentro de train :

chords.forEach(chord => { songList.allChords.add(chord) });

Os testes devem passar neste ponto.

Em seguida, uma vez que queremos que o nosso classifier seja o únicoponto de
acesso global no programa, devemos passar songList para ele:

const classifier = { 

songList: {

    allChords: new Set(),

    difficulties: ['easy', 'medium', 'hard'],

    songs: [],

    addSong: function(name, chords, difficulty){

      this.songs.push({name: name,

                      chords: chords,

                     difficulty: this.difficulties[difficulty]})

  }

},

...

E agora, há uma série de referências songList que precisam ser precedidas de


this. onde elas estão dentro do classifier objeto. Para aqueles que estão nos testes,
eles precisam ser prefixados com classifier. . Faça uma busca por “songList” e adici-
one conforme necessário. Salve, teste, verifique e confirme.

Sintaxe abreviada: seta, função de objeto e objeto

Em seguida, vamos resolver algumas inconsistências que temosem nossa sintaxe de fun-
ção. Usamos funções de seta anteriormente neste livro, mas vamos entrar em mais deta-
lhes aqui. Começaremos com uma instância em que já estamos usando a sintaxe de seta.
Em nossa train função temos o seguinte:

train: function(){

  chords.forEach(chord => { this.songList.allChords.add(chord) });

...

Já discutimos isso um pouco antes, mas o que é interessante sobre isso é, bem, this .
Nós referenciamos this dentro da função anônima aqui e somos livres para não passar
um thisArg ( this ) como o segundo parâmetro para forEach . Como prova de que
isso é algo especial com funções de seta e não código que funciona bem sem ela, tente
convertê-lo para a forma longa:
train: function(){

  chords.forEach(function(chord){

    this.songList.allChords.add(chord);

  });

...

Vai quebrar os testes! Se você quiser usar esse formulário, precisará incluir o this ,
como fazemos atualmente em trainAll :

train: function(){

  chords.forEach(function(chord){

    this.songList.allChords.add(chord);

  }, this);

...

Mas poderíamos usar bind na função em vez disso (fazemos isso com reduce uma vez
que não tem a opção de passar a thisArg como segundo parâmetro):

train: function(){

  chords.forEach(function(chord){

    this.songList.allChords.add(chord);

  }.bind(this));

...

Em vez de fazer qualquer um desses, vamos usar a sintaxe de seta, que passa o
this contexto para a função interna e nos poupa de ter que digitar tanto a função da pa-
lavra . Incrível!

Você pode estar se perguntando por que nunca usaríamos a sintaxe de seta. Existem al-
guns casos. Mais importante, você não gostaria de usá-lo nos casos em que o this que
você se importa não deve ser aquele this que você tem acesso ao entrar na função. Por
exemplo, você pode esperar que this um manipulador de cliques no jQuery se refira ao
objeto clicado. Se você usar uma função de seta, esse não será o caso.

Outra razão é que neste formulário (ignorando a dificuldade em passar parâmetros), as


funções são fáceis de extrair ou embutir, pois a sintaxe é muito semelhante à sintaxe de
declaração de função:

// form 1

chords.forEach(function myFunction(){

  this.songList.allChords.add(chord);

}, this);

// form 2

chords.forEach(myFunction, this);

function myFunction(){

  this.songList.allChords.add(chord);

};

É fácil converter entre essas duas formas. Se você começar com uma função anônima no
formulário 1 (com ou sem sintaxe de seta), será necessário um pouco de trabalho extra
para convertê-la em uma função nomeada independente. Mas para todas as nossas fun-
ções usadas com forEach , map , reduce e filter , podemos substituí-las facilmente
por funções de seta.Isso corta cerca de 10 linhas de código. A propósito, com nossa refa-
toração até agora, o que antes era cerca de 110 linhas agora está reduzido para 63. É pe-
queno o suficiente para ver tudo de uma vez (menos os testes):

const classifier = {

  labelCounts: new Map(),

  labelProbabilities: new Map(),

  chordCountsInLabels: new Map(),

  smoothing: 1.01,

  songList: {

    allChords: new Set(),

    difficulties: ['easy', 'medium', 'hard'],

    songs: [],

    addSong: function(name, chords, difficulty){

      this.songs.push({name: name,

                      chords: chords,

                      difficulty: this.difficulties[difficulty]});

    }

  },

  chordCountForDifficulty: function(difficulty, testChord){

    return this.songList.songs.reduce((counter, song) => {

      if(song.difficulty === difficulty){

        counter += song.chords.filter((chord) => {

          return chord === testChord;

        }).length;

      }

      return counter;

    }, 0);

  },

  likelihoodFromChord: function(difficulty, chord){

    return this.chordCountForDifficulty(difficulty, chord) /

this.songList.songs.length;

  },

  valueForChordDifficulty(difficulty, chord){

    const value = this.likelihoodFromChord(difficulty, chord);

    return value ? value + this.smoothing : 1;

  },

  trainAll: function(){

    this.songList.songs.forEach((song) => {

      this.train(song.chords, song.difficulty);

    });

    this.setLabelProbabilities();

  },

  train: function(chords, label){

    chords.forEach(chord => { this.songList.allChords.add(chord) } );

    if(Array.from(this.labelCounts.keys()).includes(label)){

      this.labelCounts.set(label, this.labelCounts.get(label) + 1);

    } else {

      this.labelCounts.set(label, 1);

    }

  },

  setLabelProbabilities: function(){

    this.labelCounts.forEach((_count, label) => {

      this.labelProbabilities.set(label, this.labelCounts.get(label) /

this.songList.songs.length);

    });

  },

  classify: function(chords){

    return new Map(Array.from(

      this.labelProbabilities.entries()).map((labelWithProbability) => {

      const difficulty = labelWithProbability[0];

      return [difficulty, chords.reduce((total, chord) => {

        return total * this.valueForChordDifficulty(difficulty, chord);

      }, this.labelProbabilities.get(difficulty) + this.smoothing)];

    }));

  }

};

Há algumas coisas a serem observadas com a sintaxe da seta. Primeiro, se houver ape-
nas um argumento, ele não precisa entrar entre parênteses:

return new Map(Array.from(

this.labelProbabilities.entries()).map(labelWithProbability => {

Se houver dois ou mais argumentos, você precisará deles, como neste caso:
setLabelProbabilities: function(){

this.labelCounts.forEach((_count, label) =>{

Uma coisa um tanto estranha é que também precisamos de parênteses se tivermos zero
argumentos. Neste ponto, podemos alterar todos os nossos testes para usar esta sintaxe:

describe('the file', () => {

...

  it('classifies', () => {

...

  it('classifies again', () => {

...

  it('label probabilities', () => {

...

Passando para a sintaxe abreviada de declaração de função para objetos, podemos usar
a sintaxe de seta e declarar nossa trainAll função assim:

trainAll: () => {

  this.songList.songs.forEach(song => {

this.train(song.chords, song.difficulty);

});

  this.setLabelProbabilities();

},

No entanto, não é isso que queremos. Na verdade, queremos o this que obtemos com a
palavra-chave function usada, porque esse this é o objeto ( classifier ), que é a que
nosso this interior da função se refere. Quando declaramos funções dessa maneira, ob-
temos o contexto fora do objeto (o objeto global, assumindo o modo não estrito). De qual-
quer forma, existe uma abreviação melhor para declarações de funções, que veremos
em breve. Vamos colocar de volta nisso:

trainAll: function(){

  this.songList.songs.forEach(song => {

this.train(song.chords, song.difficulty);

});

  this.setLabelProbabilities();

},

Uma outra coisa estranha sobre a sintaxe da seta é que algumas vezes usamos chaves
{} e outras não.No momento temos isso:
counter += song.chords.filter((chord) => {

          return chord === testChord;

        }).length;

Vamos mudar isso para isso:

counter += song.chords.filter(chord => chord === testChord).length;

A falta de parênteses, chaves e o return pode parecer um pouco chocante porque torna
a declaração mais densa. No entanto, há uma consideração adicional com a versão sem
chaves: ela retorna implicitamente o resultado de sua execução. No caso do nosso último
trecho de código, isso significa que ele retorna true ou false .

Observe também que, como a sintaxe de chaves é usada como forma de agrupar o corpo
da função, se você quiser retornar um objeto como este, ficará desapontado:

someFunction(someArg => {someThing: 'someValue'}) // nope

Para retornar um objeto, você precisará adicionar parênteses:

someFunction(someArg => ({someThing: 'someValue'})) // ok

Por uma questão de estilo pessoal, eu recomendaria usar menos sintaxe quando possí-
vel. Isso significa isso:

chords.forEach(chord => { this.songList.allChords.add(chord) } );

pode se tornar isso:

chords.forEach(chord => this.songList.allChords.add(chord) );

Durante a discussão das funções de seta, esbarramos na ideia de uma abreviação para
declarar funções como parte de um objeto. É assim que estamos declarando atualmente
a maioria das funções:

const classifier = {

...

    addSong: function(name, chords, difficulty){

...

  chordCountForDifficulty: function(difficulty, testChord){

...

No entanto, temos uma exceção:

valueForChordDifficulty(difficulty, chord){

// instead of

valueForChordDifficulty: function(difficulty, chord){

Usar esta sintaxe abreviada pode remover completamente a : function parte, fazendo
com que as funções fiquem assim:

const classifier = {

  songList: {

...

    addSong(name, chords, difficulty){

...

  chordCountForDifficulty(difficulty, testChord){

...

Depois de fazer isso com todas as nossas declarações de função, estamos completamente
livres da palavra- function chave neste arquivo!Uma fonte incrível de ruído visual e
digitação extra se foi. Obrigado, ES2015!

Observe, no entanto, que essa abreviação funciona apenas dentro de literais e classes de
objetos. Fora desses contextos (em uma função normal, escopo global ou escopo de fun-
ção construtora, por exemplo), o interpretador lançará um erro no arquivo { .

Mais uma vez, podemos ver que nossa sintaxe abreviada é menos portátil do que a ver-
são longa.
PROPRIEDADES COMPUTADAS

Enquanto estamos discutindo a abreviação de declaração de função, vale a pena notar


que você pode declararpropriedades de um objetot dinamicamente, assim:

songs = {
['first' + 'Song']: {},

  ['second' + 'Song']: {},

  ['third' + 'Song']: {}
}

Basicamente, você pode executar JavaScript dentro desses colchetes para gerar nomes
de propriedades. Este é um exemplo trivial (e você provavelmente gostaria de alguns da-
dos em seus objetos), mas você pode achar isso conveniente em algum momento.

Esteja ciente, no entanto, que ao definir rótulos de propriedades dinamicamente (ou


acessá-los dinamicamente como em songs['first' + 'Song'] ), você perde a capaci-
dade de pesquisar facilmente strings simples como firstSong em sua base de código.

A última abreviação que poderíamos implementar está na addSong função. No mo-


mento temos isso:

addSong(name, chords, difficulty){

  this.songs.push({name: name,

                   chords: chords,

                   difficulty: this.difficulties[difficulty]});

Mas poderíamos encurtaro objeto dentro de push para usar a abreviação do objeto ,
assim:

this.songs.push({name, chords,

difficulty: this.difficulties[difficulty]});

Obtendo novos objetos com funções construtoras

Até agora,nós temoslidando com literais de objeto.Chegaremos às classes daqui a pouco,


mas primeiro vamos explorar outra opção: criar objetos com funções construtoras. Para
usá-los, teríamos que alterar nosso código para algo como o seguinte:
const Classifier = function(){

  const SongList = function() {

    this.allChords = new Set();

    this.difficulties = ['easy', 'medium', 'hard'];

    this.songs = [];

    this.addSong = function(name, chords, difficulty){

      this.songs.push({name,

                      chords,

                      difficulty: this.difficulties[difficulty]});

    };

  };

  this.songList = new SongList();

  this.labelCounts = new Map();

  this.labelProbabilities = new Map();

  this.chordCountsInLabels = new Map();

  this.smoothing = 1.01;

  this.chordCountForDifficulty = function(difficulty, testChord){

    return this.songList.songs.reduce((counter, song) => {

      if(song.difficulty === difficulty){

        counter += song.chords.filter(chord => chord === testChord).length;

      }

      return counter;

    }, 0);

  };

  this.likelihoodFromChord = function(difficulty, chord){

    return this.chordCountForDifficulty(difficulty, chord) /

this.songList.songs.length;

  };

  this.valueForChordDifficulty = function(difficulty, chord){

    const value = this.likelihoodFromChord(difficulty, chord);

    return value ? value + this.smoothing : 1;

  };

  this.trainAll = function(){

    this.songList.songs.forEach((song) => {

      this.train(song.chords, song.difficulty);

    });

    this.setLabelProbabilities();

  };

  this.train = function(chords, label){

    chords.forEach(chord => this.songList.allChords.add(chord) );

    if(Array.from(this.labelCounts.keys()).includes(label)){

      this.labelCounts.set(label, this.labelCounts.get(label) + 1);

    } else {

      this.labelCounts.set(label, 1);

    }

  };

  this.setLabelProbabilities = function(){

    this.labelCounts.forEach((_count, label) => {

      this.labelProbabilities.set(label, this.labelCounts.get(label) /

this.songList.songs.length);

    });

  };

  this.classify = function(chords){

    return new Map(Array.from(

      this.labelProbabilities.entries()).map((labelWithProbability) => {

      const difficulty = labelWithProbability[0];

      return [difficulty, chords.reduce((total, chord) => {

        return total * this.valueForChordDifficulty(difficulty, chord);

      }, this.labelProbabilities.get(difficulty) + this.smoothing)];

    }));

  };

};

const wish = require('wish');

describe('the file', () => {

  const classifier = new Classifier();

...

Os testes têm apenas uma pequena mudança, porque precisamos inicializar nosso
classifier com:

const classifier = new Classifier();

É assim que você instancia um objeto em JavaScript usando new uma função constru-
tora. Observe que também estamos instanciando uma songList propriedade de ma-
neira semelhante dentro do classifier :

this.songList = new SongList();

A maioria de nossas mudanças são no código existente, mas as linhas new


Classifier() e são, bem, novas. new SongList()
N Ã O S E E S Q U E Ç A D E U S A R N E W N A S F U N Ç Õ E S D O C O N S T R U TO R

Se você esquecer new ao chamar uma função construtora,sua função ainda pode funcionar bem,
mas há uma chance de que não. Isso porque this será vinculado ao objeto global (ou
undefined em modo estrito).

Às vezes, esse medo (e se as pessoas esquecerem a palavra- new chave!?) é citado como a principal
motivação por trás da preferência Object.create por new uma função construtora. O argumento
mais forte para Object.create tem mais a ver com a busca de consistência e não obscurecer a
natureza prototípica do JavaScript , em vez de usar o “estilo pseudoclássico” que as funções (e clas-
ses) do construtor new promovem.

No lado mais flexível das coisas, os parênteses após a chamada do construtor a seguir new são op-
cionais quando nenhum argumento é passado para a função. É um pouco estranho, mas estes são
os mesmos:

this.songList = new SongList();

this.songList = new SongList;

Há uma diferença importante entre criarobjetos como este e usando literais de objeto. Se
você usar um literal de objeto, ficará preso fazendo algumas clonagens/cópias profun-
das/ Object.create hijinks para obter uma nova versão. Como exploramos anterior-
mente, atribuir algo a uma nova variável usando = apenas cria uma nova referência
para o mesmo objeto: dois dedos apontando para a mesma lua. Se você precisar de mais
de uma lua, precisará new de , Object.create , class e/ou algum outro utilitário de
cópia/clonagem. Se você sabe que vai querer mais de um objeto, literais de objeto podem
não ser o melhor ponto de partida para criá-los.

Este trecho também demonstra outro resultado dessa mudança: temos que adicionar
this. a todas as nossas propriedades quando usamos uma função construtora. Observe
também que essa sintaxe nos força a adicionar nossas function palavras-chave de
volta com os dois pontos substituídos por sinais de igual. Por fim, as vírgulas no final das
declarações foram substituídas por ponto e vírgula (ou não, em casos especiais, se você
for uma dessas pessoas).
FA L A N D O E M P O N TO E V Í R G U L A . . .

Algumas pessoas realmente odeiam ponto e vírgula.Eles só os usarão quando for absolutamente
necessário, como prova de seu profundo conhecimentoda inserção automática de ponto e vírgula
do JavaScript (ASI).

Pessoalmente, acho os casos extremos para quando os pontos e vírgulas são necessários um pouco
difíceis de lembrar, e sinto que outros (incluindo pessoas com quem trabalho) também. Por essas
razões, eu os incluo na maioria das vezes.

“Para ponto e vírgula, ou não para ponto e vírgula?” não é a questão final para um humano: este é
um trabalho para um linter lidar.

Embora a sintaxe possa não ser a que preferimos, os testes ainda passam, então esta é
uma refatoração bem-sucedida até agora. O que nós ganhamos? Bem, como estamos
dentro da função construtora e não restringidos pela sintaxe JSON, somos livres para es-
crever qualquer instrução que desejarmos. Isso significa que poderíamos ter funções e
variáveis ​privadas (ou seja, não expostas) dentro do construtor, bem como declarar va-
riáveis ​globais não usando var , let ou const (também poderíamos fazer isso na sin-
taxe literal do objeto, mas não diretamente). Mais importante, como mencionado anteri-
ormente, podemos obter várias instâncias do mesmo objeto simplesmente com new , em
vez de passar por etapas um tanto não intuitivas para “clonar” ou “copiar” ou “criar”
um.

Funções de construtor versus funções de fábrica

Vamos fazer uma pausa do nosso classifier por apenas um minuto.Você pode achar
este exemplo semelhante ao diary código do Capítulo 5 , pois também diz respeito à
privacidade. No entanto, esta discussão está mais focada em objetos e sua criação.

Estaremos definindo nossa API em uma seção posterior deste capítulo e, para fazer isso,
devemos estar familiarizados com o que podemos e não podemos acessar dentro das
funções do construtor. O exemplo de código a seguir é um pouco mais curto, então será
mais fácil entender isso antes de aplicar os conceitos ao nosso código NBC:

// constructor function

const Secret = function(){

  this.normalInfo = 'this is normal';

  const secret = 'sekrit';

  const secretFunction = function(){

    return secret;

  };

  this.notSecret = function(){

    return secret;

  };

  totallyNotSecret = "I'm defined in the global scope";

};

const s = new Secret();

console.log(s.normalInfo); // 'this is normal'

console.log(s.secret); // undefined

console.log(s.secretFunction()); // error

console.log(s.notSecret()); // 'sekrit'

console.log(s.totallyNotSecret); // undefined

console.log(totallyNotSecret); // I'm defined in the global scope

Uma alternativa para criar objetos com a new palavra-chave éusando Object.create .
Antes de mudarmos nosso classificador, vamos apenas observar as diferenças entre essa
construção e new , como no último trecho:

// factory function

var secretTemplate = (function(){

  var obj = {};

  obj.normalInfo = 'this is normal';

  const secret = 'sekrit';

  const secretFunction = function(){

    return secret;

  };

  obj.notSecret = function(){

    return secret;

  };

  totallyNotSecret = "I'm defined in the global scope";

  return obj;

})();

const s = Object.create(secretTemplate);

console.log(s.normalInfo); // 'this is normal'

console.log(s.secret); // undefined

console.log(s.secretFunction()); // error

console.log(s.notSecret()); // 'sekrit'

console.log(s.totallyNotSecret); // undefined

console.log(totallyNotSecret); // "I'm defined in the global scope"

Então, quando usamos Object.create , precisamos fornecer um objeto para ser padro-
nizado. Para manter a flexibilidade de usar um construtor com new , acabamos retor-
nando um objeto dentro da função. Observe que esta função é um IIFE.No entanto, tam-
bém podemos usar uma expressão de função normal:
var secretTemplate = function(){

...

};

const s = Object.create(secretTemplate());

Isso nos dá o mesmo objeto, mas parece um pouco menos claro. Além disso, nesta cons-
trução, a função terá que ser executada toda vez que criarmos um novo objeto. Na ver-
são IIFE, a secretTemplate função só precisa ser executada uma vez, e para criar no-
vos objetos basta referenciar o objeto template que foi criado.

Reordenando um pouco nosso código, poderíamos expressar nosso código de retorno de


objeto de forma mais concisa assim:

// module pattern

var secretTemplate = (function(){

  const secret = 'sekrit';

  const secretFunction = function(){

    return secret;

  };

  totallyNotSecret = "I'm defined in the global scope";

  return {normalInfo: 'this is normal',

notSecret(){

return secret;

}};

})();

const s = Object.create(secretTemplate);

console.log(s.normalInfo); // 'this is normal'

console.log(s.secret); // undefined

console.log(s.secretFunction()); // error

console.log(s.notSecret()); // 'sekrit'

console.log(s.totallyNotSecret); // undefined

console.log(totallyNotSecret); // "I'm defined in the global scope"

Isso usa o padrão de módulo , não deve ser confundido com módulos para importar e ex-
portar pacotes.Uma variação popular no padrão de módulo é o padrão de módulo revela-
dor , mostrado aqui:

// revealing module pattern

var secretTemplate = (function(){

  const secret = 'sekrit';

  const secretFunction = function(){

    return secret;

  };

  totallyNotSecret = "I'm defined in the global scope";

const normalInfo = 'this is normal';

const notSecret = function(){

    return secret;

};

  return {normalInfo, notSecret};

})();

const s = Object.create(secretTemplate);

console.log(s.normalInfo); // 'this is normal'

console.log(s.secret); // undefined

console.log(s.secretFunction()); // error

console.log(s.notSecret()); // 'sekrit'

console.log(s.totallyNotSecret); // undefined

console.log(totallyNotSecret); // "I'm defined in the global scope"

Isso é um pouco mais limpo, pois torna o objeto retornado muito conciso e legível.

MAIS UMA COISA SOBRE IIFES

Acabamos de usar um IIFE como o lado direito de nossa instrução de atribuição. Nesse
caso, a variável é provavelmente uma boa candidata para exportar como um módulo.

Em outros casos, onde um IIFE é usado especificamente para reduzir o escopo do código
interno, podemos usar um bloco. Então, é isso:

(function(){

// code we don't want outside of this scope

})();

torna-se isto:

// code we don't want outside of this scope

};

Mas se você tentar usar umblock, ou seja {} , no lado direito de uma atribuição (como
fizemos com um IIFE), ele será interpretado como um objeto e provavelmente dará um
erro. Nesses casos, você está preso ao IIFE.
Voltando ao nosso classifier , como usaríamos Object.create para uma função de
fábrica?Acaba sendo muito simples e depende muito mais da sintaxe literal do objeto do
que de como reorganizamos as coisas para usar new com as funções do construtor na úl-
tima seção:

const classifierTemplate = {

  songList: {

    allChords: new Set(),

...

const wish = require('wish');

describe('the file', () => {

  var classifier = Object.create(classifierTemplate);

Para fazer esse código funcionar, precisamos apenas de duas alterações em relação ao
que fizemos com o literal do objeto. Primeiro, precisamos considerar nosso objeto inicial
como um template , então o renomeamos classifierTemplate . Segundo, em nossos
testes (que se referem a classifier ), criamos o classifier objeto passando o objeto
de modelo para Object.create . Observe que estamos usando apenas um literal de ob-
jeto aqui, não uma função ou IIFE, porque não temos nada que nos importe em ser pri-
vado no momento.

Caso você tenha esquecido quais vantagens uma função de fábrica Object.create e
uma função de construtor new têmsobre apenas usar o literal do objeto diretamente, o
principal é que essas são maneiras de criar várias classifier variáveis. O songList é
uma propriedade de classifier , e como um objeto aninhado, uma nova versão dele
será criada para cada classifier , mesmo que use a sintaxe literal de objeto.
Object.create e new (junto com classes e módulos) também são suas portas de en-
trada para herança, que discutiremos com mais profundidade no próximo capítulo.

Uma classe para nosso classificador

Em seguida, converteremos nosso código em uma classe.Observe que nenhum de nossos


objetos é adequado Map porque ambos contêm vários atributos, incluindo funções. En-
tão, o que precisamos mudar para classificar nosso código? Não muito:

class Classifier {

  constructor(){

    this.songList = {

      allChords: new Set(),

      difficulties: ['easy', 'medium', 'hard'],

      songs: [],

      addSong(name, chords, difficulty){

        this.songs.push({name,

                        chords,

                        difficulty: this.difficulties[difficulty]});

      }

    };

    this.labelCounts = new Map();

    this.labelProbabilities = new Map();

    this.smoothing = 1.01;

  };

  chordCountForDifficulty(difficulty, testChord){

    return this.songList.songs.reduce((counter, song) => {

      if(song.difficulty === difficulty){

        counter += song.chords.filter(

          chord => chord === testChord

        ).length;

      }

      return counter;

    }, 0);

  };

...

A primeira linha pode parecer um pouco com uma definição de função, e sob o capô ela
é (embora isso esteja se tornando menos aparente à medida que as classes JS evoluem),
mas é um tipo especial de função.E como acontece com uma função, também podemos
atribuir uma expressão de classe a uma variável, assim:

const Classifier = class {

Quanto às outras mudanças em nosso código, as mudanças mais significativas na adap-


tação da sintaxe literal do objeto são (como quando usamos uma função construtora) ter
ponto e vírgula em vez de vírgulas. Caso contrário, nossas definições de função são as
mesmas. Propriedades que não sãofunções, no entanto, podem convenientementeser de-
finido dentro de uma constructor função e usar a this. sintaxe e o ponto e vírgula
no final.A constructor função é executada quando um novo objeto é criado e é res-
ponsável por atribuir propriedades .

A única outra mudança importante é como o construtor é chamado, que é idêntico ao


new padrão de função /constructor:

const classifier = new Classifier();


Lembre-se de que, se você não tiver nenhum argumento necessário para o construtor,
isso funcionará bem sem os parênteses:

const classifier = new Classifier;


FUNÇÕES ESTÁTICAS

Um utilitário que as classes oferecem é a capacidade de adicionar funções estáticas .Eles


são úteis se você tiver alguma função que não requer uma instância para ser útil. No
nosso caso, todas as funções temos referências this , um sinal claro de que nenhuma
delas pode ser estática. Se quiséssemos ser muito agressivos e extrair uma função está-
tica (que só é responsável pela divisão), poderíamos tentar desviar
likelihoodFromChord disso:

likelihoodFromChord(difficulty, chord){

  return this.chordCountForDifficulty(difficulty, chord)

/ this.songList.songs.length;

};

nestes:

likelihoodFromChord(difficulty, chord){

  return this.divide(this.chordCountForDifficulty(difficulty,

chord),

                     this.songList.songs.length);

};

divide(dividend, divisor){

  return dividend / divisor;

};

E como divide claramente não tem nada a ver com a função em si (o que é aparente
porque não há referências a this ), podemos torná-la estática e alterar a chamada para
refletir o novo contêiner da função (a classe, em vez da instância) :

likelihoodFromChord(difficulty, chord){

  return Classifier.divide(this.chordCountForDifficulty(difficulty, chord),

                      this.songList.songs.length);

};

static divide(dividend, divisor){

  return dividend / divisor;

};

Essa mudança é desnecessária, pois / faz o trabalho muito bem, mas as funções estáti-
cas são bastante fáceis de implementar. Sinta-se à vontade para reverter essas
alterações.
MAS AS AULAS NÃO SÃO RUINS?

“JavaScript não tem classes reais!Eles são apenas funções e protótipos e objetos por
baixo! É um truque. Não caia nessa! Sua verdadeira natureza é disfarçada por eles!”

Ou “JavaScript só foi construído em 10 dias ou algo assim, certo? É uma bagunça por
baixo, e agarrar-se à nova sintaxe é nossa única esperança de evitar pensar nas diferen-
ças entre .prototype , [[prototype]] , getPrototypeOf e __proto__ .”

Alternativamente, “as aulas são boas e úteis se forem executadas em sua plataforma e
sua equipe puder entendê-las”.

Além disso, à medida que mais recursos são adicionados às classes JavaScript (como atri-
butos privados reais, conforme discutido no Capítulo 5 ), eles começam a se parecer me-
nos com “açúcar sintático” e mais com uma construção única. Isso não invalida os argu-
mentos de preferência por composição de objetos ou programação funcional sobre OOP,
mas enfraquece o argumento “classes não são nada especiais”.

Escolhendo nossa API

Agora que nossa classe pareceestar embonitoboa forma, é hora de considerar nossa pró-
pria API. Em outras palavras, se alguém estivesse importando nosso código como um
módulo,o quefunções seriam públicas (acessíveis a elas) e quais seriam privadas? Como
nósComo sabemos do Capítulo 5 , a distinção privado/público é um pouco complicada
em JavaScript (no momento da redação deste artigo, “privado” atualmente significa obs-
curo ou inacessível, embora isso provavelmente mude), mas mesmo assim, podemos de-
terminar o ideal agora, mesmo antes de realizar a distinção totalmente em um módulo.
Abordaremos isso na próxima seção. Por enquanto, estamos apenas escolhendo quais
funções definitivamente queremos que sejam acessíveis. 

Existem três funções das classify quais precisamos ser públicas:

constructor
trainAll
classify

Além disso, nossa addSong função from songList precisa ser acessível. Por conveniên-
cia, vamos adicionar uma função ao classificador:

class Classifier  {

  constructor(){

...

  };

  addSong(name, chords, difficulty){

    this.songList.addSong(name, chords, difficulty);

  };

...

E O TREM?

Atualmente, nossa API depende de seguir o padrão de adicionar músicas e treiná-las todas de uma
vez.Infelizmente, por causaos efeitos colaterais que temos, nosso código não é idempotente (um
conceito abordado no Capítulo 11 ). Em outras palavras, nossas funções não são “puras”, por causa
de seus efeitos colaterais, e executá-las em uma ordem diferente da especificada (ou várias vezes)
não é bem tratada.

Ou seja, train não executa setLabelProbabilities , e se setLabelProbabilities estivesse


vinculado a train em vez de trainAll , nossos testes seriam interrompidos.

Este é um problema que não resolveremos neste capítulo, mas há uma versão idempotente (e fun-
cional) desta NBC no Capítulo 11 .

Agora podemos chamar a função diretamente do classificador, o que significa que as


chamadas em nossos testes são assim:

classifier.songList.addSong('imagine',

['c', 'cmaj7', 'f', 'am', 'dm', 'g', 'e7'], 0);

classifier.songList.addSong('somewhereOverTheRainbow',

['c', 'em', 'f', 'g', 'am'], 0);

pode se transformar nisso:

classifier.addSong('imagine',

['c', 'cmaj7', 'f', 'am', 'dm', 'g', 'e7'], 0);

classifier.addSong('somewhereOverTheRainbow',

['c', 'em', 'f', 'g', 'am'], 0);

Além disso, como nossa função no classificador se tornou pura delegação, não precisa-
mos mais ser tão específicos sobre os parâmetros:

class Classifier  {

  constructor(){

...

  };

  addSong(...songParams){ // rest

    this.songList.addSong(...songParams); // spread

  };

...

O ...songParams na funçãodefinição éconhecido como sintaxe de parâmetro rest , e


usar um estilo semelhante em uma chamada de função éconhecido como usando o ope-
rador spread . É difícil manter esses termos corretos, mas Chris Deely, um dos revisores
técnicos deste livro, sugeriu este mnemônico: resto é para “receber”, spread é para “en-
viar”. Isso funciona para mim. (Obrigado, Cris.)

A sintaxe do parâmetro rest pega todos os argumentos que você fornece e os transforma
em uma matriz. O operador spread faz o oposto, dividindo o array que você fornece em
argumentos individuais para passar para a função.

A razão pela qual este é um bom padrão para aplicar nesta situação é que já temos uma
definição de função para addSong dentro de songList . Se decidirmos alterar a função
em termos de quais argumentos ela aceita, seria bom não ter que alterar essa função
também. Usar descanso e dispersão aqui nos dá essa flexibilidade.

A propósito, se descobrirmos que na verdade songList estava fazendo todo o trabalho,


e classifier estava simplesmente delegando todas as funções para ele, songList se-
ria bom considerar remover essas funções de delegação e deixar os testes (ou código cli-
ente que usa nosso módulo) chamarem as versões diretamente. . Ter apenas um objeto
com o qual os clientes interagem é bom para eles, mas se nossa delegação estiver adicio-
nando volume suficiente, devemos repensar o design.

Tempo para um pouco de privacidade?

Como vimos no Capítulo 5 , se quisermosprivacidade falsaem uma classe, podemos ape-


nas adicionar um sublinhado ( _ ) na frente de todas as propriedadesqueremos ser pri-
vados. esteA convenção permite que as pessoas que usam a API saibam que estão em ter-
ritório estranho se estiverem abordando essas propriedades diretamente (às vezes, os
aspectos privados/internos de uma API são chamados de “encanamento” em oposição à
“porcelana”).

Felizmente, seu editor tem uma maneira de renomear facilmente as coisas (encontrar e
substituir não apenas um arquivo, mas todo o projeto). Estes são os rótulos aos quais
você deve adicionar um sublinhado:
songList
labelCounts
labelProbabilities
smoothing
chordCountForDifficulty
likelihoodFromChord
valueForChordDifficulty
train
setLabelProbabilities

Agora temos um caminho extremamente simples para exportar nossa classe como um
módulo.Vamos dividir nossos testes e arquivo principal para provar isso. Primeiro, te-
mos algumas alterações a fazer em nb.js :

module.exports = class Classifier {

  constructor(){

    this._songList = {

      allChords: new Set(),

      difficulties: ['easy', 'medium', 'hard'],

      songs: [],

      addSong(name, chords, difficulty){

        this.songs.push({name,

                        chords,

                        difficulty: this.difficulties[difficulty]});

      }

    };

    this._labelCounts = new Map();

    this._labelProbabilities = new Map();

    this._smoothing = 1.01;

  };

  addSong(...songParams){

    this._songList.addSong(...songParams);

  };

...

  classify(chords){

    return new Map(Array.from(

      this._labelProbabilities.entries()).map(

(labelWithProbability) => {

      const difficulty = labelWithProbability[0];

      return [difficulty, chords.reduce((total, chord) => {

        return total * this._valueForChordDifficulty(difficulty,

chord);

      }, this._labelProbabilities.get(difficulty) + this._smoothing)];

    }));

  }

};

Fizemos apenas duas alterações aqui. A primeira é que a primeira linha agora tem isso
no início:

module.exports =

A segunda é que estamos movendo todos os nossos testes para um arquivo separado (no
mesmo diretório) chamado nb_test.js :

const Classifier = require('./nb.js');

const wish = require('wish');

describe('the file', () => {

  const classifier = new Classifier;

  classifier.addSong('imagine',

['c', 'cmaj7', 'f', 'am', 'dm', 'g', 'e7'], 0);

...

  it('label probabilities', () => {

    wish(classifier._labelProbabilities.get('easy') ===

0.3333333333333333);

    wish(classifier._labelProbabilities.get('medium') ===

0.3333333333333333);

    wish(classifier._labelProbabilities.get('hard') ===

0.3333333333333333);

  });

});

Fizemos apenas uma alteração em nossos testes, e está no topo. Se você executar mocha
nb_test.js , não deverá ter falhas. Incrível.

Mas você quer privacidade real? Por enquanto, você tem algumas opções. A primeira é
seguir a rota do “padrão de módulo revelador”, mudara classe de volta em uma função
construtora, e fazer algumas mudanças sérias.Ou, você pode tentar algumas coisas mais
complicadas e esotéricas com s do ES2015 Symbol (criando privacidade através da obs-
curidade) ou WeakMap s, ou usando uma função de inicialização (que é basicamente pro-
jetar sua própria forma do padrão de módulo revelador).

Se pensarmos honestamente sobre os trade-offs aqui, o que você está ganhando fazendo
algo complexo e fora do padrão? Você está fazendo mais trabalho para você e outros que
podem trabalhar em seu módulo. O próprio código e os testes se tornam mais comple-
xos. Trocar sua capacidade de chamar testes por funções verdadeiramente privadas e
não endereçáveis ​parece um absurdo.

Quais são os riscos de seguir a rota do sublinhado? Alguns caracteres extras aqui e ali
tornarão seu código feio? As pessoas vão pensar que você é burro demais para usar as
soluções alternativas mais recentes e confusas? As pessoas que usam seu módulo insisti-
rão em chamar essas funções, mesmo que para os não iniciados você tenha indicado em
sua documentação que não deveriam?

Se o seu programa já está vivendo na função construtora cidade, esta é uma chamada
mais difícil, mas se você estiver usando classes, adicionar sublinhados parece ser a solu-
ção muito mais fácil.

Dito tudo isso, parece que campos e métodos privados podem vir em breve. A especifica-
ção ainda está em andamento até o momento, mas a ideia básica é que as referências a
funções e atributos privados usariam um # em vez de um _ , e seriam acessíveis apenas
de dentro da definição de classe. Consulte o Capítulo 5 para obter um exemplo de como
pode ser. 

Adaptando o classificador a um novo domínio de problema

E agora para o último tópico paraeste programa: e se, em vez de músicas com acordes,
estivermos trabalhando com aprendizado de vocabulário, e em vez de “fácil”, “médio” e
“difícil”, estivermos classificando um corpus de texto como “compreendido” ou “não
entendido”?

Algumas pessoas (tanto codificadores quanto não codificadores), ao pensar em uma abs-
tração potencial como essa, pularão para o caso geral imediatamente.Pense em abstrair
uma NBC geral com o código desde o início versus o código agora. Pessoalmente, reco-
mendo construir dois ou três tipos de algo (mesmo algo tão pequeno quanto uma fun-
ção) antes de insistir que são semelhantes e tentar abstraí-los.

Agora que nosso código está em melhor forma e nosso programa é relativamente pe-
queno, é fácil tentar adaptá-lo a um novo domínio.

Principalmente, tudo o que precisamos fazer é renomear muitos objetos:

module.exports = class Classifier {

  constructor(){

    this._textList = {

      allWords: new Set(),

      understood: ['yes', 'no'],

      texts: [],

      addText(name, words, comprehension){

        this.texts.push({name, words,

comprehension: this.understood[comprehension]});

      }

    };

    this._labelCounts = new Map();

    this._labelProbabilities = new Map();

    this._smoothing = 1.01;

  };

  addText(...textParams){

    this._textList.addText(...textParams);

  };

  _wordCountForComprehension(comprehension, testWord){

    return this._textList.texts.reduce((counter, text) => {

      if(text.comprehension === comprehension){

        counter += text.words.filter(

          word => word === testWord

        ).length;

      }

      return counter;

    }, 0);

  };

  _likelihoodFromWord(comprehension, word){

    return this._wordCountForComprehension(comprehension, word) /

this._textList.texts.length;

  };

  _valueForWordComprehension(comprehension, word){

    const value = this._likelihoodFromWord(comprehension, word);

    return value ? value + this._smoothing : 1;

  };

  trainAll(){

    this._textList.texts.forEach((text) => {

      this._train(text.words, text.comprehension);

    });

    this._setLabelProbabilities();

  };

  _train(words, label){

    words.forEach(word => this._textList.allWords.add(word) );

    if(Array.from(this._labelCounts.keys()).includes(label)){

      this._labelCounts.set(label, this._labelCounts.get(label) + 1);


    } else {

      this._labelCounts.set(label, 1);

    }

  };

  _setLabelProbabilities(){

    this._labelCounts.forEach((_count, label) => {

      this._labelProbabilities.set(label,

this._labelCounts.get(label) / this._textList.texts.length);

    });

  };

  classify(words){

    return new Map(Array.from(

      this._labelProbabilities.entries()).map(

(labelWithProbability) => {

      const comprehension = labelWithProbability[0];

      return [comprehension, words.reduce((total, word) => {

        return total * this._valueForWordComprehension(comprehension,

word);

      }, this._labelProbabilities.get(comprehension) +

this._smoothing)];

    }));

  }

};

E aqui estão os testes:

Classifier = require('./nb_new_domain.js');

const wish = require('wish');

describe('the file', () => {

  const classifier = new Classifier;

  classifier.addText('english text',

                     ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i',

                      'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q'],

                      0);

  classifier.addText('japanese text',

あ い
                     [' ',     ' ',    'う',     'え',     'お',

                       'か',     'き',    'く',    'け',    'こ'],

                       1);

  classifier.trainAll();

  it('classifies', () =>{

お は よ う
    const classified = classifier.classify([' ', ' ', ' ', ' ', ' ご', 'ざ',
い ま す
' ', ' ', ' ']);

    wish(classified.get('yes') === 1.51);

    wish(classified.get('no') === 5.19885601);

  });

  it('number of words', ()=>{

    wish(classifier._textList.allWords.size === 27);

  });

  it('label probabilities', ()=>{

    wish(classifier._labelProbabilities.get('yes') === 0.5);

    wish(classifier._labelProbabilities.get('no') === 0.5);

  });

});

Com base na entrada anterior, os testes mostram que o japonês provavelmente será in-
compreensível para alguém que classificou os dados de treinamento originais. Se quisés-
semos ir mais longe com este classificador de texto, poderíamos estar interessados ​em
processar o texto de forma mais completa. Isso pode significar por caractere/letra, por
palavra, por frase ou aprofundar-se nas gramáticas dos idiomas de destino com técnicas
como stemming . Da mesma forma, com a versão musical, poderíamos ter considerado
sequências de acordes (as transições podem ser mais difíceis do que os próprios acor-
des). Também é possível que um leitor de ambas as línguas possa ter um limiar diferente
de compreensão para um vocabulário específico (por exemplo, partes de uma motoci-
cleta), em vez de classificar tudo em um idioma como incompreensível.

Novamente (como fizemos com “fácil”, “médio”, “difícil”), estamos parando um passo an-
tes que nosso classificador imprima “sim/não” ou “japonês/inglês” (podemos treinar
para qualquer um dos cenários), então apenas escolhemos a categoria com o maior nú-
mero associado. Sinta-se à vontade para adicionar um teste e implementar esse recurso
se estiver se sentindo ambicioso.

Embora esses sejam todos os recursos a serem adicionados,nosso objetivo neste capítulo
— melhorar a qualidade do código — foi alcançado, então estamos prontos.Confira o Ca-
pítulo 11 para obter uma versão funcional deste código. 

Empacotando

Nissocapítulo (e Capítulo 6 ), cobrimos uma grande variedade de técnicas gerais de refa-


toração.No restante do livro, veremos estilos mais especializados baseados nos paradig-
mas que o JavaScript fornece, incluindo programação orientada a objetos, programação
funcional e programação assíncrona.

Apoiar Sair
© 2022 O'REILLY MEDIA, INC.  TERMOS DE SERVIÇO POLÍTICA DE PRIVACIDADE

Você também pode gostar