[Перевод] Приватные переменные в JavaScript

внешний интерфейс JavaScript Язык программирования Программа перевода самородков

Частные переменные в 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,внешний интерфейс,задняя часть,блокчейн,товар,дизайн,искусственный интеллектЕсли вы хотите видеть более качественные переводы, пожалуйста, продолжайте обращать вниманиеПрограмма перевода самородков,официальный Вейбо,Знай колонку.