Поля классов

Сначала немного истории ...

С тех пор, как TC39 (плата, управляющая ECMAScript) опубликовала ключевое слово class, многие из нас были взволнованы тем, насколько легко стало внезапно писать полуклассические классы на Javascript. Больше не нужно вручную подключать прототипы. Больше никаких опасений, что кто-то изменит <classname>.prototype и сломает те места, которые мы использовали неосознанно instanceof. И, конечно же, у нас наконец-то есть простой способ наследования от собственных типов, который не нарушает собственные функции-члены! Но чего-то не хватало.

Где была возможность объявлять элементы данных в class? Самое близкое, что мы могли найти, - это свойства аксессуаров. Но даже тогда, как мы должны были хранить данные, полученные в этих средствах доступа? Так что да, class был в стадии разработки. Они дали нам удобный способ создания прототипа с функционально-значными свойствами и функцией-конструктором. Само по себе это уже было большим благом. Мы уже привыкли размещать наши свойства данных в конструкторе как есть. Так что ничего особенного там не изменилось. И все же казалось, что чего-то не хватает.

Это забавно. TC39 знал, чего не хватало, и работал над этим еще до того, как еще не выпущена спецификация ES4. Есть много потенциальных причин, по которым свойства данных не учитывались, но я бы только догадывался, если бы попытался сказать, что это такое. Я понимаю, что существует хорошо известная проблема с добавлением свойств в прототип, но только в 1 конкретном случае. Если свойство данных является примитивом, когда код пытается изменить значение, отображаемое в экземпляре, вместо значения, записываемого в прототип, оно записывается в экземпляр и затеняет значение в прототипе. Таким образом, значение прототипа сохраняется, а экземпляр сохраняет свое значение, зависящее от экземпляра. Но что произойдет, если свойство содержит объект?

Вот где дела идут плохо. Как и в случае с примитивом, если новый объект записывается в экземпляр с этим свойством, экземпляр получает новое свойство с новым объектом в качестве его значения. Однако, если свойство объекта в свойстве прототипа изменяется посредством экземпляра, ну… объект изменяется без копирования. Таким образом, все экземпляры в конечном итоге видят это изменение. Это нехорошо, но это просто неотъемлемая часть того, как работает язык.

Так что да, class свойства данных были пропущены. Но простое существование ключевого слова class в ES привело к желанию создавать классы, содержащие скрытые данные, данные, которые нельзя было наблюдать извне экземпляра. С заводскими функциями это было достаточно просто. Просто добавьте несколько переменных в закрытие. Каждый запуск функции получит свою собственную копию этих переменных. class экземпляров, однако, не смогли этого сделать. Наиболее близкой концепцией является использование WeakMaps и определения class внутри замыкания. class затем может использовать WeakMaps для хранения скрытых данных вдали от посторонних глаз. Он работает, но требует много памяти, потенциально подвержен ошибкам и не является самым простым кодом для чтения и сопровождения.

Будущее…?

Почему "?"? Что ж, вы, вероятно, поймете, когда прочитаете эту статью. В настоящее время в разработке находится предложение, известное как «поля класса предложения». В наши дни TC39 работает из репозиториев GitHub и приглашает все сообщество Javascript прийти и поднять проблемы, написать новые предложения и обсудить все, что касается этих предложений, чтобы помочь им получить некоторую информацию о том, как это повлияет на сообщество в целом. У них есть эта политика в течение некоторого времени, но переход на GitHub значительно упростил процесс. Если вы хотите лично ознакомиться с предложением, просто посмотрите сюда:



Как сказано в теге, они пытаются добавить свойства данных к классам ES, как общедоступным, так и частным. Их подход довольно интересен. Попробуйте просмотреть этот код:

var Node = (function() {
  var pvt = new WeakMap();

  class Node {
    constructor() {
      pvt.set(this, {
        child: null,
        nodeId: Symbol("NodeID")
      });
    }
    link(newChild) {
      let cursor = pvt.get(newChild);
      let q = pvt.get(this);
      while (cursor.child !== null) {
        let p = pvt.get(cursor.child);
        if (cursor.nodeId === q.nodeId) {
          throw new TypeError('Cycle!');
        }
        cursor = p;
      }
      if (cursor.nodeId === q.nodeId) {
        throw new TypeError('Cycle!');
      }
      pvt.get(this).child = newChild;
    }
    getTail() {
      let cursor = pvt.get(this);
      while (cursor.child !== null) {
        cursor = pvt.get(cursor.child);
      }
      return cursor;
    }
  }

  return Node;
})();

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

class Node {
  #child = null;
  #nodeId = Symbol("NodeID");
  link(newChild) {
    let cursor = newChild;
    while (cursor.#child !== null) {
      if (cursor.#nodeId === this.#nodeId) {
        throw new TypeError('Cycle!');
      }
      cursor = cursor.#child;
    }
    if (cursor.#nodeId === this.#nodeId) {
      throw new TypeError('Cycle!');
    }
    this.#child = newChild;
  }
  getTail() {
    let cursor = this;
    while (cursor.#child !== null) {
      cursor = cursor.#child;
    }
    return cursor;
  }
}

Уродливый…

Все знают об этом фильме «Хороший, плохой и уродливый», верно? Почему бы нам сначала просто не убрать с дороги «уродливое», ведь оно действительно маленькое. Это # ваше средство объявления и доступа к закрытым полям. Да, их называют «поля». Идея состоит в том, чтобы отвлечь вас от мысли о них как о чем-то принадлежащем class, и заменить это на то, что вы думаете о них как о чем-то принадлежащем только экземплярам. По сути, «поле» - это свойство, зависящее от экземпляра. Но тьфу! Это #!

Поверьте мне. Учитывая то, как было задумано это предложение, лучшего решения действительно не было. Согласитесь, он небольшой, эргономичный и относительно простой для понимания. Вы знаете, что _ соглашение, которое мы сейчас используем, чтобы обозначить, что поле является «частным»? Что ж, TC39 принял лозунг: # - это новый _. В самом реальном смысле для каждого свойства экземпляра, которое вы хотите сделать частным, просто замените _ на #, и теперь движок будет защищать это поле от всего, кроме экземпляра-владельца. Вот почему, несмотря на то, насколько это уродливо, # можно не замечать….

Добро…

Теперь вы поймете, почему я хотел убрать с пути уродливое: есть много «хорошего» за цену этого маленького уродства. Во-первых, мы получаем декларативные поля (свойства экземпляра) в `class`. К сожалению, из-за вмешательства TypeScript и удерживаемого мнения других языков о том, что «частный x» должен быть доступен с «this.x», ключевое слово private просто не считалось жизнеспособным. Ну что ж. Мы все еще можем объявить наши общедоступные и частные поля как таковые:

class Example {
  #iAmPrivate = true;
  iAmPublic = this.#iAmPrivate;
}

Ты это видел? Вы заметили использование this в задании? Под капотом эти поля фактически присоединяются к объекту экземпляра во время выполнения конструктора сразу после того, как this было присвоено значение, но до кода конструктора, следующего за super(). Вот почему доступны this и все связанные с ним значения. Это немного неудобно, но удивительно полезно, если вашему конструктору нужно только присвоить полям начальные значения. Таким образом, вы можете полностью избежать использования конструктора.

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

class Example {
  #data = 1;
  data = 2;
}

Вышеупомянутый класс имеет 2 свойства с именем «данные», вроде. Частное свойство доступно через сигилу (#) и лексическое имя «данные». Однако это не настоящее название собственности. Сигил вызывает поиск, который находит скрытое имя свойства. Поскольку эти внутренние имена невозможно обнаружить никакими средствами, данные, содержащиеся в свойстве, остаются защищенными от внешнего доступа. Это позволяет разработчикам изменять личные детали класса, не затрагивая тех, кто может изменять общедоступные свойства класса.

Может быть, нужно небольшое пояснение. С частным полем вы получаете свойство, зависящее от экземпляра, которое назначается во время выполнения конструктора. Ничто за пределами лексической области определения class не может получить доступ к частным полям экземпляра этого class. Так что, если он не входил в исходное определение class, у него нет доступа к закрытым полям этого класса. Присоединение функций к прототипу или конструктору class не предоставит им доступ к закрытым полям экземпляра.

Невозможно перечислить частные поля. Ни одна из функций отражения на Object вообще не может видеть закрытые поля. Даже прокси не может перехватить доступ к частному полю. Фактически, если вы не .toString конструктор, ваш код вряд ли заметит, что там вообще есть какие-либо частные поля. Таким образом, по большей части изменение частного поля в вашей class или библиотеке не будет замечено кодом, который его использует. Детали вашей реализации можно будет безопасно спрятать в приватных полях.

За этим предложением следуют



что позволяет использовать сигил (#) в объявлениях методов так же, как и в полях, и



что позволяет частным полям и методам быть статическими. Эти два предложения завершают детали, касающиеся полей класса предложения. Так что да, похоже, что TC39 изо всех сил пытается восполнить то недостающее чувство, которое мы испытывали с момента выпуска class.

Если у вас что-то не в порядке, это, вероятно, потому, что вы помните название фильма и заметили, что я еще не упомянул «плохое». Я не делал этого из соображений психологии. Люди склонны к первому мнению, которое они рисуют. Я действительно хочу, чтобы вы увидели, что, хотя синтаксис уродлив, это не проблема. То, что вы получаете взамен этого уродства, гораздо больше, чем эстетически неприятная цена. Но было бы несправедливо, если бы я не упомянул….

Плохо

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

Позвольте мне прояснить это: можете ли вы простить «плохое» или думаете, что это слишком много, вам следует использовать ссылку на предложение в «Будущем…?» выше, чтобы высказать свое мнение. Сообщите TC39, что вы думаете об их предложении. Вполне возможно, что это ваш последний шанс высказать свое мнение. TC39 предлагает это предложение на этапе 3. Это означает, что они почти закончили настройку семантики. После перехода на этап 4 он становится постоянной частью языка.

Ни на минуту не думайте, что ваше мнение не имеет значения. Если вам нравится это предложение, несмотря на все «плохие», то вы должны высказаться, иначе вы рискуете потерять функцию, которую так долго ждали. Если вы находитесь по ту сторону забора, все будет так же. Говорите, если вы не готовы рискнуть, что вас обременят «плохим». Больше никаких задержек:

  • Общедоступные поля и наследование
    ES - это язык на основе прототипов, а не язык на основе классов. Подход, принятый для полей класса, игнорирует этот факт. Таким образом, он нарушает функцию наследования и позволяет полям базового класса затенять свойства средства доступа в производном классе. В основном это происходит из-за семантического конфликта между определениями собственных свойств и наследованием на основе прототипов.
    TC39 решил, что поля должны быть определены в экземпляре, а не просто установлены. Однако при этом свойства базового средства доступа не будут активированы, когда значение определено для поля производного класса с тем же именем. Кроме того, поскольку оно применяется непосредственно к объекту экземпляра, поле всегда будет затенять любое свойство прототипа с тем же именем в любом месте иерархии наследования. Вот пример Babel.io. Вам нужно будет открыть консоль разработчика, чтобы увидеть результат.
  • Путаница между частными и общедоступными именами
    Поскольку и частные поля, и общедоступные поля могут иметь одно и то же видимое имя, все, что требуется, - это забытый или добавленный символ (#) при попытке доступа, чтобы вызвать неожиданные результаты . Если сигил неправильно добавлен к классу, содержащему частное поле с тем же именем, личные данные будут непреднамеренно перезаписаны. Если закрытого поля не существует, будет выдана непредвиденная ошибка. Что еще хуже, если сигил случайно пропущен, код может какое-то время нормально функционировать, поскольку публичное поле будет либо перезаписано, либо создано. Не существует разумного способа обнаружить, когда сигил был случайно пропущен.
    То же самое верно и для объявлений, хотя причины обратны. Однако следует отметить, что вероятность ошибки при объявлении относительно мала, если только в одном операторе не объявлен большой набор полей (частных и общедоступных). Однако сама по себе эта возможность проблематична. В этом случае нет разумного способа обнаружить случайное добавление сигилы.
  • Путаница в синтаксисе с общедоступными полями
    Поскольку общедоступные поля не требуют каких-либо префиксов, они приводят к большей чувствительности к контексту с несовместимыми значениями. Взгляните на эти примеры:
//Case 1:
[a] = [1,2,3]      //Destructuring
class X {
  [a] = [1,2,3]    //Array assignment to calculated property
  m() {
    [a] = [1,2,3]  //Destructuring again
  }
}
//Case 2:
a = 1; b = a * a      //Simple assignment: a = 1, b = 1
class X {
  a = 2; b = a * a    //Property initialization: this.a=2, this.b=1
  m() {
    a = 1; b = a * a  //Back to simple assignment of non-fields
  }
}
  • Опасности ASI для кодирования без точек с запятой
    Если вам нравится кодировать без точек с запятой, вы можете найти поля немного проблематичными. В некоторых сценариях, связанных с назначением, они необходимы. В приведенных ниже примерах каждая строка в определении должна быть отдельным полем.
//Case 1:
class Test1 {
  out
  in
  name
}
// Test2 actually is class { out = 'out' in name }
class Test2 {
  out = 'out'
  in
  name
}
//Case 2:
class Test1 {
  set = new Set
  f() { ... }
}
// Test2 actually is class { set f() { ... } }
class Test2 {
  set
  f() { ... }
}
  • Разрыв в ожиданиях доступа
    Если вы привыкли перебирать свойства объекта, вам не повезет, пытаясь перебирать список закрытых полей. Для this.#x нет эквивалентного синтаксиса []. Таким образом, даже если вы укажете имя каждого частного поля в массиве, вы не сможете получить доступ к полям, соответствующим этим именам, не прибегая к eval.
  • Без деструктуризации
    Не существует средств деструктурирования частных полей экземпляра объекта вне или внутри класса.
  • Прокси-сервер не будет работать
    Прокси-сервер объекта, содержащего частные поля, не сможет использоваться в качестве this для любой функции-члена, которая обращается к этим частным полям. Это означает, что код, использующий прокси, сломается, если проксируемый объект содержит закрытые поля. Это не влияет на использование мембран, поскольку они назначают this в качестве прокси-цели. Однако существует множество вариантов использования прокси, которые требуют this назначения прокси вместо исходного целевого объекта. Вот пример Babel.io.
  • Отсутствуют эквиваленты модификаторов protected, friend, internal
    Class-fields не предоставляют возможности для этих модификаторов, поэтому для тех библиотек, которые используют соглашение_ как для частных, так и для защищенных свойств, вам все равно придется разобраться с _.
  • Многие решения остались для предложения, не находящегося на стадии 3
    Хотя TC39 отклонил все вышеупомянутые проблемы, они рекламируют предложение «декораторов» как решение для них. Однако это предложение еще не дошло до стадии 3. Это означает, что нам придется подождать, пока предложение декоратора достигнет стадии 3, а затем 4, прежде чем мы сможем решить любую из этих проблем. Однако главная причина, по которой декораторы не достигли стадии 3, заключается в том, что они слишком сложны и слишком мощны.

Что касается проблем, которые я перечислил здесь, я только кратко описал проблему. Я подумал, что эта статья уже достаточно длинная. Хотя я даже не перечислил все проблемы, я надеюсь, что этого списка достаточно для любого читателя, чтобы определить свою позицию по предложению о полях классов. Если после прочтения этого списка вы обнаружите, что сомневаетесь в этом предложении, есть альтернативы, которые вы также можете изучить.







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