Как использовать классы в JavaScript – Руководство для начинающих

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

Эта статья для вас, если что-то из этого звучит знакомо:

  • JavaScript - это ваш первый язык программирования.
  • Вы новичок в принципах объектно-ориентированного программирования (ООП) или не совсем знакомы с ними.
  • В основном вы использовали функции для структурирования своего кода на JavaScript.

Если вы согласны с чем-либо из этого, продолжайте читать.

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

содержание

  • Функции, Функции Повсюду, Куда бы я ни повернулся
  • Подождите секунду. Мы говорим, что функции сейчас плохи?
  • Подожди, что? В JavaScript нет реальных классов?
  • Давайте поговорим об объектах в JavaScript.
  • Функции конструктора: Схемы объектов - Давайте перейдем к практике
  • Функции конструктора: Отлично подходят для создания чертежей, но... Пустая трата памяти?
  • Прототипы спешат на помощь (снова): Эффективный обмен методами
  • Функции конструктора + прототипы: Мощное сочетание
  • Наследование с помощью функций конструктора: Передача семейных признаков (способом конструктора)
  • Введите классы ES6: синтаксический сахар для прототипов
  • Классы ES6: Синтаксис классов - Замаскированные прототипы
  • Что будет дальше? Дополнительные возможности класса и реальные примеры
  • Вывод

Функции, Функции Повсюду, Куда бы я ни повернулся

Если вы начинали с JavaScript, то, скорее всего, вы освоились с функциями. Для вас они как строительные блоки для всего, не так ли? Подумайте сами: если бы я попросил вас написать программу для приветствия кого-либо по имени, вы бы, вероятно, в мгновение ока придумали что-то подобное:

function greetUser(userName) {
  alert("Hello, " + userName + "!");
}

greetUser("Alice"); // Like magic! It greets Alice.

Хорошо, давайте немного повысим уровень. Представьте, что я попросил вас написать программу, которая вычисляет год рождения человека, просто зная его возраст. Если им 25 лет, вы бы хотели, чтобы в нем было указано "2000" (при условии, что текущий год - 2025).

Какой была бы ваша первая мысль? Вероятно, что-то вроде: "Время работы!" Я прав? Вы могли бы подумать: "Я напишу функцию, она возьмет возраст и, бум-бум, выдаст год рождения". Понимаете?

Мышление, основанное на функциях. Это совершенно естественно для JavaScript. И вот как вы могли бы это запрограммировать:

function getBirthYear(age) {
  const currentYear = 2025; //  For this example, let's say it's 2025
  const birthYear = currentYear - age;
  return birthYear;
}
console.log(getBirthYear(25)); // Yep, it logs 2000!

А теперь давайте немного усложним задачу. Что, если мы хотим быть немного умнее и убедиться, что возраст действительно соответствует действительности? Ну, знаете, а не какой-то безумной строке или отрицательному числу. Придерживаясь нашего функционального мозга, каков естественный следующий шаг? Конечно, другая функция. Вероятно, мы бы создали функцию validateAge:

function validateAge(age) {
  if (typeof age !== "number" || age <= 0 || age > 120) {
    return "Invalid age";
  } else {
    return age; //  Age is good to go!
  }
}

console.log(validateAge(25)); //  Output: 25 (valid!)
console.log(validateAge("twenty")); //  Output: Invalid age (not a number)
console.log(validateAge(-5)); //  Output: Invalid age (negative)

Видите, как мы просто нагромождаем функции? getBirthYear выполняет одно, validateAge - другое. Это отдельные небольшие блоки кода.

Давайте продвинемся немного дальше. Что, если бы мы также захотели определить чей-то знак зодиака на основе года его рождения? Да, вы угадали - мозг говорит: ”Больше функций". Давайте просто напишем еще одну функцию getZodiacSign.:

function getZodiacSign(birthYear) {
  //  Simplified zodiac for demonstration—not astrologically accurate! 😉
  const signs = [
    "Aries",
    "Taurus",
    "Gemini",
    "Cancer",
    "Leo",
    "Virgo",
    "Libra",
    "Scorpio",
    "Sagittarius",
    "Capricorn",
    "Aquarius",
    "Pisces",
  ];
  return signs[birthYear % 12]; // Simple modulo trick!
}

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

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

function Person(name, age, country, profession) {
  const personName = name;
  const personAge = age;
  const personCountry = country;
  const personProfession = profession;

  const validatedAge = validateAge(personAge);
  const birthYear = getBirthYear(validatedAge);
  const zodiacSign = getZodiacSign(birthYear);

  alert(
    `${personName}, you're ${personAge} years old, born in ${birthYear}, zodiac sign: ${zodiacSign}!`
  );
}

Что, если затем мы захотим использовать имя пользователя в других наших функциях, таких как getZodiacSign или getBirthYear? Нам придется вернуться назад и вручную добавить имя в качестве аргумента для каждой из этих функций. Представьте, что вам приходится обновлять каждую функцию всякий раз, когда вы добавляете новую информацию о персонаже.

//  Suddenly, we need 'name' everywhere!

function getZodiacSign(birthYear, name) {
  alert("Zodiac sign for " + name + " is...");
  //... rest of zodiac logic...
}

function getBirthYear(age, name) {
  alert("Birth year for " + name + " is...");
  // ... rest of birth year logic...
}

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

Подождите секунду. Мы говорим, что функции сейчас плохи?

Функции - это потрясающе. Думайте о таком функционально-ориентированном подходе как о "классическом способе работы с JavaScript". Если вы начинали с JavaScript, это, вероятно, кажется вам совершенно естественным и удобным - и это здорово. Даже такие суперпопулярные современные библиотеки, как React, создаются с использованием функций для компонентов. Функции невероятно мощные и гибкие.

Но даже в React, если вы измените некоторые основные данные (например, "реквизит" в терминах React) в главном компоненте, вам, возможно, придется покопаться во множестве других компонентов, чтобы убедиться, что все по-прежнему работает гладко. Функции - это фантастика, но иногда, для решения определенных задач, может потребоваться другой способ организации нашего кода. Для некоторых людей этот способ кажется более интуитивно понятным, особенно если они не имеют опыта программирования.

Представьте, что вы просите программиста, чьим родным языком был Java или C++, создать программу, посвященную году нашего рождения. У него может проясниться в голове, но, скорее всего, он подумает о чем-то другом. Возможно, что-то вроде этого:

"Нам нужен человек (класс). У человека есть возраст (собственность), и нам нужен способ рассчитать год рождения (действие) для этого человека".

Заметили какие-нибудь изменения? Функции - это не первое, что приходит им в голову. Речь идет скорее об объектах и вещицах, обладающих свойствами и действиями. Сногсшибательно, да? Многие программисты, которые начинали с таких языков, как Java или C++, естественно, мыслят в объектно-ориентированном стиле (или ООП). И, возможно, именно поэтому вы читаете это - возможно, вам тоже интересно изучить этот объектно-ориентированный подход, особенно в JavaScript. Не волнуйтесь, я не прошу вас внезапно переходить на Java".

Итак, об этих классах в JavaScript. Приготовьтесь к небольшому повороту в JavaScript. Дело в том, что в JavaScript технически нет классов, как в таких языках, как Java или C++. Я знаю, это может немного озадачить. Вместо классических классов, которые можно найти в таких языках, как Java или C++, JavaScript построен на так называемых прототипах. Он использует эти гибкие прототипы и объекты для имитации того, как работают классы в других языках. Итак, если вы хотите эффективно использовать классы в JavaScript, самое главное - сначала разобраться в объектах и прототипах. Вот в чем волшебство JavaScript OOP.

Подожди, что? В JavaScript нет реальных классов?

Означает ли это, что мы навсегда останемся только с функциями? Неа. Несмотря на то, что JavaScript работает с прототипами по-своему (вместо классических классов), он по-прежнему полностью поддерживает "объектно-ориентированное программирование" (ООП).

Давайте разберем ООП на простом английском языке. Две важные идеи ООП - это инкапсуляция и наследование. Звучит заманчиво, не так ли? Но на самом деле это довольно простые концепции.

Инкапсуляция? Представьте себе капсулу, например, для лекарств. Вы просто объединяете вещи, которые должны быть вместе. В ООП инкапсуляция означает группировку данных (таких как возраст, имя) и действий, которые вы можете выполнять с этими данными (например, рассчитать год рождения, поприветствовать), внутри одного "объекта". Объекты JavaScript идеально подходят для этого.

И наследование? Представьте, что это наследование черт от вашей семьи. В ООП JavaScript объекты могут "наследовать" свойства и поведение от других объектов. В JavaScript это называется прототипным наследованием, и объект, от которого вы наследуете, называется прототипом (вскоре мы углубимся в prototype).

- видишь? Здесь нет функции jail. JavaScript полностью готов к ООП. Чтобы увидеть это в действии, давайте перепишем нашу программу birth year, но на этот раз используя этот стиль ООП в JavaScript.

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

const Person = {
  //  --- Properties (Data) ---
  name: "Spruce",
  age: 25,
  country: "Nigeria",
  profession: "Engineer",

  //  --- Methods (Actions related to Person data) ---
  isValidAge: function () {
    return typeof this.age === "number" && this.age > 0;
  },

  getBirthYear: function () {
    if (!this.isValidAge()) {
      return "Invalid age!";
    }
    return new Date().getFullYear() - this.age;
  },

  getZodiacSign: function () {
    if (!this.isValidAge()) {
      return "Oops, can't get zodiac for an invalid age!";
    }

    const birthYear = this.getBirthYear();
    const zodiacSigns = [
      "Capricorn",
      "Aquarius",
      "Pisces",
      "Aries",
      "Taurus",
      "Gemini",
      "Cancer",
      "Leo",
      "Virgo",
      "Libra",
      "Scorpio",
      "Sagittarius",
    ];
    return zodiacSigns[birthYear % 12];
  },

  greet: function () {
    return (
      `Hello, I'm ${this.name}. I'm ${
        this.age
      } years old, born in ${this.getBirthYear()}, ` +
      `working as a ${this.profession} from ${
        this.country
      }.  My zodiac sign is ${this.getZodiacSign()}.`
    );
  },
};

//  --- Let's use our Person object! ---
console.log(Person.greet());
//  Output (might vary slightly depending on year):

// "Hello, I'm Spruce. I'm 25 years old, born in 2000, working as a Engineer from Nigeria.  My zodiac sign is Pig."

Видите, как это удобно? Все, что касается человека, его данные (имя, возраст и т.д.) и то, что вы можете с ним сделать (подтвердить возраст, узнать год рождения, поприветствовать), - все это собрано воедино и красиво организовано в этом единственном объекте Person. Это и есть инкапсуляция в действии. Довольно круто, правда?

Итак, хотите узнать имя человека? Очень просто:

console.log(Person.name); // Output: "Spruce"

Год рождения? Кусочек торта:

console.log(Person.getBirthYear()); // Output (if current year is 2025): 2000

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

//  Age is 25 initially...
console.log("Birth year when age is 25:", Person.getBirthYear()); // Output (if current year is 2025): 2000

//  Let's update the age directly in the Person object...
Person.age = 30;

//  Now, getBirthYear automatically uses the *new* age!
console.log("Birth year when age is 30:", Person.getBirthYear()); // Output (if current year is 2025): 1995

Итак, JavaScript использует объекты и, как мы увидим, прототипы, чтобы воплотить ООП в жизнь, даже если в нем нет классических классов. Надеюсь, вы начинаете понимать привлекательность такой организации кода. Прежде чем мы перейдем к классам, имеет смысл получить действительно четкое представление об объектах и прототипах в JavaScript, не так ли? Это то, к чему мы перейдем далее.

Давайте поговорим об объектах в JavaScript.

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

const Person = {};

Итак, является ли Person пустым объектом? На первый взгляд, он, безусловно, выглядит пустым. Если вы подумали "да", то вы не одиноки. Это распространенная первоначальная мысль. Но в JavaScript объекты немного интереснее, чем просто то, что мы в них явно вкладываем. Давайте рассмотрим, как на самом деле работают объекты под капотом.

Итак, как же работают объекты в JavaScript?

Давайте разберем это. По своей сути, объект - это набор свойств. Рассматривайте свойства как именованные контейнеры для значений. У каждого свойства есть имя (также называемое "ключом").

const Person = {
  firstName: "John",
  lastName: "Doe",
};

Имя и фамилия - это имена свойств (ключи), а "John" и "Doe" - их соответствующие значения. Свойство объекта всегда представляет собой пару ключ-значение. Значение может быть разным.

Значение, связанное со свойством, может быть примитивным типом данных. В JavaScript примитивами являются такие элементы, как строки, числа, логические значения (true или false), null, undefined и символы. Давайте рассмотрим несколько примеров:

const exampleObject = {
  name: "Example", // String
  age: 30, // Number
  isStudent: false, // Boolean
  favoriteColor: null, // null
};

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

const anotherObject = {
  address: {
    // Value is another object
    street: "123 Main St",
    city: "Anytown",
  },
  hobbies: ["reading", "hiking"], // Value is an array
  greet: function () {
    // Value is a function (a method!)
    console.log("Hello!");
  },
};

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

const calculator = {
  value: 0,
  add: function(number) {
    this.value += number; // 'this' refers to the calculator object
  },
  getValue: function() {
    return this.value;
  }
};

calculator.add(5);
console.log(calculator.getValue()); // Output: 5

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

Помните наш кажущийся пустым объект Person = {}? Мы сказали, что он выглядит пустым, верно? Что ж, пришло время немного поколдовать над JavaScript. Хотя мы сами не добавляли в него никаких свойств, он не совсем пустой. Каждый объект в JavaScript по умолчанию имеет скрытую ссылку (часто называемую внутренним свойством [[Prototype]]) на другой объект, называемый его прототипом.

Для объектов, созданных с использованием простого синтаксиса {} (например, нашего объекта person), их прототипом по умолчанию является встроенный Object.prototype. Представьте Object.prototype как своего рода родительский объект, который предоставляет некоторую базовую встроенную функциональность всем объектам.

Вот почему вы можете делать подобные вещи даже с нашим "пустым" объектом Person:

console.log(Person.toString()); // Output: [object Object]

Подождите минуту. Мы никогда не определяли метод toString() в нашем объекте Person. Так откуда он взялся? Он взят из своего прототипа Object.prototype. toString() - это метод, встроенный в Object.prototype, и поскольку прототипом пользователя является Object.prototype, пользователь может получить доступ к методу toString() и использовать его.

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

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

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

Здесь мы можем явно настроить прототипы. Вместо того, чтобы полагаться на Object.prototype по умолчанию, мы можем сказать JavaScript: "Эй, я хочу, чтобы прототипом моего объекта Developer был объект Person, который мы уже определили". Мы можем сделать это с помощью Object.create():

const Person = {
  firstName: "John",
  lastName: "Doe",
  sayHello: function () {
    console.log(`Hello, my name is ${this.firstName} ${this.lastName}`);
  },
};

const developer = Object.create(Person); // developer's prototype is now 'Person'
developer.firstName = "Spruce"; // Add a *specific* firstName for developer
developer.programmingLanguage = "JavaScript"; // Developer's own property

developer.sayHello(); // Output: Hello, my name is Spruce Person (still accesses sayHello from 'person' prototype!)
console.log(developer.programmingLanguage); // Output: JavaScript (developer's own property)
console.log(developer.lastName); // Output: Doe (inherited from 'Person' prototype!)

Давайте разберем, что происходит, когда мы получаем доступ к свойствам на Developer:"

console.log(developer.firstName); // Output: Spruce (developer's *own* property)
console.log(developer.programmingLanguage); // Output: JavaScript (developer's *own* property)
console.log(developer.lastName); // Output: Doe (found on the *prototype* 'Person')
console.log(developer.sayHello()); // Output: Hello, my name is Spruce Person (method from *prototype*)
console.log(developer.job); // Output: undefined (not on 'Developer' OR 'Person' prototype)

Когда вы пытаетесь получить доступ к такому свойству, как Developer.LastName, JavaScript выполняет следующее:

  1. Сначала он проверяет, есть ли у разработчика свойство с именем LastName непосредственно в нем самом? В нашем примере у разработчика есть только FirstName и programmingLanguage в качестве собственных свойств. LastName там отсутствует.
  2. Если он не находит его в самом объекте, JavaScript затем просматривает прототип объекта (которому мы присваиваем значение Person, используя Object.create()).
  3. Он проверяет: "Есть ли у объекта Person (прототипа) свойство с именем LastName?" Да, у Person действительно есть фамилия: "Doe". Итак, JavaScript использует это значение.
  4. Если свойство также не найдено в прототипе, JavaScript затем просматривает прототип пользователя (который по умолчанию является Object.prototype) и так далее, вверх по цепочке прототипов. Если он проходит весь путь вверх по цепочке и по-прежнему не находит свойство, он, наконец, возвращает undefined (например, когда мы пытались получить доступ к developer.job).

Собственные свойства - это просто свойства, которые определяются непосредственно в самом объекте при его создании (например, FirstName и programmingLanguage в Developer). Доступ к свойствам прототипа осуществляется через цепочку прототипов.

Вы даже можете создавать более длинные цепочки прототипов. Например, предположим, что мы хотим создать объект JavaScriptDeveloper, который является типом Developer. Мы можем сделать Developer прототипом JavaScriptDeveloper:

const JavaScriptDeveloper = Object.create(Developer); // javaScriptDeveloper's prototype is 'Developer'

JavaScriptDeveloper.framework = "React"; // JavaScriptDeveloper's own property

console.log(JavaScriptDeveloper.firstName); // Output: Spruce (from 'Developer' prototype)

console.log(JavaScriptDeveloper.lastName); // Output: Doe (from 'Person' prototype)

console.log(JavaScriptDeveloper.programmingLanguage); // Output: JavaScript (from 'Developer' prototype)

console.log(JavaScriptDeveloper.framework); // Output: React (JavaScriptDeveloper's own property)

console.log(JavaScriptDeveloper.job); // Output: undefined (not found anywhere in the chain)

(Необязательное исследование: если вам интересно, выполните поиск по javaScriptDeveloper.LastName. Это выглядит так: JavaScriptDeveloper -> Разработчик -> Пользователь -> Object.prototype).

Хорошо, прототипы - это мощная штука. Мы можем создавать объекты с общими свойствами и поведением и специализировать их для различных нужд. Но представьте, что мы захотели бы создать сотни объектов Person, сотни объектов Developer и сотни объектов JavaScriptDeveloper.

Использование Object.create() каждый раз все равно будет довольно повторяющимся, особенно если мы хотим убедиться, что каждый пользователь начинает с одних и тех же базовых свойств (например, имени и фамилии).

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

В следующем разделе мы рассмотрим, как javascript использует функции конструктора для реализации классов.

Функции конструктора: Схемы объектов - Давайте перейдем к практике

Итак, прототипы очень удобны для повторного использования кода и создания специализированных объектов. Мы видели, как Object.create() позволяет нам создавать объекты, которые наследуются от других. Но представьте, что мы хотели создать тонны объектов Person, сотни объектов для веб-сайта. Ввод Object.create(person) для каждого отдельного пользователя стал бы слишком повторяющимся, особенно если мы всегда хотим, чтобы каждый пользователь начинался с одних и тех же базовых свойств, таких как имя и фамилия.

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

Думайте о функциях конструктора как о способе JavaScript создавать шаблоны для объектов. Они похожи на фабрики объектов. И в JavaScript мы используем функции конструктора, которые являются специализированными функциями, используемыми определенным образом, для создания этих шаблонов. Да, снова функции. Но мы используем их по-особому.

Итак, что же такое функция-конструктор на самом деле?

Ну, как я уже сказал, это функция, которая создает объекты. Взгляните на этот пример:

function PersonConstructor(name, age) {
  this.name = name;
  this.age = age;
  this.greet = function () {
    console.log(`Hello, I'm ${this.name}`);
  };
}

Это выглядит как обычная функция. Вы абсолютно правы. Она выглядит так же, как и любая другая функция, которую вы, вероятно, писали на JavaScript. На самом деле, давайте докажем это. Если мы просто зарегистрируем сам PersonConstructor, мы увидим:

console.log(PersonConstructor);
// output
function PersonConstructor(name, age) {
  this.name = name;
  this.age = age;
  this.greet = function () {
    console.log(`Hello, I'm ${this.name}`);
  };
}

- видишь? Просто обычная функция. Итак, что же делает ее функцией-конструктором?

Волшебный ингредиент: Новое ключевое слово

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

Давайте посмотрим на это в действии:

const person1 = new PersonConstructor("Alice", 25);

console.log(person1);
// output

// PersonConstructor { name: 'Alice', age: 25, greet: [Function] }

Теперь на выходе мы видим не просто код функции, а объект PersonConstructor. Ключевое слово new не просто вызвало функцию, оно фактически создало совершенно новый объект, основанный на схеме элементов PersonConstructor.

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

const person1 = new PersonConstructor("Alice", 25);
const person2 = new PersonConstructor("Bob", 30);
const person3 = new PersonConstructor("Charlie", 28);

console.log(person1);
console.log(person2);
console.log(person3);
// output
PersonConstructor { name: 'Alice', age: 25, greet: [Function] }
PersonConstructor { name: 'Bob', age: 30, greet: [Function] }
PersonConstructor { name: 'Charlie', age: 28, greet: [Function] }

Круто, правда? У нас есть три разных объекта Person, созданных на основе одного и того же шаблона PersonConstructor.

приостановить... Что Это за Ключевое слово, Которое Я Постоянно Вижу?

Вы, наверное, заметили, что слово this часто встречается в этих примерах кода, например, в this.name, this.age и this.greet(). И вы, возможно, думаете: "Что же это такое в мире JavaScript?"

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

Представьте, что вы описываете себя. Вы могли бы сказать: "Меня зовут [Ваше имя]". В этом предложении слово "мой" относится к вам, собеседнику.

В JavaScript-объектах это похоже на "мой" или "me". Это способ, с помощью которого объект ссылается на себя.

Давайте сначала рассмотрим это на примере обычного объекта:

const PersonObject = {
  name: "Spruce",
  greet: function () {
    console.log("Hello, my name is " + PersonObject.name); //  Using PersonObject.name directly
  },
};

PersonObject.greet(); // Output: Hello, my name is Spruce

В этом PersonObject, внутри функции greet, мы использовали PersonObject.name для доступа к свойству name. Это работает отлично. Мы напрямую указываем JavaScript, как получить свойство name из PersonObject. Мы могли бы использовать это и здесь, но давайте посмотрим, почему это становится очень полезным, особенно в функциях конструктора.

Теперь рассмотрим эту немного иную версию, используя следующее:

const PersonObjectThis = {
  name: "Spruce",
  greet: function () {
    console.log("Hello, my name is " + this.name); // Using 'this.name'
  },
};

PersonObjectThis.greet(); // Output: Hello, my name is Spruce

- видишь? Он по-прежнему работает так же. Когда в PersonObjectThis вызывается функция greet, внутри функции greet она автоматически ссылается на PersonObjectThis. Таким образом, this.name это просто более динамичный способ сообщить "свойство name этого текущего объекта".

Зачем использовать это вместо прямого присвоения имени объекту?

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

Вернемся к функциям конструктора: что это там означает?

Давайте вернемся к нашему PersonConstructor:

function PersonConstructor(name, age) {
  this.name = name;
  this.age = age;
  this.greet = function () {
    console.log(`Hello, I'm ${this.name}`);
  };
}

const person1 = new PersonConstructor("Alice", 25);
const person2 = new PersonConstructor("Bob", 30);

Когда мы делаем const person1 = новый PersonConstructor("Алиса", 25); внутри функции PersonConstructor:

  • это становится персоной1. Это похоже на то, как если бы JavaScript выполнял: person1.name = "Алиса"; персона1.возраст = 25 лет; персона1.приветствие = функция() { ... };

И когда мы делаем const person2 = новый PersonConstructor("Bob", 30); снова внутри PersonConstructor:

  • это становится персоной2. Как это делает JavaScript: person2.name = "Боб"; персона2.возраст = 30 лет; персона2.приветствие = функция() { ... };

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

Функции конструктора: Отлично подходят для создания чертежей, но... Пустая трата памяти?

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

Но в нашем PersonConstructor скрывается небольшая проблема:

function PersonConstructor(name, age) {
  this.name = name;
  this.age = age;
  this.greet = function () {
    // 😬 Look at this greet function!
    console.log(`Hello, I'm ${this.name}`);
  };
}

const person1 = new PersonConstructor("Alice", 25);
const person2 = new PersonConstructor("Bob", 30);

console.log(person1, person2);
// output

PersonConstructor {name: "Alice", age: 25, greet: function}

PersonConstructor {name: "Bob", age: 30, greet: function}

Обратите внимание на функцию greet внутри PersonConstructor? Каждый раз, когда мы создаем новый объект Person с помощью new PersonConstructor(), мы фактически копируем всю функцию greet для каждого объекта.

Представьте, что мы создаем тысячу объектов Person. В памяти у нас будет тысяча идентичных функций greet. Для простой функции greet() влияние на память может показаться незначительным. Однако, если у вас были более сложные методы с большим количеством кода или если вы создавали тысячи или даже миллионы объектов, дублирование этих функций для каждого отдельного объекта может привести к значительной потере памяти.

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

Прототипы спешат на помощь (снова): Эффективный обмен методами

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

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

Подобный этому:

function PersonConstructor(name, age) {
  this.name = name;
  this.age = age;
}

//  --- Add the greet method to the PROTOTYPE of PersonConstructor! ---
PersonConstructor.prototype.greet = function () {
  console.log(`Hello, I'm ${this.name}`);
};

Теперь метод greet определен только один раз в PersonConstructor.prototype. Но все объекты, созданные с помощью PersonConstructor, по-прежнему могут его использовать. Они наследуют его от прототипа.

Давайте проверим это:

const person1 = new PersonConstructor("Alice", 25);
const person2 = new PersonConstructor("Bob", 30);

person1.greet(); // Output: Hello, I'm Alice  - Still works!
person2.greet(); // Output: Hello, I'm Bob    - Still works!

console.log(person1.greet === person2.greet); // Output: false - They are NOT the same function object in memory

console.log(person1.__proto__.greet === person2.__proto__.greet); // Output: true - But they share the same prototype method!

функции person1.greet() и person2.greet() по-прежнему работают отлично. Но теперь функция greet не копируется для каждого объекта. Она используется совместно с прототипом. Это намного эффективнее, особенно когда мы имеем дело с большим количеством объектов и методов.

Функции конструктора + прототипы: Мощное сочетание

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

Это ключевой шаблон в JavaScript для создания повторно используемых объектных структур.

Итак, мы рассмотрели создание объектов и эффективные методы... Но как насчет наследования с помощью функций конструктора?

Что, если мы захотим создать схему элементов DeveloperPerson, которая наследуется от нашей схемы элементов PersonConstructor? Чтобы объекты DeveloperPerson автоматически имели имя, возраст и приветствие, но также могли иметь свои собственные свойства и методы, связанные с разработчиком?

Вот тут-то и возникают сложности с функциями конструктора, и нам нужно будет использовать специальный трюк под названием call(), чтобы заставить наследование работать. Давайте углубимся в это далее.

Наследование с помощью функций конструктора: Передача семейных признаков (способом конструктора)

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

Вспомните наш пример с человеком и разработчиком. Разработчик - это человек, верно? У него есть имя, возраст, возможно, он здоровается с людьми, но у него также есть свойства, характерные для разработчика, например, любимый язык программирования и способность кодировать.

Как мы можем создать схему элементов DeveloperPersonConstructor, которая наследует все базовые элементы PersonConstructor, а затем добавляет свои собственные функции, специфичные для разработчика? Для функций конструктора вы можете использовать функцию, называемую call().

вызов(): Секретное рукопожатие по наследству

call() - это функциональный метод, который позволяет вам делать что-то немного необычное: вы можете заимствовать функцию из одного объекта и запускать ее в контексте другого объекта. Звучит запутанно? Давайте упростим.

Чтобы проиллюстрировать call(), давайте рассмотрим наш PersonConstructor. Мы хотим создать DeveloperPersonConstructor, который также задает имя и возраст таким же образом, как это делает PersonConstructor, перед добавлением свойств, специфичных для разработчика.

Вот тут-то и появляется функция call(). Мы можем использовать call(), чтобы, по сути, сказать: "Эй, PersonConstructor, запусти свой код, но запусти его так, как если бы ты был внутри DeveloperPersonConstructor, и задай имя и возраст для этого объекта DeveloperPerson, который мы сейчас создаем".

Давайте посмотрим на это в коде, чтобы было понятнее:

function PersonConstructor(name, age) {
  this.name = name;
  this.age = age;
}

PersonConstructor.prototype.greet = function () {
  console.log(`Hello, I'm ${this.name}`);
};

function DeveloperPersonConstructor(name, age, programmingLanguage) {
  //  --- "Borrow" the PersonConstructor to set up name and age! ---
  PersonConstructor.call(this, name, age); //  <--  The magic of 'call()'

  // --- Now, add developer-specific properties ---
  this.programmingLanguage = programmingLanguage;
  this.code = function () {
    console.log(`${this.name} is coding in ${this.programmingLanguage}`);
  };
}

Видите эту строку: PersonConstructor.call(это, имя, возраст); ? Это ключ к наследованию. Давайте разберем это:

  • PersonConstructor.call(...): Мы вызываем функцию PersonConstructor, но не обычным способом. Мы используем .call().
  • это: Первый аргумент для вызова() имеет решающее значение. Он определяет, что должно быть внутри функции PersonConstructor при ее запуске. Здесь мы передаем это из DeveloperPersonConstructor. Почему? Потому что мы хотим, чтобы PersonConstructor установил имя и возраст для объекта DeveloperPerson, который в данный момент создается.
  • имя, возраст: Это аргументы, которые мы передаем самой функции PersonConstructor. Итак, когда PersonConstructor запустится (благодаря .call()), он получит имя и возраст и будет делать то, что обычно делает: set this.name = имя, а this.age = возраст. Но поскольку на самом деле это объект DeveloperPerson, он устанавливает эти свойства для объекта DeveloperPerson.

Сведение всего этого воедино: Создание сотрудника-разработчика

Теперь давайте создадим объект DeveloperPerson и посмотрим, что получится:

const devPerson1 = new DeveloperPersonConstructor("Eve", 30, "JavaScript");

console.log(devPerson1.name); // Output: Eve (Inherited from PersonConstructor!)
console.log(devPerson1.age); // Output: 30 (Inherited from PersonConstructor!)
devPerson1.greet(); // Output: (Oops! Error!)
console.log(devPerson1.programmingLanguage); // Output: JavaScript (Developer-specific)
devPerson1.code(); // Output: Eve is coding in JavaScript (Developer-specific)

Обратите внимание, что там есть devPerson1.name и devPerson1.age. Разработчик personconstructor позаимствовал часть PersonConstructor, которая устанавливает эти базовые свойства. И у нас также есть devPerson1.programmingLanguage и devPerson1.code(), которые предназначены специально для разработчиков.

О-о-о! А где же грейт()?

Но подождите, devPerson1.greet() выдает ошибку. Почему? Потому что, несмотря на то, что мы позаимствовали логику конструктора у PersonConstructor, мы еще не настроили цепочку прототипов для наследования методов-прототипов, таких как greet().

На данный момент прототип devPerson1 - это просто прототип объекта по умолчанию (Object.prototype). Он не наследуется от PersonConstructor.prototype. Нам нужно это исправить.

Настройка цепочки прототипов для наследования конструктора

Чтобы объекты DeveloperPersonConstructor также наследовали методы прототипа от PersonConstructor, нам нужно вручную настроить цепочку прототипов. Мы можем сделать это с помощью Object.create() еще раз.

Мы хотим, чтобы прототип DeveloperPersonConstructor был объектом, который наследуется от PersonConstructor.prototype.

Вот код:

function PersonConstructor(name, age) {
  this.name = name;
  this.age = age;
}

PersonConstructor.prototype.greet = function () {
  console.log(`Hello, I'm ${this.name}`);
};

function DeveloperPersonConstructor(name, age, programmingLanguage) {
  PersonConstructor.call(this, name, age);
  this.programmingLanguage = programmingLanguage;
  this.code = function () {
    console.log(`${this.name} is coding in ${this.programmingLanguage}`);
  };
}

// ---  Set up the Prototype Chain for Inheritance! ---
DeveloperPersonConstructor.prototype = Object.create(
  PersonConstructor.prototype
);

Эта строка DeveloperPersonConstructor.prototype = Object.create(PersonConstructor.prototype); творит чудеса. В нем говорится: "Эй, JavaScript, задай прототипу DeveloperPersonConstructor новый объект, который наследуется от PersonConstructor.prototype".

Теперь давайте снова попробуем devPerson1.greet():

const devPerson1 = new DeveloperPersonConstructor("Eve", 30, "JavaScript");

devPerson1.greet(); // Output: Hello, I'm Eve  - 🎉 It works now!

devPerson1.greet() теперь работает. devPerson1 наследует метод greet() от PersonConstructor.prototype через цепочку прототипов, которую мы только что настроили.

Давайте проследим цепочку создания прототипа

Давайте действительно разберемся, что происходит, когда мы выполняем devPerson1.greet():

  1. JavaScript проверяет: есть ли у самого devPerson1 свойство greet? Нет.
  2. JavaScript рассматривает прототип devPerson1: DeveloperPersonConstructor.prototype. Есть ли у него свойство greetproperty? Нет, мы только добавили специфичные для разработчика методы или свойства непосредственно в DeveloperPersonConstructor, а не в его прототип в нашем примере. (Позже мы могли бы добавить методы создания прототипов для конкретных разработчиков).
  3. JavaScript передается по цепочке прототипов к прототипу DeveloperPersonConstructor.prototype: PersonConstructor.prototype. Есть ли у него свойство greet? Да. Мы определили PersonConstructor.prototype.greet = функция() { ... };
  4. JavaScript находит greet() в PersonConstructor.prototype и выполняет его в контексте devPerson1 (таким образом, this.name внутри greet() ссылается на devPerson1.name).

Цепочка прототипов в действии. devPerson1 -> DeveloperPersonConstructor.prototype -> PersonConstructor.prototype-> Object.prototype.

Идем еще дальше: Специалист по разработке JavaScript

Мы даже можем создавать более длинные цепочки наследования. Допустим, мы хотим создать JavaScriptDeveloperPersonConstructor, который представляет собой особый тип DeveloperPersonConstructor, возможно, с определенными предпочтениями JavaScript framework.

Мы можем сделать то же самое по той же схеме:

function JavaScriptDeveloperPersonConstructor(name, age, framework) {
  //  "Borrow" from DeveloperPersonConstructor first!
  DeveloperPersonConstructor.call(this, name, age, "JavaScript"); // Hardcoded "JavaScript"
  this.framework = framework;
  this.codeJavaScript = function () {
    // Specific to JavaScript developers
    console.log(`${this.name} is coding in JavaScript with ${this.framework}`);
  };
}

// Set up prototype chain: JavaScriptDeveloperPerson -> DeveloperPerson -> Person
JavaScriptDeveloperPersonConstructor.prototype = Object.create(
  DeveloperPersonConstructor.prototype
);

Теперь у нас есть трехуровневая цепочка наследования.

Функции конструктора: Мощные, но немного перегруженные... Подробный?

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

И знаете что? Разработчики JavaScript тоже заметили это. В 2015 году в JavaScript был представлен новый, более понятный синтаксис для создания схем объектов.

Введите классы ES6: синтаксический сахар для прототипов

Видите ли, в 2015 году разработчики JavaScript осознали, что прямое использование прототипов и функций конструктора для создания шаблонов, подобных классам, может стать многословным и менее простым в управлении по мере роста приложений. Поэтому они ввели синтаксис классов в ECMAScript 2015 (ES6).

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

В следующем разделе мы рассмотрим, как переписать наши примеры Person, DeveloperPerson и JavaScriptDeveloperPerson, используя новый синтаксис класса, и вы увидите, насколько более понятным и похожим на класс (каламбур) это выглядит при использовании возможностей прототипов JavaScript.

Классы ES6: Синтаксис классов - Замаскированные прототипы

Итак, мы разобрались с функциями конструктора, call() и Object.create(), чтобы заставить наследование работать с прототипами. Это мощный инструмент, но, честно говоря, он может показаться немного многословным и непрямым, особенно если вы привыкли к языкам, основанным на классах.

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

Давайте перепишем наш пример с PersonConstructor, используя синтаксис класса. Приготовьтесь к глотку свежего воздуха.

PersonClass - Функция-конструктор, переосмысленная как класс

Вот как мы можем определить нашу схему персонализации как класс:

class PersonClass {
  //  Using the 'class' keyword!
  constructor(name, age) {
    //  'constructor' method - like our old constructor function
    this.name = name; //  Still using 'this' in the constructor
    this.age = age;
  }

  greet() {
    console.log(`Hello, I'm ${this.name}`);
  }
}

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

  • class PersonClass { ... }: Мы начинаем с ключевого слова class, за которым следует название класса (в данном случае PersonClass). Названия классов обычно пишутся с заглавной буквы.
  • конструктор(имя, возраст) { ... }: Внутри класса у нас есть специальный метод, называемый constructor. Это похоже на нашу старую функцию PersonConstructor. Именно здесь мы помещаем код для инициализации свойств нового объекта PersonClass, когда он создается с помощью new. Мы по-прежнему используем это внутри конструктора для ссылки на создаваемый новый объект.
  • приветствие() { ... }: Вот как мы определяем методы в классе. Мы просто пишем название метода (greet), за которым следуют круглые скобки для параметров (в данном случае их нет), а затем текст метода в фигурных скобках. Обратите внимание, что здесь мы не используем ключевое слово function. Это просто greet() { ... }.

Создание объектов из класса - По-прежнему с использованием новых

Для создания объектов из нашей схемы элементов PersonClass мы по-прежнему используем ключевое слово new, как и в случае с функциями конструктора:

const classPerson1 = new PersonClass("Charlie", 28);
const classPerson2 = new PersonClass("Diana", 32);

console.log(classPerson1.name); // Output: Charlie
classPerson1.greet(); // Output: Hello, I'm Charlie

Да, это работает точно так же, как и в нашем примере с функцией конструктора, но синтаксис класса просто гораздо более удобочитаемый и менее загроможденный.

DeveloperPersonClass - Наследование, упрощенное с помощью extends

Теперь давайте рассмотрим наследование с помощью классов. Помните, как нам приходилось использовать call() и Object.create(), чтобы заставить DeveloperPersonConstructor наследовать от PersonConstructor? С классами наследование становится очень простым с помощью ключевого слова extends.

Вот как мы можем переписать DeveloperPersonConstructor как DeveloperPersonClass, который наследуется от PersonClass:

class DeveloperPersonClass extends PersonClass {
  //  'extends' for inheritance!
  constructor(name, age, programmingLanguage) {
    super(name, age); //  'super()' calls the parent class constructor!
    this.programmingLanguage = programmingLanguage;
  }

  code() {
    // Developer-specific method
    console.log(`${this.name} is coding in ${this.programmingLanguage}`);
  }
}

Посмотрите на это. Наследование в классах объявляется с использованием ключевого слова extends: class DeveloperPersonClass расширяет PersonClass {...}. Одна только эта строка говорит: "Эй, JavaScript, DeveloperPersonClass должен наследоваться от PersonClass".

Внутри конструктора DeveloperPersonClass у нас есть эта строка: super(имя, возраст);. функция super() имеет решающее значение для наследования класса. Именно так мы вызываем конструктор родительского класса (в данном случае PersonClass). Когда мы вызываем super(name, age), он, по сути, выполняет то же самое, что и PersonConstructor.call(this, name, age) в нашем примере функции-конструктора - он запускает конструктор PersonClass для настройки унаследованных свойств (name и age) для объекта DeveloperPersonClass.

После вызова функции super() мы можем добавить любые свойства или методы, специфичные для разработчика, в наш DeveloperPersonClass, например.programmingLanguage = Язык программирования; и метод code().

Использование наследования DeveloperPersonClass в действии, более чистый синтаксис

Давайте создадим объект DeveloperPersonClass и посмотрим на наследование в действии с помощью этого более понятного синтаксиса:

const classDevPerson1 = new DeveloperPersonClass("Eve", 35, "JavaScript");

console.log(classDevPerson1.name); // Output: Eve (Inherited from PersonClass!)
console.log(classDevPerson1.age); // Output: 35 (Inherited from PersonClass!)
classDevPerson1.greet(); // Output: Hello, I'm Eve (Inherited from PersonClass!)
console.log(classDevPerson1.programmingLanguage); // Output: JavaScript (Developer-specific)
classDevPerson1.code(); // Output: Eve is coding in JavaScript (Developer-specific)

Он работает точно так, как ожидалось. classDevPerson1 наследует имя, возраст и функцию greet() от PersonClass, а также имеет свои собственные методы programmingLanguage и code(). Но синтаксис класса делает отношения наследования гораздо более очевидными и с ними проще работать.

Классы: Синтаксический сахар, мощь прототипа под ним

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

Когда вы определяете класс, JavaScript фактически делает все это за вас под капотом:

  • Это создание функции-конструктора (подобной нашему PersonConstructor).
  • Это настройка свойства .prototype этой функции-конструктора.
  • Когда вы используете extends, вы используете Object.create() и call() для настройки цепочки прототипов для наследования.

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

Итак, являются ли Классы просто "Поддельными" Классами?

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

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

Что будет дальше? Дополнительные возможности класса и реальные примеры

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

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

Представьте, что вы хотите создать действительно простое приложение для выполнения задач. Что вам нужно для управления?

  • Задачи: У каждой задачи есть описание и статус (выполнено или нет).
  • Действия: Вы захотите добавить новые задачи, пометить их как завершенные, удалить и перечислить в списке.

Это, естественно, заставляет нас думать о задаче TODO как об объекте, и если вы создаете много задач, класс ToDo - это идеальный вариант.

Настройка Ваших файлов

Прежде чем писать какой-либо код, создайте два файла в одной папке:

  • index.html : Это структура веб-страницы.
  • script.js : Именно здесь будет находиться ваш JavaScript-код с классами.

Вы можете использовать любой текстовый редактор (например, VS Code, Sublime Text или даже Notepad) для создания этих файлов.

Создание класса ToDo

Let’с начала строительства нашего класса задач. Скопируйте и вставьте следующий код в код script.js файл:

class ToDo {

constructor(description) {

this.description = description; // Every to-do needs a description

this.completed = false; // By default, it's not completed

}

markComplete() {

this.completed = true;

console.log("${this.description}" marked as complete!);

}

// More methods (e.g., for editing the to-do) can be added later.

}

Обратите внимание, насколько это чисто. Конструктор устанавливает описание и статус завершено для каждого нового элемента задачи. Метод markComplete() обновляет статус и регистрирует сообщение с подтверждением.

Создание класса ToDoList

Далее мы создадим класс ToDoList для управления нашей коллекцией задач. Добавьте следующий код в свой файл script.js под классом ToDo.:

class ToDoList {

constructor() {

this.todos = []; // Start with an empty array of to-dos

}

addTodo(description) {

const newTodo = new ToDo(description); // Create a new ToDo object

this.todos.push(newTodo); // Add it to our list

this.renderTodoList(); // Update the webpage display

}

listTodos() {

return this.todos; // Return the array of todos (for further processing or rendering)

}

markTodoComplete(index) {

if (index >= 0 && index < this.todos.length) {

this.todos[index].markComplete();

this.renderTodoList(); // Update the display after marking complete

}

}

renderTodoList() {

const todoListElement = document.getElementById('todoList');

todoListElement.innerHTML = ''; // Clear the current list in HTML

this.todos.forEach((todo, index) => {

const listItem = document.createElement('li');

listItem.textContent = todo.description;

if (todo.completed) {

listItem.classList.add('completed'); // Add CSS class for styling completed items

}

// Create a "Complete" button for each to-do

const completeButton = document.createElement('button');

completeButton.textContent = 'Complete';

completeButton.onclick = () => this.markTodoComplete(index);

listItem.appendChild(completeButton);

todoListElement.appendChild(listItem);

});

}

}

В этом классе:

  • Конструктор инициализирует пустой массив для хранения наших текущих задач.
  • addTodo(описание) создает новый объект ToDo и добавляет его в массив, затем вызывает renderTodoList() для обновления отображения.
  • Функция listTodos() возвращает список текущих задач.
  • markTodoComplete(индекс) помечает конкретную задачу как выполненную и обновляет отображение.
  • Функция renderTodoList() находит HTML-элемент с идентификатором ToDoList, очищает его содержимое, а затем создает элементы списка для каждой задачи, включая кнопку "Завершить".

Создание HTML-структуры

Затем откройте ваш файл index.html и вставьте в него следующий HTML-код:

<!DOCTYPE html>

<html>

<head>

  <title>My Simple To-Do App</title>

  <style>

    /* Simple CSS to style completed items */

    .completed {

      text-decoration: line-through;

      color: gray;

    }

  </style>

</head>

<body>

  <h1>My To-Do List</h1>

  <input type="text" id="todoInput" placeholder="Enter new to-do...">

<button id="addButton">Add To-Do</button>

  <ul id="todoList"></ul>

  <script src="script.js"></script>

</body>

</html>

Этот HTML-файл устанавливает:

  • Заголовок для вашего списка дел.
  • Поле ввода (с идентификатором="todoInput") для ввода новых задач.
  • Кнопка "Добавить к задаче" (с идентификатором="addButton").
  • Пустой неупорядоченный список (с идентификатором="ToDoList"), в котором будут отображаться ваши задачи.
  • Ссылка на файл script.js, содержащий ваш JavaScript-код.

Заставить Все Это Работать Вместе

Наконец, давайте соединим наши HTML-элементы с нашим JavaScript. В нижней части вашего файла script.js добавьте этот код:

const myTodoList = new ToDoList(); // Create an instance of ToDoList

// Get references to the HTML elements

const addButton = document.getElementById("addButton");

const todoInput = document.getElementById("todoInput");

// Listen for clicks on the "Add To-Do" button

addButton.addEventListener("click", () => {
  const todoText = todoInput.value.trim(); // Get the text from the input box

  if (todoText) {
    // Only add if the input is not empty

    myTodoList.addTodo(todoText); // Add the new to-do

    todoInput.value = ""; // Clear the input box
  }
});

// Render the to-do list initially (it will be empty to start)

myTodoList.renderTodoList();

Этот код выполняет следующие действия:

  • Создает экземпляр класса ToDoList.
  • Находит HTML-элементы для ввода и кнопки.
  • Этот код добавляет прослушиватель событий к элементу HTML button с идентификатором "addButton". Этот прослушиватель настроен так, чтобы реагировать на события "нажатия" на эту кнопку. При нажатии кнопки "Добавить задачу" будет выполнен код внутри функции прослушивателя событий. Этот код берет текст, который пользователь ввел в поле ввода HTML с идентификатором "todoInput", и добавляет его в качестве нового элемента списка дел в наш список.
  • Первоначально список дел отображается на веб-странице.

Ваша задача: воплотить Прото-стиль в жизнь

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

  • Как бы вы создали ToDo - Blueprint, используя функцию конструктора и прототипы?
  • Как бы вы добавили метод markComplete() в прототип ToDo?
  • Как бы вы аналогично структурировали список задач "Blueprint"?

Создавая одно и то же приложение с использованием обоих подходов, вы действительно поймете, что классы - это просто более приятный и привычный способ написания кода на основе прототипов.

Вывод

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

Ваш следующий шаг? Поэкспериментируйте с подходом, основанным на прототипировании, и сравните его с подходом, основанным на классах. Чем больше вы будете программировать, тем более естественными станут эти концепции. Радуйтесь написанию кода и продолжайте создавать классные вещи.

Если у вас возникнут какие-либо вопросы, не стесняйтесь обращаться ко мне в Твиттере по адресу @sprucekhalifa и не забывайте подписываться на меня, чтобы получать новые советы и обновления. Удачного программирования!