Частная собственность в классах JavaScript ES6


444

Можно ли создать частные свойства в классах ES6?

Вот пример. Как я могу запретить доступ instance.property?

class Something {
  constructor(){
    this.property = "test";
  }
}

var instance = new Something();
console.log(instance.property); //=> "test"

5
Существует на самом деле стадия 3 предложения для этой функции - tc39.github.io/proposal-class-fields github.com/tc39/proposal-class-fields
арты

@arty Я дал ответ на этот вопрос с примерами: stackoverflow.com/a/52237988/1432509
Алистер,

Ответы:


165

Частные поля (и методы) внедряются в стандарт ECMA . Вы можете начать использовать их сегодня с предустановкой babel 7 и stage 3.

class Something {
  #property;

  constructor(){
    this.#property = "test";
  }

  #privateMethod() {
    return 'hello world';
  }

  getPrivateMessage() {
      return this.#privateMethod();
  }
}

const instance = new Something();
console.log(instance.property); //=> undefined
console.log(instance.privateMethod); //=> undefined
console.log(instance.getPrivateMessage()); //=> hello world

Мне интересно, как могут работать эти поля класса. В настоящее время вы не можете использовать thisв конструкторе перед вызовом super(). И все же Бабель ставит их перед супер.
seeker_of_bacon

Как настроить ESLint для разрешения #privateCrapсинтаксиса?
Мареки

6
А как насчет Эслинт? Я получил ошибку парсера при знаке равенства. Бабель работает, просто Эслинт не может разобрать этот новый синтаксис JS.
Martonx

6
Вау, это очень некрасиво. Хэштег является действительным персонажем. Собственность не очень частная или? .. Я проверил это в TypeScript. Закрытые члены не скомпилированы как приватные или только для чтения (извне). Просто объявлен как другая (публичная) собственность. (ES5).
Доминик

2
Как вы пишете частные методы с этим? Могу ли я сделать это #beep() {}:; и это: async #bzzzt() {}?
Константин Ван

277

Короткий ответ: нет, в классах ES6 нет встроенной поддержки частных свойств.

Но вы можете имитировать это поведение, не присоединяя новые свойства к объекту, а сохраняя их внутри конструктора класса, и используйте методы получения и установки для доступа к скрытым свойствам. Обратите внимание, что методы получения и установки получают переопределение для каждого нового экземпляра класса.

ES6

class Person {
    constructor(name) {
        var _name = name
        this.setName = function(name) { _name = name; }
        this.getName = function() { return _name; }
    }
}

ES5

function Person(name) {
    var _name = name
    this.setName = function(name) { _name = name; }
    this.getName = function() { return _name; }
}

1
Мне нравится это решение больше всего. Я согласен, что его не следует использовать для масштабирования, но он идеально подходит для классов, которые обычно создаются только один раз за включение.
Блейк Регалия

2
Также вы переопределяете каждый компонент этого класса каждый раз, когда создается новый.
Квентин Рой

10
Это так странно! В ES6 вы создаете больше «закрывающих пирамид», чем до ES6! Определение функций WITHIN в конструкторе выглядит хуже, чем в приведенном выше примере ES5.
Кокодоко

1
Поскольку OP специально задает вопросы о классах ES6, я лично считаю, что это плохое решение, хотя оно технически работает. Основным ограничением является то, что теперь каждый метод класса, который использует закрытые переменные, должен быть объявлен внутри конструктора, что серьезно подрывает преимущества наличия classсинтаксиса в первую очередь.
NanoWizard

10
Все, что это делает, это вводит косвенность. Теперь, как вы делаете getNameи setNameсвойства частными?
ау

195

Чтобы расширить ответ @ loganfsmyth:

Единственные действительно личные данные в JavaScript - это переменные в области видимости. Вы не можете иметь частные свойства в том смысле, что к свойствам обращаются изнутри так же, как к публичным свойствам, но вы можете использовать переменные области действия для хранения личных данных.

Переменные области

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

Пример:

function Person(name) {
  let age = 20; // this is private
  this.name = name; // this is public

  this.greet = function () {
    // here we can access both name and age
    console.log(`name: ${this.name}, age: ${age}`);
  };
}

let joe = new Person('Joe');
joe.greet();

// here we can access name but not age

Scaped WeakMap

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

Пример:

let Person = (function () {
  let privateProps = new WeakMap();

  class Person {
    constructor(name) {
      this.name = name; // this is public
      privateProps.set(this, {age: 20}); // this is private
    }

    greet() {
      // Here we can access both name and age
      console.log(`name: ${this.name}, age: ${privateProps.get(this).age}`);
    }
  }

  return Person;
})();

let joe = new Person('Joe');
joe.greet();

// here we can access joe's name but not age

В этом примере объект используется для использования одного WeakMap для нескольких частных свойств; Вы также можете использовать несколько WeakMaps и использовать их как age.set(this, 20), или написать небольшую оболочку и использовать его по-другому, как privateProps.set(this, 'age', 0).

Конфиденциальность этого подхода теоретически может быть нарушена путем вмешательства в глобальный WeakMapобъект. Тем не менее, весь JavaScript может быть сломан искаженными глобалами. Наш код уже построен на предположении, что этого не происходит.

(Этот метод также может быть реализован Map, но WeakMapлучше, потому что Mapон создаст утечки памяти, если вы не будете очень осторожны, и для этого они не отличаются друг от друга.)

Полуответ: символы в области видимости

Символ - это тип примитивного значения, которое может служить именем свойства. Вы можете использовать метод переменной области видимости, чтобы создать личный символ, а затем сохранить личные данные в this[mySymbol].

Конфиденциальность этого метода может быть нарушена с помощью Object.getOwnPropertySymbols, но это несколько неловко сделать.

Пример:

let Person = (function () {
  let ageKey = Symbol();

  class Person {
    constructor(name) {
      this.name = name; // this is public
      this[ageKey] = 20; // this is intended to be private
    }

    greet() {
      // Here we can access both name and age
      console.log(`name: ${this.name}, age: ${this[ageKey]}`);
    }
  }

  return Person;
})();

let joe = new Person('Joe');
joe.greet();

// Here we can access joe's name and, with a little effort, age. ageKey is
// not in scope, but we can obtain it by listing all Symbol properties on
// joe with `Object.getOwnPropertySymbols(joe)`.

Полуответ: Подчеркивает

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

Пример:

class Person {
  constructor(name) {
    this.name = name; // this is public
    this._age = 20; // this is intended to be private
  }

  greet() {
    // Here we can access both name and age
    console.log(`name: ${this.name}, age: ${this._age}`);
  }
}

let joe = new Person('Joe');
joe.greet();

// Here we can access both joe's name and age. But we know we aren't
// supposed to access his age, which just might stop us.

Вывод

Начиная с ES2017, до сих пор нет идеального способа сделать частную собственность. Различные подходы имеют свои плюсы и минусы. Переменные в области видимости являются действительно приватными; WeakMaps с областями видимости являются очень приватными и более практичными, чем переменные с областями видимости; Символы с определенной областью являются достаточно частными и достаточно практичными; подчеркивания часто бывают достаточно приватными и очень практичными.


7
Первый пример фрагмента («переменные области видимости») представляет собой общий антипаттерн - у каждого возвращаемого объекта будет свой класс. Не делай этого. Если вам нужны привилегированные методы, создайте их в конструкторе.
Берги

1
Обертывание класса внутри функции, кажется, сводит на нет всю цель использования классов в первую очередь. Если вы уже используете функцию для создания экземпляра, вы также можете поместить все свои закрытые / открытые члены в эту функцию и забыть обо всем ключевом слове класса.
Кокодоко

2
@Bergi @Kokodoko Я изменил подход к переменным в области видимости, чтобы он был немного быстрее и не ломался instanceof. Я признаю, что думал об этом подходе как о включенном только для полноты картины и должен был уделить больше внимания тому, на что он действительно способен.
Тристан

1
Отличное объяснение! Я все еще удивлен, что ES6 фактически усложнил имитацию закрытой переменной, где в ES5 вы могли просто использовать var, и это внутри функции для симуляции приватной и публичной переменных.
Кокодоко

2
@Kokodoko Если вы обойдетесь без класса и просто поместите все в функцию, вам также придется вернуться к реализации наследования с использованием метода prototype. Использование расширения для классов является гораздо более чистым подходом, поэтому использование класса внутри функции полностью приемлемо.
AndroidDev

117

Обновление: предложение с более приятным синтаксисом находится в процессе. Вклад приветствуется.


Да, есть - для ограниченного доступа в объектах - ES6 вводит Symbols .

Символы уникальны, вы не можете получить доступ к одному извне, кроме как с помощью рефлексии (как рядовые в Java / C #), но любой, кто имеет доступ к символу внутри, может использовать его для доступа к ключу:

var property = Symbol();
class Something {
    constructor(){
        this[property] = "test";
    }
}

var instance = new Something();

console.log(instance.property); //=> undefined, can only access with access to the Symbol

6
Вы не можете использовать Object.getOwnPropertySymbols? ;)
Qantas 94 Heavy

41
@BenjaminGruenbaum: очевидно символы больше не гарантируют истинную конфиденциальность: stackoverflow.com/a/22280202/1282216
d13

28
@trusktr через три ключа? Нет. Через символы? Да. Очень похоже на то, как вы можете использовать отражение в таких языках, как C # и Java, для доступа к закрытым полям. Модификаторы доступа - это не безопасность, а ясность намерений.
Бенджамин Грюнбаум

9
Кажется, что использование Symbols похоже на выполнение. const myPrivateMethod = Math.random(); Something.prototype[''+myPrivateMethod] = function () { ... } new Something()[''+myPrivateMethod]();Это на самом деле не конфиденциальность, а неясность в смысле традиционного JavaScript. Я бы подумал, что «частный» JavaScript означает использование замыканий для инкапсуляции переменных. Эти переменные, следовательно, не доступны через отражение.
trusktr

13
Кроме того , я чувствую , что использование privateи protectedключевых слов , было бы намного чище , чем Symbolили Name. Я предпочитаю точечную запись, а не скобочную. Я хотел бы продолжать использовать точку для личных вещей. this.privateVar
trusktr

33

Ответ - нет". Но вы можете создать частный доступ к таким свойствам:

  • Используйте модули. Все в модуле является частным, если оно не стало общедоступным с помощью exportключевого слова.
  • Внутри модулей используйте функцию закрытия: http://www.kirupa.com/html5/closures_in_javascript.htm

(Предложение о том, что символы можно использовать для обеспечения конфиденциальности, было верным в более ранней версии спецификации ES6, но это уже не так: https://mail.mozilla.org/pipermail/es-discuss/2014-January/035604. html и https://stackoverflow.com/a/22280202/1282216 . Более подробное обсуждение символов и конфиденциальности см. по адресу : https://curiosity-driven.org/private-properties-in-javascript ).


6
-1, это на самом деле не отвечает на ваш вопрос. (Вы также можете использовать замыкания с IIFE в ES5). Закрытые свойства перечисляются через отражение в большинстве языков (Java, C # и т. Д.). Суть частных свойств заключается в том, чтобы передавать намерения другим программистам, а не обеспечивать безопасность.
Бенджамин Грюнбаум

1
@ BenjaminGruenbaum, я знаю, я хотел бы получить лучший ответ, я тоже не доволен этим.
d13

Я думаю, что символы - все еще действительный способ достигнуть недоступных участников в среде программирования. Да, их все еще можно найти, если вы действительно хотите, но не в этом суть? Вы не должны хранить конфиденциальную информацию в нем, но вы все равно не должны делать это в коде на стороне клиента. Но это работает с целью скрыть свойство или метод от внешнего класса.
Кокодоко

Использование переменных, ограниченных областью действия на уровне модуля, в качестве замены закрытых свойств в классе приведет к поведению singleton.behavior или поведению, аналогичному свойствам statitc. Экземпляры vars будут разделены.
Адриан Мойса

30

Единственный способ получить истинную конфиденциальность в JS - это ограничить область действия, поэтому нет способа иметь свойство, являющееся членом, thisкоторое будет доступно только внутри компонента. Лучший способ хранить действительно личные данные в ES6 - это WeakMap.

const privateProp1 = new WeakMap();
const privateProp2 = new WeakMap();

class SomeClass {
  constructor() {
    privateProp1.set(this, "I am Private1");
    privateProp2.set(this, "I am Private2");

    this.publicVar = "I am public";
    this.publicMethod = () => {
      console.log(privateProp1.get(this), privateProp2.get(this))
    };        
  }

  printPrivate() {
    console.log(privateProp1.get(this));
  }
}

Очевидно, что это, вероятно, медленно и определенно некрасиво, но обеспечивает конфиденциальность.

Имейте в виду, что ДАЖЕ ЭТО не идеально, потому что Javascript настолько динамичен. Кто-то еще мог сделать

var oldSet = WeakMap.prototype.set;
WeakMap.prototype.set = function(key, value){
    // Store 'this', 'key', and 'value'
    return oldSet.call(this, key, value);
};

чтобы ловить значения по мере их хранения, поэтому, если вы хотите быть очень осторожными, вам нужно будет захватить локальную ссылку .setи .getиспользовать ее явно, вместо того чтобы полагаться на перезаписываемый прототип.

const {set: WMSet, get: WMGet} = WeakMap.prototype;

const privateProp1 = new WeakMap();
const privateProp2 = new WeakMap();

class SomeClass {
  constructor() {
    WMSet.call(privateProp1, this, "I am Private1");
    WMSet.call(privateProp2, this, "I am Private2");

    this.publicVar = "I am public";
    this.publicMethod = () => {
      console.log(WMGet.call(privateProp1, this), WMGet.call(privateProp2, this))
    };        
  }

  printPrivate() {
    console.log(WMGet.call(privateProp1, this));
  }
}

3
Как совет, вы можете избежать использования одной слабой карты для каждого свойства, используя объект в качестве значения. Таким образом, вы также можете уменьшить количество карт getдо одного на метод (например const _ = privates.get(this); console.log(_.privateProp1);).
Квентин Рой

Да, это тоже вариант. Я в основном пошел с этим, поскольку он более точно соответствует тому, что написал бы пользователь при использовании реальных свойств.
loganfsmyth

@loganfsmyth const myObj = new SomeClass(); console.log(privateProp1.get(myObj)) // "I am Private1"это означает, что ваша собственность частная или нет?
Барбу Барбу

2
Чтобы это работало, коду, обращающемуся к свойству, потребуется доступ к объекту WeakMap, который обычно находится в пределах модуля и недоступен
loganfsmyth

22

Для дальнейшего ознакомления с другими пользователями я слышу, что рекомендуется использовать WeakMaps для хранения личных данных.

Вот более понятный рабочий пример:

function storePrivateProperties(a, b, c, d) {
  let privateData = new WeakMap;
  // unique object as key, weak map can only accept object as key, when key is no longer referened, garbage collector claims the key-value 
  let keyA = {}, keyB = {}, keyC = {}, keyD = {};

  privateData.set(keyA, a);
  privateData.set(keyB, b);
  privateData.set(keyC, c);
  privateData.set(keyD, d);

  return {
    logPrivateKey(key) {
      switch(key) {
      case "a":
        console.log(privateData.get(keyA));
        break;
      case "b":
        console.log(privateData.get(keyB));
        break;
      case "c":
        console.log(privateData.get(keyC));
        break;
      case "d":
        console.log(privateData.set(keyD));
        break;
      default:
        console.log(`There is no value for ${key}`)
      }
    }
  }
}

20
Имейте в виду, что эти свойства являются статическими.
Майкл Териот

8
Я не отрицал вас, но ваш пример карты слабоват полностью.
Бенджамин Грюнбаум

4
А именно - вы делитесь данными между всеми экземплярами классов, а не для каждого экземпляра - могу я хотя бы исправить это?
Бенджамин Грюнбаум

1
Действительно, слабая карта должна быть привязана к данному экземпляру. См. Fitzgeraldnick.com/weblog/53 для примера.
видела

2
Согласно MDN, примитивные типы данных, такие как символы, не допускаются в качестве ключа WeakMap. Документация по
слабой

12

Зависит от того, кого ты спрашиваешь :-)

Нет privateМодификатор свойства не входят в минимальных классах МАКСИМАЛЬНО предложение , которое , кажется, сделал это в текущий проект .

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


3
Это весьма маловероятно , что частные имена будут делать это в ES6, хотя они думают о той или иной форме частной вещи для ES7.
Qantas 94 Heavy

@ Qantas94Heavy и частные имена и уникальные строковые значения были заменены символами из того, что я понимаю.
Бенджамин Грюнбаум

Да, это, вероятно, станет Символами. Однако на самом деле «символы», содержащиеся в настоящее время в спецификации, используются только для описания внутренних свойств, таких как [[prototype]], и нет способа создать и использовать их в пользовательском коде. Вы знаете некоторые документы?
Берги

Я только что понял, что модули могут быть использованы для настройки конфиденциальности. В сочетании с символами, которые могут быть все, что вам когда-либо нужно ...?
d13

1
@Cody: Ваш код модуля в любом случае имеет свою собственную область в ES6, нет необходимости в IEFE. И да, символы предназначены для уникальности (предотвращения столкновений), а не для конфиденциальности.
Берги

10

Использование модулей ES6 (первоначально предложенных @ d13) хорошо работает для меня. Он не полностью имитирует частные свойства, но, по крайней мере, вы можете быть уверены, что свойства, которые должны быть частными, не будут вытекать за пределы вашего класса. Вот пример:

something.js

let _message = null;
const _greet = name => {
  console.log('Hello ' + name);
};

export default class Something {
  constructor(message) {
    _message = message;
  }

  say() {
    console.log(_message);
    _greet('Bob');
  }
};

Тогда код потребления может выглядеть так:

import Something from './something.js';

const something = new Something('Sunny day!');
something.say();
something._message; // undefined
something._greet(); // exception

Обновление (важно):

Как отметил @DanyalAytekin в комментариях, эти частные свойства являются статическими, поэтому имеют глобальный охват. Они будут хорошо работать при работе с синглетонами, но нужно соблюдать осторожность для временных объектов. Расширяя пример выше:

import Something from './something.js';
import Something2 from './something.js';

const a = new Something('a');
a.say(); // a

const b = new Something('b');
b.say(); // b

const c = new Something2('c');
c.say(); // c

a.say(); // c
b.say(); // c
c.say(); // c

4
Хорошо для private static.
Данял Айтекин

@DanyalAytekin: это очень хороший момент. Эти частные свойства являются статическими, поэтому они имеют глобальный характер. Я обновил свой ответ, чтобы отразить это.
Джонни Ошика

Чем больше я узнаю о функциональном программировании (особенно Elm и Haskell), тем больше я верю, что JS-программисты выиграют от модульного подхода к «модульности», а не от ООП-класса. Если мы думаем о модулях ES6 как об основах для создания приложений и полностью забываем о классах, я считаю, что в итоге мы можем получить гораздо лучшие приложения. Могут ли опытные пользователи Elm или Haskell прокомментировать этот подход?
13

1
В обновлении второе a.say(); // aдолжно бытьb.say(); // b
grokky

Попробовал, let _message = nullкстати, не так круто, когда вызывал конструктор несколько раз, он все испортил.
Littlee

9

Завершение @ d13 и комментарии @ johnny-oshika и @DanyalAytekin:

Я предполагаю, что в примере, представленном @ johnny-oshika, мы могли бы использовать обычные функции вместо функций стрелок, а затем .bindих с текущим объектом и _privatesобъектом в качестве параметра карри:

something.js

function _greet(_privates) {
  return 'Hello ' + _privates.message;
}

function _updateMessage(_privates, newMessage) {
  _privates.message = newMessage;
}

export default class Something {
  constructor(message) {
    const _privates = {
      message
    };

    this.say = _greet.bind(this, _privates);
    this.updateMessage = _updateMessage.bind(this, _privates);
  }
}

main.js

import Something from './something.js';

const something = new Something('Sunny day!');

const message1 = something.say();
something.updateMessage('Cloudy day!');
const message2 = something.say();

console.log(message1 === 'Hello Sunny day!');  // true
console.log(message2 === 'Hello Cloudy day!');  // true

// the followings are not public
console.log(something._greet === undefined);  // true
console.log(something._privates === undefined);  // true
console.log(something._updateMessage === undefined);  // true

// another instance which doesn't share the _privates
const something2 = new Something('another Sunny day!');

const message3 = something2.say();

console.log(message3 === 'Hello another Sunny day!'); // true

Преимущества, которые я могу придумать:

  • мы можем иметь приватные методы ( _greetи _updateMessageдействовать как приватные методы, пока у нас нет exportссылок)
  • хотя они не относятся к прототипу, вышеупомянутые методы сохранят память, потому что экземпляры создаются один раз, вне класса (в отличие от определения их в конструкторе)
  • мы не пропускаем глобалы, так как мы находимся внутри модуля
  • мы также можем иметь частные свойства, используя связанный _privatesобъект

Некоторые недостатки, которые я могу вспомнить:

Работающий фрагмент можно найти здесь: http://www.webpackbin.com/NJgI5J8lZ


8

Да - вы можете создать инкапсулированное свойство , но это не было сделано с помощью модификаторов доступа (public | private), по крайней мере, с ES6.

Вот простой пример того, как это можно сделать с ES6:

1 Создайте класс, используя слово класса

2 Внутри его конструктора объявите блочную переменную, используя let OR const зарезервированные слова ->, так как они являются областью видимости блока, к ним нельзя получить доступ извне (инкапсулировано)

3 Чтобы разрешить некоторый контроль доступа (setters | getters) к этим переменным, вы можете объявить метод экземпляра внутри его конструктора, используя: this.methodName=function(){}синтаксис

"use strict";
    class Something{
        constructor(){
            //private property
            let property="test";
            //private final (immutable) property
            const property2="test2";
            //public getter
            this.getProperty2=function(){
                return property2;
            }
            //public getter
            this.getProperty=function(){
                return property;
            }
            //public setter
            this.setProperty=function(prop){
                property=prop;
            }
        }
    }

Теперь давайте проверим это:

var s=new Something();
    console.log(typeof s.property);//undefined 
    s.setProperty("another");//set to encapsulated `property`
    console.log(s.getProperty());//get encapsulated `property` value
    console.log(s.getProperty2());//get encapsulated immutable `property2` value

1
Это (пока) единственное решение этой проблемы, несмотря на то, что все методы, объявленные в конструкторе, повторно объявлены для каждого экземпляра класса. Это довольно плохая идея относительно производительности и использования памяти. Методы класса должны быть объявлены вне области конструктора.
Freezystem

@Freezystem First: во- первых, это методы экземпляра (а не методы класса). Второй вопрос ОП был: _ Как я могу запретить доступ к instance.property?_, и мой ответ: пример того, как ... В- третьих, если у вас есть идея получше - давайте послушаем
Никита Куртин,

1
Я не говорил, что вы не правы, я сказал, что ваше решение было лучшим компромиссом для достижения приватной переменной, несмотря на то, что копия каждого метода экземпляра создается каждый раз, когда вы вызываете, new Something();потому что ваши методы объявлены в конструкторе для доступа к этим частные переменные. Это может привести к значительному потреблению памяти, если вы создаете много экземпляров своего класса, поэтому проблемы с производительностью. Методы должны были быть объявлены вне области конструктора. Мой комментарий был скорее объяснением ваших недостатков, чем критикой.
Freezystem

1
Но разве это не плохая практика - определять весь класс внутри конструктора? Разве мы не просто взломали JavaScript сейчас? Просто посмотрите на любой другой язык программирования ООП, и вы увидите, что конструктор не предназначен для определения класса.
Кокодоко

1
Да, это то, что я имел в виду, и ваше решение работает! Я просто говорю, что в целом я удивлен, что ES6 добавил ключевое слово 'class', но удалил элегантное решение работы с var и этим для достижения инкапсуляции.
Кокодоко

8

Другой подход к «частному»

Вместо того чтобы бороться с тем фактом, что частная видимость в настоящее время недоступна в ES6, я решил использовать более практичный подход, который прекрасно работает, если ваша IDE поддерживает JSDoc (например, Webstorm). Идея состоит в том, чтобы использовать @privateтег . Что касается разработки, IDE не позволит вам получить доступ к любому частному члену вне его класса. Для меня это очень хорошо работает, и это действительно полезно для сокрытия внутренних методов, поэтому функция автозаполнения показывает мне, что на самом деле хотел показать класс. Вот пример:

автозаполнение показывает только публичные вещи


1
Проблема в том, что мы не хотим получать доступ к закрытым переменным через Редактор, мы не хотим защищать закрытые переменные извне - и это то, что делает public / private. Если ваш код закончен, вы можете получить доступ (и важно подумать: переопределить ) к этим переменным извне класса. Ваш @privateкомментарий не может помешать этому, это только функция для создания документации, и вы IDE.
Адриан Пройс

Да, я знаю об этом. Просто этого достаточно для меня и может быть достаточно для других людей. Я знаю, что это не делает мои переменные приватными; я лишь предупреждаю меня, чтобы я не пытался получить к нему доступ извне (конечно, только если мы с моей командой используем IDE, поддерживающую эту функцию). Javascript (и другие языки, такие как Python) не были разработаны с учетом уровней доступа. Люди делают разные вещи, чтобы как-то реализовать эту функциональность, но в итоге мы просто взломали язык, чтобы достичь этого. Я решил пойти с более "естественным" подходом, если хотите.
Лусио Пайва

6

WeakMap

  • поддерживается в IE11 (символы нет)
  • hard-private (реквизиты, использующие символы, из-за soft-private Object.getOwnPropertySymbols)
  • может выглядеть действительно чисто (в отличие от замыканий, которые требуют все подпорки и методы в конструкторе)

Сначала определим функцию для переноса WeakMap:

function Private() {
  const map = new WeakMap();
  return obj => {
    let props = map.get(obj);
    if (!props) {
      props = {};
      map.set(obj, props);
    }
    return props;
  };
}

Затем создайте ссылку за пределами вашего класса:

const p = new Private();

class Person {
  constructor(name, age) {
    this.name = name;
    p(this).age = age; // it's easy to set a private variable
  }

  getAge() {
    return p(this).age; // and get a private variable
  }
}

Примечание: класс не поддерживается IE11, но в примере он выглядит чище.


6

О, так много экзотических решений! Меня обычно не волнует конфиденциальность, поэтому я использую «псевдо-конфиденциальность», как здесь сказано . Но если все равно (если для этого есть особые требования), я использую что-то вроде этого:

class jobImpl{
  // public
  constructor(name){
    this.name = name;
  }
  // public
  do(time){
    console.log(`${this.name} started at ${time}`);
    this.prepare();
    this.execute();
  }
  //public
  stop(time){
    this.finish();
    console.log(`${this.name} finished at ${time}`);
  }
  // private
  prepare(){ console.log('prepare..'); }
  // private
  execute(){ console.log('execute..'); }
  // private
  finish(){ console.log('finish..'); }
}

function Job(name){
  var impl = new jobImpl(name);
  return {
    do: time => impl.do(time),
    stop: time => impl.stop(time)
  };
}

// Test:
// create class "Job"
var j = new Job("Digging a ditch");
// call public members..
j.do("08:00am");
j.stop("06:00pm");

// try to call private members or fields..
console.log(j.name); // undefined
j.execute(); // error

Еще одна возможная реализация функции (конструктор) Job:

function Job(name){
  var impl = new jobImpl(name);
  this.do = time => impl.do(time),
  this.stop = time => impl.stop(time)
}

5

Лично мне нравится предложение оператора связывания, :: а затем я бы объединил его с упомянутым решением @ d13, но сейчас придерживаюсь ответа @ d13, где вы используете exportключевое слово для своего класса и помещаете частные функции в модуль.

есть еще одно жесткое решение, которое не было упомянуто здесь, а это более функциональный подход, который позволил бы иметь все частные методы / методы в классе.

Private.js

export const get = state => key => state[key];
export const set = state => (key,value) => { state[key] = value; }

Test.js

import { get, set } from './utils/Private'
export default class Test {
  constructor(initialState = {}) {
    const _set = this.set = set(initialState);
    const _get = this.get = get(initialState);

    this.set('privateMethod', () => _get('propValue'));
  }

  showProp() {
    return this.get('privateMethod')();
  }
}

let one = new Test({ propValue: 5});
let two = new Test({ propValue: 8});
two.showProp(); // 8
one.showProp(); // 5

комментарии по этому вопросу будут оценены.


Вообще мне нравится подход. Обратная связь: 1. Для предотвращения конфликтов вам понадобится отдельный модуль private.js для каждого класса. 2. Мне не нравится возможность сделать конструктор действительно длинным, встроенным определением каждого из ваших личных методов. 3. Было бы хорошо, если бы все методы класса были в одном файле.
Дуг Кобурн

5

Я наткнулся на этот пост, когда искал лучшую практику для «личных данных для классов». Было упомянуто, что у некоторых шаблонов будут проблемы с производительностью.

Я собрал несколько тестов jsperf, основанных на 4 основных шаблонах из онлайн-книги «Исследование ES6»:

http://exploringjs.com/es6/ch_classes.html#sec_private-data-for-classes

Тесты можно найти здесь:

https://jsperf.com/private-data-for-classes

В Chrome 63.0.3239 / Mac OS X 10.11.6 наиболее эффективными шаблонами были «Личные данные через среды конструктора» и «Личные данные через соглашение об именах». Для меня Safari показала хорошие результаты для WeakMap, но Chrome не так хорошо.

Я не знаю влияния на память, но шаблон для «среды конструктора», который некоторые предупреждали, будет проблемой производительности, был очень производительным.

4 основных шаблона:

Частные данные в среде конструктора

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

Частные данные в среде конструктора 2

class Countdown {
    constructor(counter, action) {
        this.dec = function dec() {
            if (counter < 1) return;
            counter--;
            if (counter === 0) {
                action();
            }
        }
    }
}
const c = new Countdown(2, () => {});
c.dec();
c.dec();

Личные данные через соглашение об именах

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();
        }
    }
}
const c = new Countdown(2, () => {});
c.dec();
c.dec();

Личные данные через WeakMaps

const _counter = new WeakMap();
const _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)();
        }
    }
}
const c = new Countdown(2, () => {});
c.dec();
c.dec();

Личные данные через символы

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]();
        }
    }
}
const c = new Countdown(2, () => {});
c.dec();
c.dec();

4

Я считаю, что можно получить «лучшее из обоих миров», используя замыкания внутри конструкторов. Есть два варианта:

Все данные члены являются частными

function myFunc() {
   console.log('Value of x: ' + this.x);
   this.myPrivateFunc();
}

function myPrivateFunc() {
   console.log('Enhanced value of x: ' + (this.x + 1));
}

class Test {
   constructor() {

      let internal = {
         x : 2,
      };
      
      internal.myPrivateFunc = myPrivateFunc.bind(internal);
      
      this.myFunc = myFunc.bind(internal);
   }
};

Некоторые участники являются частными

ПРИМЕЧАНИЕ: это по общему признанию некрасиво. Если вы знаете лучшее решение, отредактируйте этот ответ.

function myFunc(priv, pub) {
   pub.y = 3; // The Test object now gets a member 'y' with value 3.
   console.log('Value of x: ' + priv.x);
   this.myPrivateFunc();
}

function myPrivateFunc() {
   pub.z = 5; // The Test object now gets a member 'z' with value 3.
   console.log('Enhanced value of x: ' + (priv.x + 1));
}

class Test {
   constructor() {
      
      let self = this;

      let internal = {
         x : 2,
      };
      
      internal.myPrivateFunc = myPrivateFunc.bind(null, internal, self);
      
      this.myFunc = myFunc.bind(null, internal, self);
   }
};


4

На самом деле это возможно с помощью символов и прокси. Вы используете символы в области видимости класса и устанавливаете две ловушки в прокси: одну для прототипа класса, чтобы Reflect.ownKeys (instance) или Object.getOwnPropertySymbols не передавали ваши символы, а другая - для самого конструктора. поэтому, когда new ClassName(attrs)вызывается, возвращаемый экземпляр будет перехвачен и заблокирован символы собственных свойств. Вот код:

const Human = (function() {
  const pet = Symbol();
  const greet = Symbol();

  const Human = privatizeSymbolsInFn(function(name) {
    this.name = name; // public
    this[pet] = 'dog'; // private 
  });

  Human.prototype = privatizeSymbolsInObj({
    [greet]() { // private
      return 'Hi there!';
    },
    revealSecrets() {
      console.log(this[greet]() + ` The pet is a ${this[pet]}`);
    }
  });

  return Human;
})();

const bob = new Human('Bob');

console.assert(bob instanceof Human);
console.assert(Reflect.ownKeys(bob).length === 1) // only ['name']
console.assert(Reflect.ownKeys(Human.prototype).length === 1 ) // only ['revealSecrets']


// Setting up the traps inside proxies:
function privatizeSymbolsInObj(target) { 
  return new Proxy(target, { ownKeys: Object.getOwnPropertyNames });
}

function privatizeSymbolsInFn(Class) {
  function construct(TargetClass, argsList) {
    const instance = new TargetClass(...argsList);
    return privatizeSymbolsInObj(instance);
  }
  return new Proxy(Class, { construct });
}

Reflect.ownKeys()работает так: Object.getOwnPropertyNames(myObj).concat(Object.getOwnPropertySymbols(myObj))вот почему нам нужна ловушка для этих объектов.


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

4

Даже Typescript не может этого сделать. Из их документации :

Когда член помечен как закрытый, к нему нельзя получить доступ извне его содержащего класса. Например:

class Animal {
    private name: string;
    constructor(theName: string) { this.name = theName; }
}

new Animal("Cat").name; // Error: 'name' is private;

Но переносится на их площадку это дает:

var Animal = (function () {
    function Animal(theName) {
        this.name = theName;
    }
    return Animal;
}());
console.log(new Animal("Cat").name);

Поэтому их «личное» ключевое слово неэффективно.


2
Ну, это все еще эффективно, потому что это предотвращает "плохое" программирование, в то время как в IDE. Он показывает, какие члены вы должны и не должны использовать. Я думаю, что это главная причина использования частного и публичного. (Например, когда вы компилируете C # в машинный код, private все равно будет приватным? Кто знает?). Читая другие ответы, кажется, что использование @Symbol также может сделать член недоступным. Но даже символы все еще можно найти с консоли.
Кокодоко

Возникает ли ошибка TypeScript при переносе TypeScript в JavaScript? (Подобно тому, как проверка типов происходит во время передачи. Вместо какого-то частного механизма времени выполнения.)
Eljay

4

Пришел очень поздно на эту вечеринку, но я задал вопрос OP в поиске, так что ... Да, вы можете иметь частные свойства, заключив объявление класса в закрытие

Существует пример того, как у меня есть частные методы в этом коде . В приведенном ниже фрагменте класс Subscribeable имеет две «приватные» функции processи processCallbacks. Любые свойства могут быть добавлены таким образом, и они остаются закрытыми благодаря использованию замыкания. Конфиденциальность IMO является редкой необходимостью, если проблемы хорошо разделены, и Javascript не нужно раздуваться, добавляя больше синтаксиса, когда замыкание аккуратно делает свою работу.

const Subscribable = (function(){

  const process = (self, eventName, args) => {
    self.processing.set(eventName, setTimeout(() => processCallbacks(self, eventName, args)))};

  const processCallbacks = (self, eventName, args) => {
    if (self.callingBack.get(eventName).length > 0){
      const [nextCallback, ...callingBack] = self.callingBack.get(eventName);
      self.callingBack.set(eventName, callingBack);
      process(self, eventName, args);
      nextCallback(...args)}
    else {
      delete self.processing.delete(eventName)}};

  return class {
    constructor(){
      this.callingBack = new Map();
      this.processing = new Map();
      this.toCallbacks = new Map()}

    subscribe(eventName, callback){
      const callbacks = this.unsubscribe(eventName, callback);
      this.toCallbacks.set(eventName,  [...callbacks, callback]);
      return () => this.unsubscribe(eventName, callback)}  // callable to unsubscribe for convenience

    unsubscribe(eventName, callback){
      let callbacks = this.toCallbacks.get(eventName) || [];
      callbacks = callbacks.filter(subscribedCallback => subscribedCallback !== callback);
      if (callbacks.length > 0) {
        this.toCallbacks.set(eventName, callbacks)}
      else {
        this.toCallbacks.delete(eventName)}
      return callbacks}

    emit(eventName, ...args){
      this.callingBack.set(eventName, this.toCallbacks.get(eventName) || []);
      if (!this.processing.has(eventName)){
        process(this, eventName, args)}}}})();

Мне нравится этот подход, потому что он хорошо разделяет проблемы и сохраняет конфиденциальность. Единственным недостатком является необходимость использовать «я» (или что-то подобное), чтобы ссылаться на «это» в частном контенте.


4

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

Однако, если по какой-то причине вам нужно предотвратить доступ с Object.getOwnPropertySymbols()помощью метода, который я рассмотрел, используется присоединение уникального, неконфигурируемого, не перечисляемого, недоступного для записи свойства, которое можно использовать в качестве идентификатора свойства для каждого объекта в конструкции. (например, уникальное Symbol, если у вас еще нет другого уникального свойства, такого как id). Затем просто сохраните карту «частных» переменных каждого объекта, используя этот идентификатор.

const privateVars = {};

class Something {
    constructor(){
        Object.defineProperty(this, '_sym', {
            configurable: false,
            enumerable: false,
            writable: false,
            value: Symbol()
        });

        var myPrivateVars = {
            privateProperty: "I'm hidden"
        };

        privateVars[this._sym] = myPrivateVars;

        this.property = "I'm public";
    }

    getPrivateProperty() {
        return privateVars[this._sym].privateProperty;
    }

    // A clean up method of some kind is necessary since the
    // variables won't be cleaned up from memory automatically
    // when the object is garbage collected
    destroy() {
        delete privateVars[this._sym];
    }
}

var instance = new Something();
console.log(instance.property); //=> "I'm public"
console.log(instance.privateProperty); //=> undefined
console.log(instance.getPrivateProperty()); //=> "I'm hidden"

Потенциальным преимуществом этого подхода по сравнению с использованием WeakMapявляется более быстрое время доступа, если производительность становится проблемой.


1
Поправьте меня, если я ошибаюсь, но не будет ли этот код содержать утечки памяти, так как privateVars по-прежнему будет хранить приватные переменные объекта, даже если объект уже уничтожен?
Рассел Сантос

@RussellSantos вы правы, если предположить, что объекты в какой-то момент будут собирать мусор. Спасибо за указание на это. В моем примере я добавил destroy()метод, который должен вызываться кодом использования всякий раз, когда требуется удалить объект.
NanoWizard

4

Да, вполне может, и довольно легко тоже. Это делается путем предоставления ваших личных переменных и функций путем возврата графа прототипа объекта в конструктор. В этом нет ничего нового, но потратьте немного js foo, чтобы понять его элегантность. Этот способ не использует глобальные области видимости или слабые карты. Это форма отражения, встроенная в язык. В зависимости от того, как вы используете это; Можно либо вызвать исключение, которое прерывает стек вызовов, либо похоронить исключение как undefined. Это показано ниже, и вы можете узнать больше об этих функциях здесь

class Clazz {
  constructor() {
    var _level = 1

    function _private(x) {
      return _level * x;
    }
    return {
      level: _level,
      public: this.private,
      public2: function(x) {
        return _private(x);
      },
      public3: function(x) {
        return _private(x) * this.public(x);
      },
    };
  }

  private(x) {
    return x * x;
  }
}

var clazz = new Clazz();

console.log(clazz._level); //undefined
console.log(clazz._private); // undefined
console.log(clazz.level); // 1
console.log(clazz.public(1)); //1
console.log(clazz.public2(2)); //2
console.log(clazz.public3(3)); //27
console.log(clazz.private(0)); //error


3
class Something {
  constructor(){
    var _property = "test";
    Object.defineProperty(this, "property", {
        get: function(){ return _property}
    });
  }
}

var instance = new Something();
console.log(instance.property); //=> "test"
instance.property = "can read from outside, but can't write";
console.log(instance.property); //=> "test"

2
Лучше избегать только ответов кода. Было бы лучше, если бы вы могли объяснить, как ваш код отвечает на вопрос ОП
Stewart_R

Это действительно, как сделать переменную только для чтения больше, чем личную переменную. Закрытая переменная не должна быть доступна извне. console.log(instance.property)должен бросить или дать вам неопределенный, а не вернуть вам «тест».
оооаяя

3

Еще один способ, похожий на последние два опубликованных

class Example {
  constructor(foo) {

    // privates
    const self = this;
    this.foo = foo;

    // public interface
    return self.public;
  }

  public = {
    // empty data
    nodata: { data: [] },
    // noop
    noop: () => {},
  }

  // everything else private
  bar = 10
}

const test = new Example('FOO');
console.log(test.foo); // undefined
console.log(test.noop); // { data: [] }
console.log(test.bar); // undefined

2

Большинство ответов либо говорят, что это невозможно, либо требуют использования WeakMap или Symbol, которые являются функциями ES6, для которых, вероятно, потребуются полифилы. Однако есть и другой способ! Проверьте это:

// 1. Create closure
var SomeClass = function() {
  // 2. Create `key` inside a closure
  var key = {};
  // Function to create private storage
  var private = function() {
    var obj = {};
    // return Function to access private storage using `key`
    return function(testkey) {
      if(key === testkey) return obj;
      // If `key` is wrong, then storage cannot be accessed
      console.error('Cannot access private properties');
      return undefined;
    };
  };
  var SomeClass = function() {
    // 3. Create private storage
    this._ = private();
    // 4. Access private storage using the `key`
    this._(key).priv_prop = 200;
  };
  SomeClass.prototype.test = function() {
    console.log(this._(key).priv_prop); // Using property from prototype
  };
  return SomeClass;
}();

// Can access private property from within prototype
var instance = new SomeClass();
instance.test(); // `200` logged

// Cannot access private property from outside of the closure
var wrong_key = {};
instance._(wrong_key); // undefined; error logged

Я называю этот метод Accessor Pattern . Основная идея заключается в том, что у нас есть замыкание , ключ внутри замыкания, и мы создаем частный объект (в конструкторе), к которому можно получить доступ только при наличии ключа .

Если вы заинтересованы, вы можете прочитать больше об этом в моей статье . Используя этот метод, вы можете создавать для каждого объекта свойства, к которым нельзя получить доступ за пределами замыкания. Следовательно, вы можете использовать их в конструкторе или прототипе, но не где-либо еще. Я нигде не видел, чтобы этот метод использовался, но я думаю, что он действительно мощный.


Вопрос был о том, как этого добиться в классах ES6.
Майкл

Вы можете использовать точно такой же метод в классах ES6. Классы ES6 - это в основном просто сахар поверх функций, которые я представил в своем примере. Вполне возможно, что оригинальный постер использует транспортер, и в этом случае WeakMaps или Symbols по-прежнему требуют полифилов. Мой ответ действителен независимо.
гитарино

2

Посмотрите этот ответ для чистого и простого «классного» решения с приватным и общедоступным интерфейсом и поддержкой композиции.


2

Я нашел очень простое решение, просто используйте Object.freeze(). Конечно, проблема в том, что вы не можете ничего добавить к объекту позже.

class Cat {
    constructor(name ,age) {
        this.name = name
        this.age = age
        Object.freeze(this)
    }
}

let cat = new Cat('Garfield', 5)
cat.age = 6 // doesn't work, even throws an error in strict mode

это также отключит метод установки, такой какsetName(name) { this.name = name; }
ngakak

2

Я использую этот шаблон, и он всегда работал для меня

class Test {
    constructor(data) {
        class Public {
            constructor(prv) {

                // public function (must be in constructor on order to access "prv" variable)
                connectToDb(ip) {
                    prv._db(ip, prv._err);
                } 
            }

            // public function w/o access to "prv" variable
            log() {
                console.log("I'm logging");
            }
        }

        // private variables
        this._data = data;
        this._err = function(ip) {
            console.log("could not connect to "+ip);
        }
    }

    // private function
    _db(ip, err) {
        if(!!ip) {
		    console.log("connected to "+ip+", sending data '"+this.data+"'");
			return true;
		}
        else err(ip);
    }
}



var test = new Test(10),
		ip = "185.167.210.49";
test.connectToDb(ip); // true
test.log(); // I'm logging
test._err(ip); // undefined
test._db(ip, function() { console.log("You have got hacked!"); }); // undefined


2

На самом деле это возможно.
1. Сначала создайте класс и в конструкторе верните вызванную _publicфункцию.
2. В вызываемой _publicфункции передать thisссылку (чтобы получить доступ ко всем закрытым методам и реквизитам) и все аргументы из constructor (которые будут переданы в new Names())
3. В области _publicдействия функции также есть Namesкласс с доступом к this(_this ) ссылка частного Namesкласса

class Names {
  constructor() {
    this.privateProperty = 'John';
    return _public(this, arguments);
  }
  privateMethod() { }
}

const names = new Names(1,2,3);
console.log(names.somePublicMethod); //[Function]
console.log(names.publicProperty); //'Jasmine'
console.log(names.privateMethod); //undefined
console.log(names.privateProperty); //undefind

function _public(_this, _arguments) {
  class Names {
    constructor() {
      this.publicProperty = 'Jasmine';
      _this.privateProperty; //"John";
      _this.privateMethod; //[Function]
    }

    somePublicMethod() {
      _this.privateProperty; //"John";
      _this.privateMethod; //[Function]
    }

  }
  return new Names(..._arguments);
}

2

Вы можете попробовать это https://www.npmjs.com/package/private-members

Этот пакет сохранит участников по экземпляру.

const pvt = require('private-members');
const _ = pvt();

let Exemplo = (function () {    
    function Exemplo() {
        _(this).msg = "Minha Mensagem";
    }

    _().mensagem = function() {
        return _(this).msg;
    }

    Exemplo.prototype.showMsg = function () {
        let msg = _(this).mensagem();
        console.log(msg);
    };

    return Exemplo;
})();

module.exports = Exemplo;
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.