Предыстория
Одно из самых больших упущений JavaScript это невозможность создания приватных полей в пользовательских типах. Есть только один хороший путь создания приватной переменной внутри конструктора и создания привилегированных методов, которые будут иметь к ним доступ, например:
function Person(name) {
this.getName = function() {
return name;
};
}
В данном примере метод getName() использует аргумент name (по факту являющийся локальной переменной) для возврата значения имени персоны без раскрытия name как свойство объекта. Данный подход вполне себе подходящий, но очень неэффективен с точки зрения производительности. Как вы знаете функции в JavaScript являются объектами и если вы используете больше кол-во экземпляров объекта Person, каждый будет хранить свою копию метода getName(), вместо того, что бы использовать всего один из прототипа.
В качестве альтернативного решения, можно выбрать путь создания поля приватным по договоренности, сделав в его имени префикс, как правило в виде подчеркивания. Подчеркивание не является магией, это не защищает поле от использования, скорей всего лишь напоминание, о том, что это не стоит трогать. Например:
function Person(name) {
this._name = name;
}
Person.prototype.getName = function() {
return this._name;
};
Данный паттерн более эффективный так как каждый экземпляр будет использовать один и тот же метод из прототипа. Метод как и поле доступны извне, все, что мы сделали — согласились, что трогать ._name нельзя. Это решение далеко не идеальное, но им пользуется достаточно большое количество программистов. В свое время это пришло к нам из Python.
Есть еще вариант когда мы используем общие поле для всех экземпляров, которое можно легко создания используя IIFE функцию, которая содержит конструктор. Например:
var Person = (function() {
var sharedName;
function Person(name) {
sharedName = name;
}
Person.prototype.getName = function() {
return sharedName;
};
return Person;
}());
Здесь sharedName является общим для всех экземпляров Person, и каждый новый экземпляр будет перезаписывать значение аргументом name. Это очевидно не имеющее смысла решение, но очень важное на пути понимания того, как же нам реализовать действительно приватные поля.
На пути к приватным полям
Паттерн общих приватных полей указывает на потенциальное решение: что если приватные данные будут хранится не в экземпляре, но будет иметь доступ к ним? Что если будет объект который сможет хранить скрытые поля подальше и скрывать всю приватную информацию о реализации? До ECMAScript 6 вы скорей всего реализовали бы это примерно так:
var Person = (function() {
var privateData = {},
privateId = 0;
function Person(name) {
Object.defineProperty(this, "_id", { value: privateId++ });
privateData[this._id] = {
name: name
};
}
Person.prototype.getName = function() {
return privateData[this._id].name;
};
return Person;
}());
Таким способом мы к чему-то все такие пришли :) Объект privateData не доступен из вне IIFE, полностью скрывает всю информацию которая хранится внутри. Переменная privateId хранит следующий доступный ID, который использует экземпляр. К несчастью, ID приходится хранить в экземпляре и следует следить за тем, что бы он тоже не был доступен во время использования. Следовательно, мы используем Object.defineProperty() для установки значения и для того что бы убедиться, что переменная только для чтения. Затем внутри getName(), метод получает доступ к приватным переменным с помощью _id, для чтения и записи.
Данный подход хороший, вполне походит для хранения приватных переменных. Но лишнее использование _id лишь часть беды. Данный подход также порождает проблемы — если даже экземпляр будет удален сборщиком мусора, данные которые он записывал в privateData останутся в памяти. Как бы там ни было, это лучшее, что мы можем реализовать в ECMAScript 5.
Выход WeakMap
WeakMap решит оставшиеся проблемы предыдущего примера. Во-первых нам не надо больше хранить уникальный ID, так как экземпляр сам по себе может быть уникальным ID. Во-вторых, сборщик мусора сможет удалить запись которая нам больше не нужна, так как WeakMap это коллекция со слабыми ссылками. После того как экземпляр будет удален, запись не будет иметь жестких ссылок. Сборщик мусора в таком случае забирает из жизни все записи, которые были связанные с этим экземпляром. Точно такой же базовый паттерн из предыдущего примера, но более кошерный:
var Person = (function() {
var privateData = new WeakMap();
function Person(name) {
privateData.set(this, { name: name });
}
Person.prototype.getName = function() {
return privateData.get(this).name;
};
return Person;
}());
В этом примере privateData экземпляр WeakMap. Когда новый Person создается, новая запись добавляется в WeakMap для хранения приватных переменных. Ключом в WeakMap является this, не смотря на то, что разработчик может получить доступ к экземпляру объекта Person, он не может получить доступ к privateData из вне этого экземпляра. Любой метод который хочет получить доступ к этим данным, просто передает свой экземпляр (внутри которого находится метод). В данном примере getName() получает запись и возвращает свойство name.
Заключение
Это может быть отличным примером для людей, которые так и не нашли применение нового объекта WeakMap в ECMAScript 6. Многие пророчат грядущие перемены в том, как мы пишем JavaScript код. Лично для меня это своего рода переломный момент в мощи ООП JavaScript.