Объектно-ориентированное программирование (ООП) - это парадигма программирования, основанная на концепции «объектов», где данные и функции (атрибуты и методы) связаны внутри объекта.

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

Какие же существуют способы создания объектов в JavaScript?

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

Способ 1 - Литеральная запись

let user = {
  name: 'Иван',
  points: 5,
  increment: function() {
    user.points++;
  }
};

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

Способ 2 - Object.create()

Object.create(proto, [ propertiesObject ])

Метод Object.create принимает два аргумента:

  • proto: объект, который должен быть прототипом вновь созданного объекта. Это должен быть объект или null.
  • propertiesObject: свойства нового объекта. Это необязательный аргумент.

По сути, вы указываете в Object.create объект, от которого хотите наследоваться, и вам соответственно возвращается новый объект, унаследованный от объекта, который вы ему передали.

let user = Object.create(null);

user.name = 'Иван';
user.points = 8;
user.increment = function() {
  user.points++;
}

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

Как нам это преодолеть?

Решение 1 - Генерировать объекты с помощью функции

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

function createUser(name, points) {
  let newUser = {};
  newUser.name = name;
  newUser.points = points;
  newUser.increment = function() {
    newUser.points++;
  };
  return newUser;
}

Теперь чтобы создать пользователя, нам нужно просто ввести данные в параметрах функции:

let user = createUser('Иван', 5);
user.increment();

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

Решение 2 - Использовать особенности prototype

В отличие от объектно-ориентированных языков, таких как Python и Java, в JavaScript нет классов. Он использует концепцию прототипов и цепочки прототипов для наследования.

Когда вы, допустим, создаете новый массив, вы автоматически получаете доступ к встроенным методам, таким как Array.join, Array.sort и Array.filter. Это связано с тем, что объекты массива наследуют свойства от Array.prototype.

цепочка прототипов в JavaScript

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

function createUser(name, points) {
  let newUser = Object.create(userFunction);
  newUser.name = name;
  newUser.points = points;
  return newUser;
}

let userFunction = {
  increment: function() {this.points++};
}

let user = createUser('Иван', 5);
user.increment();

При создании объекта user была сформирована связь прототипной цепочки с userFunction.

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

Решение 3 - ключевые слова new и this

Оператор new используется для создания экземпляра объекта, который имеет функцию конструктора.

Когда мы вызываем функцию-конструктор с помощью new, происходят следующие действия:

  • Создаётся новый объект;
  • К нему подвязывается this;
  • prototype функции-конструктора становится свойством __proto__ нового объекта;
  • Возвращает объект из функции.

Такая автоматизация приводит к менее повторяющемуся коду!

function User(name, points) {
 this.name = name; 
 this.points = points;
}
User.prototype.increment = function(){
 this.points++;
}

let user = new User('Иван', 6);
user.increment();

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

Интерпретатор поднимется по цепочке прототипов и найдет функцию increment в свойстве прототипа User, которое также является объектом с данными внутри. Помните - все функции в JavaScript также являются объектами. Теперь, когда интерпретатор нашел то, что ему нужно, он может создать новый локальный контекст выполнения для запуска user.increment().

Разница между __proto__ и prototype

Если вы уже запутались в __proto__ и prototype, не волнуйтесь! Вы далеко не единственный, кого это смущает.

Prototype - это свойство функции-конструктора, которое определяет, что станет свойством __proto__ для сконструированного объекта.

А __proto__ - это созданная ссылка, которая является связью цепи прототипа.

Решение 4 - синтаксический сахар ES6

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

class User {
  constructor(name, points) {
    this.name = name;
    this.points = points;
  }
  increment () {
    this.points++;
  }
}

let user = new User('Иван', 12);
user.increment();

В решении 3 связанные методы были реализованы с использованием User.prototype.functionName. В этом решении достигаются те же результаты, но синтаксис выглядит чище.