Какие ошибки делают программисты в реализации ООП в JavaScript
hard

Какие ошибки делают программисты в реализации ООП в JavaScript

Статья для опытных

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

❗️ Это не самая простая статья для новичков, поэтому, если будет ничего не понятно, — почитайте наш стартовый цикл статей про ООП.

Почему при реализации ООП в JavaScript возникают ошибки

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

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

Неправильная организация объектов, классов и модулей

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

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

var person = {
  name: 'Маша',
  age: 25,
  sayHello: function() {
    console.log('Привет, ' + this.name);
  }
};

var car = {
  brand: 'Toyota',
  model: 'Camry',
  startEngine: function() {
    console.log('Запуск двигателя');
  }
};

person.sayHello();
car.startEngine();

Что здесь происходит:

  • У нас есть два объекта: 'person' и 'car', каждый из которых содержит свои свойства и методы. 
  • Эти объекты находятся в глобальной области видимости, что делает их доступными и изменяемыми из любого места в коде.
  • Это может привести к конфликтам и нежелательным побочным эффектам.

Для правильной организации объектов, классов и модулей в JS лучше использовать паттерны проектирования, такие как модуль, конструктор или пространство имён. Эти паттерны помогают структурировать код, разделять его на модули или классы и обеспечивать инкапсуляцию и повторное использование кода.

Вот как организовать эти же объекты с использованием модульного паттерна:

var personModule = (function() {
  var name = 'Маша';
  var age = 25;
  
  function sayHello() {
    console.log(Привет, ' + name);
  }
  
  return {
    sayHello: sayHello
  };
})();

var carModule = (function() {
  var brand = 'Toyota';
  var model = 'Camry';
  
  function startEngine() {
    console.log('Запуск двигателя');
  }
  
  return {
    startEngine: startEngine
  };
})();

personModule.sayHello();
carModule.startEngine();

Здесь мы организуем объекты 'person' и 'car' в отдельные модули, каждый из которых имеет своё приватное состояние и публичные методы. Это помогает избежать конфликтов имён и обеспечивает более чёткую структуру кода.

Неправильное использование конструкторов

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

function Person(name, age) {
  this.name = name;
  this.age = age;
  this.greet = function() {
    console.log('Привет, меня зовут ' + this.name + ', и мне ' + this.age + '.');
  };
}

var person1 = new Person('Маша', 25);
var person2 = new Person('Паша', 30);

person1.greet();
person2.greet();

Что здесь происходит:

  • Мы создаём конструктор 'Person', который принимает имя и возраст и устанавливает их в качестве свойств 'name' и 'age' для каждого созданного экземпляра объекта 'Person'.
  • Затем мы добавляем метод 'greet', который выводит приветствие с именем и возрастом. Но метод определён внутри самого конструктора. Это значит, что каждый раз при создании нового экземпляра 'Person' будет создана новая функция 'greet'.
  • В итоге это может привести к избыточному использованию памяти и потере производительности.

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

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

Person.prototype.greet = function() {
  console.log'Привет, меня зовут ' + this.name + ', и мне ' + this.age + '.');
};

var person1 = new Person('Маша', 25);
var person2 = new Person('Паша', 30);

person1.greet();
person2.greet();

Здесь мы определяем метод 'greet' в прототипе 'Person', а не внутри конструктора. Теперь метод будет создан только один раз и будет использоваться всеми экземплярами объектов 'Person'.

Некорректное использование прототипов

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

function Person(name) {
  this.name = name;
}

Person.prototype.greet = function() {
  console.log('Привет, меня зовут  ' + this.name);
};

var person1 = new Person(Маша');
var person2 = new Person('Паша');

person1.greet();
person2.greet();

Person.prototype.greet = function() {
  console.log('Привет, я ' + this.name);
};

person1.greet();
person2.greet();

Что здесь происходит:

  • Мы создаём конструктор 'Person', который принимает имя и устанавливает его в качестве свойства 'name' для каждого созданного экземпляра объекта 'Person'.
  • Затем мы добавляем метод 'greet' в прототип 'Person', который выводит приветствие с именем. Но если мы после этого попытаемся изменить 'Person.prototype.greet', это повлияет на все экземпляры объектов 'Person', которые уже были созданы.
  • В результате, когда мы меняем 'Person.prototype.greet', добавляя новую реализацию метода, при вызове 'greet' у объектов 'person1' и 'person2' они выводят новое приветствие, а не ожидаемое "Привет, меня зовут...".

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

Неправильная реализация наследования

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

function Animal(name) {
 this.name = name;
}

function Mammal(name) {
 Animal.call(this, name);
}

function Dog(name) {
 Mammal.call(this, name);
}

Dog.prototype.bark = function() {
 console.log('Гав! Гав!');
};

let myDog = new Dog('Барри');
myDog.bark(); // Гав! Гав!

Что здесь происходит:

  • С помощью функции 'Animal' мы определяем базовый класс для всех животных, а функции 'Mammal' и 'Dog' наследуют от него.
  • Функция 'Dog' добавляет метод 'bark', который является специфичным для собак. Но из-за того, что метод 'bark' объявлен внутри функции 'Dog', а не внутри её прототипа, метод 'bark' будет доступен только для экземпляров класса 'Dog', но не для экземпляров класса 'Mammal'.

Чтобы исправить эту ошибку, метод 'bark' следует объявить внутри прототипа класса 'Mammal':

function Animal(name) {
 this.name = name;
}

function Mammal(name) {
 Animal.call(this, name);
}

function Dog(name) {
 Mammal.call(this, name);
}

Mammal.prototype.bark = function() {
 console.log('Гав! Гав!');
};

let myDog = new Dog('Барри');
myDog.bark(); // Гав! Гав!

Теперь метод 'bark' доступен для всех экземпляров класса 'Mammal', включая экземпляры класса 'Dog'.

Мутация объектов вместо создания новых

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

function Rectangle(width, height) {
  this.width = width;
  this.height = height;
}

Rectangle.prototype.calculateArea = function() {
  return this.width * this.height;
};

var rect = new Rectangle(10, 5);

console.log(rect.calculateArea());

rect.width = 20;
rect.height = 10;

console.log(rect.calculateArea());

Что здесь происходит:

  • Мы создаём конструктор 'Rectangle', который принимает ширину и высоту прямоугольника и устанавливает их как свойства объекта.
  • Затем мы определяем метод 'calculateArea', который вычисляет площадь прямоугольника. Но при этом мы изменяем свойства объекта 'rect' напрямую.
  • Это приводит к изменению состояния объекта — ширины и высоты прямоугольника — что влияет на результат, возвращаемый методом 'calculateArea'.

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

Вместо мутации объекта 'rect' нужно создать новый объект 'newRect' с обновлёнными свойствами:

function Rectangle(width, height) {
  this.width = width;
  this.height = height;
}

Rectangle.prototype.calculateArea = function() {
  return this.width * this.height;
};

var rect = new Rectangle(10, 5);

console.log(rect.calculateArea());

var newRect = new Rectangle(20, 10);

console.log(newRect.calculateArea());

Здесь мы создаём новый объект `newRect` с обновлёнными значениями ширины и высоты. Теперь каждый объект имеет своё состояние, и метод `calculateArea` будет возвращать ожидаемый результат.

Неправильное управление памятью — не только в ООП, но про это тоже нужно помнить

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

function createArray() {
  var array = [];
  
  for (var i = 0; i < 1000000; i++) {
    array.push(i);
  }
  
  return array;
}

function processArray() {
  var array = createArray()

  array = null;
}

processArray();

Что здесь происходит:

  • С помощью функции 'createArray' мы создаём массив с миллионом элементов.
  • Затем с помощью функции 'processArray' мы вызываем 'createArray' и получаем ссылку на этот массив.
  • После этого мы присваиваем переменной 'array' значение 'null', чтобы удалить ссылку на массив. Но при этом массив, созданный в функции 'createArray', не очищается сразу после использования, а остаётся в памяти, занимая значительное количество ресурсов.

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

function processArray() {
  var array = createArray();

  array.length = 0;
}

Здесь мы не присваиваем переменной 'array' значение 'null', а устанавливаем длину массива в 0, очищая его содержимое. Это позволяет освободить память, занимаемую массивом, и избежать утечек памяти.

Обложка:

Алексей Сухов

Корректор:

Ирина Михеева

Вёрстка:

Маша Климентьева

Соцсети:

Юлия Зубарева

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