Содержание

Варианты объектно-ориентированного программирования (на JavaScript)

Перевод: Zell Liew — The Flavors of Object-Oriented Programming (in JavaScript)

В своем исследовании я рассмотрел четыре подхода к объектно-ориентированному программированию в JavaScript:

  1. Использование функций конструктора
  2. Использование классов
  3. Объекты, связанные с другими объектами (OLOO)
  4. Использование фабричных функций

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

Чтобы принять это решение, мы не просто рассмотрим разные подходы, но и сравним их концептуальные аспекты:

  1. Классы против Фабричных функций — Наследование
  2. Классы против Фабричных функций — Инкапсуляция
  3. Классы против Фабричных функций — this
  4. Классы против Фабричных функций — Event listeners

Начнем с основ ООП в JavaScript.

Что такое объектно-ориентированное программирование?

Объектно-ориентированное программирование — это способ написания кода, который позволяет создавать разные объекты из объекта. Общий объект обычно называется blueprint (базовая схема), а созданные объекты — экземплярами.

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

Второй аспект объектно-ориентированного программирования касается структурирования кода, когда у вас есть несколько уровней blueprint элементов. Обычно это называется наследованием.

Третий аспект объектно-ориентированного программирования — это инкапсуляция, при которой вы скрываете определенные фрагменты информации внутри объекта, чтобы они были недоступны.

Если вам нужно нечто большее, чем это краткое введение, вот статья, которая знакомит с аспектами объектно-ориентированного программирования подробнее.

Начнем с основ — введение в четыре разновидности объектно-ориентированного программирования.

Четыре разновидности объектно-ориентированного программирования

Есть четыре способа использовать объектно-ориентированное программирование на JavaScript:

  1. Использование функции конструктора
  2. Использование классов
  3. Использование объектов, связанные с другими объектами
  4. Использование фабричных функций
Использование функций конструктора

Конструкторы — это функции, содержащие ключевое слово this.

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

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

Вы можете создать экземпляр с ключевым словом new.

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
Синтаксис Class

Ключевое слово class считаются «синтаксическим сахаром» функций-конструкторов.

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

Классы могут быть написаны со следующим синтаксисом:

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

Обратите внимание, что функция constructor содержит тот же код, что и синтаксис конструктора выше? Нам используем это, поскольку мы хотим инициализировать значения для this. (Мы можем пропустить constructor , если нам не нужно инициализировать значения. Подробнее об этом позже в разделе «Наследование»).

На первый взгляд кажется, что классы уступают конструкторам — потому что нужно писать больше кода! Но придержите лошадей и не делайте выводов на этом этапе. Нам есть еще много чего рассказать. Классы рассмотрим позже.

Как и раньше, вы можете создать экземпляр с ключевым словом new.

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

console. log(chris.firstName) // Chris
console.log(chris.lastName) // Coyier
Объекты, связанные с другими объектами (OLOO — Objects Linking to Other Objects)

OLOO был придуман и популяризирован Kyle Simpson. В OLOO вы определяете blueprint как обычный объект. Затем вы используете метод (часто называемый

init, но он не требуется, как конструктор для класса) для инициализации экземпляра.

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

Далее используете Object.create для создания экземпляра. После создания экземпляра необходимо запустить функцию инициализации.

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

Вы можете связать init после Object.create, если вы вернули его внутри init.

const Human = { init () { // ... return this } } const chris = Object.create(Human).init('Chris', 'Coyier') console.log(chris.firstName) // Chris console.log(chris.lastName) // Coyier
Фабричные функции

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

Вот самый простой способ создания фабричной функций:

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

Для создания экземпляров с фабричной функцией не требуется new. Вы просто вызываете функцию.

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

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

Теперь, когда мы рассмотрели эти четыре возможности ООП, давайте посмотрим, как объявлять свойства и методы для каждого из них, чтобы мы могли лучше понять работу с ними, прежде чем перейти к более подробному сравнению.


Объявление свойств и методов

Методы — это функции, объявленные как свойство объекта.

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

В объектно-ориентированном программировании есть два способа объявления свойств и методов:

  1. Непосредственно на экземпляре
  2. В прототипе

Давай научимся делать и то, и другое.

Объявление свойств и методов с помощью конструкторов

Если вы хотите объявить свойство непосредственно в экземпляре, вы можете записать свойство внутри функции-конструктора. Обязательно установите его как свойство для this.

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

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

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

Методы обычно объявляются в Prototype, потому что Prototype позволяет экземплярам использовать один и тот же метод. Это типа как «отпечаток кода».

Чтобы объявить свойства в прототипе, вам необходимо использовать свойство prototype.

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

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

Это может быть неудобно, если вы хотите объявить несколько методов в прототипе.

// Declaring methods on a prototype
Human.prototype.method1 = function () { /*...*/ }
Human.prototype.method2 = function () { /*...*/ }
Human.prototype.method3 = function () { /*...*/ }

Вы можете упростить задачу, используя функции слияния, например с помощью Object. assign.

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

Object.assign не поддерживает объединение функций Getter и Setter. Вам нужен другой инструмент. Вот почему. А вот инструмент, который я создал для объединения объектов с Getter и Setter.

Объявление свойств и методов с помощью классов

Вы можете объявить свойства для каждого экземпляра внутри функции constructor.

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

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

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

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

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

Объявлять несколько методов в классах так же проще, чем в конструкторах. Вам не нужен синтаксис Object.assign. Вы просто пишете больше функций.

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

  method1 () { /*...*/ }
  method2 () { /*...*/ }
  method3 () { /*...*/ }
}
Объявление свойств и методов с помощью OLOO

Вы можете использовать тот же процесс для объявления свойств и методов в экземпляре. Для этого назначьте их как свойство this.

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)

Чтобы объявить методы, просто опишите метод как обычный объект.

const Human = {
  init () { /*. ..*/ },
  sayHello () {
    console.log(`Hello, I'm ${this.firstName}`)
  }
}
Объявление свойств и методов с помощью фабричных функций

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

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

Нельзя объявлять методы в прототипе при использовании фабричной функций. Если вам действительно нужны методы в прототипе, вам нужно вернуть экземпляр Constructor, Class или OLOO. (Но не делайте этого, потому что в этом нет никакого смысла.)

// Do not do this
function createHuman (. ..args) {
  return new Human(...args)
}

Где объявлять свойства и методы

Следует ли объявлять свойства и методы непосредственно в экземпляре? Или вам стоит использовать прототип как можно чаще?

Многие люди гордятся тем, что JavaScript — это «язык прототипов» (что означает, что он использует прототипы). Из этого утверждения вы можете сделать предположение, что использование «Прототипов» лучше.

Настоящий ответ таков: это не имеет значения.

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

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

Я написал отдельную статью о понимании прототипов JavaScript, если вам интересно узнать больше.

Предварительный вердикт

Резюмируем, то что я написал выше. Но учтите это только мое собственное мнение!

  1. Классы лучше, чем конструкторы, потому что в классах проще написать несколько методов.
  2. Использование OLOO довольно неудобно из-за Object.create. Некоторое время я пробовал использовать OLOO, но я всегда забываю написать Object.create.
  3. Проще всего использовать классы и фабричные функции. Проблема в том, что фабричные функции не поддерживают прототипы. Но, как я уже сказал, это не всегда имеет значение.

Остался один вопрос. Что нам выбрать классы или фабричные функции? Давай сравним их!


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

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

  1. Наследование
  2. Инкапсуляция
  3. this

Начнем с наследования.

Что такое наследование?

Наследование — это многозначное слово. На мой взгляд, многие в индустрии неправильно используют наследование. Слово «наследование» используется, когда вы получаете что-то откуда-то. Например:

  • Если вы получаете наследство от родителей, это означает, что вы получаете от них деньги и имущество.
  • Если вы наследуете гены от своих родителей, это означает, что вы получаете свои гены от них.
  • Если вы унаследовали процесс от своего учителя, это означает, что вы получаете этот процесс от него.

Довольно просто.

В JavaScript наследование может означать то же самое: это то что вы получаете свойства и методы из родительского проекта.

Это означает, что все экземпляры фактически наследуют свои blueprint. Они наследуют свойства и методы двумя способами:

  1. путем создания свойства или метода непосредственно при создании экземпляра
  2. через цепочку прототипов

У наследования в JavaScript есть второе значение: вы создаете текущую структуру на основе родительской структуры. Этот процесс более точно называется Subclassing, но люди иногда также называют его наследованием.

Понимание Subclassing

Создание Subclassing — это создание производной структуры из общей начально структуры. Вы можете использовать любой вариант объектно-ориентированного программирования для создания Subclass.

Поговорим об этом с синтаксисом класса, потому что его легче понять.

Создание Subclassing  с помощью класса

Для создания Subclass, нужно использовать ключевое слово extends.

class Child extends Parent {
  // ... Stuff goes here
}

Например, предположим, что мы хотим создать класс Developer из класса Human.

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

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

Класс Developer унаследуется от Human следующим образом:

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

    // Add other methods
}

Примечание: super вызывает класс Human (также называемый «родительским») классом. Точнее он запускает constructor из Human. Если вам не нужен дополнительный код запуска, вы можете полностью пропустить constructor .

class Developer extends Human {
  // Add other methods
}

Допустим, Developer может писать код. Тогда мы можем добавить метод code прямо в Developer.

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

Вот пример экземпляра Developer:

const chris = new Developer('Chris', 'Coyier')
console.log(chris)
Subclassing с помощью фабричных функций

Есть четыре шага для создания Subclass с фабричными функциями:

  1. Создайте новую фабричную функцию
  2. Создайте экземпляр родительского класса
  3. Создать новую копию этого экземпляра
  4. Добавьте свойства и методы к этой новой копии

Процесс выглядит так:

function Subclass (. ..args) {
  const instance = ParentClass(...args)
  return Object.assign({}, instance, {
    // Properties and methods go here
  })
}

Мы можем использовать тот же пример — создание подкласса Developer — чтобы проиллюстрировать этот процесс. Вот фабричная функция Human:

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

Далее создаем Developer следующим образом:

function Developer (firstName, lastName) {
  const human = Human(firstName, lastName)
  return Object.assign({}, human, {
    // Properties and methods go here
  })
}

Затем добавляем метод code:

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

Вот пример создания экземпляра Developer:

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

Примечание. Вы не можете использовать Object.assign, если используете геттеры и сеттеры. Вам понадобится другой инструмент, например микс. Я объясняю это в этой статье.

Переопределение родительского метода

Иногда вам нужно переопределить родительский метод внутри подкласса. Это можно сделать так:

  1. Создать одноименный метод
  2. Вызвать родительский метод (необязательно)
  3. Изменить все, что нужно, в методе подкласса

С классами процесс выглядит так:

class Developer extends Human {
  sayHello () {
    // Calls the parent method
    super. sayHello() 

    // Additional stuff to run
    console.log(`I'm a developer.`)
  }
}

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

С фабричными функциями это выглядит так:

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

  return Object.assign({}, human, {
      sayHello () {
        // Calls the parent method
        human.sayHello() 

        // Additional stuff to run
        console.log(`I'm a developer.`)
      }
  })
}

const chris = new Developer('Chris', 'Coyier')
chris.sayHello()
Наследование против Композиции

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

«Предпочитайте композицию объектов наследованию классов», «Design Patterns: Elements of Reusable Object Oriented Software»

«В информатике составной тип данных — это любой тип данных, который может быть сконструирован в программе с использованием примитивных типов данных языка программирования и других составных типов. […] Построение составного типа известно как композиция ». ~ Википедия

Итак, давайте посмотрим на композицию глубже и разберемся, что это такое.

Понимание композиции

Композиция — это соединение двух вещей вместе. Речь идет о слиянии. Самый распространенный (и самый простой) способ объединения объектов — Object.assign.

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

Использование композиции можно лучше объяснить на примере. Допустим, у нас уже есть два подкласса: Designer и Developer. Designers могут проектировать, а Developer могут писать код. И Designers, и Developer наследуют от класса Human.

Пример:

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

Теперь предположим, что вы хотите создать третий подкласс. Этот подкласс представляет собой смесь Designer и Developer — они могут проектировать и кодировать. Назовем его DesignerDeveloper (или DeveloperDesigner, как вам нравится).

Как бы вы создали третий подкласс?

Мы не можем расширять классы Designer и Developer одновременно. Это невозможно, потому что мы не можем решить, какие свойства будут первыми. Это часто называют проблемой алмаза (The Diamond Problem).

Проблема с алмазом может быть легко решена, если мы сделаем что-то вроде Object.assign — где мы будем отдавать приоритет одному объекту над другим. Если мы воспользуемся подходом Object.assign, мы сможем расширить такие классы. Но это не поддерживается в JavaScript.

// Doesn't work
class DesignerDeveloper extends Developer, Designer {
  // ...
}

Поэтому нам нужно полагаться на композицию.

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

На практике это может выглядеть так:

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

В этом случае мы можем полностью пропустить Human и создать три разных класса в зависимости от их навыков.

Вот код для 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)

Вы можете сделать то же самое с Developer и Designer.

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

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

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

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

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

Композиция с фабричными функциями

Композиция с фабричными функциями по сути добавляет общие методы в возвращаемый объект.

function DesignerDeveloper (firstName, lastName) {
  return {
    firstName,
    lastName,    
    code: skills. code,
    design: skills.design,
    sayHello: skills.sayHello
  }
}
Наследование и Композиция одновременно

Никто не говорит, что нельзя использовать наследование и композицию одновременно. Мы можем!

Если взять пример, который мы до сих пор разобрали, Designer, Developer и DesignerDeveloper Human по-прежнему останется Human. Но подклассы будут наследоваться от Human.

Пример, в котором мы используем и наследование, и композицию с синтаксисом класса.

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

То же самое и с фабричными функциями:

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
  }
}
Subclassing в реальном мире

И последнее, о подклассах и композиции. Несмотря на то, что эксперты отметили, что композиция более гибкая (и, следовательно, более полезная), Subclassing все же имеет свои достоинства. Многие вещи, которые мы используем сегодня, построены на стратегии Subclassing.

Например: событие click , которое мы знаем и любим, — это MouseEvent. MouseEvent — это подкласс UIEvent, который, в свою очередь, является подклассом Event.

Другой пример: элементы HTML — это подклассы Nodes. Вот почему они могут использовать все свойства и методы Nodes.

Предварительный вердикт

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

Далее мы рассмотрим классы и фабричные функции более подробно.


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

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

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

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

  1. Наследование
  2. Инкапсуляция
  3. this

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

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

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

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

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

Самая простая форма инкапсуляции — это область видимости блока.

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

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

const food = 'Hamburger'

{
  console.log(food)
}

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

{
  const food = 'Hamburger'
}

console.log(food)

Примечание. Переменные, объявленные с помощью var, не учитывают область действия блока. Вот почему я рекомендую вам использовать let или const для объявления переменных.

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

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

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()
Инкапсуляция и объектно-ориентированное программирование

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

Давайте рассмотрим это на примере, чтобы прояснить ситуацию. Допустим, у нас есть 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
  }

  getFuel () {
    return this._fuel
  }

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

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

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

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

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

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

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

class Car {
  #fuel = 50
}

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

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

Примечание: лучше использовать геттеры и сеттеры вместо 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

Мы можем создать функции получения и установки для использования этой частной переменной 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. В event listener

Я подробно рассмотрел эти шесть контекстов тут. Прочтите эту статью, если вам нужна помощь в понимании this.

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

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

Еще не вернулся? Хороший. Пошли!

Использование 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 указывает на 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() 

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

Подробный пример

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

У нас есть blueprint Developer, созданный на основе Human. Developer могут кодировать, поэтому у них будет метод code. Developer также хотят объявить себя разработчиками, поэтому нам нужно перезаписать sayHello и добавить в консоль «Я разработчик».

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

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

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

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

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

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

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

Разработчики также хотят объявить себя разработчиками. Нам нужно перезаписать sayHello и добавить в консоль «Я разработчик». Мы делаем это, вызывая метод SayHello для Human. Сделаем это с помощью super.

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

  sayHello () {
    super. sayHello()
    console.log(`I'm a developer`)
  }
}
Пример с фабричными функциями (с this)

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

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

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

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

Разработчики также хотят объявлять себя разработчиками. Нам нужно перезаписать sayHello и добавить в консоль «Я разработчик».
Мы делаем это, вызывая метод SayHello для Human. Мы можем сделать это на примере 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)

Вот полный код с использованием функций Factory (с 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 непосредственно в обоих схемах.

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, а фабричные функции — нет. Я предпочитаю использовать фабричные функции, потому что:

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

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

Классы и фабричные функции — Event listeners

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

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

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

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

Создание счетчика

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

Скажем, счетчик содержит две вещи:

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

Вот простейший HTML-код счетчика:

<div>
  <p>Count: <span>0</span>
  <button>Increase Count</button>
</div>
Создание счетчика с использованием классов
class Counter () {
  constructor (counter) {
    // Do stuff 
  } 
}

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

Нам нужно получить два элемента в классе 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. К сожалению, мы не можем сразу использовать extensionCount в качестве обратного вызова. Если попробуем, то получим сообщение об ошибке.

class Counter () {
  // ...

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

  // Methods
}

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

Нам нужно изменить значение 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 с помощью стрелочных функций. Если вы сделаете это, значение this для increaseCount будет сразу же привязано к значению экземпляра.

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

class Counter () {
  // . ..

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

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

  // ...
}
Полная версия кода

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

https://codepen.io/anon/embed/VwabbEE?height=450&theme-id=1&slug-hash=VwabbEE&default-tab=result

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

Здесь мы сделаем то же самое.

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

const counter = Counter(document.querySelector('.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 было изменено. Вам потребуется то же самое, что и с классами — с функциями привязки или стрелочными функциями, чтобы снова заставить обработчик событий работать.

И это подводит меня ко второй проблеме.

Вторая уловка с this

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

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

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

Код

https://codepen.io/anon/embed/WNwjjaQ?height=450&theme-id=1&slug-hash=WNwjjaQ&default-tab=result

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

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

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


Заключение

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

  1. Использование функций конструктора
  2. Использование классов
  3. Объекты, связанные с другими объектами (OLOO)
  4. Использование фабричных функций

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

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

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

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

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

Я надеюсь, что это статья прольет свет на объектно-ориентированное программирование в JavaScript для вас. Если вам понравилась эта статья, возможно, вам понравится мой курс JavaScript, Learn JavaScript, где я объясню (почти) все, что вам нужно знать о JavaScript.

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

Была ли вам полезна эта статья?

[3 / 5]

Основные принципы ООП на языке JavaScript

Это первая часть про ООП в JS, или Объектно Ориентированное Программирования, в ней вы изучите основные принципы ООП в JavaScript.

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

Классы и объекты:

Перед тем как работать с классами и объектам в JS, рассмотрим что это вообще такое.

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

Также класс принято считать типом данных, как числа и строки, но которые создаёт сам программист, а объект соответственно значением этого типа.

JavaScript классы и объекты:

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

// Создание класса Car

function Car (marka) {

    // Создание метода класса

    this.showMarka = function () {

        // Вывод сообщения о марки

        alert(this. marka)

    }

}

 

// Объявление объекта класса Car

let car = new Car(«ВАЗ»);

car.showMarka();

Тут в начале создаём класс виде функции, внутри делаем метод showMarka() для отображение сообщения, в котором будет написано название машины, его назначаем во время объявление класса, через ключевое слово new,  после вызываем метод, что такое метод, будет чуть ниже.

Важно:

Важно понимать, что любая функция это объект и любую функцию можно использовать как класс или объект.

Вот что должно получится:

Вот пример с ключевым словом class.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

«use strict»;

 

// Объявление класса Car

class Car {

    // Конструктор класса

    constructor(marka) {

        // Создаём свойство marka

        this. marka = marka;

    }

 

    // Метод Для показа марки

    showMarka() {

        // Вывод сообщения о марки

        alert(this.marka)

    }

}

 

// Объявление объекта класса Car

let car = new Car(«ВАЗ»)

// Запуск метода showMarka()

car.showMarka()

Как видите всё плюс/минус одинаково, единственное появился constructor, но о нём следующей части, в этой сейчас разберём различия если использовать ключевое слово class.

  • Обязательно при объявление нужно использовать слово new;
  • Класс с точки зрения видимости ведёт себя как let;
  • Все методы работают в режиме use strict, даже если вы его не указали;
  • У каждого метода есть родитель;
  • Нельзя перечислить методы через for in;

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

JS методы и свойства:

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

Но тут стоит сказать, что как говорилось выше, каждая функция это объект, и внутри класса метод тоже может работать как объект, внутри объекта.

Свойства как провела объявляются в конструкторе класса, методы же вне конструктора, а внутри класс.

Примечание:

Если вы не можете понять как это выглядит, то посмотрите второй пример, где показывается создание класса через class.

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

Ключевое слово this:

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

Если кратко говоря, то this используется для обращение к контексту и когда мы тут его используем, то обрушаемся к контексту класса или объекта.

Подробнее про контекст прочитаете в одной из следующих частей учебника.

Вывод:

Это не очень большая часть, так как, про классы и объекты в JS особо нечего сказать, здесь были показаны только основные принципы ООП JavaScript.

Подписываетесь на соц-сети:

Оценка:

(Пока оценок нет)

Загрузка…

Дополнительно:

JavaScript и ООП — привычки и стереотипы

Эта статья про скриптовый язык JavaScript, прототипную модель и DOM браузеров. Цель — размышление на досуге.

Сразу приведу свои ключевые позиции и утверждения:

  • Отличительная черта ООП — полиморфизм. И то спорно.
  • Абстрация, инкапсуляция и наследование от стандартных классов — это НЕ ООП, это всегда было и будет везде.
  • Полиформизм не нужен при слабой типизации, а в JavaScript полиформизм вообще ненужная фантазия, как и абстрактные классы.
  • Прототипное программирование может трактоваться как стиль ООП-подхода, но это совершенно другое мировоззрение.
  • Прототипная модель самая простая и гибкая при наследовании и инкапсуляции, сложно даже представить что-то лучшее.
  • Прототипная модель позволяет наиболее естественно эмулировать объекты реального мира (текущее состояние и контекст).
  • Прототипная модель идеально подходит для скриптовых языков (imho, JavaScript — ассемблер веба и лучший скриптовый язык).

Привычки, Навыки, Стереотипы.

Привычки и ритуалы являются очень полезными и необходимыми качествами человека.
На самом деле, если-бы мы каждом шаге задумывались о перемещении тела, то не смогли-бы нормально ходить.
Часто проще сказать «это непринято», «так не делают» — это экономит время и другие ресурсы.
Привычки возникают в процессе воспитания и обучения (навязанные), а так же в результате быстрой адаптации к окружению (выработанные).

У привычек есть недостаток — мы не способны отследить все детали меняющегося мира вокруг нас, и внезапно можем попасть в неприятную ситуацию — когда поступаем привычно, но в корне неправильно.
Все время от времени попадают в такие ситуации, отсюда и поговорки типа «не зарекайся», «век живи — век учись» и т.д.

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

Стереотип — психологически удобная позиция, не объективная, зависит от набора привычек и умственных способностей, целеустремлённости, лени, пороков. Помогает объединяться в социальные и/или рабочие группы, так как общие разделяемые стереотипы позволяют достигать психологического комфорта наименьшими усилиями. Религиозные секты и срачи — самый показательный пример тяжёлых запущенных стереотипов.

ООП и JavaScript

Термин «ООП» размыт. Прототипное программирование считают стилем ООП, мне это непонятно, зачем навешивать ярлыки?
Стремление к «классическому ООП» с абстрактными классами не всегда оправдано, особенно в скриптовых приложениях со слабой типизацией.

Для чего суют абстрактные ООП-классы в JavaScript браузеров? Там-же прототипы, замыкания, динамика DOM-объектов, контекст выполнения!
Специфика DOM такова, что любой объект может содержать в разные моменты времени картинку, текст, другой объект, и т.д.
JavaScript был придуман для работы с DOM браузеров, плюс дополнительно реализованы простые и гениальные идеи.

ООП-стереотипы, применяемые при front-end разработке на JavaScript, противоречат динамике DOM, не используют гибкость и мощь прототипов, используя прототипы для реализации синтаксического сахара ООП, причём без толку, потому что нет компилятора, который будет следить за выстелами в ногу (контрольный выстрел в голову был произведён в момент решения использовать абстрактные классы в JS браузера).

Вообще и без JS существует много вопросов к ООП: Почему ООП провалилось?

Некоторые догадываются и что-то подозревают. Не все, умеющие думать, вымерли.

Существуют ООП-надстройки над JS, например TypeScript и другие. Поэтому странно, что лезут в нативный JS и «расширяют» его ООП-шаблонами.
Почему-то в ассемблер ООП массово не суют. Видимо, разница в пороге вхождения и массовости. Повальное использование JS-библиотек и ООП-фреймворков, одновременно с неспособностью решать элементарные задачи на JavaScript. Любой кухарке достаточно освоить ООП, остальное сделают фреймворки и среда разработки. Почему до сих пор нет стандартного ООП-класса для кнопки «Сделать всё»?

В EcmaScript 6 добавили поддержку ООП-классов. Но JavaScript бывает разный! В браузерах, например, ООП на классах противоречит DOM 🙂
Зачем себя обманывать и заниматься абстрактным программированием ради абстрактного программирования?
Умение делать просто и эффективно — это редкий навык, особенно это видно во время анализа существующих веб-решений.
«Когда нужно забить гвоздь, я беру молоток и бью по шляпке» — что мешает так работать? Что мешает думать? Ответ см. ниже.

Итого

Моё мнение — использование абстрактных классов в JavaScript при работе в браузерах — это тяжёлый стереотип и дикий предрассудок.

Это лишь подтверждает, что средний уровень веб-программистов довольно низок, во всех отношениях. Низкий порог вхождения…
Тонны красивого лишнего кода, включая CSS и ООП фреймворки, тормоза в работе и лишняя нагрузка на устройства пользователей.
Такими темпами, скоро каждая домохозяйка будет разбираться в правильном коде и рассуждать о перспективных технологиях.

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

Особенности JavaScript / Методология / БЭМ

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

Декларативность JavaScript в БЭМ-проекте проявляется в следующем:

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

Подробнее о применении уровней переопределния в JavaScript

Принципы ООП в JavaScript по БЭМ

В БЭМ-методологии к JavaScript применяются основные принципы объектно-ориентированного программирования (ООП).

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

В БЭМ JavaScript-реализация одного блока отделена от другого. Каждый блок предоставляет API для взаимодействия с другими блоками.

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

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

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

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

Примеры реализации доступны в документации к i-bem. js.

Представление динамических блоков в DOM

Блокам с JavaScript-реализацией могут соответствовать узлы в HTML. В этом случае говорится о том, что блоки имеют DOM-представление.

В простейшем случае блок соответствует DOM-узлу один к одному. Однако DOM-узел и блок — это не всегда одно и то же. Можно разместить несколько блоков на одном DOM-узле (это называется микс), а также реализовать один блок на нескольких DOM-узлах.

Существуют блоки без DOM-представления. В JavaScript они представлены в виде объектов, имеющих свои методы.

Примеры реализации доступны в документации к i-bem.js.

Взаимодействие блоков

БЭМ-методология предполагает работу с независимыми блоками. Однако на практике полная независимость блоков недостижима.

Блоки могут взаимодействовать друг с другом с помощью:

  • Подписки на события других экземпляров блоков.
  • Подписки на изменения модификаторов.
  • Непосредственного вызова методов других экземпляров блоков или статических методов класса другого блока.
  • Любых паттернов взаимодействия. Например, канала событий: все коммуникации происходят благодаря сообщениям, которые компоненты публикуют и слушают с помощью посредника.

Примеры реализации доступны в документации к i-bem.js.

БЭМ-методология рекомендует выстраивать взаимодействие между блоками в иерархическом порядке в соответствии с их расположением в DOM-дереве. Вложенный блок не должен ничего знать о родительском блоке, так как это нарушает принцип независимости компонентов.

Взаимодействие блока с его элементами

Элемент — это внутренняя реализация блока. В БЭМ-методологии принято реализовывать дополнительные хелперы блока для работы с его элементами. Обращение напрямую к элементу другого блока невозможно. Взаимодействие с элементом происходит только только через API блока, которому принадлежит данный элемент.

Если вы заметили ошибку или хотите чем-то дополнить статью, вы всегда можете или написать нам об этом на Гитхабе, или поправить статью с помощью prose.io.

Как Я Понимаю Ооп В Javascript?

JS имеет один сложный тип данных: Object. Все в JS является либо объектом, либо, в случае примитивов, помещается в объект, чтобы можно было использовать методы (затем распаковывать обратно в примитив). Это чисто ОО. _However_, JS использует прототипные ОО, а не классические ОО. Это все еще ОО, но книги по общим методам / шаблонам ОО вас никуда не приведут и приведут к крайне плохому JS-коду.

По сути, все начинается с нуля. Затем у вас есть объект с именем `Object.prototype`, который наследуется от нуля. Object.prototype имеет некоторые свойства и методы (например, свойство `length` или метод` toString`). Сам объект `Object` ссылается на` Object.prototype` и имеет доступ к этим свойствам и методам. Все остальное наследуется от этого объекта таким же образом: например, Array. Массив ссылается на свой `Array.prototype`, который имеет свойства и методы (например, сверхспособность` length` или метод `map`). Эти свойства доступны только для массивов, но поскольку они наследуются от Object, массивы также имеют доступ к определенным там свойствам и методам (длина указывается, но работает toString). Если вы расширяете объект массива, как `class MyArray extends Array {`, вы можете добавить свои собственные методы, которые будут присоединены к прототипу MyArray, который наследует методы из Array, который наследует методы из Object…. И т. Д.

Он имеет синтаксическую структуру, называемую `класс`, которую можно заставить работать _kinda_ аналогично классу, скажем, в C ++ / C # / Java / Ruby. Но это просто синтаксический сахар для структур-прототипов, чтобы объединить миллион и одну раскрученную вручную версию классов, появившуюся за эти годы.

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

По сути, разбирайтесь в прототипах ОО, не используйте шаблоны GoF OO, предназначенные для разработчиков на C ++ / Java, не пытайтесь слепо эмулировать классические ОО, при необходимости применяйте методы функционального программирования (в большинстве случаев, как выясняется)

Принципы объектно-ориентированного программирования — HTML, CSS, JavaScript, Perl, PHP, MySQL: Weblibrary.biz

В этой главе вы познакомитесь с терминологией объектно-ориентированного программирования (ООП) и убедитесь в важности применения в программировании объектно-ориентированных концепций. Бытует мнение, что во многих языках, таких как C++ и Microsoft Visual Basic, есть “поддержка объектов”, однако на самом деле лишь немногие из них следуют всем принципам, составляющим основу ООП, и язык С# — один из них. Он изначально разрабатывался как настоящий объектно-ориентированный язык, в основе которого лежит технология компонентов. Поэтому, чтобы чтение этой книги принесло максимальную пользу, вам следует очень хорошо усвоить представленные здесь понятия.

Мне известно, что читатели, стремящиеся поскорее “окунуться” в код, часто пропускают такого рода концептуальные главы, однако тому, кто собирается стать “гуру объектов”, настоятельно рекомендую прочитать эту главу. Представленные здесь сведения будут полезны и тем, кто только начинает знакомиться с ООП. Кроме того, имейте в виду, что в дальнейших главах мы будем оперировать терминами и понятиями, рассмотренными в данной главе.

Повторяю: многие языки претендуют называться “объектно-ориентированными;” либо “основанными на объектах”, но лишь немногие являются таковыми на самом деле. Взять, например, C++. Ни для кого не секрет, что своими корнями он глубоко уходит в язык С, и ради поддержки программ, написанных когда-то на С, в нем пришлось пожертвовать очень многими идеями ООП. Даже в Java есть вещи, не позволяющие считать его по-настоящему объектно-ориентированным языком.

Прежде всего, я имею в виду базисные типы и объекты, которые обрабатываются и ведут себя по-разному. Однако в центре внимания этой главы будет не анализ того, насколько полно реализованы принципы ООП в тех или иных языках программирования, а объективное и фундаментальное изложение самих этих принципов.

Еще мне хотелось бы отметить, что объектно-ориентированное программирование — это не только модный термин (хотя для многих это именно так), не только новый синтаксис или новый интерфейс прикладного программирования (API). ООП — это целый набор концепций и идей, позволяющих осмыслить задачу, стоящую при разработке компьютерной программы, а затем найти путь к ее решению более понятным, а значит, и более эффективным способом.

Моя первая работа была связана с языком Pascal, на котором я писал прикладные программки по выпуску бухгалтерских отчетов и составлению маршрутов гастролей для балета на льду. Со временем я стал программировать на PL/I и RPGIII (и RPG/400), а потом и на С. В каждом случае мне было нетрудно применять знания, приобретенные ранее. Каждый следующий язык учить было проще — независимо от его сложности. Это связано с тем, что все языки до перехода к программированию на C++ были процедурными и отличались главным образом синтаксисом.

Сразу хочу предупредить новичков в ООП: опыт, полученный при работе с не объектно-ориентированными языками, вам не пригодится. Объектно-ориентированное программирование — это иной способ осмысления, формулирования и решения задач по созданию программ. Практика показывает, что начинающие программисты намного быстрее овладевают объектно-ориентированными языками, чем те, кто начинал с процедурных языков вроде BASIC, COBOL и С. Им не нужно “забывать” навыки работы с процедурами, которые лишь мешают в освоении ООП. Тут лучше всего начинать “с чистого листа”. Если вы долгие годы программировали на процедурных языках и С# — ваш первый объектно-ориентированный язык, то советую набраться терпения и реализовать предлагаемые мной идеи еще до того, как у вас опустятся руки и вы скажете: “Пожалуй, я обойдусь и (вставьте сюда название своего любимого процедурного языка)”. Но тот, кто прошел трудный путь от программирования процедур к ООП, скажет, что игра стоит свеч. У программирования на объектно-ориентированном языке масса преимуществ, причем это относится не только к созданию более эффективного кода, но и к модификации и расширению возможностей уже имеющихся систем. Многим поначалу такое утверждение не кажется столь очевидным. Однако почти 20 лет разработки ПО (включая 8 последних лет на объектно-ориентированных языках) убедили меня, что концепции ООП, применяемые с умом, действительно оправдывают возлагаемые на них надежды. А теперь, закатав рукава, разберемся, из-за чего весь сыр-бор.

12 полезных книг по JavaScript — Блог HTML Academy

Делимся подборкой книг, которая пригодится любому программисту (но особенно веб–разработчику) — в ней 12 книг, от подробных руководств по JavaScript до классики Роберта Мартина о чистом коде.

Изучаем программирование на JavaScript

Эрик Фримен, Элизабет Робсон

«Изучаем программирование на JavaScript»

Если вы начинаете путь в разработке, и пока для  вас программирование больше похоже на магию — присмотритесь к этой  книге. Она познакомит вас с одним из самых популярных языков программирования — JavaScript. Основная её особенность — повествование в стиле комиксов. Обучение строится через иллюстрации и многочисленные рассуждения в виде прямой речи героев.

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

Серия «Вы не знаете JavaScript»

Кайл Симпсон

«Типы и грамматические конструкции JS»

В одной из предыдущих книжных подборок мы рассказывали о книге Кайла Симпсона «ES6 и не только», которая отлично подходит для быстрого введения в новые возможности языка JavaScript и является продолжением серии «Вы не знаете JavaScript» (You don’t know JS).

В серии шесть книг — в них автор подробно рассказывает о нюансах работы языка JavaScript. Асинхронность, типы данных, прототипы, замыкания и другие темы разбираются максимально детально, да ещё и с практическими нетривиальными примерами. Первое издание вышло около пяти лет назад, но за это время книги не потеряли актуальности. 

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

Книги доступны для изучения в репозитории автора или в русском переводе.

Как устроен JavaScript

Дуглас Крокфорд

«Как устроен JavaScript»

Дуглас Крокфорд — известный специалист в мире JavaScript. Он рассказывает, как язык устроен «под капотом». Книга не учит программированию, а объясняет нюансы языка, поэтому рекомендуем тем, кто осилил Кайла Симпсона — это логическое продолжение его работ.

Вот неполный список вопросов, на которые отвечает книга:

  • Как устроены объекты
  • Почему ООП в JavaScript реализовано именно так
  • Как работают генераторы
  • Зачем нужен и как используется оператор this
  • Как JavaScript работает с числами.

Если книга не учит программировать, то зачем её читать разработчикам? Чтобы лучше понимать, как работает основной инструмент — язык программирования, а заодно подготовиться к очередному собеседованию. 

Отдельного внимания заслуживает глава про Wat. Это краткий обзор одноимённого доклада и разбор примеров, которые могут ввести в ступор даже опытных разработчиков.

Секреты JavaScript ниндзя

Джон Резиг, Блэр Либо

«Секреты JavaScript ниндзя»

Изучение JavaScript порой вводит в ступор даже разработчиков с опытом. При этом разобраться с основами языка обычно несложно — трудней понять нюансы и особенности. Например, замыкания, объекты высшего порядка, асинхронное выполнение кода и ряд других тем могут оказаться очень сложными для новичков. Разобраться с этими вопросами на реальных примерах поможет книга «Секреты JavaScript ниндзя».

Книга ориентирована на опытных JavaScript-программистов, которые хотят прокачать свои навыки. В книге есть главы с разбором синтаксиса, но основное внимание уделено практическому решению задач, тестированию кода, работе Event Loop и другим прикладным задачам. К концу 2020 года большая часть информации в книге остаётся актуальной, хотя некоторые главы и устарели.

Рефакторинг кода на JavaScript

Мартин Фаулер

«Рефакторинг кода на JavaScript»

Мартин Фаулер вряд ли нуждается в особом представлении. Он написал с десяток книг, где поделился богатым опытом написания качественного кода. Одна из последних его работ — «Рефакторинг кода на JavaScript».

Автор на примерах разбирает проблемные места и объясняет, как улучшить код. В результате читатель буквально слушает историю опытного коллеги и может сразу всё проверить на практике.

Книга читается легко, можно читать всё по порядку или отдельные главы. Совсем новичкам книга не подойдёт: перед прочтением важно разобраться с основными возможностями языка JavaScript.

Чистый код. Создание, анализ и рефакторинг

Роберт Мартин

«Чистый код. Создание, анализ и рефакторинг»

Все хотят писать чистый код — он понятен коллегам, его легко поддерживать и улучшать, в нём каждая строчка написана по делу.

Эта книга — классика и настоящая находка для разработчиков любого уровня. Роберт Мартин приводит много примеров хорошего и плохого кода, заостряет внимание на проектировании и типичных ошибках, которые возникают во время этого процесса. Суть книги — не в готовых решениях, а в том, что автор учит думать о чистом коде и делится подходами, которые помогут развить навык его написания. Примеры в книге приведены на языке Java, но существует репозиторий c адаптацией кода под JavaScript.

JavaScript. Подробное руководство

Дэвид Флэнаган

«JavaScript. Подробное руководство»

Если вы хотите глубоко погрузиться в основы JavaScript, то эта книга для вас. Книга очень большая, в ней рассматриваются все нюансы работы с языком — от сложения разных типов, до движков работы с кодом.

В книге разобраны логические операции, типы данных, выражения, операторы, работа в браузере и лексическая структура. Автор не забывает про смежные темы, которые помогут при разработке, например, регулярные выражения и серверный JavaScript.

Хорошие книги — сила, но без практики никуда.

Тренажёры по JavaScript дают навыки работы с живым кодом.

Написать код

Объектно-ориентированный JavaScript для начинающих — Изучите веб-разработку

Изложив основы, мы сосредоточимся на объектно-ориентированном JavaScript (OOJS) — в этой статье представлен базовый взгляд на теорию объектно-ориентированного программирования (ООП), а затем исследуется, как JavaScript эмулирует классы объектов с помощью функций-конструкторов. и как создавать экземпляры объектов.

Предварительные требования: Базовая компьютерная грамотность, базовое понимание HTML и CSS, знакомство с основами JavaScript (см. Первые шаги и Строительные блоки) и основами OOJS (см. Введение в объекты).
Цель: Чтобы понять основную теорию объектно-ориентированного программирования, как это связано с JavaScript («все является объектом») и как создавать конструкторы и экземпляры объектов.

Для начала давайте дадим вам упрощенное общее представление о том, что такое объектно-ориентированное программирование (ООП). Мы говорим упрощенно, потому что ООП может быстро стать очень сложным, и полное рассмотрение этого вопроса, вероятно, больше запутает, чем поможет.Основная идея ООП состоит в том, что мы используем объекты для моделирования вещей реального мира, которые мы хотим представить в наших программах, и / или обеспечиваем простой способ доступа к функциям, которые в противном случае было бы трудно или невозможно использовать.

Объекты могут содержать связанные данные и код, которые представляют информацию об объекте, который вы пытаетесь смоделировать, а также о функциональных возможностях или поведении, которые вы хотите, чтобы он имел. Данные объекта (а часто и функции) могут аккуратно храниться (официальное слово инкапсулировано ) внутри пакета объекта (которому можно дать конкретное имя для ссылки, которое иногда называют пространством имен ), что делает его легкая структура и доступ; объекты также обычно используются в качестве хранилищ данных, которые можно легко пересылать по сети.

Определение шаблона объекта

Рассмотрим простую программу, отображающую информацию об учениках и учителях в школе. Здесь мы рассмотрим теорию ООП в целом, а не в контексте какого-либо конкретного языка программирования.

Для начала мы могли бы вернуться к нашему типу объекта Person из нашей первой статьи об объектах, которая определяет общие данные и функциональность человека. Есть много вещей, которые вы, , могли бы знать о человеке (их адрес, рост, размер обуви, профиль ДНК, номер паспорта, важные черты личности…), но в данном случае нас интересует только отображение их имени, возраста, пола и интересов, а также мы хотим иметь возможность написать о них краткое введение на основе этих данных и заставить их поздороваться. Это известно как абстракция — создание простой модели более сложной вещи, которая представляет ее наиболее важные аспекты в виде, с которым легко работать для целей нашей программы.

Создание реальных объектов

Из нашего класса мы можем создать экземпляра объекта — объекты, которые содержат данные и функции, определенные в классе. Из нашего класса Person мы теперь можем создавать реальных людей:

Когда экземпляр объекта создается из класса, для его создания запускается функция конструктора класса . Этот процесс создания экземпляра объекта из класса называется экземпляром — экземпляр объекта создан экземпляром из класса.

Классы специалистов

В данном случае нам не нужны обычные люди — нам нужны учителя и ученики, которые являются более конкретными типами людей.В ООП мы можем создавать новые классы на основе других классов — эти новые дочерние классы (также известные как подклассы ) можно сделать так, чтобы наследовал данные и функции кода своего родительского класса , поэтому вы можете повторно использовать функциональность, общая для всех типов объектов, вместо того, чтобы дублировать ее. Если функциональные возможности разных классов различаются, вы можете при необходимости определять специализированные функции непосредственно на них.

Это действительно полезно — учителя и ученики имеют много общих черт, таких как имя, пол и возраст, поэтому удобно определять эти особенности только один раз.Вы также можете определить одну и ту же функцию отдельно в разных классах, поскольку каждое определение этой функции будет находиться в другом пространстве имен. Например, приветствие ученика может иметь форму «Йо, я [имя]» (например, Йо, я Сэм ), тогда как учитель может использовать что-то более формальное, например «Привет, меня зовут [Prefix] [lastName], и ​​я преподаю [Subject] ». (например, Здравствуйте, меня зовут мистер Гриффитс, я преподаю химию ).

Примечание : Модное слово для обозначения способности нескольких типов объектов реализовывать одну и ту же функциональность — это полиморфизм .На всякий случай вам было интересно.

Теперь вы можете создавать экземпляры объектов из дочерних классов. Например:

В оставшейся части статьи мы начнем смотреть, как теорию ООП можно применить на практике в JavaScript.

JavaScript использует специальные функции, называемые функциями конструктора , для определения и инициализации объектов и их функций. Они полезны, потому что вы часто будете сталкиваться с ситуациями, в которых вы не знаете, сколько объектов вы будете создавать; конструкторы предоставляют средства для эффективного создания сколь угодно большого количества объектов, присоединяя к ним данные и функции по мере необходимости.

Давайте рассмотрим создание классов с помощью конструкторов и создание из них экземпляров объектов в JavaScript. Прежде всего, мы хотели бы, чтобы вы сделали новую локальную копию файла oojs.html, который мы видели в нашей первой статье об объектах.

Простой пример

  1. Давайте начнем с рассмотрения того, как вы могли бы определить человека с нормальной функцией. Добавьте эту функцию в элемент сценария :
      function createNewPerson (name) {
      const obj = {};
      obj.name = имя;
      объектwelcome = function () {
        alert ('Привет! Я' + имя объекта + '. ');
      };
      return obj;
    }  
  2. Теперь вы можете создать нового человека, вызвав эту функцию - попробуйте следующие строки в консоли JavaScript вашего браузера:
      const salva = createNewPerson ('Salva');
    salva.name;
    salva.greeting ();  
    Это работает достаточно хорошо, но немного затянуто; если мы знаем, что хотим создать объект, зачем нам явно создавать новый пустой объект и возвращать его? К счастью, JavaScript предоставляет нам удобный ярлык в виде функций-конструкторов - давайте сделаем его сейчас!
  3. Замените вашу предыдущую функцию следующей:
      function Person (имя) {
      этот.name = name;
      this.greeting = function () {
        alert ('Привет! Я' + this.name + '.');
      };
    }  

Функция-конструктор - это версия класса в JavaScript. Обратите внимание, что он имеет все функции, которые вы ожидаете от функции, хотя он ничего не возвращает и не создает явно объект - он в основном просто определяет свойства и методы. Обратите внимание, что здесь также используется ключевое слово this - в основном это говорит о том, что всякий раз, когда создается один из этих экземпляров объекта, свойство name объекта будет равно значению имени, переданному в вызов конструктора, и приветствию () также будет использовать значение имени, переданное в вызов конструктора.

Примечание : Имя функции конструктора обычно начинается с заглавной буквы - это соглашение используется для упрощения распознавания функций конструктора в коде.

Так как же вызвать конструктор для создания некоторых объектов?

  1. Добавьте следующие строки под предыдущим добавлением кода:
      let person1 = new Person («Боб»);
    let person2 = новый человек ('Сара');  
  2. Сохраните свой код, перезагрузите его в браузере и попробуйте ввести следующие строки в консоль JS:
      человек1.имя
    person1.greeting ()
    person2. name
    person2.greeting ()  

Круто! Теперь вы можете видеть, что у нас есть два новых объекта на странице, каждый из которых хранится в другом пространстве имен - когда вы обращаетесь к их свойствам и методам, вы должны начинать вызовы с person1 или person2 ; функциональность, содержащаяся внутри, аккуратно упакована, поэтому она не будет конфликтовать с другими функциями. Однако они имеют те же свойства name, и welcome () .Обратите внимание, что они используют собственное имя , значение , которое было присвоено им при создании; это одна из причин, почему очень важно использовать и , поэтому каждый из них использует свое собственное значение, а не какое-либо другое значение.

Давайте еще раз посмотрим на вызовы конструктора:

  let person1 = new Person («Боб»);
let person2 = новый человек ('Сара');  

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

  function Person (имя) {
  this.name = имя;
  this.greeting = function () {
    alert ('Привет! Я' + this.name + '.');
  };
}  

После создания новых объектов переменные person1 и person2 содержат следующие объекты:

  {
  имя: 'Боб',
  приветствие: function () {
    alert ('Привет! Я' + this.name + '.');
  }
}

{
  имя: 'Сара',
  приветствие: function () {
    alert ('Привет! Я' + это.имя + '.');
  }
}  

Обратите внимание, что когда мы вызываем нашу функцию-конструктор, мы каждый раз определяем welcome () , что не идеально. Чтобы избежать этого, мы можем вместо этого определять функции в прототипе, что мы рассмотрим позже.

Создание готового конструктора

Пример, который мы рассмотрели выше, был всего лишь простым примером для начала. Давайте теперь приступим к созданию нашей последней функции-конструктора Person () .

  1. Удалите код, который вы вставили до сих пор, и добавьте в этот конструктор замены - это в принципе то же самое, что и простой пример, только с немного большей сложностью:
      функция Человек (имя, фамилия, возраст, пол, интересы) {
      этот.name = {
         первое: первое,
         последний: последний
      };
      this.age = возраст;
      this.gender = пол;
      this.interests = интересы;
      this.bio = function () {
        alert (this.name.first + '' + this.name.last + '' + this.age + 'лет. Ему нравится' + this.interests [0] + 'и' + this.interests [1] + '.');
      };
      this.greeting = function () {
        alert ('Привет! Я' + this.name.first + '.');
      };
    }  
  2. Теперь добавьте следующую строку под ним, чтобы создать из него экземпляр объекта:
      let person1 = new Person («Боб», «Смит», 32, «мужчина», [«музыка», «катание на лыжах»]);  

Теперь вы можете видеть, что у вас есть доступ к свойствам и методам, как мы делали раньше - попробуйте их в своей консоли JS:

  человек1 ["возраст"]
person1. интересы [1]
person1.bio ()
  

Дальнейшие упражнения

Для начала попробуйте добавить еще пару собственных строк создания объекта и попробуйте получить и установить члены полученных экземпляров объекта.

Кроме того, есть несколько проблем с нашим методом bio () - вывод всегда включает местоимение «Он», даже если ваш человек - женщина, или какой-либо другой предпочтительный гендерный класс. И биография включает только два интереса, даже если больше перечислено в массиве интересов .Можете придумать, как исправить это в определении класса (конструкторе)? Вы можете поместить в конструктор любой код, который вам нравится (возможно, вам понадобится несколько условных выражений и цикл). Подумайте о том, как предложения должны быть по-разному структурированы в зависимости от пола и в зависимости от того, составляет ли количество перечисленных интересов 1, 2 или более 2.

До сих пор мы видели два разных способа создания экземпляра объекта - объявление литерала объекта и использование функции-конструктора (см. Выше).

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

Конструктор Object ()

Прежде всего, вы можете использовать конструктор Object () для создания нового объекта. Да, даже у универсальных объектов есть конструктор, который генерирует пустой объект.

  1. Попробуйте ввести это в консоль JavaScript вашего браузера:
      let person1 = new Object ();  
  2. Сохраняет пустой объект в переменной person1 . Затем вы можете добавить свойства и методы к этому объекту, используя точечную или квадратную нотацию по желанию; попробуйте эти примеры на своей консоли:
      человек1.name = 'Крис';
    person1 ['age'] = 38;
    person1.greeting = function () {
      alert ('Привет! Я' + this.name + '.');
    };  
  3. Вы также можете передать литерал объекта в конструктор Object () в качестве параметра, чтобы предварительно заполнить его свойствами / методами. Попробуйте это в своей консоли JS:
      let person1 = new Object ({
      имя: 'Крис',
      возраст: 38,
      приветствие: function () {
        alert ('Привет! Я' + this.name + '.');
      }
    });  

Использование метода create ()

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

Однако некоторые люди предпочитают создавать экземпляры объектов без предварительного создания конструкторов, особенно если они создают только несколько экземпляров объекта. В JavaScript есть встроенный метод create () , который позволяет вам это делать. С его помощью вы можете создать новый объект, используя существующий объект в качестве прототипа вновь созданного объекта.

  1. Загрузив в браузере завершенное упражнение из предыдущих разделов, попробуйте выполнить это в консоли JavaScript:
      let person2 = Объект. создать (person1);  
  2. Теперь попробуйте следующее:
      person2.name;
    person2.greeting ();  

Вы увидите, что person2 было создано на основе person1 в качестве своего прототипа - он имеет те же свойства и доступные ему методы.

Одно из ограничений create () заключается в том, что IE8 не поддерживает его. Так что конструкторы могут быть более эффективными, если вы хотите поддерживать старые браузеры.

Мы рассмотрим эффекты create () более подробно позже.

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

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

В этой статье представлен упрощенный взгляд на объектно-ориентированную теорию - это еще не вся история, но она дает вам представление о том, с чем мы здесь имеем дело.Кроме того, мы начали изучать различные способы создания экземпляров объектов.

В следующей статье мы рассмотрим прототипы объектов JavaScript.

Разновидности объектно-ориентированного программирования (на JavaScript)

В своем исследовании я обнаружил четыре подхода к объектно-ориентированному программированию на JavaScript:

Какие методы мне следует использовать? Какой из них «лучший»? Здесь я представлю свои выводы вместе с информацией, которая поможет вам решить, что подходит именно вам.

Чтобы принять это решение, мы не просто рассмотрим различные вкусы, но и сравним их концептуальные аспекты:

Давайте начнем с основы ООП в JavaScript.

Что такое объектно-ориентированное программирование?

Объектно-ориентированное программирование - это способ написания кода, который позволяет создавать различные объекты из общего объекта. Общий объект обычно называется blueprint , в то время как созданные объекты называются экземплярами .

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

Второй аспект объектно-ориентированного программирования - это структурирования кода , когда у вас есть несколько уровней схем элементов. Обычно это называется наследованием или подклассом.

Третий аспект объектно-ориентированного программирования - это инкапсуляция , при которой вы скрываете определенные фрагменты информации внутри объекта, чтобы они были недоступны.

Если вам нужно нечто большее, чем это краткое введение, вот статья, которая знакомит с этим аспектом объектно-ориентированного программирования, если вам нужна помощь.

Начнем с основ - введение в четыре разновидности объектно-ориентированного программирования.

Четыре разновидности объектно-ориентированного программирования

Есть четыре способа написать объектно-ориентированное программирование на JavaScript. Их:

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

Конструкторы - это функции, содержащие ключевое слово и .

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

этот позволяет хранить (и получать доступ) уникальные значения, созданные для каждого экземпляра. Вы можете создать экземпляр с ключевым словом new .

  const chris = новый человек ('Крис', 'Койер')
console.log (chris.firstName) // Крис
console.log (chris.lastName) // Койер

const zell = новый человек ('Zell', 'Liew')
console.log (zell.firstName) // Zell
консоль.log (zell.lastName) // Liew  
Синтаксис класса

Классы считаются «синтаксическим сахаром» функций-конструкторов. Как и в случае с классами, проще писать функции-конструкторы.

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

Классы могут быть записаны с использованием следующего синтаксиса:

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

Обратите внимание, что функция конструктора содержит тот же код, что и синтаксис конструктора выше? Нам нужно это сделать, поскольку мы хотим инициализировать значения в , это . (Мы можем пропустить конструктор , если нам не нужно инициализировать значения. Подробнее об этом позже в разделе «Наследование»).

На первый взгляд кажется, что классы уступают конструкторам - нужно писать больше кода! Держите лошадей и не делайте выводов на этом этапе. Нам есть еще много чего рассказать. Классы засветятся позже.

Как и раньше, вы можете создать экземпляр с ключевым словом new .

  const chris = новый человек ('Крис', 'Койер')

console.log (chris.firstName) // Крис
console.log (chris.lastName) // Coyier  
Объекты, связанные с другими объектами (OLOO)

OLOO был придуман и популяризирован Кайлом Симпсоном.В OLOO вы определяете чертеж как обычный объект. Затем вы используете метод (часто называемый init , но это не требуется, как конструктор для класса) для инициализации экземпляра.

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

Для создания экземпляра используется Object.create . После создания экземпляра вам необходимо запустить функцию init .

  const chris = Object.create (Человек)
chris.init ('Крис', 'Койер')

console.log (chris.firstName) // Крис
console.log (chris.lastName) // Coyier  

Вы можете связать init после Object.create , если вы вернули , это внутри init .

  const Human = {
  в этом () {
    // ...
    верни это
  }
}

const chris = Object.create (Человек) .init ('Крис', 'Койер')
console.log (chris.firstName) // Крис
консоль.журнал (chris.lastName) // Coyier  
Заводские функции

Заводские функции - это функции, возвращающие объект. Вы можете вернуть любой объект. Вы даже можете вернуть экземпляр класса или экземпляр OLOO - и он по-прежнему будет действующей функцией Factory.

Вот самый простой способ создания функций Factory:

  function Human (firstName, lastName) {
  вернуть {
    имя,
    фамилия
  }
}  

Вам не нужно новых для создания экземпляров с функциями Factory. Вы просто вызываете функцию.

  const chris = Человек ('Крис', 'Койер')

console.log (chris.firstName) // Крис
console.log (chris.lastName) // Coyier  

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


Объявление свойств и методов

Методы - это функции, объявленные как свойство объекта.

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

В объектно-ориентированном программировании есть два способа объявления свойств и методов:

  1. Непосредственно на экземпляре
  2. В прототипе

Давайте научимся делать и то, и другое.

Объявление свойств и методов с помощью конструкторов

Если вы хотите объявить свойство непосредственно в экземпляре, вы можете записать свойство внутри функции конструктора. Обязательно установите его как свойство для и .

  function Human (firstName, lastName) {
  // Объявляет свойства
  this.firstName = firstName
  this.lastname = lastName

  // Объявляет методы
  this.sayHello = function () {
    console.log (`Привет, я $ {firstName}`)
  }
}

const chris = новый человек ('Крис', 'Койер')
console.log (Крис)  

Методы обычно объявляются в Prototype, потому что Prototype позволяет экземплярам использовать один и тот же метод. Это меньший размер кода.”

Чтобы объявить свойства в прототипе, вам нужно использовать свойство prototype .

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

// Объявление метода в прототипе
Human.prototype.sayHello = function () {
  console.log (`Привет, я $ {this.firstName}`)
}  

Это может быть неудобно, если вы хотите объявить несколько методов в прототипе.

  // Объявление методов на прототипе
Человек.prototype.method1 = function () {/*...*/}
Human.prototype.method2 = function () {/*...*/}
Human.prototype.method3 = function () {/*...*/}  

Вы можете упростить задачу, используя функции слияния, такие как Object.assign .

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

Object.assign не поддерживает объединение функций Getter и Setter. Вам нужен другой инструмент.Вот почему. А вот инструмент, который я создал для объединения объектов с геттерами и сеттерами.

Объявление свойств и методов с помощью классов

Вы можете объявить свойства для каждого экземпляра внутри функции конструктора .

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

      this.sayHello = function () {
        console.log (`Привет, я $ {firstName}`)
      }
  }
}  

На прототипе проще объявить методы.Вы пишете метод после , конструктор , как обычную функцию.

  class Human (firstName, lastName) {
  конструктор (firstName, lastName) {/ * ... * /}

  скажи привет () {
    console.log (`Привет, я $ {this.firstName}`)
  }
}  

Объявлять несколько методов в классах проще, чем в конструкторах. Вам не нужен синтаксис Object.assign . Вы просто пишете больше функций.

Примечание: нет , между объявлениями методов в классе.

  class Human (firstName, lastName) {
  конструктор (firstName, lastName) {/ * ... * /}

  method1 () {/*...*/}
  method2 () {/*...*/}
  method3 () {/*...*/}
}  
Объявление свойств и методов с помощью OLOO

Вы используете тот же процесс для объявления свойств и методов в экземпляре. Вы назначаете им как свойство этот .

  const Human = {
  init (firstName, lastName) {
    this.firstName = firstName
    этот.lastName = lastName
    this.sayHello = function () {
      console.log (`Привет, я $ {firstName}`)
    }

    верни это
  }
}

const chris = Object.create (Человек) .init ('Крис', 'Койер')
console.log (Крис)  

Чтобы объявить методы в прототипе, вы пишете метод как обычный объект.

  const Human = {
  в этом () { /*...*/ },
  скажи привет () {
    console.log (`Привет, я $ {this.firstName}`)
  }
}  
Объявление свойств и методов с помощью фабричных функций

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

  function Human (firstName, lastName) {
  вернуть {
    имя,
    фамилия,
    скажи привет () {
      console. log (`Привет, я $ {firstName}`)
    }
  }
}  

Вы не можете объявлять методы в прототипе при использовании функций Factory. Если вам действительно нужны методы в прототипе, вам нужно вернуть экземпляр Constructor, Class или OLOO. (Не делайте этого, потому что в этом нет никакого смысла.)

  // Не делайте этого
function createHuman (... args) {
  вернуть новый Human (...args)
}  

Где объявлять свойства и методы

Следует ли объявлять свойства и методы непосредственно в экземпляре? Или вам стоит использовать прототип как можно чаще?

Многие люди гордятся тем, что JavaScript является «языком прототипов» (что означает, что он использует прототипы). Из этого утверждения вы можете сделать предположение, что использование «Прототипов» лучше.

Настоящий ответ: Неважно.

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

Например, если вы используете классы или OLOO, вам будет лучше использовать прототипы, поскольку код легче писать. Если вы используете функции Factory, вы не можете использовать прототипы. Вы можете создавать свойства и методы только непосредственно в экземпляре.

Я написал отдельную статью о понимании прототипов JavaScript, если вам интересно узнать больше.

Предварительный приговор

Мы можем сделать несколько заметок из кода, который мы написали выше. Это мое собственное мнение!

  1. Классы лучше, чем конструкторы , потому что в классах проще написать несколько методов.
  2. OLOO странно из-за части Object. create . Некоторое время я пробовал OLOO, но я всегда забываю написать Object.создать . Для меня достаточно странно не использовать его.
  3. Классы и Factry-функции проще всего использовать. Проблема в том, что функции Factory не поддерживают прототипы. Но, как я уже сказал, в производстве это не имеет значения.

Осталось двое. Должны ли мы тогда выбирать классы или фабричные функции? Давай сравним их!


Классы и фабричные функции - Наследование

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

  1. Наследование
  2. Инкапсуляция
  3. это

Начнем с наследования.

Что такое наследование?

Наследование - загруженное слово. На мой взгляд, многие в индустрии неправильно используют наследование. Слово «наследование» используется, когда вы получаете что-то откуда-то. Например:

  • Если вы получаете наследство от родителей, это означает, что вы получаете от них деньги и имущество.
  • Если вы унаследовали гены от родителей, это означает, что вы получили свои гены от них.
  • Если вы унаследовали процесс от учителя, это означает, что вы получаете этот процесс от него.

Довольно просто.

В JavaScript наследование может означать то же самое: где вы получаете свойства и методы из родительского проекта.

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

  1. путем создания свойства или метода непосредственно при создании экземпляра
  2. через цепочку прототипов

Мы обсуждали, как использовать оба метода в предыдущей статье, поэтому вернитесь к нему, если вам нужна помощь в просмотре этих процессов в коде.

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

Общие сведения о подклассах

Создание подклассов - это создание производного проекта на основе общего плана. Вы можете использовать любой вариант объектно-ориентированного программирования для создания подкласса.

Сначала мы поговорим об этом с синтаксисом класса, потому что его легче понять.

Подкласс с классом

Когда вы создаете подкласс, вы используете ключевое слово extends .

  class Child extends Parent {
  // ... Здесь вся информация
}  

Например, предположим, что мы хотим создать класс Developer из класса Human .

  // Человеческий класс
class Human {
  constructor (firstName, lastName) {
    this.firstName = firstName
    this. lastName = lastName
  }

  скажи привет () {
    консоль.log (`Привет, я $ {this.firstName}`)
  }
}  

Класс Developer расширит Human следующим образом:

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

    // Добавляем другие методы
}  

Примечание: super вызывает Human (также называемый «родительским») классом. Он инициирует конструктор из Human . Если вам не нужен дополнительный код запуска, вы можете полностью опустить конструктор .

  class Developer extends Human {
  // Добавляем другие методы
}  

Допустим, Developer умеет писать код. Мы можем добавить метод code непосредственно в Developer .

  class Developer extends Human {
  code (thing) {
    console. log (`$ {this.firstName} закодировал $ {thing}`)
  }
}  

Вот пример экземпляра Developer :

  const chris = новый разработчик ('Крис', 'Койер')
консоль.журнал (крис)  
Создание подклассов с заводскими функциями

Есть четыре шага для создания подклассов с фабричными функциями:

  1. Создание новой функции Factory
  2. Создание экземпляра родительской схемы
  3. Создание новой копии этого экземпляра
  4. Добавление свойств и методов к этой новой копии

Процесс выглядит следующим образом:

  function Subclass (... args) {
  const instance = ParentClass (...args)
  return Object.assign ({}, instance, {
    // Свойства и методы идут сюда
  })
}  

Мы будем использовать тот же пример - создание подкласса Developer , чтобы проиллюстрировать этот процесс. Вот заводская функция Human :

  function Human (firstName, lastName) {
  вернуть {
    имя,
    фамилия,
    скажи привет () {
      console.log (`Привет, я $ {firstName}`)
    }
  }
}  

Мы можем создать Developer вот так:

  function Developer (firstName, lastName) {
  const human = Человек (firstName, lastName)
  вернуть объект.assign ({}, человек, {
    // Свойства и методы идут сюда
  })
}  

Затем мы добавляем метод code следующим образом:

  function Developer (firstName, lastName) {
  const human = Человек (firstName, lastName)
  return Object.assign ({}, человек, {
    code (thing) {
      console.log (`$ {this.firstName} закодировал $ {thing}`)
    }
  })
}  

Вот пример экземпляра Developer :

  const chris = Разработчик ('Крис', 'Койер')
консоль. журнал (крис)  

Примечание: Вы не можете использовать Object.assign , если используете геттеры и сеттеры. Вам понадобится другой инструмент, например mix . Я объясняю почему в этой статье.

Замена родительского метода

Иногда вам нужно перезаписать родительский метод внутри подкласса. Вы можете сделать это по:

  1. Создание метода с тем же именем
  2. Вызов родительского метода (необязательно)
  3. Изменение всего, что вам нужно в методе подкласса

Процесс выглядит следующим образом с Классами:

  class Developer extends Human {
  скажи привет () {
    // Вызывает родительский метод
    супер.скажи привет()

    // Дополнительные материалы для запуска
    console.log (`Я разработчик. ')
  }
}

const chris = новый разработчик ('Крис', 'Койер')
chris.sayHello ()  

Процесс выглядит так с заводскими функциями:

  function Developer (firstName, lastName) {
  const human = Человек (firstName, lastName)

  return Object. assign ({}, человек, {
      скажи привет () {
        // Вызывает родительский метод
        human.sayHello ()

        // Дополнительные материалы для запуска
        консоль.log (`Я разработчик. ')
      }
  })
}

const chris = новый разработчик ('Крис', 'Койер')
chris.sayHello ()  
Наследование и состав

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

«Предпочитайте композицию объектов наследованию классов» Группа четырех, «Шаблоны проектирования: элементы многоразового объектно-ориентированного программного обеспечения»

«В информатике составной тип данных или составной тип данных - это любой тип данных, который может быть построен в программа, использующая примитивные типы данных языка программирования и другие составные типы.[…] Построение составного типа известно как композиция ». ~ Википедия

Итак, давайте посмотрим на Composition глубже и разберемся, что это такое.

Понимание композиции

Композиция - это соединение двух вещей в одно. Речь идет о слиянии вещей. Самый распространенный (и самый простой) способ объединения объектов - Object.assign .

  const one = {один: 'один'}
const two = {два: 'два'}
константная комбинация = Объект.назначить ({}, один, два)  

Использование композиции можно лучше пояснить на примере. Допустим, у нас уже есть два подкласса: Designer и Developer . Дизайнеры могут проектировать, а разработчики могут писать код. И дизайнеры, и разработчики наследуют от класса Human .

Вот пока код:

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

  скажи привет () {
    консоль.log (`Привет, я $ {this.firstName}`)
  }
}

class Designer extends Human {
  дизайн (вещь) {
    console. log (`$ {this.firstName} разработан $ {thing}`)
  }
}

class Developer extends Designer {
  code (thing) {
    console.log (`$ {this.firstName} закодировал $ {thing}`)
  }
}  

Теперь предположим, что вы хотите создать третий подкласс. Этот подкласс представляет собой смесь дизайнера и разработчика - они могут проектировать и кодировать. Назовем его DesignerDeveloper (или DeveloperDesigner , как вам нравится).

Как бы вы создали третий подкласс?

Мы не можем одновременно расширять классы Designer и Developer . Это невозможно, потому что мы не можем решить, какие свойства будут первыми. Это часто называют проблемой алмаза.

Проблема с бриллиантом может быть легко решена, если мы сделаем что-то вроде Object.assign - где мы будем отдавать приоритет одному объекту над другим. Если мы воспользуемся подходом Object.assign , мы сможем расширить такие классы.Но это не поддерживается в JavaScript.

  // Не работает
class DesignerDeveloper расширяет Developer, Designer {
  // ...
}  

Итак, нам нужно полагаться на композицию.

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

На практике это может выглядеть так:

  const навыки = {
  код (вещь) {/ *... * /},
  дизайн (вещь) {/ * ... * /},
  скажи привет () { /* ... */ }
}  

Затем мы можем вообще пропустить Human и создать три разных класса на основе их навыков.

Вот код для DesignerDeveloper :

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

    Object. assign (this, {
      код: skills.code,
      дизайн: навыки. дизайн,
      sayHello: навыки.скажи привет
    })
  }
}

const chris = новый DesignerDeveloper ('Крис', 'Койер')
console.log (Крис)  

То же самое можно сделать с Developer и Designer .

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

    Object.assign (this, {
      дизайн: навыки. дизайн,
      sayHello: навыки.sayHello
    })
  }
}

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

    Object.assign (this, {
      код: skills.code,
      sayHello: навыки.sayHello
    })
  }
}  

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

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

Объект.assign (DesignerDeveloper.prototype, {
  код: skills.code,
  дизайн: навыки. дизайн,
  sayHello: навыки.sayHello
})  

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

Композиция с заводскими функциями

Composition с функциями Factory по сути добавляет общие методы в возвращаемый объект.

  function DesignerDeveloper (firstName, lastName) {
  вернуть {
    имя,
    фамилия,
    код: навыки.код,
    дизайн: навыки. дизайн,
    sayHello: навыки.sayHello
  }
}  
Наследование и состав одновременно

Никто не говорит, что нельзя использовать наследование и композицию одновременно. Мы можем!

Если использовать рассмотренный нами пример, Designer , Developer и DesignerDeveloper Люди все еще остаются людьми. Они могут расширить объект Human .

Вот пример, в котором мы используем и наследование, и композицию с синтаксисом класса.

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

  скажи привет () {
    console.log (`Привет, я $ {this.firstName}`)
  }
}

class DesignerDeveloper расширяет Human {}
Object.assign (DesignerDeveloper.prototype, {
  код: skills.code,
  дизайн: Skills.design
})  

То же самое и с заводскими функциями:

  function Human (firstName, lastName) {
  вернуть {
    имя,
    фамилия,
    скажи привет () {
      консоль.log (`Привет, я $ {this.firstName}`)
    }
  }
}

function DesignerDeveloper (firstName, lastName) {
  const human = Человек (firstName, lastName)
  return Object.assign ({}, человек, {
    код: skills.code,
    дизайн: Skills.design
  }
}  
Создание подклассов в реальном мире

И последнее о подклассах и композиции. Несмотря на то, что эксперты отметили, что композиция более гибкая (и, следовательно, более полезная), создание подклассов все же имеет свои достоинства. Многие вещи, которые мы используем сегодня, построены на стратегии создания подклассов.

Например: событие click , которое мы знаем и любим, - это MouseEvent . MouseEvent является подклассом UIEvent , который, в свою очередь, является подклассом Event .

Другой пример: HTML-элементы являются подклассами узлов. Вот почему они могут использовать все свойства и методы узлов.

Приговор предварительный
Классы

и функции Factory могут использовать наследование и композицию. Хотя композиция кажется более чистой в функциях Factory, но это не большая победа над классами.

Далее мы рассмотрим классы и фабричные функции более подробно.


Классы и фабричные функции - инкапсуляция

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

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

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

  1. Наследование
  2. Инкапсуляция
  3. это

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

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

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

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

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

Самая простая форма инкапсуляции - это область видимости блока.

  {
  // Объявленные здесь переменные не просочатся
}  

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

  const food = 'Гамбургер'

{
  консоль.журнал (еда)
}  

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

  {
  const food = 'Гамбургер'
}

console.log (еда)  

Примечание. Переменные, объявленные с var , не учитывают область действия блока. Вот почему я рекомендую вам использовать let или const для объявления переменных.

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

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

  function sayFood () {
  const food = 'Гамбургер'
}

sayFood ()
console.log (еда)  

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

  const food = 'Гамбургер'

function sayFood () {
  console.log (еда)
}


sayFood ()  

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

  function sayFood () {
  вернуть 'гамбургер'
}

console.log (sayFood ())  
Заглушки
Замыкания

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

  // Вот закрытие
function outsideFunction () {
  function insideFunction () {/ * ... * /}
}  

Переменные, объявленные в externalFunction , могут использоваться в insideFunction .

  function outsideFunction () {
  const food = 'Гамбургер'
  console.log ('Вызывается снаружи')

  return function insideFunction () {
    console.log ('Вызывается внутри')
    console.log (еда)
  }
}

// Вызывает `outsideFunction`, которая возвращает` insideFunction`
// Сохраняет `insideFunction` как переменную` fn`
const fn = externalFunction ()

// Вызывает `insideFunction`
fn ()  
Инкапсуляция и объектно-ориентированное программирование

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

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

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

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

  const car = новый автомобиль ()
console.log (car.fuel) // 50  

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

  const car = новый автомобиль ()
car.fuel = 3000
console.log (car.fuel) // 3000  

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

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

  1. Частные по соглашению
  2. Реальные частные члены
Частные по соглашению

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

  class Car {
  constructor () {
    // Обозначает, что `_fuel` является частным. Не используйте это!
    this._fuel = 50
  }
}  

Мы часто создаем методы для получения и установки этой «частной» переменной _fuel .

  class Car {
  constructor () {
    // Обозначает, что `_fuel` является частным. Не используйте это!
    this._fuel = 50
  }

  getFuel () {
    вернуть this. _fuel
  }

  setFuel (значение) {
    этот._fuel = значение
    // Крышка топливного бака на 100 литров
    if (значение> 100) this._fuel = 100
  }
}  

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

  const car = новый автомобиль ()
console.log (car.getFuel ()) // 50

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

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

  const car = новый автомобиль ()
console.log (car.getFuel ()) // 50

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

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

Реальные частные члены

Члены здесь относятся к переменным, функциям и методам. Это собирательный термин.

Частные члены с классами
Классы

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

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

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

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

  class Car {
  // Объявляет приватную переменную
  # топливо
  constructor () {
    // Используем приватную переменную
    this. # fuel = 50
  }
}  

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

  class Car {
  #fuel = 50
}  

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

  const car = новый автомобиль ()
console.log (автомобиль. # топливо)  

Вам нужны методы (например, getFuel или setFuel ) для использования переменной #fuel .

  class Car {
  #fuel = 50

  getFuel () {
    вернуть это. # топливо
  }

  setFuel (значение) {
    this. # fuel = value
    если (значение> 100) это.#fuel = 100
  }
}

const car = новый автомобиль ()
console.log (car.getFuel ()) // 50

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

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

  class Car {
  #fuel = 50

  получить топливо () {
    вернуть это. # топливо
  }

  установить топливо (значение) {
    this.  # fuel = value
    if (значение> 100) this. # fuel = 100
  }
}

const car = новый автомобиль ()
console.log (автомобиль.топливо) // 50

car.fuel = 3000
console.log (car.fuel) // 100  
Частные члены с заводскими функциями

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

  function Car () {
  const топливо = 50
}

const car = новый автомобиль ()
console.log (car.fuel) // не определено
консоль.log (fuel) // Ошибка: `fuel` не определено  

Мы можем создать функции получения и установки для использования этой частной переменной fuel .

  function Car () {
  const топливо = 50

  вернуть {
    получить топливо () {
      возврат топлива
    },

    установить топливо (значение) {
      топливо = значение
      если (значение> 100) топливо = 100
    }
  }
}

const car = новый автомобиль ()
console. log (car.fuel) // 50

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

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

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

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

С другой стороны, инкапсуляция

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

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


Классы и фабричные функции - это переменная

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

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

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

  1. В глобальном контексте
  2. Создание объекта Inan
  3. В свойстве / методе объекта
  4. В простой функции
  5. В функции стрелки
  6. В прослушивателе событий

Я подробно рассмотрел эти шесть контекстов.Прочтите его, если вам нужна помощь в понимании и .

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

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

Еще не вернулся? Хорошо. Пойдем!

Использование этого в классах

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

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

const chris = новый человек ('Крис', 'Койер')  
Использование этого в функциях конструктора

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

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

const chris = новый человек ('Крис', 'Койер')  

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

  // НЕ функция конструктора, потому что мы не создавали экземпляры с ключевым словом `new`
function Human (firstName, lastName) {
  этот.firstName = firstName
  this.lastName = lastName
  console.log (это)
}

const chris = Человек ('Крис', 'Койер')  

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

Использование этого в заводской функции

Правильный способ использования и этого в функции Factory - это использовать его «в контексте свойства / метода объекта».

  function Human (firstName, lastName) {
  вернуть {
    имя,
    фамилия,
    скажи это () {
      console. log (это)
    }
  }
}

const chris = Человек ('Крис', 'Койер')
chris.sayThis ()  

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

  function Human (firstName, lastName) {
  const human = {
    имя,
    фамилия,
    скажи привет() {
      консоль.log (`Привет, я $ {human.firstName}`)
    }
  }

  вернуть человека
}

const chris = Человек ('Крис', 'Койер')
chris.sayHello ()  

human.firstName яснее, чем this.firstName , потому что human определенно указывает на экземпляр. Вы знаете, когда видите код.

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

  function Human (firstName, lastName) {
  const human = {
    имя,
    фамилия,
    скажи привет() {
      console.log (`Привет, я $ {firstName}`)
    }
  }

  вернуть человека
}

const chris = Человек ('Крис', 'Койер')
chris.sayHello ()  

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

Подробный пример

Вот установка.Допустим, у нас есть чертеж Human . Это Human ha firstName и lastName и метод sayHello .

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

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

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

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

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

  скажи привет () {
    консоль.log (`Привет, я $ {this.firstName}`)
  }
}  

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

  class Developer extends Human {
  code (thing) {
    console.log (`$ {this.firstName} закодировал $ {thing}`)
  }
}  

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

  class Developer extends Human {
  code (thing) {
    console.log (`$ {this.firstName} закодировал $ {thing}`)
  }

  скажи привет () {
    super.sayHello ()
    console.log (`Я разработчик`)
  }
}  
Пример с заводскими функциями (с это )

Опять же, во-первых, у нас есть чертеж Human .Этот Human имеет свойства firstName и lastName , а также метод sayHello .

  function Human () {
  вернуть {
    имя,
    фамилия,
    скажи привет () {
      console.log (`Привет, я $ {this.firstName}`)
    }
  }
}  

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

  function Developer (firstName, lastName) {
  const human = Человек (firstName, lastName)
  вернуть объект.assign ({}, человек, {
    code (thing) {
      console.log (`$ {this.firstName} закодировал $ {thing}`)
    }
  })
}  

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

  function Developer (firstName, lastName) {
  const human = Человек (firstName, lastName)
  вернуть объект.assign ({}, человек, {
    code (thing) {
      console.log (`$ {this.firstName} закодировал $ {thing}`)
    },

    скажи привет () {
      human. sayHello ()
      console.log ("Я разработчик")
    }
  })
}  
Пример с заводскими функциями ( без это )

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

  function Human (firstName, lastName) {
  вернуть {
    имя,
    фамилия,
    скажи привет () {
      консоль.log (`Привет, я $ {this.firstName}`)
    }
  }
}

function Developer (firstName, lastName) {
  const human = Человек (firstName, lastName)
  return Object.assign ({}, человек, {
    code (thing) {
      console.log (`$ {this.firstName} закодировал $ {thing}`)
    },

    скажи привет () {
      human.sayHello ()
      console.log ("Я разработчик")
    }
  })
}  

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

  function Human (firstName, lastName) {
  вернуть {
    // ...
    скажи привет () {
      console.log (`Привет, я $ {firstName}`)
    }
  }
}

function Developer (firstName, lastName) {
  // ...
  return Object.assign ({}, человек, {
    code (thing) {
      console.log (`$ {firstName} закодировал $ {thing}`)
    },

    скажи привет () { /* ... */ }
  })
}  

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

Вердикт по это

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

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

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

Классы и фабричные функции - Слушатели событий

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

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

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

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

Строительство прилавка

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

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

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

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

  

Счетчик: 0

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

Чтобы упростить задачу, мы попросим пользователей найти и передать HTML-код счетчика в класс Counter .

  class Counter () {
  конструктор (счетчик) {
    // Делаем что-нибудь
  }
}

// Применение
const counter = новый счетчик (document. querySelector ('. counter'))  

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

  1. , который содержит счетчик - нам нужно обновить этот элемент, когда счетчик увеличивается
  Counter () {
  конструктор (счетчик) {
    этот.countElement = counter.querySelector ('диапазон')
    this.buttonElement = counter.querySelector ('кнопка')
  }
}  

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

  class Counter () {
  #count
  конструктор (счетчик) {
    // ...

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

Когда пользователь нажимает