← ← ← 10/21/2022, 11:56:40 AM | Posted by: Felippe Regazio
Imagine que estou apostando corrida de carrinhos Hot Wheels com meu filho, e preciso de uma função JS pra registrar o placar.
O problema é que eu só tenho o Internet Explorer de 2010 pra fazer essa função, então tenho algumas regras
O comportamento esperado é:
var score = scoreboard({ felippe: 0, lorenzo: 0 });
score();
output:
Felippe: 0
Lorenzo: 0
Para marcar pontos precisaremos de um metodo
score.point('felippe');
score();
output:
Felippe: 1
Lorenzo: 0
E eu posso marcar mais de um ponto se preciso, basta usar um segundo parametro para incrementar mais de um ponto
score.point('lorenzo', 3);
score();
output:
Felippe: 1
Lorenzo: 3
E a qualquer momento da brincadeira nós podemos adicionar novas pessoas:
score.add('flavia');
score();
output:
Felippe: 1
Lorenzo: 3
Flavia: 0
E também podemos adicionar essa pessoa com uma pontuação inicial se preciso:
score.add('Joana', 2);
score();
output:
Felippe: 1
Lorenzo: 3
Flavia: 0
Joana: 2
Eu posso inicializar o score com mais de 2 pessoas mas nunca somente com uma
Eu não posso somar pontos para uma pessoa que não existe no placar
Você pode ter uma única função global, mas pode ter quantas funções quiser dentro dela
As manipulações que requerem que nomes sejam passados devem ser case INSENSITIVE, e o output deve ser escrito em lowercase com a primeira letra do nome maiúscula.
ex:
score.point('FeLiPPe'); // ponto pra felippe
https://stackoverflow.com/questions/690251/what-happened-to-console-log-in-ie8
Boa sorte!
Primeiramente temos que considerar que faremos um código vanilla JS para IE10. Sendo assim muitas das funcionalidades modernas e syntax sugar do JS não estarão mais a mão. A primeira coisa que perdemos ao pensar logo de cara no código: classes
. Mas peraí, classes
em JS são em grande parte syntax sugar para funções. Então podemos começar criando nossa main function:
function scoreboard(initialPeople) {
//
}
De acordo com a especificação do desafio, a função scoreboard
recebe um objeto com a estrutura { [key: string]: number }
e retorna uma nova função que controla este objeto. Portanto scoreboard
é uma HOF: Higher Order Function
. Funções que manipulam outras funções (seja recebendo como parametro ou retornando funções) são chamadas de HOF
. Em JavaScript quando você cria uma dessas, o escopo da função mãe é preservado mesmo após retornamos outra função, e é dessa tecnica que vamos nos beneficiar. Isso significa o seguinte:
function scoreboard(initialPeople) {
var value = 0;
return function(n) {
value += (n || 0);
console.log("Value is now: "+value);
}
}
No exemplo acima nós declaramos um valor no escopo de scoreboard
e retornamos uma função que manipula esse valor. Se executarmos esse código, teremos o seguinte comportamento:
var score = scoreboard({});
// como scoreboard retorna uma função, score é uma function
score();
// Value is now: 0
score(3);
// Value is now: 3
score(2);
// Value is now: 5
score(10);
// Value is now: 15
Como observado, scoreboard é uma HOF que retorna uma função. A função retornada permanece ligada ao escopo da função mãe. Assim podemos manipular value
de forma cumulativa, pois o escopo é preservado. Isso é justamente a ideia central da nossa scoreboard
.
Pensando nisso, vamos então fazer um dos primeiros requisitos do nosso desafio: a função que adiciona uma pessoa nova ao jogo. Basicamente significa que vamos adicionar uma nova chave com um novo valor ao que foi passado em initialPeople
. Como sabemos o funcionamento de uma HOF, isso agora fica fácil. Os passos são:
function scoreboard(initialPeople) {
var people = initialPeople;
return function(person, points) {
if (people[person]) {
console.log("A person named '" + person + "' is already playing.");
return false;
}
people[person] = (points || 0);
}
}
Está feita nossa função. Veja que assinamos os parametros em initialPeople
para uma nova variável people
. Fazemos isso porque vamos modificar os valores originais, e é uma boa prática não modificar diretamente os parametros da função.
Depois disso verificamos se existe uma chave com o nome passado em nosso objeto, se sim, avisamos e não fazemos nada. Se não, adicionamos a chave com um novo valor ao objeto. Se nenhum ponto foi passado, a pessoa começa com 0. Se testarmos:
var score = scoreboard({});
score('Felippe', 1);
Bom, de cara você já deve ter visto o problema: a notação da nossa função está errada. Se voltarmos aos requisitos do desafio, ele espera que para adicionar uma nova pessoa façamos o seguinte:
score.add('flavia');
score.add('Joana', 2);
Para fazermos isso precisamos saber de outra questão conceitual: funções são objetos em javascript. Um objeto pode conter uma chave e um valor, logo uma função também. Quando aplicarmos uma chave e um valor a uma função como se ela fosse um objeto, chamamos esse par chave: valor
de propriedade. Por exemplo, se quisermos adicionar uma propriedade a nossa função score precisamos antes:
function scoreboard(initialPeople) {
var people = initialPeople;
function score(person, points) {
return "fizz";
}
score.customProp = 1;
return score;
}
Acima então criamos a função score
que retorna "fizz", e adicionamos a ela a propriedade customProp
cuja chave é homonima (customProp) e o valor é '1'. Veja:
var score = scoreboard({});
score(); // -> "fizz"
console.log(score.customProp); // 1
Podemos assinar diferentes tipos de valores a uma propriedade, inclusive uma função. Quando criamos uma propriedade em uma função e o valor dessa propriedade será outra função, nós temos um método
:
function scoreboard(initialPeople) {
var people = initialPeople;
function score(person, points) {
return "fizz";
}
score.myMethod = function() {
return "buzz"
};
return score;
}
Acima nossa função score retorna fizz
, e possui o método myMethod
que retorna buzz
:
var score = scoreboard({});
score(); // -> "fizz"
score.myMethod(); // -> "buzz"
Sabendo disso, agora podemos arrumar nossa função scoreboard
para ter o método add
que adiciona pessoas ao objeto inicial:
function scoreboard(initialPeople) {
var people = initialPeople;
function score() {
//
}
score.add = function(person, points) {
if (people[person]) {
console.log("A person named '" + person + "' is already playing.");
return false;
}
people[person] = (points || 0);
}
return score;
}
Pronto, agora temos um método add. Mas ainda temos dois problemas: verificações diretas como people[person]
poderiam ser inseguras em determinadas versões do IE. Como não sabemos se o IE sofre disso mas somos precavidos, vamos criar um método que verifica se uma chave existe em dado objeto e usar ele pra fazer essa verificação em add
. Como antigamente também não tínhamos a global Object
para manipularmos objetos, vamos ter que usar o método hasOwnProperty
diretamente. O método hasOwnProperty
verifica justamente se determinada propriedade existe em um determinado objeto.
function scoreboard(initialPeople) {
var people = initialPeople;
function keyExists(key, obj) {
return hasOwnProperty.call(obj, key);
}
function score() {
//
}
score.add = function(person, points) {
if (keyExists(person, people)) {
console.log("A person named '" + person + "' is already playing.");
return false;
}
people[person] = (points || 0);
}
return score;
}
E temos um segundo problema: Algumas versões do IE só permitiam que usássemos console.log
quando o inspector estivesse aberto no browser, do contrário a função não existiria no escopo global gerando um erro no código. Pra mitigar esse problema podemos criar uma função log
nossa que faz um try
. Ou seja: ela tenta usar console.log
, se der tudo bem (é porque o inspector estava aberto), mas se não der tudo bem também hahaha (IE Rules):
function scoreboard(initialPeople) {
var people = initialPeople;
function log(s) {
try { console.log(s) } catch {};
}
function keyExists(key, obj) {
return hasOwnProperty.call(obj, key);
}
function score() {
//
}
score.add = function(person, points) {
if (keyExists(person, people)) {
log("A person named '" + person + "' is already playing.");
return false;
}
people[person] = (points || 0);
}
return score;
}
Pronto, ufa! Agora parece estar tudo certo. Temos nosso método add
e podemos testá-lo:
var score = scoreboard({});
score(); // void
score.add("felippe", 2); // void
Mas como vamos saber se está tudo funcionando sem ver como está nosso objeto {people}
? Poderíamos usar a nossa função log
pra inspecionar, mas seria melhor se tivéssemos mesmo uma função que mostra o placar, né? Problema é que quando chamamos a função score()
, ela que deveria mostrar o placar ainda não faz nada. Então vamos fazer a função que mostra o placar:
Para mostrar o placar nós precisaremos:
Ou seja, precisaremos de iteração. Iteração é a arte de passar item por item de um dado conjunto. No nosso caso vamos passar item por item do nosso objeto. Se estivessemos numa versão mais recente de JavaScript poderíamos utilizar Object.keys
, Object.values
, Object.entries
, etc. para iterar propriedades de um objeto. Porém não estamos, então teremos que escrever nosso próprio iterador.
Para criar nosso iterador utilizaremos o loop for in
, que é compatível com IE:
function scoreboard(initialPeople) {
/* (...) */
function objEntries(obj, callback) {
for (key in obj) {
if (keyExists(key, obj)) {
callback(key, obj[key]);
}
}
}
/* (...) */
return score;
}
Observe que no código acima criamos a função objEntries
. Esta função passa item por item de um objeto, assegura que a chave realmente existe e chama um callback passando chave, valor
como argumentos. Ou seja: é basicamente um forEach
. Vamos então utiliza-la para printar nosso objeto como solicitado na especificação do desafio:
function scoreboard(initialPeople) {
var people = initialPeople;
function log(s) {
try { console.log(s) } catch {};
}
function keyExists(key, obj) {
return hasOwnProperty.call(obj, key);
}
function objEntries(obj, callback) {
for (key in obj) {
if (keyExists(key, obj)) {
callback(key, obj[key]);
}
}
}
function score() {
objEntries(people, function(key, value) {
log(key + ": " + value);
});
}
score.add = function(person, points) {
if (keyExists(person, people)) {
log("A person named '" + person + "' is already playing.");
return false;
}
people[person] = (points || 0);
}
return score;
}
O que dissemos na função score então é o seguinte: para cada key: value
no objeto people
execute um console.log
. A saída seria:
var score = scoreboard({ felippe: 0 });
score.add("lorenzo", 2);
score();
// felippe: 0
// lorenzo: 2
Ótimo, agora podemos ver nossas funções em ação. Mas se observarmos, os nomes estão no padrão que foram escritos na chave do objeto. O ideal seria se normalizássemos para o padrão: Primeira letra maiúscula, o resto em minúsculo.
Vamos agora fazer uma função que converte uma string para o padrão "Primeira letra maiúscula, o resto em minúscula`, ou seja: Capitaliza uma string.
Pode não parecer mas nesse caso temos duas tarefas, e seria ideal que usassemos funções diferentes para cada uma delas.
Então mãos a obra. Faremos primeiro a função que converte uma string para lowercase:
function lc(str) {
if (!str) return "";
return str.toLowerCase();
}
Simples assim: veríficamos se a string existe, se não retorna string vazia, do contrário chama toLowerCase
. Basicamente criamos uma sintax sugar. Agora vamos usar isso para criar uma função ucFirst
, que capitalizará uma string pra nós:
function ucFirst(str) {
if (!str) return "";
return str.charAt(0).toUpperCase() + lc(str.slice(1));
}
Pronto. Utilizamos charAt
para compatibilidade com IE. Pegamos o caractere de index zero (primeiro caractere) e passamos pra uppercase, e utilizamos nossa lc para noramlizar o resto pra lower case.
Agora vamos acoplar nossas funções na nossa lib, e usar elas pra normalizar o output do método score
:
function scoreboard(initialPeople) {
var people = initialPeople;
function log(s) {
try { console.log(s) } catch {};
}
function keyExists(key, obj) {
return hasOwnProperty.call(obj, key);
}
function objEntries(obj, callback) {
for (key in obj) {
if (keyExists(key, obj)) {
callback(key, obj[key]);
}
}
}
function lc(str) {
if (!str) return "";
return str.toLowerCase();
}
function ucFirst(str) {
if (!str) return "";
return str.charAt(0).toUpperCase() + lc(str.slice(1));
}
function score() {
objEntries(people, function(key, value) {
log(ucFirst(key) + ": " + value);
});
}
score.add = function(person, points) {
if (keyExists(person, people)) {
log("A person named '" + ucFirst(person) + "' is already playing.");
return false;
}
people[person] = (points || 0);
}
return score;
}
Observe acima que adicionamos as duas funções de manipulação de strings ao nosso modulo, e depois chamamos a função ucFirst
na saída do método score
. Agora temos:
var score = scoreboard({ felippe: 0 });
score.add("lorenzo", 2);
score();
// Felippe: 0
// Lorenzo: 2
Não importa o quão fora de padrão um nome seja adicionado a nossa coleção de pessoas, a saída dele será sempre normalizada.
Se você olhar para o código que escrevemos até aqui, perceberá que temos funções de duas naturezas diferentes nele:
A primeira opção damos o nome de métodos privados
. Alguém utilizando nossa lib não conseguiria acesso fácil as funções internas delas, como por exemplo as recém criadas lc
e ucFirst
. Estas funções dizem respeito ao maquinário interno do nosso módulo e não tem porque alguém acessá-las.
Por outro lado temos a função score
que é retornada, e que possui varios métodos próprios. Esta por sua vez é uma função publica. Também é comum chamar esse tipo de retorno de API Pública do Módulo
. Dizemos isso porque é através dessa função que o módulo oferece funcionalidades e são essas funções que estarão a mão para o consumer.
Para facilitar a compreensão e melhorar a organização do nosso código, vamos adicionar um comentário que demarcará uma fronteira entre privado x público
function scoreboard(initialPeople) {
var people = initialPeople;
function log(s) {
try { console.log(s) } catch {};
}
function keyExists(key, obj) {
return hasOwnProperty.call(obj, key);
}
function objEntries(obj, callback) {
for (key in obj) {
if (keyExists(key, obj)) {
callback(key, obj[key]);
}
}
}
function lc(str) {
if (!str) return "";
return str.toLowerCase();
}
function ucFirst(str) {
if (!str) return "";
return str.charAt(0).toUpperCase() + lc(str.slice(1));
}
// ------ Public ------
function score() {
objEntries(people, function(key, value) {
log(ucFirst(key) + ": " + value);
});
}
score.add = function(person, points) {
if (keyExists(person, people)) {
log("A person named '" + ucFirst(person) + "' is already playing.");
return false;
}
people[person] = (points || 0);
}
return score;
}
Agora vamos fazer uma função que adiciona pontos para uma pessoa. Se observarmos, essa função é uma variação simples do nosso método de add, porém os concerns dela são:
Vamos adicionar logo após a função score.add
uma nova função: score.point
. Logo temos:
score.point = function(person, points) {
if (!keyExists(person, people)) {
log("There is no person named '" + ucFirst(person) + "' in the game.");
return false;
}
var newValue = (people[person] || 0) + (points || 1);
people[person] = newValue;
}
Veja que a execução é bem simples: Se a pessoa recebendo pontos não existir, avisa e não faz nada. Se a pessoa existir pega os pontos atuais dela e soma aos novos pontos passados no argumento points
. Se não foi passado um valor para points, soma 1 ponto. Pega os novos pontos e assina como sendo o novo valor para a pessoa no objeto people
.
Para testar:
var score = scoreboard({ felippe: 0 });
score.add("lorenzo", 2);
score();
// Felippe: 0
// Lorenzo: 2
score.point('felippe', 1);
score();
// Felippe: 0
// Lorenzo: 2
score.point('joana', 3);
// There is no person named 'Joana' in the game.
A esta altura já concluímos quase todos os requisitos do desafio. Então temos duas pendencias de bonus para concluir:
scoreboard
com zero ou uma pessoaA ideia aqui é que as operações com nomes sejam case insensitive para que evitemos nomes duplicados escritos de forma diferente, por ex:
Felippe
FeLIPPE
FELIPPE
feliPpe
Para a nossa lib, todos os nomes acima devem ser o mesmo: felippe
. Para isso temos que seguir um princípio: Todo input e comparação entre strings devem ser normalizadas. Você pode normalizar como quiser desde que sempre utilize a mesma normalização. No nosso caso vamos usar lowercase
, e temos 3 pontos mapeados onde devemos normalizar nossas strings de forma a deixar a lib case-insenstive:
initialPeople
point
add
Ou seja: não importa como o usuário passa um nome para nós, este nome sempre vai ser salvo e manipulado em lower-case, dessa forma garantimos que tudo estará case-insenstive.
Para nossa felicidade nós já escrevemos um método que converte uma String pra lowercase, o lc
. Então podemos utilizá-lo no item 1 (método point) e 2 (método add) da nossa lista. Lembre-se de não manipular o parametro original da função.
Warn: nem sempre é uma boa ideia mutar objetos passados como parametro mesmo que não seja diretamente. Isso porque existe uma ideia cognitiva associada a eles. Esse exercício teve um caráter mais pedagógico (e também teve algum sentido par ao que estamos fazendo). Mas não tome esse tipo de prática como regra, entenda que esse tipo de coisa deve ser analisada com cautela e raramente é necessária.
Até aqui nossa lib está assim:
function scoreboard(initialPeople) {
var people = initialPeople;
function log(s) {
try { console.log(s) } catch {};
}
function keyExists(key, obj) {
return hasOwnProperty.call(obj, key);
}
function objEntries(obj, callback) {
for (key in obj) {
if (keyExists(key, obj)) {
callback(key, obj[key]);
}
}
}
function lc(str) {
if (!str) return "";
return str.toLowerCase();
}
function ucFirst(str) {
if (!str) return "";
return str.charAt(0).toUpperCase() + lc(str.slice(1));
}
// ------ Public ------
function score() {
objEntries(people, function(key, value) {
log(ucFirst(key) + ": " + value);
});
}
score.add = function(person, points) {
if (keyExists(person, people)) {
log("A person named '" + ucFirst(person) + "' is already playing.");
return false;
}
people[person] = (points || 0);
}
score.point = function(person, points) {
if (!keyExists(person, people)) {
log("There is no person named '" + ucFirst(person) + "' in the game.");
return false;
}
var newValue = (people[person] || 0) + (points || 1);
people[person] = newValue;
}
return score;
}
Vamos refatorar o método add
para:
score.add = function(person, points) {
var lcPerson = lc(person);
if (keyExists(lcPerson, people)) {
log("A person named '" + ucFirst(person) + "' is already playing.");
return false;
}
people[lcPerson] = (points || 0);
}
E o método point
para:
score.point = function(person, points) {
var lcPerson = lc(person);
if (!keyExists(lcPerson, people)) {
log("There is no person named '" + ucFirst(person) + "' in the game.");
return false;
}
var newValue = (people[lcPerson] || 0) + (points || 1);
people[lcPerson] = newValue;
}
Bom, como vc pôde ver, todos os nossos dados de input estão normalizados, com exceção do objeto initialPeople
. Isso porque teremos que tratar ele de forma especial: vamos iterar esse objeto gerando um novo objeto normalizado.
initialPeople
Até aqui, nosso objeto people (o qual é manipulado por nossa lib) é inicialmente derivado diretamente do dado que o consumer passou pra nós:
function scoreboard(initialPeople) {
var people = initialPeople;
/** ... **/
}
Neste ponto, o que queremos que aconteça é: a variavel people deve conter os dados de inicialPeople, porém com todas as keys (nomes de pessoas) normalizadas. Pra isso vamos criar uma função que itera o objeto e converte as keys para lower case, retornando um novo objeto:
function objKeysToLower(obj) {
var result = {};
objEntries(obj, function(key, value) {
result[lc(key)] = value;
});
return result;
}
Pronto, agora basta utilizar nosso novo método para normalizar o valor em people
, que deriva de initialPeople
:
function scoreboard(initialPeople) {
var people = objKeysToLower(initialPeople);
/** ... **/
}
Pronto, agora não importa como o consumer insira os dados, nossa lib vai sempre lidar com eles em formato lowercase.
initialPeople
Agora nos resta uma ultima pendencia: validar existe 2 ou mais pessoas passadas inicialmente ao iicializar nossa lib. Para isso vamos fazer nosso último método que conta o número de chaves em um Objeto. Note que esta função é uma variação da função objKeysToLower
, mas invés de gerar um novo objeto, nós geramos um array com as keys do objeto atual ou um array vazio caso o parameto não seja um objeto;
function objKeys(obj) {
// Este if utiliza uma tecnica antiga para
// assegurar que um valor é um objeto e não é um Array
if (typeof obj !== 'object' || hasOwnProperty.call(obj, 'length')) {
return [];
}
var result = [];
objEntries(obj, function(key) {
result.push([lc(key)]);
});
return result;
}
Pronto, agora antes de inicializar nossa lib, nós validamos se temos um initialPeople
valido:
function scoreboard(initialPeople) {
if (objKeys(initialPeople).length <= 1) {
throw Error('You need at least two people to start');
}
var people = objKeysToLower(initialPeople);
/** ... **/
}
Se tivermos um parametro inicial inválido, lançamos um erro.
Pronto, nossa lib está feita e contempla todos os requisitos passados pelo desafio:
function scoreboard(initialPeople) {
if (objKeys(initialPeople).length <= 1) {
throw Error('You need at least two people to start');
}
var people = objKeysToLower(initialPeople);
function log(s) {
try { console.log(s) } catch {};
}
function keyExists(key, obj) {
return hasOwnProperty.call(obj, key);
}
function objEntries(obj, callback) {
for (key in obj) {
if (keyExists(key, obj)) {
callback(key, obj[key]);
}
}
}
function lc(str) {
if (!str) return "";
return str.toLowerCase();
}
function ucFirst(str) {
if (!str) return "";
return str.charAt(0).toUpperCase() + lc(str.slice(1));
}
function objKeysToLower(obj) {
var result = {};
objEntries(obj, function(key, value) {
result[lc(key)] = value;
});
return result;
}
function objKeys(obj) {
if (typeof obj !== 'object' || hasOwnProperty.call(obj, 'length')) {
return [];
}
var result = [];
objEntries(obj, function(key) {
result.push([lc(key)]);
});
return result;
}
// ------ Public ------
function score() {
objEntries(people, function(key, value) {
log(ucFirst(key) + ": " + value);
});
}
score.add = function(person, points) {
var lcPerson = lc(person);
if (keyExists(lcPerson, people)) {
log("A person named '" + ucFirst(person) + "' is already playing.");
return false;
}
people[lcPerson] = (points || 0);
}
score.point = function(person, points) {
var lcPerson = lc(person);
if (!keyExists(lcPerson, people)) {
log("There is no person named '" + ucFirst(person) + "' in the game.");
return false;
}
var newValue = (people[lcPerson] || 0) + (points || 1);
people[lcPerson] = newValue;
}
return score;
}
Você pode rodar os casos abaixo para testar:
var score = scoreboard({ felippe: 0, lorenzo: 0 });
score.point('felippe');
score.point('lorenzo', 2);
score.add('flavia');
score.add('joana', 3);
score.point('fLaViA', 3);
score();
// Felippe: 1
// Lorenzo: 2
// Flavia: 3
// Joana: 3
Você pode rodar os casos baixo para testar verificações de error:
var score = scoreboard({});
// VM394:3 Uncaught Error: You need at least two people to start
// at scoreboard (<anonymous>:3:11)
// at <anonymous>:1:15
var score = scoreboard({ felippe: 0 });
// VM441:3 Uncaught Error: You need at least two people to start
// at scoreboard (<anonymous>:3:11)
// at <anonymous>:1:15
var score = scoreboard({ felippe: 0, lorenzo: 0 });
score.point('joana', 10)
// VM472:9 There is no person named 'Joana' in the game.
// false
Embora bastante funcional, nosso código ainda possui infinitas imperfeições que você pode querer ou não arrumar. Porém o importante aqui é que ele cumpriu um papel principal: Ser didático. Como é um código de complexidade média, existe ainda margem para muita refatoração. Sinta-se livre pra brincar com isso.
A ideia aqui nunca foi escrever um código pra IE na verdade, essa era apenas uma desculpa para examinarmos conceitos e práticas de base. Não importa se o código roda no IE ou não porque o IE morreu, o que importa é que escrevendo esse código com essa premissa você foi forçado/a a verificar como funciona cada aspecto do que você estava fazendo. Logo você aprendeu muitas coisas novas sobre o que você já sabia.
Ninguém precisa saber escrever código assim hoje em dia, e mesmo se souber, ainda utilizaríamos os recursos mais recentes ainda assim. Porém, as tecnicas e conceitos demonstrados aqui podem ser utilizados em qualquer código, em qualquer aplicação. Por exemplo: depois desse exercício você não olhará para uma classe da mesma forma nunca mais, você saberá a dinamica entre Objetos privados e públicos dessa classe, propriedades e a relação delas com seu escopo. Você olhará para componentes React sabendo que são Higher Order Functions dentro de outras Higher Order Functions, cada um podendo manter e manipular escopos internos com propriedades (estados) próprias, etc.
Outro ponto importante sobre este exercício: Atualmente os diversos frameworks e syntax sugars escondem complexidades em métodos mágicos, isso nos dá velocidade e simplifica semanticamente o código, porém no momento em que um bug ocorre os motivos podem ficar ocultos e obscuros dentro desses mesmos métodos mágicos. Portanto, conhecer conceitos de base e o maquinário que derivou o que utilizamos hoje é importante para debugarmos e entendermos melhor as relações entre os componentes que utilizamos para contruir nossas aplicações.
Uma sugestão de estudo a partir daqui seria: reescreva essa mesma lib em ES6 utilizando classes. Você verá na prática como o ecossistema JavaScript evoluiu, e o mais importante: o motivo disso. As diferenças de escopo e comportamento entre var, let e const. As diferenças de escopo entre functions e arrow functions. Os comportamentos gerados pelo hoisting, etc.