Книга «JavaScript с нуля до профи»

imageПривет, Хаброжители!

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

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

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

Для кого эта книга

Для комфортного знакомства с книгой не требуется никакого опыта в JavaScript. Конечно, упражнения дадутся немного легче, если вы хотя бы немного умеете программировать. Знакомство с основами HTML и CSS будет вашим преимуществом. Если вы начинающий программист, для нас большая честь поприветствовать вас в мире программирования. Вначале он может показаться сложным, но мы проведем вас через все трудности.

Объектно-ориентированное программирование


Прежде чем мы начнем погружаться прямиком в увлекательные классы, давайте кратко поговорим об объектно-ориентированном программировании (ООП). ООП — очень важная парадигма программирования, в которой код структурируется в виде объектов, что приводит к более удобному обслуживанию и многократному использованию кода. Работа с ООП учит вас по-настоящему продумывать всевозможные ключевые моменты в объектах, объединив свойства таким образом, чтобы их можно было сгруппировать в схеме, называемой классом, который, в свою очередь, может наследовать свойства родительского класса.

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

Данный подход очень важен для Java.NET и других классических объектно-ориентированных способов написания кода. JavaScript не обязательно вращается вокруг объектов. Они нам понадобятся, и мы будем их использовать, но они, так сказать, не являются звездами нашего кода.

Классы и объекты


Кратко напомним: объекты представляют собой набор свойств и методов. Мы рассматривали их в главе 3. Свойства объекта должны иметь разумные имена. Так, например, если у нас есть объект person (человек), у него должны быть свойства с названиями age (возраст) и lastName (фамилия) и заданными значениями. Перед вами пример объекта:

let dog = { dogName: "JavaScript",
            weight: 2.4,
            color: "brown",
            breed: "chihuahua"
          };


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

class ClassName {
  constructor(prop1, prop2) {
    this.prop1 = prop1;
    this.prop2 = prop2;
  }
}

let obj = new ClassName("arg1", "arg2");


Код определяет класс с именем ClassName, объявляет переменную obj и делает ее новым экземпляром объекта. Приводятся два аргумента, они будут использоваться конструктором для инициализации свойств. Как вы заметили, параметры для конструктора и свойства класса (prop1 и prop2) носят одинаковые названия. Свойства класса можно распознать по ключевому слову this перед ними. Ключевое слово this относится к объекту, которому принадлежит, поэтому является первым свойством экземпляра ClassName.

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

function Dog(dogName, weight, color, breed) {
  this.dogName = dogName;
  this.weight = weight;
  this.color = color;
  this.breed = breed;
}

let dog = new Dog("Jacky", 30, "brown", "labrador");


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

class Dog {
  constructor(dogName, weight, color, breed) {
    this.dogName = dogName;
    this.weight = weight;
    this.color = color;
    this.breed = breed;
  }
}

let dog = new Dog("JavaScript", 2.4, "brown", "chihuahua");


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

console.log(dog.dogName, "is a", dog.breed, "and weighs", dog.weight, "kg.");


Результат вывода на экран:

JavaScript is a chihuahua and weighs 2.4 kg.


В следующем разделе мы погрузимся во все детали классов.

Классы


Вы можете задаться вопросом: если классы делают то же самое, что и простое определение объекта, зачем они вообще нужны? Ответ заключается в том, что классы, по сути, являются схемами для объектов. Если нужно создать 20 записей собак, придется писать гораздо меньше кода, когда у нас есть класс dog. При описывании каждого объекта по отдельности нам придется каждый раз указывать имена всех свойств. И можно легко сделать опечатку или неправильно прописать свойство. Классы пригодятся в подобных ситуациях.

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

Давайте рассмотрим различные элементы классов.

Метод constructor


Особый метод constructor используется для инициализации объектов с помощью классов. В классе может быть только один метод constructor. Он содержит свойства, которые будут установлены при инициализации класса.

Ниже вы можете рассмотреть пример использования метода constructor при создании класса Person:

class Person {
  constructor(firstname, lastname) {
    this.firstname = firstname;
    this.lastname = lastname;
  }
}


Под внешней оболочкой JavaScript инициализирует специальную функцию, основанную на методе constructor. Эта функция получает имя класса и создает объект с заданными свойствами. С помощью данной специальной функции вы можете прописывать экземпляры (объекты) класса.

А теперь перед вами код, создающий новый объект на основе класса Person:

let p = new Person("Maaike", "van Putten");


Именно слово new дает понять JavaScript, что требуется искать специальную функцию конструктора в классе Person и создавать новый объект. Конструктор вызывается и возвращает экземпляр объекта person с заданными свойствами. Этот объект сохраняется в переменной p.

Если использовать новую переменную p для логирования, можно увидеть, что свойства действительно установлены:

console.log("Hi", p.firstname);


Так будет выглядеть результат:

Hi Maaike


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

let p = new Person("Maaike");


Многие языки программирования выдали бы ошибку, но не JavaScript. Он просто присвоит оставшимся свойствам значение undefined. Давайте понаблюдаем за тем, что происходит, логируя результат:

console.log("Hi", p.firstname, p.lastname);


В консоли отобразится следующая запись:

Hi Maaike undefined


В методе constructor вы можете задать значения по умолчанию. Реализовать это можно следующим образом:

constructor(firstname, lastname = "Doe") {
    this.firstname = firstname;
    this.lastname = lastname;
  }


В результате на экран будет выведено не Hi Maaike undefined, а Hi Maaike Doe.

Практическое занятие 7.1

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

1. Создайте класс Person, включающий конструктор для firstname и lastname.
2. Создайте переменную и присвойте значение новому объекту Person, используя имя и фамилию вашего первого друга.
3. Теперь добавьте вторую переменную с именем второго друга, используя его имя и фамилию.
4. Выведите на экран обе записи с приветствием hello.

Методы


В классе можно указывать функции. Благодаря этому наш объект может начать выполнять действия, используя собственные свойства, — например, выводить имя на экран. Функции в классе называются методами. Для определения таких методов не используется ключевое слово function. Мы сразу начинаем писать имя:

class Person {
  constructor(firstname, lastname) {
    this.firstname = firstname;
    this.lastname = lastname;
  }

  greet() {
    console.log("Hi there! I'm", this.firstname);
  }
}


Вызвать метод greet для объекта Person можно следующим образом:

let p = new Person("Maaike", "van Putten");
p.greet();


Полученный результат:

Hi there! I'm Maaike


В классе можно указывать любое количество методов. В данном примере мы используем свойство firstname. Мы вызываем его с помощью this.property. Для чело­века с другим значением firstname, например Rob, на экран будет выведено следующее:

Hi there! I'm Rob


Методы, как и функции, могут принимать параметры и возвращать результаты:

class Person {
  constructor(firstname, lastname) {
    this.firstname = firstname;
    this.lastname = lastname;
  }

  greet() {
    console.log("Hi there!");
  }

  compliment(name, object) {
    return "That's a wonderful " + object + ", " + name;
  }
}


Функция compliment сама не выводит никаких значений, поэтому мы логируем ее:

let compliment = p.compliment("Harry", "hat");
console.log(compliment);


Результат будет следующим:

That's a wonderful hat, Harry


В данном случае мы отправляем параметры методу, потому что обычно не хвалим сами себя (отличное предложение, Майке!). Однако всякий раз, когда метод не требует внешнего ввода, а только свойств объекта, никакие параметры не будут работать, и метод может использовать именно свойства объекта. Выполним упражнение, а затем перейдем к использованию свойств классов вне класса.

Практическое занятие 7.2

Получите полное имя своего друга.

1. Используйте класс Person из практического занятия 7.1, добавив метод с названием fullname, который будет возвращать совокупное значение firstname и lastname.
2. Создайте значения для person1 и person2, используя фамилии и имена друзей.
3. Используя метод fullname внутри класса, верните полное имя одного или обоих человек.

Свойства


Свойства (иногда называемые полями) содержат данные класса. В конструкторах мы уже видели один тип свойств:

class Person {
  constructor(firstname, lastname) {
    this.firstname = firstname;
    this.lastname = lastname;
  }
}


В данном случае класс Person получает два свойства от конструктора: firstname и lastname. Свойства можно добавлять и удалять, как мы проделывали это с объектами. К свойствам можно получить доступ за пределами класса, мы видели подобное, когда логировали значения вне класса, получая доступ к свойствам в экземпляре:

let p = new Person("Maaike", "van Putten");
console.log("Hi", p.firstname);


Прямой доступ к свойствам лучше не предоставлять. Мы хотим, чтобы класс контролировал значения свойств, по нескольким причинам — например, нам необходимо убедиться, что свойство имеет требуемое значение. Допустим, вы хотите подтвердить, что возраст пользователя больше 18 лет. Можно достичь этого, сделав невозможным прямой доступ к свойству за пределами класса.

Вот как добавить свойства, недоступные извне. Ставим перед ними символ #:

class Person {
  #firstname;
  #lastname;
  constructor(firstname, lastname) {
    this.#firstname = firstname;
    this.#lastname = lastname;
  }
}


В данный момент к свойствам firstname и lastname доступа извне нет. Если попробуем ввести:

let p = new Person("Maria", "Saga");
console.log(p.firstname);


то получим:

undefined


Если бы мы хотели убедиться, что можем создавать объекты только с именами, начинающимися на букву М, мы могли бы немного изменить конструктор:

constructor(firstname, lastname) {
    if(firstname.startsWith("M")){
      this.#firstname = firstname;
    } else {
      this.#firstname = "M" + firstname;
    }
    this.#lastname = lastname;
  }


Теперь при попытке ввести значение firstname, которое не начинается на М, код добавит М в начало. Так, например, в консоли значение следующего имени будет отображено как Mkay:

let p = new Person("kay", "Moon");


Это очень простой пример проверки. На данный момент мы вообще не можем получить доступ к свойству извне класса после конструктора. Оно доступно только изнутри класса. Вот тут-то и вступают в игру геттеры и сеттеры.

Геттеры и сеттеры


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

Аксессоры начинаются с ключевых слов get и set. Хорошей практикой считается максимально закрывать поля и предоставлять доступ к ним с помощью геттеров и сеттеров. Благодаря этому свойства не могут быть заданы извне без контроля самого объекта. Подобный принцип называется инкапсуляцией. Класс инкапсулирует (помещает) данные и объект под контроль собственных полей.

Вот как это реализовать:

class Person {
  #firstname;
  #lastname;
  constructor(firstname, lastname) {
    this.#firstname = firstname;
    this.#lastname = lastname;
  }

  get firstname() {
    return this.#firstname;
  }

  set firstname(firstname) {
    this.#firstname = firstname;
  }

  get lastname() {
    return this.#lastname;
  }

  set lastname(lastname) {
    this.#lastname = lastname;
  }
}


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

let p = new Person("Maria", "Saga");
console.log(p.firstname);


Результат вывода на экран:

Maria


Мы создали новый объект Person с именем Maria и фамилией Saga. На выходе отображается имя, что стало возможным только благодаря наличию у нас средства доступа — геттера. Мы также можем задать значение чему-то другому, используя сеттер. Давайте обновим значение имени так, что теперь это будет не Maria, а Adnane:

p.firstname = "Adnane";


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

set firstname(firstname) {
    if(firstname.startsWith("M")){
      this.#firstname = firstname;
    } else {
      this.#firstname = "M" + firstname;
    }
  }


Теперь код проверит, начинается ли firstname с буквы M. Если да, то значение firstname обновится до значения параметра. Если нет, код добавит M перед параметром.

Обратите внимание, что мы не обращаемся к firstname так, как если бы это была функция. Если вы допишете круглые скобки (()), то получите сообщение об ошибке, говорящей, что это не функция.

Наследование


Наследование — одна из ключевых концепций ООП. Согласно ей классы могут иметь дочерние классы, которые наследуют свойства и методы родительского класса. Например, если вам понадобятся все виды транспортных средств в приложении, вы можете задать класс с названием Vehicle, в котором укажете некоторые общие свойства и методы объектов. Для продолжения можете создавать дочерние классы, основанные на классе Vehicle, например boat, car, bicycle и motorcycle.

Вот сильно упрощенная версия класса Vehicle:

class Vehicle {
  constructor(color, currentSpeed, maxSpeed) {
    this.color = color;
    this.currentSpeed = currentSpeed;
    this.maxSpeed = maxSpeed;
  }

  move() {
    console.log("moving at", this.currentSpeed);
  }

  accelerate(amount) {
    this.currentSpeed += amount;
  }
}


Здесь представлены два метода в классе Vehicle: move и accelerate. Это может быть класс Motorcyle, наследуемый от данного класса с использованием ключевого слова extends:

class Motorcycle extends Vehicle {
  constructor(color, currentSpeed, maxSpeed, fuel) {
    super(color, currentSpeed, maxSpeed);
    this.fuel = fuel;
  }

  doWheelie() {
    console.log("Driving on one wheel!");
  }
}


С помощью ключевого слова extends мы указываем, что определенный класс является дочерним по отношению к другому. В данном случае Motorcycle — дочерний класс Vehicle. Это значит, что у нас будет доступ к свойствам и методам класса Vehicle в классе Motorcycle. Сюда же мы добавили особый метод doWheelie () для описания езды на заднем колесе. Включать данную возможность в класс Vehicle не было смысла, потому что она специфична для определенных транспортных средств.

Слово super в конструкторе вызывает конструктор родителя — в данном случае конструктор Vehicle. Это гарантирует, что поля от родителя также заданы и что методы доступны без необходимости делать что-либо еще: они наследуются автоматически. Вызов super () не является обязательным, но вы должны использовать это ключевое слово при нахождении в классе, который наследуется от другого класса — иначе получите ReferenceError.

В классе Motorcycle у нас есть доступ к полям класса Vehicle, поэтому следующая конструкция будет работать:

let motor = new Motorcycle("Black", 0, 250, "gasoline");
console.log(motor.color);
motor.accelerate(50);
motor.move();


В результате вы увидите:

Black
moving at 50


Мы не можем получить доступ к каким-либо конкретным свойствам или методам класса Motorcycle в классе Vehicle: не все транспортные средства являются мотоциклами, поэтому мы не можем быть уверены, что при описании нашего конкретного объекта понадобятся дочерние свойства или методы.

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

Прототипы


Прототипы — это механизмы JavaScript, которые делают возможным существование объектов.

Если при создании класса никакие детали специально не уточняются, объекты наследуются от прототипа Object.prototype. Среди всех встроенных классов JavaScript, которые мы можем применять, этот довольно сложный. Нет необходимости изучать, как он реализован. Его можно считать базовым объектом, который всегда находится на вершине дерева наследования, а значит, всегда присутствует в наших объектах.

Для всех классов доступно свойство prototype, и оно всегда называется «прототип». Вызвать его можно так:

ClassName.prototype


Давайте приведем пример того, как добавить функцию в класс, используя свойство prototype. Для этого мы будем использовать класс Person:

class Person {
  constructor(firstname, lastname) {
    this.firstname = firstname;
    this.lastname = lastname;
  }

  greet() {
    console.log("Hi there!");
  }
}


И вот как добавить функцию в этот класс, используя prototype:

Person.prototype.introduce = function () {
  console.log("Hi, I'm", this.firstname);
};


Свойство prototype содержит в себе все методы и свойства объекта. Таким образом, добавление функции в prototype — это добавление функции в класс. Вы можете использовать prototype, чтобы добавлять свойства или методы объекту (как мы сделали выше с помощью функции introduce). Данный способ применим и для свойств:

Person.prototype.favoriteColor = "green";


После их можно вызывать из экземпляра класса Person:

let p = new Person("Maria", "Saga");
console.log(p.favoriteColor);
p.introduce();


Мы получим следующий результат:

green Hi, I'm Maria


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

Следовательно, методы и свойства, определенные через prototype, точно такие же, как если бы они были определены в классе. Это означает, что, перезаписывая их для определенного экземпляра, вы не перезаписываете методы и свойства всех экземпляров. Например, если бы у нас был второй объект Person, он мог бы перезаписать значение favoriteColor, и это не изменило бы значение любимого цвета для объекта со значением firstname, равным Maria.

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

Практическое занятие 7.3

Напишите класс, содержащий свойства для разных животных — в том числе звуки, издаваемые каждым видом — и создайте два (или более) объекта.

1. Создайте метод, который выводит на экран название этого животного и звук, который оно издает.
2. Добавьте прототип с другим действием для животного.
3. Выведите весь объект животного на экран.

Об авторах
Лоренс Ларс Свекис является экспертом в области инновационных технологий, имеет широкий спектр теоретических знаний и реальный опыт в области веб-разработки. Начиная с 1999 года участвует в различных веб-проектах, как крупных, так и небольших. Преподает с 2015 года и обожает воплощать идеи в онлайн-жизнь. Для Лоренса учить и помогать людям — потрясающая возможность, поскольку ему нравится делиться знаниями с обществом.

Майке ван Путтен известна страстью к обучению и разработке программного обеспечения, стремлением сопровождать людей на их пути к новым высотам в карьере. К числу ее любимых языков относятся JavaScript, Java и Python. Как разработчик участвует в проектах по созданию программного обеспечения и как тренер — в различных сферах, начиная с IT для «чайников» и заканчивая продвинутыми темами для опытных специалистов. Любит создавать образовательный онлайн-контент на различных платформах, предназначенный для широкой аудитории.

Роб Персиваль — уважаемый веб-разработчик и преподаватель Udemy с аудиторией более 1,7 миллиона учеников. Более 500 000 из них приобрели его «Полный курс веб-разработчика 2.0», а также курсы разработчиков Android и iOS.


О научном редакторе
Крис Минник — активный автор, блогер, тренер, спикер и веб-разработчик. Его компания WatzThis? занимается поиском лучших способов обучения новичков навыкам обращения с компьютером и программированию.

Крис более 25 лет трудится full-stack-разработчиком и более десяти лет — преподавателем. Обучает веб-разработке, ReactJS и продвинутому JavaScript во многих крупнейших мировых компаниях, а также в публичных библиотеках, коворкингах и на личных встречах.

Крис Минник — автор и соавтор более десятка технических изданий для взрослых и детей, включая React JS Foundations, HTML and CSS for Dummies («HTML и CSS для чайников»), Coding with JavaScript for Dummies («JavaScript для чайников»), JavaScript for Kids, Adventures in Coding и Writing Computer Code.


Более подробно с книгой можно ознакомиться на сайте издательства:
» Оглавление
» Отрывок

По факту оплаты бумажной версии книги на e-mail высылается электронная книга.
Для Хаброжителей скидка 25% по купону — JavaScript

© Habrahabr.ru