[jsexpert] Понятный JavaScript (Advanced) - Part 1

ОБЪЕКТНО ОРИЕНТИРОВАННОЕ ПРОГРАММИРОВАНИЕ

Основные понятия ООП

Объектно-ориентированное программирование (ООП) – это методология, которая позволяет представить приложение, в виде совокупности объектов , взаимодействующих друг с другом. В большинстве объектно-ориентированных языках программирования такие объекты создаются с помощью специальных конструкций, называемых Классами (Classes).
Класс – это программный код, который представляет из себя шаблон или заготовку, на основе которой в последствии и будет создан объект. Класс не имеет состояния и не предполагает вызов методов, описанных в классе. Это только заготовка/схема/чертеж.
Объект – структура, которая была создана из класса. Объект часто называют экземпляром класса. Работа программы происходит именно с объектами.
В JavaScript классов не существует, поэтому все объекты создаются с использованием функций конструкторов. Объекты, созданные из класса (функции-конструктора) называются экземплярами класса . Таким образом, объекты используются как составные блоки для приложения. Да и все приложение, по сути, является набором объектов со своими свойствами и методами, которые взаимодействуют друг с другом.
Следует добавить, что понятие класса появилось в синтаксисе ES6, однако на самом деле, эти классы, являются лишь удобной синтаксической конструкцией. На самом деле, все еще используется механизм прототипов, который будет рассмотрен далее.
При использовании ООП, следует придерживаться следующих принципов:

  • • инкапсуляция (encapsulation) – каждый объект отвечает за конкретную функциональность;
  • • наследование (inheritance) – объекты могут наследовать функциональность других объектов;
  • • полиморфизм (polymorphism) – объекты могут предоставлять одинаковый интерфейс и его использование, но внутренняя реализация этого интерфейса будет разной.

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

Прототип готового объекта proto

У любого созданного объекта всегда присутствует ссылка на другой объект, который называется прототипом . Не имеет значения каким образом создается объект, с помощью литерала объекта или конструктора new Object(), все они наследуются от Object. Прототипом всех объектов является глобальный объект Object .

const newObject = {};
console.log(newObject);

В консоли увидим пустой объект, со свойством, которое явно в нем не определялось – proto .


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

let functionality = {
    open: function() {
        console.log(`${this.room} is open.`);
    }
}

let bathRoom = {
    room: 'Bath room'
}

let kitchen = {
    room: 'Kitchen'
}

bathRoom.__proto__ = functionality;
kitchen.__proto__ = functionality;

bathRoom.open(); // Bath room is open.
kitchen.open(); // Kitchen is open.

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


То есть непосредственно в самом объекте bathRoom метода open нет. Но благодаря механизму прототипов, когда мы вызовем bathRoom.open(), произойдут следующие действия:

  1. поиск метода open внутри объекта bathRoom – там он не найден;
  2. поиск метода open у прототипа объекта bathRoom – найден и запущен.

Рассмотрим, как прототипы используются самим JavaScript на примере встроенного объекта Array.
Если создать новый массив с помощью конструктора new Array(), то прототипом будет не объект Object, а объект Array. Но прототипом самого объекта Array уже будет все тот же объект Object. Такая структура называется цепочкой прототипов .

const newArray = new Array();
console.log(newArray);


Именно в прототипе Array хранятся все методы для работы с массивами. Таким образом прототип – это место где хранятся все общедоступные свойства и методы, которые будут унаследованы при создании нового экземпляра объекта. Такие свойства и методы не копируются в каждый экземпляр объекта, они постоянно хранятся в прототипе.
Цепочка прототипов работает следующим образом: создав экземпляр объекта и вызвав какой-либо метод на нем, интерпретатор ищет сначала этот метод внутри текущего экземпляра объекта, если его там не обнаружилось, поиск продолжается в прототипе экземпляра объекта, и этот прототип доступен по ссылке proto.
Например, создав массив с числами, обратившись к свойству length, оно не будет найдено в самом массиве и будет искаться в прототипе массива, которым является Array. Так же, можно на массиве вызвать метод valueOf. Поиск метода сначала будет происходить в прототипе Array, но не найдя его там, поиск продолжится в прототипе Object, откуда и будет вызван.

const array = [1, 2, 3];

console.log(array.length); // 3
console.log(array.valueOf()); // [1, 2, 3]

Таким образом, если какое-то свойство или метод будут сразу найдены в самом объекте, то их поиск по цепочке прототипов не происходит. Например, у объекта Array есть метод toString и у объекта Object так же есть метод toString. При вызове этого метода на массиве, вызовется метод именно из объекта Array, а не объекта Object. Метод toString в Array переопределяет одноименный метод в Object.
Таким образом можно организовать цепочку поиска свойств и методов. Если их нет в оригинальном объекте, то они ищутся в объекте прототипа. Это и есть суть наследования в JavaScript.

Установка прототипа для функции-конструктора

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

let room = {
    area : 12
};

function BusinessRoom() {
    this.isMeetingAvailable = true;
};

BusinessRoom.prototype = room;
    
const businessRoom = new BusinessRoom();
console.log(businessRoom.area); // 12

Из свойства prototype в ссылку proto был установлен объект room как прототип для BusinessRoom.

Реализация «класса» с помощью прототипа

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

function Printer(doc) {
    this.document = doc;

    this.print = function () {
        console.log(this.document);
    };
}

const newPrinter = new Printer('Some text.');
const somePrinter = new Printer('Another text.');

newPrinter.print(); // Some text.
somePrinter.print(); // Another text.

Данный код отработает без ошибок и на первый взгляд является весьма приемлемым.
Выведем созданные на основе Printer объекты в консоль.


Как видно, оба объекта содержат в себе метод print.
Теперь реализуем то же, но с помощью prototype.

function Printer(doc) {
    this.document = doc;
}

Printer.prototype.print = function () {
    console.log(this.document);
}

const newPrinter = new Printer('Some text.');
const somePrinter = new Printer('Another text.');

newPrinter.print(); // Some text.
somePrinter.print(); // Another text.

И выведем новые объекты в консоль.

Как видно, теперь метод print не находится в каждом созданном объекте, а лишь в их прототипах. Прототип один, общий для обеих объектов. То есть, находясь в прототипе, как уже было указано выше, метод записывается в памяти и другие объекты просто ссылаются на это место в памяти для вызова метода.
Создание новых экземпляров newPrinter и somePrinter, с помощью функции-конструктора, позволило унаследовать свойства и методы (в приведенном примере только один метод print) из родительского Printer благодаря тому, что метод print был добавлен в свойство prototype родительской функции-конструктора.
Далее, на созданном экземпляре, благодаря прототипному наследованию, можно вызывать метод print, не создавая такой метод в самом экземпляре.

НАСЛЕДОВАНИЕ

Основные понятия

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

  1. При вызове метода у объекта происходит его поиск с начала у самого объекта, затем у прототипа этого объекта.
  2. Прототип уже созданного объекта доступен по ссылке proto. И вы можете при необходимости его задать, установив значение в это поле.
  3. Если вы планируете создавать объект с помощью функции конструктора, то вы устанавливаете необходимый вам прототип в свойство prototype функции-конструктора. Затем, при создании объекта значение в proto подставляется из свойства prototype функции-конструктора.

Записав новую функцию-конструктор и выведя ее свойство prototype в консоль, можно обнаружить, что по умолчанию, там находится не только ссылка на прототип объекта Object, а и свойство constructor .

function NewFunction() {}
console.log(NewFunction.prototype);


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

Прототипное наследование в JavaScript

Наследование – это концепция ООП, благодаря которой вы можете расширить функциональность одного класса за счет методов и свойств другого класса. Другими словами, некий класс «наследник» имеет возможность вызывать и использовать методы «родителя». При этом, фактически у самого «наследника» этих методов нет. С помощью специального механизма наследования он их вызывает у «родителя» как будто они принадлежат ему самому.
Класс, определённый через наследование от другого класса, называется: производным классом, классом потомком (derived class) или подклассом (subclass). Класс, от которого новый класс наследуется, называется: предком (parent), базовым классом (base class) или суперклассом (parent class).
Напомним, что классов в JavaScript нет, условные «классы» реализуются с помощью функций-конструкторов.
Механизм наследования дает возможность получить доступ к функциональности родительских объектов («классов»), благодаря чему, можно легко повторно использовать код и расширять функциональность.
Схематически наследование можно отобразить так.

Экземпляры объекта могут как использовать унаследованную функциональность, так и иметь свою собственную.
Создадим родительский объект Machine, который будет унаследован и дочерний объект TapeRecorder, который будет наследовать.

function Machine(product) {
    this.product = product;
}

Machine.prototype.on = function() {
    console.log(`${this.product} is ON!`);
}

Machine.prototype.off = function() {
    console.log(`${this.product} is OFF!`);
}

function TapeRecorder(product) {
    this.product = product;
}

Реализация наследования осуществляется с помощью одной строки кода.

TapeRecorder.prototype = Object.create(Machine.prototype);

Метод Object.create() создаёт новый объект и устанавливает в его прототип то, что было указано в качестве аргумента.
Теперь можно использовать методы родительского объекта.

function Machine(product) {
    this.product = product;
}

Machine.prototype = {
    on: function() {
        console.log(`${this.product} is ON!`);
    },
    off: function() {
        console.log(`${this.product} is OFF!`);
    }
}

function TapeRecorder(product) {
    this.product = product;
}

TapeRecorder.prototype = Object.create(Machine.prototype);

const tapeRecorder = new TapeRecorder('Tape Recorder');
tapeRecorder.on(); // Tape Recorder is ON!

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

function TapeRecorder(product) {
    this.product = product;
}

TapeRecorder.prototype = Object.create(Machine.prototype);

TapeRecorder.prototype.pause = function() {
    console.log(`${this.product} on PAUSE!`);
}

const tapeRecorder = new TapeRecorder('Tape Recorder');
tapeRecorder.on(); // Tape Recorder is ON!
tapeRecorder.pause(); // Tape Recorder on PAUSE!

Свойство constructor и переопределение методов

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

TapeRecorder.prototype.constructor === Machine // true
TapeRecorder.prototype.constructor === TapeRecorder // false

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

TapeRecorder.prototype.constructor = TapeRecorder;

TapeRecorder.prototype.constructor === Machine // false
TapeRecorder.prototype.constructor === TapeRecorder // true

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

function Machine(product) {
    this.product = product;
}

Machine.prototype.on = function() {
    console.log(`${this.product} is ON!`);
}

Machine.prototype.off = function() {
    console.log(`${this.product} is OFF!`);
}

function TapeRecorder(product) {
    this.product = product;
}

TapeRecorder.prototype = Object.create(Machine.prototype);
TapeRecorder.prototype.constructor = TapeRecorder;

TapeRecorder.prototype.pause = function() {
    console.log(`${this.product} on PAUSE!`);
}

const tapeRecorder = new TapeRecorder('Tape Recorder');
tapeRecorder.on(); // Tape Recorder is ON!
tapeRecorder.pause(); // Tape Recorder on PAUSE!

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

function Machine(product) {
    this.product = product;
}

Machine.prototype.on = function() {
    console.log(`${this.product} is ON!`);
}

function TapeRecorder(product, model) {
    this.product = product;
}

TapeRecorder.prototype = Object.create(Machine.prototype);
TapeRecorder.prototype.constructor = TapeRecorder;

TapeRecorder.prototype.on = function() {
    console.log(`${this.product} is ON! Music is playing!`);
}

const tapeRecorder = new TapeRecorder('Tape Recorder');
tapeRecorder.on(); // Tape Recorder is ON! Music is playing!

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

function Machine(product) {
    this.product = product;
}

Machine.prototype.on = function() {
    console.log(`${this.product} is ON!`);
}

Machine.prototype.off = function() {
    console.log(`${this.product} is OFF!`);
}

function TapeRecorder(product, model) {
    this.product = product;
}

TapeRecorder.prototype = Object.create(Machine.prototype);
TapeRecorder.prototype.constructor = TapeRecorder;

TapeRecorder.prototype.on = function() {
    console.log(`${this.product} is ON! Music is playing!`);
}

TapeRecorder.prototype.pause = function() {
    console.log(`${this.product} on PAUSE!`);
}

const tapeRecorder = new TapeRecorder('Tape Recorder');
tapeRecorder.on(); // Tape Recorder is ON! Music is playing!
tapeRecorder.pause(); // Tape Recorder on PAUSE!

Родительские свойства и методы в контексте дочернего объекта

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

function TapeRecorder(product, model) {
    Machine.apply(this, arguments);
    this.model = model;
}

TapeRecorder.prototype = Object.create(Machine.prototype);
TapeRecorder.prototype.constructor = TapeRecorder;

TapeRecorder.prototype.pause = function() {
    console.log(`${this.product} on PAUSE!`);
}

const tapeRecorder = new TapeRecorder('Tape Recorder', 'TH-45');
tapeRecorder.on(); // Tape Recorder is ON!

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

function Machine(product) {
    this.product = product;
}

Machine.prototype.on = function() {
    console.log(`${this.product} is ON!`);
}

function TapeRecorder(product, model) {
    Machine.apply(this, arguments);
    this.model = model;
}

TapeRecorder.prototype = Object.create(Machine.prototype);
TapeRecorder.prototype.constructor = TapeRecorder;

TapeRecorder.prototype.on = function() {
    Machine.prototype.on.apply(this, arguments);
    console.log(`Model: ${this.model}.`);
}

const tapeRecorder = new TapeRecorder('Tape Recorder', 'TH-45');
tapeRecorder.on(); 
// Tape Recorder is ON!
// Model: TH-45.

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

function Machine(product) {
    this.product = product;
}

Machine.prototype.on = function() {
    console.log(`${this.product} is ON!`);
}

Machine.prototype.off = function() {
    console.log(`${this.product} is OFF!`);
}

function TapeRecorder(product, model) {
    Machine.apply(this, arguments);
    this.model = model;
}

TapeRecorder.prototype = Object.create(Machine.prototype);
TapeRecorder.prototype.constructor = TapeRecorder;

TapeRecorder.prototype.on = function() {
    Machine.prototype.on.apply(this, arguments);
    console.log(`Model: ${this.model}.`);
}

TapeRecorder.prototype.pause = function() {
    console.log(`${this.product} on PAUSE!`);
}

const tapeRecorder = new TapeRecorder('Tape Recorder', 'TH-45');
tapeRecorder.on(); 
// Tape Recorder is ON!
// Model: TH-45.

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

function inheritance(parent, child) {
    let tempChild = child.prototype;
    child.prototype = Object.create(parent.prototype);
    child.prototype.constructor = child;

    for (let key in tempChild) {
        if (tempChild.hasOwnProperty(key)) {
            child.prototype[key] = tempChild[key];
        }
    }
}

Функция делает следующее.

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

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

function inheritance(parent, child) {
    let tempChild = child.prototype;
    child.prototype = Object.create(parent.prototype);
    child.prototype.constructor = child;

    for (let key in tempChild) {
      if (tempChild.hasOwnProperty(key)) {
        child.prototype[key] = tempChild[key];
      }
    }
  }

  
function Machine(product) {
    this.product = product;
}

Machine.prototype = {
    on: function() {
        console.log(`${this.product} is ON!`);
    },
    off: function() {
        console.log(`${this.product} is OFF!`);
    }
}

function TapeRecorder(product, model) {
    this.product = product;
    this.model = model;
}

TapeRecorder.prototype = {
    on: function() {
        console.log(`Model: ${this.model}.`);
    },
    pause: function() {
        console.log(`${this.product} on PAUSE!`);
    }
}

inheritance(Machine, TapeRecorder);

const tapeRecorder = new TapeRecorder('Tape Recorder', 'TH-45');
tapeRecorder.on(); // Model: TH-45.

ДОМАШНЕЕ ЗАДАНИЕ

Цель : Научиться создавать модули с использованием функции конструктора и прототипов. Научиться базовым приемам наследования.
Вам необходимо совместить компонент «залогинивания» (проверки логина и пароля) который был реализован в прошлом домашнем задании с галереей изображений.
А точнее нужно сымитировать авторизацию. Если ничего другого не указано, то логин форма и галерея должны работать так как описано в предыдущих домашних заданиях.

Архив с заготовкой вы можете скачать здесь.

Описание базового задания:
Для реализации этого задания использовать синтаксис ES6 НЕ нужно.
Приложение должно состоять из нескольких модулей. Каждый модуль располагается в своем отдельном файле.
Каждый модуль является отдельной функцией конструктором. Методы располагаются не внутри функции конструктора, а внутри протопипа (prototype). Приблизительная схема организации модулей и последовательность их инициализации доступна во вложении.
В базовой версии задания наследование использовать не нужно. Только реализовать все через прототипы.

Создание объектов из функций конструкторов должно происходить в файле main.js.
Всего должно быть 3 модуля: «модуль залогинивания», «модуль валидации», «модуль галереи».

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

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

В Header menu (шапке или верхнем меню) есть три кнопки. «Галерея», «О пользователе» и «Выход». При нажатии на кнопку «галерея» — видна галерея. При нажатии на кнопку «О пользователе» — открывается информация о пользователе. При нажатии на кнопку «выход» происходит «разлогинивание» или «выход». Пользователь перебрасывается назад на страницу ввода пароля. И пока он снова не укажет правильный логин пароль, не сможет увидеть галерею или свой профиль.

Описание расширенного задания:
Функционал добавления нового изображения и удаления из галереи должен быть вынесен в отдельный «класс». То есть базовый «класс» галереи будет содержать весь функционал, кроме кода который отвечает за кнопки «добавить изображение» и кнопку «удалить».
Отдельный класс, который наследует базовый, реализует эти возможности.
Таким образом необходимо создать экземпляр класса наследника, который будет наследовать базовый класс используя функцию наследования (inheritance):

    function inheritance(parent, child) {
        let tempChild = child.prototype;
        child.prototype = Object.create(parent.prototype);
        child.prototype.constructor = child;
    
        for (let key in tempChild) {
            if (tempChild.hasOwnProperty(key)) {
                child.prototype[key] = tempChild[key];
            }
        }
    }

Пошаговый алгоритм:
1. Функция конструктор модуля «авторизации» принимает объект как параметр. В этом объекте устанавливается значение правильного логина пароля. {login: «admin», password: «qaz1234»}
2. При вводе пароля пользователем проверяем его с помощью модуля «валидации».
3. Применяются следующие проверки: поля не пустые, email соответствует шаблону, пароль минимум 8 символов в длину. Для каждого случая отображается свое сообщение об ошибке.
4. Если все проверки прошли, сверяем введенные данные (логин/пароль) с эталонными.
5. Если все ок, инициализируем галерею.
6. Открываем галерею и подсвечиваем текущую открытую страницу в верхнем меню.
7. В логин модуле должен быть блок кода, который отвечает за отображение и работу кнопки «выйти».
8. Соответственно после успешного входа, кнопка появляется и при нажатии на нее происходит выход («разлогинивание»).
9. При нажатии в верхнем меню кнопки «О пользователе» мы увидим информацию о пользователе. Его логин и пароль, а также кнопку «показать пароль». Также как и в предыдущем дз.
10. При нажатии в верхнем меню кнопки «галерея» мы увидим галерею.

НАСЛЕДОВАНИЕ И ES6

Классы в ES6

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

class Car {
    constructor(brand) {
        this.brand = brand;
        this.wheels = 4;
    }

    drive() {
        console.log('Car is on road');
    }
}

const bmw = new Car('M5');
bmw.drive();

Используя старый синтаксис, этот класс выглядел бы следующим образом:

function Car(brand) {
    this.brand = brand;
    this.wheels = 4;
};

Car.prototype.drive = function() {
    console.log('Car is on road');
};

const bmw = new Car('M5');
bmw.drive();

Разберемся что происходит при использовании нового синтаксиса.
Метод constructor – определяет функцию, которая представляет собой класс. Это специальный метод, который служит для создания и инициализации объектов, созданных с использованием ключевого слова class. Запуск этого метода происходит во время инициализации нового экземпляра класса с помощью new. Фактически это аналог знакомой нам функции-конструктора.
Все остальные методы, которые находятся внутри тела класса, помещаются в прототип объекта.
В теле класса могут определяться только методы, все свойства должны определяются в методе constructor.
Рассмотрим остальные «виды» методов, которые могут использоваться в классах.
Статические методы являются методами самого класса, которые вызываются непосредственно на самом классе и принадлежат ему. Определяются такие методы с помощью ключевого слова static.

class Car {
    constructor(brand) {
        this.brand = brand;
        this.wheels = 4;
    }

    drive() {
        console.log('Car is on road');
    }

    static info() {
        console.log('Class for creating cars.');
    }
}

const bmw = new Car('M5');

Car.info(); // Class for creating cars.
bmw.info(); // Uncaught TypeError: bmw.info is not a function

Эти методы не используются для наследования и создаются исключительно для собственных потребностей класса.
Геттеры и сеттеры – это так называемые свойства-аксессоры (свойства доступа). Они, как правило, используются для получения/определения значений свойств объекта, чтобы исключить прямой доступ к этим свойствам.

class Car {
    constructor(brand) {
        this.brand = brand;
        this._wheels = 4;
    }

    drive() {
        console.log('Car is on road');
    }
    
    get wheels() {
        console.log(this._wheels);
    }
    
    set wheels(value) {
        this._wheels = value;
    }
}

const bmw = new Car('M5');

bmw.wheels; // 4
bmw.wheels = 10;
bmw.wheels; // 10;

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

Наследование с помощью классов

Для наследования в ES6 существует специальное ключевое слово extends .

class Car {
    constructor(name = 'default') {
        this.wheels = 4;
        this.name = name;
    }

    drive() {
        console.log(`Car ${this.name} is on road`);
    }

    stop() {
        console.log(`Car ${this.name} is stopped`);
    }
}

class ElectroCar extends Car {
    constructor(name) {
        super(name);
    }
}

const tesla = new ElectroCar('Model S');
tesla.drive();
tesla.stop();

Обратите внимание на использование super . С его помощью возможен вызов конструктора-родителя. Ключевое слово super вызывает конструктор родительского класса и позволяет «записать» все свойства из него в текущий класс (то есть в this текущего объекта).
С помощью старого синтаксиса это реализовывалось через ParentFunction.apply(this, arguments).
Кроме того, использование super помогает получить доступ к методам родительского класса, без необходимости напрямую обращаться к прототипу конструктора родителя, как это реализовывалось в старом синтаксисе ParentFunction.prototype.methodName.apply(this, arguments).

class Car {
    constructor(name = 'default') {
        this.wheels = 4;
        this.name = name;
    }

    drive() {
        console.log(`Car ${this.name} is on road`);
    }

    stop() {
        console.log(`Car ${this.name} is stoped`);
    }
}

class ElectroCar extends Car {
    constructor(name) {
        super(name);
    }

    drive() {
        super.drive();
        console.log(`Tesla ${this.name} is using battery to drive`);
    }
}

var tesla = new ElectroCar('Model S');
tesla.drive();
// Car Model S is on road
// Tesla Model S is using battery to drive

Конструкция super.drive() дала возможность использовать результат вызова родительского метода и расширить его новым функционалом, который необходим для дочернего класса.
Если наследование реализуется с использованием синтаксиса ES6 и существует необходимость работать с дочерним конструктором, super() следует вызывать перед добавлением любого нового свойства в дочерний конструктор. В ином случае, новые свойства будут затёрты.
Если же в дочернем классе не будет указан собственный метод constructor, то ему (дочернему классу), автоматически будет присвоен конструктор родительского класса.
Полная реализация наследования выглядит так:

class Car {
    constructor(name = 'default') {
        this.wheels = 4;
        this.name = name;
    }

    drive() {
        console.log(`Car ${this.name} is on road`);
    }

    stop() {
        console.log(`Car ${this.name} is stoped`);
    }
}

class ElectroCar extends Car {
    constructor(name, year) {
        super(name);
        this.year = year;
    }

    drive() {
        super.drive();
        console.log(`Tesla ${this.name} is using battery to drive`);
        console.log(`Year ${this.year}`);
    }
}

var tesla = new ElectroCar('Model S', 2017);
tesla.drive();
// Car Model S is on road
// Tesla Model S is using battery to drive
// Year 2017

СЕТЬ И ОБМЕН ДАННЫМИ

Общее понятие сетевых взаимодействий

Любое web приложение или сайт состоит из двух основных частей.

Frontend включает в себя то, что видит пользователь. Находится в вашем браузере. Основу Frontend составляет комплекс HTML, CSS, JavaScript.

Backend часть соответственно находится на сервере. Основная цель – сохранять и обрабатывать данные.

С этой точки зрения работа любого веб сайта это серия сетевых взаимодействий.
Клиент (ваш браузер) запрашивает данные у сервера (request).
Сервер обрабатывает запрос и отвечает браузеру (response).


Обычно при открытии сайта происходят десятки, а иногда и сотни таких пар запрос/ответ.

Для статического сайта схема обычно выглядит таким образом:

  • • браузер выполняет запрос на определенный адрес URL;
  • • сервер обрабатывает запрос и отсылает назад браузеру полностью сформированную страницу;
  • • браузер получает ответ и показывает страницу.

Таким образом необходимо каждый раз «обновлять» страницу чтоб загрузить новый контент.

Для SPA (Single Page Application) схема выглядит таким образом:

  • • браузер выполняет запрос на определенный адрес URL;
  • • сервер обрабатывает запрос, и отсылает назад браузеру HTML страницу, которая содержит только основные элементы верстки, подключенные стили и JavaScript файлы;
  • • браузер получает ответ и показывает страницу, однако в таком виде страница является «полупустой» и пользователю обычно бесполезна;
  • • начинают выполнятся скрипты, которые снова обращаются к серверу и загружают все недостающие данные с помощью технологии AJAX. Загрузка новых данных может происходить «на фоне». Не нужно перезагружать страницу.

Протокол HTTP

HTTP (англ. HyperText Transfer Protocol – «протокол передачи гипертекста») –
специальный протокол предназначенный для передачи данных в виде текста.
Изначально задумывался для передачи документов в формате «HTML», в настоящий момент используется для передачи произвольных данных.
Для непосредственной передачи данных по сети используется протокол TCP, HTTP работает на его основе и отвечает за «смысловую нагрузку» тех данных, которые пересылаются.

Протокол HTTP строиться на следующих принципах:

  • • взаимодействие клиент – сервер;
  • • отсутствие состояний;
  • • разные методы передачи данных (GET, POST, PUT, DELETE);
  • • различные статусы ответов (Status Codes);
  • • служебная информация содержится в заголовках (Headers).

Рассмотрим некоторые ключевые особенности работы с протоколом HTTP.

Структура протокола:

  • • стартовая строка (Starting line) – тип сообщения;
  • • заголовки (Headers) – параметры сообщения;
  • • тело сообщения (Message Body) – данные сообщения (обязательно должно отделяться от заголовков пустой строкой).

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

Стартовая строка запроса (метод, url, версия):
GET /wiki/HTTP HTTP/1.0
Host: ru.wikipedia.org
Стартовая строка ответа (версия, код состояния, комментарий):
HTTP/1.0 200 OK

Методы HTTP – ключевые слова, которые определяют способ
взаимодействия с сервером (операцию, которую надо произвести с ресурсом).
GET – запрашивает данные из определенного ресурса. Наиболее популярный способ загрузить html, css, png или другие данные.
Поддерживает передачу параметров в виде key=value.
GET /path/page?param1=value1&param2=value2 HTTP/1.1

HEAD запрашивает ресурс так же, как и метод GET, но без тела ответа.
Применяется для проверки наличия ресурса и чтобы узнать, не изменился ли он с момента последнего обращения.

POST используется для отправки данных пользователя на определённый ресурс (URL). Передаваемые данные включаются в тело запроса. Используется для загрузки файлов на сервер.

PUT заменяет текущие содержимое ресурса данными запроса.
Метод POST предназначен для «создания» новых сущностей, а метод PUT для «обновления» ранее созданных данных.

OPTIONS используется для определения параметров соединения с определенным ресурсом или для определения возможностей сервера.

DELETE удаляет указанные данные.
Существует не малое количество методов, однако наиболее часто применяют два из них: GET и POST.

REST интерфейс

REST набор архитектурных принципов построения распределенных веб сервисов и систем.
Это архитектурный стиль, который имеет достаточно абстрактные требования. Это не протокол и не стандарт, по этому он не является на 100% регламентированным.

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

Фактически RESTful API – всего лишь набор URI, HTTP вызовов к этим URI и некоторое количество представлений ресурсов в формате JSON и/или XML.
Для реализации RESTfull интерфейса чаще всего используют следующие методы: POST, GET, PUT и DELETE. Они соответствуют операциям создания, чтения, обновления и удаления (или в совокупности – CRUD) Так же иногда используются OPTIONS и HEAD.

Методы и ответы для разных случаев

Метод Список (http://test.com/users/) Элемент списка (http://test.com/users/123)
GET 200 (OK), происходит считывание списка пользователей. 200 (OK), происходит считывание данных об одном пользователе.
PUT 404 (Not Found), обновление всего списка недопустимо. 200 (OK), обновляем одного пользователя.
POST 201 (Created), новый пользователь был создан и добавлен в список. 404 (Not Found), нельзя создать нового пользователя при этом обращаясь по URL под
которым доступен другой пользователь.
DELETE 404 (Not Found), удаление всего списка недопустимо. 200 (OK), удаление прошло успешно или 404 (Not Found), по такому адресу нет пользователя для
удаления.

Коды состояния

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

1xx: Информационные сообщения

Набор этих кодов был введён в HTTP/1.1. Сервер может отправить запрос вида: Expect: 100-continue, что означает, что клиент ещё отправляет оставшуюся часть запроса. Клиенты, работающие с HTTP/1.0 игнорируют данные заголовки.

2xx: Сообщения об успехе

Если клиент получил код из серии 2xx, то запрос ушёл успешно. Самый распространённый вариант — это 200 OK. При GET запросе, сервер отправляет ответ в теле сообщения. Также существуют и другие возможные ответы:
202 Accepted: запрос принят, но может не содержать ресурс в ответе. Это полезно для асинхронных запросов на стороне сервера. Сервер определяет, отправить ресурс или нет.
204 No Content: в теле ответа нет сообщения.
205 Reset Content: указание серверу о сбросе представления документа.
206 Partial Content: ответ содержит только часть контента. В дополнительных заголовках определяется общая длина контента и другая инфа.

3xx: Перенаправление

Своеобразное сообщение клиенту о необходимости совершить ещё одно действие. Самый распространённый вариант применения: перенаправить клиент на другой адрес.
301 Moved Permanently: ресурс теперь можно найти по другому URL адресу.
303 See Other: ресурс временно можно найти по другому URL адресу. Заголовок Location содержит временный URL.
304 Not Modified: сервер определяет, что ресурс не был изменён и клиенту нужно задействовать закэшированную версию ответа. Для проверки идентичности информации используется ETag (хэш Сущности — Enttity Tag);

4xx: Клиентские ошибки

Данный класс сообщений используется сервером, если он решил, что запрос был отправлен с ошибкой. Наиболее распространённый код: 404 Not Found. Это означает, что ресурс не найден на сервере. Другие возможные коды:
400 Bad Request: вопрос был сформирован неверно.
401 Unauthorized: для совершения запроса нужна аутентификация. Информация передаётся через заголовок Authorization.
403 Forbidden: сервер не открыл доступ к ресурсу.
405 Method Not Allowed: неверный HTTP метод был задействован для того, чтобы получить доступ к ресурсу.
409 Conflict: сервер не может до конца обработать запрос, т.к. пытается изменить более новую версию ресурса. Это часто происходит при PUT запросах.

5xx: Ошибки сервера

Ряд кодов, которые используются для определения ошибки сервера при обработке запроса. Самый распространённый: 500 Internal Server Error. Другие варианты:
501 Not Implemented: сервер не поддерживает запрашиваемую функциональность.
503 Service Unavailable: это может случиться, если на сервере произошла ошибка или он перегружен. Обычно в этом случае, сервер не отвечает, а время, данное на ответ, истекает.

JSON формат

{
    "isAvailable": true,
    "list": [
        {
            "name": "1",
            "seo_name": "ukraine",
            "value": "All",
            "count": 43,
            "is_path": 0
        },
        {
            "name": "1-3",
            "value": "Волинь",
            "seo_name": "volinska-oblast",
            "count": 21,
            "is_path": 1
        }
    ]
}

Для работы с данными в этом формате используется встроенный в браузер объект, который называется так же JSON.
У него есть 2 метода:

  • JSON.parse();
  • JSON.stringify().

JSON.parse(text, [revieverFunction]) – превращает строку в JSON формате в JavaScript сущность (объект, массив, строку).

const jsonData='{"isAvailable":true, "list":[{"name": "All","count":43}]}';
const data = JSON.parse(jsonData);
console.log(data);

Метод принимает и второй, необязательный, аргумент – функция, которая может влиять на результат преобразования.

const jsonData='{"isAvailable":true,"list":[{"name": "All","count":43}]}';
const data = JSON.parse(jsonData, function (key, value) {
    if (key == "isAvailable") {
    return "Data is Available";
    } else {
    return value;
    }
});
console.log(data);

Функция используется при необходимости выполнить какую-то дополнительную фильтрацию и заменить либо преобразовать какие-то данные.

JSON.stringify(data, [filter]) – сериализирует любой объект, массив или структуру в JSON формат.

const data = {
    id: 12,
    name: "john",
    params: [12,15],
    date: new Date(),
    calc: function() { return 12; }
}
const result = JSON.stringify(data);
console.log(result);

В процессе сериализации происходит преобразование объекта в текстовое представление, при этом действуют такие правила:

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

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

const data = {
    id: 12,
    name: "john",
    params: [12,15],
    date: new Date(),
    calc: function() { return 12; }
}
const result = JSON.stringify(data, ['id', 'name']);
console.log(result);

AJAX

Понятие AJAX

AJAX (Asynchronous Javascript And Xml) – технология, которая позволят обращаться к серверу без необходимости перезагрузки текущей страницы.
При использовании AJAX есть возможность обновлять страницу частично, при этом запросы на сервер отправляются как бы в «фоновом» режиме.
AJAX работает, используя все те же HTTP запросы (POST, GET, и т.д.) и может обмениваться данными с сервером в различных форматах: текст, HTML, XML, JSON и др.
Используется асинхронная передача данных. Это означает, что пока происходит передача данных, пользователь может совершать другие действия на странице.
При этом, AJAX не является частью JavaScript. Технология AJAX, для своей реализации, использует комбинацию:
встроенный в браузер объект XMLHttpRequest (для запроса данных с сервера);
JavaScript и DOM (для обработки и отображения данных).
Для обмена данными, должен быть создан объект XMLHttpRequest, который является своеобразным посредником между браузером и сервером. С помощью XMLHttpRequest можно отправить запрос на сервер, а также получить ответ в виде различного рода данных.
Этапы отправки и получения данных:

  • • событие, которое инициализирует запрос (например, клик);
  • • создание XMLHttpRequest объекта;
  • • отправка HTTP запроса с помощью XMLHttpRequest объекта;
  • • сервер обрабатывает запрос;
  • • сервер отправляет ответ;
  • • обработка ответа с помощью JavaScript.

XMLHttpRequest – основа AJAX технологии

XMLHttpRequest – встроенный в браузер объект, который позволяет совершать HTTP-запросы к серверу.
Для работы с AJAX запросами, в первую очередь, необходимо создать новый XMLHttpRequest объект.

const xhr = new XMLHttpRequest();

Далее, для создания запроса используются специальные методы XMLHttpRequest объекта.
Для начала необходимо определить тип (метод) HTTP-запроса, его URL (на какой адрес отправить запрос). Настроить все это поможет метод open() .

xhr.open('GET', 'https://jsonplaceholder.typicode.com/posts?userId=1', true);

Первый параметр, это тип запроса, как правило это GET или POST.
Второй – URL на который необходимо отправить запрос.
Третий параметр, определяет каким будет запрос, асинхронным или синхронным (более детальнее это будет рассмотрено далее). Установив значение true – асинхронный, false – синхронный. Если же этот параметр оставить пустым, то запрос, по умолчанию, будет асинхронным.
После определения параметров для запроса, можно его отправить. За данное действие отвечает метод send() .

xhr.send();

Сразу следует обратить внимание, что параметры (если они необходимы) для GET запроса, передаются непосредственно в URL запроса, в примере выше userId=1 – это параметры запроса (‘https://jsonplaceholder.typicode.com/posts? userId=1 ‘). При POST запросе, параметры передаются в теле запроса, а именно – передаются как параметры в метод send().
Пример POST запроса, будет рассмотрен после того, как разберемся с тем, как получить и обработать ответ от сервера.
Рассмотрим следующие свойства объекта xhr: readyState и status .
Свойство readyState – содержит текущее состояние XMLHttpRequest объекта.
Существует 5 состояний:

  • • 0 – UNSENT – объект создан, но метод open() ещё не вызван;
  • • 1 – OPENED – метод open() был вызван;
  • • 2 – HEADERS_RECEIVED – метод send() был вызван;
  • • 3 – LOADING – загрузка, начало получение данных от сервера;
  • • 4 – DONE – данные полностью получены, операция завершена.

Свойство status – HTTP-код ответа от сервера. Существует множество таких кодов, например, 200 (OK), 404 (Not Found), 500 (Internal Server Error) и т.д.
После отправки запроса, ответ от сервера можно получить с помощью свойства responseText – данные с сервера автоматически попадают в это свойство.
Таким образом, необходимо проверить состояние объекта и статус ответа от сервера, если все успешно, то можно обработать данные от сервера.

const xhr = new XMLHttpRequest();

xhr.open('GET', 'https://jsonplaceholder.typicode.com/posts?userId=1', true);
xhr.send();

if (xhr.readyState === 4 && xhr.status === 200) {
    console.log(xhr.responseText);
}

Хорошей практикой является не просто проверка на состояние объекта и статус ответа, а вызов коллбек функции, после соответствующего события. На объекте XMLHttpRequest можно определить события, при инициализации которых, будет вызвана соответствующая функция.
Для отслеживания успешного выполнения запроса можно использовать два события onreadystatechange или onload .
Событие onreadystatechange срабатывает каждый раз, когда изменяется состояние XMLHttpRequest объекта. То есть, каждый раз, когда изменяется значение свойства readyState.
Событие onload срабатывает, когда запрос был успешно выполнен.
Обработку ответа от сервера лучше совершать при срабатывании одного из вышеописанных событий. Эффект от их использования одинаковый, onreadystatechange более старое событие, onload было введено в стандарт позже.

const xhr = new XMLHttpRequest();

xhr.onload = function() {
    if (xhr.readyState === 4 && xhr.status === 200) {
        console.log(xhr.responseText);
    }
};

xhr.open('GET', 'https://jsonplaceholder.typicode.com/posts?userId=1', true);
xhr.send();

Таким образом при получении ответа от сервера произойдет событие и будет вызвана функция, внутри которой сначала происходит проверка в каком состоянии находится XMLHttpRequest объекта и успешный ли ответ от сервера. После этого производится обработка ответа.
Для установки HTTP-заголовков существует специальный метод setRequestHeader() . Этот метод вызывается после open() и перед send().
Рассмотрим установку HTTP-заголовков при отправке POST запроса. Помните, что при POST запросах данные передаются не в URL, а теле запроса, который передается в send().

const url = 'https://jsonplaceholder.typicode.com/posts';
const data = {
    title: 'John',
    body: Admin,
    userId: 1
};
const json = JSON.stringify(data);

const xhr = new XMLHttpRequest();
xhr.open("POST", url, true);
xhr.setRequestHeader('Content-type','application/json; charset=utf-8');
xhr.onload = function () {
    const response = JSON.parse(xhr.responseText);
    if (xhr.readyState == 4 && xhr.status == "200") {
        console.table(response);
    } else {
        console.error(response);
    }
};
xhr.send(json);

Обратите внимание, в примере выше вызываются методы JSON.stringify() и JSON.parse() , которые используются для преобразования данных в JSON формат и для преобразования JSON данных в обычный объект соответственно.
Для обработки ошибок можно использовать событие onerror.

const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://jsonplaceholder.typicode.com/posts?userId=1', true);

xhr.onload = function() {
    if (xhr.readyState === 4 &&+ xhr.status === 200) {
        console.log(xhr.responseText);
    }
};

xhr.onerror = function() {
  console.log('Error!');
};

xhr.send();

АСИНХРОННЫЙ JAVASCRIPT

Синхронное и асинхронное программирование

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

console.log('One');
console.log('Two');
console.log('Three');

// One
// Two
// Three

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

console.log('One');

const xhr = new XMLHttpRequest();
xhr.open('GET', '/', true);

xhr.onload = function() {
    console.log('Two');
};

xhr.send();

console.log('Three');

// One
// Three
// Two

При изучении AJAX запросов, упоминалось, что третий параметр в методе open указывает на то, как будет выполнен запрос – асинхронно или синхронно. Таким образом, указав третий параметр как false, AJAX будет выполнен в синхронном режиме и заблокирует дальнейшее выполнение кода до тех пор, пока не будет получен ответ от сервера.

console.log('One');

const xhr = new XMLHttpRequest();
xhr.open('GET', '/', false);

xhr.onload = function() {
    console.log('Two');
};

xhr.send();

console.log('Three');

// One
// Two
// Three

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

Promise

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

  • • pending – начальное состояние, ожидание;
  • • resolved – операция завершена успешно;
  • • rejected – операция завершена с ошибкой;
  • • settled – выполнено или отклонено, но не находится в состоянии ожидания.

Рассмотрим использование Promise на примере. Успешность или не успешность операции будем имитировать с помощью обычного условного оператора. Например, в зависимости от полученной зарплаты (значение переменной enoughSalary), зависит будет ли куплен новый телефон.

const enoughSalary = true;

const buyNewPhone = new Promise(
    function(resolve, reject) {
        if (enoughSalary) {
            const phone = {
                brand: 'Samsung',
                model: 'Galaxy S9',
                color: 'black'
            };
            resolve(phone);
        } else {
            const reason = new Error('Not enough money.');
            reject(reason);
        }
    }
);

Для создания нового Promise необходимо вызвать его с ключевым словом new.
Функция-конструктор, как параметр, принимает функцию, в которою автоматически передаются функции resolve и reject , которые вызываются при успешном выполнении операции или ошибке соответственно. Каждая из этих функций принимает результат выполнения операции. Например, после успешного запроса к серверу, результат этого запроса передается в resolve, после чего с ним далее можно будет работать. Конкретно в этом примере в resolve передается объект с данными, а в reject объект с ошибкой.
Последующую обработку данных можно совершить в методах then и catch . Для обработки успешного выполнения используется then, а ошибки – catch. Рассмотрим это на примере.

const enoughSalary = true;

const buyNewPhone = new Promise(
    function(resolve, reject) {
        if (enoughSalary) {
            const phone = {
                brand: 'Samsung',
                model: 'Galaxy S9',
                color: 'black'
            };
            resolve(phone);
        } else {
            const reason = new Error('Not enough money.');
            reject(reason);
        }
    }
);

buyNewPhone
    .then(function(data) {
        console.log(`I've bought ${data.color} ${data.brand} ${data.model}.`);
    });

Как видно, Promise поддерживают последовательную цепочку вызовов (chaining).
В переменную buyNewPhone записывается объект (типа Promise), результат выполнения функции конструктора Promise, после чего, данные переданные в resolve (или reject) автоматически попадают в коллбек функцию метода then (или catch при ошибке).
Эмуляция ошибки.

const enoughSalary = false;

const buyNewPhone = new Promise(
    function(resolve, reject) {
        if (enoughSalary) {
            const phone = {
                brand: 'Samsung',
                model: 'Galaxy S9',
                color: 'black'
            };
            resolve(phone);
        } else {
            const reason = new Error('Not enough money.');
            reject(reason);
        }
    }
);

buyNewPhone
    .then(function(data) {
        console.log(`I've bought ${data.color} ${data.brand} ${data.model}.`);
    })
    .catch(function(error) {
        console.log(error);
    });

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

const enoughSalary = true;

const buyNewPhone = new Promise(
    function(resolve, reject) {
        if (enoughSalary) {
            const phone = {
                brand: 'Samsung',
                model: 'Galaxy S9',
                color: 'black'
            };
            resolve(phone);
        } else {
            const reason = new Error('Not enough money.');
            reject(reason);
        }
    }
);

buyNewPhone
    .then(function(data) {
        console.log(`I've bought ${data.color} ${data.brand} ${data.model}.`);
        return `I'm very happy!`;
    })
    .then(function(data) {
        console.log(data);
    })
    .catch(function(error) {
        console.log(error);
    });

Существует понятие «промисификация» , которое обозначает, что асинхронное действие «заворачиваем» в Promise, после чего можно использовать чейнинг.

const url = 'https://jsonplaceholder.typicode.com/posts?userId=1';
const makeRequest = new Promise(function(resolve, reject) {
    const xhr = new XMLHttpRequest();
    xhr.open('GET', url, true);

    xhr.onload = function() {
        if (xhr.status === 200) {
            resolve(xhr.response);
        } else {
            reject('Error');
        }
    };

    xhr.send();
});

makeRequest
    .then((data) => {
        console.log(data);
    })
    .catch((error) => {
        console.log(error);
    });

Кроме того, существует сокращенная запись для возврата resolve или reject состояния: Promise.resolve() и Promise.reject() соответственно. При использовании такого синтаксиса из функции будет возвращен выполненный Promise с результатом resolve или reject.

const getMessage = function (phone) {
    const message = 'Resolved Promise.';

    return Promise.resolve(message);
};

getMessage()
    .then((data) => {
        console.log(data);
    });

Fetch

Глобальный метод fetch позволяет, как и XMLHttpRequest, отправлять асинхронный запрос по сети. Преимуществом fetch является упрощенный синтаксис, он возвращает Promise, позволяет легче конфигурировать запросы.
Метод fetch принимает два параметра, первый, обязательный, URL для запроса, второй, необязательный – настройки запроса.
Для отправки GET запроса достаточно указать URL.

fetch('https://jsonplaceholder.typicode.com/posts?userId=1')
    .then((response) => {
        return response.json();
    })
    .then((data) => {
        console.log(data);
    });

Важно отметить, что в первый then попадает не сам ответ от сервера, а Promise, таким образом в нем мы обрабатываем его, а уже следующий then получит непосредственно ответ от сервера.
Так как запрос был на получение JSON, ответ обрабатывался с помощью json() , ккроме него, для обработки ответов существует text() и много других методов.
Из ответа от сервера можно получить и другую информацию.

fetch('https://jsonplaceholder.typicode.com/posts?userId=1')
    .then((response) => {
        console.log(response.headers.get('Content-Type'));
        console.log(response.status);
        console.log(response.url);
    });

Для отправки POST запроса, следует определить второй параметр fetch.

const options = {
    method: 'post',
    headers: {
        'Content-type': 'application/x-www-form-urlencoded; charset=UTF-8'
    },
    body: 'title=foo&body=bar&userId=1'
}

fetch('https://jsonplaceholder.typicode.com/posts', options)
    .then((response) => {
        return response.json();
    })
    .then((data) => {
        console.log('Request succeeded with JSON response', data);
    })
    .catch((error) => {
        console.log('Request failed', error);
    });

Async/await

Конструкция async/await позволяет асинхронные функции.
Такая функция возвращает Promise.
Ключевое слово async , написанное перед функцией, определяет, что функция является асинхронной (такая функция возвращает Promise) и позволяет использовать внутри нее оператор await , который приостанавливает выполнение функции, на время получения результата. Приостановленная с помощью await функция, так же должна возвращать Promise.

const getProducts = () => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(['bread', 'water', 'oil']);
        }, 2000);
    });
};

const buy = (products) => {
    const msg = `You bought: ${products.length} products.`;
    return Promise.resolve(msg);
}

async function order() {
    let products = await getProducts();
    let orderMessage = await buy(products);

    return orderMessage;
};

order()
    .then((response) => {
        console.log(response);
    });
// You bought: 3 products.

Функция order является асинхронной, внутри нее вызываются внешние функции, первая из которых имитирует запрос к серверу с задержкой в две секунды. С помощью ключевого слова await выполнение функции order приостанавливается до момента получения результата от функции getProducts. Далее, после получения результата от getProducts, он передается в следующую функцию buy, ожидание ответа от которой так же приостанавливает выполнение функции order. После выполнения всех внутренних функций, результат order можно обработать через уже известный then, благодаря тому, что функция вернула Promise.
Данный код выглядит как синхронный, но на самом деле все функции с ключевым словом await выполняются асинхронно, одна за другой.
Попробуем выполнить данный код без async/await.

const getProducts = () => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(['bread', 'water', 'oil']);
        }, 2000);
    });
};

const buy = (products) => {
    return `You bought: ${products.length} products.`;
}

function order() {
    let products = getProducts();
    let orderMessage = buy(products);

    return orderMessage;
};

const done = order();
console.log(done);
// You bought: undefined products.

Так как «ответ от сервера» в виде выполнения функции getProducts еще не получен, не дождавшись ее результата, функция order была выполнена и результат получился некорректным.

ДОМАШНЕЕ ЗАДАНИЕ

Видеоинструкция к домашнему заданию

Цель: Научится работать с Backend частью приложения, выполнять AJAX запросы, получать и сохранять данные. Реализовать CRUD с помощью простого RESTApi

Описание задания:
Вам необходимо завершить предыдущее домашнее задание переписав все что у вас есть с использованием классов ES6 вместо функций конструкторов.
Все данные теперь необходимо брать с Backend (об этом далее). Так же для каждого элемента галереи необходимо сделать все CRUD операции, а именно

  • • create — создать новый элемент галереи (изменить реализацию
  • • read — отобразить элемент (уже есть)
  • • update — обновить элемент галереи (не реализовано)
  • • delete — удалить элемент галереи (изменить реализацию)

Работа с RESTApi рассмотрена в видео инструкции, описать ее текстом в полной мере достаточно сложно. Для реализации Backend используется Json Server. Специальное приложение на node.js Документация расположена здесь: https://github.com/typicode/json-server
Заготовка с Backend частью доступна по ссылке.

Пошаговый алгоритм. Базовое задание:

  1. Переписываете весь функционал который у вас есть с функций конструкторов на ES6 классы без изменения самой логика
  2. Менять общую структуру кода и логику не нужно. Разделение по модулям с использованием MVC будет реализовано в следующем ДЗ
  3. Скачать заготовку и разархивировать архив.
  4. Установить Node.js если он у вас не установлен
  5. Открыть консоль и зайти внутрь папки, которую только что разархивировали.
  6. Установить ‘json-server’, который можно найти в заготовке с помощью команды
    «npm install -g json-server»
  7. Запускаете сервер который можно найти в заготовке с помощью команды
    «node index.JavaScript»
  8. Открыть адрес http://localhost:3000 и убедиться что ваше приложение запущено
  9. Открыть адрес http://localhost:3000/cars и убедиться что список элементов загружается
  10. Используя информацию из документации и приложение Postman протестировать работоспособность вашего RESTApi
  11. Внести в файл db.json данные для вашей галереи. Структура файла находится в заготовке. Данные в ваших предыдущих домашних заданиях.
  12. Получить все данные не из глобального объекта data, а с сервера с помощью функции fetch(). Например для получения списка элементов галереи можно обратиться по адресу http://localhost:3000/cars
  13. После получения данных из сервера они сохраняются в объект с данными (например data) и доступны как раньше.

Пошаговый алгоритм. Расширенное задание:

  1. Изменить реализацию «создания» нового элемента галереи. По нажатию на кнопку открывается форма где можно ввести все необходимые данные в соответствующие input поля.
  2. После нажатия на кнопку «Создать», отсылает соответствующий запрос на сервер и в случае получения успешного кода ответа, перестраиваете список заново. Новый элемент отображается на экране.
  3. При нажатии на кнопку «редактировать/обновить» на конкретном элементе, открывается форма с input полями. Поля заполнены данными, которые вы собираетесь редактировать.
  4. После нажатия на кнопку «Обновить», отсылает соответствующий запрос на сервер и в случае получения успешного кода ответа, перестраиваете список заново. Новый элемент отображается на экране.
  5. Изменить реализацию «удаления» нового элемента галереи. После нажатия на кнопку «Удалить», отсылает соответствующий запрос на сервер и в случае получения успешного кода ответа, перестраиваете список заново. Новый элемент отображается на экране.

Пошаговый алгоритм. Сложное задание:

  1. Заменяете механизм «залогинивания» из предыдущего задания на новый.
  2. Снимаем значения с полей «Логин» и «Пароль» при нажатии на кнопку «Войти»
  3. Формируем объект из этих двух полей и отправляем его на сервер в виде POST запроса на адрес http://localhost:3000/login
  4. В теле запроса передать логин и пароль
  5. Модифицировать код в файле index.js и добавить проверку на правильность логина и пароля
  6. В случае успешного совпадения вернуть сообщение на клиент и авторизировать пользователя как раньше.
  7. В дальнейшем и далее использовать localStorage при обновлении страницы (используем новый подход один раз при первом заходе)

ПАТТЕРНЫ ПРОЕКТИРОВАНИЯ

Паттерны (шаблоны) проектирования – это типовые подходы к решению задач возникающих в процессе проектирования вашего приложения. Паттерны предлагают определенную идею или схему как можно решить ту
или иную задачу.

Следует понимать, что не существует «правильной» реализации паттерна.
Конкретная реализация может отличаться, но она должна следовать основным критериям (идеям) паттерна.

Большинство из них разрабатывалось не для JavaScript, по этому их реализация и понимание может сильно варьироваться.
Существует четыре типа шаблонов:

  • • порождающие (Creational);
  • • структурные (Structural);
  • • поведенческие (Behavioral);
  • • архитектурные (Architectural).

Порождающие – описывают подходы и механизмы инициализации объектов.

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

Поведенческие – описывают различные взаимодействия между объектами и структурами.

Архитектурные – описывают общую архитектуру приложения в целом.

К порождающим относятся такие паттерны:

  • • Builder
  • • Factory
  • • Prototype
  • • Singleton

Структурные паттерны:

  • • Module
  • • Adapter
  • • Bridge
  • • Composite
  • • Decorator
  • • Facade
  • • Proxy

Поведенческие паттерны:

  • • Chain of Responsibility
  • • Command
  • • Iterator
  • • Mediator
  • • Memento
  • • Observer
  • • Strategy

Архитектурные паттерны:

  • • Model View Controller
  • • Model View Presenter
  • • Model View ViewModel

Module

Структурный паттерн, который позволяет изолировать (инкапсулировать) ваш код в отдельный модуль и предотвращать попадание в глобальный контекст приватных методов и переменных, где они могут конфликтовать с другими интерфейсами. Паттерн «модуль» возвращает только публичную часть API, оставляя всё остальное доступным только внутри замыканий. Для реализации чаще всего используется Immediately-Invoked-Function-Expressions (IIFE)

let Cafee = (function() {
    let money = 10;
    // приватные методы
    let countMoney = function(value) {
        console.log('Кофе успешно продано');
        money += value;
    };
    let getCoffee = function() {
        console.log('Возьмите ваше кофе!');
    };
    let payForCoffee = function(value) {
        countMoney(value);
    };
    // публичный интерфейс
    return {
        getCoffee: getCoffee,
        payForCoffee: payForCoffee
    };
})();

Caffe.getCoffee();
Caffe.payForCoffee(5);

Реализация модуля с помощью механизма export/import, который был внедрен в ES6.

// экспортируем listService, который содержит в себе публичный интерфейс
let listService = (function(){
    function externalMethod() {
        // some code
    }
    return {
        externalMethod: externalMethod,
    }
}());

export default listService;

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

import listService as service from './service';

service.externalMethod();

К сожалению далеко не все браузеры поддерживают механизм export/import, поэтому необходимо использовать транспайлеры, например Babel.JS.

Prototype

Создание нового объекта не с помощью конструктора, а путем копирования из специальной «заготовки».

function User(name, role) {
    this.name = name;
    this.role = role;
    this.say = function () {
        console.log(`My name is ${this.name}. My role is ${this.role}.`);
    };
}

function UserPrototype(proto) {
    this.proto = proto;
    this.clone = function () {
        let user = new User();
        user.name = proto.name;
        user.role = proto.role;
        return user;
    };
}

let defaultUser = new User("John", "admin");
let prototype = new UserPrototype(defaultUser);
let newUser = prototype.clone();
newUser.say();

Singleton

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

let Singleton = (function () {
    let instance;
    function createInstance() {
        return new Object('I am the instance');
    }
    return {
        init: function () {
            if (!instance) {
                instance = createInstance();
            }
            return instance;
        }
    };
})();

function run() {
    let instance1 = Singleton.init();
    let instance2 = Singleton.init();
    console.log('Equals ' + (instance1 === instance2));
}
run();

Factory

Структура, которая умеет создавать объекты по заданным параметрам.

Основная идея в том, что мы создаем фабрику, которая может создавать нам объекты.

И сразу же возникает вопрос, а почему мы не можем просто использовать оператор new, чтобы создавать объекты? Есть ситуации, когда мы хотим скрыть снаружи реализацию создания объекта и в этом случае нам поможет паттерн Factory.

Разберем на простом примере:

function Factory() {
    this.createCar = function (model) {
        let car;
        if (model === "bmw") {
            car = new BMW();
        } else if (model === "porshe") {
            car = new Porshe();
        } else if (model === "mercedes") {
            car = new Mercedes();
        }
        car.model = model;
        car.getInfo = function () {
            console.log(`This is ${this.model}. It's a ${this.type} car.`);
        }
        return car;
    }
}

let BMW = function () {
    this.type = "fast";
};
let Porshe = function () {
    this.type = "expencive";
};

Observer

Создание связи «один ко многим». Когда в одном объекте (Subject) выполняется специальный метод, все подписчики (observers) оповещаются об этом автоматически.
Все события, возникающие в броу­зерах (mouseover, keypress и другие), являются примерами реализации этого шаб­лона. Этот шаб­лон иногда называют механизмом собствен­ных событий, то есть событий, которые возбуждаются программно, в отличие от тех, что возбуждаются броузером.
Вместо вызова одним объектом метода другого объекта некоторый объект подписывается на получение извещений об определенных событиях от другого объекта. Подписчик также называется наблюдателем,
а объект, за которым ведется наблюдение, называется издателем, или объектом наблюдения. Издатель оповещает (вызывает) всех подписчиков о наступлении какого-то события, и часто сообщение передается им в форме объекта события.

const Subject = function() {
    this.observers = [];
    this.subscribeObserver = observer => {
        this.observers.push(observer);
    }
    this.notifyObserver = (observer, data) => {
        let index = this.observers.indexOf(observer);
        if (index > -1) {
            this.observers[index](data);
        }
    }
    this.notifyObservers = data => {
        this.observers.forEach(observer => {
            observer(data);
        });
    }
};

const subject = new Subject();
subject.subscribeObserver(data => {
    console.log(“Notification: "+ data);
});

subject.subscribeObserver(() => {
    console.log("Do other stuff");
})

subject.notifyObservers("msg");

Facade

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

let DOMControls = {
    getElement: function(selector) {
        if (typeof document.querySelector == 'function') {
            document.querySelector(selector);
        } else {
            document.getElementById(selector);
        }
    }
};

DOMControls.getElement("#wrapper");

Decorator

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

(function() {
let UserProfile = function(name) {
    this.name = name;
    this.login = function() {
        console.log("User: " + this.name + " is logged in");
    };
}

let AdminProfile = function(user, isAdmin, loginType) {
    this.name = user.name;
    this.isAdmin = isAdmin;
    this.loginType = loginType;
    this.originalLogin = user.login;
    this.login = function() {
        console.log("Admin User: " + this.name + ", Is admin: " +
            this.isAdmin + ", Login type: " + this.loginType);
    };
}

let user = new UserProfile("Connor");
user.login();

let decoratedUser = new AdminProfile(user, true, "OAuth");
decoratedUser.originalLogin();
decoratedUser.login();
}());

АРХИТЕКТУРНЫЕ ШАБЛОНЫ

MVC (Model View Controller) – архитектурный шаблон который разграничивает логику приложения по трём основным компонентам.

Модель – отвечает за работу с данными. Выполнение запросов на сервер и при необходимости преобразование полученных данных. Преимущество шаблона MVC в том, что все взаимодействие с источником данных, сосредоточено в одном месте. Такой подход помогает разработчикам, которые не знакомы с проектом, разобраться в нём.

Представление – взаимодействует с DOM. В MVC только представление отвечает за изменения DOM. Представление может выполнять подключение обработчиков событий пользовательского интерфейса, но обработка событий чаще функция контроллера. Основной задачей представления является управление тем, что пользователь видит на экране.

Контроллер – занимается обработкой событий и служит посредником между представлением и моделью. Контроллер является входной точкой для событий и единственным посредником между представлением и моделью (данными).

Одна из возможных реализаций паттерна MVC, когда Модель сообщает об изменениях Представление.

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

MVVM (Model-View-ViewModel) — другой архитектурный шаблон программирования, основанный на MVC характерной особенностью которого является двухсторонняя коммуникация с представлением.
ViewModel – свойства View совпадают со свойствами ViewModel. Модель Представления (ViewModel) является, с одной стороны, абстракцией Представления, а с другой, предоставляет обёртку данных из Модели, которые подлежат связыванию. То есть, она содержит Модель, которая преобразована к Представлению, а также содержит в себе команды, которыми может пользоваться Представление, чтобы влиять на Модель.
Изменение состояния ViewModel автоматически изменяет View и наоборот, поскольку используется механизм связывания данных (bindings);
Один экземпляр ViewModel связан с одним View.

Рассмотрим работу шаблона MVC с помощью следующего демо приложения

Для работы приложения необходим запущенный сервер.
Для успешного запуска приложения, скачайте архив, разархивируйте его, перейдите в директорию приложения и выполните команду npm start используя консоль или командную строку.
Результатом того, что сервер успешно запущен будет строка «Express server listening on port 1337»
В адресной строке браузера введите: http://localhost:1337/

Рассмотрим пример контроллера реализованного с помощью классов ES6:

(function() {

    class Controller { 
        constructor(model, view, observer) {
            this.model = model;
            this.view = view;
            this.observer = observer;
        }        
        bindEvents() {
            this.view.DOMElements.saveBtn.addEventListener("click", () => {
                let item = this.view.getItemToSave();
                this.model.saveData(item).then(data => this.view.setSavedData(data));
            });
            this.view.DOMElements.refreshBtn.addEventListener("click", () => {
                let count = this.view.counter++;
                this.observer.callEvent("update", count);
            });
        }

        bindSubscribers() {
            this.observer.subscribeEvent("update", (count) => {
                this.model.updateData(count).then((data) => {
                    this.view.setUpdatedData(data);
                });    
            });     
        }  
        
        initView(data) {
            if(!this.view.isReady()) {
                this.view.init(data);   
            }
            //this.view.isReady() || this.view.init(data);
        }
        
        init() {
            this.model.getData().then((data) => {
                this.initView(data);
                this.bindSubscribers();
                this.bindEvents();
            });    
        }
        
    }

    window.app = window.app || {};
    window.app.Controller = Controller;

}());

В качестве параметров контроллер принимает model, view и утилитный объект observer.
В контроллере реализован метод init, который обращается к модели для получения данных.
Методы initView(), bindSubscribers() и bindEvents() вызовутся только после того как данные будут получены.
Метод initView вызывает метод init класса View и передает в него данные. Проверка с помощью метода isReady обычно используется для избежания повторного назначения обработчиков событий.

Метод bindEvents() используется для установки обработчиков событий на полученные из View DOM элементы.
При клике на кнопку сохранить происходит вызов колбек функции обработчика, в которой снимаются данные с View (это могли быть поля формы) и передаются в Model для сохранения. Метод model.saveData() после успешного сохранения, возвращает promise. С помощью .then данные, которые вернул класс Model передаются во View для отображения.

При клике на кнопку обновить запускается другой метод взаимодействия компонентов, который позволяет оповестить View об изменениях в Model. Для его реализации используется паттерн Pub/Sub (Publisher/Subscriber). При вывозве this.observer.callEvent(«update», count), вызывается метод с именем события «update» хранящаяся в объекте observer.
Сохранение этого события и функции, которая в последствии будет выполнена, происходит в методе bindSubscribers().

Как видно из примера контроллер выполняет управляющие функции.

В Модели реализованы методы для работы с данными:

(function(){
    
    class Model {
        constructor(url) {
            this.getUrl = url;
        }
   
        getData() {
            return fetch(this.getUrl).then(responce => responce.json())
            .then(data => {
                console.log("Initial data is loaded");
                return data;
            })         
        }
        
        saveData(item) {         
            console.log("Saving item... " + item.name);
            let iphone = {
                "name": "Saved iPhone",
                "price": 12458,
                "popular": true,
                "date": 1467440203
            }
            return new Promise(
                function(resolve, reject) {            
                    resolve(iphone);          
                }
            );
        }
        
        updateData(counter) {
            console.log("Updating item... " + counter);
            let samsung = {
                "name": "Saved Samsung",
                "price": 12458,
                "popular": true,
                "date": 1467440203
            }
            return Promise.resolve(samsung);
        }
    
    }
    
    window.app = window.app || {};
    window.app.Model = Model;
    
}());

Когда вызван метод getData() , с помощью fetch мы получаем данные по ссылке переданной в класс Model.
Далее после получения данных происходит их преобразование в объект JSON и данные возвращаются в виде массива объектов.
Методы saveData() и updateData() схожи и как упоминалось ранее возвращают promise.

Для работы с DOM, получение данных с форм, вывода и обновления информации в HTML используется View:

(function() {

    function View () {        
        this.DOMElements = {
            saveBtn     : document.querySelector("#saveBtn"),
            refreshBtn  : document.querySelector("#refreshBtn")
        };

        this.ready = false;
        this.counter = 0;
    }
    
    View.prototype = {
        
        init : function (items) {
            this.items = items;
            this.buildView();
            this.ready = true;
        },
        
        buildView : function () {
            console.log("View is ready");
            console.log(this.items);
        },

        getItemToSave : function(){
            let item = this.items[0];
            item.name = "iPhone";
            return item;
        },

        setSavedData : function (data) {
            console.log("View item is successfully saved!");
            console.table(data);
        },

        setUpdatedData : function (data) {
            console.log("View item is successfully updated!");
            console.table(data);
        },

        isReady : function (){
            return this.ready;
        }
    }
    
    window.app = window.app || {};
    window.app.View = View;

}());

Класс View также и остальные компоненты содержит метод init, который получает данные и запускает метод buildView() для вывода полученной информации в консоль, а также устанавливается инициализационное свойство ready.
Метод getItemToSave() позволяет получить элемент массива, который необходимо сохранить.
setSavedData() и setUpdatedData() являются коллбек методами сообщающими об успехе сохранения и обновления данных с выводом их в консоль.
Метод isReady() возвращает свойство ready.
Эти примеры наглядно демонстрируют взаимодействие компонентов между собой.

Класс Observer имеет 2 метода:
subscribeEvent() — сохраняет в качестве объекта имя события и его метод
callEvent() — выполняет метод с полученным в качестве параметра именем

(function() {

    class Observer {
        constructor() {
            this.events = {};
        }

        subscribeEvent(name, func) {
            this.events[name] = func;
        }
        
        callEvent(name, arg) {
            if (this.events[name]) {
                this.events[name](arg);
            }
        }
    }

    window.app = window.app || {};
    window.app.Observer = Observer;

}());

Подробное описание методов и взаимодействия компонентов вы можете найти в видеоматериалах к лекции.o2o3o3o4

Домашнее задание:

Цель: Научится реализовывать MVC паттерн на практике. Приводить код в порядок и правильно его структурировать. Научиться подписываться на события Представления в Контроллере, вызывать методы в Модели и с помощью Promise получать ответ из Модели назад в Контроллер.

Важно: перед началом реализации задания необходимо посмотреть сессию №35 «Архитектурный шаблон MVC». Из нее вы узнаете как реализовывать приложения с использованием MVC.

Пример применения паттерна MVC

Описание задания:
• Вам необходимо переделать предыдущее домашнее задание переписав его с использованием MVC паттерна.
• Вся логика для работы с базой должна находиться только в Модели (Model)
• Все взаимодействия с HTML и все ссылки на DOM элементы должны находиться только в Представлении (View)
• Контроллер содержит в себе ссылки на Модель и Представление и отвечает за их взаимодействие между собой.
• Для реализации взаимодействия необходимо выбрать один из предложенных подходов

  • с помощью Promise
  • с помощью утилитного класса Observer

То есть все взаимодействия в Контроллере должны быть реализованы только одним способом. Узнать как работают эти способы можно из Сессии №35 «Архитектурный шаблон MVC»

Продвинутая версия:

Необходимо реализовать механизм роутинга (ruoting mechanizm)
Этот механизм позволяет, в зависимости от того какое значение указанно в адресе страницы, загружать то либо иное состояние вашего приложения.
Фактически, в зависимости от изменения хеша в url страницы, вы загружаете соответственно тот либо другой комплект model, view, controller. Каждый из таких комплектов отвечает за свою часть функциональности приложения.
Это позволит вам в какой-то мере имитировать полноценное SPA предложение.
Более детальное описание можно найти в соответствующем видео. Примеры кода, включая разделение на отдельные комплекты model, view, controller, можно скачать в архиве по ссылке.

4 Likes