Варианты объектно-ориентированного программирования (на JavaScript)
Перевод: Zell Liew — The Flavors of Object-Oriented Programming (in JavaScript)
В своем исследовании я рассмотрел четыре подхода к объектно-ориентированному программированию в JavaScript:
- Использование функций конструктора
- Использование классов
- Объекты, связанные с другими объектами (OLOO)
- Использование фабричных функций
Какой способ лучше других? Какой нужно использовать, а какой нет? Здесь я представлю свои выводы вместе с информацией, которая поможет вам решить, что подходит именно вам.
Чтобы принять это решение, мы не просто рассмотрим разные подходы, но и сравним их концептуальные аспекты:
- Классы против Фабричных функций — Наследование
- Классы против Фабричных функций — Инкапсуляция
- Классы против Фабричных функций — this
- Классы против Фабричных функций — Event listeners
Начнем с основ ООП в JavaScript.
Что такое объектно-ориентированное программирование?
Объектно-ориентированное программирование — это способ написания кода, который позволяет создавать разные объекты из объекта. Общий объект обычно называется blueprint (базовая схема), а созданные объекты — экземплярами.
У каждого экземпляра есть свойства, которые не используются другими экземплярами. Например, если у вас есть blueprint человека, вы можете создавать экземпляры людей с разными именами.
Второй аспект объектно-ориентированного программирования касается структурирования кода, когда у вас есть несколько уровней blueprint элементов. Обычно это называется наследованием.
Третий аспект объектно-ориентированного программирования — это инкапсуляция, при которой вы скрываете определенные фрагменты информации внутри объекта, чтобы они были недоступны.
Если вам нужно нечто большее, чем это краткое введение, вот статья, которая знакомит с аспектами объектно-ориентированного программирования подробнее.
Начнем с основ — введение в четыре разновидности объектно-ориентированного программирования.
Четыре разновидности объектно-ориентированного программирования
Есть четыре способа использовать объектно-ориентированное программирование на JavaScript:
- Использование функции конструктора
- Использование классов
- Использование объектов, связанные с другими объектами
- Использование фабричных функций
Использование функций конструктора
Конструкторы — это функции, содержащие ключевое слово this.
function Human (firstName, lastName) { this.firstName = firstName this.lastName = lastName }
this
позволяет хранить уникальные значения, созданные для каждого экземпляра.
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 как обычный объект. Затем вы используете метод (часто называемый
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 () { /* ... */ } }
В объектно-ориентированном программировании есть два способа объявления свойств и методов:
- Непосредственно на экземпляре
- В прототипе
Давай научимся делать и то, и другое.
Объявление свойств и методов с помощью конструкторов
Если вы хотите объявить свойство непосредственно в экземпляре, вы можете записать свойство внутри функции-конструктора. Обязательно установите его как свойство для 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, если вам интересно узнать больше.
Предварительный вердикт
Резюмируем, то что я написал выше. Но учтите это только мое собственное мнение!
- Классы лучше, чем конструкторы, потому что в классах проще написать несколько методов.
- Использование OLOO довольно неудобно из-за Object.create. Некоторое время я пробовал использовать OLOO, но я всегда забываю написать Object.create.
- Проще всего использовать классы и фабричные функции. Проблема в том, что фабричные функции не поддерживают прототипы. Но, как я уже сказал, это не всегда имеет значение.
Остался один вопрос. Что нам выбрать классы или фабричные функции? Давай сравним их!
Классы против фабричных функций — Наследование
Чтобы продолжить обсуждение классов и фабричных функций, нам нужно понять еще три концепции, которые тесно связаны с объектно-ориентированным программированием.
- Наследование
- Инкапсуляция
this
Начнем с наследования.
Что такое наследование?
Наследование — это многозначное слово. На мой взгляд, многие в индустрии неправильно используют наследование. Слово «наследование» используется, когда вы получаете что-то откуда-то. Например:
- Если вы получаете наследство от родителей, это означает, что вы получаете от них деньги и имущество.
- Если вы наследуете гены от своих родителей, это означает, что вы получаете свои гены от них.
- Если вы унаследовали процесс от своего учителя, это означает, что вы получаете этот процесс от него.
Довольно просто.
В JavaScript наследование может означать то же самое: это то что вы получаете свойства и методы из родительского проекта.
Это означает, что все экземпляры фактически наследуют свои blueprint. Они наследуют свойства и методы двумя способами:
- путем создания свойства или метода непосредственно при создании экземпляра
- через цепочку прототипов
У наследования в 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 с фабричными функциями:
- Создайте новую фабричную функцию
- Создайте экземпляр родительского класса
- Создать новую копию этого экземпляра
- Добавьте свойства и методы к этой новой копии
Процесс выглядит так:
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, если используете геттеры и сеттеры. Вам понадобится другой инструмент, например микс. Я объясняю это в этой статье.
Переопределение родительского метода
Иногда вам нужно переопределить родительский метод внутри подкласса. Это можно сделать так:
- Создать одноименный метод
- Вызвать родительский метод (необязательно)
- Изменить все, что нужно, в методе подкласса
С классами процесс выглядит так:
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.
Предварительный вердикт
И классы, и фабричные функции могут использовать наследование и композицию. Хотя композиция кажется более чистой в фабричных функциях, это не большая победа над классами.
Далее мы рассмотрим классы и фабричные функции более подробно.
Классы и фабричные функции — инкапсуляция
Мы рассмотрели четыре различных варианта объектно-ориентированного программирования. Две из них — классы и фабричные функции — проще в использовании по сравнению с остальными.
Но остаются вопросы: что использовать? И почему?
Чтобы продолжить обсуждение классов и фабричных функций, нам нужно понять три концепции, которые тесно связаны с объектно-ориентированным программированием:
- Наследование
- Инкапсуляция
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, потому что они могут сломать машину.
Есть два способа запретить пользователям устанавливать топливо:
- По соглашению
- Использовать настоящие приватные переменные
По соглашению
В 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
.
Шесть контекстов:
- В глобальном контексте
- В конструкторе объекта
- В свойствах объекта
- В простых функциях
- В стрелочных функциях
- В 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, а фабричные функции — нет. Я предпочитаю использовать фабричные функции, потому что:
- Контекст this может измениться (что может сбивать с толку)
- Код, написанный с использованием фабричных функций, короче и чище (поскольку мы можем использовать инкапсулированные переменные, не используя this.#Variable).
Далее идет последний раздел, в котором мы создаем простой компонент вместе с классами и фабричными функциями. Вы увидите, чем они отличаются и как использовать Event listeners.
Классы и фабричные функции — Event listeners
В большинстве статей по объектно-ориентированному программированию приведены примеры без Event listeners. Эти примеры проще для понимания, но они не отражают работу, которую мы выполняем как разработчики внешнего интерфейса. Для работы, которую мы выполняем, требуются Event listeners — по простой причине — потому что нам нужно создавать вещи, которые полагаются на ввод данных пользователем.
Поскольку listeners изменяют контекст this, они могут затруднить работу с классами. В то же время они делают фабричные функции более привлекательными.
Но на самом деле это не так.
Это изменение this
не имеет значения, если вы знаете, как использовать this
как в классах, так и в фабричных функциях. Существует множество статей посвящены этой теме, поэтому я подумал, что было бы неплохо завершить эту статью простым компонентом, использующим разновидности объектно-ориентированного программирования.
Создание счетчика
Далее мы построим простой счетчик. Мы используем все, что вы узнали из этой статьи, включая приватные переменные.
Скажем, счетчик содержит две вещи:
- Сам счетчик
- Кнопку для увеличения значения счетчика
Вот простейший 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:
- <span> содержащий счетчик — нам нужно обновить этот элемент, когда счетчик увеличивается
- <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
, чтобы все работало. Сделать это можно двумя способами:
- Используя
bind
- Используя стрелочную функцию
Большинство людей используют первый метод (но второй проще).
Добавление прослушивателя событий с 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, потому что это может вас запутать. Это все!
Заключение
Мы говорили о четырех разновидностях объектно-ориентированного программирования:
- Использование функций конструктора
- Использование классов
- Объекты, связанные с другими объектами (OLOO)
- Использование фабричных функций
Во-первых, мы пришли к выводу, что классы и фабричные функции проще использовать с точки зрения кода.
Во-вторых, мы сравнили, как использовать подклассы с классами и фабричными функциями. Здесь мы видим, что создание подклассов проще с классами, но композиция проще с фабричными функциями.
В-третьих, мы сравнили инкапсуляцию с классами и фабричными функциями. Здесь мы видим, что инкапсуляция с помощью фабричных функций естественна, как и 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, который мы видели в нашей первой статье об объектах.
Простой пример
- Давайте начнем с рассмотрения того, как вы могли бы определить человека с нормальной функцией. Добавьте эту функцию в элемент сценария
:
function createNewPerson (name) { const obj = {}; obj.name = имя; объектwelcome = function () { alert ('Привет! Я' + имя объекта + '. '); }; return obj; }
- Теперь вы можете создать нового человека, вызвав эту функцию - попробуйте следующие строки в консоли JavaScript вашего браузера:
const salva = createNewPerson ('Salva'); salva.name; salva.greeting ();
- Замените вашу предыдущую функцию следующей:
function Person (имя) { этот.name = name; this.greeting = function () { alert ('Привет! Я' + this.name + '.'); }; }
Функция-конструктор - это версия класса в JavaScript. Обратите внимание, что он имеет все функции, которые вы ожидаете от функции, хотя он ничего не возвращает и не создает явно объект - он в основном просто определяет свойства и методы. Обратите внимание, что здесь также используется ключевое слово this
- в основном это говорит о том, что всякий раз, когда создается один из этих экземпляров объекта, свойство name
объекта будет равно значению имени, переданному в вызов конструктора, и приветствию ()
также будет использовать значение имени, переданное в вызов конструктора.
Примечание : Имя функции конструктора обычно начинается с заглавной буквы - это соглашение используется для упрощения распознавания функций конструктора в коде.
Так как же вызвать конструктор для создания некоторых объектов?
- Добавьте следующие строки под предыдущим добавлением кода:
let person1 = new Person («Боб»); let person2 = новый человек ('Сара');
- Сохраните свой код, перезагрузите его в браузере и попробуйте ввести следующие строки в консоль 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 ()
.
- Удалите код, который вы вставили до сих пор, и добавьте в этот конструктор замены - это в принципе то же самое, что и простой пример, только с немного большей сложностью:
функция Человек (имя, фамилия, возраст, пол, интересы) { этот.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 + '.'); }; }
- Теперь добавьте следующую строку под ним, чтобы создать из него экземпляр объекта:
let person1 = new Person («Боб», «Смит», 32, «мужчина», [«музыка», «катание на лыжах»]);
Теперь вы можете видеть, что у вас есть доступ к свойствам и методам, как мы делали раньше - попробуйте их в своей консоли JS:
человек1 ["возраст"]
person1. интересы [1]
person1.bio ()
Дальнейшие упражнения
Для начала попробуйте добавить еще пару собственных строк создания объекта и попробуйте получить и установить члены полученных экземпляров объекта.
Кроме того, есть несколько проблем с нашим методом bio ()
- вывод всегда включает местоимение «Он», даже если ваш человек - женщина, или какой-либо другой предпочтительный гендерный класс. И биография включает только два интереса, даже если больше перечислено в массиве интересов
.Можете придумать, как исправить это в определении класса (конструкторе)? Вы можете поместить в конструктор любой код, который вам нравится (возможно, вам понадобится несколько условных выражений и цикл). Подумайте о том, как предложения должны быть по-разному структурированы в зависимости от пола и в зависимости от того, составляет ли количество перечисленных интересов 1, 2 или более 2.
До сих пор мы видели два разных способа создания экземпляра объекта - объявление литерала объекта и использование функции-конструктора (см. Выше).
Это имеет смысл, но есть и другие способы - мы хотим познакомить вас с ними на тот случай, если вы встретите их во время своих путешествий по Интернету.
Конструктор Object ()
Прежде всего, вы можете использовать конструктор Object ()
для создания нового объекта. Да, даже у универсальных объектов есть конструктор, который генерирует пустой объект.
- Попробуйте ввести это в консоль JavaScript вашего браузера:
let person1 = new Object ();
- Сохраняет пустой объект в переменной
person1
. Затем вы можете добавить свойства и методы к этому объекту, используя точечную или квадратную нотацию по желанию; попробуйте эти примеры на своей консоли:человек1.name = 'Крис'; person1 ['age'] = 38; person1.greeting = function () { alert ('Привет! Я' + this.name + '.'); };
- Вы также можете передать литерал объекта в конструктор
Object ()
в качестве параметра, чтобы предварительно заполнить его свойствами / методами. Попробуйте это в своей консоли JS:let person1 = new Object ({ имя: 'Крис', возраст: 38, приветствие: function () { alert ('Привет! Я' + this.name + '.'); } });
Использование метода create ()
Конструкторы могут помочь вам упорядочить код - вы можете создавать конструкторы в одном месте, а затем создавать экземпляры по мере необходимости, и ясно, откуда они взялись.
Однако некоторые люди предпочитают создавать экземпляры объектов без предварительного создания конструкторов, особенно если они создают только несколько экземпляров объекта. В JavaScript есть встроенный метод create ()
, который позволяет вам это делать. С его помощью вы можете создать новый объект, используя существующий объект в качестве прототипа вновь созданного объекта.
- Загрузив в браузере завершенное упражнение из предыдущих разделов, попробуйте выполнить это в консоли JavaScript:
let person2 = Объект. создать (person1);
- Теперь попробуйте следующее:
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 () {/ * ... * /}
}
В объектно-ориентированном программировании есть два способа объявления свойств и методов:
- Непосредственно на экземпляре
- В прототипе
Давайте научимся делать и то, и другое.
Объявление свойств и методов с помощью конструкторов
Если вы хотите объявить свойство непосредственно в экземпляре, вы можете записать свойство внутри функции конструктора. Обязательно установите его как свойство для и
.
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, если вам интересно узнать больше.
Предварительный приговор
Мы можем сделать несколько заметок из кода, который мы написали выше. Это мое собственное мнение!
- Классы лучше, чем конструкторы , потому что в классах проще написать несколько методов.
- OLOO странно из-за части
Object. create
. Некоторое время я пробовал OLOO, но я всегда забываю написатьObject.создать
. Для меня достаточно странно не использовать его. - Классы и Factry-функции проще всего использовать. Проблема в том, что функции Factory не поддерживают прототипы. Но, как я уже сказал, в производстве это не имеет значения.
Осталось двое. Должны ли мы тогда выбирать классы или фабричные функции? Давай сравним их!
Классы и фабричные функции - Наследование
Чтобы продолжить обсуждение классов и фабричных функций, нам нужно понять еще три концепции, которые тесно связаны с объектно-ориентированным программированием.
- Наследование
- Инкапсуляция
-
это
Начнем с наследования.
Что такое наследование?
Наследование - загруженное слово. На мой взгляд, многие в индустрии неправильно используют наследование. Слово «наследование» используется, когда вы получаете что-то откуда-то. Например:
- Если вы получаете наследство от родителей, это означает, что вы получаете от них деньги и имущество.
- Если вы унаследовали гены от родителей, это означает, что вы получили свои гены от них.
- Если вы унаследовали процесс от учителя, это означает, что вы получаете этот процесс от него.
Довольно просто.
В JavaScript наследование может означать то же самое: где вы получаете свойства и методы из родительского проекта.
Это означает, что все экземпляры фактически наследуют от своих чертежей . Они наследуют свойства и методы двумя способами:
- путем создания свойства или метода непосредственно при создании экземпляра
- через цепочку прототипов
Мы обсуждали, как использовать оба метода в предыдущей статье, поэтому вернитесь к нему, если вам нужна помощь в просмотре этих процессов в коде.
Для наследования в 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 = новый разработчик ('Крис', 'Койер')
консоль.журнал (крис)
Создание подклассов с заводскими функциями
Есть четыре шага для создания подклассов с фабричными функциями:
- Создание новой функции Factory
- Создание экземпляра родительской схемы
- Создание новой копии этого экземпляра
- Добавление свойств и методов к этой новой копии
Процесс выглядит следующим образом:
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
. Я объясняю почему в этой статье.
Замена родительского метода
Иногда вам нужно перезаписать родительский метод внутри подкласса. Вы можете сделать это по:
- Создание метода с тем же именем
- Вызов родительского метода (необязательно)
- Изменение всего, что вам нужно в методе подкласса
Процесс выглядит следующим образом с Классами:
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, но это не большая победа над классами.
Далее мы рассмотрим классы и фабричные функции более подробно.
Классы и фабричные функции - инкапсуляция
До сих пор мы рассмотрели четыре различных варианта объектно-ориентированного программирования. Две из них - классы и фабричные функции - проще в использовании по сравнению с остальными.
Но остаются вопросы: что использовать? И почему?
Чтобы продолжить обсуждение классов и фабричных функций, нам нужно понять три концепции, которые тесно связаны с объектно-ориентированным программированием:
- Наследование
- Инкапсуляция
-
это
Мы только что говорили о наследовании.Теперь поговорим об инкапсуляции.
Инкапсуляция
Инкапсуляция - это большое слово , но оно имеет простое значение. Инкапсуляция - это процесс помещения одной вещи внутрь другой, чтобы то, что внутри, не просочилось наружу. Подумайте о хранении воды в бутылке. Бутылка предотвращает вытекание воды.
В 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
, потому что они могут сломать машину.
Есть два способа запретить пользователям устанавливать топлива
:
- Частные по соглашению
- Реальные частные члены
Частные по соглашению
В 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.
С другой стороны, инкапсуляция с классами требует добавления #
к частной переменной. Это может сделать вещи неуклюжими.
Мы рассмотрим окончательную концепцию - это
, чтобы завершить сравнение классов и фабричных функций - в следующем разделе.
Классы и фабричные функции - это
переменная
это
(ха!) - один из главных аргументов против использования классов для объектно-ориентированного программирования.Зачем? Поскольку это значение
меняется в зависимости от того, как оно используется. Это может сбивать с толку многих разработчиков (как новичков, так и опытных).
Но на самом деле концепция и этого
относительно проста. Есть только шесть контекстов, в которых вы можете использовать и
. Если вы освоите эти шесть контекстов, у вас не будет проблем с использованием и
.
Шесть контекстов:
- В глобальном контексте
- Создание объекта Inan
- В свойстве / методе объекта
- В простой функции
- В функции стрелки
- В прослушивателе событий
Я подробно рассмотрел эти шесть контекстов.Прочтите его, если вам нужна помощь в понимании и
.
Примечание: Не упустите возможность научиться использовать и
. Это важная концепция, которую вам необходимо понять, если вы собираетесь освоить 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 - нет.Я предпочитаю здесь заводские функции, потому что:
- Контекст
, этот
может изменяться (что может сбивать с толку) - Код, написанный с фабричными функциями, короче и чище (поскольку мы можем использовать инкапсулированные переменные без записи
this. # Variable
).
Далее идет последний раздел, в котором мы создаем простой компонент вместе с классами и функциями Factory. Вы увидите, чем они отличаются и как использовать прослушиватели событий с каждым flavolr.
Классы и фабричные функции - Слушатели событий
В большинстве статей по объектно-ориентированному программированию приведены примеры без прослушивателей событий. Эти примеры легче понять, но они не отражают ту работу, которую мы выполняем как разработчики внешнего интерфейса. Для работы, которую мы делаем, требуются прослушиватели событий - по простой причине - потому что нам нужно создавать вещи, которые полагаются на ввод данных пользователем.
Поскольку слушатели событий изменяют контекст на
, они могут затруднить работу с классами.В то же время они делают функции Factory более привлекательными.
Но на самом деле это не так.
Изменение на этот
не имеет значения, если вы знаете, как обрабатывать , этот
как в классах, так и в фабричных функциях. Несколько статей посвящены этой теме, поэтому я подумал, что было бы хорошо завершить эту статью с помощью простого компонента, использующего разновидности объектно-ориентированного программирования.
Строительство прилавка
В этой статье мы собираемся построить простой счетчик.Мы будем использовать все, что вы узнали из этой статьи, включая частные переменные.
Допустим, счетчик содержит две вещи:
- Сам счет
- Кнопка для увеличения счетчика
Вот простейший HTML-код для счетчика:
Счетчик: 0
Построение счетчика с классами
Чтобы упростить задачу, мы попросим пользователей найти и передать HTML-код счетчика в класс Counter
.
class Counter () {
конструктор (счетчик) {
// Делаем что-нибудь
}
}
// Применение
const counter = новый счетчик (document. querySelector ('. counter'))
Нам нужно получить два элемента в классе Counter
:
Counter () {
конструктор (счетчик) {
этот.countElement = counter.querySelector ('диапазон')
this.buttonElement = counter.querySelector ('кнопка')
}
}
Мы инициализируем переменную count
и установим для нее то, что показывает countElement
. Мы будем использовать частную переменную #count
, так как счетчик не должен отображаться где-либо еще.
class Counter () {
#count
конструктор (счетчик) {
// ...
this. # count = parseInt (countElement.textContent)
}
}
Когда пользователь нажимает
, мы хотим увеличить #count
.Мы можем сделать это другим методом. Назовем этот метод Увеличить счет
.
class Counter () {
#count
конструктор (счетчик) {/ * ... * /}
увеличитьCount () {
this. # count = this. # count + 1
}
}
Затем нам нужно обновить DOM новым #count
. Для этого создадим метод updateCount
. Звоним updateCount
из увеличитьCount
:
class Counter () {
#count
конструктор (счетчик) {/ *... * /}
увеличитьCount () {
this. # count = this. # count + 1
this.updateCount ()
}
updateCount () {
this.countElement.textContent = this. # count
}
}
Теперь мы готовы добавить прослушиватель событий.
Добавление прослушивателя событий
Мы добавим прослушиватель событий в this.buttonElement
. К сожалению, мы не можем сразу использовать Increase
в качестве обратного вызова. Если попробуете, вы получите сообщение об ошибке.
class Counter () {
//...
конструктор (счетчик) {
// ...
this.buttonElement.addEventListener ('щелкните', this.increaseCount)
}
// Методы
}
Вы получаете сообщение об ошибке, потому что это
указывает на buttonElement
. (Это контекст прослушивателя событий.) Вы увидите кнопку buttonElement
, если вы вошли в консоль , это
.
Нам нужно изменить значение , это
обратно на экземпляр IncreaseCount
, чтобы все работало.Это можно сделать двумя способами:
- Использовать привязку
- Использовать стрелочные функции
Большинство людей используют первый метод (но второй проще).
Добавление прослушивателя событий с привязкой
bind
возвращает новую функцию. Это позволяет вам заменить это
на первый переданный аргумент. Обычно слушатели событий создаются путем вызова bind (this)
.
class Counter () {
//...
конструктор (счетчик) {
// ...
this.buttonElement.addEventListener ('щелкните', this.increaseCount.bind (это))
}
// ...
}
Это работает, но читать не очень приятно. Это также не подходит для новичков, потому что bind
рассматривается как расширенная функция JavaScript.
Стрелочные функции
Второй способ - использовать стрелочные функции. Стрелочные функции работают, потому что они сохраняют это значение
в лексическом контексте.
Большинство людей пишут методы внутри обратного вызова стрелочной функции, например:
class Counter () {
//...
конструктор (счетчик) {
// ...
this.buttonElement.addEventListener ('щелчок', _ => {
this.increaseCount ()
})
}
// Методы
}
Это работает, но это долгий путь. На самом деле есть ярлык.
Вы можете создать Increase
с помощью стрелочных функций. Если вы сделаете это, значение , это значение
для IncreaseCount
будет сразу же привязано к значению экземпляра.
Итак, вот код, который вам нужен:
class Counter () {
//...
конструктор (счетчик) {
// ...
this.buttonElement.addEventListener ('щелкните', this.increaseCount)
}
увеличитьCount = () => {
this. # count = this. # count + 1
this.updateCounter ()
}
// ...
}
Код
Вот полная версия кода на основе классов (с использованием стрелочных функций).
Создание счетчика с заводскими функциями
Здесь мы сделаем то же самое. Мы попросим пользователей передать HTML-код счетчика на завод Counter
.
функция Счетчик (счетчик) {
// ...
}
const counter = Counter (document.querySelector ('. counter'))
Нам нужно получить два элемента от counter
-
и
. Мы можем использовать здесь обычные переменные (без , это
), потому что они уже являются частными переменными. Мы их не разоблачаем.
функция Счетчик (счетчик) {
const countElement = counter.querySelector ('диапазон')
const buttonElement = counter.querySelector ('кнопка')
}
Мы инициализируем переменную счетчика значением, которое присутствует в HTML.
функция Счетчик (счетчик) {
const countElement = counter.querySelector ('диапазон')
const buttonElement = counter.querySelector ('кнопка')
пусть count = parseInt (countElement.textContext)
}
Мы увеличим эту переменную count до
с помощью метода IncreaseCount
. Вы можете выбрать здесь обычную функцию, но мне нравится создавать метод, позволяющий поддерживать порядок и порядок.
функция Счетчик (счетчик) {
// ...
const counter = {
увеличитьCount () {
count = count + 1
}
}
}
Наконец, мы обновим счетчик с помощью метода updateCount
. Мы также будем называть updateCount
из Increase
.
функция Счетчик (счетчик) {
// ...
const counter = {
увеличитьCount () {
count = count + 1
counter.updateCount ()
}
updateCount () {
увеличитьCount ()
}
}
}
Обратите внимание, что я использовал счетчик .updateCount
вместо this.updateCount
? Мне это нравится, потому что счетчик
более понятен по сравнению с этим
. Я делаю это также потому, что новички также могут ошибиться с , этим
внутри функций Factory (о которых я расскажу позже).
Добавление слушателей событий
Мы можем добавить слушателей событий к buttonElement
. Когда мы это сделаем, мы можем сразу использовать counter.increaseCount
в качестве обратного вызова.
Мы можем это сделать, потому что мы не использовали , это
, поэтому это не имеет значения, даже если прослушиватели событий изменят это значение
.
function Counter (counterElement) {
// Переменные
// Методы
const counter = {/ * ... * /}
// Слушатели событий
buttonElement.addEventListener ('щелчок', counter.increaseCount)
}
это
gotcha
Вы можете использовать и
в заводских функциях. Но вам нужно использовать , это
в контексте метода.
В следующем примере, если вы вызываете counter.increaseCount
, JavaScript также вызовет counter.updateCount
. Это работает, потому что это
указывает на переменную counter
.
function Counter (counterElement) {
// Переменные
// Методы
const counter = {
IncreaseCount () {
count = count + 1
this.updateCount ()
}
}
// Слушатели событий
buttonElement.addEventListener ('щелчок', counter.increaseCount)
}
К сожалению, прослушиватель событий не будет работать, потому что это значение
было изменено. Вам потребуется то же самое, что и с классами - с функциями привязки или стрелок, чтобы снова заставить обработчик событий работать.
И это подводит меня ко второй проблеме.
Второй это
gotcha
Если вы используете синтаксис функции Factory, вы не можете создавать методы с функциями стрелок. Это потому, что методы создаются в простом контексте функции
.
function Counter (counterElement) {
// ...
const counter = {
// Не делайте этого.
// Не работает, потому что `this` равно` Window`
увеличитьCount: () => {
count = count + 1
этот.updateCount ()
}
}
// ...
}
Итак, я настоятельно рекомендую полностью пропустить это
, если вы используете функции Factory. Так намного проще.
Код
Вердикт для слушателей событий
Слушатели событий изменяют значение на
, поэтому мы должны быть очень осторожны при использовании на значение
. Если вы используете классы, я рекомендую создавать обратные вызовы прослушивателей событий с функциями стрелок, чтобы вам не приходилось использовать привязку
.
Если вы используете заводские функции, я рекомендую полностью пропустить , этот
, потому что это может вас запутать. Это оно!
Заключение
Мы говорили о четырех разновидностях объектно-ориентированного программирования. Их:
- Функции конструктора
- Классы
- OLOO
- Заводские функции
Во-первых, мы пришли к выводу, что классы и заводские функции легче использовать с точки зрения кода.
Во-вторых, мы сравнили, как использовать подклассы с классами и фабричными функциями.Здесь мы видим, что создание подклассов проще с классами, но компоновка проще с функциями Factory.
В-третьих, мы сравнили инкапсуляцию с классами и фабричными функциями. Здесь мы видим, что инкапсуляция с фабричными функциями естественна, как и JavaScript, в то время как инкапсуляция с классами требует, чтобы вы добавили #
перед переменными.
В-четвертых, мы сравнили использование и
в классах и фабричных функциях. Я чувствую, что здесь выигрывают заводские функции, потому что и
могут быть неоднозначными.Написание this. # PrivateVariable
также создает более длинный код по сравнению с использованием privateVariable
.
Наконец, в этой статье мы создали простой счетчик с классами и фабричными функциями. Вы узнали, как добавлять прослушиватели событий в обе разновидности программирования объектно-ориентированного программирования. Здесь работают оба вкуса. Вам просто нужно быть осторожным, используете ли вы это
или нет.
Вот и все!
Надеюсь, это проливает свет на объектно-ориентированное программирование на JavaScript.Если вам понравилась эта статья, возможно, вам понравится мой курс JavaScript, Learn JavaScript, где я объясняю (почти) все, что вам нужно знать о JavaScript, в таком ясном и кратком формате.
Если у вас есть какие-либо вопросы по JavaScript или фронтенд-разработке в целом, не стесняйтесь обращаться ко мне. Я посмотрю, чем могу помочь!
Объектно-ориентированное программирование на JavaScript - Scotch.io
Объектно-ориентированное программирование - это популярный стиль программирования, с самого начала укоренившийся в JavaScript.
Он настолько глубоко внедрен в JavaScript, что многие из собственных функций и методов JavaScript написаны в объектно-ориентированном стиле; вы также найдете множество популярных библиотек, написанных в объектно-ориентированном стиле.
Если у вас уже есть опыт работы с объектно-ориентированным программированием на другом языке, отложите в сторону знания, которые вы знаете, и прочитайте весь модуль как новичок.
Это связано с тем, что объектно-ориентированное программирование в JavaScript сильно отличается от объектно-ориентированного программирования на других языках.Если вы будете придерживаться своих текущих идей, вы можете сделать объектно-ориентированное программирование в JavaScript экспоненциально сложнее, чем оно есть на самом деле.
А теперь давайте начнем с выяснения, что такое объектно-ориентированное программирование.
Объектно-ориентированное программирование - это две вещи:
- Создание отдельных объектов из общего объекта
- Наследование
В этой статье мы рассмотрим только первую часть. Мы рассмотрим наследование в другой статье.
Вы знаете, что у объектов в JavaScript есть свойства и методы. Эти объекты могут напоминать реальные вещи (даже людей!).
Предположим, вы пытаетесь создать объект Human
на JavaScript. У человека firstName
, lastName
и возраст
. Вы можете добавить эти атрибуты как свойства в JavaScript.
Люди могут не только иметь имена и возраст, но и произносить их имена. Вы также можете написать функцию sayName
как метод в объекте human
.
const human = {
firstName: 'Zell'
lastName: 'Liew',
возраст: 29,
sayName () {
console.log (`Я Зелл Лью`)
}
}
human.sayName ()
Поскольку люди очень похожи друг на друга, вы также можете создать другой человеческий объект с такими же свойствами ( firstName
, lastName
, age
) и методами ( sayName
).
К сожалению, вы больше не можете называть это человек
, потому что используется переменная человек
.
const human2 = {
firstName: 'Винси',
lastName: 'Чжан',
возраст: 28,
sayName () {
console.log (`Я Винси Чжан`)
}
}
Предполагая, что вы знаете Винси, странно называть ее человек2
, верно? Вы, вероятно, вместо этого назовете ее Винси.
Итак, вместо того, чтобы называть наши человеческие объекты human
и human2
, имеет смысл называть их Zell
и Vincy
.
const Zell = {
firstName: 'Zell'
lastName: 'Liew',
возраст: 29,
sayName () {
консоль.журнал (`Я Зелл Лью`)
}
}
const Винси = {
firstName: 'Винси',
lastName: 'Чжан',
возраст: 28,
sayName () {
console.log (`Я Винси Чжан`)
}
}
Ну вот, намного лучше.
А что, если вам нужно создать еще человек
? Вам придется написать весь объект с нуля, но это же утомительно, верно? Вы копируете так много кода!
const Николас = {
firstName: 'Николас',
lastName: 'Tze',
возраст: 30,
sayName () {
консоль.журнал (`Я Николас Цзе`)
}
}
Здесь вы заметите, что люди немного отличаются друг от друга - у нас разные имена и мы разного возраста.
Но у нас есть общая точка - мы можем назвать свое имя.
Разве не было бы хорошо, если бы вы могли создать функцию (или что-то подобное), которая создает людей?
Оказывается, можно! Все, что вам нужно сделать, это создать функцию с ключевым словом this
, и вы можете создавать отдельных лиц с новым ключевым словом
.
Вот как это выглядит:
function Human (firstName, lastName, age) {
this.firstName = firstName
this.lastName = lastName
this.age = возраст
this.sayName = function () {
console.log (`Я $ {firstName} $ {lastName}`)
}
}
const zell = новый человек ('Zell', 'Liew', 29)
Если вы запустите console.log (zell)
, вы увидите, что это человек с firstName
, lastName
, возрастом
и способностью sayName
.
Созданный человек имеет свойства firstName, lastName и age. У него также есть метод sayName.
С этого момента вы можете создавать любое количество людей с одинаковым синтаксисом. Каждый созданный вами человек сохранит свою индивидуальность, и все они смогут произнести свое имя!
const vincy = новый человек ('Винси', 'Чжан', 28)
const nicolas = новый человек ('Николас', 'Цзе', 30)
vincy.sayName ()
nicolas.sayName ()
Круто, не правда ли?
И вот что в двух словах представляет собой объектно-ориентированная часть объектно-ориентированного программирования: вы создаете функцию ( Human
) , которая может создавать экземпляры (такие люди, как zell
, vincy
и nicolas
) .Каждый создаваемый вами экземпляр должен содержать отдельные данные, которые отличаются от других экземпляров.
Функция, которую вы используете для создания экземпляров, называется конструктором .
В объектно-ориентированном программировании первая буква конструктора пишется с заглавной буквы ( Human
), тогда как каждый экземпляр записывается как обычная переменная ( zell
, vincy
, nicolas
).
Это небольшое различие сразу показывает разницу между конструкторами и экземплярами в вашем коде.
это
- ключевое слово в JavaScript. Когда он используется в конструкторе, он относится к экземпляру, созданному с помощью конструктора.
Если вы попытаетесь ввести console.log (this)
в конструкторе, вы заметите, что это то же самое, что и запись самого экземпляра в журнал.
function Human (firstName, lastName, age) {
console.log (это)
}
const zell = новый человек ('Zell', 'Liew', 29)
этот
в конструкторе указывает на экземпляр
Этот
является очень важным понятием в объектно-ориентированном программировании.Так что вам нужно быть с ним знакомым. Вот статья, которая подробно объясняет и
.
Одна из основных частей объектно-ориентированного программирования - создание экземпляров из конструкторов . Каждый создаваемый вами экземпляр должен сохранять свою индивидуальность, чтобы он отличался от других экземпляров.
Когда вы создаете конструктор, вы должны использовать первую букву его имени с заглавной буквы (например, Human
), чтобы отличать его от экземпляров (например, zell
).
Если вам понравилась эта статья, вам понравится Learn JavaScript - курс, который поможет вам научиться создавать настоящие компоненты с нуля с помощью Javascript.
Понравилась эта статья? Подпишитесь на @zellwk в Twitter
Быстрое изучение современного JavaScript и объектно-ориентированного программирования
Что такое объектно-ориентированное программирование (ООП)?
Объектно-ориентированное программирование (ООП) - популярная парадигма программирования или стиль программирования. Он существует с 70-х годов, но в отличие от инструментов и фреймворков, которые приходят и уходят, ООП по-прежнему очень актуально.Это потому, что это не язык программирования и не инструмент. Это стиль программирования.
Зачем изучать ООП?
ООП помогает управлять программным обеспечением и снижать его сложность за счет создания многократно используемых строительных блоков (объектов). Правильно спроектированные объекты обеспечивают простой интерфейс и скрывают ненужную сложность извне, как DVD-плеер! DVD-плеер имеет сложную логическую плату внутри и несколько кнопок снаружи. Когда вы нажимаете кнопку воспроизведения, вам все равно, как все эти микрочипы общаются друг с другом.
Вам поможет объектно-ориентированное программирование:
- Управление и снижение сложности
- Устранение избыточного кода
- Создание повторно используемых строительных блоков
- Написание кода очистки
Необходимые навыки для каждого разработчика
ООП встречается во многих технических интервью. Итак, если вы действительно хотите стать серьезным разработчиком, вам необходимо разбираться в объектно-ориентированном программировании. Как технический интервьюер, если я вижу кандидата с ООП в резюме, этот кандидат выделяется для меня.
Вам может быть интересно узнать, что многие из популярных фреймворков, которые вы, возможно, используете, на самом деле разработаны с учетом концепций ООП. Angular - пример таких фреймворков!
Пошаговая инструкция, курс от А до Я
Что вы получите, когда запишетесь на этот курс:
- Узнайте все об объектно-ориентированном программировании на JavaScript
- Больше не нужно тратить время на разрозненные учебники
- Учитесь в своем собственном темпе - пожизненный доступ - так что не торопитесь, если вы предпочитаете
- Смотрите на любом устройстве, онлайн или офлайн
- Укрепите свое понимание JavaScript
- Подготовьтесь к техническим собеседованиям
- Уверенно создавайте приложения с помощью Node, Angular, React, Vue и других библиотек JavaScript
- Изучите передовой опыт и распространенные ошибки, которых можно избежать
- Сертификат об окончании для представления вашему работодателю
Вы узнаете
- Основные принципы ООП: инкапсуляция, абстракция, наследование и полиморфизм
- Как реализовать концепции ООП в JavaScript
- Все об объектах, их свойствах, дескрипторах свойств, геттерах и сеттерах
- Разница между функциями фабрики и конструктора
- Как "this" работает в JavaScript
- Прототипы и прототипное наследование: одна из запутывающих частей JavaScript
- Функции ES6 +, используемые в объектно-ориентированном программировании
Этот курс для вас, если:
Вы разработчик, который уже знает принципы ООП, но хочет узнать, как реализовать их в JavaScript.
Вы - разработчик, не знакомый с ООП, возможно, вы просто знаете основы JavaScript и хотите укрепить свое понимание JS и подготовиться к техническим собеседованиям.
Использование ООП и JS в вашем резюме поможет вам найти больше работы и заработать больше денег.
Готовы ли вы вывести свои навыки JavaScript на новый уровень? Запишитесь на курс и начните.
Принципы ООП | Проект Odin
Введение
К этому моменту вы изучите и сможете практиковать наиболее распространенные шаблоны создания и организации объектов в JavaScript.Но это всего лишь верхушка айсберга. Более важным, чем изучение синтаксиса фабричных функций или модулей, является выяснение того, как их эффективно использовать.
Вся эта серия уроков посвящена парадигме «объектно-ориентированного программирования» (ООП). Основы создания объектов и классов относительно просты. Но непросто решить, что добавить в каждый объект, или когда создать новый объект, или когда позволить объекту «унаследовать» от другого.
Результаты обучения
К концу этого урока вы должны уметь делать следующее:
- Объясните «Принцип единой ответственности».
- Кратко объясните дополнительные принципы SOLID.
- Объясните, что такое «тесно связанные» объекты и почему мы хотим их избегать.
К счастью, есть несколько концепций и принципов, которые помогут нам принять правильные решения, когда дело касается наших объектов. Этот урок представляет собой введение в наиболее важные из этих концепций.Имейте в виду, что обычно нет очень четкого ответа на вопросы о дизайне вашего приложения. Некоторые шаблоны и идеи явно лучше других, но часто приходится идти на компромисс при принятии решения о том, где разместить конкретную функцию. Другими словами .. эти принципы не правила - это полезные рекомендации.
Читая эти ресурсы, вы можете вернуться к некоторым проектам, которые вы уже сделали, и подумать о том, как то, что вы написали, соответствует примерам, которые вы видите.И, конечно же, помните об этом, когда будете создавать новые проекты.
Единственная ответственность
Когда вы создаете свои объекты, одна из самых важных вещей, которые следует помнить, - это принцип единой ответственности , который гласит, что класс (или объект, или модуль… вы поняли) должен иметь только и одну ответственность. Это не означает, что объект может делать только одну вещь, но это означает, что все, что делает объект, должно быть частью одной ответственности.
Вот действительно распространенный пример. В большей части нашего кода есть функции для обновления и записи вещей в DOM в дополнение к логике нашего приложения. Отличная идея - это действительно , чтобы отделить DOM от логики приложения.
Так вместо этого:
function isGameOver () {
// игра окончена логикой!
if (gameOver) {
const gameOverDiv = document.createElement ('div')
gameOverDiv.classList.add ('игра окончена')
gameOverDiv.textContent = `$ {this.победитель} выиграл игру! `
document.body.appendChild (gameOverDiv)
}
}
Вы должны извлечь все манипуляции с DOM в отдельный модуль и использовать его так:
function isGameOver () {
// игра окончена логикой!
if (gameOver) {
DOMStuff.gameOver (this.winner)
}
}
Фактически - функция isGameOver
в любом случае не должна вызывать функцию DOM. Это должно быть в другом месте (прямо в игровом цикле).
Другой способ подумать о принципе единой ответственности состоит в том, что у данного метода / класса / компонента должна быть единственная причина для изменения.В противном случае, если объект пытается иметь несколько обязанностей, изменение одного аспекта может повлиять на другой.
Принцип единой ответственности - это первый из обычно встречающихся 5 принципов проектирования, называемых принципами SOLID . Вы узнаете больше об этих принципах в статьях с заданиями ниже.
Слабосвязанные объекты
Очевидно, что все наши объекты предназначены для совместной работы, чтобы сформировать наше окончательное приложение. Однако вам следует позаботиться о том, чтобы ваши отдельные объекты могли как можно дольше стоять отдельно. Плотно связанные объекты - это объекты, которые настолько сильно зависят друг от друга, что удаление или изменение одного будет означать, что вам придется полностью изменить другой - настоящий облом.
Этот вопрос довольно сильно связан с «единственной ответственностью», но имеет другой угол зрения. Например, если мы писали игру и хотели полностью изменить способ работы пользовательского интерфейса, мы могли бы сделать это без полной переделки игровой логики. Таким образом, мы сможем начать писать нашу игру, используя в основном консоль .logs ()
, а затем добавить кучу функций DOM
позже, не затрагивая логику игры.
Переуступка
- В следующих статьях упоминается аббревиатура SOLID перед тем, как говорить о единой ответственности. Единая ответственность определенно является наиболее актуальной из пяти. Если хотите, вы можете изучить остальные принципы SOLID ... но обратите особое внимание на единую ответственность.
- Прочтите SOLID JavaScript: The Single Responsibility Principle. ПРИМЕЧАНИЕ. В этой статье действительно используется JQuery, одна из самых ранних и самых популярных библиотек JavaScript до стандарта ES6. Хотя проект Odin не учит JQuery, и от вас не ожидается, что вы поймете этот пример, не забудьте сосредоточиться меньше на самом коде и больше на выражаемых концепциях SOLID.
- 5 принципов, которые сделают вас SOLID разработчиком JavaScript, затрагивает ту же тему, а также кратко охватывает остальную часть «SOLID».
- И прочтите С.O.L.I.D. Первые 5 принципов объектно-ориентированного дизайна с использованием JavaScript для хорошей меры.
- Как написать высокомасштабируемый и поддерживаемый JavaScript: Coupling довольно хорошо объясняет слабосвязанные объекты.
Дополнительные ресурсы
Лучшая книга по теме слабой связи, которую мы когда-либо читали, - это «Практический объектно-ориентированный дизайн в Ruby». К сожалению, это не бесплатно .. и не JavaScript. Мы уверены, что можем рекомендовать его в любом случае.Если вы не знаете Ruby, это достаточно ясный язык, поэтому вам не обязательно учить его, чтобы следовать примерам, а содержание книги действительно фантастическое. Кроме того, 99 бутылок ООП написаны как на JavaScript, так и на Ruby. Он написан тем же автором и может быть лучшим вариантом, если вы новичок в ООП (это тоже не бесплатно).
Хороший обзор наиболее распространенных принципов ООП с использованием JavaScript
Объектно-ориентированное программирование на Javascript
Что такое ООП (объектно-ориентированное программирование)?
ООП - это шаблон программирования, в котором говорится, что состояние (свойство) и действие (метод) должны храниться вместе в одном блоке (классе).ООП пытается моделировать объекты реального мира. Наш код полезно систематизировать.
Есть два типа языков ООП:
- Языки на основе классов, такие как java, C #, php, C ++ и т. Д.
- Языки на основе прототипов javascript
Есть четыре основных / столпа ООП:
- Инкапсуляция:
- ООП помещает вещи (код) в объект и организует вещи в единицах, которые моделируют реальный мир. Это инкапсуляция.
- Абстракция
- ООП скрывает сложность и детали реализации, нам просто нужно вызвать метод, а остальная сложность скрыта от нас, это абстракция.Абстракция также обеспечивает уровень безопасности, такой как частная собственность или частный метод
- Наследование
- Наследование означает повторное использование кода, который расширяется от родительского класса, где все свойства и методы (защищенные или общедоступные) доступны для дочернего класса
- Полиморфизм
- Полиморфизм означает множество форм (Поли означает множество; морфизм означает форму). Но как это связано с ООП. Это связано с ООП концепцией, называемой переопределением и перегрузкой.
- overriding: Предположим, у нас есть метод getMarks с именем в родительском классе и с тем же именем у нас есть метод в дочернем классе, который переопределит функциональность родительского класса, это переопределение. Это полезно, когда мы хотим, чтобы пользовательская логика для дочернего класса все еще использовала часть логики родительского класса Перегрузка
- : в одном классе у нас может быть два или более метода с одинаковым именем, но с разным количеством аргументов или с другим типом данных, это называется перегрузкой.
Выше было определение и принцип ООП. Теперь давайте разберемся, как реализовать ООП в javascript.
Для понимания ООП в javascript у вас должно быть четкое представление о прототипном наследовании в javascript. Если вам это неясно, то, пожалуйста, проверьте мой блог по этому поводу https://dev.to/bhaveshdaswani93/prototype-inheritance-in-javascript- 1c3g
У нас есть четыре способа работы с ООП в javascript:
1. Заводская функция.
2.Конструктор функций
3.Object.create ()
4.ES6 классы
Давайте посмотрим на них по одному
1.Factory Function:
Здесь мы создаем функцию, которая получает параметр и взамен предоставляет объект, давайте посмотрим, как это полезно для предотвращения дублирования кода.
Здесь, в примере, у personA и personB есть определение функции getFullName, которое указывает на дублирование кода, что не является хорошей практикой программирования. Чтобы избежать этого, мы создали фабричную функцию с именем person, в которой мы передаем first_name и last_name в качестве параметра, и она предоставит требуемый объект.Используя фабричную функцию, мы избегаем повторения нашего кода. Но есть проблема с эффективностью памяти, поскольку функция getFullName будет создаваться каждый раз, когда вызывается функция person, что нехорошо, потому что, если мы можем разместить getFullName в общем месте в памяти, где каждый новый объект person может вызывать его, тогда это также будет эффективно с точки зрения памяти , для этого перейдем к функции-конструктору.
2. Конструктор функции:
Что такое функция конструктора? Функция, которая запускается с помощью ключевого слова new
, является функцией-конструктором, и функция-конструктор должна начинаться с заглавной буквы (рекомендуется называть функцию-конструктор первой буквой заглавной, например, Person, хорошо против человека).Давайте посмотрим на это на примере и на то, как это помогает эффективно использовать память.
В приведенном выше примере Person - это функция-конструктор, и у нее есть функция getFullName в своем объекте-прототипе. Я создал два экземпляра personA, personB из конструктора Person. Мы видим, что код можно восстановить, имея конструктор и прототип. Функция конструктора имеет этот код, который является уникальным для экземпляра, что означает, что personA и personB имеют собственные свойства first_name и last_name, в то время как прототип имеет тот код, который используется экземпляром, а также свойства прототипа не копируются в экземпляр, они разрешаются через цепочка прототипов, которая позволяет эффективно использовать память функции конструктора.
3.Object.create ():
Метод Object.create ()
создает новый объект, используя существующий объект в качестве прототипа вновь созданного объекта. В предыдущем разделе мы узнали, как создать прототип с помощью функции конструктора, давайте посмотрим, как мы можем создать прототип с помощью Object.create ()
на примере
В приведенном выше примере я создал объект person и использовал его в качестве прототипа объекта personA с помощью объекта Object.создать (человек)
. Object.create создаст цепочку прототипов, в которой свойство __proto__
для personA будет указывать на объект person.
4.ES6 classes:
ES6 содержит класс ключевых слов, знакомый большинству разработчиков OOPS. Класс в javascript похож на синтаксический сахар за кулисами, он по-прежнему следует прототипному наследованию. Ключевое слово class было введено, чтобы упростить разработчикам написание ООП-программирования на javascript.давайте посмотрим на классы в действии.
Выше приведен пример класса. Я создал класс Person, который содержит функцию-конструктор, свойства и метод которой будут скопированы в каждый экземпляр, в то время как остальная часть метода или свойств, таких как getFullName, является общей.
Позвольте мне предоставить вам шпаргалку по классам: все, что вы хотите быть уникальным для экземпляра, например first_name, last_name, уникально для каждого экземпляра, поместите это свойство или метод в функцию конструктора, а разделяемое свойство или метод, например getFullName, должно быть размещено снаружи конструктор и должен находиться внутри класса.
Заключение:
ООП - это стиль программирования, который помогает писать ясный, понятный, легко расширяемый, простой в обслуживании, эффективный с точки зрения памяти и СУХИЙ код.
В Javascript у нас есть 4 способа написать код ООП
- Заводская функция
- Функция-конструктор
- Object.create
- Классы
Разбираемся в путанице классов ES6
JavaScript - странный язык. Хотя он вдохновлен Smalltalk, он использует синтаксис, подобный Си.Он сочетает в себе аспекты парадигм процедурного, функционального и объектно-ориентированного программирования (ООП). В нем есть множество, часто избыточных, подходов к решению практически любой мыслимой проблемы программирования, и нет особых мнений относительно того, какие из них предпочтительны. Он слабо и динамично типизирован, с лабиринтным подходом к приведению типов, который сбивает с толку даже опытных разработчиков.
JavaScript также имеет свои недостатки, ловушки и сомнительные особенности. Новые программисты борются с некоторыми из его более сложных концепций - например, асинхронность, замыкания и подъем.Программисты, имеющие опыт работы с другими языками, разумно предполагают, что вещи с похожими именами и внешним видом будут работать в JavaScript таким же образом, и часто ошибаются. Массивы на самом деле не массивы; Что такое , этот
, что такое прототип и что на самом деле делает новый
?
Проблема с классами ES6
Самым серьезным нарушителем, безусловно, является новичок в последней версии JavaScript, ECMAScript 6 (ES6): классы . Некоторые разговоры вокруг классов откровенно тревожны и раскрывают глубоко укоренившееся непонимание того, как на самом деле работает язык:
«JavaScript, наконец, настоящий объектно-ориентированный язык , теперь, когда у него есть классы!»
или:
«Классы освобождают нас от размышлений о сломанной модели наследования в JavaScript.”
Или даже:
«Классы - более безопасный и простой подход к созданию типов в JavaScript».
Эти утверждения меня не беспокоят, потому что они подразумевают, что с прототипным наследованием что-то не так; давайте отбросим эти аргументы. Эти утверждения беспокоят меня, потому что ни одно из них не является правдой, и они демонстрируют последствия подхода JavaScript «все для всех» к языковому дизайну: он калечит понимание языка программистом чаще, чем позволяет.Прежде чем я продолжу, давайте проиллюстрируем.
Популярная викторина по JavaScript №1: в чем принципиальная разница между этими блоками кода?
function PrototypicalGreeting (welcome = "Hello", name = "World") {
this.greeting = приветствие
this.name = имя
}
PrototypicalGreeting.prototype.greet = function () {
return `$ {this.greeting}, $ {this.name}!`
}
const greetProto = new PrototypicalGreeting («Привет», «ребята»)
console.log (greetProto.greet ())
class ClassicalGreeting {
конструктор (приветствие = "Hello", name = "World") {
этот.приветствие = приветствие
this.name = имя
}
greet () {
return `$ {this.greeting}, $ {this.name}!`
}
}
const classyGreeting = new ClassicalGreeting («Привет», «ребята»)
console.log (classyGreeting.greet ())
Ответ: , нет ни одного . По сути, они делают то же самое, вопрос только в том, использовался ли синтаксис класса ES6.
Правда, второй пример более выразительный. Уже по одной этой причине вы можете утверждать, что class
- хорошее дополнение к языку.К сожалению, проблема более тонкая.
Популярный тест на JavaScript № 2: что делает следующий код?
function Proto () {
this.name = 'Proto'
вернуть это;
}
Proto.prototype.getName = function () {
вернуть this.name
}
class MyClass extends Proto {
constructor () {
супер()
this.name = 'MyClass'
}
}
const instance = новый MyClass ()
console.log (instance.getName ())
Proto.prototype.getName = function () {return 'Overridden in Proto'}
консоль.журнал (instance.getName ())
MyClass.prototype.getName = function () {return 'Overridden in MyClass'}
console.log (instance.getName ())
instance.getName = function () {return 'Переопределено в экземпляре'}
console.log (instance.getName ())
Правильный ответ - вывод на консоль:
> MyClass
> Переопределено в Proto
> Переопределено в MyClass
> Переопределено в экземпляре
Если вы ответили неправильно, вы не понимаете, что такое class
на самом деле.Это не твоя вина. Как и Array
, class
не является особенностью языка, это синтаксический обскурантизм . Он пытается скрыть прототипную модель наследования и неуклюжие идиомы, которые с ней связаны, и подразумевает, что JavaScript делает что-то, чего не делает.
Вам, возможно, сказали, что class
был введен в JavaScript, чтобы сделать классических разработчиков ООП, пришедших с таких языков, как Java, более комфортными с моделью наследования классов ES6.Если вы, , являетесь одним из тех разработчиков, этот пример, вероятно, вас ужаснул. Должно. Это показывает, что ключевое слово class
в JavaScript не дает никаких гарантий, которые должен предоставить класс. Он также демонстрирует одно из ключевых различий в модели наследования прототипов: прототипы - это экземпляра объекта , а не типа .
Прототипы и классы
Наиболее важное различие между наследованием на основе классов и прототипов состоит в том, что класс определяет тип , который может быть создан во время выполнения, тогда как прототип сам является экземпляром объекта.
Дочерним элементом класса ES6 является другое определение типа , которое расширяет родительский элемент новыми свойствами и методами, которые, в свою очередь, могут быть созданы во время выполнения. Дочерним элементом прототипа является другой объект , экземпляр , который делегирует родительскому объекту любые свойства, не реализованные в дочернем элементе.
Боковое примечание: вам может быть интересно, почему я упомянул методы класса, но не методы прототипа. Это потому, что в JavaScript нет концепции методов.Функции являются первоклассными в JavaScript, и они могут иметь свойства или быть свойствами других объектов.
Конструктор класса создает экземпляр класса. Конструктор в JavaScript - это просто старая функция, возвращающая объект. Единственная особенность конструктора JavaScript заключается в том, что при вызове с ключевым словом new
он назначает свой прототип в качестве прототипа возвращаемого объекта. Если вас это немного сбивает с толку, значит, вы не одиноки - это так, и это большая часть того, почему прототипы плохо изучены.
Если уточнить это, то дочерний элемент прототипа не является копией своего прототипа и не является объектом с той же формой , что и его прототип. У дочернего элемента есть действующая ссылка с на прототипа, и любое свойство прототипа, которое не существует в дочернем элементе, является односторонней ссылкой на свойство с тем же именем в прототипе.
Рассмотрим следующее:
let parent = {foo: 'foo'}
пусть ребенок = {}
Object.setPrototypeOf (дочерний, родительский)
консоль.журнал (child.foo) // 'фу'
child.foo = 'бар'
console.log (child.foo) // 'бар'
console.log (parent.foo) // 'foo'
удалить child.foo
console.log (child.foo) // 'foo'
parent.foo = 'баз'
console.log (child.foo) // 'баз'
Примечание. В реальной жизни вы почти никогда не напишете подобный код - это ужасная практика, - но она лаконично демонстрирует принцип.
В предыдущем примере, в то время как child.foo
было undefined
, он ссылался на parent.foo
.Как только мы определили foo
для дочернего элемента
, child.foo
имело значение 'bar'
, но parent.foo
сохранил свое исходное значение. Как только мы удаляем child.foo
, он снова обращается к parent.foo
, что означает, что когда мы изменяем значение родительского объекта, child.foo
ссылается на новое значение.
Давайте посмотрим, что только что произошло (для наглядности представим, что это Strings
, а не строковые литералы, разница здесь не имеет значения):
То, как это работает под капотом, и особенно особенности нового
и этого
, - это тема для другого дня, но у Mozilla есть подробная статья о цепочке наследования прототипов JavaScript, если вы хотите узнать больше.
Ключевой вывод состоит в том, что прототипы не определяют тип
; они сами являются экземплярами
, и они могут изменяться во время выполнения, со всеми вытекающими и вытекающими отсюда последствиями.
Все еще со мной? Вернемся к анализу классов JavaScript.
Популярный тест на JavaScript № 3: Как реализовать конфиденциальность в классах?
Наши свойства прототипа и класса, указанные выше, не столько «инкапсулированы», сколько «ненадежно свисают из окна». Мы должны это исправить, но как?
Здесь нет примеров кода.Ответ в том, что вы не можете.
JavaScript не имеет концепции конфиденциальности, но имеет закрытие:
function SecretiveProto () {
const secret = "Класс - ложь!"
this.spillTheBeans = function () {
console.log (секрет)
}
}
const blabbermouth = новый SecretiveProto ()
пытаться {
console.log (blabbermouth.secret)
}
catch (e) {
// TypeError: SecretiveClass.secret не определен
}
blabbermouth.spillTheBeans () // "Класс - ложь!"
Вы понимаете, что только что произошло? Если нет, то вы не понимаете замыканий.Это нормально - они не такие устрашающие, как их выставляют, они очень полезны, и вам следует потратить некоторое время, чтобы узнать о них.
Популярный тест на JavaScript № 4: Что можно сравнить с вышеупомянутым, используя ключевое слово class
?
Извините, это еще один вопрос с подвохом. Вы можете сделать то же самое, но это выглядит так:
class SecretiveClass {
constructor () {
const secret = "Я - ложь!"
this.spillTheBeans = function () {
консоль.журнал (секрет)
}
}
Свободные губы () {
console.log (секрет)
}
}
const liar = новый SecretiveClass ()
пытаться {
console.log (liar.secret)
}
catch (e) {
console.log (e) // Ошибка типа: SecretiveClass.secret не определен
}
liar.spillTheBeans () // "Я - ложь!"
Сообщите мне, если это выглядит проще или понятнее, чем в SecretiveProto
. На мой взгляд, это несколько хуже - он нарушает идиоматическое использование деклараций class
в JavaScript и работает не так, как вы ожидаете от, скажем, Java.Это будет ясно из следующего:
Популярный тест на JavaScript № 5: Что делает SecretiveClass :: FreeLips ()
?
Давайте узнаем:
try {
liar.looseLips ()
}
catch (e) {
// ReferenceError: секрет не определен
}
Ну… это было неловко.
Популярный тест по JavaScript № 6: Что предпочитают опытные разработчики JavaScript - прототипы или классы?
Как вы уже догадались, это еще один вопрос с подвохом - опытные разработчики JavaScript стараются избегать обоих, когда могут.Вот хороший способ сделать это с помощью идиоматического JavaScript:
function secretFactory () {
const secret = "Предпочитайте композицию наследованию,` new` считается вредным, и конец близок! "
const spillTheBeans = () => console.log (секрет)
вернуть {
spillTheBeans
}
}
const leaker = secretFactory ()
leaker.spillTheBeans ()
Речь идет не только о том, чтобы избежать уродливого наследования или принудительной инкапсуляции. Подумайте, что еще вы могли бы сделать с secretFactory
и leaker
, что было бы нелегко сделать с прототипом или классом.
Во-первых, вы можете деструктурировать его, потому что вам не нужно беспокоиться о контексте , это
:
const {spillTheBeans} = secretFactory ()
spillTheBeans () // Предпочтение композиции перед наследованием, (...)
Это довольно мило. Помимо исключения новых
и этого дурачества
, это позволяет нам использовать наши объекты взаимозаменяемо с модулями CommonJS и ES6. Это также немного упрощает композицию:
function spyFactory (infiltrationTarget) {
вернуть {
exfiltrate: infiltrationTarget.spillTheBeans
}
}
const blackHat = spyFactory (утечка)
blackHat.exfiltrate () // Предпочитайте композицию наследованию, (...)
console.log (blackHat.infiltrationTarget) // undefined (похоже, нам это сошло с рук)
Клиентам blackHat
не нужно беспокоиться о том, откуда взялся exfiltrate
, а spyFactory
не нужно возиться с Function :: bind, манипулируя контекстом
или глубоко вложенными свойствами. Имейте в виду, что нам не нужно сильно беспокоиться о и
в простом синхронном процедурном коде, но это вызывает всевозможные проблемы в асинхронном коде, которых лучше избегать.
Немного подумав, spyFactory
можно было бы превратить в очень сложное средство шпионажа, способное справиться со всеми видами целей проникновения - или, другими словами, в фасад.
Конечно, это можно сделать и с классом, или, скорее, с набором классов, все из которых наследуются от абстрактного класса
или интерфейса
… за исключением того, что в JavaScript нет никакой концепции абстрактов или интерфейсов.
Давайте вернемся к примеру приветствия, чтобы увидеть, как мы реализуем его с помощью фабрики:
function greeterFactory (приветствие = "Hello", name = "World") {
вернуть {
greet: () => `$ {приветствие}, $ {name}!`
}
}
консоль.log (greeterFactory ("Привет", "народ"). greet ()) // Привет, ребята!
Возможно, вы заметили, что эти фабрики по мере продвижения становятся все более краткими, но не волнуйтесь - они делают то же самое. Тренировочные колеса снимаются, ребята!
Это уже меньше шаблонов, чем прототип или классовая версия того же кода. Во-вторых, он более эффективно обеспечивает инкапсуляцию своих свойств. Кроме того, в некоторых случаях он имеет меньший объем памяти и производительность (на первый взгляд может показаться, что это не так, но JIT-компилятор незаметно работает за кулисами, сокращая дублирование и выявляя типы).
Так безопаснее, часто быстрее и проще писать такой код. Зачем снова нужны занятия? Ну конечно, многоразовость. Что произойдет, если нам понадобятся недовольные и восторженные варианты приветствия? Что ж, если мы используем класс ClassicalGreeting
, мы, вероятно, сразу перейдем к придумыванию иерархии классов. Мы знаем, что нам нужно параметризовать пунктуацию, поэтому проведем небольшой рефакторинг и добавим несколько дочерних элементов:
// Класс приветствия
class ClassicalGreeting {
конструктор (приветствие = "Hello", name = "World", punctuation = "!") {
этот.приветствие = приветствие
this.name = имя
this.punctuation = пунктуация
}
greet () {
return `$ {this.greeting}, $ {this.name} $ {this.punctuation}`
}
}
// Несчастливое приветствие
class UnhappyGreeting extends ClassicalGreeting {
конструктор (приветствие, имя) {
super (приветствие, имя, ":(")
}
}
const classyUnhappyGreeting = new UnhappyGreeting («Привет», «все»)
console.log (classyUnhappyGreeting.greet ()) // Всем привет :(
// Восторженное приветствие
class EnthusiasticGreeting extends ClassicalGreeting {
конструктор (приветствие, имя) {
супер (приветствие, имя, "!!")
}
greet () {
вернуть супер.приветствовать (). toUpperCase ()
}
}
const welcomeWithEnthusiasm = новый EnthusiasticGreeting ()
console.log (приветствиеWithEnthusiasm.greet ()) // ПРИВЕТ, МИР !!
Это хороший подход, пока кто-то не придет и не попросит функцию, которая не вписывается в иерархию, и все это не перестанет иметь смысл. Включите эту мысль, пока мы пытаемся написать ту же функциональность с фабриками:
const greeterFactory = (приветствие = "Hello", name = "World", punctuation = "!") => ({
greet: () => `$ {приветствие}, $ {имя} $ {пунктуация}`
})
// Делает встречающего недовольным
const unhappy = (приветствующий) => (приветствие, имя) => приветствующий (приветствие, имя, ":(")
консоль.log (unhappy (greeterFactory) ("Привет", "все"). greet ()) // Привет всем :(
// Делает встречу восторженной
const энтузиазм = (приветствующий) => (приветствие, имя) => ({
greet: () => greeter (приветствие, имя, "!!"). greet (). toUpperCase ()
})
console.log (энтузиазм (greeterFactory) (). greet ()) // ПРИВЕТ, МИР !!
Не очевидно, что этот код лучше, даже если он немного короче. Фактически, вы можете возразить, что это труднее читать, и, возможно, это тупой подход.Разве у нас не может быть просто unhappyGreeterFactory
и энтузиастовGreeterFactory
?
Затем приходит ваш клиент и говорит: «Мне нужен новый встречающий, который недоволен и хочет, чтобы об этом узнала вся комната!»
console.log (энтузиазм (unhappy (greeterFactory)) (). Greet ()) // ПРИВЕТ, МИР :(
Если бы нам понадобилось несколько раз воспользоваться этим недовольным энтузиазмом приветствием, мы могли бы облегчить себе задачу:
const агрессивныйGreeterFactory = энтузиазм (недовольный (greeterFactory))
консоль.log (агрессивныйGreeterFactory ("Ты опоздал", "Джим"). greet ())
Есть подходы к этому стилю композиции, которые работают с прототипами или классами. Например, вы можете переосмыслить UnhappyGreeting
и EnthusiasticGreeting
как декораторы. Это все равно потребует большего количества шаблонов, чем использованный выше функциональный подход, но это цена, которую вы платите за безопасность и инкапсуляцию реальных классов .
Дело в том, что в JavaScript вы не получаете автоматическую безопасность.Фреймворки JavaScript, которые делают упор на использование класса class
, делают много «волшебства», чтобы скрыть подобные проблемы и заставить классы вести себя самостоятельно. Смею вас взглянуть как-нибудь на исходный код Polymer ElementMixin
. Это уровни аркано-волшебника арканы JavaScript, и я имею в виду это без иронии или сарказма.
Конечно, мы можем исправить некоторые из проблем, описанных выше, с помощью Object.freeze
или Object.defineProperties
для большего или меньшего эффекта.Но зачем имитировать форму без функции, игнорируя инструменты JavaScript , которые изначально предоставляет нам , которых мы не можем найти в таких языках, как Java? Вы бы использовали молоток с надписью «отвертка» для заворачивания шурупа, если бы рядом с вашим ящиком с инструментами была настоящая отвертка?
В поисках хороших запчастей
Разработчики JavaScript часто подчеркивают достоинства языка как в разговорной речи, так и в связи с одноименной книгой. Мы стараемся избегать ловушек, создаваемых его более сомнительным выбором языкового дизайна, и придерживаемся тех частей, которые позволяют нам писать чистый, читаемый, с минимизацией ошибок и повторно используемый код.
Существуют разумные аргументы в пользу того, какие части JavaScript подходят, но я надеюсь, что убедил вас, что class
не входит в их число. В противном случае, надеюсь, вы понимаете, что наследование в JavaScript может сбивать с толку и что класс
не исправляет его и не избавляет вас от необходимости разбираться в прототипах. Дополнительная благодарность, если вы уловили намеки на то, что объектно-ориентированные шаблоны проектирования отлично работают без классов или наследования ES6.
Я не говорю вам полностью избегать class
.Иногда вам нужно наследование, и класс
предоставляет более чистый синтаксис для этого. В частности, класс X расширяет Y
намного лучше, чем старый прототип. Кроме того, многие популярные интерфейсные фреймворки поощряют его использование, и вам, вероятно, следует избегать написания странного нестандартного кода в одиночку. Мне просто не нравится, к чему все идет.
В моих кошмарах целое поколение библиотек JavaScript написано с использованием класса
с ожиданием того, что он будет вести себя так же, как и другие популярные языки.Обнаруживаются совершенно новые классы ошибок (каламбур). Возрождаются старые, которые легко могли бы остаться на кладбище искаженного JavaScript, если бы мы по неосторожности не попали в ловушку класса
.