Разновидности объектно-ориентированного программирования (ОПП) в JavaScript. Часть 2

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!

Для будущих студентов курса "JavaScript Developer. Basic" и всех желающих подготовили перевод полезной статьи.

Приглашаем также посмотреть открытый урок на тему "Прототипное наследование в JavaScript".


В первой части мы рассмотрели четыре различных разновидности Объектно-ориентированного программирования. Два из них — Классы и Фабричные функции — проще в использовании по сравнению с остальными.

Но вопросы остаются: Какую из них использовать? И почему?

Чтобы продолжить обсуждение функций Классов и Фабричных функций, необходимо понять три понятия, которые тесно связаны с Объектно-ориентированным программированием:

  1. Наследование

  2. Инкапсуляция

  3. this

Мы только что говорили о наследстве. Теперь давайте поговорим об инкапсуляции.

Инкапсуляция

Инкапсуляция — это большое слово, но оно имеет простое значение. Инкапсуляция — это акт помещения одной вещи внутрь другой, чтобы вещь внутри не просочилась наружу. Подумайте о том, чтобы хранить воду в бутылке. Бутылка предотвращает утечку воды.

В JavaScript мы заинтересованы в том, чтобы ограничить область видимости (которые могут включать в себя функции), чтобы эти переменные не просочились во внешнюю область видимости. Это означает, что для понимания инкапсуляции необходимо понимать границы видимости. Мы рассмотрим объяснение, но вы также можете использовать эту статью, чтобы подкрепить свои знания о диапазонах.

Простая инкапсуляция

Самая простая форма инкапсуляции — блочная сфера применения.

{
  // Variables declared here won't leak out
}

Когда вы находитесь в блоке, вы можете получить доступ к переменным, которые декларируются вне блока.

const food = 'Hamburger'

{
  console.log(food)
}

Замечание: Переменные декларированные с  var соблюдают область видимого блока. Поэтому  я рекомендую использовать let or const для декларирования переменных.

Инкапсулирование с функциями

Функции ведут себя как блок области. Когда вы декларируете переменную внутри функции, они не могут утечь из этой функции. Это работает для всех переменных, даже для тех, которые декларированы с помощью функции.

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

sayFood()
console.log(food)

Аналогично, когда вы находитесь внутри функции, вы можете получить доступ к переменным, которые декларируются вне этой функции.

const food = 'Hamburger'

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


sayFood()

Функции могут возвращать значение. Это возвращаемое значение может быть использовано позже, вне функции.

function sayFood () {
  return 'Hamburger'
}

console.log(sayFood())

Функции-замыкания

Функции-замыкания — это усовершенствованная форма инкапсуляции. Это просто функции, завернутые в функции.

// Here's a closure
function outsideFunction () {
  function insideFunction () { /* ...*/ }
}

Переменные декларированные в outsideFunction могут быть использованы в insideFunction.

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

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

// Calls `outsideFunction`, which returns `insideFunction`
// Stores `insideFunction` as variable `fn`
const fn = outsideFunction() 

// Calls `insideFunction`
fn()

Инкапсулирование и объектно-ориентированное программирование

Когда вы строите объекты, вы хотите сделать некоторые свойства общедоступными (чтобы люди могли их использовать). Но вы также хотите сохранить некоторые свойства приватными (чтобы другие не могли нарушить Вашу реализацию).

Давайте проработаем это на примере, чтобы сделать вещи более понятными. Допустим, у нас есть a Car blueprint. Когда мы производим новые автомобили, мы заправляем каждый автомобиль 50 литрами топлива.

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

Здесь мы раскрыли fuel свойство. Пользователи могут использовать fuel, чтобы получить количество топлива, оставшегося в их машинах.

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

Пользователи также могут использовать fuel свойство для установки любого количества топлива.

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

Добавим условие и скажем, что каждый автомобиль имеет максимальную вместимость 100 литров. При таком условии мы не хотим, чтобы пользователи свободно устанавливали свойство топлива, потому что они могут сломать автомобиль.

Есть два способа запретить пользователям устанавливать fuel свойство:

  1. Приватные по соглашению

  2. Действительные приватные члены класса

Приватные по соглашению

В JavaScript существует практика добавления подчеркивания. Это означает, что переменная является приватной и не должна использоваться.

class Car {
  constructor () {
    // Denotes that `_fuel` is private. Don't use it!
    this._fuel = 50
  }
}

Мы часто создаем методы, чтобы получить и установить переменную fuel "приватной”.

class Car {
  constructor () { 
    // Denotes that `_fuel` is private. Don't use it!
    this._fuel = 50
  }

  getFuel () {
    return this._fuel
  }

  setFuel (value) {
    this._fuel = value
    // Caps fuel at 100 liters
    if (value > 100) this._fuel = 100
  }
}

Пользователи должны использовать методы getFuel и setFuel для получения и установки fuel.

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

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

Но fuel на самом деле не приватный. Это все еще открытая переменная. Вы все еще можете получить к ней доступ, вы все еще можете использовать ее, и вы все еще можете злоупотреблять ею (даже если злоупотребление является случайностью).

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

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

Нам нужно использовать действительные приватные переменные, если мы хотим полностью запретить пользователям доступ к ним.

Действительные приватные члены класса

Здесь участники ссылаются на переменные, функции и методы. Это коллективный термин.

Приватные члены с классами

Классы позволяют создавать приватные члены класса, добавляя # к переменной.

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

К сожалению, вы не можете использовать # напрямую в функции constructor

Сначала необходимо декларировать приватную переменную вне конструктора.

class Car {
  // Declares private variable
  #fuel 
  constructor () {
    // Use private variable
    this.#fuel = 50
  }
}

В этом случае мы можем использовать сокращение и объявить #fuel заранее, так как мы установили fuel на 50.

class Car {
  #fuel = 50
}

Вы не можете получить доступ к #fuel из Car. У вас будет ошибка.

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

Вам понадобятся методы (такие как getFuel или setFuel) чтобы использовать переменную  #fuel.

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

Примечание: Я предпочитаю Getters and Setters вместо getFuel и setFuel. Синтакс легче читать.

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

Приватные члены класса с фабричными функциями

Фабричные функции создают Приватных членов автоматически. Вам просто нужно декларировать переменную, как обычно. Пользователи не смогут получить эту переменную нигде больше. Причина в том, что переменные находятся в функции и, следовательно, инкапсулируются по умолчанию.

function Car () {
  const fuel = 50 
}

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

Мы можем создать функции getter and setter чтобы использовать приватную переменную fuel.

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

Вот так! Просто и легко!

Вердикт об инкапсуляции

Инкапсуляция с фабричными функциями проще и понятнее. Они опираются на сферы применения, которые составляют большую часть языка JavaScript.

Инкапсуляция с классами, с другой стороны, требует добавления # к приватной переменной. Это может сделать вещи неуклюжими.

Мы рассмотрим финальную концепцию — это для завершения сравнения между Классами и Фабричными функциями — в следующем разделе.


Классы в сравнении с Фабричными функциями — this переменная

this — один из основных аргументов против использования классов для объектно-ориентированного программирования. Почему? Потому что this значение изменяется в зависимости от того, как оно используется. Это может сбить с толку многих разработчиков (как новых, так и опытных).

Но концепция this относительно проста в реальности. Существует только шесть контекстов, в которых вы можете использовать this. Если вы освоите эти шесть контекстов, у вас не будет проблем с использованием this.

Шесть контекстов:

  1. В глобальном контексте

  2. В создании объекта

  3. В свойстве объекта / метода

  4. В простой функции

  5. В стрелочной функции

  6. В слушателе событий

Я затронул эти 6 контекстов в деталях. Прочитайте, если вам нужно понять this.

Замечание: Не стесняйтесь изучить this. Это важная концепция, которую вам необходимо понять, если вы намерены овладеть JavaScript.

Возвращайся к этой статье после того, как закрепишь свои знания об this. Мы более подробно обсудим использование this в классах и фабричных функциях.

Уже вернулись? Хорошо. Поехали! 

Использование this в классах

This в случае использования в классе. (Он использует контекст "В свойстве/методе объекта".) Вот почему свойства и методы можно устанавливать на экземпляре внутри функции constructor.

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

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

Использование This в функции Конструктора

Если вы используете this в функции и new чтобы создать экземпляр, this будет ссылаться на экземпляр. Так создается функция Конструктора.

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

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

Я упомянул о функциях конструктора, потому что вы можете использовать this внутри фабричных функций. Но this указывает на Window (или undefined если вы используете ES6 Модули, или пакеты такие как webpack).

// NOT a Constructor function because we did not create instances with the `new` keyword
function Human (firstName, lastName) {
  this.firstName = firstName 
  this.lastName = lastName
  console.log(this)  
}

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

По существу, когда вы создаете фабричную функцию, вы не должны использовать this как будто это функция Конструктора. Это одна маленькая заминка, которую люди испытывают с this. Я хотел осветить проблему и прояснить ее.

Использование This в фабричной функции 

Правильным способом использования this в фабричные функции является его использование в контексте "свойство/метод объекта".

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

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

Несмотря на то, что вы можете использовать this в фабричных функциях, вам не нужно их использовать. Вы можете создать переменную, указывающую на экземпляр. Как только вы это сделаете, вы можете использовать эту переменную вместо this. Вот пример в работе.

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 ясней чем this.firstName потому что human бесконечно указывает на экземпляр. Ты знаешь, когда видишь код.

Если вы привыкли к JavaScript, вы также можете заметить, что нет необходимости даже писать human.firstName изначально! Достаточно только firstName потому что firstName находится в лексическом диапазоне. (Прочитайте эту статью если вам необходима помощь с областью применения.)

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

  return human
}

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

То, что мы пока охватили, очень просто. Нелегко решить, действительно ли this необходимо, пока мы не создадим достаточно сложный пример. Так что давайте сделаем это.

Детальный пример

Вот и схема. Допустим, у нас есть Human blueprint. У Human есть свойства firstName и lastName, и метод sayHello.

У нас есть Developer blueprint, который выходит из Human. Разработчики могут программировать, так что у них будет метод code. Разработчики также хотят декларировать, что они разработчики, так что нам нужно перезаписать sayHello и добавить I'm a Developer к консоли.

Мы создадим этот пример с Классами и Фабричными функциями. (Приведем пример с this и пример без this для фабричных функций).

Пример с Классами

Для начала у на есть Human blueprint. У Human есть свойства firstName и lastName, а также метод sayHello.

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

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

У нас есть Developer blueprint который выходит из Human. Разработчики могут кодить, так что у них будет метод code.

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

Разработчики также хотят заявить, что они разработчики. Нам нужно переписать sayHello и добавить I'm a Developer в консоль. Мы это делаем вызывая метод Human‘s sayHello. Мы делаем это используя super.

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

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

Пример фабричных функций (с  this)

Также, во-первых, у нас есть Human blueprint. У Human есть свойства firstName и lastName, а также метод sayHello.

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

Следующее, у нас есть Developer blueprint который выходит из Human. Разработчики могут программировать, так что у них будут метод code

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

Разработчики также хотят заявить, что они разработчики. Нам нужно переписать sayHello и добавить I'm a Developer в консоль. Мы это делаем вызывая метод Human‘s sayHello. Мы делаем это используя экземпляр human.

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')
    }
  })
}

Пример фабричных функций (без this)

Вот полный код, использующий фабричные функции (с this):

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

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')
    }
  })
}

Вы заметили, что firstName доступен в области лексики у обоих Human и Developer? Это значит, что мы можем пропустить this и использовать firstName напрямую в обоих blueprints

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 () { /* ... */ }
  })
}

Видите? Это означает, что вы можете безопасно пропустить this из своего кода при использовании фабричных функций.

Вердикт для this 

Проще говоря, Классы требуют this, а Фабричные функции — нет. Я предпочитаю фабричные функции здесь, потому что:

• Контекст this может измениться (что может сбить с толку).

• Код, написанный с помощью фабричных функций, короче и чище (так как мы можем использовать инкапсулированные переменные без написания this.#variable).

Далее следует последний раздел, где мы строим простой компонент вместе с Классами и фабричными функциями. Вы увидите, чем они отличаются и как использовать слушателей событий с каждой особенностью

Классы против Фабричных функций — Слушатели событий

Большинство статей по объектно-ориентированному программированию показывают примеры без слушателей событий. Эти примеры может быть проще понять, но они не отражают работу, которую мы выполняем как фронтенд-разработчики. Работа, которую мы выполняем, требует наличия слушателей событий — по простой причине — потому что нам нужно строить вещи, которые полагаются на пользовательский ввод.

Так как слушатели событий меняют контекст this, они могут сделать классы неудобными в работе. В то же время они делают фабричные функции более привлекательными.

Но на самом деле это не так.

Изменение в this не имеет значения, если вы знаете, как справиться с this как в Классах, так и в Фабричных функциях. Мало статей на эту тему, так что я подумал, что было бы неплохо дополнить эту статью простым компонентом, использующим разнообразие по объектно-ориентированному программированию.

Построение счетчика

В этой статье мы построим простой счетчик. Мы будем использовать все, что вы узнали в этой статье — включая приватные переменные.

Допустим, счетчик содержит две вещи:

  1. Сам счетчик

  2. Кнопку для увеличения количества

Вот самый простой HTML для счетчика:

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

Построение счетчика с Классами

Для простоты мы попросим пользователей найти и передать HTML счетчика в класс Counter.

class Counter () {
  constructor (counter) {
    // Do stuff 
  } 
}

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

Нам нужно получить два элемента в классе Счетчик:

  1. <span>, который содержит счетчик — нам нужно обновить этот элемент, когда счетчик увеличится.

  2. Кнопка <button> — нам нужно добавить слушателя событий в этот класс элементов.

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

Мы инициализируем count и устанавливаем ее в то, что показывает элемент countElement. Мы будем использовать приватную #count, так как счетчик не должен быть выставлен в другом месте.

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

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

Когда пользователь нажимает кнопку <button>, мы хотим увеличить #count. Мы можем сделать это с помощью другого метода. Мы назовем этот метод increaseCount.

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

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

Далее, нам нужно обновить DOM новым #count. Для этого создадим метод под названием updateCount. Мы будем называть updateCount из increaseCount

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

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

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

Теперь мы готовы добавить слушателя событий.

Добавление слушателя событий

Мы добавим слушателя событий в this.buttonElement. К сожалению, мы не можем сразу использовать increaseCount в качестве обратного вызова. Вы получите ошибку, если попробуете.

class Counter () {
  // ...

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

  // Methods
}

Вы получаете ошибку, потому что this указывает на buttonElement. (Это контекст слушателя событий.) Вы увидите buttonElement, если войдете в консоль.

Нам нужно изменить значение this обратно в экземпляр для increaseCount, чтобы все заработало. Есть два способа сделать это:

  1. Использовать bind

  2. Использовать стрелочные функции 

Большинство людей используют первый метод (но второй проще).

Добавление слушателя событий с bind

Bind возвращает новую функцию. Она позволяет изменить this на первый переданный аргумент. Обычно люди создают слушателей событий, вызывая bind(this).

class Counter () {
  // ...

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

  // ...
}

Это работает, но не очень приятно читать. Он также неудобен для начинающих, потому что bind рассматривается как продвинутая функция JavaScript.

Стрелочные функции 

Второй способ — использование стрелочных функций. Стрелочные функции работают, так как они сохраняют значение this в лексическом контексте.

Большинство людей пишут методы внутри обратного вызова стрелочных функций, как это:

class Counter () {
  // ...

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

  // Methods
}

Это работает, но это долгий путь. Вообще-то, есть короткий путь.

Вы можете создать increaseCount с помощью функций со стрелками. Если вы сделаете это, то это значение для increaseCount будет привязано к значению экземпляра сразу.

Итак, вот код, который вам нужен:

class Counter () {
  // ...

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

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

  // ...
}

Код 

Вот полная версия кода на основе классов (с использованием стрелочных функций).

HTML

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

CSS

html {
  font: 1em sans-serif;
}

body {
  padding: 1em;
}

p {
  font-size: 2em;
  line-height: 1;
  margin: 1rem 0;
}

button {
  font: inherit;
  padding: 0.5em 0.75em;
  background-color: #eee;
  border: 1px solid #222;
  color: black;
}

Babel

class Counter {
  #count = 0

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

    // Initializes count
    this.#count = parseInt(this.countElement.textContent)

    // Adds event listener
    this.buttonElement.addEventListener('click', this.increaseCount)
  }

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

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

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

Результат:

Создание счетчика с фабричными функциями

Мы сделаем то же самое здесь. Мы заставим пользователей передать HTML счетчика на фабричный Counter.

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

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

Нам нужно получить два элемента из counter<span> и <button>. Здесь мы можем использовать обычные переменные (без this), потому что они уже являются приватными. Мы не будем их раскрывать.

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

Мы инициализируем переменную счетчика значением, присутствующим в HTML.

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

  let count = parseInt(countElement.textContext)
}

Мы увеличим переменную count методом increaseCount. Здесь можно выбрать обычную функцию, но мне нравится создавать метод, чтобы все было аккуратно и аккуратно.

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

Наконец, мы обновим счет методом updateCount. Мы также вызовем updateCount из метода increaseCount.

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

    updateCount () {
      increaseCount()
    }
  }
}

Обратите внимание, что я использовал counter.updateCount вместо this.updateCount? Мне это нравится, потому что counter более понятен по сравнению с this. Я также делаю это, потому что новички могут также сделать ошибку с this внутри фабричных функций (о которых я расскажу позже).

Добавление слушателей событий

Мы можем добавить слушателей событий в buttonElement. Когда мы это сделаем, то сразу же сможем использовать counter.increaseCount в качестве обратного вызова.

Мы можем сделать это, потому что мы не использовали this, так что это не имеет значения, даже если слушатели события изменят значение this.

function Counter (counterElement) {
  // Variables 

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

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

Понимание this

this можно использовать в фабричных функциях. Но вы должны использовать this в контексте метода.

В следующем примере, если вы вызываете counter.increaseCount, JavaScript также вызовет counter.updateCount. this работает, так как указывает на переменную counter.

function Counter (counterElement) {
  // Variables 

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

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

К сожалению, слушатель событий не смог бы работать, так как this значение было изменено. Вам понадобится то же самое, что и к классам — с функциями bind или стрелками, чтобы заставить слушателя событий снова работать.

И это приводит меня ко второму пониманию.

Второе понимание this 

Если вы используете синтаксис фабричной функции, вы не можете создавать методы с помощью стрелочных функций. Это связано с тем, что методы создаются в контексте простой функции.

function Counter (counterElement) {
  // ...
  const counter = {
    // Do not do this. 
    // Doesn't work because `this` is `Window`
    increaseCount: () => {
      count = count + 1
      this.updateCount()
    }
  }
  // ...
}

Так что я настоятельно рекомендую полностью пропустить this, если вы используете фабричные функции. Так намного проще.

Код

HTML

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

CSS

html {
  font: 1em sans-serif;
}

body {
  padding: 1em;
}

p {
  font-size: 2em;
  line-height: 1;
  margin: 1rem 0;
}

button {
  font: inherit;
  padding: 0.5em 0.75em;
  background-color: #eee;
  border: 1px solid #222;
  color: black;
}

Babel

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

  let count = parseInt(countElement.textContent) || 0

  const counter = {
    updateCounter () {
      countElement.textContent = count
    },

    increaseCount: (event) => {
      count = count + 1
      counter.updateCounter()
    }
  }

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

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

Вердикт для слушателей события

Слушатели событий изменяют this значение, поэтому мы должны быть очень осторожны в использовании значения this. Если вы используете Классы, я рекомендую создавать обратные вызовы слушателей событий с стрелочными функциями, так что вам не придется использовать bind.

Если вы используете фабричные функции, я рекомендую пропустить this полностью, потому что это может сбить вас с толку. Вот и все!

Заключение

Мы говорили о четырех разновидностях Объектно-ориентированного программирования таких как:

  1. Функции конструктора

  2. Классы

  3. OLOO

  4. Фабричные функции 

  • Во-первых, мы пришли к выводу, что Классы и фабричные функции легче использовать с точки зрения кода.

  • Во-вторых, мы сравнили, как использовать Подклассы с Классами и Фабричными функциями. Здесь мы видим, что создание Подклассов проще с Классами, но Состав проще с Фабричными функциями.

  • В-третьих, мы сравнили Инкапсуляцию с Классами и Фабричными функциями. Здесь мы видим, что инкапсуляция с фабричными функциями естественна, как и в JavaScript, в то время как инкапсуляция с классами требует добавления переменной # перед переменными.

  • В-четвертых, мы сравнили использование this в Классах и Фабричных функциях. Я чувствую, что Фабричные функции здесь выигрывают, потому что this может быть неоднозначно. Написание this.#privateVariable также создает более длинный код по сравнению с использованием самой privateVariable.

Наконец, в этой статье мы построили простой Счетчик с Классами и Фабричными функциями. Вы научились добавлять слушателей событий в обе особенности Объектно-ориентированного программирования. Здесь обе особенности работают. Нужно просто быть осторожным, используете вы это или нет.

Вот и все!

Надеюсь, это прольет свет на объектно-ориентированное программирование на JavaScript для вас.

Если у вас есть вопросы по JavaScript или передовым разработкам в целом, не стесняйтесь обращаться ко мне. Посмотрим, чем я могу помочь!


Узнать подробнее о курсе "JavaScript Developer. Basic".

Посмотреть открытый урок на тему "Прототипное наследование в JavaScript".

Рекомендуем обратить внимание на смежные курсы:

  • JavaScript Developer. Professional

  • React.js Developer

  • Node.js Developer


ЗАБРАТЬ СКИДКУ

Источник: https://habr.com/ru/company/otus/blog/532946/


Интересные статьи

Интересные статьи

Я давно знаком с Битрикс24, ещё дольше с 1С-Битрикс и, конечно же, неоднократно имел дела с интернет-магазинами которые работают на нём. Да, конечно это дорого, долго, местами неуклюже...
Выход PostgreSQL 13, о возможностях которого мы уже писали, планируется только осенью. Ничего принципиально нового в нем уже не появится. Поэтому самое время перейти к PostgreSQL 14. ...
Первая часть Джек Трэмел, директор компьютерной фирмы, которого так и хочется сравнить с Дартом Вейдером. Интересно, почему?.. Позволяя крайнему сроку подписания лицензионного соглашения с ...
Raspberry PI 3 Model B+ В этом мануале мы разберем основы использования Swift на Raspberry Pi. Raspberry Pi — небольшой и недорогой одноплатный компьютер, потенциал которого ограничен лишь ...
Одной из «киллер-фич» 12й версии Битрикса была объявлена возможность отдавать статические файлы из CDN, тем самым увеличивая скорость работы сайта. Попробуем оценить практический выигрыш от использова...