Приложения на JavaScript во время своей работы создают и удаляют множество объектов. Иногда эти объекты совсем разные, а иногда они относятся к одному понятию, но отличаются данными. Когда речь идет про понятия предметной области (или, как говорят, сущности), то важно иметь абстракцию, которая скроет от нас структуру этого объекта.
Возьмем понятие "компания" и построим абстракцию вокруг него без использования инкапсуляции:
// Реальное устройство будет значительно сложнее
// файл: company.js
// Конструктор (в общем смысле этого слова)
const make = (name, website) => {
return { name, website }
}
// Селекторы
const getName = company => company.name
const getWebsite = company => company.website
Теперь использование:
import { make, getName } from './company.js'
const company = make('Hexlet', 'https://hexlet.io')
console.log(getName(company)) // Hexlet
Такая абстракция упрощает работу с компаниями (особенно при изменениях структуры), прячет детали реализации и делает код более "человечным". Попробуем сделать то же самое, используя инкапсуляцию:
// Реальное устройство будет значительно сложнее
// файл: company.js
const make = (name, website) => {
return {
name,
website,
getName() {
return this.name
},
getWebsite() {
return this.website
},
}
}
И использование:
import { make } from './company.js'
const company = make('Hexlet', 'https://hexlet.io')
console.log(company.getName()) // Hexlet
Здесь мы видим несколько удобных моментов по сравнению с вариантом на функциях:
- Нам больше не надо импортировать
getName
, он уже содержится внутри компании. - Можно пользоваться автодополнением кода (среда разработки сама подскажет все свойства и методы, существующие в
company
).
Но вместе с плюсами пришли и минусы. Посмотрите еще раз внимательно на код конструктора. Каждый его вызов возвращает новый объект и это ожидаемое поведение, но чего мы точно не хотим, так это создания методов на каждый вызов конструктора (а они будут создаваться при каждом создании объекта). Методы, в отличие от обычных данных, не меняются. Нет никакого смысла создавать их на каждый вызов заново, расходуя память и процессорное время.
Перепишем наш пример, избежав постоянного создания методов:
// Не забываем что нам нужны обычные, а не стрелочные функции!
function getName() {
return this.name
}
function getWebsite() {
return this.website
}
// С точки зрения использования ничего не поменялось, но зато перестали копироваться функции.
const make = (name, website) => {
return {
name,
website,
getName,
getWebsite,
}
}
Оператор new
Все описанные выше способы создания объектов имеют право на существование и используются в реальной жизни, но в JavaScript есть встроенная поддержка генерации объектов. Перепишем наш пример с помощью функции-конструктора.
// Такую функцию принято называть конструктором (хотя технически это обычная функция с контекстом)
// Конструкторы пишутся с заглавной буквы
function Company(name, website) {
this.name = name
this.website = website
// Методы по-прежнему определены снаружи как обычные функции
this.getName = getName
this.getWebsite = getWebsite
}
Теперь использование:
const company = new Company('Hexlet', 'https://hexlet.io')
console.log(company.getName()) // Hexlet
Самое интересное в этом примере – оператор new
(как и многое в js, он работает не так, как new
в других языках). Фактически он создает объект, устанавливает его как контекст во время вызова конструктора (в данном случае Company
) и возвращает созданный объект. Именно поэтому сам конструктор ничего не возвращает (хотя может, но это другой разговор), а внутри константы company
оказывается нужный нам объект.
// Упрощенная иллюстрация работы new внутри интерпретатора при таком вызове:
// new Company();
const obj = {}
Company.bind(obj)(name, website) // этот вызов просто наполнил this (равный obj) нужными данными
return obj
Визуально этот способ выглядит не лучше, чем предыдущее ручное создание, но он задействует еще один важный механизм в JavaScript – прототипы (Подробнее о них в следующем уроке).
Все типы данных в JavaScript, которые могут быть представлены объектами (или являются объектами внутри себя, например, функции), имеют встроенные конструкторы. Иногда они заменяют специальный синтаксис создания данных (как в случае с массивами), а иногда это единственный способ создать данные этого типа (как в случае с датами):
// Специальный синтаксис создания массивов
// Массивы это объекты, вспомните свойство length
const numbers = [10, 3, -3, 0] // литерал
// Объектный способ создания через конструктор
// Результат ниже эквивалентен тому что происходит выше
const numbers = [10, 3, -3, 0]
// У дат нет литералов, они создаются как объекты
const date = new Date('December 17, 1995 03:24:00')
// У дат очень много методов
date.getMonth() // 11, в JS месяцы нумеруются с нуля
// Так можно создавать даже функции
// Последний аргумент это тело, все предыдущие – аргументы
const sum = new Function('a', 'b', 'return a + b')
sum(2, 6) // 8
Но не все функции могут быть конструкторами. Отсутствие своего контекста делает невозможным использование оператора new
вместе со стрелочными функциями:
const f = () => {}
// TypeError: function is not a constructor
const obj = new f()
Дополнительные материалы
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.