5 мая 2015 в 15:21

Реализация приватных полей с помощью WeakMap в JavaScript из песочницы

Предыстория


Одно из самых больших упущений 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


image
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.
Ольховой Дмитрий @auine
карма
–9,0
рейтинг 0,0
Master of Computer Science
Похожие публикации
Самое читаемое Разработка

Комментарии (23)

  • +4
    Мне кажется, что для приватных полей надо использовать Symbol, не? Они же как раз для этого в стандарт внедрялись вроде.
    • +1
      Символы — уникальные ключи, а не приватные. Их можно получить через Object.getOwnPropertySymbols, Reflect.ownKeys, скопировать через Object.assign. Их задача — предотвращение перекрытия свойств в пользовательском коде. Впрочем WeakMap тоже не является полностью приватным — при желании можно вытащить дынные, обернув методы прототипа коллекции. По идее, настоящие приватные поля могут появиться в ES7.
    • +1
      Да конечно их можно использовать в некотором роде. Но мы полностью не спрячем такие поля. Символы могут быть получены с помощью методов Object.getOwnPropertySymbols и Object.getOwnPropertyKeys.

      UPD. Выше меня опередили.
      • 0
        Нет такого метода Object.getOwnPropertyKeys и не планируется.
          • 0
            Это внутренний метод, который используют Object.getOwnPropertyNames и Object.getOwnPropertySymbols. А метода Object.getOwnPropertyKeys нет и не планируется.
      • +6
        Но мы полностью не спрячем такие поля

        Я никогда не понимал: а зачем? Это ведь должно быть как соглашение, как помощь программисту. Откуда маниакальное желание физически скрыть их, сделать абсолютно невозможным доступ? Как отлаживать такой код, если не можно в дебагере посмотреть значения приватных свойств? Тем более, что часто гараздо полезнее именно протектед модификатор, а не приват.
  • +8
    Зачем вы из Javascript пытаетесь сделать язык, на которым вы писали до этого? Не надо пытаться запихать в js приватные поля или классическое ООП. Перестаньте. Пользуйтесь идиоматичными подходами, которые и делают javascript таким интересным: замыкания, литералы обьектов и фабричные методы и Object.create/Object.assign.
    • +1
      Инкапсуляция.
      Подход приведенный в публикации активно используется индженерами в мазиле. Например Антон Ковалев использовал его при сокрытии нижнего слоя редактора. Для чего? Они не хотели показывать пользователям их API, что у них под капотом CodeMirror. Не из соображений сокрытия реализации, а из за соображений того, что они могут его свободно обновлять, изменять, или сменить полность на другой. Без страха сломать сторонние плагины под CodeMirror, что черевато крахом продакшена
      • +2
        Замыкания полностью решают задачи инкапсуляции. При этом такой подход остается «типичным» для javascript, идиоматичным, если хотите. Посмотрите как сделана основная масса крупных и известных библиотек, там есть очень много интересного на эту тему.

        Попытка же сделать приватные поля в виде java-образного ООП приводит к грязным хакам и корявым решениям.
      • +3
        Инкапсуляция

        Вы не понимаете смысл, дух инкапсуляции. Инкапсуляция — это не модификаторы «private» и «protected». Инкапсуляция — это сокрытие реализации за интерфейсом. Это когда вы смотрите на метод "findItems()" и не задумываетесь, какой код он вызывает, какие публичные или приватные методы дергает, главное, чтобы он соблюдал соглашение (интерфейс). Инкапсуляция неразрывно связана с, например, полиморфизмом, где метод изменяется, оставляя старое соглашение, но за счет инкапсуляции вас не интересует, что именно внутри него.

        А модификаторы private и protected — не более чем сахар и не являются обязательными для инкапсуляции.
        • –1
          Так именно это я и имел ввиду. Но — инкапсуляция это не только сокрытие реализации. Она так же может преследовать следующие цели:
          Организовать доступ пользователя к атрибутам и методам так, чтобы предотвратить несанкционированное использование (у нас пока нет интерфейсов, но есть необходимость).
          Защита от ошибок программиста заключается в неразрешенных объектах, способах использования методов (атрибутов).

          Концептуальная модель ООП предполагает 2 области видимости: — методы видны для всех пользователей класса — методы видны только для объектов данного класса.

          Для кого-то это важно, для кого-то нет. JS является решением множества проблем. И есть ряд задач где сокрытие как данных так и реализации необходимы в равной степени. Я привел отличный пример выше.
  • +5
    > Одно из самых больших упущений JavaScript это невозможность создания приватных полей в пользовательских типах.

    Почему это упущение? Никогда не страдал по этому поводу, что в Питоне, что в Джаваскрипте.
  • +3
    Оффтопик: не укора ради, а просто интересно, почему про Python таких статей не появляется? Про JS на одном только хабре каждые полгода кто-нибудь изобретает приватные поля.
  • +2
    Если уж рассказываете о ES6, то могли бы использовать классы и объектные литералы.
  • 0
    Спасибо, познавательно. Только недавно задумывался над тем как реализовать приватные свойства, чтоб были видны в конструкторе и прототипах, но не извне.
    • –3
      Приватные свойства? В прототипах? Оригинально!
    • 0
      • 0
        Да, спасибо, но такой подход и мне в голову пришел, я не захотел заниматься «выносом кишок» из конструктора и дублировать методы (публичный -> прототип).

        Вариант с объектом в замыкании мне нравится больше, конструктор выглядит чище, необходимо всего лишь не забывать удалять личные данные из объекта при убивании инстанса.
  • +2
    В ES7 этих proposal-ов приватных свойств чуть ли не больше чем реализаций классов на JS.
    Вот, например, что в свое время поддерживал babel как экспериментальную фичу (уже нет)
    class Pos {
        @x;
        @y;
        constructor(x,y){
            @x = x;
            @y = y;
        }
    }
    


    Как раз на викмапах работало. Что тоже не очень честно. Можно сделать паблика морозова при желании.

    А вообще — если серьезно — ну не надо этой приватщины в джаваскрипте. Есть договоренность о том, что свойства с _ в начале — приватные и никто в них не лезет. И всем хорошо. Дебажить удобнее, работать удобнее, никакого геммороя.
    • +1
      Серьёзно больше? Что-то не наблюдаю :) Этот синтаксис не поддерживался babel. Поддерживались абстрактные ссылки и ключевое слово private, в том числе и в литерале класса, как сахар для WeakMap (хотя для этого дела изначально планировались специальные PrivateMap), но Kevin Smith разделил предложение на 2 куда более узкие и, с моей точки зрения, не такие удачные части — private fields (ссылку на которые привёл выше) и bind operator.
      • 0
        Я образно. Но штук 10-20 точно были и есть.

        Про пример — да, извиняюсь, this::x поддерживался, а не @x, перепутал.

        • 0
          Не было 10 и уж тем более для ES7. Хотя не суть важно.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.