Как стать автором
Обновить

Наследование в JavaScript с точки зрения занудного ботаника: Фабрика Конструкторов

Время на прочтение15 мин
Количество просмотров8.3K
Автор оригинала: https://habr.com/ru/users/wentout/
lamp of light and apple of discordЭто история об одной очень специальной части JavaScript, самого используемого искусственного языка в мире в настоящее время (2019).

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

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

( все ссылки подчёркнуты )


Уверен так же, что при этом ни у кого из нас не вызовет сомнений то, что Брендан Айк (Эйх) — автор языка программирования JavaScript — является выдающимся гением! И не только лишь потому, что он частенько повторяет:
Всегда ставьте на JavaScript!
Давайте начнём! И нашей первой отправной точкой будет Воображение, в котором мы сперва выключим все предрассудки, недомолвки и прочие побочные эффекты.

Мы отправляемся Назад в Будущее, в эпоху предшествующую созданию современного Internet'а в ранних 1990х.

Со времени первых Хакеров, которые изобрели всё, чем мы (IT-шники) сейчас пользуемся, мы переносимся к впечатляющей зарисовке: о войне браузеров Netstcape Navigator 2 и Internet Explorer 3. Java только вышла, и почти всё из современного Интернет ещё не изобретено или не пре-открыто. Вполне возможно, что как и я, в те «старые добрые времена» вы были молоды, и смогли запомнить это прекрасное чувство причастности ко всему тому великолепию, что прям сейчас создаётся «на ваших глазах».

Итак, у вас есть очень мощный PC на самом современном Intell Pentium 200 MMX с 32Mb оперативки, Windows 3.11 или даже Windows 95 и вы с надеждой вглядываетесь в будущее! И, конечно же оба браузера у вас тоже установлены. У вас есть Dial-Up, через который вы подключаетесь к Сети за новым барахлишком, по-учёбе или просто пообщаться, по-chat'иться. Хотя, погодите, вы пока не можете чатиться прям в браузере, скорее всего вы пока пользуетесь системами с отложенной доставкой сообщений, чем-то вроде EMail или может быть UseNet, или, вполне возможно вы уже освоили мгновенную доставку посредством IRC.

Проходит пара лет и буквально ВСЁ меняется… Вдруг вы наблюдаете за анимацией снежинок на web-страничках поздравляющих вас с Новым Годом и Рождеством. Конечно же вы заинтересовываетесь тем, как это сделано, и обнаруживаете новый язык — JavaScript. Поскольку HTML для вас уже не нов, вы начинаете изучать эту заманчивую техно-поделку. Вскоре вы обнаруживаете CSS и оказывается, что это тоже важно, поскольку всё теперь сделано из комбинации этих трёх: HTML, JavaSript и CSS. Wow.

Примерно в это же время вы могли заметить пару замечательных штук в самой Windows, в ней появились CScript и HTA, и уже тогда стало можно создавать полноценные desktop приложения прям на JS (и это до сих пор работает).

И вот вы начали делать свой первый Web-Server, возможно на Perl или C~C++. Возможно даже вы начали использовать Unix-like операционку и делаете его на bash. И всё оно «крутится-вертится» благодаря Common Gateway Interface (не путайте с тем другим CGI). PHP ещё тоже почти не существует, но возможно он вскоре вам понравится.

Эра 200х. Вы теперь делаете ASP на JScript. Это очень похоже на тот JavaScript который работает внутри ваших web-страничек. Это так здорово! Вы подумываете о создании собственного шаблонизатора, некоей пародии на XML. И тут вдруг кто-то называет AJAX'ом все эти весёлые способы динамической загрузки содержимого, которые вы уже несколько лет как используете. И все они теперь думают, что есть только XMLHTTPRequest, но вы то помните, что данные можно передать в BMP, IFrame или даже вставив тэг <script>. А потом кто-то вдруг восторженно говорит о JSON и как он полезен, когда вы то уже вечность как гоняете данные посредством чего-то наподобие:

document.write("<" + "script src=" + path + ">");

It is all not that matter now, but you still can remember how

Опомнившись, время от времени вы начинаете обнаруживать себя ковыряющимся с Rhino или Nashorn в попытках удовлетворить пожелания Java-заказчиков, использующих Alfresco или Asterisk. И вы уже слышали о скором пришествии JavaScript в мир микрочипов, и очень вдохновлены этой новостью. И, конечно же, теперь у вас есть jQuery и Backbone.

Наблюдая за снегопадом приходящего 2010 вы уже знаете, в вашем мире скоро поменяются все правила игры, так как на поле вышел «Игрок № 1»: Node.js . И следующие 10 лет вы проведёте за этой новой игрушкой, и даже сейчас, в 2019 вы до сих пор не можете нарадоваться тому, какая она клёвая.

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

Но есть один маленький вопрос, который вы задаёте себе изо дня в день, из ночи в ночь на протяжении вот уже двух десятилетий:

Если бы вам пришлось это сделать, то как бы вы объяснили Эмпатию пользуясь JavaScript?


Вы же знаете, что одной из самых сложных тем в JavaScript является Прототипное Наследование и Цепочка Прототипов. И вы любите эту тему, вы можете объяснить как это всё устроено и работает, просто потому, что вы учили это почти что с самого начала, ещё до того, как Первая версия Стандарта увидела на свет, и где, как вы помните, есть 4.2.1 Objects:
ECMAScript supports prototype-based inheritance. Every constructor has an associated prototype, and every object created by that constructor has an implicit reference to the prototype (called the object’s prototype) associated with its constructor. Furthermore, a prototype may have a non-null implicit reference to its prototype, and so on; this is called the prototype chain.
Каждый объект создаётся с неявной ссылкой на прототип. Это называется цепочкой прототипного наследования, которую вы можете продолжать сколь угодно долго.

Ух-тыж… И если вдруг как и я вы может быть думаете, что это является одним из величайших изобретений в Compuer Science, тогда как бы вы выразили эффект, который оказало на вас прочтение этого утверждения?

Давайте вернёмся в начало. На дворе 1995 год. Вы Брендан Айк, и вам необходимо изобрести новый язык программирования. Возможно вам нравится Lisp или Scheme, во всяком случае некоторые их избранные части. И перед вами стоит необходимость решить проблему Наследования, поскольку должно сделать вид, что в языке есть некая реализация ООП. Подумаем: вам необходимо смешать все вещи которые вам нравятся, возможно также некоторые вещи которые вам не нравятся, и получившийся коктейль должен быть Достаточно Хорош, для того, чтобы никто не заметил подвоха до тех пор, пока не будет реальной необходимости разбираться в том как оно устроено изнутри.

И вот теперь вопрос: что бы такое сотворить с Наследованием?

Давайте вернёмся на миг в реальность. Что нам всем известно о Наследовании? Некоторые очевидные кусочки ответов на этот вопрос:

  1. Большинство живых форм основаны на Геноме. Это такое хранилище информации о вероятных характеристиках и предполагаемом поведении живых существ. Когда вы являетесь живым существом, вы носите в себе часть генома, можете её распространять, а получили вы её от предыдущих поколений.
  2. Создать живое существо можно пользуясь двумя техниками: смешать (геномы) двух предков или, возможно, использовать однодомное клонирование одного из них. Конечно же, сегодня нам доступны технологии, позволяющие смешивать геномы более чем одного существа, но это гораздо менее очевидно и не так естественно.
  3. Фактор времени важен. Если какого-нибудь наследного свойства ещё нет, или уже нет, то наш единственный выход: создать его «с нуля». Помимо этого есть ещё Наследие, то, что переходит к существу от его предков не через геном, а через законы собственности, и это тоже может быть существенно.

Возвращаемся назад в прошлое, и правильный вопрос, который нам необходимо самим себе задать, звучит теперь так: А, собственно, наследование Чего мы хотим получить?

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

Давайте теперь «добъём» себя: мы ведь в 1995м, и у нас мощнейший ПК со всего лишь 32 Мегабайтами оперативной памяти, а мы пытаемся создать интерпретируемый язык, так что мы должны очень сильно позаботиться об этой памяти. Каждый кусочек данных, особенно Строковые объекты, потребляет много памяти, и у нас должна быть возможность объявить этот кусочек только один раз, и по возможности в дальнейшем всегда пользоваться лишь указателями на область памяти занятую этим кусочком. Резюмируем: очень Нелёгкий вопрос.

Есть популярное мнение: "JavaScript создан из объектов". Пользуясь этой банальщиной мы очень просто можем ответить на вопрос "из чего" наследовать и "во что": из Объектов в Объекты. В угоду вопросу об экономии памяти получается, что все данные должны храниться в этих объектах, и все эти ссылки на данные должны так же храниться в Наследных свойствах этих объектов. Возможно теперь ясно почему в 1995 нам в самом деле могло понадобиться создать Дизайн, основанный на цепочке прототипов: он позволяет экономить память так долго, как это вообще возможно! И в общем мне думается, что это по прежнему может быть очень важным аспектом.

Основываясь на указанном дизайне и мнении "всё есть Объект" мы можем попытаться клонировать что-нибудь этакое. Только вот что такое тут у нас теперь Клонирование? Думаю, что выполняя предписания наших требований мы можем предположить что-нибудь вроде Структурных или Поверхностных Копий, в что-то похожее на предшественников современного Object.assign.
Давайте в 1995 реализуем простую структурную копию пользуясь for (var i in) {}, поскольку стандарт это уже позволял:

// back in 1995 cloning
// it is not deep clone,
// though we might not need deep at all
var cloneProps = function (clonedObject, destinationObject) {
  for (var key in clonedObject) {
    destinationObject[key] = clonedObject[key];
  }
};

Как вы можете видеть, такой подход до сих пор «работает», хотя вообще, конечно же я бы порекомендовал посмотреть на модуль deep-extend для более детального понимания того, как сделать клонирование в JavaScript, но для целей статьи нам вполне подойдёт и последовательное применение описанного cloneProps, ведь мы вполне могли бы пользоваться им в те стародавние времена:

  • клонирование Объектов с применением Конструктора: используя конструктор создадим как минимум два разных клона

    
    // cloneProps is described above
    var SomeConstructor = function (clonedObject) {
      cloneProps(clonedObject, this);
    };
    var someExistingObjectToClone = {
      foo : 'bar'
    };
    var clone1 = new SomeConstructor(someExistingObjectToClone);
    var clone2 = new SomeConstructor(someExistingObjectToClone);
    // clone1.foo == clone2.foo
    
  • клонирование Конструктора из Конструктора: реализуем использование поведения одного конструктора из другого конструктора

    var SomeConstructor = function () {
      this.a = 'cloned';
    };
    var AnotherConstructor = function () {
      // Function.prototype.call
      // was already invented in 1st ECMA-262
      SomeConstructor.call(this);
    };
    
  • клонирование Конструктора с применением Объекта: мы будем использовать один и тот же объект для реализации клонирования как минимум в двух конструкторах

    
    var existentObject = {
      foo : 'bar'
    };
    var SomeConstructor = function () {
      cloneProps(foo, this);
    };
    var OtherConstructor = function () {
      cloneProps(foo, this);
    };
    
  • клонирование одного Объекта из другого Объекта: используем один объект для создания нескольких его клонов. Тут описывать нечего, берём просто как есть наш cloneProps из первого примера выше.

С клонированием в общем всё просто, как мы видим, всё понятно и вообще, но...

Настолько ли просто будет нам сделать именно Наследование Сущностей, пользуясь комбинацией их предшественников?

  • Наследование объекта с применением Конструктора: это и есть само предназначение конструкторов, просто покажем как оно исходно устроено.

    
    var existentObject = {
      foo : 'bar'
    };
    var SomeConstructor = function () {};
    SomeConstructor.prototype = existentObject;
    
    var inheritedObject = new SomeConstructor();
    
    // we have no instanceof yet in ECMA 262 of 1995
    // therefore we are unable to rely on this
    window.alert(inheritedObject.foo); // bar
    
  • Наследование Конструктора из другого Конструктора: вне сомнений, первый кто это заметил был выдающимся Гением. В общем это ещё один классический пример отовсюду.

    
    var FirstConstructor = function () {
      this.foo = 'bar';
    };
    var InheritedConstructor = function () {
    	FirstConstructor.call(this);
    };
    InheritedConstructor.prototype = {
      bar : 'foo'
    };
    InheritedConstructor.prototype.constructor = FirstConstructor;
    var inherited = new InheritedConstructor(); // { foo : 'bar', bar : 'foo' }
    

    можно было бы изложить чего-нибудь замудрёное, но зачем
  • Наследование Конструктора из Объекта: опять же, мы просто воспользуемся .prototype = object каждый раз, когда оно нам будет необходимо, нечего описывать, нам всегда достаточно присвоить Constructor.prototype так как туда положено класть объект, и по неявной ссылке мы получим все его свойства.
  • Наследование Объекта из Объекта: тоже самое. Мы просто кладём первый объект в Constructor.prototype и как только мы скажем new Constructor мы создадим унаследованную копию, в которой будут неявные ссылки на свойства нашего первого объекта.

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

Правда, осталась тут такая вот маленькая деталька из пункта 4.2.1:
иметь возможность делать это так долго, как это будет нужно, поскольку там написано:
and so on
Что же, давайте попробуем сделать наследование действительно бесконечным, пользуясь технологиями из 1995 года.

В самом деле, давайте представим, что у нас есть две Сущности { objects }, и Не конструкторы, а простые объекты. И нам захотелось унаследовать один от другого, а потом возможно от другого, и ещё от другого и так далее…

Но Как?


Взглянем немного дальше, глубже.
Правильный ответ здесь опять же в том: Наследование чего нам необходимо создать?

Ведь нам не нужны эти Сущности сами по себе. Нам нужны их свойства: ассоциированные данные, потребляющие память; а так же нам, возможно нужно некоторое поведение: методы, использующие эти данные. И будет так же вполне Честно, если у нас будет возможность проверить откуда и куда мы унаследовали и с использованием чего. В целом будет так же здорово, если бы мы могли Воспроизводить заложенный дизайн паттернов Наследования в будущем, подразумевая, что если мы наследуем одно от другого много раз, мы будем всегда получать один и тот же результат, согласно тому, что мы написали (контракт). Хотя и вот ещё, может быть так же полезным, чтобы мы каким-то образом зафиксировали момент создания, так как наши «предшествующие» сущности могут меняться со временем, и «наследники», отдавая им в этом уважение, всё же меняться вместе с ними вполне могут и не совсем захотеть.

И, поскольку весь наш код это комбинация данных и поведения, то будет ли это вообще нормальным использовать тот же самый способ — комбинирование данных и представления — при проектировании системы Наследования?

Как по мне, все это напоминает то, что мы видим наблюдая Жизнь во всех её невероятных формах. От первых одноклеточных, к многоклеточным и их потомкам, и далее к Животным, людям, гуманнизму и племенам, цивилизациям, Интеллекту, и его искусственным формам, и далее В Космос, в Галактику, к Звёздам! и:
“…All we need to do is make sure we keep talking…”
(всё, что нам необходимо делать — это продолжать общение)

Невероятная в своей продуманности цитата из Стивена Хокинга, впоследствии популяризованная в вот этом шедевре Pind Floyd.

Языки программирования, основанные на передаче сообщений и Flow-based концепции, реализованной через устойчивое внутреннее API позволяют, двигаясь от просто данных развивать к более высоким абстракциям описание и всего остального. Думаю, это и есть Искусство в чистом виде, и то, как это работает в частности в глубоко скрытых структурах JavaScript через неявные связи между данными в цепочках прототипов.

Представим снова двух предков, они общаются и в один момент их эмоции и чувства создают ребёнка. Ребёнок растёт, встречает другого ребёнка, они общаются, и появляется следующий потомок, и далее и далее и далее… И нам всегда нужно Именно Два родителя, иначе это не естественно, это уже будет генная инженерия. Два, не больше и не меньше. Потомок, получает так же и их Наследие (Legacy), так вот просто и понятно.

Понимаю, прозвучит странно, но да, у нас есть всё необходимое для того, чтобы создать именно эту модель Наследования в 1995 году. И основой всего этого служит именно 4.2.1 Objects, неявные ссылки через прототип (implicit referencing through prototype).

И именно вот это всё, как оно есть, комбинирование ParentObject с ParentConstructor через указание .prototype и потом Constructor возможно создаст нам ChildObject, конечно же если мы скажем волшебное слово "new":

var ParentObject = {
  foo : 'bar'
};
var ParentConstructor = function () {};
ParentConstructor.prototype = ParentObject;

var ChildObject = new ParentConstructor();

// starting from 1995 and then ECMA 262
// we are able to say new
// each time we need a ChildObject 

Мы можем разглядеть здесь обоих наших предков. В момент когда мы сказали волшебное слово "new" мы попросили их пообщаться. Если они не захотят общаться, Жизнь прекратится, процесс упадёт с ошибкой и компилятор (интерпретатор) скажет нам об этом.

Конечно же да, но мы ведь просили о Дереве Наследования или пусть, что может быть явно гораздо проще, хотя бы о Генеалогическом дереве. И ответ по прежнему тот же самый… наш Child Object вырастает, и сам становится Parent Object, потом встречает новый Constructor Object и как только мы скажем заветное слово "new" — магия:

// this Assignment is just to show it grew up
var ChildObjectGrownToParent = ChildObject;

var AnotherConstructor = function () {};
AnotherConstructor.prototype = ChildObjectGrownToParent;

var SequentialChildObject = new AnotherConstructor();
// checking Life Cycle ;^)
console.log(ChildObject instanceof ParentConstructor); // true
console.log(SequentialChildObject instanceof ParentConstructor); // true
console.log(SequentialChildObject instanceof AnotherConstructor); // true

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

1: Сообщество... Как вы можете легко проверить сами, указание в .prototype ParentConstructor'а или AnotherConstructor'а является очень серьёзным и строгим Социальным Контрактом в нашем Племени. Оно создаёт ссылку на свойства ParentObject (.foo) для Наследников: ChildObject и SequentialChildObject. И если мы избавимся от этого указания, то данные ссылки исчезнут. Если мы исхитримся, и переназначим это указание на какой-нибудь другой объект, то наши наследники мгновенно унаследуют другие свойства. Поэтому соединяя предков через .prototype, вероятно мы бы могли сказать, что мы создаём некое подобие ячейки общества, потому что ведь указанные «предки» могут воспроизводить множество одинакового потомства каждый раз как только мы просим их об этом используя new. И, таким образом, разрушив «семейство», мы губим наследные качества его потомков, такая вот драма ;^)

Пожалуй, это всё разговор про Legacy в нашем коде, мы Должны об этом заботиться когда собираемся создать безопасный и поддерживаемый код! Конечно же, ни о каком SOLID, Liskov Principle и Design by Contract и о GRASP и речи не шло в том самом 1995, но ведь очевидно же, что эти методологии не были созданы «с нуля», всё началось гораздо раньше.

2: Семья... Мы можем легко проверить, что нашему ParentObject позволено быть очень фривольным с другими Конструкто[ршами~рами]. Это не честно, но мы можем задействовать столько Конструкторов, сколько пожелаем при Наследовании нашего ParentObject и, таким образом, создать сколько угодно семейств. С другой стороны каждый Конструктор очень тесно связан с ParentObject через задание .prototype, и если мы не желаем нанести вред нашим наследникам, мы должны сохранять эту связь так долго, как это вообще возможно. Мы могли бы назвать это искусством трагедии в истории нашего племени. Хотя, конечно же, это защищает нас от Амнезии — забывчивости в том, что мы унаследовали и от кого, и почему наши наследники получают такое Legacy. И, восхваляя великую Мнемозину!, мы действительно можем легко протестировать наше Дерево цепочки прототипов и найти Артефакты того, что именно мы сделали неправильно.

3: Старость... Наши ParentObject и Constructor безусловно подвержены повреждениям во время жизненного цикла нашей программы. Мы можем попытаться об этом заботиться, но никто не застрахован от ошибок. И все эти изменения могут нанести вред Наследникам. Мы должны заботиться об утечках памяти. Конечно же, мы вполне можем уничтожать ненужные части кода в runtime и, таким образом освобождать память, более не используемую в нашем Life Cycle. Кроме того мы должны избавляться от всех возможностей создания Временных Парадоксов в цепочках прототипов, так как в самом деле это достаточно просто унаследовать Предка от его собственного Потомка. А вот это вот уже может быть очень опасно, так как подобные техники заигрывания с прошлым из будущего могут создавать целые кучи сложно воспроизводимых Гейзенбагов, в особенности если мы будем пытаться измерять что-нибудь, что может само по себе меняться с течением времени.

Хроника Решения


Пусть это будет просто, очевидно и не очень полезно, но вместо размышлений о наших Конструкторе и ParentObject'е как о Маме и Папе, давайте опишем их как яйцеклетку и… пыльцу. Тогда, в дальнейшем, когда мы создадим Зиготу, используя заветное слово "new", это больше не будет нести никакого вреда нашему Воображению!

В тот момент как мы проделаем это, мы сразу избавимся от всех трёх вышеперечисленных проблем! Конечно же, для этого нам понадобится возможность создавать сами зиготы, а значит, нам нужна Фабрика Конструкторов. И называйте теперь как хотите, мамы, папы, какая разница, ведь в суть в том, что если мы собираемся сказать "new", то мы должны создать «новенькую» цветочную клетку-конструктор, положить на неё пыльцу и только это и позволит нам вырастить новый «правильный» Подснежник в далёком и таком заснеженном 2020м:

var Pollen = {
  season : 'Spring'
};
// factory of constructors 
var FlowersFactory = function (proto) {
  var FlowerEggCell = function (sort) {
    this.sort = sort;
  };
  FlowerEggCell.prototype = proto;
  return FlowerEggCell;
};
var FlowerZygote = FlowersFactory(Pollen);
var galanthus = new FlowerZygote('Galanthus');

(и да, не забудьте проверить сезон у этого подснежника, а то, ведь снежинки то падали-падали, а подснежник то весенний цветок...)

Безусловно, Цикломатическая Сложность решений, которые вы попытаетесь создать с использованием этого подхода будет вполне сопоставима с Загадкой Энштейна. Поэтому я тут «на коленке» состряпал библиотечку, она может помочь с созданием цепочек конструкторов и мемоизацией, (прим. ред.: ну, такоэ, возьми с полки пирожок, bla-bla-bla)…

И хотя я не могу это доказать, но этот подход вполне успешно время от времени применяется уже как два десятилетия, если необходимо быть 146% уверенным в том, что с наследованием всё сделано нормально. Вы легко сами сможете убедиться, что он элементарно тестируем, воспроизводим и поддерживаем (прим. ред.: ага, щас, всё бросили и пошли убеждаться).

Конечно же, здесь не вся история, мы просто изложили факт: JavaScript спроектирован достаточно хорошо для того, чтобы описывать Генеалогический Граф прямо через Наследование. Конечно же, здесь мы лукаво не коснулись темы Классовой деградации, но уверен, вы и сами легко можете заменить FlowerEggCell на FlowerEggCellClass внутри FlowersFactory: суть останется прежней, если вы захотите проверить ваши цветочки через instanceof вы увидите что все они потомки FlowerEggCell на которую вы ссылаетесь через FlowerZygote. И конечно же теперь вы можете изменить свойства самой FlowerZygote, ведь это не принесёт никакого вреда самой FlowersFactory, она останется способной создавать другие, новые FlowerEggCell или FlowerEggCellClass конструкторы и в будущем согласно из исходного "reference" дизайна который вы туда заложили.

Надеюсь, что эта статья развеяла все сомнения в важности слова .prototype и в следующий раз когда вы увидите null в том месте где должен бы был быть this, например .call(null, .apply(null или .bind(null вы испытаете Печаль от осознания того насколько их code style беден по дизайну (прим. ред.: а вот Sorrow найди и послушай, есть и на ютубчике, она годная, там даже про сломанные промисы есть пара правильных строк).

Спасибо за прочтение!

До скорых встреч!

Искренне ваш V
Теги:
Хабы:
Всего голосов 17: ↑11 и ↓6+5
Комментарии1

Публикации

Истории

Работа

Ближайшие события