JavaScript orientado a objetos em termos simples





Bom dia amigos!



Existem 4 maneiras em JavaScript de criar um objeto:



  • Função construtora
  • Classe (classe)
  • Vinculação de objeto a outro objeto (OLOO)
  • Função de fábrica


Qual método você deve usar? Qual é o melhor?



Para responder a essas perguntas, não apenas consideraremos cada abordagem separadamente, mas também compararemos classes e funções de fábrica de acordo com os seguintes critérios: herança, encapsulamento, a palavra-chave "this", manipuladores de eventos.



Vamos começar com o que é Programação Orientada a Objetos (OOP).



O que é OOP?



Essencialmente, OOP é uma maneira de escrever código que permite criar objetos usando um único objeto. Essa também é a essência do padrão de design do Construtor. Um objeto compartilhado é geralmente chamado de blueprint, blueprint ou blueprint, e os objetos que ele cria são instâncias.



Cada instância tem propriedades herdadas do pai e propriedades próprias. Por exemplo, se tivermos um projeto Human, podemos criar instâncias com nomes diferentes com base nele.



O segundo aspecto da OOP é estruturar o código quando temos vários projetos de diferentes níveis. Isso é chamado de herança ou subclasse.



O terceiro aspecto da OOP é o encapsulamento, quando ocultamos detalhes de implementação de estranhos, tornando as variáveis ​​e funções inacessíveis de fora. Esta é a essência dos padrões de projeto de Módulo e Fachada.



Vamos prosseguir com os métodos de criação de objetos.



Métodos de criação de objetos



Função construtora


Construtores são funções que usam a palavra-chave "this".



    function Human(firstName, lastName) {
        this.firstName = firstName
        this.lastName = lastName
    }


isso permite que você armazene e acesse os valores exclusivos da instância que está sendo criada. As instâncias são criadas usando a palavra-chave "novo".



const chris = new Human('Chris', 'Coyier')
console.log(chris.firstName) // Chris
console.log(chris.lastName) // Coyier

const zell = new Human('Zell', 'Liew')
console.log(zell.firstName) // Zell
console.log(zell.lastName) // Liew


Classe


As classes são uma abstração ("açúcar sintático") sobre as funções do construtor. Eles facilitam a criação de instâncias.



    class Human {
        constructor(firstName, lastName) {
            this.firstName = firstName
            this.lastName = lastName
        }
    }


Observe que o construtor contém o mesmo código que a função do construtor acima. Temos que fazer isso para inicializar isso. Podemos omitir o construtor se não precisarmos atribuir valores iniciais.



À primeira vista, as classes parecem ser mais complexas do que os construtores - você precisa escrever mais código. Segure seus cavalos e não tire conclusões precipitadas. As aulas são legais. Você entenderá o porquê um pouco mais tarde.



As instâncias também são criadas usando a palavra-chave "novo".



const chris = new Human('Chris', 'Coyier')

console.log(chris.firstName) // Chris
console.log(chris.lastName) // Coyier


Objetos de ligação


Este método de criação de objetos foi proposto por Kyle Simpson. Nesta abordagem, definimos o projeto como um objeto comum. Então, usando um método (que geralmente é chamado de init, mas isso não é obrigatório, ao contrário do construtor da classe), inicializamos a instância.



const Human = {
    init(firstName, lastName) {
        this.firstName = firstName
        this.lastName = lastName
    }
}


Object.create é usado para criar uma instância. Após a instanciação, o init é chamado.



const chris = Object.create(Human)
chris.init('Chris', 'Coyier')

console.log(chris.firstName) // Chris
console.log(chris.lastName) // Coyier


O código pode ser melhorado um pouco retornando isso ao init.



const Human = {
  init () {
    // ...
    return this
  }
}

const chris = Object.create(Human).init('Chris', 'Coyier')
console.log(chris.firstName) // Chris
console.log(chris.lastName) // Coyier


Função de fábrica


Uma função de fábrica é uma função que retorna um objeto. Qualquer objeto pode ser devolvido. Você pode até retornar uma instância de uma classe ou associações de objeto.



Aqui está um exemplo simples de uma função de fábrica.



function Human(firstName, lastName) {
    return {
        firstName,
        lastName
    }
}


Não precisamos da palavra-chave "this" para criar uma instância. Nós apenas chamamos a função.



const chris = Human('Chris', 'Coyier')

console.log(chris.firstName) // Chris
console.log(chris.lastName) // Coyier


Agora vamos ver maneiras de adicionar propriedades e métodos.



Definição de propriedades e métodos



Métodos são funções declaradas como propriedades de um objeto.



    const someObject = {
        someMethod () { /* ... */ }
    }


Em OOP, existem duas maneiras de definir propriedades e métodos:



  • Em uma instância
  • Em protótipo


Definição de propriedades e métodos no construtor


Para definir uma propriedade em uma instância, você deve adicioná-la à função do construtor. Certifique-se de adicionar a propriedade a isso.



function Human (firstName, lastName) {
  //  
  this.firstName = firstName
  this.lastname = lastName

  //  
  this.sayHello = function () {
    console.log(`Hello, I'm ${firstName}`)
  }
}

const chris = new Human('Chris', 'Coyier')
console.log(chris)






Os métodos são geralmente definidos no protótipo, pois isso evita a criação de uma função para cada instância, ou seja, Permite que todas as instâncias compartilhem uma única função (chamada de função compartilhada ou distribuída).



Para adicionar uma propriedade ao protótipo, use protótipo.



function Human (firstName, lastName) {
  this.firstName = firstName
  this.lastname = lastName
}

//    
Human.prototype.sayHello = function () {
  console.log(`Hello, I'm ${this.firstName}`)
}






Criar vários métodos pode ser entediante.



//    
Human.prototype.method1 = function () { /*...*/ }
Human.prototype.method2 = function () { /*...*/ }
Human.prototype.method3 = function () { /*...*/ }


Você pode tornar sua vida mais fácil com Object.assign.



Object.assign(Human.prototype, {
  method1 () { /*...*/ },
  method2 () { /*...*/ },
  method3 () { /*...*/ }
})


Definição de propriedades e métodos em uma classe


As propriedades da instância podem ser definidas no construtor.



class Human {
  constructor (firstName, lastName) {
    this.firstName = firstName
      this.lastname = lastName

      this.sayHello = function () {
        console.log(`Hello, I'm ${firstName}`)
      }
  }
}






As propriedades do protótipo são definidas após o construtor como uma função normal.



class Human (firstName, lastName) {
  constructor (firstName, lastName) { /* ... */ }

  sayHello () {
    console.log(`Hello, I'm ${this.firstName}`)
  }
}






Criar vários métodos em uma classe é mais fácil do que em um construtor. Não precisamos de Object.assign para isso. Estamos apenas adicionando outros recursos.



class Human (firstName, lastName) {
  constructor (firstName, lastName) { /* ... */ }

  method1 () { /*...*/ }
  method2 () { /*...*/ }
  method3 () { /*...*/ }
}


Definir propriedades e métodos ao vincular objetos


Para definir propriedades para uma instância, adicionamos uma propriedade a esta.



const Human = {
  init (firstName, lastName) {
    this.firstName = firstName
    this.lastName = lastName
    this.sayHello = function () {
      console.log(`Hello, I'm ${firstName}`)
    }

    return this
  }
}

const chris = Object.create(Human).init('Chris', 'Coyier')
console.log(chris)






O método do protótipo é definido como um objeto regular.



const Human = {
  init () { /*...*/ },
  sayHello () {
    console.log(`Hello, I'm ${this.firstName}`)
  }
}






Definição de propriedades e métodos em funções de fábrica (FF)


Propriedades e métodos podem ser incluídos no objeto retornado.



function Human (firstName, lastName) {
  return {
    firstName,
    lastName,
    sayHello () {
      console.log(`Hello, I'm ${firstName}`)
    }
  }
}






Ao usar FF, você não pode definir propriedades de protótipo. Se precisar de propriedades como essa, você pode retornar uma instância da classe, construtor ou vinculações de objeto (mas isso não faz sentido).



//   
function createHuman (...args) {
  return new Human(...args)
}


Onde definir propriedades e métodos



Onde você deve definir propriedades e métodos? Instância ou protótipo?



Muitas pessoas pensam que os protótipos são melhores para isso.



No entanto, isso realmente não importa.



Ao definir propriedades e métodos em uma instância, cada instância consumirá mais memória. Ao definir métodos em protótipos, a memória será consumida menos, mas de forma insignificante. Dado o poder dos computadores modernos, essa diferença não é significativa. Portanto, faça o que funcionar melhor para você, mas ainda prefira protótipos.



Por exemplo, ao usar classes ou associações de objetos, é melhor usar protótipos porque torna o código mais fácil de escrever. No caso do FF, os protótipos não podem ser usados. Apenas propriedades de instâncias podem ser definidas.



Aproximadamente. por.: discordo do autor. A questão de usar protótipos em vez de instâncias na definição de propriedades e métodos não é apenas uma questão de consumo de memória, mas acima de tudo uma questão de propósito da propriedade ou método que está sendo definido. Se uma propriedade ou método deve ser exclusivo para cada instância, ele deve ser definido na instância. Se uma propriedade ou método deve ser o mesmo (comum) para todas as instâncias, ele deve ser definido no protótipo. Neste último caso, se você precisar fazer alterações em uma propriedade ou método, bastará fazê-las no protótipo, ao contrário das propriedades e métodos de instâncias, que são ajustados individualmente.



Conclusão preliminar



Com base no material estudado, várias conclusões podem ser tiradas. É minha opinião pessoal.



  • As classes são melhores do que os construtores porque tornam mais fácil definir vários métodos.
  • A vinculação de objetos parece estranha devido à necessidade de usar Object.create. Eu sempre me esquecia disso ao estudar essa abordagem. Para mim, isso foi motivo suficiente para recusar mais uso.
  • Classes e FFs são os mais fáceis de usar. O problema é que protótipos não podem ser usados ​​no FF. Mas, como observei anteriormente, isso realmente não importa.


A seguir, compararemos classes e FFs como as duas melhores maneiras de criar objetos em JavaScript.



Classes vs. FF - Herança



Antes de passar para a comparação de classes e FFs, você precisa se familiarizar com os três conceitos subjacentes ao OOP:



  • herança
  • encapsulamento
  • isto


Vamos começar com herança.



O que é herança?


Em JavaScript, herança significa passar propriedades de pai para filho, ou seja, do projeto à instância.



Isso acontece de duas maneiras:



  • usando inicialização de instância
  • usando uma cadeia de protótipo


No segundo caso, o projeto pai é expandido com um projeto filho. Isso é chamado de subclasse, mas alguns também chamam de herança.



Compreendendo a subclasse


Subclasse é quando um projeto filho estende o pai.



Vejamos o exemplo de classes.



Subclassificação com uma classe


A palavra-chave "extends" é usada para estender a classe pai.



class Child extends Parent {
    // ...
}


Por exemplo, vamos criar uma classe "Desenvolvedor" que estende a classe "Humano".



//  Human
class Human {
  constructor (firstName, lastName) {
    this.firstName = firstName
    this.lastName = lastName
  }

  sayHello () {
    console.log(`Hello, I'm ${this.firstName}`)
  }
}


A classe Developer irá estender Human da seguinte forma:



class Developer extends Human {
  constructor(firstName, lastName) {
    super(firstName, lastName)
  }

    // ...
}


A palavra-chave super chama o construtor da classe Human. Se você não precisar disso, super pode ser omitido.



class Developer extends Human {
  // ...
}


Digamos que o desenvolvedor possa escrever código (quem diria). Vamos adicionar um método correspondente a ele.



class Developer extends Human {
  code (thing) {
    console.log(`${this.firstName} coded ${thing}`)
  }
}


Aqui está um exemplo de uma instância da classe "Desenvolvedor".



const chris = new Developer('Chris', 'Coyier')
console.log(chris)






Subclassificação com FF


Para criar subclasses usando FF, você precisa realizar 4 etapas:



  • criar um novo FF
  • criar uma instância do projeto pai
  • crie uma cópia desta instância
  • adicione propriedades e métodos a esta cópia


Este processo se parece com isso.



function Subclass (...args) {
  const instance = ParentClass(...args)
  return Object.assign({}, instance, {
    //   
  })
}


Vamos criar uma subclasse "Desenvolvedor". É assim que o FF "Humano" se parece.



function Human (firstName, lastName) {
  return {
    firstName,
    lastName,
    sayHello () {
      console.log(`Hello, I'm ${firstName}`)
    }
  }
}


Criar desenvolvedor.



function Developer (firstName, lastName) {
  const human = Human(firstName, lastName)
  return Object.assign({}, human, {
    //   
  })
}


Adicione o método "código" a ele.



function Developer (firstName, lastName) {
  const human = Human(firstName, lastName)
  return Object.assign({}, human, {
    code (thing) {
      console.log(`${this.firstName} coded ${thing}`)
    }
  })
}


Crie uma instância de Developer.



const chris = Developer('Chris', 'Coyier')
console.log(chris)






Substituindo o método pai


Às vezes, é necessário sobrescrever um método pai em uma subclasse. Isso pode ser feito da seguinte forma:



  • crie um método com o mesmo nome
  • chame o método pai (opcional)
  • crie um novo método na subclasse


Este processo se parece com isso.



class Developer extends Human {
  sayHello () {
    //   
    super.sayHello()

    //   
    console.log(`I'm a developer.`)
  }
}

const chris = new Developer('Chris', 'Coyier')
chris.sayHello()






O mesmo processo usando FF.



function Developer (firstName, lastName) {
  const human = Human(firstName, lastName)

  return Object.assign({}, human, {
      sayHello () {
        //   
        human.sayHello()

        //   
        console.log(`I'm a developer.`)
      }
  })
}

const chris = new Developer('Chris', 'Coyier')
chris.sayHello()






Herança versus composição


Uma conversa sobre herança raramente passa sem mencionar a composição. Especialistas como Eric Elliot acreditam que a composição deve ser usada sempre que possível.



O que é composição?



Compreendendo a composição


Basicamente, a composição é a combinação de várias coisas em uma. A maneira mais comum e simples de combinar objetos é usando Object.assign.



const one = { one: 'one' }
const two = { two: 'two' }
const combined = Object.assign({}, one, two)


A composição é mais fácil de explicar com um exemplo. Digamos que temos duas subclasses, Desenvolvedor e Designer. Os designers sabem como projetar e os desenvolvedores sabem como escrever o código. Ambos são herdeiros da classe "Humana".



class Human {
  constructor(firstName, lastName) {
    this.firstName = firstName
    this.lastName = lastName
  }

  sayHello () {
    console.log(`Hello, I'm ${this.firstName}`)
  }
}

class Designer extends Human {
  design (thing) {
    console.log(`${this.firstName} designed ${thing}`)
  }
}

class Developer extends Designer {
  code (thing) {
    console.log(`${this.firstName} coded ${thing}`)
  }
}


Agora, suponha que desejamos criar uma terceira subclasse. Esta subclasse deve ser uma mistura de designer e desenvolvedor - deve ser capaz de projetar e escrever código. Vamos chamá-lo de DesignerDeveloper (ou DeveloperDesigner, se preferir).



Como o criamos?



Não podemos estender as classes "Designer" e "Desenvolvedor" ao mesmo tempo. Isso não é possível porque não podemos decidir quais propriedades devem vir primeiro. Isso é chamado de problema do diamante (herança do diamante) .







O problema do losango pode ser resolvido com Object.assign se dermos prioridade a um objeto sobre outro. No entanto, o JavaScript não oferece suporte a herança múltipla.



//  
class DesignerDeveloper extends Developer, Designer {
  // ...
}


É aqui que a composição é útil.



Essa abordagem afirma o seguinte: em vez de criar uma subclasse de DesignerDeveloper, crie um objeto que contenha habilidades que você pode criar como subclasse, conforme necessário.



A implementação desta abordagem leva ao seguinte.



const skills = {
    code (thing) { /* ... */ },
    design (thing) { /* ... */ },
    sayHello () { /* ... */ }
}


Não precisamos mais da classe Human, porque podemos criar três classes diferentes usando o objeto especificado.



Aqui está o código para DesignerDeveloper.



class DesignerDeveloper {
  constructor (firstName, lastName) {
    this.firstName = firstName
    this.lastName = lastName

    Object.assign(this, {
      code: skills.code,
      design: skills.design,
      sayHello: skills.sayHello
    })
  }
}

const chris = new DesignerDeveloper('Chris', 'Coyier')
console.log(chris)






Podemos fazer o mesmo para Designer e Desenvolvedor.



class Designer {
  constructor (firstName, lastName) {
    this.firstName = firstName
    this.lastName = lastName

    Object.assign(this, {
      design: skills.design,
      sayHello: skills.sayHello
    })
  }
}

class Developer {
  constructor (firstName, lastName) {
    this.firstName = firstName
    this.lastName = lastName

    Object.assign(this, {
      code: skills.code,
      sayHello: skills.sayHello
    })
  }
}


Você percebeu que criamos métodos em uma instância? Esta é apenas uma das opções possíveis. Também podemos colocar métodos no protótipo, mas acho desnecessário (essa abordagem parece que estamos de volta aos construtores).



class DesignerDeveloper {
  constructor (firstName, lastName) {
    this.firstName = firstName
    this.lastName = lastName
  }
}

Object.assign(DesignerDeveloper.prototype, {
  code: skills.code,
  design: skills.design,
  sayHello: skills.sayHello
})






Use a abordagem que melhor lhe convier. O resultado será o mesmo.



Composição com FF


Composição com FF é sobre adicionar métodos distribuídos ao objeto retornado.



function DesignerDeveloper (firstName, lastName) {
  return {
    firstName,
    lastName,
    code: skills.code,
    design: skills.design,
    sayHello: skills.sayHello
  }
}






Herança e composição


Ninguém disse que não podemos usar herança e composição ao mesmo tempo.



Voltando aos exemplos Designer, Developer e DesignerDeveloper, deve-se observar que eles também são humanos. Portanto, eles podem estender a classe Humana.



Aqui está um exemplo de herança e composição usando sintaxe de classe.



class Human {
  constructor (firstName, lastName) {
    this.firstName = firstName
    this.lastName = lastName
  }

  sayHello () {
    console.log(`Hello, I'm ${this.firstName}`)
  }
}

class DesignerDeveloper extends Human {}
Object.assign(DesignerDeveloper.prototype, {
  code: skills.code,
  design: skills.design
})






E aqui é o mesmo com o uso de FF.



function Human (firstName, lastName) {
  return {
    firstName,
    lastName,
    sayHello () {
      console.log(`Hello, I'm ${this.firstName}`)
    }
  }
}

function DesignerDeveloper (firstName, lastName) {
  const human = Human(firstName, lastName)
  return Object.assign({}, human, {
    code: skills.code,
    design: skills.design
  })
}






Subclasses no mundo real


Embora muitos especialistas argumentem que a composição é mais flexível (e, portanto, mais útil) do que as subclasses, as subclasses não devem ser desconsideradas. Muitas das coisas com que lidamos se baseiam nessa estratégia.



Por exemplo: o evento "click" é um MouseEvent. MouseEvent é uma subclasse de UIEvent (evento de interface do usuário), que por sua vez é uma subclasse de Event (evento).







Outro exemplo: Elementos HTML são subclasses de nós. Portanto, eles podem usar todas as propriedades e métodos dos nós.







Conclusão preliminar sobre herança


Herança e composição podem ser usadas em ambas as classes e FF. No FF, a composição parece "mais limpa", mas esta é uma pequena vantagem sobre as classes.



Vamos continuar a comparação.



Classes vs. FF - Encapsulamento



Basicamente, o encapsulamento consiste em esconder uma coisa dentro da outra, tornando a essência interna inacessível de fora.



Em JavaScript, entidades ocultas são variáveis ​​e funções que estão disponíveis apenas no contexto atual. Nesse caso, o contexto é o mesmo que o escopo.



Encapsulamento simples


A forma mais simples de encapsulamento é um bloco de código.



{
  // ,  ,     
}


Enquanto em um bloco, você pode acessar uma variável declarada fora dele.



const food = 'Hamburger'

{
  console.log(food)
}






Mas não vice-versa.



{
  const food = 'Hamburger'
}

console.log(food)






Observe que as variáveis ​​declaradas com a palavra-chave "var" têm escopo global ou funcional. Tente não usar var para declarar variáveis.



Encapsulamento com uma função


O escopo funcional é semelhante ao escopo do bloco. Variáveis ​​declaradas em uma função são acessíveis apenas dentro dela. Isso se aplica a todas as variáveis, mesmo aquelas declaradas com var.



function sayFood () {
  const food = 'Hamburger'
}

sayFood()
console.log(food)






Quando estamos dentro de uma função, temos acesso às variáveis ​​declaradas fora dela.



const food = 'Hamburger'

function sayFood () {
  console.log(food)
}

sayFood()






As funções podem retornar valores que podem ser usados ​​posteriormente fora da função.



function sayFood () {
  return 'Hamburger'
}

console.log(sayFood())






Fecho


O fechamento é uma forma avançada de encapsulamento. É apenas uma função dentro de outra função.



//  
function outsideFunction () {
  function insideFunction () { /* ... */ }
}




Variáveis ​​declaradas em outsideFunction podem ser usadas em insideFunction.



function outsideFunction () {
  const food = 'Hamburger'
  console.log('Called outside')

  return function insideFunction () {
    console.log('Called inside')
    console.log(food)
  }
}

//  outsideFunction,   insideFunction
//  insideFunction   "fn"
const fn = outsideFunction()






Encapsulamento e OOP


Ao criar objetos, queremos que algumas propriedades sejam públicas (públicas) e outras privadas (privadas ou privadas).



Vejamos um exemplo. Digamos que temos um projeto de carro. Ao criar uma nova instância, adicionamos uma propriedade "combustível" a ela com um valor de 50.



class Car {
  constructor () {
    this.fuel = 50
  }
}




Os usuários podem usar esta propriedade para determinar a quantidade de combustível restante.



const car = new Car()
console.log(car.fuel) // 50




Os usuários também podem definir a quantidade de combustível por conta própria.



const car = new Car()
car.fuel = 3000
console.log(car.fuel) // 3000


Vamos adicionar a condição de que o tanque do carro tenha no máximo 100 litros de combustível. Não queremos que os usuários possam definir a quantidade de combustível por conta própria, pois podem quebrar o carro.



Existem duas maneiras de fazer isso:



  • uso de propriedades privadas por convenção
  • usando campos privados reais


Propriedades privadas por acordo


Em JavaScript, as variáveis ​​e propriedades privadas geralmente são denotadas com um sublinhado.



class Car {
  constructor () {
    //   "fuel"  ,       
    this._fuel = 50
  }
}


Normalmente, criamos métodos para gerenciar propriedades privadas.



class Car {
  constructor () {
    this._fuel = 50
  }

  getFuel () {
    return this._fuel
  }

  setFuel (value) {
    this._fuel = value
    //   
    if (value > 100) this._fuel = 100
  }
}


Os usuários devem usar os métodos getFuel e setFuel para determinar e definir a quantidade de combustível, respectivamente.



const car = new Car()
console.log(car.getFuel()) // 50

car.setFuel(3000)
console.log(car.getFuel()) // 100


Mas a variável "_fuel" não é realmente privada. É acessível do exterior.



const car = new Car()
console.log(car.getFuel()) // 50

car._fuel = 3000
console.log(car.getFuel()) // 3000


Use campos privados reais para restringir o acesso às variáveis.



Campos verdadeiramente privados


Campos é o termo usado para combinar variáveis, propriedades e métodos.



Campos de aulas particulares


As classes permitem que você crie variáveis ​​privadas usando o prefixo "#".



class Car {
  constructor () {
    this.#fuel = 50
  }
}


Infelizmente, esse prefixo não pode ser usado no construtor.







Variáveis ​​privadas devem ser definidas fora do construtor.



class Car {
  //   
  #fuel
  constructor () {
    //  
    this.#fuel = 50
  }
}


Nesse caso, podemos inicializar a variável quando definida.



class Car {
  #fuel = 50
}


Agora a variável "#fuel" está disponível apenas dentro da classe. Tentar acessá-lo fora da classe gerará um erro.



const car = new Car()
console.log(car.#fuel)






Precisamos de métodos apropriados para manipular a variável.



class Car {
  #fuel = 50

  getFuel () {
    return this.#fuel
  }

  setFuel (value) {
    this.#fuel = value
    if (value > 100) this.#fuel = 100
  }
}

const car = new Car()
console.log(car.getFuel()) // 50

car.setFuel(3000)
console.log(car.getFuel()) // 100


Eu pessoalmente prefiro usar getters e setters para isso. Acho essa sintaxe mais legível.



class Car {
  #fuel = 50

  get fuel () {
    return this.#fuel
  }

  set fuel (value) {
    this.#fuel = value
    if (value > 100) this.#fuel = 100
  }
}

const car = new Car()
console.log(car.fuel) // 50

car.fuel = 3000
console.log(car.fuel) // 100


Campos FF privados


FFs criam campos privados automaticamente. Precisamos apenas declarar uma variável. Os usuários não poderão acessar esta variável de fora. Isso se deve ao fato de que as variáveis ​​têm escopo de bloco (ou funcional), ou seja, são encapsulados por padrão.



function Car () {
  const fuel = 50
}

const car = new Car()
console.log(car.fuel) // undefined
console.log(fuel) // Error: "fuel" is not defined


Getters e setters também são usados ​​para controlar a variável privada "combustível".



function Car () {
  const fuel = 50

  return {
    get fuel () {
      return fuel
    },

    set fuel (value) {
      fuel = value
      if (value > 100) fuel = 100
    }
  }
}

const car = new Car()
console.log(car.fuel) // 50

car.fuel = 3000
console.log(car.fuel) // 100


Como isso. Simples e facilmente!



Conclusão preliminar sobre encapsulamento


O encapsulamento FF é mais simples e fácil de entender. É baseado no escopo, que é uma parte importante do JavaScript.



O encapsulamento de classes envolve o uso do prefixo "#", que pode ser entediante.



Classes contra FF - este



este é o principal argumento contra o uso de classes. Por quê? Porque o significado disso depende de onde e como isso é usado. Esse comportamento costuma ser confuso não apenas para iniciantes, mas também para desenvolvedores experientes.



No entanto, o conceito disso não é tão difícil. Existem 6 contextos no total em que isso pode ser usado. Se você for bom nesses contextos, não deverá ter problemas com isso.



Os contextos nomeados são:



  • contexto global
  • contexto do objeto sendo criado
  • o contexto de uma propriedade ou método de um objeto
  • função simples
  • função de seta
  • contexto do manipulador de eventos


Mas voltando ao artigo. Vejamos os detalhes de como usar isso em classes e FFs.



Usando isso nas aulas


Quando usado em uma classe, aponta para a instância que está sendo criada (contexto de propriedade / método). É por isso que a instância é inicializada no construtor.



class Human {
  constructor (firstName, lastName) {
    this.firstName = firstName
    this.lastName = lastName
    console.log(this)
  }
}

const chris = new Human('Chris', 'Coyier')






Usando isso em funções de construtor


Ao usar isso dentro de uma função e new para criar uma instância, isso apontará para a instância.



function Human (firstName, lastName) {
  this.firstName = firstName
  this.lastName = lastName
  console.log(this)
}

const chris = new Human('Chris', 'Coyier')






Em contraste com FK em FF, isso aponta para a janela (no contexto do módulo, geralmente tem o valor "indefinido").



//        "new"
function Human (firstName, lastName) {
  this.firstName = firstName
  this.lastName = lastName
  console.log(this)
}

const chris = Human('Chris', 'Coyier')






Portanto, isso não deve ser usado em FF. Esta é uma das principais diferenças entre FF e FC.



Usando isso no FF


Para poder usar isso no FF, é necessário criar um contexto de propriedade / método.



function Human (firstName, lastName) {
  return {
    firstName,
    lastName,
    sayThis () {
      console.log(this)
    }
  }
}

const chris = Human('Chris', 'Coyier')
chris.sayThis()






Embora possamos usar isso no FF, não precisamos disso. Podemos criar uma variável apontando para a instância. Essa variável pode ser usada em vez disso.



function Human (firstName, lastName) {
  const human = {
    firstName,
    lastName,
    sayHello() {
      console.log(`Hi, I'm ${human.firstName}`)
    }
  }

  return human
}

const chris = Human('Chris', 'Coyier')
chris.sayHello()


human.firstName é mais preciso do que this.firstName porque human está apontando explicitamente para uma instância.



Na verdade, nem mesmo precisamos escrever human.firstName. Podemos nos limitar a firstName, já que esta variável tem um escopo léxico (isto é, quando o valor da variável é retirado do ambiente externo).



function Human (firstName, lastName) {
  const human = {
    firstName,
    lastName,
    sayHello() {
      console.log(`Hi, I'm ${firstName}`)
    }
  }

  return human
}

const chris = Human('Chris', 'Coyier')
chris.sayHello()






Vejamos um exemplo mais complexo.



Exemplo complexo



As condições são as seguintes: temos um projeto “Humano” com as propriedades “firstName” e “lastName” e um método “sayHello”.



Também temos um projeto "Desenvolvedor" que herda de Humano. Os desenvolvedores sabem como escrever código, portanto, devem ter um método de "código". Além disso, eles devem declarar que estão na casta do desenvolvedor, portanto, precisamos sobrescrever o método sayHello.



Vamos implementar a lógica especificada usando classes e FF.



Aulas


Criamos um projeto "Humano".



class Human {
  constructor (firstName, lastName) {
    this.firstName = firstName
    this.lastname = lastName
  }

  sayHello () {
    console.log(`Hello, I'm ${this.firstName}`)
  }
}


Crie um projeto "Desenvolvedor" com o método "código".



class Developer extends Human {
  code (thing) {
    console.log(`${this.firstName} coded ${thing}`)
  }
}


Sobrescrevemos o método "sayHello".



class Developer extends Human {
  code (thing) {
    console.log(`${this.firstName} coded ${thing}`)
  }

  sayHello () {
    super.sayHello()
    console.log(`I'm a developer`)
  }
}


FF (usando isso)


Criamos um projeto "Humano".



function Human () {
  return {
    firstName,
    lastName,
    sayHello () {
      console.log(`Hello, I'm ${this.firstName}`)
    }
  }
}


Crie um projeto "Desenvolvedor" com o método "código".



function Developer (firstName, lastName) {
  const human = Human(firstName, lastName)
  return Object.assign({}, human, {
    code (thing) {
      console.log(`${this.firstName} coded ${thing}`)
    }
  })
}


Sobrescrevemos o método "sayHello".



function Developer (firstName, lastName) {
  const human = Human(firstName, lastName)
  return Object.assign({}, human, {
    code (thing) {
      console.log(`${this.firstName} coded ${thing}`)
    },

    sayHello () {
      human.sayHello()
      console.log('I\'m a developer')
    }
  })
}


Ff (sem isso)


Como firstName tem escopo léxico direto, podemos omitir isso.



function Human (firstName, lastName) {
  return {
    // ...
    sayHello () {
      console.log(`Hello, I'm ${firstName}`)
    }
  }
}

function Developer (firstName, lastName) {
  // ...
  return Object.assign({}, human, {
    code (thing) {
      console.log(`${firstName} coded ${thing}`)
    },

    sayHello () { /* ... */ }
  })
}


Conclusão preliminar sobre este


Em palavras simples, as classes exigem o uso disso, mas os FFs não. Neste caso, prefiro usar FF porque:



  • este contexto pode mudar
  • o código escrito usando FF é mais curto e mais limpo (também devido ao encapsulamento automático de variáveis)


Classes vs. FF - manipuladores de eventos



Muitos artigos sobre OOP negligenciam o fato de que, como desenvolvedores front-end, estamos constantemente lidando com manipuladores de eventos. Eles fornecem interação com os usuários.



Como os manipuladores de eventos mudam esse contexto, trabalhar com eles em classes pode ser problemático. Ao mesmo tempo, esses problemas não surgem no FF.



Porém, mudar este contexto não importa se sabemos como lidar com isso. Vejamos um exemplo simples.



Criar contador


Para criar um contador, usaremos o conhecimento adquirido, incluindo variáveis ​​privadas.



Nosso contador conterá duas coisas:



  • o próprio contador
  • botão para aumentar seu valor






Esta é a aparência da marcação:



<div class="counter">
  <p>Count: <span>0</span></p>
  <button>Increase Count</button>
</div>


Criação de um contador usando uma classe


Para facilitar as coisas, peça ao usuário para encontrar e passar a marcação do contador para a classe Counter:



class Counter {
  constructor (counter) {
    // ...
  }
}

// 
const counter = new Counter(document.querySelector('.counter'))


Você precisa obter 2 elementos na classe:



  • <span> contendo o valor do contador - precisamos atualizar este valor quando o contador aumenta
  • <button> - precisamos adicionar um manipulador de eventos para este elemento


class Counter {
  constructor (counter) {
    this.countElement = counter.querySelector('span')
    this.buttonElement = counter.querySelector('button')
  }
}


Em seguida, inicializamos a variável "count" com o conteúdo de texto de countElement. A variável especificada deve ser privada.



class Counter {
  #count
  constructor (counter) {
    // ...

    this.#count = parseInt(countElement.textContent)
  }
}


Quando o botão é pressionado, o valor do contador deve aumentar em 1. Implementamos isso usando o método "aumentarContagem".



class Counter {
  #count
  constructor (counter) { /* ... */ }

  increaseCount () {
    this.#count = this.#count + 1
  }
}


Agora precisamos atualizar o DOM. Vamos implementar isso usando o método "updateCount" chamado em boostCount:



class Counter {
  #count
  constructor (counter) { /* ... */ }

  increaseCount () {
    this.#count = this.#count + 1
    this.updateCount()
  }

  updateCount () {
    this.countElement.textContent = this.#count
  }
}


Resta adicionar um manipulador de eventos.



Adicionar um manipulador de eventos


Vamos adicionar um manipulador a this.buttonElement. Infelizmente, não podemos usar aumentarCount como uma função de retorno de chamada. Isso resultará em um erro.



class Counter {
  // ...

  constructor (counter) {
    // ...
    this.buttonElement.addEventListener('click', this.increaseCount)
  }

  // 
}






A exceção é lançada porque aponta para buttonElement (contexto do manipulador de eventos). Você pode verificar isso imprimindo esse valor no console.







Este valor deve ser alterado para apontar para a instância. Isso pode ser feito de duas maneiras:



  • usando ligação
  • usando a função de seta


A maioria usa o primeiro método (mas o segundo é mais simples).



Adicionando um manipulador de eventos com bind


bind retorna uma nova função. Como primeiro argumento, é passado um objeto para o qual este apontará (ao qual será vinculado).



class Counter {
  // ...

  constructor (counter) {
    // ...
    this.buttonElement.addEventListener('click', this.increaseCount.bind(this))
  }

  // ...
}


Funciona, mas não parece bom. Além disso, o bind é um recurso avançado que é difícil para iniciantes.



Funções de seta


As funções de seta, entre outras coisas, não têm isso próprio. Eles o pegam emprestado do ambiente léxico (externo). Portanto, o código do contador pode ser reescrito da seguinte forma:



class Counter {
  // ...

  constructor (counter) {
    // ...
    this.buttonElement.addEventListener('click', () => {
      this.increaseCount()
    })
  }

  // 
}


Existe uma maneira ainda mais fácil. Podemos criar aumentarCount como uma função de seta. Nesse caso, isso apontará para a instância.



class Counter {
  // ...

  constructor (counter) {
    // ...
    this.buttonElement.addEventListener('click', this.increaseCount)
  }

  increaseCount = () => {
    this.#count = this.#count + 1
    this.updateCounter()
  }

  // ...
}


O código


Aqui está o código de exemplo completo:







Criação de um contador usando FF


O início é semelhante - pedimos ao usuário para encontrar e passar a marcação do contador:



function Counter (counter) {
  // ...
}

const counter = Counter(document.querySelector('.counter'))


Obtemos os elementos necessários, que serão privados por padrão:



function Counter (counter) {
  const countElement = counter.querySelector('span')
  const buttonElement = counter.querySelector('button')
}


Vamos inicializar a variável "count":



function Counter (counter) {
  const countElement = counter.querySelector('span')
  const buttonElement = counter.querySelector('button')

  let count = parseInt(countElement.textContext)
}


O valor do contador será aumentado usando o método "boostCount". Você pode usar uma função regular, mas prefiro uma abordagem diferente:



function Counter (counter) {
  // ...
  const counter = {
    increaseCount () {
      count = count + 1
    }
  }
}


O DOM será atualizado usando o método "updateCount" que é chamado dentro de boostCount:



function Counter (counter) {
  // ...
  const counter = {
    increaseCount () {
      count = count + 1
      counter.updateCount()
    },

    updateCount () {
      increaseCount()
    }
  }
}


Observe que estamos usando counter.updateCount em vez de this.updateCount.



Adicionar um manipulador de eventos


Podemos adicionar um manipulador de eventos ao buttonElement usando counter.increaseCount como callback.



Isso funcionará, pois não estamos usando isso, então não importa para nós que o manipulador mude o contexto disso.



function Counter (counterElement) {
  // 

  // 
  const counter = { /* ... */ }

  //  
  buttonElement.addEventListener('click', counter.increaseCount)
}


A primeira característica deste


Você pode usar isso no FF, mas apenas no contexto de um método.



No exemplo a seguir, chamar counter.increaseCount chamará counter.updateCount porque isso aponta para counter:



function Counter (counterElement) {
  // 

  // 
  const counter = {
    increaseCount() {
      count = count + 1
      this.updateCount()
    }
  }

  //  
  buttonElement.addEventListener('click', counter.increaseCount)
}


No entanto, o manipulador de eventos não funcionará porque o valor this foi alterado. Este problema pode ser resolvido com bind, mas não com funções de seta.



A segunda característica deste


Ao usar a sintaxe FF, não podemos criar métodos na forma de funções de seta, porque os métodos são criados no contexto de uma função, ou seja, isso irá apontar para a janela:



function Counter (counterElement) {
  // ...
  const counter = {
    //   
    //  ,  this   window
    increaseCount: () => {
      count = count + 1
      this.updateCount()
    }
  }
  // ...
}


Portanto, ao usar o FF, recomendo fortemente evitá-lo.



O código








Veredicto do manipulador de eventos


Os manipuladores de eventos alteram o valor disso, portanto, use-o com muito cuidado. Ao usar classes, aconselho você a criar retornos de chamada do manipulador de eventos na forma de funções de seta. Então você não precisa recorrer a serviços de ligação.



Ao usar o FF, recomendo ficar sem isso.



Conclusão



Portanto, neste artigo, vimos quatro maneiras de criar objetos em JavaScript:



  • Funções de construtor
  • Aulas
  • Objetos de ligação
  • Funções de fábrica


Primeiro, chegamos à conclusão de que classes e FFs são as maneiras mais ideais de criar objetos.



Em segundo lugar, vimos que as subclasses são mais fáceis de criar com classes. Porém, no caso de composição, é melhor usar FF.



Terceiro, resumimos que, quando se trata de encapsulamento, os FFs têm uma vantagem sobre as classes, já que as últimas requerem o uso de um prefixo especial "#" e os FFs tornam as variáveis ​​privadas automaticamente.



Quarto, os FFs permitem que você faça isso sem usar isso como uma referência de instância. Nas aulas, você deve recorrer a alguns truques para retornar ao contexto original alterado pelo manipulador de eventos.



Isso é tudo para mim. Eu espero que você tenha gostado do artigo. Obrigado pela atenção.



All Articles