В этой статье рассматриваются 4 подхода к управлению приватными данными классов ES6:
1. Хранение данных в конструкторе класса.
2. Маркировка приватных свойств через соглашение об именовании (например, префиксное подчеркивание).
3. Хранение приватных данных в WeakMaps.
4. Использование символов в виде ключей для приватных свойств.
Первый и второй подходы широко использовались в ES5, а третий и четвертый – появились только в ES6. Давайте поочередно рассмотрим каждый на одном примере.
1. Хранение данных в конструкторе класса
Наш текущий пример – это класс Countdown, который вызывает функцию action, когда счетчик counter становится равным нулю. При этом counter и action должны быть сохранены как приватные переменные.
Во-первых, мы сохраняем action и counter в контекст класса конструктора. Контекст – это внутренняя структура данных, где движок JavaScript хранит параметры и локальные переменные, существующие при внедрении новой области видимости (например через вызов функции или конструктора). Вот, собственно, код:
Использование Countdown выглядит следующим образом:
Преимущества:
? Приватные данные находятся в полной безопасности.
? Имена приватных свойств не будут конфликтовать с именами других приватных свойств родительского и дочернего классов.
Недостатки:
? Код становится менее изящным из-за необходимости определять все методы экземпляра в конструкторе (как минимум те, которым нужен доступ к приватным данным).
? Именно поэтому код тратит много памяти. Если использовались методы прототипов, они будут распределены.
Подробнее об этом подходе читайте в разделе Private Data in the Environment of a Constructor (Crockford Privacy Pattern) книги Speaking JavaScript.
2. Маркировка приватных свойств через соглашение об именовании
Следующий код хранит приватные данные в свойствах с префиксным подчеркиванием имен:
Преимущества:
? Код выглядит красиво.
? Можно использовать методы прототипов.
Недостатки:
? Небезопасно. Это всего лишь инструкция для клиентского кода.
? Имена приватных свойств могут конфликтовать.
3. Хранение приватных данных в WeakMaps
Этот метод совмещает в себе преимущества первого и второго подходов: безопасность и возможность использования методов прототипов. Для сохранения приватных данных используются WeakMaps _counter и _action:
Переменные _counter и _action хранят соответствие объектов своим приватным данным. Исходя из того, как работает WeakMaps, объекты могут удаляться сборщиком мусора. Приватные данные находятся в безопасности до тех пор, пока WeakMaps скрыт. Чтобы обезопасить себя, можно также сохранить WeakMap.prototype.get и WeakMap.prototype.set во временные переменные и вызывать их вместо динамического вызова методов. Даже если вредоносный код заменит эти методы теми, которые имеют доступ к приватным данным, на наш код это не повлияет. Однако защита распространяется только на код, который был запущен после нашего – защитить тот, который был запущен до него, к сожалению, невозможно.
Преимущества:
? Можно использовать методы прототипов.
? Безопаснее, чем соглашение об именовании для ключей свойств.
? Имена приватных свойств не могут конфликтовать.
Недостаток:
? Код не такой изящный, как при соглашении об именовании.
4. Использование символов в виде ключей для приватных свойств
Еще одно расположение хранилища приватных данных – это свойства с ключами в виде символов:
Свойство с ключом-символом никогда не будет конфликтовать с другим свойством, потому что каждый символ уникален. К тому же символы, хоть и не полностью, но скрыты от внешнего воздействия:
Преимущества:
? Можно использовать методы прототипов.
? Имена приватных свойств не могут конфликтовать.
Недостатки:
• По сравнению с соглашением об именовании, код не так изящен.
• Небезопасно, так как все ключи свойств объекта, включая символы, можно определить с помощью Reflect.ownKeys().
5. Дополнительная литература:
? Speaking. JavaScript, раздел Keeping Data Private;
? Exploring ES6, глава Classes;
? Exploring ES6, глава Symbols.
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)
vintage
02.03.2016 18:56-1Забыли написать, что заниматься отладкой кода с приватными членами сложнее. Или вы такой ерундой не занимаетесь? ;-)
Large
03.03.2016 02:55+2И еще забыли померить скорость работы, если это важно, то подход с подчеркиваниями остается единственным выбором (подход с символом тоже должен быть быстрым). Еще справедливо было бы написать про геттеры/сеттеры (проблему приватности не решает, но скрывать данные помогает)
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; } }
Igogo2012
Не безопасно и не изящно, но в любом случае пост полезен, спасибо!