Достижение инкапсуляции и конфиденциальности всегда было известной проблемой в JavaScript.

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

  1. Инкапсулируйте частные данные в конструкторе объекта и добавьте все методы, которые работают с этими данными, в конструктор объекта.
function Counter() { 
  var count = 0; 
  this.get = function() { 
    return count; 
  }; 
  this.increment = function() { 
    return count += 1; 
  }; 
} 

Определяя частную переменную внутри конструктора объекта и затем ссылаясь на нее только из замыканий, созданных в конструкторе, вы делаете всех, кроме явно назначенных функций-членов, неспособными видеть и использовать переменную «counter».

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

2. Добавление личных данных к общедоступным свойствам объекта и использование соглашения об именах и доброты пользователей вашего API.

function Counter() {
  this._count = 0;
}
Counter.prototype.get = function () {
  return this._count;
}
Counter.prototype.increment = function() {
  return this._count += 1;
}

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

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

3. Не очень популярный метод на основе WeakMap.

// As this method requires some other state bookkeeping 
// we will wrap it's internals in an IIFE (Immediately-Invoked Function Expression).
var Counter = (function () {
  // Create a new WeakMap, which maps the object's
  // instance to it's private data.
  var privateData = new WeakMap();
  function Counter() {
    // Set the initial state of the object.
    privateData.set(this, {
      count: 0,
    });
  }
  Counter.prototype.get = function () {
    // Find the data that matches this instance 
    // and return it's the count.
    return privateData.get(this).count;
  }
  Counter.prototype.increment = function() {
    return privateData.get(this).count+= 1;
  }
  return Counter;
}());

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

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

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

4. Время для хорошего. Этот последний метод сочетает в себе лучшие части первых двух методов и отсутствие их слабых мест.

// A convenience function, that creates an accessor named _getPrivateData 
// which takes a matching key to expose it's data and adds it to the provided object.
function initPrivateData(self, privateKey, data) {
  Object.defineProperty(self, '_getPrivateData', {
    configurable: false,
    enumerable: false,
    writable: false,
    value: function getPrivateData(key) {
      if (key !== privateKey) {
        throw new Error("Illegal access");
      } else {
        return data;
      }
    },
  });
}
var Counter = (function () {
  
  // This is the private key for the Counter class.
  // In JS two objects compare equal only if they are the <same instance>.
  var privateKey = {};
  // Get the Couter's private data by providing the private key.
  function getPrivateData(self) {
    return self._getPrivateData(privateKey);
  }
  function Counter() {
    // Initialise the private data with the private key
    // and some initial state.
    initPrivateData(this, privateKey, {
      count: 0,
    });
  }
  Counter.prototype.get = function () {
    // Get the count from the object's private data.
    return getPrivateData(this).count;
  }
  Counter.prototype.increment = function() {
    return getPrivateData(this).count += 1;
  }
  return Counter;
}());

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

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

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

Примечание. Все примеры в этой статье написаны с использованием синтаксиса ES5 для большей простоты.

Вот последний четвертый метод, переписанный в синтаксисе ES6.

// in private-data.js
export default function initPrivateData(self, privateKey, data) {
  Object.defineProperty(self, '_getPrivateData', {
    configurable: false,
    enumerable: false,
    writable: false,
    value: function getPrivateData(key) {
      if (key !== privateKey) {
        throw new Error("Illegal access");
      } else {
        return data;
      }
    },
  });
}
// in counter.js
import initPrivateData from './private-data.js';
const privateKey = {};
const getPrivateData = self => self._getPrivateData(privateKey);
export default class Counter {
  constructor() {
    initPrivateData(this, privateKey, {
      count: 0,
    });
  }
  get() {
    return getPrivateData(this).count;
  }
  increment() {
    return getPrivateData(this).count += 1;
  }
}

Еще одно замечание читателю: это моя первая публикация на Medium. Ваша поддержка приветствуется :)