В этой статье рассматриваются 4 подхода к управлению приватными данными классов ES6:

1. Хранение данных в конструкторе класса.
2. Маркировка приватных свойств через соглашение об именовании (например, префиксное подчеркивание).
3. Хранение приватных данных в WeakMaps.
4. Использование символов в виде ключей для приватных свойств.

Первый и второй подходы широко использовались в ES5, а третий и четвертый – появились только в ES6. Давайте поочередно рассмотрим каждый на одном примере.



1. Хранение данных в конструкторе класса

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

Во-первых, мы сохраняем action и counter в контекст класса конструктора. Контекст – это внутренняя структура данных, где движок JavaScript хранит параметры и локальные переменные, существующие при внедрении новой области видимости (например через вызов функции или конструктора). Вот, собственно, код:

 class Countdown {
        constructor(counter, action) {
            Object.assign(this, {
                dec() {
                    if (counter < 1) return;
                    counter--;
                    if (counter === 0) {
                        action();
                    }
                }
            });
        }
    }


Использование Countdown выглядит следующим образом:

    > let c = new Countdown(2, () => console.log('DONE'));
    > c.dec();
    > c.dec();
    DONE


Преимущества:

? Приватные данные находятся в полной безопасности.
? Имена приватных свойств не будут конфликтовать с именами других приватных свойств родительского и дочернего классов.

Недостатки:

? Код становится менее изящным из-за необходимости определять все методы экземпляра в конструкторе (как минимум те, которым нужен доступ к приватным данным).
? Именно поэтому код тратит много памяти. Если использовались методы прототипов, они будут распределены.

Подробнее об этом подходе читайте в разделе Private Data in the Environment of a Constructor (Crockford Privacy Pattern) книги Speaking JavaScript.

2. Маркировка приватных свойств через соглашение об именовании

Следующий код хранит приватные данные в свойствах с префиксным подчеркиванием имен:

  class Countdown {
        constructor(counter, action) {
            this._counter = counter;
            this._action = action;
        }
        dec() {
            if (this._counter < 1) return;
            this._counter--;
            if (this._counter === 0) {
                this._action();
            }
        }
    }


Преимущества:

? Код выглядит красиво.
? Можно использовать методы прототипов.

Недостатки:

? Небезопасно. Это всего лишь инструкция для клиентского кода.
? Имена приватных свойств могут конфликтовать.

3. Хранение приватных данных в WeakMaps

Этот метод совмещает в себе преимущества первого и второго подходов: безопасность и возможность использования методов прототипов. Для сохранения приватных данных используются WeakMaps _counter и _action:

 let _counter = new WeakMap();
    let _action = new WeakMap();
    class Countdown {
        constructor(counter, action) {
            _counter.set(this, counter);
            _action.set(this, action);
        }
        dec() {
            let counter = _counter.get(this);
            if (counter < 1) return;
            counter--;
            _counter.set(this, counter);
            if (counter === 0) {
                _action.get(this)();
            }
        }
    }


Переменные _counter и _action хранят соответствие объектов своим приватным данным. Исходя из того, как работает WeakMaps, объекты могут удаляться сборщиком мусора. Приватные данные находятся в безопасности до тех пор, пока WeakMaps скрыт. Чтобы обезопасить себя, можно также сохранить WeakMap.prototype.get и WeakMap.prototype.set во временные переменные и вызывать их вместо динамического вызова методов. Даже если вредоносный код заменит эти методы теми, которые имеют доступ к приватным данным, на наш код это не повлияет. Однако защита распространяется только на код, который был запущен после нашего – защитить тот, который был запущен до него, к сожалению, невозможно.

Преимущества:

? Можно использовать методы прототипов.
? Безопаснее, чем соглашение об именовании для ключей свойств.
? Имена приватных свойств не могут конфликтовать.

Недостаток:

? Код не такой изящный, как при соглашении об именовании.

4. Использование символов в виде ключей для приватных свойств

Еще одно расположение хранилища приватных данных – это свойства с ключами в виде символов:

    const _counter = Symbol('counter');
    const _action = Symbol('action');
    class Countdown {
        constructor(counter, action) {
            this[_counter] = counter;
            this[_action] = action;
        }
        dec() {
            if (this[_counter] < 1) return;
            this[_counter]--;
            if (this[_counter] === 0) {
                this[_action]();
            }
        }
    }


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

 let c = new Countdown(2, () => console.log('DONE'));
    
    console.log(Object.keys(c));
        // []
    console.log(Reflect.ownKeys(c));
        // [ Symbol(counter), Symbol(action) ]


Преимущества:

? Можно использовать методы прототипов.
? Имена приватных свойств не могут конфликтовать.

Недостатки:

• По сравнению с соглашением об именовании, код не так изящен.
• Небезопасно, так как все ключи свойств объекта, включая символы, можно определить с помощью Reflect.ownKeys().

5. Дополнительная литература:

? Speaking. JavaScript, раздел Keeping Data Private;
? Exploring ES6, глава Classes;
? Exploring ES6, глава Symbols.

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


  1. Igogo2012
    02.03.2016 16:55
    +2

    Не безопасно и не изящно, но в любом случае пост полезен, спасибо!


  1. markoffko
    02.03.2016 17:09

    Существует какой-нибудь способ реализации protected свойств?


    1. Ohar
      02.03.2016 22:59

      Старые добрые замыкания?


      1. vintage
        02.03.2016 23:16
        +1

        protected — доступные в подклассах. С замыканиями такое не реализовать.


      1. vintage
        02.03.2016 23:17

        protected — доступные в подклассах. С замыканиями такое не реализовать.


    1. gooddaytoday
      11.03.2016 17:29
      -1

      В Typescript они есть.


  1. vintage
    02.03.2016 18:56
    -1

    Забыли написать, что заниматься отладкой кода с приватными членами сложнее. Или вы такой ерундой не занимаетесь? ;-)


    1. Large
      03.03.2016 02:55
      +2

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


  1. ioncreature
    03.03.2016 12:42

    Классная идея с WeakMap + JS Class. Возьму себе на вооружение.


  1. 3axap4eHko
    05.03.2016 22:39
    -1

    получается, что для каждого приватного свойства, создается отдельная WeakMap переменная и мне кажется это не практично, можно использовать следующий подход:

    //private.js
    'use strict';
    
    export default function () {
        var map = new WeakMap();
        return (...args) => {
            if (args.length === 2) {
                return map.set(args[0], args[1]); //set private property
            } else if (args.length === 1) {
                return map.get(args[0]); //get private property
            } else if (args.length === 3 && args[2]) {
                return map.remove(args[0]);  //remove private property
            }
            throw new Error(`Private scope storage called with wrong count of arguments: ${arguments.length}`);
        };
    };

    использовать так:

    //my-class.js
    'use strict';
    
    const $ = require('./private')(); 
    
    export default class MyClass {
        constructor(options) {
            $(this, {
                name: options.name
            });
        }
        get name() {
            return $(this).name;
        }
    }

    или даже так:

    //my-class.js
    'use strict';
    
    const $ = require('./private')();
    
    export default class MyClass {
        constructor(options) {
            $(this, options || {});
        }
        get name() {
            return $(this).name;
        }
    }