что такое ооп javascript
Объектно-ориентированный JavaScript для начинающих
Разобравшись с основами, сосредоточимся на объектно-ориентированном JavaScript (OOJS) — данная статья даёт базовое представление о теории объектно-ориентированного программирования (ООП), далее рассмотрено как JavaScript эмулирует классы объектов с помощью функции-конструктора и как создаются экземпляры объектов.
Необходимые знания: | |
---|---|
Цель: | Понять основную теорию объектно-ориентированного программирования, как это относится к JavaScript («все является объектом») и как создавать конструкторы и экземпляры объектов. |
Объектно-ориентированное программирование: основы
Начнём с упрощённого высокоуровневого представления о том, что такое объектно-ориентированное программирование (ООП). Мы говорим упрощённого, потому что ООП может быстро стать очень сложным, и если сейчас дать полный курс, вероятно, можно запутать больше, чем помочь. Основная идея ООП заключается в том, что мы используем объекты для отображения моделей из реального мира в наших программах и/или упрощения доступа к функциям, которые в противном случае было бы трудно или невозможно использовать.
Объекты могут содержать данные и код, представляющие информацию о том, что вы пытаетесь смоделировать, а также о том, какие у этих объектов должны быть функциональные возможности или поведение. Данные объекта (а часто так же и функции) могут быть точно сохранены (официальный термин «инкапсулированы») внутри пакета объекта, упрощая структуру и доступ к ним. Пакету объекта может быть присвоено определённое имя, на которое можно сослаться и которое иногда называют пространством имён. Объекты также широко используются в качестве хранилищ данных, которые могут быть легко отправлены по сети.
Определение шаблона объекта
Рассмотрим простую программу, которая отображает информацию об учениках и учителях в школе. Здесь мы рассмотрим теорию ООП в целом, а не в контексте какого-либо конкретного языка программирования.
В некоторых языках ООП, это общее определение типа объекта называется class (JavaScript использует другой механизм и терминологию, как вы увидите ниже) — это на самом деле не объект, а шаблон, который определяет, какие характеристики должен иметь объект.
Создание реальных объектов
Из нашего класса мы можем создать экземпляры объектов — объекты, содержащие данные и функциональные возможности, определённые в классе. Из нашего класса Person мы теперь можем создавать модели реальных людей:
Когда экземпляр объекта создаётся из класса, для его создания выполняется функция-конструктор класса. Этот процесс создания экземпляра объекта из класса называется создание экземпляра (instantiation) — из класса создаётся экземпляр объекта.
Специализированные классы
В нашем случае нам не нужны все люди — нам требуются учителя и ученики, которые являются более конкретными типами людей. В ООП мы можем создавать новые классы на основе других классов — эти новые дочерние классы могут быть созданы для наследования данных и характеристик родительского класса, так чтобы можно было использовать функциональные возможности, общие для всех типов объекта, вместо того чтобы дублировать их. Когда функциональность различается между классами, можно по мере необходимости определять специализированные функции непосредственно на них.
Это действительно полезно — преподаватели и студенты имеют много общих характеристик, таких как имя, пол и возраст, и удобно определить их только один раз. Вы можете также задать одну и ту же характеристику отдельно в разных классах, поскольку каждое определение этой характеристики будет находиться в отдельном пространстве имён. Например, приветствие студента может быть в форме «Yo, I’m [firstName]» (например Yo, I’m Sam), в то время как учитель может использовать что-то более формальное, такое как «Hello, my name is [Prefix] [lastName], and I teach [Subject].» (например Hello, My name is Mr Griffiths, and I teach Chemistry).
Теперь вы можете создавать экземпляры объекта из дочерних классов. Например:
Далее мы рассмотрим, как ООП теорию можно применить на практике в JavaScript.
Конструкторы и экземпляры объектов
JavaScript использует специальные функции, называемые функциями конструктора (constructor functions) для определения объектов и их свойств. Они полезны, потому что вы часто будете сталкиваться с ситуациями, в которых не известно, сколько объектов вы будете создавать; конструкторы позволяют создать столько объектов, сколько нужно эффективным способом, прикреплением данных и функций для объектов по мере необходимости.
Рассмотрим создание классов через конструкторы и создание экземпляров объектов из них в JavaScript. Прежде всего, мы хотели бы, чтобы вы создали новую локальную копию файла oojs.html, который мы видели в нашей первой статье «Объекты».
Простой пример
Итак, как мы вызываем конструктор для создания некоторых объектов?
Давайте снова посмотрим на вызовы конструктора:
После создания новых объектов переменные person1 и person2 содержат следующие объекты:
Обратите внимание, что когда мы вызываем нашу функцию-конструктор, мы определяем greeting() каждый раз, что не является идеальным. Чтобы этого избежать, вместо этого мы можем определить функции на прототипе, о которых мы поговорим позже.
Создавая наш готовый конструктор
Дальнейшие упражнения
Для начала, попробуйте добавить ещё пару собственных строк создания объекта и попробуйте получить и установить элементы полученных экземпляров объектов.
Примечание: Если у вас возникли трудности с решением задачи, мы предоставили ответ в нашем репозитории GitHub (см. это в действии) — но сначала попробуйте написать сами!
Другие способы создания экземпляров объектов
Конструктор Object ()
Прежде всего, вы можете использовать конструктор Object() для создания нового объекта. Да, даже общие объекты имеют конструктор, который генерирует пустой объект.
Использование метода create()
Одно ограничение метода create() заключается в том, что IE8 не поддерживает его. Поэтому конструкторы могут быть более эффективными, если вы хотите поддерживать старые браузеры.
Подробнее мы рассмотрим особенности метода create() немного позже.
Сводка
В этой статье представлен упрощённый взгляд на объектно-ориентированную теорию — это ещё не вся история, но она даёт представление о том, с чем мы имеем дело. Кроме того, мы начали рассматривать различные способы создания экземпляров объектов.
В следующей статье мы рассмотрим прототипы объектов JavaScript.
Краткое руководство по ООП в JS
Mar 14, 2019 · 4 min read
В данной статье я постараюсь объяснить новые возможности ES6 JavaScript с точки зрения объектно-ориентированной парадигмы.
Что такое парадигма программирования?
Парадигма — это пример или модель чего-то. То есть некий шаблон, которого необходимо придерживаться. В данном случае — для создания компьютерных программ.
Что такое объектно-ориентированная парадигма?
Очевидно, что это парадигма программирования. Но помимо объектно-ориентированной парадигмы бывают и другие разновидности: функциональное программирование, реактивное программирование и т.д.
Каковы характеристики этой парадигмы?
В данной парадигме мы программируем ближе к реальности, то есть в рамках классов, объектов, свойств и т.д. Для ОО-парадигмы характерны специфические термины: абстракция, инкапсуляция, модульность, безопасность, полиморфизм, наследование и др.
Главная проблема JavaScript заключается в том, что это не самый объектно-ориентированный язык. Почему? Потому что в JavaScript объектом считается все. Это можно исправить с помощью известного прототипа.
В ES5 такой пример делался бы через шаблон «Фабрика»:
В E S 6 то же самое можно сделать намного проще. Но помните, что все это — синтаксический сахар.
Тот же пример в синтаксисе ES6.
В данном примере через ключевое слово extends мы как бы говорим: «Ок, я хочу наследовать свойства класса Person». Но на задворках происходит все то же самое, что и в примере с прототипами в ES5.
Статичные методы:
Private методы
В отличие от Java и С#, в JavaScript нет ключевого слова private. Однако в JavaScript есть определенная договоренность по использованию «приватных» значений — добавление нижнего подчеркивания перед словом. Давайте посмотрим на примере:
Однако в ES6 нам доступен вызов объекта WeakMap, который позволяет создавать приватные свойства. Давайте посмотрим:
Геттеры и сеттеры
Если у нас есть приватные методы, то можно создавать и публичные методы, возвращающие приватные значения. Для возвращения значения используется get, а определение нового значения делается через set.
Полиморфизм
Это способность объекта в процессе своего выполнения ссылаться на экземпляры собственного класса или любого класса-потомка. Классы-потомки могут переопределять метод.
Другие концепции:
class: создание нового класса/модели.
method: функция внутри класса.
constructor: метод, который инициирует объект при создании экземпляра класса.
extends: используется для определения наследования.
super: метод, который устанавливает свойства наследования за счет вызова родительского конструктора. Метод super должен стоять первой строкой в методе constructor.
get: возвращает значение.
set: переопределяет новое существующее значение.
new: создание объекта через метод конструктора класса.
Объектно-ориентированное программирование в ванильном JavaScript
Этот перевод — для новичков, делающих первые шаги в JavaScript, или даже в программировании вообще.
JavaScript — мощный объектно-ориентированный (ООП) язык. Но, в отличие от многих других языков, он использует ООП-модель на основе прототипов, что делает его синтаксис непривычным для многих разработчиков. Кроме того, JavaScript работает с функциями как с объектами первого класса, что может путать программистов, не знакомых с этими концепциями. Можно обойти их, применяя производный язык вроде TypeScript, имеющий знакомый синтаксис и предлагающий дополнительные возможности. Но такие языки всё-равно компилируются в чистый JavaScript, и простое знание об этом не поможет вам понять, как они работают на самом деле, а также когда целесообразно их применять.
О чём мы поговорим в этой статье:
Пространство имён
В сети появляется всё больше сторонних библиотек, фреймворков и зависимостей, поэтому определение пространства имён является необходимостью в JavaScript-разработке, если мы хотим избежать коллизий между объектами и переменными в глобальном пространстве имён.
К сожалению, JS не имеет встроенной поддержки определения пространства имён, но мы можем использовать объекты для достижения того же результата. Есть много разных паттернов для реализации, но мы рассмотрим только самый распространённый — вложенные пространства имён.
Этот паттерн использует объектный литерал, чтобы собрать функциональность в пакет и дать ему уникальное, специфичное для приложения имя. Можем начать с создания глобального объекта и присвоения его к переменной:
По той же методике можно создавать подпространства имён:
Сделав контейнер, мы можем использовать его для определения методов и свойств, а затем применять их в нашем глобальном пространстве имён без риска возникновения коллизий.
Подробнее о паттернах определения пространств имён в JavaScript можно почитать здесь: Essential JavaScript Namespacing Patterns.
Объекты
Если вы уже писали код на JavaScript, то в той или иной мере использовали объекты. JavaScript имеет три различных типа объектов:
Нативные объекты (Native Objects)
Нативные объекты — часть спецификации языка. Они доступны нам вне зависимости от того, на каком клиенте исполняется наш код. Примеры: Array, Date и Math. Полный список нативных объектов.
Хост-объекты (Host Objects)
В отличие от нативных, хост-объекты становятся нам доступны благодаря клиентам, на которых исполняется наш код. На разных клиентах мы в большинстве случаев можем взаимодействовать с разными хост-объектами. Например, если пишем код для браузера, то он предоставляет нам window, document, location и history.
Пользовательские объекты (User Objects)
Пользовательские объекты, иногда называемые предоставленными (contributed objects), — наши собственные объекты, определяемые в ходе run time. Есть два способа объявления своих объектов в JS, и мы рассмотрим их далее.
Объектные литералы (Object Literals)
Мы уже коснулись объектных литералов в главе про определение пространства имён. Теперь поясним: объектный литерал — это разделённый запятыми список пар имя-значение, помещённый в фигурные скобки. Эти литералы могут содержать свойства и методы, и как и любые другие объекты в JS могут передаваться функциям и возвращаться ими. Пример объектного литерала:
Объектные литералы являются синглтонами. Чаще всего их используют для инкапсулирования кода и заключения его в аккуратный пакет во избежание коллизий с переменными и объектами в глобальной области видимости (пространстве имён), а также для передачи конфигураций в плагины и объекты.
Объектные литералы полезны, но не могут быть инстанцированы и от них нельзя наследовать. Если вам нужны эти возможности, то придётся обратиться к другому методу создания объектов в JS.
Функции-конструкторы
В JavaScript функции считаются объектами первого класса, то есть они поддерживают те же операции, что доступны для других сущностей. В реалиях языка это означает, что функции могут быть сконструированы в ходе run time, переданы в качестве аргументов, возвращены из других функций и присвоены переменным. Более того, они могут иметь собственные свойства и методы. Это позволяет использовать функции как объекты, которые могут быть инстанцированы и от которых можно наследовать.
Пример использования определения объекта с помощью функции-конструктора:
Создание функции-конструктора аналогично созданию регулярного выражения за одним исключением: мы используем ключевое слово this для объявления свойств и методов.
Инстанцирование функций-конструкторов с помощью ключевого слова new аналогично инстанцированию объекта в традиционном языке программирования, основанном на классах. Однако здесь есть одна неочевидная, на первый взгляд, проблема.
При создании в JS новых объектов с помощью ключевого слова new мы раз за разом выполняем функциональный блок (function block), что заставляет наш скрипт КАЖДЫЙ РАЗ объявлять анонимные функции для каждого метода. В результате программа потребляет больше памяти, чем следует, что может серьёзно повлиять на производительность, в зависимости от масштабов программы.
К счастью, есть способ прикрепления методов к функциям-конструкторам без загрязнения глобальной области видимости.
Методы и прототипы
JavaScript — прототипный (prototypal) язык, то есть мы можем использовать прототпы в качестве шаблонов объектов. Это поможет нам избежать ловушки с анонимными функциями по мере масштабирования наших приложений. Prototype — специальное свойство в JavaScript, позволяющее добавлять к объектам новые методы.
Вот вариант нашего примера, переписанный с использованием прототипов:
В этом примере sayHey() будет совместно использоваться всеми экземплярами объекта User.
Наследование
Также прототипы используются для наследования в рамках цепочки прототипов. В JS каждый объект имеет прототип, а раз прототип — всего лишь ещё один объект, то и у него тоже есть прототип, и так далее… пока не дойдём до прототипа со значением null — это последнее звено цепочки.
Когда мы обращаемся к методу или свойству, JS проверяет, задан ли он в определении объекта, и если нет, то проверяет прототип и ищет определение там. Если и в прототипе не находит, то идёт по цепочке прототипов, пока не найдёт или пока не достигнет конца цепочки.
Вот как это работает:
У вас может уйти какое-то время на привыкание к прототипному наследованию, но важно освоить эту концепцию, если вы хотите достигнуть высот в ванильном JavaScript. Хотя её часто называют одним из слабых мест языка, прототипная модель наследования фактически мощнее классической модели. Например, не составит труда построить классическую модель поверх прототипной.
В ECMAScript 6 появился новый набор ключевых слов, реализующих классы. Хотя эти конструкты выглядят так же, как в основанных на классах языках, это не одно и то же. JavaScript по прежнему основан на прототипах.
JavaScript развивался долгое время, в течение которого в него внедрялись разные практики, которых по современным меркам нужно избегать. С появлением ES2015 ситуация начала медленно меняться, но всё же многие разработчики придерживаются всё ещё придерживаются старых методов, которые ставят под угрозу релевантность их кода. Понимание и применение в JavaScript ООП-концепций критически важно для написания устойчивого кода, и я надеюсь, что это краткое введение поможет вам в этом.
ООП в JavaScript
Вкратце об объектно-ориентированном программировании.
В ООП объект представляет собой блок, содержащий информацию (состояние / атрибуты) и операции (методы).
Ключевое слово this
Стрелочные функции связывают this с лексической областью действия.
Object имеет свойство prototype, которое является базовым объектом для всех вещей в JavaScript, включая функции JavaScript.
Object.create() vs. Classes
Private vs. Public vs. Protected
Во многих объектно-ориентированных языках программирования, которые имеют классы, идея private и public полей действительно важна. В JavaScript этого нет. Ранее, если нужно было сделать поле private, к которому нельзя обращаться из класса, мы добавляли подчеркивание _ перед именем, чтобы другие программисты знали, что это private метод. Но, к сожалению, подчеркивание на самом деле ничего не делает.
В JavaScript есть предложение ECMAScript, которое предназначено для объявлений полей класса.
Это модификаторы доступа, которые помогают нам реализовать Encapsulation (или скрытие информации). Они сообщают компилятору, какие другие классы должны иметь доступ к определенному полю или методу.
Так как в Javascript таких полей пока нет, для их реализации мы можем использовать TypeScript.
Инкапсуляция включает в себя идею о том, что данные объекта не должны быть напрямую доступны. Нужно вызывать методы вместо прямого доступа к данным. Инкапсуляция позволяет нам скрывать/показывать свойства функций.
ООП заключаем код в блоки, которые связаны друг с другом, чтобы эти блоки могли просто взаимодействовать друг с другом, используя методы и свойства, которые мы делаем доступными. Данный принцып делает код проще в обслуживании и более пригодным для повторного использования.
Инкапсуляция с использованием замыкания
Многие программные процессы повторяются снова и снова. Поэтому, на этапе декомпозиции проблемы, мы удалим дублирование, записывая какой-либо компонент (функцию, модуль, класс и т. Д.), присваивая ему имя (идентификатор) и повторно используя его столько раз, сколько нам нужно.
Полиморфизмом является одним из принципов объектно-ориентированного программирования (ООП). Это помогает проектировать объекты таким образом, чтобы они могли совместно использовать или переопределять любое поведение с конкретными предоставленными объектами.
Само слово означает много форм. Существует много толкований того, что именно оно означает, но идея заключается в способности вызывать один и тот же метод для разных объектов, и при этом каждый объект реагирует по-своему.
Чтобы это произошло полиморфизм использует наследование.
Tight Coupling (сильная связанность) относится к волновым эффектам, которые могут произойти с подклассами (дочерние классы), когда вносится изменение в суперкласс (родительский класс).
Понимание ООП в JavaScript [Часть 1]
— Прототипное наследование — это прекрасно
JavaScript — это объектно-ориентированный (ОО) язык, уходящий корнями в язык Self, несмотря на то, что внешне он выглядит как Java. Это обстоятельство делает язык действительно мощным благодаря некоторым приятным особенностям.
Одна из таких особенностей — это реализация прототипного наследования. Этот простой концепт является гибким и мощным. Он позволяет сделать наследование и поведение сущностями первого класса, также как и функции являются объектами первого класса в функциональных языках (включая JavaScript).
К счастью, в ECMAScript 5 появилось множество вещей, которые позволили поставить язык на правильный путь (некоторые из них раскрыты в этой статье). Также будет рассказано о недостатках дизайна JavaScript и будет произведено небольшое сравнение с классической моделью прототипного ОО (включая его достоинства и недостатки).
Статья предполагает, что вы уже знакомы с основами JavaScript, имеете представление о функциях (включая концепты замыкания и функций первого класса), примитивных значениях, операторах и т.д.
1. Объекты
Объект в JavaScript — это просто коллекция пар ключ-значение (и иногда немного внутренней магии).
Однако, в JavaScript нет концепции класса. К примеру, объект с свойствами не является экземпляром какого-либо класса или класса Object. И Object, и Linda являются экземплярами самих себя. Они определяются непосредственно собственным поведением. Тут нет слоя мета-данных (т.е. классов), которые говорили бы этим объектам как нужно себя вести.
Вы можете спросить: «Да как так?», особенно если вы пришли из мира классических объектно-ориентированных языков (таких как Java или C#). «Но если каждый объект обладает собственным поведением (вместо того чтобы наследовать его от общего класса), то если у меня 100 объектов, то им соответствует 100 разных методов? Разве это не опасно? А как мне узнать, что, например, объект действительно является Array-ем?»
Чтобы ответить на все эти вопросы необходимо забыть о классическом ОО-подходе и начать всё с нуля. Поверьте, оно того стоит.
Модель прототипного ОО приносит несколько новых динамичных и экспрессивынх путей решения старых проблем. В ней также представлены мощные модели расширения и повторного использования кода (а это и интересует людей, которые говорят об объектно-ориентированном программировании). Однако, эта модель даёт меньше гарантий. Например, нельзя полагаться, что объект x всегда будет иметь один и тот же набор свойств.
1.1. А что такое объекты?
Ранее упоминалось, что объекты — это просто пары уникальных ключей с соответствующими значениями — такие пары называются свойства. К примеру, вы хотите описать несколько аспектов своего старого друга (назовём его Мишей, он же Mikhail), таких как возраст, имя и пол:
Объект в JavaScript создаётся с помощью функции Object.create. Эта функция из родителя и опционального набора свойств создаёт новую сущность. Пока что мы не будем беспокоиться о параметрах.
Пустой объект — это объект без родителя, без свойств. Посмотрим на синтакс создания такого объекта в JavaScript:
1.2. Создание свойств
Так, значит у нас уже есть объект, но у него пока нет свойств — мы должны исправить эту ситуацию для описания нашего объекта Mikhail.
Свойства в JavaScript являются динамическими. Это означает, что мы их можем создавать или удалять в любое время. Свойства уникальны в том смысле, что ключ свойства внутри объекта соответствует ровно одному значению.
Создадим новые свойства через функцию Object.defineProperty, которая в качестве аргументов использует объект, имя свойства для создания и дескриптор, описывающий семантику свойства.
Функция Object.defineProperty создаёт новое свойство, если свойство с данным ключём ранее не существовало (в противном случае произойдёт обновление семантики и значения существующего свойства).
Кстати, вы также можете использовать Object.defineProperties когда необходимо добавить больше одного свойства в объект:
Очевидно, что оба вызова аналогичны, они вполне конфигурируемы, но не предназначены для конечного пользователя кода. Лучше создать уровень абстракции над ними.
1.3. Дескрипторы
Маленькие объекты, которые содержат в себе семантику, называются дескрипторами (мы их использовали при вызове Object.defineProperty). Дескрипторы бывают одного из двух типов — дескрипторы данных и дескрипторы доступа.
Оба типа дескрипторов содержат флаги, которые определяют как свойство будет рассматриваться языком. Если флаг не установлен, то его значение по умолчанию false (к сожалению это не всегда хорошее значение по умолчанию, что влечёт возрастание объёма описания дескрипторов).
1.4. Стремимся к лаконичности
К счастью, дескрипторы свойств — это не единственный путь работать со свойствами в JavaScript — их можно создавать более лаконично.
JavaScript также понимает ссылки на свойства, используя так называемую скобочную запись. Основное правило записывается следующим образом:
Тут identifier — это переменная, которая хранит объект, содержащий свойство, значение которого мы хотим установить, а expression — любое валидное JavaScript-выражение, определяющее имя свойства. Нет ограничений на то, какое имя может иметь свойство, всё позволяется.
Таким образом, мы можем переписать предыдущий пример:
На заметку: все имена свойств в конечном счёте конвертируются в строку, т.е. записи object[1], object[[1]], object[‘1’] и object[variable] (где значение variable равно 1) эквивалентны.
Существует другой способ обращения к свойству, который называется точечной записью. Он выглядит проще и лаконичнее, чем скобочная альтернатива. Однако, при этом способе имя свойство должно соответствовать правилам валидного JavaScript-идентификатора и не может быть представлено выражением (т.е. нельзя использовать переменные).
Общее правило для точечной записи:
Таким образом, предыдущий пример стал ещё более красивым:
Оба варианта синтаксиса выполняют эквивалентный процесс создания свойств, с выставлением семантических флагов в значение true.
1.5. Доступ к свойствам
Очень просто получить значение, хранящиеся в заданном свойстве — синтаксис очень похож на создание свойства с той лишь разницей, что в нём нет присваивания.
Например, если мы хотим узнать возраст Миши, то мы напишем:
Но если мы попробуем получить значение свойства, которого не существует в нашем объекте, то мы получим undefined:
1.6. Удаление свойств
Для удаления свойства из объекта в JavaSCript предусмотрен оператор delete. К примеру, если вы хотите удалить свойство gender из нашего объекта mikhail:
Оператор delete вернёт true, если свойство удалено, и falseв противном случае. Не будем углубляться в то, как работает этот оператор. Но если вам всё-таки интересно, то вы можете почитать самую прекрасную статью о том как работает delete.
1.6. Getter-ы и setter-ы
Getter-ы и setter-ы обычно используются в классических объектно-ориентированных языках для обеспечения инкапсуляции. Они не особо нужны в JavaScript, но, у нас динамический язык, и я против этой функциональности.
Но, с любой точки зрения, они позволяет обеспечить proxy для запросов на чтение и запись свойств. Например, у нас были отдельные слоты для имени и фамилии, но мы хотим иметь удобный способ читать и устанавливать их.
Для начала, создадим имя и фамилию нашего друга, описав соответствующие свойства:
Затем мы опишем общий способ получения и установки сразу двух свойств за один раз — назовём их объединение name:
Теперь, каждый раз когда мы попытаемся узнать значение свойства name нашего друга на самом деле вызовется функция get_full_name:
Мы также можем установить name объекта, обратившись к соответствующему свойству, но на самом деле вызов set_full_name выполнит всю грязную работу:
Есть сценарии, в которых действительно удобно так делать, но стоит помнить, что такой механизм работает очень медленно.
Кроме того, следует учитывать что getter-ы и setter-ы обычно используются в других языках для инкапсуляции, а в ECMAScript 5 вы всё ещё не можете так делать — все свойства объекта являются публичными.
1.8. Перечисление свойств
Ввиду того, что свойства являются динамическими, JavaScript обеспечивает функционал по проверке набора свойств объекта. Существует два способа перечисления всех свойств объекта, зависящих от того, какой вид свойств вас интересует.
Первый способ заключается в вызове функции Object.getOwnPropertyNames, которая вернёт вам Array, содержащий имена всех свойств, установленных для данного объекта — мы будет называть эти свойства собственными. Например, посмотрим, что мы знаем о Мише:
Второй способ заключается в использовании Object.keys, который вернёт список собственных свойств, которые помечены флагом enumerable :
1.9. Литералы
Простой способ создать объект заключается в использовании литерального синтаксиса JavaScript. Литеральный объект определяет новый объект, родитель которого Object.prototype (о родителях поговорим немного позже).
В любом случае, синтаксис литеральных объектов позволяет определять простые объекты и инициализировать их свойства. Перепишем пример создания объекта Mikhail:
Невалидные имена свойств могут быть заключены в кавычки. Учитывайте, что запись для getter/setter в литеральном виде определяется анонимными функциями. Если вы хотите связать ранее объявленную функцию с getter/setter, то вы должны использовать метод Object.defineProperty.
Посмотрим на общее правила литерального синтаксиса:
Литеральные объекты могут появляться внутри выражений в JavaScript. Из-за некоторой неоднозначности новички иногда путаются:
2. Методы
До сих пор объект Mikhail имел только слоты для хранения данных (ну, за исключением getter/setter для свойства name). Описание действий, которые можно делать с объектом делается в JavaScript очень просто. Просто — потому что в JavaScript нет разницы между манипулированием такими вещами, как Function, Number, Object. Всё делается одинаково (не забываем, что функции в JavaScript являются сущностями первого класса).
Опишем действие над данным объектом, просто установив функцию, как значение нашего свойства. К примеру, мы хотим, чтобы Миша мог приветствовать других людей:
После выставления значения свойства, мы можем использовать аналогичный способ для выставления конкретных данных, связанных с объектом. Таким образом, доступ к свойствам будет возвращать ссылку на функцию, хранящуюся в нём, которую мы можем вызвать:
2.1. Динамический this
Следует учитывать одну вещь при описании функции greet — эта функция должна обращаться к getter/setter свойства name, а для этого она использует магическую переменную this.
Она хранит в себе ссылку на объект, которому принадлежит исполняющаяся функция. Это не обязательно означает, что this всегда равно объекту, в котором функция хранится. Нет, JavaScript не настолько эгоистичен.
Функции являются generic-ами. Т.е. в JavaScript переменная this определяет динамическую ссылку, которая разрешается в момент исполнения функции.
Процесс динамического разрешения this обеспечивает невероятно мощный механизм для динамизации объектной ориентированности JavaScript и компенсирует отсутствие строгого соответствия заданным структурам (т.е. классам). Это означает, что можно применить функцию к любому объекту, который отвечает требованиям запуска, независимо от того, как устроен объект (как и в CLOS).
2.2. Разрешение this
Существует четыре различных способа разрешения this в функции, зависящие от того, как функция вызывается: непосредственно, как метод, явно применяется, как конструктор. Мы посмотрим первые три, а к конструкторам вернёмся позже.
Для следующих примеров вы примем:
2.2.1 Вызов как метод
Если функция вызывается, как метод объекта, то this внутри функции ссылается на сам объект. Т.е. когда мы явно указываем какой объект выполняет действие, то объект и будет значением this в нашей функции.
Это произойдёт, когда мы вызовем mikhail.greet(). Эта запись говорит JavaScript-у, что мы хотим применить действие greet к объекту mikhail.
2.2.2 Непосредственный вызов
Когда функция вызывается непосредственно, то this разрешается в глобальный объект движка (window в браузере, global в Node.js)
2.2.3. Явное применение
В заключении, функция может быть явно применена к любому объекту, несмотря на то, есть ли в объекте соответствующее свойство или нет. Эта функциональность достигается с помощью методов call или apply.
Различие между двумя методами заключается в параметрах передаваемых в функцию и времени исполнения — apply работает примерно в 55 раз медленнее, чем непосредственный вызов, а вот call обычно не особо хуже. Всё очень зависит от текущего движка, так что используйте Perf test, чтобы быть уверенными — не оптимизируйте код раньше времени.
В любом случае, call ожидает объект, как первый параметр функции, за которым следуют обычные аргументы исходной функции:
С другой стороны, apply позволяет описывать вторым параметром массив параметров исходной функции:
На заметку. Учтите, что разрешение this в null или undefined зависит от семантики используемого движка. Обычно результат бывает таким же, как и применение функции к глобальному объекту. Но если движок работает в strict mode, то this будет разрешено как и ожидается — ровно в ту вещь, к которой применяется:
2.3. Связывание методов
Отвлечёмся от динамической сущности функций в JavaScript, пойдём по пути создания функций, связывая их с определёнными объектами, так чтобы this внутри функции всегда указывал на данный объект, несмотря на то, как он вызывается — как метод объекта или непосредственно.
Функция обеспечивает функциональность, называемую bind: берётся объект и дополнительный параметр (очень похоже на вызов call) и возвращается новая функция, которая будет применять параметры к исходной функции при вызове:
3. Наследование
До сих пор мы видели, как объекты могут определять своё поведение и как мы можем использовать их действия на других объектах, но, мы до сих пор не увидели нормального пути для повторного использования кода и его расширяемости.
Вот тут-то нам и пригодится наследование. Оно позволит разделить задачи, в которых объекты определяют специализированное поведение от создания общего поведения для других объектов.
Модель прототипирования идёт дальше. Хоть она и поддерживает такие технологии, как «selective extensibility» и «behaviour sharing», но мы их не будем особо изучать. Печальная вещь: конкретные модели прототипного ОО, реализованные в JavaScript несколько ограниченны. Мы можем обойти эти ограничения, но накладные расходы будут велики.
3.1. Прототипы
Наследование в JavaScript осуществляется через клонирование поведения объекта и расширение его специализированным поведением. Объект, поведение которого клонируют, называется прототипом.
Прототип — это обычный объект, который делится своим поведением с другими объектами — в этом случае он выступает в качестве родителя.
Концепт клонирования поведения не означает, что вы будете иметь две различные копии одной и той же функции или данных. На самом деле JavaScript реализует наследование через делегирование, т.е. все свойства хранятся в родителе, а доступ к ним расширен через ребёнка.
Как упоминалось ранее, родитель (или [[Prototype]]) объекта определяется вызовом Object.create с первым аргументом, ссылающимся на объект-родитель.
Вернёмся к примеру с Мишей. Выделим его имя и способность приветствовать людей в отдельный объект, который поделится с Мишей своим поведением. Вот как будет выглядеть наша модель:
Реализуем её на JavaScript:
3.2 Но как же [[Prototype]] работает?
Как вы видели в прошлом примере, ни одно из свойств, определённых в Person мы не определяли явно в Mikhail, но всё же смогли получить к ним доступ. Это произошло благодаря тому, что JavaScript реализует делегирование доступа к свойствам, т.е. свойство ищется через всех родителей объекта.
Эта цепь родителей определяется скрытым слотом в каждом объекте, который называется [[Prototype]]. Вы не можете изменить его непосредственно, существует только один способ задать ему значение — при создании нового объекта.
Когда свойство запрашивается из объекта, движок сначала пытается получить свойство из целевого объекта. Если свойство не найдено, то рассматривается непосредственный родитель объекта, затем родитель родителя и т.д.
Это означает, что мы можем изменить поведение прототипа в середине программы, то автоматически изменится поведение всех объектов, которые были от него унаследованы. Например, пусть мы хотим изменить приветствие по умолчанию:
3.3. Перегрузка свойств
Так, прототипирование (т.е. наследование) используется для того, чтобы можно было поделиться данными с другими объектами. Причём этот способ работает быстро и экономичен по отношению к памяти, т.к. мы всегда имеем только один экземпляр используемых данных.
Но что если мы хотим добавить специализированное поведение, которым можно было бы делиться с другим объектами? Мы уже видели как объекты определяют своё поведение с помощью свойств, специализированное поведение определяется аналогичным образом — вы просто устанавливаете значение нужному свойству.
Для лучшей демонстрации положим, что Person реализует только обобщённое приветствие, а каждый наследник Person будет реализовывать своё уникальное приветствие. Также добавим новую персону в наш сценарий, чтобы лучше показать как расширяется объект:
Учтите, что и mikhail, и kristin определяют собственную версию метода greet. В этом случае мы вызовем метод greet из собственной версии поведения объекта, а не обобщённый метод greet, унаследованный от Person: