O que é "isso" e o que ele come


Foto de Sebastian Herrmann .

Bom dia amigos

Apresento a você a tradução do artigo de Daniel James “O que é isso? Por que isso? ” .

O que é "isso" e o que ele come


Quando comecei a aprender JavaScript, o conceito disso me pareceu extremamente confuso.

1. Introdução


O rápido crescimento da popularidade da JS pode ser parcialmente explicado por um baixo limite de entrada. Recursos como funções e isso geralmente funcionam como esperado. Para se tornar um profissional em JS, você não precisa conhecer muitos pequenos detalhes e detalhes (eu argumentaria com isso - aprox. Por.). Mas uma vez, todo desenvolvedor encontra um erro causado pelo valor disso.

Depois disso, você deseja entender como isso funciona no JS. Esse é um conceito para programação orientada a objetos (OOP)? JS é uma linguagem de programação orientada a objetos (OOJP)? Se você "pesquisar" no Google, receberá em resposta a menção de alguns protótipos. Que tipo de protótipo? Para que foi usada a palavra-chave "new" antes das classes aparecerem em JS?

Todas essas coisas estão intimamente relacionadas. Mas antes de explicar como isso funciona, vou me permitir uma pequena digressão. Eu quero falar um pouco sobre por que JS é o que é.

OOP em JS


O paradigma de programação de protótipo (herança) em JS é uma das características da OOP. Mesmo antes do advento das classes JS, havia OOJP. JS é uma linguagem simples que usa apenas algumas coisas do OOP. O mais importante deles são funções, fechamentos, protótipos, literais de objetos e a nova palavra-chave.

Encapsulamento e reutilização com fechamentos


Vamos criar a classe Counter. Essa classe deve ter métodos para redefinir e incrementar o contador. Podemos escrever algo como isto:

function Counter(initialValue = 0){ let _count = initialValue return { reset: function(){ _count = 0 }, next: function(){ return ++_count } } } const myCounter = Counter() console.log(myCounter.next()) // 1 

Nesse caso, nos limitamos a usar funções e literais de objetos sem isso ou novos. Sim, já recebemos algo do OOP. Temos a oportunidade de criar novas instâncias do Counter. Cada instância do Counter tem sua própria contagem de variáveis ​​interna. Implementamos encapsulamento e reutilização de uma maneira puramente funcional.

Problema de desempenho


Suponha que estamos escrevendo um programa que usa um grande número de contadores. Cada contador terá seus próprios métodos reset e next (Counter (). Reset! = Counter (). Reset). Criar esses fechamentos para cada método de cada instância exigirá uma quantidade enorme de memória! Essa arquitetura é "insustentável". Portanto, precisamos encontrar uma maneira de armazenar em cada instância do Counter apenas referências aos métodos que ele usa (de fato, é isso que todos os OOJPs, como Java) fazem.

Podemos resolver esse problema da seguinte maneira (sem envolver recursos adicionais de idioma):

 let Counter = { reset: function(counter){ counter._count = 0 }, next: function(counter){ return ++counter._count }, new: function(initialValue = 0){ return { _count: initialValue } } } const myCounter = Counter.new() console.log(Counter.next(myCounter)) // 1 

Essa abordagem resolve o problema de desempenho, mas tivemos que fazer um sério compromisso, que consiste na necessidade de um alto grau de participação do programador na execução do programa (para garantir que o código funcione). Sem ferramentas adicionais, teríamos que nos contentar com essa abordagem.

Isso corre para o resgate


Reescrevemos nosso exemplo usando este:

 let Counter = { reset: function(){ this._count = 0 }, next: function(){ return ++this._count }, new: function(initialValue = 0){ return { _count: initialValue, //          reset: Counter.reset, next: Counter.next } } } const myCounter = Counter.new() myCounter.next() // ,  reset     myCounter (Counter.new()).reset() console.log(myCounter.next()) // 2 

Observe que ainda estamos criando funções simples reset e next (Counter.new (). Reset == Counter.new (). Reset). No exemplo anterior, para que o programa funcionasse, fomos forçados a fornecer um descritor de instância para métodos implementados em conjunto. Agora chamamos myCounter.next () e nos referimos à instância usando isso. Mas como isso funciona? Redefinir e próximo são declarados no objeto Contador. Como JS sabe a que isso se refere ao chamar uma função?

Chamada de Função em JS


Você sabe muito bem que as funções em JS possuem um método de chamada (também existe um método de aplicação; a diferença entre esses métodos não é significativa. A diferença está na maneira como passamos os parâmetros: em aplicar como uma matriz, em chamada separada por vírgulas - aprox. Por.) . Usando a chamada, você decide o que isso significa quando a função é chamada:

 const myCounter = Counter.new() Counter.next.call(myCounter) 

Na verdade, é isso que a notação de ponto faz nos bastidores quando chamamos a função. lhs.fn () é idêntico a fn.call (lhs).

Portanto, esse é um identificador especial que é definido quando a função é chamada.

Começam os problemas


Digamos que você queira criar um contador e incrementar seu valor a cada segundo. Veja como fazê-lo:

 const myCounter = Counter.new() setInterval(myCounter.next, 1000) //    console.log(`Why is ${myCounter.next()} still 0?`) //  myCounter.next()    0? 

Você vê algum erro aqui? Quando setInterval é iniciado, o valor disso é indefinido, para que nada aconteça. Esse problema pode ser resolvido da seguinte maneira:

 const myCounter = Counter.new() setInterval(function(){ myCounter.next() }, 1000) 

Um pouco sobre o bind


Há outra maneira de resolver esse problema:

 function bindThis(fn, _this){ return function(...args){ return fn.call(_this, ...args) } } const myCounter = Counter.new() setInterval(bindThis(myCounter.next, myCounter), 1000) 

Usando a função de fábrica bindThis, podemos ter certeza de que Counter.next sempre chama myCounter como esse, independentemente de como a nova função é chamada. De fato, não alteramos a função do Counter.next. JS possui um método de ligação interno. Portanto, podemos reescrever o exemplo acima desta forma: setInterval (myCounter.next.bind (myCounter), 1000).

Trabalhamos com protótipos


No momento, temos uma boa aula de Counter, mas ainda está um pouco torta. Estas são as seguintes linhas:

 // ... reset: Counter.reset, next: Counter.next, // ... 

Precisamos de uma maneira melhor de compartilhar métodos de classe com suas instâncias. Os protótipos fazem um excelente trabalho nisso. Se você se referir à propriedade de uma função ou objeto que não existe, o JS procurará essa propriedade no protótipo dessa função ou objeto (depois no protótipo do protótipo e assim por diante no Object.prototype localizado na parte superior da cadeia de protótipos - aprox. Trans.). Você pode definir o protótipo de um objeto usando Object.setPrototypeOf. Vamos reescrever nossa classe Counter usando protótipos:

 let Counter = { reset: function(){ this._count = 0 }, next: function(){ return ++this._count }, new: function(initialValue = 0){ this._count = initialValue } } function newInstanceOf(klass, ...args){ //       "klass",  "class"   const instance = {} Object.setPrototypeOf(instance, klass) instance.new(...args) return instance } const myCounter = newInstanceOf(Counter) console.log(myCounter.next()) // 1 

Palavra-chave "novo"


O uso de setPrototypeOf é muito parecido com o funcionamento do operador "novo". A diferença é que new usará o construtor de protótipo da função passada. Portanto, em vez de criar um objeto para nossos métodos, passamos para o protótipo do construtor da função:

 function Counter(initialValue = 0){ this._count = initialValue } Counter.prototype.reset = function(){ this._count = 0 } Counter.prototype.next = function(){ return ++this._count } const myCounter = new Counter() console.log(`${myCounter.next()}`) // 1 

Finalmente, temos o código na forma em que ele pode ser encontrado na prática. Antes do surgimento das classes no JS, essa era a abordagem padrão para criar e inicializar classes.

Palavra-chave "classe"


Espero que agora você entenda por que usamos o protótipo do construtor de funções e como isso funciona nos métodos de funções. No entanto, nosso código pode ser aprimorado. Felizmente, hoje em JS existe uma maneira melhor de declarar classes:

 class Counter { reset(){ this._count = 0 } next(){ return ++this._count } constructor(initialValue = 0){ this._count = initialValue } } const myCounter = new Counter() console.log(`${myCounter.next()}`) // 1 

A palavra-chave "classe" não faz nada, especialmente em "gato". Você pode pensar nisso como açúcar sintático, um invólucro para a abordagem de "protótipo". Se você executar o transportador orientado para o ES3, obterá algo como isto:

 var Counter = /** @class **/ (function(){ function Counter(initialValue){ if(initialValue === void 0) { initialValue = 0 } this._count = initialValue } Counter.prototype.reset = function(){ this._count = 0 } Counter.prototype.next = function(){ ++this._count } return Counter }()); var myCounter = new Counter() console.log(myCounter.next()) 

Observe que o transpiler gerou um código quase idêntico ao exemplo anterior.

Funções de seta


Se você escreve código em JS nos últimos 5 anos, pode se surpreender ao mencionar funções de seta. Meu conselho: sempre use as funções de seta até que você realmente precise de uma função regular. Aconteceu que a definição do construtor e dos métodos da classe é justamente o caso em que devemos usar funções comuns. Uma das características das funções das setas é a ofuscação.

Isso nas funções de seta


Alguns podem assumir que as funções de seta assumem o valor atual disso quando criadas. Isso está incorreto do ponto de vista técnico (o significado disso não é definido, é retirado do ambiente lexical), mas esse é um bom modelo mental. Uma função de seta como esta:

 const myArrowFunction = () => { this.doSomething() } 

Você pode reescrevê-lo assim:
 const _this = this const myRegularFunction = function(){ _this.doSomething() } 

Obrigado pela atenção. Tudo de bom.

Source: https://habr.com/ru/post/pt486048/


All Articles