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

Рассматривать функцию-конструктор будем на примере создания круга, причем мы реально отрисуем его  на html-странице с помощью тега <svg>.  Давайте сначала просто посмотрим на код, позволяющий нам получить круг в теге <svg> без всякого JavaScript.

Вот, что получилось:

Мы получили черный круг внутри прямоугольной области шириной 600px и высотой 350px. Давайте добавим стилей, а также атрибуты fill и stroke для тега <circle>:

Наш круг остался в тех же координатах, но поменял цвет заливки и обводки. Плюс сам тег svg приобрел css-свойства, которые позволили увидеть его границы и координатную сетку для того, чтобы было удобно ориентироваться в пределах области, занимаемой тегом <svg>. В коде нам также понадобится атрибут id, т.к. мы можем обратиться к любому элементу с таким атрибутом из JavaScript с помощью метода document.getElementById('some_id').

Это был тестовый пример, чтобы убедиться в том, что мы можем рисовать круги с помощью тегов. Для нашего кода следует оставить <svg> абсолютно пустым, т.е. код  в html будет таким:

Весь код мы разместим в файле circle.js, который нужно подключить после нашего тега <svg>, чтобы иметь возможность рисовать в нем. Дальнейший код нужно писать именно в circle.js. Начать этот код необходимо с создания переменной forDraw:

Создание функции-конструктора

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

В этой функции благодаря возможности задавать параметры по умолчанию мы сразу описываем, что наш круг  может иметь радиус в 10px, черную заливку и прозрачный контур, а также расположение в координатах x=100px, y=100px, т.е. за исключением размера радиуса, полностью повторять круг из первого примера. Именно такой круг мы рисуем после создания переменной c1 и вызова для нее метода c1.draw(). Посмотрим на результат.

Действительно - у нас есть маленький черный круг в координатах  x=100px, y=100px.

Давайте добавим еще одну переменную с большим радиусом и другими цветами заливки и обводки.

Однако в области <svg> мы увидим всего один круг - желтый с красным контуром, размещенный в координатах x=200px, y=200px. Куда же делся наш первый черный круг?

На самом деле проблема в свойстве innerHTML. Когда мы присваиваем ему строку с тегом <circle> с атрибутами нового круга, все , что было перед этим внутри тега <svg> заменяется на новый тег <circle>. Причем он будет всегда в единственном экземпляре - том, для которого была последней вызвана функция draw(). Для того чтобы изменить это состояние, необходимо не перезаписывать (заменять) содержимое <svg>, а дописывать, используя оператор += для свойства innerHTML. Наша функция draw() теперь будет выглядеть так:

Посмотрим теперь на наши 2 круга:

Добавление метода translate()

Сейчас хотелось бы добавить метод перемещения круга, который представляет собой еще одну функцию для нашего объекта Circle. Назовем ее translate(). Она будет принимать 2 координаты - x и y, на которые мы будем смещать наши круги. Весь код теперь будет выглядеть так:

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

Все хорошо, но круг вместо того, чтобы переместиться на 50px по вертикали и горизонтали, просто перерисовался в этих координатах. И опять-таки это произошло за счет свойства innerHTML, т.к. теперь мы дописываем при вызове метода draw() новый тег <circle> в наш <svg>. Мы можем побороться с этими добавлениями, присвоив каждому нашему  тегу <circle> атрибут id, который будем получать, как случайное число в достаточно большом диапазоне. Затем в методе draw() будем отслеживать есть ли элемент с таким id и удалять его, если он есть, и только затем рисовать его в новых координатах.

Изменяем код еще раз:

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

Добавление геттера и сеттера

Добавим еще методы, которые получили название "геттеров" и "сеттеров". Это такие методы, которые позволяют получить (get) или назначить (set) какое-либо свойство объекта. В нашем примере этим свойством будет длина круга (circleLength).  Т.е. пользователь получает или устанавливает именно свойство, а мы описываем метод, который позволяет это сделать. Записать это в функции-конструкторе можно только с помощью специального метода Object.defineProperty() самого главного объекта в JavaScript - Object.

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

Используем назначение длины круга (окружности) для переменной c1. Заодно поменяем для нее цвет заливки на полупрозрачный голубой и цвет контура. В консоль браузера выведем значение нового радиуса после назначения длины окружности.

Наш первый круг обновился, но остался в тех же координатах x=100px, y=100px.

А теперь давайте выведем в консоль наши переменные.

Вот, что можно увидеть в браузере Chrome:

Объект Circle в консоли

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

Методы прототипа

javascript_prototype

Наш код сейчас изменится: в конструкторе останутся только определения свойств, а в прототип "переедут" 2 метода и геттер с сеттером. Код теперь будет таким:

Посмотрим в консоль:

Circle.prototype

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

Наследование объектов

Предположим, мы хотим рисовать не только круги, но и квадраты. Принцип рисования квадратов в теге <svg> несколько иной - они создаются с помощью тега <rect> с одинаковыми значениями атрибутов width и height:

Тем не менее, у кругов и квадратов много общего:

  1. радиус круга может заменить значение ширины и высоты квадрата
  2. координаты x и y нужны и кругу, и квадрату
  3. цвет заливки и обводки может быть назначен обеим фигурам
  4. атрибут id является универсальным, поэтому может быть использован для любого элемента

Поэтому мы используем функцию-конструктор Circle для создания квадрата. Для него также объявим функцию-конструктор Square:

Если посмотреть в консоль, то мы создали 2 объекта с парамерами, соответсвующими параметрам объекта-круга, т.е. такие свойства, как radius, location, fill, stroke и даже случайно формируемый id появились у наших переменных sq1 и sq2, которые являются экземплярами объекта Square.

Square на основе Circle

Однако метод draw() вызвал ошибку. А это значит, что недостаточно только выполнить вызов функции-конструктора Circle методом call() (или методом apply(), передав аргуметы в виде массива). Необходимо также указать, что прототипом объекта Square является прототип объекта Circle. Иногда также стоит указать и функцию-конструктор:

Теперь наш код выполнится, а в консоли мы увидим, что объект Square унаследовал методы draw() и translate(), а также геттер и сеттер для circleLength:

Square.prototype

Давайте теперь посмотрим, что же нарисовалось в теге <svg>?

Переписываем методы прототипа

Не знаю, насколько вы удивлены, но у нас добавились ... еще 2 круга - маленький черный по центру голубого и розовый чуть выше желтого. На самом деле, удивляться-то нечему - метод draw() наследуется от  Circle.prototype и по-прежнему рисует в <svg> круги. Поэтому перепишем метод draw() для Square.prototype так, чтобы у нас рисовался квадрат и заодно применим принцип полиморфизма из ООП, переопределив метод родительского объекта Circle для Square:

Смотрим на результат:

Наконец-то у нас получились квадраты.

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

Последнее, что мы проверим - это работу метода translate() и назначение свойства  perimeter:

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

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

Теперь посмотрим на то, что получилось со случайными размерами и цветами:

Обновите страницу несколько раз и получите разные наборы кругов и квадратов с разными цветами. При клике на круг/квадрат он удалится. Have fun!

Краткое резюме

Синтаксис функций-конструкторов предполагает создание на их основе целого ряда объектов-экземпляров с разными значениями свойств-переменных, но с едиными действиями в методах-функциях. Поэтому свойства определяются в конструкторе, а методы - в прототипе. Кстати, именно так реализованы встроенные объекты JavaScript - например, Array, String или Date. Принцип прототипного наследования экономит ресурсы, особенно, когда объектов много.

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

Ссылки по теме:

  1. Великий Прототип
  2. What is Javascript Prototypal Chain
  3. Расширение объектов. Свойство Prototype
Автор: Админ

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *