Некоторые программисты могут иметь недостаточно опыта работы с объектно-ориентированным программированием или 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, очищая его содержимое. Это позволяет освободить память, занимаемую массивом, и избежать утечек памяти.