Все мы давным давно хотим нормальную инкапсуляцию в 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 дефолтное значение было изменено.

Больше оригинальных дисскусий на эту тему можно прочитать здесь и здесь.


Приватные поля


А теперь мы перейдём к самой спорной части этого пропозала. Настолько спорной, что:


  1. несмотря на то, что он уже реализован в Chrome Canary и публичные поля уже включены по умолчанию, приватные всё ещё за флагом;
  2. несмотря на то, что изначальный пропозал для приватных полей был объеденён с нынешним, до сих пор создаются запросы на отделение этих двух фич (например раз, два, три и четыре);
  3. даже некоторые члены комитета (например Allen Wirfs-Brock и Kevin Smith) высказываються против и предлагают альтернативы, несмотря на stage3;
  4. этот пропозал поставил рекорд по количеству issues — 129 в текущем репозитории + 96 в оригинальном, против 126 для BigInt, при чём у рекордсмена это в основном негативные комментарии;
  5. пришлось создать отдельный тред с попыткой хоть как-то суммировать все претензии к нему;
  6. пришлось написать отдельный ЧаВо, который опрадывает эту часть
    правда, из-за довольно слабой аргументации, появились и такие обсуждения (раз, два)
  7. я, лично, тратил всё своё свободное время (а иногда и рабочее) на протяжении длительного периода времени на то, что бы во всём разобраться и даже найти объяснение почему он таков или предложить подходящую альтернативу;
  8. в конце концов, я решил написать эту обзорную статью.

Объявляются приватные поля следующим образом:


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, и, как следствие, ни имел даже шансов быть достаточно проработанным. Тем не менее, я перечислю здесь альтернативы, которые так или иначе решают проблемы описанные выше.


  1. Symbol.private — альтернативный пропозал одного из членов комитета.
    1. Решает все выше перечисленные проблемы (хотя может имеет и свои, но, в виду отсутствия активной работы над ним, найти их тяжело)
    2. в очередной раз был откинут на последней встрече комитета по причине отсутствия встроенного brand-check, проблем с паттерном мембраны (хотя вот это + это предлагают адекватное решение) и отсутствием удобного синтаксиса
    3. удобный синтаксис можно построить поверх самого пропозала, как показано мной здесь и здесь
  2. Classes 1.1 — более ранний пропозал от того же автора
  3. Использование private как объекта

Вместо заключения


По тону статьи, наверное, может показатся, что я осуждаю комитет — это не так. Мне лишь кажется, что за те годы (в зависимости от того, что брать точкой отсчёта, это могут быть даже десятилетия), которые комитет работал над инкапсуляцией в JS, многое в индустрии изменилось, а взгляд мог замылиться, что привело к ложной растановке приоритетов.


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


Есть мнение, что в данном случае процесс просто дал сбой.


После окунания в это с головой и общения с некоторыми представителями, я решил, что приложу все усилия, что бы не допустить повторения подобной ситуации — но я могу сделать немного (написать обзорную статью, сделать имплементацию stage1 пропозала в babel и всего-то).


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

Комментарии (49)


  1. mayorovp
    29.10.2018 17:27

    А для методов точно нужно делать .bind(proxy)? Вроде же рантайм сам подставит прокси вместо this, когда этот самый метод будет вызывать.


    Это, кстати, еще сильнее усугубляет проблему с прокси: они перестают работать даже если не пытаться делать что-то хитрое, просто по умолчанию.


    1. Igmat Автор
      29.10.2018 18:01

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


  1. Arlekcangp
    29.10.2018 18:59
    -2

    Почему в голосовалке нет поля «Закопать этот гребанный недоязык вместе с его ущербным комитетом»?
    Если серьезно, то по моему мнению дело совсем не в отсутствии фидбека — комитеты по развитию других языков как то справляются с этим (java например). Остается только предположить, что в комитете по js участвуют определенного сорта люди… Почему нельзя было сделать просто модификатор private и не заигрывать с дурацкими символами типа #$@ (ну мешает оно type-script так его пусть потом и правят а сейчас получается что и c ним синтаксически не совместимо и синтаксис убог и неоднозначен) и заканчивая этим «brand-check» (вот реально причем тут приватные поля в языке и какая то безопасность? что с этими проверками будет в nodejs, где нет песочницы ?)
    Закончится это видимо как с perl — язык станет настолько «законспирирован» символами и неявными связями, что его станут избегать.


    1. staticlab
      29.10.2018 19:07

      This 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.


    1. Drag13
      29.10.2018 19:21
      +2

      Зря вы так. Нормальный язык, просто со спецификой. Ну и взвалили на него внезапно очень многое чего изначально небыло в дизайне (шутку про дизайн не надо). Плюс ко всему принцип полной обратной совместимости помогает. С таким нелегко справиться.


      1. MaM
        29.10.2018 20:54
        +1

        Такое ощущение, что комментарий по работе комитета С++ читаю.


      1. Arlekcangp
        29.10.2018 22:15

        Я это все понимаю. Про вариант голосования это конечно шутка. Но расширять язык как бог на душу положит это же не выход. Значит нужно потратить больше времени на проработку ситуации. А здесь решение выглядит так: «Ээ… ну у нас есть неиспользуемый символ #. Давайте его используем для обозначения приватных членов класса. Ну потом когда придется еще и protected добавить как-нибудь выкрутимся».


        1. Drag13
          29.10.2018 23:12

          Тут согласен полностью. Лучше не добавлять чем добавить и потом думать как же нам теперь с таким жить.


    1. Igmat Автор
      29.10.2018 19:24

      Почему не private там есть отдельное объяснение в FAQ.
      А здесь я показал как можно таки использовать ключевое слово private, но у такого подхода есть определённые ограничения, самое главное из которых — это увеличение когнитивной нагрузки на и без того сложный this.


      1. Arlekcangp
        29.10.2018 22:03

        не совсем понимаю это объяснение. Чем запись
        class A {
        private x

        method() {
        this.x;
        }
        }

        синтаксически отличается от
        class A {
        #x

        method() {
        this.#x;
        }
        }

        Это практически эквивалентная запись за исключением того, что во втором случае атрибут приватности указан еще и при использовании. Почему в других языках без этого все нормально работает (включая и интерпретируемые), а в js ссылаются на то что это будет медленно и небезопасно? Я не тролю если что — на самом деле не понимаю мотивацию. Если такие проблемы с производительностью и безопасностью, так может лучше тогда остановиться и подумать что еще нужно изменить в языке что бы стало возможным дать ему нормальный синтаксис и указывать область видимости один раз при объявлении, а не кругом и всюду, жертвуя читаемостью.
        По поводу того что это можно реализовать через 27 строк кода — ну так многое из введенного в стандарт можно было реализовать через определенное количество строк кода. Но это добавили потому, что не хорошо когда есть стопятсот реализаций фичи языка с помощью библиотек. Добавление в стандарт дает единообразие. И с этой фичей будет также — либо реализация своего собственного private станет моветоном и закрепится не читаемый синтаксис, либо опять будут изменения в стандарте. К сожалению скорее всего будет первый вариант.


        1. justboris
          29.10.2018 22:40
          +1

          На этот вопрос уже есть ответ в упомянутом в статье FAQ. В Javascript можно задать любому объекту любое свойство. Как вы будете различать публичный obj.x, заданный снаружи и приватный this.x заданный изнутри?


          Почему в других языках без этого все нормально работает (включая и интерпретируемые)

          Дело не в интепретируемости, а в динамической типизации. Есть у вас пример языка с динамической типизацией и приватными полями?


          1. staticlab
            29.10.2018 23:15

            Ruby? (Хоть там и не полноценная приватность)


          1. 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.php


            1. Igmat Автор
              29.10.2018 23:32

              Ответил здесь. Постарайтесь больше не промахиватся веткой;)


              1. 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. Возможно что сейчас на его проектирование стоит тратить даже больше времени чем на Си++ т к сложность и запутанность уже сравнимая а популярность все растет.


            1. justboris
              29.10.2018 23:34

              Так может над этой проблемой поработать?

              Так над ней уже и поработали, и мы сейчас видим результат – предложение использовать диез в качестве префикса.


              Есть один и очень известный — это PHP

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


              1. Arlekcangp
                30.10.2018 00:21

                Приватные поля и не должно быть можно назначить снаружи. Публичные в php вполне себе назначаются
                $foo = new EmptyObject();
                $foo->bar = '1234';

                Но так можно делать если не заявлен strict mode. По моему очень не плохое решение.

                Так над ней уже и поработали, и мы сейчас видим результат – предложение использовать диез в качестве префикса.

                По моему это очень поспешный результат. За отсутствие приватных полей js критиковали чуть ли не со дня его появления… Уж можно было еще лет 5 потратить чтобы выработать какое то более логичное решение.


                1. justboris
                  30.10.2018 00:42

                  Так можно делать если не заявлен strict mode. По моему очень не плохое решение.

                  В качестве своеобразного strict mode у нас есть Typescript. Там приватные поля есть с самого начала.


                  По моему это очень поспешный результат

                  Интересно, откуда следует такой вывод. Если в результате нет "приватности через ключевое слово private" – это значит, что недостаточно поработали?


                  1. Arlekcangp
                    30.10.2018 00:54

                    В качестве своеобразного strict mode у нас есть Typescript.

                    Я его считаю другим языком. Примерно с тем же успехом можно компилировать C# в js.
                    Если в результате нет «приватности через ключевое слово private» – это значит, что недостаточно поработали?

                    Приведу первым «стандартный» аргумент — во многих языках так. Почему js должен настолько отличаться? Ну тут можно возразить, что для того и существуют разные языки. На что я отвечу, что js имеет мягко говоря слегка особое положение среди остальных. И хорошо бы, что бы он не был столь экзотическим (исторически сложилось что уже так, но этот разрыв продолжают увеличивать) И отсюда прямо вытекает второй аргумент — если нотация с приват так распространена, так значит видимо она удобна. Иначе бы языки постепенно от нее избавлялись. Ну и третий аргумент — логика. По логике это не дело программиста постоянно напоминать языку что он объявил приватное поле. Это ухудшает читаемость. И использование для этого одного символа лишь кажется хорошей идеей. Следуя этому правилу через некоторое время весь код начнет кишить символами, которые человеческому разуму придется именно дешифровать в отличие от слов, которые он более менее понимает.


                    1. Chamie
                      31.10.2018 11:35

                      На что я отвечу, что js имеет мягко говоря слегка особое положение среди остальных. И хорошо бы, что бы он не был столь экзотическим
                      Простите, но это аргумент вида «а *ули он вы*бывается?!»
                      если нотация с приват так распространена, так значит видимо она удобна
                      Она удобна в языках с классовым ООП, а в JS оно прототипное. И так вам сахар для имитации классов подвезли, так вы всё равно пишете, что «разрыв продолжают увеличивать».


  1. Arlekcangp
    29.10.2018 22:00

    del


  1. justboris
    29.10.2018 23:01

    Спасибо за интересную статью!


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


    Применять приватные поля для скрытия данных не выйдет, потому что они легко достаются вот таким кодом (поправьте, если это не так):


    class WithPrivate {
       #secretValue = 123;
    }
    
    function steal() {
      return this.#secretValue;
    }
    
    const test = new WithPrivate();
    steal.call(test); // 123

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


    Остается еще сценарий грамотной организации кода и инкапсулирования своих собственных классов. Для этого лучше подходит проверка во время сборки (Typescript), чтобы невалидный код даже не имел шанса попасть на сервер. Рантайм-проверка это слишком поздно – ваш код уже попал в браузеры пользователей и работает там с ошибками.


    1. Igmat Автор
      29.10.2018 23:15

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


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


      Это, правда, тоже можно решить, если вы контролируете окружение: тот же паттерн Мембрана и Realms пропозал для этого и придуманы.


      P.S.
      Function.protoype.toString — тоже можно обмануть заставив его возвращать строку [native code], но пусть это будет задачкой для любопытных;)


      1. justboris
        29.10.2018 23:39

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


        А второй все-таки остается – лучше проверять такие вещи перед выкладкой, а не читать ошибки в консоли браузера.


        1. Igmat Автор
          30.10.2018 14:30

          Вы, тут упускаете один момент. Да, для приложения, которое просто выкладывается в прод soft private из TS вполне достаточно, а вот для библиотеки — мало.


          Hard private нужен как раз разработчикам библиотек — например Joyee, один из мейнтенеров node.js, приводил примеры, когда отсутствие нормальной инкапсуляции приводило к серьёзным проблемам.


          Для авторов библиотек это большая проблема, когда консьюмеры начинают зависеть от деталей имплементации, а на данный момент это сплошь и рядом: _x, который является приватным только по соглашению, по факту начинают использовать в разнообразных даунстрим проектах, и хотя изначально это была просто деталь имплементации, теперь автор библиотеки не может её просто так убрать, не словам зависимые проекты.


    1. Drag13
      29.10.2018 23:18

      Идея не в почитать, идея в не поменять. Т.е. не нарушить внутреннюю структуру и, как следствие, работу объекта.


    1. xander27
      30.10.2018 12:15

      Мне кажется, суть в основном в том что бы, семантический обозначить какие поля приватные и уберечь от "случайных" использований. В других языках (java) можно вытащить приватные значения через relection. Если есть очень большое желание — всегда можно вытащить


    1. 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, т.к. скрыт в замыкании.


  1. Arlekcangp
    29.10.2018 23:02

    del


    1. Igmat Автор
      29.10.2018 23:28

      Ответ на комментарий пользователя Arlekcangp, который опять промахнулся веткой ?\_(?)_/?


      Как, я уже говорил ранее, вот здесь я предоставил рабочий пример использования private синтаксиса, но и у него есть проблема — ключевое слово this получает ещё одно усложнение в понимании (а оно и так непростое) т.к. в зависимости от лексического контекста обладает разным смыслом, как вы и предлагаете.


      Поэтому эта идея развилась в более непротиворечивый синтаксический сахар для всё того же Symbol.private стоящего в основе этой альтернативы.


      В любом случае, скорее всего, ключевое слово private будет жить только в TypeScript, а в ES сможет появиться только вместе с аннотациями типов.


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


  1. andreyverbin
    29.10.2018 23:46
    +3

    А зачем вообще убиваться по поводу доступности приватных полей извне? В большинстве языков это сделать можно и никто не жалуется. Безопасность на такой сомнительной вещи строить не получится. Так зачем усложнять язык? По мне текущего де-факто стандарта «this._x” более чем достаточно.


    Возможность залезть в кишки объекта это большое благо на самом деле. Например чтобы починить баг в библиотеке, бывает в тестах сильно проще проверить приватную переменную чем городить что-то вокруг и т.п. Я не призываю лезть в внутрь всех объектов, но эта фича имеет своё применение.


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


    1. jeron-diovis
      30.10.2018 14:30

      Полностью поддерживаю. Фронтенд полнится всевозможными библиотеками самого разного уровня качества. И часто действительно приходится самому их патчить – потому что либо исправление их автором либо займёт слишком много времени, а вам надо вотпрямщас; либо вы даже сами пуллреквест сделали, но их там уже 50 штук висит и автору принять их некогда; либо репозиторий вообще заброшен, а форк сделать не вариант, потому что эта библиотека находится в зависимостях ещё нескольких используемых вами пакетов… и так далее.

      Простой и доступный синтаксис для приватных полей приведёт только к тому, что сообщество начнёт его пихать куда надо и не надо (а оно начнёт, это же js-сообщество, в котором, ну будем честны – средний уровень компетенции довольно-таки посредственный). И этим катастрофически осложнит жизнь всем, кто будет плодами этого творчества пользоваться.

      #js_is_public


    1. Igmat Автор
      30.10.2018 14:34

      Собственно, была уже тьма дисскусий на тему hard private vs soft private.


      Авторы популярных фреймворков/библиотек в большинстве своё выступают за hard private потому что это позволяет им четко отделить публичный контракт от деталей имплементации и потом спокойно проводить рефакторинги, делать новые фичи и, в целом, развивать свои продукты, не боясь сломать проекты, которые зависят от деталей имплементации, потому что таких просто не будет при хорошо инкапсулированом коде.


      1. andreyverbin
        30.10.2018 16:23
        +1

        Это все попытка выдать желаемое за действительное. Если все хорошо сделано, то пользователю и не нужно лезть в кишки, а если все плохо, то любой значимый рефакторинг сломает контракт. И затем, найдите мне программиста на JS, который не в курсе, что если меняешь/читаешь что-то, что начинается с «_», то можно огрести? Все уже и так все знают, в от идиотов защититься невозможно.


        Самые частые проблемы с контрактом выглядят не «переименовало приватную переменную», а как «был метод O(1), стал O(n^2)”, не было исключения, теперь есть, раньше можно было метод дергать до «onload”, а теперь нельзя и т.п. Если сообщество обеспокоено контрактом, то нужно говорить не о private, а о полноценной ala Eifell системе. Наличие hard private автору библиотеки даст ложное спокойствие, а юзеру даст вполне конкретный геморрой. Я не вижу тут реальной выгоды ни для авторов ни для юзеров.


        1. 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.


          Не все авторы библиотек, готовы говорить своим пользователям, что раз вы юзали свойства с префиксом, то идите лесом.


          1. andreyverbin
            31.10.2018 12:16

            Почему же не готовы? В Java, .NET, С и C++ и в других языках готовы, и прямо говорят, «если вы меняете приватные переменные, то идите лесом». В JS сейчас чуть проще, чем в перечисленных языках поменять приватные переменные, но что с того? Авторы говорят обычно даже более сильную вещь — «используете недокументированные методы и функции — идите лесом». В вашем примере "_x" де факто приватная переменная, «де-юре» недокументированная функция. В любом случае юзер идет лесом и это ожидаемо.


            1. Igmat Автор
              31.10.2018 15:27

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


              Кстати, приводился пример какого-то тестового фреймворка из Java, для которого существуют популярные плагины, которые используют приватное состояние через рефлексию, что в итоге существенно замедлило развитие самого фреймворка. (в деталях мог ошибиться, т.к. описывал по памяти, но суть думаю ясна)


  1. koldoon
    30.10.2018 11:35
    +1

    На первый взгляд складывается впечатление, что комитет поставил своей задачей не решить конкретную проблему, а насолить, например, typescript (ну, или babel), и показать «кто тут главный», мол, сделаем как угодно, но только не так как у вас там принято. По-моему приватные переменные как таковые в js особо не нужны (поправьте меня, если не прав), а защиту на этапе разработки можно обеспечить любым статическим анализатором (typescript, coffeescript, dart — кому что нравится)


    1. justboris
      30.10.2018 12:04

      Со вторым аргументом согласен, от приватных переменных в рантайме пользы немного.

      А вот первый – это какая-то теория заговора. Там выше уже разбирались причины почему сделано именно так, а вы все туда же.


      1. koldoon
        30.10.2018 12:07

        Я это понимаю. Поэтому и говорю, что «на первый взгляд».


  1. AngReload
    30.10.2018 11:56

    Define для полей вообще зачем? В этом нет смысла!


    1. Igmat Автор
      30.10.2018 15:10

      Ответ довольно прост: [[Define]] семантика сделает декораторы для полей класса более предсказуемыми.


  1. 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, и защититься от его модификации извне.


    1. Igmat Автор
      30.10.2018 18:10

      Мешает то, что прокси, которую вы взяли из примера в статье, была всего лишь демонстрацией проблемы. Ваш пример с реальной прозрачной прокси работать не будет, потому что:


      1. Доступ к __self тоже дернёт get trap, который обернёт результат, а по скольку ссылка на this уже имеет соответствующую ей прокси (в примере из статьи этого нет, но можно глянуть в моём комментарии здесь), то она и будет возвращена, а значит методы будут вызываться в контексте прокси и бренд-чек будет выкидывать ошибку.
      2. Допустим наша реализация не обарачивает __self проперти, тогда исключение не будет выброшенно, но методы/геттеры/сеттеры вызванные после __self будут вызваны в контексте __self, а нам нужно, что бы они вызывались в контексте прокси.
      3. const __self = Symbol("self"); не защищает от модификации — Object.getOwnPropertySymbols позволяет извлечь любой символ из объекта.


      1. Cryvage
        30.10.2018 20:05

        Допустим наша реализация не обарачивает __self проперти, тогда исключение не будет выброшенно, но методы/геттеры/сеттеры вызванные после __self будут вызваны в контексте __self, а нам нужно, что бы они вызывались в контексте прокси.

        Вообще это интересный момент. Как выполнять методы, обращающиеся к полям, которых в текущем контексте просто не существует? Дело ведь даже не в brand-checking. Ну допустим не будет явной проверки. Но ведь приватных полей, имеющихся в оригинальном объекте, у прокси просто нет. И инициализировать он их не сможет, т.к. не имеет к ним доступа, и не знает, какие у них в данный момент значения, чтобы скопировать их себе. А после не сможет записать полученное состояние в оригинальный объект, т.к. опять же, доступа не имеет.
        Получается, проблема не столько в конкретной реализации приватных полей, сколько в невозможности обойти инкапсуляцию. Такая возможность конечно должны быть. И как правило, в других языках она есть. Если рассмотреть тот же C#, то там можно без проблем, как прочитать, так и записать приватное поле. Через рефлексию. Возможно и в JS стоит добавить новый метод в Reflect, позволяющий получить доступ к скрытому WeakMap, в котором хранятся приватные поля. Что-то вроде Reflect.GetPrivate(obj);


        1. Igmat Автор
          30.10.2018 20:22

          Symbol.private предлагает решение и оно довольно простое.
          У прокси действительно нет приватных полей оригинального объекта, но:


          1. Для каждой прокси всегда известен его таргет (и рантайм его знает)
          2. Доступ к полю используя что Symbol.private, что #priv отличается от доступа к публичному полю.

          Это приводит к простому решению, если код пытается осуществить доступ к приватному полю на прокси то вместо обращения к прокси, он сразу обращаеться к таргету этого прокси.


          Что-то вроде Reflect.GetPrivate(obj);

          Это нарушает концепцию hard private, коммитет подобные escape hatch не будет даже рассматривать.


          1. Drag13
            30.10.2018 21:22

            Это нарушает концепцию hard private, коммитет подобные escape hatch не будет даже рассматривать.


            А почему? Другие, более жесткие языки такое позволяют и всем от этого более менее хорошо. Чем аргументируют?


            1. Cryvage
              30.10.2018 22:17

              Я сначала тоже думал возразить, начал писать, что в других языках, рефлексия именно так и работает. Потом нашёл обсуждение на GitHub, где в принципе все эти аргументы, были сказаны и разжёваны.
              Лично я, почитав аргументы, так и не понял, в чём смысл такой жёсткости. Аргументы противников мне показались сильнее. Единственный весомый аргумент, что я увидел, это то что, было бы хорошо, если бы такая возможность была. Именно как возможность, с учётом того, что есть и другие возможности, реализовать иные типы приватности, я могу это принять. Правда не совсем понятно, зачем тут отдельный синтаксис. Для редких случаев, когда такая приватность действительно понадобилась, можно и явно WeakMap использовать.
              Но тут возникает проблема, исключения вылетают вовсе не при попытке обратиться к приватным полям напрямую, а при вызове публичных методов. И внешнему коду не понятно, какие методы можно вызывать из другого контекста, а какие нет. Вот в этом плане, однозначно, надо что-то улучшать. Хотя бы добавить возможность проверки, можно ли с данным методом делать call, apply и bind.


          1. Cryvage
            30.10.2018 21:54

            Это приводит к простому решению, если код пытается осуществить доступ к приватному полю на прокси то вместо обращения к прокси, он сразу обращаеться к таргету этого прокси.

            В принципе, в своём примере, я именно к этому и стремился. Вот только с «решёточным» синтаксисом, класс должен быть намеренно написан с расчётом на прокси: объявлять __self, и обращаться к приватным полям через него. И Proxy должен сделать исключение для __self.
            Это нарушает концепцию hard private, коммитет подобные escape hatch не будет даже рассматривать.

            Ок, если основной целью добавления «решёточного» синтаксиса была именно жёсткая приватность, и это было сделано осознанно, то я могу сказать только одно. Пусть будет. Но использовать её следует лишь тогда, когда это действительно необходимо. Во всех остальных случаях, можно просто использовать символы. Либо, как промежуточный вариант, явное объявление WeakMap — тут и жёсткая приватность, и возможность объявить некоторые объекты «друзьями», на которых эта приватность не распространяется. Главное, чего не надо делать, так это позиционировать приватные поля с решёткой, как главный и единственный способ инкапсуляции. Хорошо бы ещё был способ заранее различать методы, которые можно вызывать с другим this, и которые нельзя.