Все мы давным давно хотим нормальную инкапсуляцию в JS, которую можно было бы использовать без лишних телодвижений. А ещё мы хотим удобные конструкции для объявления свойств класса. И, напоследок, мы хотим что бы все эти фичи в языке появились так, что бы не сломать уже существующие приложения.
Казалось бы, вот оно счастье: class-fields-proposal, который спустя долгие годы мучений коммитета tc39 таки добрался до stage 3
и даже получил реализацию в хроме.
Честно говоря, я бы очень хотел написать статью просто о том, почему стоит пользоваться новой фишкой языка и как это сделать, но, к сожалению, статья будет совсем не об этом.
Описание текущего пропозала
Я не буду здесь повторять оригинальное описание, ЧаВо и изменения в спецификации, а лишь кратко изложу основные моменты.
Поля класса
Объявление полей и использование их внутри класса:
class A {
x = 1;
method() {
console.log(this.x);
}
}
Доступ к полям вне класса:
const a = new A();
console.log(a.x);
Казалось бы всё очевидно и мы уже многие годы пользуемся этим синтаксисом с помощью Babel и TypeScript.
Только есть нюанс. Этот новый синтаксис использует [[Define]]
, а не [[Set]]
семантику, с которой мы жили всё это время.
На практике это означает, что код выше не равен этому:
class A {
constructor() {
this.x = 1;
}
method() {
console.log(this.x);
}
}
А на самом деле эвивалентен вот этому:
class A {
constructor() {
Object.defineProperty(this, "x", {
configurable: true,
enumerable: true,
writable: true,
value: 1
});
}
method() {
console.log(this.x);
}
}
И, хотя для примера выше оба подхода делают, по сути, одно и то же, это ОЧЕНЬ СЕРЬЁЗНОЕ отличие, и вот почему:
Допустим у нас есть такой родительский класс:
class A {
x = 1;
method() {
console.log(this.x);
}
}
На его основе мы создали другой:
class B extends A {
x = 2;
}
И спользовали его:
const b = new B();
b.method(); // это выведет 2 в консоль
После чего по каким-либо причинам класс A
был изменён, казалось бы, обратно-совместимым способом:
class A {
_x = 1; // для упрощения, опустим тот момент, что в публичном интерфейсе появилась новое свойство
get x() { return this._x; };
set x(val) { return this._x = val; };
method() {
console.log(this._x);
}
}
И для [[Set]]
семантики это действительно обратно-совместимое изменение, но не для [[Define]]
. Теперь вызов b.method()
выведет в консоль 1
вместо 2
. А произойдёт это потому что Object.defineProperty
переопределяет дексриптор свойства и соответственно гетер/сетер из класса A
вызваны не будут. По сути, в дочернем классе мы затенили свойство x
родителя, аналогично тому как мы можем сделать это в лексическом скоупе:
const x = 1;
{
const x = 2;
}
Правда, в этом случае нас спасёт линтер с его правилами no-shadowed-variable
/no-shadow
, но вероятность того, что кто-то сделает no-shadowed-class-field
, стремится к нулю.
Кстати, буду благодарен за более удачный русскоязычный термин для shadowed
.
Несмотря на всё сказанное выше, я не являюсь непримеримым противником новой семантики (хотя и предпочёл бы другую), потому что у неё есть и свои положительные стороны. Но, к сожалению, эти плюсы не перевешивают самый главный минус — мы уже много лет используем [[Set]]
семантику, потому что именно она используеться в babel6
и TypeScript
, по умолчанию.
Правда, стоит заметить, что в babel7
дефолтное значение было изменено.
Больше оригинальных дисскусий на эту тему можно прочитать здесь и здесь.
Приватные поля
А теперь мы перейдём к самой спорной части этого пропозала. Настолько спорной, что:
- несмотря на то, что он уже реализован в Chrome Canary и публичные поля уже включены по умолчанию, приватные всё ещё за флагом;
- несмотря на то, что изначальный пропозал для приватных полей был объеденён с нынешним, до сих пор создаются запросы на отделение этих двух фич (например раз, два, три и четыре);
- даже некоторые члены комитета (например Allen Wirfs-Brock и Kevin Smith) высказываються против и предлагают альтернативы, несмотря на stage3;
- этот пропозал поставил рекорд по количеству issues — 129 в текущем репозитории + 96 в оригинальном, против 126 для BigInt, при чём у рекордсмена это в основном негативные комментарии;
- пришлось создать отдельный тред с попыткой хоть как-то суммировать все претензии к нему;
- пришлось написать отдельный ЧаВо, который опрадывает эту часть
правда, из-за довольно слабой аргументации, появились и такие обсуждения (раз, два)
- я, лично, тратил всё своё свободное время (а иногда и рабочее) на протяжении длительного периода времени на то, что бы во всём разобраться и даже найти объяснение почему он таков или предложить подходящую альтернативу;
- в конце концов, я решил написать эту обзорную статью.
Объявляются приватные поля следующим образом:
class A {
#priv;
}
А доступ к ним осуществляется так:
class A {
#priv = 1;
method() {
console.log(this.#priv);
}
}
Я даже не буду поднимать тему того, что ментальная модель, стоящая за этим, не очень интуитивна (this.#priv !== this['#priv']
), не использует уже зарезервированные слова private
/protected
(что обязательно вызовет дополнительную боль для TypeScript-разработчиков), непонятно как это расширять для других модификаторов доступа, и синтаксис сам по себе не очень красив. Хотя всё это и было изначальной причиной, толкнувшей меня на более глубокое исследование и участие в обсуждениях.
Это всё касается синтаксиса, где очень сильны субъективные эстэтические предпочтения. И с этим можно было бы жить и со временем привыкнуть. Если бы не одно но: тут существует очень существенная проблема семантики...
Cемантика WeakMap
Давайте взглянем на то, что стоит за за существующим пропозалом. Мы можем переписать пример сверху с инкапсуляцией и без использования нового синтаксиса, но сохраняя семантику текущего пропозала:
const privatesForA = new WeakMap();
class A {
constructor() {
privatesForA.set(this, {});
privatesForA.get(this).priv = 1;
}
method() {
console.log(privatesForA.get(this).priv);
}
}
Кстати, на основе этой семантики один из членов коммитета даже построил небольшую утилити библиотеку, которая позволяет использовать приватное состояние уже сейчас, для того, что бы показать, что такая функциональность слишком переоценена комитетом. Отформатированный код занимает всего 27 строк.
В целом всё довольно неплохо, мы получаем hard-private
, который никак нельзя достать/перехватить/отследить из внешнего кода и при этом можем получить доступ к приватным полям другого инстанса того же класса, например вот так:
isEquals(obj) {
return privatesForA.get(this).id === privatesForA.get(obj).id;
}
Что ж, это очень удобно, за исключением того факта, что эта семантика, помимо самой инкапсуляции, включает в себя ещё и brand-checking
(можете не гуглить, что это такое — вряд ли вы найдёте релевантную информацию).
brand-checking
— это противоположность duck-typing
, в том смысле, что она проверяет не публичный интефрейс объекта, а факт того, что объект был построен с помощью доверенного кода.
У такой проверки, на самом деле, есть определённая область применения — она, в основном, связана с безопасностью вызова недоверенного кода в едином адресном пространстве с доверенным и возможностью обмена объектами напрямую без сериализации.
Хотя некоторые инженеры считают это необходимой частью правильной инкапсуляции.
Несмотря на то, что это довольно любопытная возможность, которая тесно связано с патерном Мембрана
(краткое и более длинное описание), Realms
-пропозалом и научными работами в области Computer Science, которыми занят Mark Samuel Miller (он тоже член комитета), по моему опыту, в практике большинства разработчиков это почти никогда не встречается.
Я, кстати говоря, таки сталкивался с мембраной (правда тогда не знал, что это), когда переписывал vm2 под свои нужды.
Проблема brand-checking
Как уже было сказано ранее, brand-checking
— это противоположность duck-typing
. На практие это означает, что имея такой код:
const brands = new WeakMap();
class A {
constructor() {
brands.set(this, {});
}
method() {
return 1;
}
brandCheckedMethod() {
if (!brands.has(this)) throw 'Brand-check failed';
console.log(this.method());
}
}
brandCheckedMethod
может быть вызван только с инстансом класса A
и даже если таргетом выступает объект, сохраняющий инварианты этого класса, этот метод выкинет исключение:
const duckTypedObj = {
method: A.prototype.method.bind(duckTypedObj),
brandCheckedMethod: A.prototype.brandCheckedMethod.bind(duckTypedObj),
};
duckTypedObj.method(); // тут исключения не будет и метод вернёт 1
duckTypedObj.brandCheckedMethod(); // а здесь будет выброшенно исключение
Очевидно, что этот пример довольно синтетический и польза подобного duckTypedObj
сомнительна, до тех пор, пока мы не вспоминаем про Proxy
.
Один из очень важных сценариев использования прокси — это метапрограммирование. Для того, что бы прокси выполняла всю необходимую полезную работу, методы объектов, которые обёрнуты с помощью прокси должны выполняться в контексте прокси, а не в контексте таргета, т.е.:
const a = new A();
const proxy = new Proxy(a, {
get(target, p, receiver) {
const property = Reflect.get(target, p, receiver);
doSomethingUseful('get', retval, target, p, receiver);
return (typeof property === 'function')
? property.bind(proxy)
: property;
}
});
Вызов proxy.method();
сделает полезную работу объявленную в прокси и вернёт 1
, в то время как вызов proxy.brandCheckedMethod();
вместо того, что бы дважды сделать полезную работу из прокси, выкинет исключение, потому что a !== proxy
, а значит brand-check
не прошёл.
Да, мы можем выполнять методы/функции в котексте реального таргета, а не прокси, и для некоторых сценариев этого достаточно (например для реализации паттерна Мембрана
), но этого не хватит для всех случаев (например для реализации реактивных свойств: MobX 5 уже использует прокси для этого, Vue.js и Aurelia эксперементируют с этим подходом для следующих релизов).
В целом, до тех пор пока brand-check
нужно делать явно, это не проблема — разработчик просто осознанно должен решить какой trade-off он совершает и нужен ли он ему, более того в случае явного brand-check
можно его реализовать таким образом, что бы ошибка не выбрасывалась на довереных прокси.
К сожалению, текущий пропозал лишает нас этой гибкости:
class A {
#priv;
method() {
this.#priv; // в этой точке brand-check происходит ВСЕГДА
}
}
Такой method
всегда будет выбрасывать исключение, если вызван не в контексте объекта построенного с помощью конструктора A
. И самое ужасное, что brand-check
здесь неявный и смешан с другой функциональностью — инкапсуляцией.
В то время как инкапсуляция
почти необходима для любого кода, brand-check
имеет довольно узкий круг применения. А объединение их в один синтаксис приведёт к тому, что в пользовательском коде появиться очень много неумышленных brand-check
ов, когда разработчик намеривался только скрыть детали реализации.
А слоган, который используют для продвижения этого пропозала # is the new _
ситуацию только усугубляет.
Можете так же почитать подробное обсуждение того, как существующий пропозал ломает прокси. В дискуссии высказались один из разработчиков Aurelia и автор Vue.js.
Так же мой комментарий, более подробно описывающий разницу между разными сценариями использования прокси, может показатся кому-то интересным. Как и в целом всё обсуждение связи приватных полей и мембраны.
Альтернативы
Все эти обсуждения имели бы мало смысла, если бы не существовало альтернатив. К сожалению, ни один альтернативный пропозал не попал даже в stage1, и, как следствие, ни имел даже шансов быть достаточно проработанным. Тем не менее, я перечислю здесь альтернативы, которые так или иначе решают проблемы описанные выше.
- Symbol.private — альтернативный пропозал одного из членов комитета.
- Решает все выше перечисленные проблемы (хотя может имеет и свои, но, в виду отсутствия активной работы над ним, найти их тяжело)
- в очередной раз был откинут на последней встрече комитета по причине отсутствия встроенного
brand-check
, проблем с паттерном мембраны (хотя вот это + это предлагают адекватное решение) и отсутствием удобного синтаксиса - удобный синтаксис можно построить поверх самого пропозала, как показано мной здесь и здесь
- Classes 1.1 — более ранний пропозал от того же автора
- Использование private как объекта
Вместо заключения
По тону статьи, наверное, может показатся, что я осуждаю комитет — это не так. Мне лишь кажется, что за те годы (в зависимости от того, что брать точкой отсчёта, это могут быть даже десятилетия), которые комитет работал над инкапсуляцией в JS, многое в индустрии изменилось, а взгляд мог замылиться, что привело к ложной растановке приоритетов.
Более того, мы, как комьюнити, давим на tc39 заставляя их выпускать фичи быстрее, при этом даём крайне мало фидбека на ранних стадиях пропозалов, обрушивая своё негодование только в тот момент, когда уже мало что можно изменить.
Есть мнение, что в данном случае процесс просто дал сбой.
После окунания в это с головой и общения с некоторыми представителями, я решил, что приложу все усилия, что бы не допустить повторения подобной ситуации — но я могу сделать немного (написать обзорную статью, сделать имплементацию stage1
пропозала в babel
и всего-то).
Но самое важное это обратная связь — поэтому я попросил бы вас принять участие в этом небольшом опросе. А я, в свою очередь, постараюсь его донести до комитета.
Комментарии (49)
Arlekcangp
29.10.2018 18:59-2Почему в голосовалке нет поля «Закопать этот гребанный недоязык вместе с его ущербным комитетом»?
Если серьезно, то по моему мнению дело совсем не в отсутствии фидбека — комитеты по развитию других языков как то справляются с этим (java например). Остается только предположить, что в комитете по js участвуют определенного сорта люди… Почему нельзя было сделать просто модификатор private и не заигрывать с дурацкими символами типа #$@ (ну мешает оно type-script так его пусть потом и правят а сейчас получается что и c ним синтаксически не совместимо и синтаксис убог и неоднозначен) и заканчивая этим «brand-check» (вот реально причем тут приватные поля в языке и какая то безопасность? что с этими проверками будет в nodejs, где нет песочницы ?)
Закончится это видимо как с perl — язык станет настолько «законспирирован» символами и неявными связями, что его станут избегать.
staticlab
29.10.2018 19:07This sort of declaration is what other languages use (notably Java), and implies that access would be done with this.x. Assuming that isn't the case (see below), in JavaScript this would silently create or access a public field, rather than throwing an error. This is a major potential source of bugs or invisibly making public fields which were intended to be private.
Drag13
29.10.2018 19:21+2Зря вы так. Нормальный язык, просто со спецификой. Ну и взвалили на него внезапно очень многое чего изначально небыло в дизайне (шутку про дизайн не надо). Плюс ко всему принцип полной обратной совместимости помогает. С таким нелегко справиться.
Arlekcangp
29.10.2018 22:15Я это все понимаю. Про вариант голосования это конечно шутка. Но расширять язык как бог на душу положит это же не выход. Значит нужно потратить больше времени на проработку ситуации. А здесь решение выглядит так: «Ээ… ну у нас есть неиспользуемый символ #. Давайте его используем для обозначения приватных членов класса. Ну потом когда придется еще и protected добавить как-нибудь выкрутимся».
Drag13
29.10.2018 23:12Тут согласен полностью. Лучше не добавлять чем добавить и потом думать как же нам теперь с таким жить.
Igmat Автор
29.10.2018 19:24Почему не
private
там есть отдельное объяснение в FAQ.
А здесь я показал как можно таки использовать ключевое словоprivate
, но у такого подхода есть определённые ограничения, самое главное из которых — это увеличение когнитивной нагрузки на и без того сложныйthis
.Arlekcangp
29.10.2018 22:03не совсем понимаю это объяснение. Чем запись
class A {
private x
method() {
this.x;
}
}
синтаксически отличается от
class A {
#x
method() {
this.#x;
}
}
Это практически эквивалентная запись за исключением того, что во втором случае атрибут приватности указан еще и при использовании. Почему в других языках без этого все нормально работает (включая и интерпретируемые), а в js ссылаются на то что это будет медленно и небезопасно? Я не тролю если что — на самом деле не понимаю мотивацию. Если такие проблемы с производительностью и безопасностью, так может лучше тогда остановиться и подумать что еще нужно изменить в языке что бы стало возможным дать ему нормальный синтаксис и указывать область видимости один раз при объявлении, а не кругом и всюду, жертвуя читаемостью.
По поводу того что это можно реализовать через 27 строк кода — ну так многое из введенного в стандарт можно было реализовать через определенное количество строк кода. Но это добавили потому, что не хорошо когда есть стопятсот реализаций фичи языка с помощью библиотек. Добавление в стандарт дает единообразие. И с этой фичей будет также — либо реализация своего собственного private станет моветоном и закрепится не читаемый синтаксис, либо опять будут изменения в стандарте. К сожалению скорее всего будет первый вариант.justboris
29.10.2018 22:40+1На этот вопрос уже есть ответ в упомянутом в статье FAQ. В Javascript можно задать любому объекту любое свойство. Как вы будете различать публичный
obj.x
, заданный снаружи и приватныйthis.x
заданный изнутри?
Почему в других языках без этого все нормально работает (включая и интерпретируемые)
Дело не в интепретируемости, а в динамической типизации. Есть у вас пример языка с динамической типизацией и приватными полями?
Arlekcangp
29.10.2018 23:15На этот вопрос уже есть ответ
Having a private field named x must not prevent there from being a public field named x, so accessing a private field can't just be a normal lookup.
Ок. Это понятно. Ну получается вопрос то лежит глубже — механизм получения доступа к полю класса не продуман. Информация о типе здесь как бы не причем. К приватному полю снаружи доступа нет, поэтому ничто не мешает писать obj.x и интерпретатор должен вызвать при этом obj.getX() В то время как внутри будет объявлен private x и при написании this.x геттер не должен вызываться. Ну разумеется если позволить по прежнему делать this = obj то получится шлак. Так может над этой проблемой поработать?
Есть у вас пример языка с динамической типизацией и приватными полями?
Хм. Есть один и очень известный (правда я его не сразу вспомнил т к и он мне не нравился никогда) — это PHP
php.net/manual/ru/language.oop5.visibility.phpIgmat Автор
29.10.2018 23:32Ответил здесь. Постарайтесь больше не промахиватся веткой;)
Arlekcangp
30.10.2018 00:28как вы и предлагаете.
Ну я собственно предлагал запретить некоторые действия с this — в частности присвоение ему другого значения руками (т е не через apply а как this=something) Это бы вполне себе дало возможность сделать нормальные геттеры/сеттеры и приватность. Тем более что в некоторых случаях так уже запрещено делать (https://stackoverflow.com/questions/9713323/why-cant-i-assign-a-new-value-to-this-in-a-prototype-function) А вместо этого предлагают костыль. Ну это конечно мое мнение. Вариантов всегда более одного и некоторые хуже по реализации зато быстрее. Вот только я бы не делал такого в столь распространенном и востребованном языке как js. Возможно что сейчас на его проектирование стоит тратить даже больше времени чем на Си++ т к сложность и запутанность уже сравнимая а популярность все растет.
justboris
29.10.2018 23:34Так может над этой проблемой поработать?
Так над ней уже и поработали, и мы сейчас видим результат – предложение использовать диез в качестве префикса.
Есть один и очень известный — это PHP
Насколько я понял, там нельзя назначать поля классам снаружи, поэтому и указанной проблемы нет. Хорошо, сузим круг только до языков, где можно такое делать.
Arlekcangp
30.10.2018 00:21Приватные поля и не должно быть можно назначить снаружи. Публичные в php вполне себе назначаются
$foo = new EmptyObject();
$foo->bar = '1234';
Но так можно делать если не заявлен strict mode. По моему очень не плохое решение.
Так над ней уже и поработали, и мы сейчас видим результат – предложение использовать диез в качестве префикса.
По моему это очень поспешный результат. За отсутствие приватных полей js критиковали чуть ли не со дня его появления… Уж можно было еще лет 5 потратить чтобы выработать какое то более логичное решение.justboris
30.10.2018 00:42Так можно делать если не заявлен strict mode. По моему очень не плохое решение.
В качестве своеобразного strict mode у нас есть Typescript. Там приватные поля есть с самого начала.
По моему это очень поспешный результат
Интересно, откуда следует такой вывод. Если в результате нет "приватности через ключевое слово private" – это значит, что недостаточно поработали?
Arlekcangp
30.10.2018 00:54В качестве своеобразного strict mode у нас есть Typescript.
Я его считаю другим языком. Примерно с тем же успехом можно компилировать C# в js.
Если в результате нет «приватности через ключевое слово private» – это значит, что недостаточно поработали?
Приведу первым «стандартный» аргумент — во многих языках так. Почему js должен настолько отличаться? Ну тут можно возразить, что для того и существуют разные языки. На что я отвечу, что js имеет мягко говоря слегка особое положение среди остальных. И хорошо бы, что бы он не был столь экзотическим (исторически сложилось что уже так, но этот разрыв продолжают увеличивать) И отсюда прямо вытекает второй аргумент — если нотация с приват так распространена, так значит видимо она удобна. Иначе бы языки постепенно от нее избавлялись. Ну и третий аргумент — логика. По логике это не дело программиста постоянно напоминать языку что он объявил приватное поле. Это ухудшает читаемость. И использование для этого одного символа лишь кажется хорошей идеей. Следуя этому правилу через некоторое время весь код начнет кишить символами, которые человеческому разуму придется именно дешифровать в отличие от слов, которые он более менее понимает.Chamie
31.10.2018 11:35На что я отвечу, что js имеет мягко говоря слегка особое положение среди остальных. И хорошо бы, что бы он не был столь экзотическим
Простите, но это аргумент вида «а *ули он вы*бывается?!»
если нотация с приват так распространена, так значит видимо она удобна
Она удобна в языках с классовым ООП, а в JS оно прототипное. И так вам сахар для имитации классов подвезли, так вы всё равно пишете, что «разрыв продолжают увеличивать».
justboris
29.10.2018 23:01Спасибо за интересную статью!
Я немного порефлексировал на эту тему и пришел к выводу, что я скорее всего не буду использовать приватные поля, даже когда они станут нативными, потому что в этом нет смысла.
Применять приватные поля для скрытия данных не выйдет, потому что они легко достаются вот таким кодом (поправьте, если это не так):
class WithPrivate { #secretValue = 123; } function steal() { return this.#secretValue; } const test = new WithPrivate(); steal.call(test); // 123
Поэтому лучше не делать важную бизнес-логику, основанную на приватных полях, где-нибудь во встраиваемых виджетах, например. Гораздо лучше сохранять в данные в замыкании, что можно сделать и сейчас.
Остается еще сценарий грамотной организации кода и инкапсулирования своих собственных классов. Для этого лучше подходит проверка во время сборки (Typescript), чтобы невалидный код даже не имел шанса попасть на сервер. Рантайм-проверка это слишком поздно – ваш код уже попал в браузеры пользователей и работает там с ошибками.
Igmat Автор
29.10.2018 23:15Нет, ваш пример не получит доступа к приватному значению. Я, конечно, могу тут долго расписывать почему и давать ссылки на спеку и релевантные обсуждения, но надеюсь вы мне просто поверите. Да, аргумент плохой, но и вы ведь тоже можете прочитать спецификацию и полистать обсуждения на гитхабе (хотя задача это не тривиальная).
Единственный способ получить доступ к приватным полям — это программное получение доступа к сорсам (браузер и нода позволяют это нам), и такое их изменение, которое убирает прайват модификатор, после чего мы уже исполняем код, в котором приватных полей нет.
Это, правда, тоже можно решить, если вы контролируете окружение: тот же паттерн Мембрана и Realms пропозал для этого и придуманы.
P.S.
Function.protoype.toString
— тоже можно обмануть заставив его возвращать строку[native code]
, но пусть это будет задачкой для любопытных;)justboris
29.10.2018 23:39С первым аргументом понятно. Может быть, кому-то такой синтаксис зайдет лучше WeakMap в замыкании, хотя, как я понимаю, особых преимуществ у него нет, только дополнительные проблемы с Proxy.
А второй все-таки остается – лучше проверять такие вещи перед выкладкой, а не читать ошибки в консоли браузера.
Igmat Автор
30.10.2018 14:30Вы, тут упускаете один момент. Да, для приложения, которое просто выкладывается в прод
soft private
из TS вполне достаточно, а вот для библиотеки — мало.
Hard private
нужен как раз разработчикам библиотек — например Joyee, один из мейнтенеров node.js, приводил примеры, когда отсутствие нормальной инкапсуляции приводило к серьёзным проблемам.
Для авторов библиотек это большая проблема, когда консьюмеры начинают зависеть от деталей имплементации, а на данный момент это сплошь и рядом:
_x
, который является приватным только по соглашению, по факту начинают использовать в разнообразных даунстрим проектах, и хотя изначально это была просто деталь имплементации, теперь автор библиотеки не может её просто так убрать, не словам зависимые проекты.
Drag13
29.10.2018 23:18Идея не в почитать, идея в не поменять. Т.е. не нарушить внутреннюю структуру и, как следствие, работу объекта.
xander27
30.10.2018 12:15Мне кажется, суть в основном в том что бы, семантический обозначить какие поля приватные и уберечь от "случайных" использований. В других языках (java) можно вытащить приватные значения через relection. Если есть очень большое желание — всегда можно вытащить
Cryvage
30.10.2018 17:03+1Поскольку "#" является синтаксическим сахаром над тем же WeakMap, очевидно что указанный вами способ не сработает.
Чтобы было нагляднее, запишем аналог приведённого вами кода, заменив новый синтаксис явным использованием WeakMap:
const WithPrivate = ( function (){ const privates = new WeakMap(); return class { constructor(){ privates.set( this, {} ); privates.get( this ).secretValue = 123; } } }) ( ); function steal (){ return privates.get(this).secretValue; } const test = new WithPrivate(); steal.call(test); // ReferenceError: privates is not defined
Соответственно, чтобы получить доступ к secretValue, нужны две составляющие: ссылка на this, и доступ к WeakMap privates. Последний недоступен функции steal, т.к. скрыт в замыкании.
Arlekcangp
29.10.2018 23:02del
Igmat Автор
29.10.2018 23:28Ответ на комментарий пользователя Arlekcangp, который опять промахнулся веткой ?\_(?)_/?
Как, я уже говорил ранее, вот здесь я предоставил рабочий пример использования
private
синтаксиса, но и у него есть проблема — ключевое словоthis
получает ещё одно усложнение в понимании (а оно и так непростое) т.к. в зависимости от лексического контекста обладает разным смыслом, как вы и предлагаете.
Поэтому эта идея развилась в более непротиворечивый синтаксический сахар для всё того же Symbol.private стоящего в основе этой альтернативы.
В любом случае, скорее всего, ключевое слово
private
будет жить только в TypeScript, а в ES сможет появиться только вместе с аннотациями типов.
P.S.
Несмотря на то, что моей изначальной причиной погружения в эту тему был, как раз, неприятный синтаксис, сейчас я готов с ним даже смириться, лишь бы не было проблем с семантикой, которые я описал в статье.
andreyverbin
29.10.2018 23:46+3А зачем вообще убиваться по поводу доступности приватных полей извне? В большинстве языков это сделать можно и никто не жалуется. Безопасность на такой сомнительной вещи строить не получится. Так зачем усложнять язык? По мне текущего де-факто стандарта «this._x” более чем достаточно.
Возможность залезть в кишки объекта это большое благо на самом деле. Например чтобы починить баг в библиотеке, бывает в тестах сильно проще проверить приватную переменную чем городить что-то вокруг и т.п. Я не призываю лезть в внутрь всех объектов, но эта фича имеет своё применение.
А инкапсуляция она от private не сильно зависит. У вас либо понятный поток данных и управления в системе или каша. А в каше существенная часть приватного состояния все равно будет косвенно видно в деталях контракта, большом количестве методов, сложном состоянии, о котором надо знать клиентам и т.п.
jeron-diovis
30.10.2018 14:30Полностью поддерживаю. Фронтенд полнится всевозможными библиотеками самого разного уровня качества. И часто действительно приходится самому их патчить – потому что либо исправление их автором либо займёт слишком много времени, а вам надо вотпрямщас; либо вы даже сами пуллреквест сделали, но их там уже 50 штук висит и автору принять их некогда; либо репозиторий вообще заброшен, а форк сделать не вариант, потому что эта библиотека находится в зависимостях ещё нескольких используемых вами пакетов… и так далее.
Простой и доступный синтаксис для приватных полей приведёт только к тому, что сообщество начнёт его пихать куда надо и не надо (а оно начнёт, это же js-сообщество, в котором, ну будем честны – средний уровень компетенции довольно-таки посредственный). И этим катастрофически осложнит жизнь всем, кто будет плодами этого творчества пользоваться.
#js_is_public
Igmat Автор
30.10.2018 14:34Собственно, была уже тьма дисскусий на тему
hard private
vssoft private
.
Авторы популярных фреймворков/библиотек в большинстве своё выступают за
hard private
потому что это позволяет им четко отделить публичный контракт от деталей имплементации и потом спокойно проводить рефакторинги, делать новые фичи и, в целом, развивать свои продукты, не боясь сломать проекты, которые зависят от деталей имплементации, потому что таких просто не будет при хорошо инкапсулированом коде.andreyverbin
30.10.2018 16:23+1Это все попытка выдать желаемое за действительное. Если все хорошо сделано, то пользователю и не нужно лезть в кишки, а если все плохо, то любой значимый рефакторинг сломает контракт. И затем, найдите мне программиста на JS, который не в курсе, что если меняешь/читаешь что-то, что начинается с «_», то можно огрести? Все уже и так все знают, в от идиотов защититься невозможно.
Самые частые проблемы с контрактом выглядят не «переименовало приватную переменную», а как «был метод O(1), стал O(n^2)”, не было исключения, теперь есть, раньше можно было метод дергать до «onload”, а теперь нельзя и т.п. Если сообщество обеспокоено контрактом, то нужно говорить не о private, а о полноценной ala Eifell системе. Наличие hard private автору библиотеки даст ложное спокойствие, а юзеру даст вполне конкретный геморрой. Я не вижу тут реальной выгоды ни для авторов ни для юзеров.
Igmat Автор
30.10.2018 17:05В кишки могут полезть не из-за того, что сделано плохо, а потому что разработчик плохо понимает ограничения библиотеки. Для примера:
class A { _x; get() { return this._x; } set(val) { const result = (Array.isArray(val)) ? val : [val]; result.forEach(doSomeSideEffect); return this._x = result; } method() { const transformedX = this._x.map(doImportantTransformation); doSomething(transformedX); } }
doSomeSideEffect
— делает что-то важное, но на первый взгляд незаметное (например проставляет каких-нибудь слушателей), и влияет на скорость выполнения.
Конечный пользователь решает проставить
_x
напрямую, не замечает что сайд-эффекта нет (в этот момент он ему не нужен), а скорость выполнения возрасла — "УРА, какой я молодец" думает он и переходит к другим задачам.
Автор библиотеки понимаеть, что он никогда не использует
_x
без применения к немуdoImportantTransformation
, а все другие методы (например,doSomething
) расчитывают на вход получить уже трансформаированный массив, и решает сделать трансформацию только один раз, что ускорит работу его либы.
class A { _x; get() { return this._x; } set(val) { const result = (Array.isArray(val)) ? val : [val]; result.forEach(doSomeSideEffect); return this._x = result.map(doImportantTransformation);; } method() { doSomething(this._x); } }
На первый взгляд автору кажется, что это даже не минорный апдейт либы, а просто патч. Но для пользователя, который использовал
_x
напрямую это breaking change, который ещё и не так просто найти т.к. ошибка возникает не в месте использования_x
, а там где вызываетьсяmethod
.
Не все авторы библиотек, готовы говорить своим пользователям, что раз вы юзали свойства с префиксом, то идите лесом.
andreyverbin
31.10.2018 12:16Почему же не готовы? В Java, .NET, С и C++ и в других языках готовы, и прямо говорят, «если вы меняете приватные переменные, то идите лесом». В JS сейчас чуть проще, чем в перечисленных языках поменять приватные переменные, но что с того? Авторы говорят обычно даже более сильную вещь — «используете недокументированные методы и функции — идите лесом». В вашем примере "_x" де факто приватная переменная, «де-юре» недокументированная функция. В любом случае юзер идет лесом и это ожидаемо.
Igmat Автор
31.10.2018 15:27Я же сказал "не все авторы".
Было очень много дисскусий на эту тему и оба мнения были достаточно широко представлены. Но, в целом, победила позиция, что лучше не давать пользователю доступа к деталям имплементации вообще, чем потом ему говорить, что он сам дурак.
Кстати, приводился пример какого-то тестового фреймворка из Java, для которого существуют популярные плагины, которые используют приватное состояние через рефлексию, что в итоге существенно замедлило развитие самого фреймворка. (в деталях мог ошибиться, т.к. описывал по памяти, но суть думаю ясна)
koldoon
30.10.2018 11:35+1На первый взгляд складывается впечатление, что комитет поставил своей задачей не решить конкретную проблему, а насолить, например, typescript (ну, или babel), и показать «кто тут главный», мол, сделаем как угодно, но только не так как у вас там принято. По-моему приватные переменные как таковые в js особо не нужны (поправьте меня, если не прав), а защиту на этапе разработки можно обеспечить любым статическим анализатором (typescript, coffeescript, dart — кому что нравится)
justboris
30.10.2018 12:04Со вторым аргументом согласен, от приватных переменных в рантайме пользы немного.
А вот первый – это какая-то теория заговора. Там выше уже разбирались причины почему сделано именно так, а вы все туда же.
Cryvage
30.10.2018 17:45В целом, до тех пор пока brand-check нужно делать явно, это не проблема — разработчик просто осознанно должен решить какой trade-off он совершает и нужен ли он ему, более того в случае явного brand-check можно его реализовать таким образом, что бы ошибка не выбрасывалась на довереных прокси.
К сожалению, текущий пропозал лишает нас этой гибкости:
class A { #priv; method() { this.#priv; // в этой точке brand-check происходит ВСЕГДА } }
Такой method всегда будет выбрасывать исключение, если вызван не в контексте объекта построенного с помощью конструктора A
Что мешает сделать так:
class A { #secret = 42; __self = this; getSecret(){return this.__self.#secret;} setSecret(val){this.__self.#secret = val} } const a = new A(); const proxy = new Proxy(a, { get(target, p, receiver) { const property = Reflect.get(target, p, receiver); return (typeof property === 'function') ? property.bind(proxy) : property; } }); a.getSecret(); //42 proxy.getSecret(); //42 proxy.setSecret(43); a.getSecret(); //43 proxy.getSecret(); //43
Проверил в Chrome Canary. Вроде всё работает.
Либо так:
const A = ( function (){ const __self = Symbol("self"); return class { #secret = 42; constructor(){ this[__self] = this; } getSecret(){return this[__self].#secret;} setSecret(val){this[__self].#secret = val} } })();
Если хотим скрыть __self, и защититься от его модификации извне.Igmat Автор
30.10.2018 18:10Мешает то, что прокси, которую вы взяли из примера в статье, была всего лишь демонстрацией проблемы. Ваш пример с реальной прозрачной прокси работать не будет, потому что:
- Доступ к
__self
тоже дернётget
trap, который обернёт результат, а по скольку ссылка наthis
уже имеет соответствующую ей прокси (в примере из статьи этого нет, но можно глянуть в моём комментарии здесь), то она и будет возвращена, а значит методы будут вызываться в контексте прокси и бренд-чек будет выкидывать ошибку. - Допустим наша реализация не обарачивает
__self
проперти, тогда исключение не будет выброшенно, но методы/геттеры/сеттеры вызванные после__self
будут вызваны в контексте__self
, а нам нужно, что бы они вызывались в контексте прокси. const __self = Symbol("self");
не защищает от модификации — Object.getOwnPropertySymbols позволяет извлечь любой символ из объекта.
Cryvage
30.10.2018 20:05Допустим наша реализация не обарачивает __self проперти, тогда исключение не будет выброшенно, но методы/геттеры/сеттеры вызванные после __self будут вызваны в контексте __self, а нам нужно, что бы они вызывались в контексте прокси.
Вообще это интересный момент. Как выполнять методы, обращающиеся к полям, которых в текущем контексте просто не существует? Дело ведь даже не в brand-checking. Ну допустим не будет явной проверки. Но ведь приватных полей, имеющихся в оригинальном объекте, у прокси просто нет. И инициализировать он их не сможет, т.к. не имеет к ним доступа, и не знает, какие у них в данный момент значения, чтобы скопировать их себе. А после не сможет записать полученное состояние в оригинальный объект, т.к. опять же, доступа не имеет.
Получается, проблема не столько в конкретной реализации приватных полей, сколько в невозможности обойти инкапсуляцию. Такая возможность конечно должны быть. И как правило, в других языках она есть. Если рассмотреть тот же C#, то там можно без проблем, как прочитать, так и записать приватное поле. Через рефлексию. Возможно и в JS стоит добавить новый метод в Reflect, позволяющий получить доступ к скрытому WeakMap, в котором хранятся приватные поля. Что-то вроде Reflect.GetPrivate(obj);Igmat Автор
30.10.2018 20:22Symbol.private
предлагает решение и оно довольно простое.
У прокси действительно нет приватных полей оригинального объекта, но:
- Для каждой прокси всегда известен его таргет (и рантайм его знает)
- Доступ к полю используя что
Symbol.private
, что#priv
отличается от доступа к публичному полю.
Это приводит к простому решению, если код пытается осуществить доступ к приватному полю на прокси то вместо обращения к прокси, он сразу обращаеться к таргету этого прокси.
Что-то вроде Reflect.GetPrivate(obj);
Это нарушает концепцию
hard private
, коммитет подобные escape hatch не будет даже рассматривать.Drag13
30.10.2018 21:22Это нарушает концепцию hard private, коммитет подобные escape hatch не будет даже рассматривать.
А почему? Другие, более жесткие языки такое позволяют и всем от этого более менее хорошо. Чем аргументируют?Cryvage
30.10.2018 22:17Я сначала тоже думал возразить, начал писать, что в других языках, рефлексия именно так и работает. Потом нашёл обсуждение на GitHub, где в принципе все эти аргументы, были сказаны и разжёваны.
Лично я, почитав аргументы, так и не понял, в чём смысл такой жёсткости. Аргументы противников мне показались сильнее. Единственный весомый аргумент, что я увидел, это то что, было бы хорошо, если бы такая возможность была. Именно как возможность, с учётом того, что есть и другие возможности, реализовать иные типы приватности, я могу это принять. Правда не совсем понятно, зачем тут отдельный синтаксис. Для редких случаев, когда такая приватность действительно понадобилась, можно и явно WeakMap использовать.
Но тут возникает проблема, исключения вылетают вовсе не при попытке обратиться к приватным полям напрямую, а при вызове публичных методов. И внешнему коду не понятно, какие методы можно вызывать из другого контекста, а какие нет. Вот в этом плане, однозначно, надо что-то улучшать. Хотя бы добавить возможность проверки, можно ли с данным методом делать call, apply и bind.
Cryvage
30.10.2018 21:54Это приводит к простому решению, если код пытается осуществить доступ к приватному полю на прокси то вместо обращения к прокси, он сразу обращаеться к таргету этого прокси.
В принципе, в своём примере, я именно к этому и стремился. Вот только с «решёточным» синтаксисом, класс должен быть намеренно написан с расчётом на прокси: объявлять __self, и обращаться к приватным полям через него. И Proxy должен сделать исключение для __self.
Это нарушает концепцию hard private, коммитет подобные escape hatch не будет даже рассматривать.
Ок, если основной целью добавления «решёточного» синтаксиса была именно жёсткая приватность, и это было сделано осознанно, то я могу сказать только одно. Пусть будет. Но использовать её следует лишь тогда, когда это действительно необходимо. Во всех остальных случаях, можно просто использовать символы. Либо, как промежуточный вариант, явное объявление WeakMap — тут и жёсткая приватность, и возможность объявить некоторые объекты «друзьями», на которых эта приватность не распространяется. Главное, чего не надо делать, так это позиционировать приватные поля с решёткой, как главный и единственный способ инкапсуляции. Хорошо бы ещё был способ заранее различать методы, которые можно вызывать с другим this, и которые нельзя.
- Доступ к
mayorovp
А для методов точно нужно делать
.bind(proxy)
? Вроде же рантайм сам подставит прокси вместо this, когда этот самый метод будет вызывать.Это, кстати, еще сильнее усугубляет проблему с прокси: они перестают работать даже если не пытаться делать что-то хитрое, просто по умолчанию.
Igmat Автор
Нет, не нужно. В моём оригинальном коде для такой прокси этого и нет — просто хотел указать явно на важный момент того, что функция будет вызвана в контексте прокси.