Объекты, прототипы и классы в JavaScript

Учитывая тот факт, что объектами в JavaScript является почти всё, объектно-ориентированный код JavaScript сильно отличается от кода других объектно-ориентированных языков. Объектная система JS основана на прототипах.

Читайте также: Объектно-ориентированные шаблоны JavaScript: шаблон factory

Если у вас есть опыт работы с C++, вы, скорее всего, знаете о парадигме объектно-ориентированного программирования и имеете четкое представление о том, как должны работать объекты и классы. Знакомство с другими языками, такими как Java, должно только укрепить эти знания. Для опытного разработчика C++, который никогда не работал с JavaScript, Javascript – настоящее открытие (хотя семантика объектов и классов в этих двух языках отличается).

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

let Reptile = new Object() {

 // ...

}

или с помощью конструктора функции:

function Reptile() {

 // ...

}

Во-вторых, объекты JavaScript очень гибкие. Если классические объектно-ориентированные языки позволяют изменять только свойства или их слоты, то JavaScript позволяет изменять свойства и методы объектов: объекты JavaScript имеют слоты свойств и методов.

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

Свойство prototype

Все объекты JavaScript создаются с помощью конструктора Object:

var Reptile = function(name, canItSwim) {

  this.name = name;

  this.canItSwim = canItSwim;

}

А prototype позволяет добавлять в конструктор объектов новые методы. Это значит, что следующий метод теперь существует во всех экземплярах Reptile.

Reptile.prototype.doesItDrown = function() {

  if (this.canItSwim) {

    console.log(`${this.name} can swim`);

  } else {

    console.log(`${this.name} has drowned`);

  }

};

Теперь можно создать экземпляры объектов Reptile:

// для примера предположим, что аллигаторы умеют плавать, а крокодилы не умеют

let alligator = new Reptile("alligator", true);

alligator.doesItDrown(); // alligator can swim

let croc = new Reptile("croc", false);

croc.doesItDrown(); // croc has drowned

// Как видите, аллигатор умеет плавать, а крокодил утонул.

Свойство prototype объекта Reptile становится основой для наследования, а метод doesItDrown доступен и для alligator, и для croc, поскольку этот метод содержится в прототипе Reptile. Свойство prototype является общим для всех его экземпляров и доступно через свойство __proto__ конкретного экземпляра.

Благодаря слотам методов и общему для всех экземпляров свойству prototype в JavaScript можно использовать несколько довольно изящных приемов, которые разработчикам С++ могут показаться странными:

croc.__proto__.doesItDrown = function() {

  console.log(`the croc never drowns`);

};

croc.doesItDrown(); // the croc never drowns

alligator.doesItDrown(); // the croc never drowns

Измените свойство или метод prototype одного экземпляра – и это изменение коснется всех экземпляров объекта. Таким путем можно удалять файлы. Однажды изменив свойство на «the croc never drowns» (что значит «крокодил не тонет»), мы повлияли на оба экземпляра. Крокодил, которому надоело постоянно тонуть, потенциально может сделать следующее:

delete croc.__proto__.doesItDrown

alligator.doesItDrown();

//TypeError: alligator.doesItDrown

// is not a function

 Теперь никто не может плавать.

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

Классы в JavaScript

Вместе с синтаксисом ES6 JavaScript получил возможность создавать классы.

Однако по сути понятия классов в JavaScript не существует, оно эмулируется с помощью прототипа, а синтаксис класса – это просто синтаксический сахар. Следовательно, умение работать с prototype важно для понимания преимуществ и ограничений классов ES6.

Если мы используем синтаксис классов в нашем примере Reptile, он будет определяться вот так:

class Reptile {

  constructor (name, canItSwim) {

    this.name = name;

    this.canItSwim = canItSwim;

  }

  doesItDrown () {

   if(this.canItSwim)

    console.log(`${this.name} can swim`);

   else

    console.log(`${this.name} has drowned`);

  }

}

let alligator = new Reptile("alligator", true);

alligator.doesItDrown(); //alligator can swim

Некоторых подводных камней можно избежать, используя классы ES6, например, ключевое слово new можно сделать обязательным для создания экземпляров:

let croc = Reptile("croc", false);

//TypeError: Class constructor Reptile cannot be invoked without 'new'

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

Вместо заключения

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

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

class Foo {

  #a; #b; // # indicates private members here

  #sum = function() { return #a + #b; };

}

// этот формат немного напоминает $variable в PHP.

Tags: ,

Добавить комментарий