- Оригинальный адрес:Private Variables in JavaScript
- Оригинальный автор:Marcus Noble
- Перевод с:Программа перевода самородков
- Постоянная ссылка на эту статью:GitHub.com/rare earth/gold-no…
- Переводчик:Noah Gao
- Корректор:старый профессор ryouaki
Частные переменные в JavaScript
В последнее время JavaScript претерпел множество улучшений, и постоянно добавляются новый синтаксис и функции. Но некоторые вещи не изменились, все по-прежнему является объектом, почти все можно изменить во время выполнения, и нет понятия общедоступных и частных свойств. Но мы можем использовать некоторые приемы, чтобы изменить эту ситуацию самостоятельно, в этой статье я представляю различные способы реализации приватных переменных.
В 2015 году JavaScriptДобрыйДля тех программистов, которые пришли из более традиционных языков языка C (таких как Java и C#), они будут более знакомы с этим способом работы. Но понятно, что эти классы не такие, как вы привыкли — их атрибуты не имеют модификаторов для управления доступом, и все атрибуты должны быть определены в функции.
Итак, как мы можем защитить данные, которые не должны изменяться во время выполнения? Давайте рассмотрим некоторые варианты.
В этой статье я буду повторно использовать пример класса для построения фигур. Его ширину и высоту можно задать только при инициализации, укажите свойство для получения площади. Для этих примеров используется
get
Для получения дополнительной информации о ключевых словах см. мою предыдущую статьюГеттеры и сеттеры.
соглашение об именовании
Первый и наиболее зрелый подход заключается в использовании определенного соглашения об именах, чтобы указать, что свойство следует считать частным. Обычно перед именами свойств ставится знак подчеркивания (например,_count
). На самом деле это не предотвращает доступ к переменной или ее изменение, но зависит от взаимопонимания между разработчиками в отношении того, что переменная должна рассматриваться как ограниченный доступ.
class Shape {
constructor(width, height) {
this._width = width;
this._height = height;
}
get area() {
return this._width * this._height;
}
}
const square = new Shape(10, 10);
console.log(square.area); // 100
console.log(square._width); // 10
WeakMap
Чтобы быть немного более строгим, вы можете использовать WeakMap для хранения всех частных значений. Это по-прежнему не препятствует доступу к данным, но отделяет частные значения от объектов, которыми может манипулировать пользователь. Для этого метода мы устанавливаем ключ WeakMap на экземпляр объекта, которому принадлежит частное свойство, и используем функцию (назовем ееinternal
) для создания или возврата объекта, в котором будут храниться все свойства. Преимущество этого метода заключается в обходе свойств или при выполненииJSON.stringify
Частные свойства экземпляра не раскрываются, но он зависит от переменной WeakMap, расположенной вне класса, к которой можно получить доступ и которой можно манипулировать.
const map = new WeakMap();
// 创建一个在每个实例中存储私有变量的对象
const internal = obj => {
if (!map.has(obj)) {
map.set(obj, {});
}
return map.get(obj);
}
class Shape {
constructor(width, height) {
internal(this).width = width;
internal(this).height = height;
}
get area() {
return internal(this).width * internal(this).height;
}
}
const square = new Shape(10, 10);
console.log(square.area); // 100
console.log(map.get(square)); // { height: 100, width: 100 }
Symbol
Реализация Symbol очень похожа на WeakMap. Здесь мы можем использоватьSymbolСоздайте свойство экземпляра в качестве ключа. Это предотвращает пересечение или использование свойстваJSON.stringify
видно когда. Однако этот метод требует создания символа для каждого частного свойства. Если вы можете получить доступ к символу вне класса, вы все равно можете получить частное свойство.
const widthSymbol = Symbol('width');
const heightSymbol = Symbol('height');
class Shape {
constructor(width, height) {
this[widthSymbol] = width;
this[heightSymbol] = height;
}
get area() {
return this[widthSymbol] * this[heightSymbol];
}
}
const square = new Shape(10, 10);
console.log(square.area); // 100
console.log(square.widthSymbol); // undefined
console.log(square[widthSymbol]); // 10
Закрытие
Все методы, показанные до сих пор, по-прежнему позволяют получить доступ к закрытым свойствам извне класса, а замыкания предоставляют нам обходной путь. Вы можете использовать замыкания с WeakMap или Symbol, если хотите, но этот подход также работает со стандартными объектами JavaScript. Идея замыканий состоит в том, чтобы инкапсулировать данные в пределах области действия функции, которая создается при ее вызове, но возвращать результат функции изнутри, делая эту область действия недоступной извне.
function Shape() {
// 私有变量集
const this$ = {};
class Shape {
constructor(width, height) {
this$.width = width;
this$.height = height;
}
get area() {
return this$.width * this$.height;
}
}
return new Shape(...arguments);
}
const square = new Shape(10, 10);
console.log(square.area); // 100
console.log(square.width); // undefined
С этой техникой есть небольшая проблема, теперь у нас есть два разныхShape
объект. Код вызовет внешнийShape
и взаимодействовать с ним, но возвращаемый экземпляр будет внутреннимShape
. Вероятно, в большинстве случаев это не имеет большого значения, но может привести кsquare instanceof Shape
выражение возвращаетfalse
, что может быть проблемой в вашем коде.
Решение этой проблемы состоит в том, чтобы установить внешний Shape в качестве прототипа возвращаемого экземпляра:
return Object.setPrototypeOf(new Shape(...arguments), this);
К сожалению, этого недостаточно, простое обновление этой строки теперьsquare.area
Рассматривается как неопределенное. Это потому чтоget
Ради ключевых слов, работающих за кулисами. Мы можем исправить это, вручную указав геттер в конструкторе.
function Shape() {
// 私有变量集
const this$ = {};
class Shape {
constructor(width, height) {
this$.width = width;
this$.height = height;
Object.defineProperty(this, 'area', {
get: function() {
return this$.width * this$.height;
}
});
}
}
return Object.setPrototypeOf(new Shape(...arguments), this);
}
const square = new Shape(10, 10);
console.log(square.area); // 100
console.log(square.width); // undefined
console.log(square instanceof Shape); // true
В качестве альтернативы мы можем положитьthis
Установите прототип прототипа экземпляра, чтобы мы могли использовать обаinstanceof
а такжеget
. В приведенном ниже примере у нас есть цепочка прототиповObject -> 外部的 Shape -> 内部的 Shape 原型 -> 内部的 Shape
.
function Shape() {
// 私有变量集
const this$ = {};
class Shape {
constructor(width, height) {
this$.width = width;
this$.height = height;
}
get area() {
return this$.width * this$.height;
}
}
const instance = new Shape(...arguments);
Object.setPrototypeOf(Object.getPrototypeOf(instance), this);
return instance;
}
const square = new Shape(10, 10);
console.log(square.area); // 100
console.log(square.width); // undefined
console.log(square instanceof Shape); // true
Proxy
Proxy— это замечательная новая функция в JavaScript, которая позволит вам эффективно обернуть объект в объект, называемый прокси, и перехватывать все взаимодействия с этим объектом. Мы будем использовать прокси и следовать вышеизложенному命名约定
для создания закрытых переменных, но сделать доступ к этим закрытым переменным ограниченным за пределами класса.
Прокси может перехватывать множество различных типов взаимодействий, но мы хотим сосредоточиться наget
а такжеset
, Proxy позволяет нам перехватывать операции чтения и записи для свойства соответственно. При создании прокси вы указываете два параметра: первый — это экземпляр, который вы собираетесь обернуть, а второй — объект «обработчик», который вы определяете для перехвата различных методов.
Наш процессор будет выглядеть так:
const handler = {
get: function(target, key) {
if (key[0] === '_') {
throw new Error('Attempt to access private property');
}
return target[key];
},
set: function(target, key, value) {
if (key[0] === '_') {
throw new Error('Attempt to access private property');
}
target[key] = value;
}
};
В каждом случае мы проверяем, начинается ли имя свойства, к которому осуществляется доступ, с символа подчеркивания, и если да, то выдаем ошибку, препятствующую доступу к нему.
class Shape {
constructor(width, height) {
this._width = width;
this._height = height;
}
get area() {
return this._width * this._height;
}
}
const handler = {
get: function(target, key) {
if (key[0] === '_') {
throw new Error('Attempt to access private property');
}
return target[key];
},
set: function(target, key, value) {
if (key[0] === '_') {
throw new Error('Attempt to access private property');
}
target[key] = value;
}
}
const square = new Proxy(new Shape(10, 10), handler);
console.log(square.area); // 100
console.log(square instanceof Shape); // true
square._width = 200; // 错误:试图访问私有属性
Как вы можете видеть в этом примере, мы оставляем за собой право использоватьinstanceof
способности, и неожиданных результатов не будет.
К сожалению, когда мы пытаемся выполнитьJSON.stringify
Проблема возникает из-за того, что он пытается отформатировать частное свойство. Чтобы исправить это, нам нужно переписатьtoJSON
функция для возврата только "общедоступных" свойств. Мы можем справиться с этим, обновив наш обработчик gettoJSON
Конкретный случай:
Примечание: это переопределит любой пользовательский
toJSON
функция.
get: function(target, key) {
if (key[0] === '_') {
throw new Error('Attempt to access private property');
} else if (key === 'toJSON') {
const obj = {};
for (const key in target) {
if (key[0] !== '_') { // 只复制公共属性
obj[key] = target[key];
}
}
return () => obj;
}
return target[key];
}
Теперь мы инкапсулировали наши частные свойства, и ожидаемая функциональность все еще существует, единственное предостережение заключается в том, что наши частные свойства все еще доступны для просмотра.for(const key in square)
перечислит_width
а также_height
. К счастью, процессор также доступен здесь! Мы также можем перехватитьgetOwnPropertyDescriptor
Результат вызова и управления нашей частной собственностью:
getOwnPropertyDescriptor(target, key) {
const desc = Object.getOwnPropertyDescriptor(target, key);
if (key[0] === '_') {
desc.enumerable = false;
}
return desc;
}
Теперь мы собираем все функции вместе:
class Shape {
constructor(width, height) {
this._width = width;
this._height = height;
}
get area() {
return this._width * this._height;
}
}
const handler = {
get: function(target, key) {
if (key[0] === '_') {
throw new Error('Attempt to access private property');
} else if (key === 'toJSON') {
const obj = {};
for (const key in target) {
if (key[0] !== '_') {
obj[key] = target[key];
}
}
return () => obj;
}
return target[key];
},
set: function(target, key, value) {
if (key[0] === '_') {
throw new Error('Attempt to access private property');
}
target[key] = value;
},
getOwnPropertyDescriptor(target, key) {
const desc = Object.getOwnPropertyDescriptor(target, key);
if (key[0] === '_') {
desc.enumerable = false;
}
return desc;
}
}
const square = new Proxy(new Shape(10, 10), handler);
console.log(square.area); // 100
console.log(square instanceof Shape); // true
console.log(JSON.stringify(square)); // "{}"
for (const key in square) { // No output
console.log(key);
}
square._width = 200; // 错误:试图访问私有属性
Прокси — это, безусловно, мой любимый способ создания частных свойств в JavaScript. Такие классы построены таким образом, с которым знакомы JS-разработчики старой школы, поэтому они совместимы со старым существующим кодом, заключая их в один и тот же обработчик Proxy.
Приложение: Обработка в TypeScript
TypeScriptЯвляется расширенным набором JavaScript, который компилируется в собственный JavaScript для использования в производстве. Разрешение указывать частные, общедоступные или защищенные свойства — одна из функций TypeScript.
class Shape {
private width;
private height;
constructor(width, height) {
this.width = width;
this.height = height;
}
get area() {
return this.width * this.height;
}
}
const square = new Shape(10, 10)
console.log(square.area); // 100
При использовании TypeScript важно отметить, что он работает только тогда, когдакомпилироватьЭти типы известны только во время компиляции, а модификаторы private и public вступают в силу только во время компиляции. если вы попытаетесь получить доступsquare.width
, вы обнаружите, что это действительно возможно. Просто TypeScript выдаст вам ошибку при компиляции, но не остановит компиляцию.
// 编译时错误:属性 ‘width’ 是私有的,只能在 ‘Shape’ 类中访问。
console.log(square.width); // 10
TypeScript не будет достаточно умен, чтобы попытаться предотвратить доступ кода к закрытым свойствам во время выполнения. Я просто перечисляю это здесь, чтобы люди поняли, что это не решает проблему напрямую. Ты сможешьпонаблюдайте за собойКод JavaScript, созданный TypeScript выше.
будущее
Я показал вам методы, которые вы можете использовать сейчас, но что насчет будущего? На самом деле, будущее выглядит интересно. В настоящее время есть предложение ввести классы в JavaScriptprivate fields, который использует#
Обозначение означает, что это личное. Он используется так же, как метод соглашения об именах, но предоставляет практические ограничения на доступ к переменным.
class Shape {
#height;
#width;
constructor(width, height) {
this.#width = width;
this.#height = height;
}
get area() {
return this.#width * this.#height;
}
}
const square = new Shape(10, 10);
console.log(square.area); // 100
console.log(square instanceof Shape); // true
console.log(square.#width); // 错误:私有属性只能在类中访问
Если вам это интересно, вы можете прочитать следующееполное предложениедля деталей, которые ближе к истине. Одна вещь, которую я нахожу интересной, заключается в том, что частные свойства должны быть определены заранее и не могут быть временно созданы или уничтожены. Мне кажется, что это очень чуждое понятие в JavaScript, поэтому будет интересно посмотреть, как это предложение продолжит развиваться. В настоящее время это предложение больше фокусируется на частных свойствах класса, а не на частных функциях или частных членах на уровне объекта, которые могут появиться позже.
пакет npm -- приватизировать
На момент написания этой статьи я также выпустил пакет NPM, помогающий создавать частные свойства —privatise. Я использовал метод Proxy, описанный выше, и добавил дополнительный обработчик, позволяющий передавать сам класс, а не экземпляр. Весь код можно найти на GitHub, любой PR или Issue приветствуется.
Программа перевода самородковэто сообщество, которое переводит высококачественные технические статьи из Интернета сНаггетсДелитесь статьями на английском языке на . Охват контентаAndroid,iOS,внешний интерфейс,задняя часть,блокчейн,товар,дизайн,искусственный интеллектЕсли вы хотите видеть более качественные переводы, пожалуйста, продолжайте обращать вниманиеПрограмма перевода самородков,официальный Вейбо,Знай колонку.