Здесь собраны самые популярные вопросы, задаваемые на русскоязычных собеседованиях front-end разработчиков на React.js. Тематика вопросов включает в себя как основы JavaScript и веб-технологий так и глубокое понимание работы React.js и смежных технологий (Redux, MobX и прочего).Красным цветом обозначены узлы, которые были обновлены. Эти узлы представляют элементы UI, состояние которых изменилось. После этого вычисляется разница между предыдущей и текущей версиями виртуального DOM-дерева. Затем все родительское поддерево подвергается повторному рендерингу для представления обновленного UI. Наконец, это обновленное дерево используется для обновления RDOM.
CSS:
1. Зачем за ранее указывать размер картинок?
2. Что такое блочная модель, и как, с помощью CSS, менять расположение блоков на странице в браузере?
3. Что вы можете рассказать о медиа-запросах?
4. Что такое препроцессор и зачем он нужен?
CSS препроцессор (от англ. CSS preprocessor) — это надстройка над CSS, которая добавляет ранее недоступные возможности для CSS, с помощью новых синтаксических конструкций.
Основная задача препроцессора — это предоставление удобных синтаксических конструкций для разработчика, чтобы упростить, и тем самым, ускорить разработку и поддержу стилей в проектах.
CSS препроцессоры преобразуют код, написанный с использованием препроцессорного языка, в чистый и валидный CSS-код.
5. Что такое Mixins, переменные и вложенность в препроцессорах?
@flex ($justify, $align) {
display: flex;
justify-content: $justify;
align-items: $align;
flex-flow: column;
}
div {
.flex(center, stretch);
}
Да, они есть и в «чистом» CSS, но реализованы менее удобно.
@colorForButton: #76AB04
button {
background: @colorForButton;
}
Благодаря препроцессорам не нужно каждую строку прописывать отдельно. В них действуют элементарные правила вложенности, как и в структуре HTML.
div {
dispaly: flex;
anotherDiv {
margin: 20px 0px;
}
@media (max-width: 720px) {
position: absolute;
}
}
6. Расскажите про блочные и строчные элементы?
Такой элемент представляет собой прямоугольник, который по умолчанию занимает всю доступную ширину страницы (если иное значение не указано в CSS), а длина элемента зависит от его содержимого. Такой элемент всегда начинается с новой строки, то есть, располагается под предыдущим элементом. Блочный элемент может содержать в себе другие блочные и строчные элементы.
Примеры блочных элементов: <div>, <p>, <ul>, <ol>, <h1> и т. д.
В отличие от блочного, строчный элемент не переносится на новую строку, а располагается на той же строке, что и предыдущий элемент. Такие элементы, как правило, находятся внутри блочных элементов и их ширина зависит лишь от содержимого и настроек CSS. Еще одно отличие строчного элемента от блочного заключается к том, что в нем может находиться только контент и другие строчные элементы. Блочные элементы в строчные вкладывать нельзя.
Примеры строчных элементов: <a>, <span>, <strong>, <em>, <img> и т. д.
7. Какие бывают значения display? Расскажите как ведёт себя каждое свойство?
display: block | inline | inline-block | inline-table |
inline-flex | flex | list-item | none |
run-in | table | table-caption | table-cell | table-column-group
| table-column |
table-footer-group | table-header-group | table-row | table-row-groupЭлемент показывается как блочный. Применение этого значения для строчных элементов, например span, заставляет его вести подобно блокам — происходит перенос строк в начале и в конце содержимого.
Элемент отображается как строчный. Использование блочных элементов, таких, как div и p, автоматически создаёт перенос и показывает их содержимое с новой строки. Значение inline отменяет эту особенность, поэтому содержимое блочных элементов начинается с того места, где окончился предыдущий элемент.
Это значение генерирует блочный элемент, который обтекается другими элементами веб-страницы подобно строчному элементу. Фактически такой элемент по своему действию похож на встраиваемые элементы (вроде img). При этом его внутренняя часть форматируется как блочный элемент, а сам элемент — как строчный.
Определяет, что элемент является таблицей, как при использовании table, но при этом таблица является строчным элементом и происходит её обтекание другими элементами, например, текстом.
Элемент ведёт себя как строчный и выкладывает содержимое согласно флекс-модели.
Элемент ведёт себя как блочный и выкладывает содержимое согласно флекс-модели.
Приведёт к тому, что элемент будет вести себя как элемент списка.
8. Какие бывают значения у свойства position? Расскажите как ведёт себя каждое свойство?
position: absolute | fixed | relative | static | sticky
Относительное позиционирование. Cмещает элемент относительно того места, где он должен находится в нормальном потоке. (top - смещение сверху на указанные единицы , right смещает относительно правой стороны "статичного" положения и тд и тп). Элемент остается в потоке.
Статичное позиционирование. Элементы отображаются как обычно. Использование свойств left, top, right и bottom не приводит к каким-либо результатам. Элемент остается в потоке.
Абсолютное позиционирование. Элемент удаляется из потока и позиционируется относительно ближайшего позиционированного предка (т.е. любого не статичного).Другие элементы отображаются на веб-странице словно абсолютно позиционированного элемента и нет.
Блочные элементы при АП начинают напоминать строчные. Они перестают занимать всю ширину, потому что больше не находятся в потоке. Занимают ширину только своего контента и отступов (внутренних и внешних).
Если указать left и right одновременно = 0. То блок растянется на всю рабочую ширину браузера. Так как должен находиться и в левой и в правой крайней точке.
Фиксированное позиционирование. По своему действию это значение близко к absolute, но в отличие от него привязывается к указанной свойствами left, top, right и bottom точке на экране и не меняет своего положения при прокрутке веб-страницы. Отличительной чертой фиксированного позиционирования является то, что элемент позиционируется относительно ТОЛЬКО (не дрегого позиционированного элемента) видимой области браузера.
Это сочетание относительного и фиксированного позиционирования. Элемент рассматривается как позиционированный относительно , пока он не пересекает определённый порог, после чего рассматривается как фиксированный.
Состоит из двух основных частей: «липкого» элемента и «липкого» контейнера. «Липкий» элемент — это элемент, которому мы задали position: sticky. Элемент будет становиться плавающим, как только область видимости достигнет определённой позиции, например top: 0px.
Пример: .some-component { position: sticky; top: 0px; }
«Липкий» контейнер — это HTML-элемент, который оборачивает «липкий» элемент. Это максимальная область, в которой может перемещаться наш элемент. Sticky header example
Не залипает если является единственным ребенком своего родителя. Без хотя бы top:0px; не залипает.
9. Расскажите про float?
Плавающий элемент (float : right; или float: left;) вынимается из потока и “прижимается” (“притягивается”, “плывёт”) к левой или правой стороне содержащего блока. Поток всё также идет от самого верха родительского элемента-контейнера, но ширина потока уменьшается на ширину плавающего элемента. После “плавающего” элемента поток опять займёт всю доступную ширину.
Если в блоке-контейнере нет элементов кроме плавающих (как в первом примере из предыдущего урока) его высота равна нулю, контейнер ведет себя как пустой, потому что плавающие элементы удалены из потока.
10. Какие существуют методы очистки floats и какие из них подходят для какого контекста ?
left, right, both
Использование правила clear: both для элемента, который необходимо «очистить».
Создание пустого блочного элемента. У него впоследствии указывается правило clear: both.
Свойство overflow со значением hidden. Само по себе свойство overflow управляет отображением содержимого блока (добавляет полосы прокрутки, обрезает не поместившийся контент), в добавок, значения auto, scroll или hidden отменяют свойство float. В данном случае текс уменьшится на ширину флоата по всей длинне.
11. Расскажите о различия padding и margin?
Это расстояние между границей текущего элемента веб-страницы и границами соседних элементов, либо родительского элемента. Размер расстояния регулируется свойством margin. Такой отступ находится вне элемента.
Это расстояние от воображаемой границы элемента до его содержимого. Величина расстояния задается с помощью параметра padding. Такой отступ принадлежит самому элементу и находится внутри него.
12. Как ведут себя margin у двух элементов по соседству?
Схлопывание не срабатывает:
для элементов, у которых на стороне схлопывания задано свойство padding.
для элементов, у которых на стороне схлопывания задана граница; на элементах с абсолютным позиционированием, т. е. таких, у которых position установлено как absolute;
на плавающих элементах (для них свойство float задано как left или right);
для строчных элементов;
для html.
13. Какие вы знаете псевдоэлементы? Где их используют??
Псевдоэлемент — это дополнение к селектору, с помощью которого можно стилизовать элемент, не определённый в структуре HTML документа. Добавляется он к селектору c помощью символов ::, т.е. так селектор::псевдоэлемент
Предназначены для создания псевдоэлемента внутри элемента перед/после его контента. По умолчанию данный псевдоэлемент имеет display: inline. Если псевдоэлементу before нужно установить другое отображение, то его нужно указать явно (например: display: block).
Содержимое данного псевдоэлемента задаётся с помощью CSS свойства content. При этом если псевдоэлемент будет без содержимого, то данное свойство всё равно необходимо указывать и использовать в качестве его значения пустую строку content: "". Без указания content псевдоэлемент отображаться не будет.
Псевдоэлемент before не наследует стили. Поэтому если необходимо чтобы у него были стили как у родительского элемента, то ему необходимо их явно прописывать.
Псевдоэлемент, с помощью которого задаётся стилевое оформление подсказывающего текста, созданного атрибутом
Применяет стиль к выделенному пользователем тексту. В правилах стилей допускается использовать следующие свойства: color, background и background-color.
14. Что такое БЭМ?
.block__element_mod
Блок — это независимый интерфейсный компонент. Блок может быть простым или составным (содержать другие блоки). При создании блока нужно обеспечивать возможность его использования в любом месте web-страницы, а также повторения в том же самом месте страницы (родительском элементе). Блок должен включать в себя всю реализацию, необходимую для представления части интерфейса, которую он выражает.
Элемент — это составная часть блока. Элементы контекстно-зависимы: они имеют смысл только в рамках своего блока. Элемент — не обязательная составляющая блока, небольшие блоки обходятся без элементов.
Модификатор — это свойство блока или элемента, задающее изменения в их внешнем виде или поведении. Модификатор может быть булевым (например, button_big) или парой ключ-значение (например, menu_type_bullet, menu_type_numbers). У блока или элемента может быть несколько модификаторов одновременно.
block-name__elem-name_mod-name_mod-valИмена записываются латиницей в нижнем регистре.
Для разделения слов в именах используется дефис (-).
Имя блока задает пространство имен для его элементов и модификаторов.
Имя элемента отделяется от имени блока двумя подчеркиваниями (__).
Имя модификатора отделяется от имени блока или элемента одним подчеркиванием (_).
Значение модификатора отделяется от имени модификатора одним подчеркиванием (_).
Значение булевых модификаторов в имени не указывается.
15. Что такое семантические элементы и для чего они нужны?
<article> Определяет статью
<aside> Определяет блок сбоку от основного контента
<details> Определяет дополнительную информацию, которую пользователь может открывать или закрывать
<figcaption> Определяет пояснение для элемента <figure>
<figure> Используется для группирования различных самодостаточных элементов - иллюстраций, диаграмм, фотографий, листингов кода и т.д.
<footer> Определяет "подвал" документа или раздела
<header> Определяет "шапку" документа или раздела
<main> Определяет основной контент документа
<mark> Определяет маркированный/подсвеченный текст
<nav> Определяет блок навигационных ссылок
<section> Определяет раздел в документе
<summary> Определяет видимый заголовок элемента <details>
<time> Определяет дату/время
В качестве примера не семантических элементов можно привести теги <'div> и <'span>. Они ничего не говорят о характере их контента.
Примеры семантических элементов: <'form>, <'table> и <'article>. Они четко описывают, какого характера контент они содержат.
Семантические элементы HTML5 поддерживаются всеми современными браузерами.
16. Что такое box-sizing: border-box?
17. Как реализовать кастомный check-box?
<body>
<input id="check" type="checkbox">
</body>
Основной элемент будет input, ниже его стили:
input {
width: 100px;
height: 100px;
background-color: black;
border-radius: 50%;
}
Что бы стилизовать внутренности checkbox, создается label:
<body>
<input id="check" type="checkbox">
<label for="check"></label>
</body>
Затем для label пишутся стили, которыми должен обладать сам checkbox (копируем из input):
input {
}
label {
width: 200px;
height: 200px;
background-color: black;
border-radius: 50%;
}
Сам input надо скрыть opacity:0;
input {
opacity: 0
}
label {
width: 200px;
height: 200px;
background-color: black;
border-radius: 50%;
}
Для отслеживания того нажат checkbox или нет воспользуемся псевдоэлементами
input:checked + label {
background-color: red;
}
Перед чекбоксом создается label и привязывается к инпуту. После чего инпут скрывается, а label стилизуется так, как необходимо
<div class="new">
<form>
<div class="form-group">
<input type="checkbox" id="html">
<label for="html">HTML</label>
</div>
<div class="form-group">
<input type="checkbox" id="css">
<label for="css">CSS</label>
</div>
<div class="form-group">
<input type="checkbox" id="javascript">
<label for="javascript">Javascript</label>
</div>
</form>
</div>
.new {
padding: 50px;
}
.form-group {
display: block;
margin-bottom: 15px;
}
.form-group input {
padding: 0;
height: initial;
width: initial;
margin-bottom: 0;
display: none;
cursor: pointer;
}
.form-group label {
position: relative;
cursor: pointer;
}
.form-group label:before {
content:'';
-webkit-appearance: none;
background-color: transparent;
border: 2px solid #0079bf;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05), inset 0px -15px 10px -12px rgba(0, 0, 0, 0.05);
padding: 10px;
display: inline-block;
position: relative;
vertical-align: middle;
cursor: pointer;
margin-right: 5px;
}
.form-group input:checked + label:after {
content: '';
display: block;
position: absolute;
top: 2px;
left: 9px;
width: 6px;
height: 14px;
border: solid #0079bf;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
18. Как отцентровать блок по горизонтали и вертикали?
Существует несколько способов, но самый простой из них - сделать родительский элемент display: flex; и задать два свойства:
align-items: center;
justify-content: center;
19. Расскажите о свойстве transition?
Transition позволяет определять переходное состояние между двумя состояниями элемента. Различные состояния могут быть определены с помощью псевдоклассов, таких как :hover или :active или установлены динамически с помощью JavaScript.
20. Чем border отличается от outline?
1.Outline не влияет на положение элемента и его размеры.
2. Outline не позволяет задать рамку с определённой стороны элемента (только сразу со всех).
3. На outline рамку не действует скругление углов, устанавливаемое с помощью свойства border-radius.
21. Что такое семантические элементы и для чего они нужны?
С html5 были введены так называемые семантические теги, которые определяют конкретные части страницы. Правильное использование их улучшает СЕО оптимизацию, что позволяет поисковым роботам лучше индексировать страницу. Добавляя семантические HTML теги на ваши страницы, вы даете дополнительную информацию, которая помогает поисковикам понимать роли и относительную важность разных частей ваших страниц.
22. Свойство overflow, что делает и когда использовать?
Свойство CSS overflow определяет, необходимо ли для переполненного блочного элемента содержимое обрезать, предоставить полосы прокрутки или просто отобразить.
23. Как убрать маркер у списка?
Чтобы скрыть отображение маркеров в списке применяется стилевое свойство list-style-type со значением none. Его следует добавить к селектору ul или li.
24.Что вы знаете о приоритете селекторов? Специфичность?
Специфичность селекторов (selector's specificity) определяет их приоритет в таблице стилей. Чем специфичнее селектор, тем выше его приоритет. Каждый селектор имеет вес.
Элемент – 1
Класс – 10
id – 100
inline style – 1000
!important – имеет наибольший вес.
25.Какие css свойства для браузера самые тяжелые?
Большое количество подключенных шрифтов, тени, анимации, прозрачность..
26. Какие бывают значения у background-size? Кратко о каждом?
27. Как повернуть блок на 45 градусов?
28. Как увеличить в размере при наведении элемент, не сдвигая соседние?
для этого необходимо воспользоваться свойством transform. CSS3-трансформации позволяют сдвигать, поворачивать и масштабировать элементы. Трансформации преобразовывают элемент, не затрагивая остальные элементы веб-страницы, т.е. другие элементы не сдвигаются относительно него.
29.Для чего рекомендуется использовать атрибуты data?
Хранения значений.
Создания всплывающих подсказок без применения скриптов.
Определения стиля элемента на основе значения атрибута.
Получения и изменения значений через скрипты.
JavaScript:
1. Что такое функциональное программирование и какие особенности JS позволяют говорить о нем как о функциональном языке программирования? (L)
Парадигма программирования это подход к конструированию программного обеспечения, основанный на нескольких определяющих принципах. Функциональное программирование это одна из таких парадигм, которая состоит из чистых функций (Pure Function), и позволяет избежать разделяемого состояния (Shared State), изменчивых данных (Mutable Data) и побочных эффектов (Side effect). Функциональное программирование является больше декларативным, чем императивным. Cостояние приложения в функциональном программировании, в отличие от объектно-ориентированного программирования, протекает через чистые функции.
Код в функциональном программировании имеет тенденцию быть более кратким, более предсказуемым и более простым для тестирования, чем императивный код, но он может быть сложным для обучения.
Первое фундаментальное понятие это чистые функции. Чистые функции задействованы в надежном параллелизме (concurrency), React/Redux приложениях и функциональном программировании. Рассмотрим, что делает функцию «чистой»:
- чистая функция всегда возвращает одинаковое значение при одинаковых входах;
- чистая функция не имеет побочных эффектов.
Чистые функции полностью независимы от внешнего состояния и, следовательно, невосприимчивы ко многим ошибкам, связанным с разделяемым и изменчивым состоянием. Благодаря этому, чистые функции чрезвычайно легко перемещать. С ними легко делать рефакторинг и реорганизацию кода, что адаптирует программы к будущим изменениям.
Композиция функций — это способ объединения чистых функций для создания более сложных. Подобно обычной композиции функций в математике, результат одной функции передается в качестве аргумента следующей, а результат последней является результатом целого.
Каррированные функции — это функции, которые принимают несколько параметров, только по одному за раз (имеют целую армию из одного параметра). Их можно создать в JavaScript с помощью функций высокого порядка.
Разделяемое состояние это любая переменная, объект или пространство памяти, которые существуют в общей области (shared scope), или свойство объекта, передаваемое между областями. Проблема с разделяемым состоянием состоит в том, что нужно знать всю историю каждой общей переменной, которую функция использует или затрагивает, чтобы понять результат вызова функции. Вторая проблема, связанная с общим состоянием, заключается в том, что изменение порядка вызова функций может вызвать ряд сбоев.
Функциональное программирование избегает разделяемое состояние, используя структуры неизменных данных и чистые вычисления для получения новых данных из уже существующих.
Неизменный объект (immutable object) это объект, который нельзя изменить после его создания. Если вы хотите изменить неизменяемый объект, лучше всего создать новый объект с новым значением. Неизменность это ключевое понятие функционального программирования. Без него теряется история состояний, и ошибки могут проникнуть в вашем программное обеспечение.
Побочный эффект (Side Effect) это любое изменение состояния приложения, которое наблюдается за пределами вызываемой функции, кроме ее возвращаемого значения. Примеры побочных эффектов:
- изменение любой внешней переменной или свойства объекта;
- вход в консоль;
- запись в файл.
Функциональное программирование позволяет избежать побочных эффектов, что облегчает расширение, рефакторинг, отладку, тестирование и обслуживание программы.
Функциональное программирование предпочитает чистые функции вместо разделяемого состояния и побочных эффектов, неизменность вместо изменяемых данных и композицию функций вместо обязательного управления потоком. Функциональное программирование часто легче понять, потому что оно не меняет состояние и зависит только от предусмотренного ввода.
Объект Array содержит методы map, filter и reduce, которые являются самыми известными функциями в мире функционального программирования из-за их полезности, а также потому, что они не изменяют массив, что делает эти функции «чистыми». Также в JS имеются замыкание и функции высшего порядка, которые являются характеристиками функционального языка программирования.
2. Hoisting?
Это механизм перемещения объявления переменных и функций в верхнюю часть области видимости функции (или глобальной области видимости, если она находится вне какой-либо функции).
Hoisting влияет на жизненный цикл переменной, который состоит из следующих 3 шагов:
- Declaration - создать новую переменную. Например. переменная. myValue
- Initialization - инициализировать переменную значением. myValue = 150
- Usage - получить доступ и использовать значение переменной. alert(myValue)
// Declare
var strNumber;
// Initialize
strNumber = '16';
// Use
parseInt(strNumber); // => 16
JavaScript не следует строго этой последовательности и предлагает большую гибкость. Например, перед объявлением можно использовать функции: use -> declare.
Это происходит потому, что объявление функции в JavaScript поднимается наверх области видимости.
Hoisting действует по-разному:
- объявления переменных: использование ключевых слов var, let или const
- объявления функций: использование синтаксиса function name () {...}
- объявления класса: использование ключевого слова класса
Разница между объявлениями var / function и объявлениями let / const / class заключается в инициализации. Первые инициализируются с неопределенным значением undefined. Однако, вторые, лексически объявленные переменные, остаются не инициализированными. Это означает, что ReferenceError выбрасывается при попытке доступа к ним. Они будут инициализированы только после того, как операторы let / const / class будут определены. Всё что до, называется временной мертвой зоной.
Временная мертвая зона - это не синтаксическое местоположение, а время между созданием переменной(DECLARE) и инициализацией. Ссылка на переменную в коде над объявлением не является ошибкой, если этот код не выполняется (например, тело функции или просто мертвый код), но ошибка будет выдана, если мы запросим доступ к переменной до её инициализации.
Var:
Переменные, объявленные с помощью var, поднимаются в верхнюю часть области действия функции. Если к переменной обращаются до объявления, она оценивается как undefined.
Когда переменная поднимается, объявление перемещается вверх, но начальное присвоение значения остается на месте:
// Declare num variable
var num;
console.log(num); // => undefined
// Declare and initialize str variable
var str = 'Hello World!';
console.log(str); // => 'Hello World!'
С объявлениями var невозможно отличить объявленную переменную от инициализированной переменной, потому что объявления var автоматически инициализируются в начале области видимости, поэтому они доступны для использования во всей области видимости. Вы не можете наблюдать uninitialized объявление var.
Синтаксис var позволяет не только объявлять, но и сразу присваивать начальное значение: var str = 'начальное значение'. Когда переменная поднимается, объявление перемещается вверх, но начальное присвоение значения остается на месте:
function sum(a, b) {
console.log(myString); // => undefined
var myString = 'Hello World';
console.log(myString); // => 'Hello World'
return a + b;
}
sum(16, 10); // => 26
Let/Const:
function isTruthy(value) {
var myVariable = 'Value 1';
if (value) {
/**
* temporal dead zone for myVariable
*/
// Throws ReferenceError: myVariable is not defined
console.log(myVariable);
let myVariable = 'Value 2';
// end of temporary dead zone for myVariable
console.log(myVariable); // => 'Value 2'
return true;
}
return false;
}
isTruthy(1); // => true
Возникает интересный вопрос: действительно ли myVariable поднята до начала блока, или может быть просто не определена во временной мертвой зоне (до объявления)? Исключение ReferenceError выдается также, когда переменная вообще не определена.
Если вы посмотрите на начало функционального блока, var myVariable = 'Value 1' объявляет переменную для всей области действия функции. В блоке if (value) {...}, если бы переменные let не покрывали переменные внешней области видимости, то во временной мертвой зоне myVariable имело бы значение 'Value 1', чего не происходит. Таким образом, блочные переменные грубо подняты.
В точном описании, когда движок встречает блок с оператором let, сначала переменная объявляется в верхней части блока. В объявленном состоянии переменная по-прежнему не может использоваться, но она покрывает переменную из внешней области видимости с тем же именем. Позже, когда передается строка let myVar, переменная находится в инициализированном состоянии и может использоваться.
расширение let во всем блоке защищает переменные от модификации внешними областями видимости даже до объявления. Генерация ошибок ссылок при доступе к переменным let во временной мертвой зоне обеспечивает лучшую практику кодирования: сначала объявить, а затем использовать.
Функция:
Подъем в объявлении функции позволяет использовать функцию в любом месте включающей области видимости, даже до объявления. Другими словами, функция может быть вызвана из любого места текущей или внутренней области видимости (без неопределенных значений, временных мертвых зон или ошибок ссылок).
Обратите внимание на разницу между объявлением функции function name() {...} и выражением функции var name = function() {...}. Оба используются для создания функций, однако имеют разные механизмы подъема. Следующий пример демонстрирует различие (TypeError: substraction is not a function):
// Call the hoisted function
addition(4, 7); // => 11
// The variable is hoisted, but is undefined
substraction(10, 7); // TypeError: substraction is not a function
// Function declaration
function addition(num1, num2) {
return num1 + num2;
}
// Function expression
var substraction = function (num1, num2) {
return num1 - num2;
};
Класс:
Переменные класса регистрируются в начале области блока. Но если вы попытаетесь получить доступ к классу до определения, JavaScript выдаст ReferenceError: имя не определено. Таким образом, правильный подход заключается в том, чтобы сначала объявить класс, а затем использовать его для создания экземпляров объектов.
Hoisting в объявлениях классов похож на переменные, объявленные с помощью оператора let.
3. Что такое область видимости (Scope)?
Область видимости — это место, где (или откуда) мы имеем доступ к переменным или функциям. JS имеем три типа областей видимости: глобальная, функциональная и блочная (ES6).Глобальная область видимости — переменные и функции, объявленные в глобальном пространстве имен, имеют глобальную область видимости и доступны из любого места в коде.
// глобальное пространство имен
var g = 'global'
function globalFunc() {
function innerFunc() {
console.log(g) // имеет доступ к переменной g, поскольку она является глобальной
}
innerFunc()
}
Функциональная область видимости (область видимости функции) — переменные, функции и параметры, объявленные внутри функции, доступны только внутри этой функции.
function myFavouriteFunc(a) {
if (true) {
var b = 'Hello ' + a
}
return b
}
myFavouriteFunc('World')
console.log(a) // Uncaught ReferenceError: a is not defined
console.log(b) // не выполнится
Блочная область видимости — переменные (объявленные с помощью ключевых слов «let» и «const») внутри блока ({ }), доступны только внутри него. Блок кода не создает область для переменных var, в отличие от тела функции.
function testBlock() {
if (true) {
let z = 5
}
return z
}
testBlock() // Uncaught ReferenceError: z is not defined
Модульная область видимости - ES2015 также создает область видимости для переменных, функций, классов.
// "circle" module scope
const pi = 3.14159;
console.log(pi); // 3.14159
// Usage of pi
Переменная pi объявлена в рамках модуля Circle. Также переменная pi не экспортируется из модуля.
Затем импортируется модуль circle:
import './circle';
console.log(pi); // throws ReferenceError
Лексическая область видимости -состоит из внешних областей видимости, определенных статически.
function outerFunc() {
// the outer scope
let outerVar = 'I am from outside!';
function innerFunc() {
// the inner scope
console.log(outerVar); // 'I am from outside!'
}
return innerFunc;
}
const inner = outerFunc();
inner();
Посмотрите на последнюю строку фрагмента inner(): вызов innerFunc() происходит за пределами области externalFunc(). Тем не менее, как JavaScript понимает, что значение externalVar внутри innerFunc() соответствует переменной externalVar функции externalFunc()?
Ответ: благодаря лексической области.
Лексическая область видимости означает, что доступность переменных определяется статически положением переменных во вложенных областях действия функции: внутренняя область действия функции может получить доступ к переменным из области действия внешней функции.
В примере лексическая область видимости innerFunc() состоит из области видимости externalFunc().
Кроме того, innerFunc() является замыканием, поскольку захватывает переменную externalVar из лексической области видимости.
Область видимости — это также набор правил, по которым осуществляется поиск переменной. Если переменной не существует в текущей области видимости, ее поиск производится выше, во внешней по отношению к текущей области видимости. Если и во внешней области видимости переменная отсутствует, ее поиск продолжается вплоть до глобальной области видимости. Если в глобальной области видимости переменная обнаружена, поиск прекращается, если нет — выбрасывается исключение. Поиск осуществляется по ближайшим к текущей областям видимости и останавливается с нахождением переменной. Это называется цепочкой областей видимости (Scope Chain).
// цепочка областей видимости
// внутренняя область видимости -> внешняя область видимости -> глобальная область видимости
// глобальная область видимости
var variable1 = 'Comrades'
var variable2 = 'Sayonara'
function outer() {
// внешняя область видимости
var variable1 = 'World'
function inner() {
// внутренняя область видимости
var variable2 = 'Hello'
console.log(variable2 + ' ' + variable1)
}
inner()
}
outer()
// в консоль выводится 'Hello World',
// потому что variable2 = 'Hello' и variable1 = 'World' являются ближайшими
// к внутренней области видимости переменными
4. var VS let VS const?
Var выходит за пределы блоков if, for и подобных. Это происходит потому, что на заре развития JavaScript блоки кода не имели лексического окружения. Поэтому можно сказать, что var – это пережиток прошлого. Но var не может выйти за пределы блока функции.
Если в блоке кода дважды объявить одну и ту же переменную let, будет ошибка:
let user;
let user; // SyntaxError: 'user' has already been declared
Используя var, можно переобъявлять переменную сколько угодно раз. Повторные var игнорируются:
var user = "Pete";
var user; // ничего не делает, переменная объявлена раньше
// ...нет ошибки
alert(user); // Pete
Если дополнительно присвоить значение, то переменная примет новое значение:
var user = "Pete";
var user = "John";
alert(user); // John
Объявления переменных var обрабатываются в начале выполнения функции (или запуска скрипта, если переменная является глобальной).
Другими словами, переменные var считаются объявленными с самого начала исполнения функции вне зависимости от того, в каком месте функции реально находятся их объявления (при условии, что они не находятся во вложенной функции).
function sayHi() {
phrase = "Привет";
alert(phrase);
var phrase;
}
sayHi();
Поскольку все объявления переменных var обрабатываются в начале функции, мы можем ссылаться на них в любом месте. Однако, переменные имеют значение undefined до строки с присвоением значения.
Существует 2 основных отличия var от let/const:
Переменные var не имеют блочной области видимости, они ограничены, как минимум, телом функции. Объявления (инициализация) переменных var производится в начале исполнения функции (или скрипта для глобальных переменных).
Эти особенности, как правило, не очень хорошо влияют на код. Блочная область видимости – это удобно. Поэтому много лет назад let и const были введены в стандарт и сейчас являются основным способом объявления переменных.
5. Какие типы данных существуют в JavaScript?
Все типы данных в JavaScript, кроме объектов, являются иммутабельными (значения не могут быть модифицированы, а только перезаписаны новым полным значением). Например, в отличии от C, где строку можно посимвольно корректировать, в JavaScript строки пересоздаются только полностью. Значения таких типов называются «примитивными значениями».
- «number» - - Единый тип число используется как для целых, так и для дробных чисел. Существуют специальные числовые значения Infinity (бесконечность) и NaN (ошибка вычислений). Например, бесконечность Infinity получается при делении на ноль. Ошибка вычислений NaN будет результатом некорректной математической операции. Значения ограничены диапазоном ±(253-1).
- «bigint» - это специальный числовой тип, который предоставляет возможность работать с целыми числами произвольной длины.
- «string» - Строка может содержать ноль или больше символов, нет отдельного символьного типа.
- «boolean» - Булевый (логический) тип «boolean»
- «undefined» - Специальное значение Значение undefined, как и null, образует свой собственный тип, состоящий из одного этого значения. Оно имеет смысл «значение не присвоено». Если переменная объявлена, но в неё ничего не записано, то её значение как раз и есть undefined.
- «symbol» - для уникальных идентификаторов. Символы создаются вызовом функции Symbol(), в которую можно передать описание (имя) символа. Варианты использования: 1) «Скрытые» свойства объектов (Так что, используя символьные свойства, мы можем спрятать что-то нужное нам, но что другие видеть не должны), 2) Существует множество системных символов, используемых внутри JavaScript, доступных как Symbol.*. Мы можем использовать их, чтобы изменять встроенное поведение ряда объектов. Например, в дальнейших главах мы будем использовать Symbol.iterator для итераторов, Symbol.toPrimitive для настройки преобразования объектов в примитивы и так далее.
Символ — это уникальное примитивное значение. Если подходить к символам с этой позиции, то можно заметить, что символы в этом плане похожи на объекты, так как создание нескольких экземпляров символов приведёт к созданию разных значений. Но символы, кроме того, являются иммутабельными примитивными значениями.
Symbol не попадает в Object.keys.
- «object» - Первые 6 типов называют «примитивными». Особняком стоит шестой тип: «объекты». Он используется для коллекций данных и для объявления более сложных сущностей. Объявляются объекты при помощи фигурных скобок {...}
- «null» - Специальное значение В JavaScript null не является «ссылкой на несуществующий объект» или «нулевым указателем», как в некоторых других языках. Это просто специальное значение, которое имеет смысл «ничего» или «значение неизвестно». Специальный примитив, используемый не только для данных но и в качестве указателя на финальную точку в Цепочке Прототипов;
Оператор typeof позволяет нам увидеть, какой тип данных сохранён в переменной Имеет две формы: typeof x или typeof(x). Возвращает строку с именем типа. Например, "string". Для null возвращается "object" – это ошибка в языке, на самом деле это не объект.
6. Расскажите про преобразование типов в JavaScript?
Существует 3 наиболее широко используемых преобразования: строковое, численное и логическое.
Строковое – Происходит, когда нам нужно что-то вывести. Может быть вызвано с помощью String(value). Для примитивных значений работает очевидным образом.
Численное – Происходит в математических операциях. Может быть вызвано с помощью Number(value).
Преобразование подчиняется правилам:
| Значение | Становится… |
|---|---|
| undefined | NaN |
| null | 0 |
| true / false | 1 / 0 |
| string | Пробельные символы по краям обрезаются. Далее, если остаётся пустая строка, то получаем 0, иначе из непустой строки «считывается» число. При ошибке результат NaN. |
Логическое – Происходит в логических операциях. Может быть вызвано с помощью Boolean(value).|
Подчиняется правилам:
| Значение | Становится… |
|---|---|
| 0, null, undefined, NaN, "" | false |
| любое другое значение | true |
Большую часть из этих правил легко понять и запомнить. Особые случаи, в которых часто допускаются ошибки:
undefined при численном преобразовании становится NaN, не 0.
"0" и строки из одних пробелов типа " " при логическом преобразовании всегда true.
7. В чем разница между явным и неявным преобразованием или приведением к типу (Implicit and Explicit Coercion)?
Неявное преобразование — это способ приведения значения к другому типу без нашего ведома (участия).
Предположим, у нас есть следующее:
console.log(1 + '6')
console.log(false + true)
console.log(6 * '2')
Результатом первого console.log будет 16. В других языках это привело бы к ошибке, но в JS 1 конвертируется в строку и конкатенируется (присоединяется) c 6. Мы ничего не делали, преобразование произошло автоматически.
Результатом второго console.log будет 1. False было преобразовано в 0, true — в 1. 0 + 1 = 1.
Результатом третьего console.log будет 12. Строка 2 была преобразована в число перед умножением на 6.
Явное преобразование предполагает наше участие в приведении значения к другому типу:
console.log(1 + parseInt('6'))
В этом примере мы используем parseInt для приведения строки 6 к числу, затем складываем два числа и получаем 7.
8. Какие значения в JS являются ложными?
const falsyValues = ['', 0, null, undefined, NaN, false]
Ложными являются значения, результатом преобразования которых в логическое значение является false. Как проверить, является ли значение ложным?
Следует использовать функцию Boolean или оператор "!!" (двойное отрицание).
9. В чем разница между undefined и is not defined?
undefined - это значение, присваемое объявленной, но не проинициализированной переменной. Мы получаем undefined, обращаясь к существующей переменной. А в случае обращения к несуществующей (необъявленной) переменной, мы получим ошибку is not defined.10. Что такое this?
This это контекст вызова функции. В JavaScript 4 типа вызова функции:
- function invocation: alert('Hello World!') (Optional: with strict mode)
- method invocation: console.log('Hello World!')
- constructor invocation: new RegExp('\\d')
- indirect invocation: alert.call(undefined, 'Hello World!')/ .apply
Отдельно от них можно отметить:
- bound function invocation: multiply.bind(2)
- arrow function invocation: () => {}
This это глобальная область в function invocation. Глобальный обьект определяется средой исполнения. В браузере глобальный обьект это window.
function sum(a, b) {
console.log(this === window); // => true
this.myNumber = 20; // add 'myNumber' property to global object
return a + b;
}
// sum() is invoked as a function
// this in sum() is a global object (window)
sum(15, 16); // => 31
window.myNumber; // => 20
This = undefined при вызове функции в строгом режиме
Обычная ловушка с вызовом функции состоит в том, что вы думаете, что this то же самое во внутренней функции, что и во внешней функции.
Контекст внутренней функции (кроме стрелочной функции) зависит только от ее собственного типа вызова, но не от контекста внешней функции.
Ошибка:const numbers = {
numberA: 5,
numberB: 10,
sum: function() {
console.log(this === numbers); // => true
function calculate() {
// this is window or undefined in strict mode
console.log(this === numbers); // => false
return this.numberA + this.numberB;
}
return calculate();
}
};
numbers.sum(); // => NaN or throws TypeError in strict mode
Корректно:
const numbers = {
numberA: 5,
numberB: 10,
sum: function() {
console.log(this === numbers); // => true
const calculate = () => {
console.log(this === numbers); // => true
return this.numberA + this.numberB;
}
return calculate();
}
};
numbers.sum(); // => 15
This это объект, которому принадлежит метод в вызове метода
В синтаксисе класса ECMAScript 2015 контекст вызова метода также является самим экземпляром
class Planet {
constructor(name) {
this.name = name;
}
getName() {
console.log(this === earth); // => true
return this.name;
}
}
const earth = new Planet('Earth');
// method invocation. the context is earth
earth.getName(); // => 'Earth'
Разделение метода и обьекта: Метод можно извлечь из объекта в отдельную переменную const alone = myObj.myMethod. Когда метод вызывается только один alone() , отделенный от исходного объекта, вы можете подумать, что this объект myObject, для которого был определен метод.
Правильно сказать, если метод вызывается без объекта, то происходит вызов функции, где this глобальный объект window или undefined в строгом режиме
function Pet(type, legs) {
this.type = type;
this.legs = legs;
this.logInfo = function() {
console.log(this === myCat); // => false
console.log(`The ${this.type} has ${this.legs} legs`);
}
}
const myCat = new Pet('Cat', 4);
// logs "The undefined has undefined legs"
// or throws a TypeError in strict mode
setTimeout(myCat.logInfo, 1000);
Метод отделяется от своего объекта при передаче в качестве параметра: setTimeout(my Cat.logInfo). Следующие случаи эквивалентны:
setTimout(myCat.logInfo);
// is equivalent to:
const extractedLogInfo = myCat.logInfo;
setTimout(extractedLogInfo);
Когда разделенный logInfo вызывается как функция, this глобальный объект или неопределенный в строгом режиме (но не объект myCat). Таким образом, информация об объекте записывается неправильно.
👍 Функция связывается с объектом с помощью метода .bind() Если отдельный метод связан с объектом myCat, проблема контекста решается:
// Create a bound function
const boundLogInfo = myCat.logInfo.bind(myCat);
// logs "The Cat has 4 legs"
setTimeout(boundLogInfo, 1000);
This это вновь созданный объект в вызове конструктора
Контекст вызова конструктора — это вновь созданный объект. Конструктор инициализирует объект данными, поступающими из аргументов конструктора, устанавливает начальные значения свойств, прикрепляет обработчики событий и т. д.
Забыть про слово new. Использование вызова функции для создания объектов является потенциальной проблемой, поскольку некоторые конструкторы могут опускать логику для инициализации объекта, когда отсутствует ключевое слово new.
function Vehicle(type, wheelsCount) {
this.type = type;
this.wheelsCount = wheelsCount;
return this;
}
// Function invocation
const car = Vehicle('Car', 4);
car.type; // => 'Car'
car.wheelsCount // => 4
car === window // => true
Вы можете подумать, что это хорошо работает для создания и инициализации новых объектов.
Однако this это объект window в вызове функции, поэтому Vehicle('Car', 4) задает свойства объекта window. Это ошибка. Новый объект не создается.
Indirect вызов выполняется, когда функция вызывается с использованием методов myFun.call() или myFun.apply().
This это первый аргумент .call() или .apply() в косвенном вызове
Функции в JavaScript являются объектами первого класса, что означает, что функция является объектом. Тип функционального объекта — Function.
Из списка методов, которые есть у функционального объекта, .call() и .apply() используются для вызова функции с настраиваемым контекстом.
Bound function это функция чей контекст и/или аргументы привязаны к определенным значениям. Связанная функция создается с помощью метода .bind(). Исходная и связанная функции имеют один и тот же код и область действия, но разные контексты и аргументы при выполнении.
function multiply(number) {
'use strict';
return this * number;
}
// create a bound function with context
const double = multiply.bind(2);
// invoke the bound function
double(3); // => 6
double(10); // => 20
This это первый аргумент функции .bind()
.bind() создает постоянную контекстную ссылку и всегда будет ее сохранять. Связанная функция не может изменить свой связанный контекст при использовании .call() или .apply() с другим контекстом, или даже bind() не имеет никакого эффекта.
Стрелочная функция предназначена для объявления функции в более короткой форме и лексической привязки контекста.
This это билжайший контекст в том месте где определена стрелочная функция.
Стрелочная функция не создает свой собственный контекст выполнения, а берет его из внешней функции, в которой она определена. Другими словами, стрелочная функция разрешает это лексически.
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
log() {
console.log(this === myPoint); // => true
setTimeout(() => {
console.log(this === myPoint); // => true
console.log(this.x + ':' + this.y); // => '95:165'
}, 1000);
}
}
const myPoint = new Point(95, 165);
myPoint.log();
Стрелочная функция связана с лексикой this раз и навсегда. This нельзя изменить даже при использовании методов модификации контекста.
Стрелочная функция распространенные ошибки:
Возможно, вы захотите использовать стрелочные функции для объявления методов объекта. Достаточно справедливо: их объявление довольно короткое по сравнению с функциональным выражением: (param) => {...} вместо function(param) {..}.
function Period (hours, minutes) {
this.hours = hours;
this.minutes = minutes;
}
Period.prototype.format = () => {
console.log(this === window); // => true
return this.hours + ' hours and ' + this.minutes + ' minutes';
};
const walkPeriod = new Period(2, 30);
walkPeriod.format(); // => 'undefined hours and undefined minutes'
Поскольку format является стрелочной функцией и определяется в глобальном контексте (самая верхняя область видимости), он имеет this как объект window.
Даже если format выполняется как метод объекта walkPeriod.format(), window сохраняется как контекст вызова. Это происходит потому, что стрелочная функция имеет статический контекст, который не меняется при различных типах вызовов.
Function expression решает проблему, потому что обычная функция меняет свой контекст в зависимости от вызова
Итог:
Поскольку наибольшее влияние на this оказывает вызов функции, впредь не спрашивайте себя:
Откуда берется this?
но спросите себя:
Как `функция вызывается?
Для стрелочной функции задайте вопрос?
Чем является this внутри внешней функции, в которой определена стрелочная функция?
11. Опишите разницу между == и === в JS ?
Оба оператора сравнения проверяют тождественность. Различие заключается в том, что двойное равно при сравнении значений неявно приводит (преобразует) типы значений к единому, так строка “1” и цифра 1 при таком сравнении будут равны. Тройное равно не выполняет никаких неявных трансформаций, а значит исходные типы будут иметь значения. Таким образом строка не будет равна числу и не важно что в обоих операндах фигурирует единица.
Строки сравниваются посимвольно в лексикографическом порядке. Значения разных типов при сравнении приводятся к числу.
Исключением является сравнение с помощью операторов строгого равенства/неравенства.
Значения null и undefined равны == друг другу и не равны любому другому значению. (при === они не равны)
Будьте осторожны при использовании операторов сравнений вроде > и < с переменными, которые могут принимать значения null/undefined. Хорошей идеей будет сделать отдельную проверку на null/undefined.
alert( null >= 0 ); // (3) true
console.log(1550 >= null) // true
console.log(1550 >= undefined) // false
console.log(2 <= null) // false
console.log(null <= 2) //true
12. Что такое цикл событий (event loop) и как он работает?
Event loop - это бесконечный цикл в котором движок JavaScript ожидает задачи и исполняет их.
Код, написанный на Javascript, выполняется синхронно – одна команда в один момент времени. Код работает в браузере, в котором множество процессов работают параллельно и для взаимодействия с этими процессами придумали следующее:
- Для каждой html страницы в браузере Javascript выполняется в своем отдельном потоке (Main Thread).
- Содержимое тега <script>…</script> или содержимое файла переданного в свойстве тега <script src=”file.js”>…</script> начнёт исполняться в процессе загрузки документа в браузер. Причём подгрузиться он может раньше, чем HTML-элементы
- Для того чтобы взаимодействовать с Web формой в HTML можно назначить функцию обработки на какое-либо событие: В HTML: onclick = ‘myFunc()’ В Javascript: element.addEventListener("click", myFunc(), false);
- Для взаимодействия с другими процессами браузера существует набор интерфейсов Web API. При вызове функций этих интерфейсов нужно обязательно в качестве параметра передать функцию обратного вызова (callback function), которой будет передано управление после того, как Web API функция отработает. Эти интерфейсы не ограниченны одним потоком и могут работать параллельно. Кстати, setTimeout() так же является частью Web API.
Далее при наступлении события или по окончанию работы функции Web API, функции обработки события и функции обратного вызова попадают в очередь - Event Queue. Откуда их извлекает и передаёт на исполнение Event Loop.
Main Thread - основной поток, где браузер выполняет JS.
На основе этой схемы строится работа всего Event Loop.
Call Stack (На исполнение): это место, где выполняется ваш код (ваши функции загружаются и выполняются, движок V8 в Chrome и NodeJS). Это обычеый LIFO stack(last-in-first-out). Когда он пуст, тоесть выполнил все текущие задачи Tick, он становится готовым принять следующий Tick из цикла событий
Browsers APIs: связь между вашим кодом и внутренними компонентами браузера для планирования задач, взаимодействия с DOM и многого другого (setTimeout, AJAX, createElement, querySelector, append, click, etc.) В случае обратных вызовов они добавят ваш код обратного вызова в Event queue событий, вместо этого, в случае then (метод промиса), ваш then-код будет добавлен в Job queue.
Event queue (Task queue, Callback Queue, Macrotask queue): каждый раз, когда вы добавляете обратный вызов (например, через setTimeout или API AJAX), он добавляется в эту очередь
Job queue (Microtask queue): его очередь зарезервирована для promise then (Promise.prototype.then() и Promise.prototype.catch(), а так же код который выполнится внутри async функций после выполнения таски с ключевым словом await), это очередь с приоритетом, ее смысл примерно такой: «выполнить этот код позже (= асинхронно), но как можно скорее!» (= до следующего тика Event Loop)», и поэтому браузеры ввели эту новую очередь для выполнения спецификаций промисов. Микротаски выполняются сразу после завершения Таски, c которой они связанны. Таким образом это не совсем отдельная очередь. Просто это некий довесок к таске, который должен выполнится сразу после неё. Сюда так же входит код, который выполниться сразу после await, в функции с ключевым словом async.
Next Tick: это то, что будет выполняться дальше, в основном это состоит из ОДНОГО обратного вызова из Event queue, ПОЛНОЙ Job queue (этот момент важен, текущий тик завершится только после того, как очередь заданий будет пуста, поэтому вы можете непреднамеренно заблокировать его выполнение). до следующего тика, если вы постоянно добавляете новые задания в эту очередь), может выполняться повторный рендеринг (выполните необходимые шаги в очереди рендеринга для обновления экрана)
Render Queue: которая отслеживает все изменения DOM модели. На данный момент раз в 16.6 мс. (при 60FPS) происходит перерисовка Web страницы и обновляются все связанные с ней элементы DOM. Задачи от Render оптимизируются браузером и, если он посчитает, что в этом цикле не нужно ничего перерисовывать, то Event Loop просто пойдет дальше.
Event Loop работает с очередями в следующем приоритете:
- Render Queue
- Task Queue (или Callback Queue, Macrotask Queue, Event Queue)
- После каждой выполненной Таски выполняются связанные с ней Микротаски
- 1. Выбирается самая старая таска (task A) в Event queue
- 2. Если task A = null (т е очередь из тасок пуста), переходим на шаг 6
- 3. set "текущая запущенная таска" для task A
- 4. run task A (значит запуск ее callback функции)
- 5. set "текущая запущенная таска" = null , удалить task A
- 6. Выполнение очереди job queue
- a. Выбирается самая старая таска (task x) в Job queue
- b. Если task x = null (т е очередь из тасок пуста), переходим на шаг g
- c. set "текущая запущенная таска" для task x
- d. run task x
- e. set "текущая запущенная таска" = null , удалить task x
- f. select "следующая самая старая таска" в очереди job queue, переход на шан b
- g. окончание работы с job queue
macrotasks: setTimeout, setInterval, setImmediate, requestAnimationFrame, I/O, UI rendering microtasks: process.nextTick, Promises, queueMicrotask, MutationObserver
Код задачи:
console.log(1)
setTimeout( () => {
console.log(2)
})
const promise1 = new Promise( resolve => {
console.log(3)
resolve(4)
})
const promise2 = new Promise( resolve => {
console.log(5)
resolve(6)
})
promise1.then(console.log)
promise2.then(console.log)
console.log(7)
Решение: Алгоритм решения. Делим код на "Основной поток кода", "Очередь микрозадач" и "Очередь макрозадач".
| Main | Micro | Macro |
|---|---|---|
| 1 | 4 | 2 |
| 3 | 6 | |
| 5 | ||
| 7 |
Сначала исполняется основной код. Потом очередь Micro, потом очередь Macro.
13. Что такое web workers, service workers и worklets? В чем разница?
Веб-воркеры, сервис-воркеры и ворклеты все это скрипты которые выполняются в отдельном потоке.
Веб-воркеры являются воркерами общего типа. В отличие от сервис-воркеров и ворклетов как мы увидим ниже, они не имеют конкретных вариантов использования, кроме возможности выполнения в отдельном потоке. Как результат веб-воркеры можно использовать для выгрузки почти любой тяжелой работы из основного потока.
Данные отправляются между потоком воркера и основным потоком через систему сообщений - обе стороны отправляют свои сообщения с помощью метода postMessage () и отвечают на сообщения через обработчик события onmessage (сообщение содержится в атрибуте данных события Message. Данные копируются, а не используются совместно.
/* main.js */
const myWorker = new Worker('worker.js');
// Send message to worker
myWorker.postMessage('Hello!');
// Receive message from worker
myWorker.onmessage = function(e) {
console.log(e.data);
}
/* worker.js */
// Receive message from main file
self.onmessage = function(e) {
console.log(e.data);
// Send message to main file
self.postMessage(workerResult);
}
Хороший пример веб-приложение обработки изображений, Squoosh, который использует веб-воркеры для обработки задач по преобразованию изображений, оставляя основной поток для взаимодействия пользователя с приложением.
Сервис-воркеры — это тип воркеров, которые служат определенной цели, быть прокси между браузером и сетью и/или кэшем. Это прокси между браузером и сетью. Перехватывая запросы, сделанные документом, сервис-воркеры могут перенаправлять запросы, позволяя работать автономно.
Service worker — это сценарий, который ваш браузер запускает в фоновом режиме отдельно от веб-страницы, открывая доступ к функциям, которым не требуется веб-страница или взаимодействие с пользователем. Сегодня они уже включают такие функции, как push-уведомления и фоновая синхронизация, а также могут перехватывать и обрабатывать сетевые запросы, включая программное управление кешем ответов.
/* main.js */
navigator.serviceWorker.register('/service-worker.js');
/* service-worker.js */
// Install
self.addEventListener('install', function(event) {
// ...
});
// Activate
self.addEventListener('activate', function(event) {
// ...
});
// Listen for network requests from the main document
self.addEventListener('fetch', function(event) {
// ...
});
После перехвата сервис-воркеры могут, например, ответить, вернув документ из кэша вместо того чтобы идти за ним в сеть, тем самым позволяя веб-приложению работать автономно!
/* service-worker.js */
self.addEventListener('fetch', function(event) {
// Return data from cache
event.respondWith(
caches.match(event.request);
);
});
Ворклеты — это очень легкие и специфичные воркеры. Они позволяют нам как разработчикам подключаться к различным частям процесса рендеринга браузера. Ворклеты это хуки внутри rendering pipeline браузера, позволяющий нам иметь низкоуровневый доступ к процессу рендеринга браузера, таким как вычисление стилей и расчет макета.
Ворклеты ограничены конкретными вариантами использования; их нельзя использовать для произвольных вычислений, таких как Web Workers. Интерфейс Worklet абстрагирует свойства и методы, общие для всех видов worklet, и не может быть создан напрямую. Вместо этого вы можете использовать один из следующих классов:
PaintWorklet - Для программного создания изображения, где свойство CSS ожидает файл. Получите доступ к этому интерфейсу через CSS.paintWorklet.
AudioWorklet - Для обработки звука с помощью пользовательских AudioNodes.
AnimationWorklet - Для создания связанных с прокруткой и других высокопроизводительных процедурных анимаций.
LayoutWorklet - Для определения положения и размеров пользовательских элементов.
14. Расскажите про requestAnimationFrame?
Указывает браузеру на то, что вы хотите произвести анимацию, и просит его запланировать перерисовку на следующем кадре анимации. В качестве параметра метод получает функцию, которая будет вызвана перед перерисовкой. Вы должны вызывать этот метод всякий раз, когда готовы обновить анимацию на экране, чтобы запросить планирование анимации. Обычно запросы происходят 60 раз в секунду, но чаще всего совпадают с частотой обновления экрана. В большинстве браузеров в фоновых вкладках или скрытых <'iframe>, вызовы requestAnimationFrame() приостанавливаются, для того, чтобы повысить производительность и время работы батареи.
let pos = 0;
function myAnimation() {
pos++;
elem.style.top = pos + "px";
elem.style.left = pos + "px";
if (pos < 300) {
requestAnimationFrame(myAnimation);
}
}
let id = requestAnimationFrame(myAnimation);
cancelAnimationFrame(id);
Метод requestAnimationFrame предоставляет разработчикам доступ к жизненному циклу фрейма, позволяя выполнять операции перед вычислением стилей и формированием макета (layout) документа браузером. Вот почему данный метод отлично подходит для реализации анимации. Собственно, для этого он и предназначен.
- Во-первых, он вызывается не чаще и не реже, чем браузер вычисляет макет (правильная частота).
- Во-вторых, он вызывается перед формированием макета (правильное время). Поэтому rAF также отлично подходит для внесения изменений в DOM или CSSOM. Он синхронизирован с vsync, как и любой другой механизм рендеринга, используемый браузером.
requestAnimationFrame вернет айди для возможности остановки анимации
var start = null; var element = document.getElementById('SomeElementYouWantToAnimate'); function step(timestamp) {
if (!start) start = timestamp;
var progress = timestamp - start;
element.style.transform = 'translateX(' + Math.min(progress / 10, 200) + 'px)';
if (progress < 2000) {
window.requestAnimationFrame(step);
}
}
window.requestAnimationFrame(step);
При написании кода, мы вручную устанавливаем, как именно будет происходить анимация покадрово. Частота кадров на мониторе и частота перерисовки браузером контента может быть не постоянной, это зависит от нагрузки, сколько одновременно запущено программ на компьютере клиента. В таких случаях можно наблюдать, как какие-то кадры замирают. Вдобавок частота кадров стремится к какому-то значению. Вот тогда и происходит рассинхронизация: setTimeout принуждает перерисовывать кадры, не синхронно со сменой кадров у компьютера. Однако самая большая проблема состоит в том, что если открыто несколько вкладок браузера, и на одной из них есть анимация, то она все равно работает, активна эта вкладка или нет. В результате, все это ведет к избыточной нагрузке на компьютер.
// HTML разметка
<button class="btn">Запуск анимации</button>
<div class="wrapper">
<div class="box"></div>
</div>
Найдем оба элемента по селектору и поместим их в переменные для наших манипуляций.
const btn = document.querySelector('.btn'),
elem = document.querySelector('.box');
// Изначальная позиция квадрата
let pos = 0;
requestAnimationFrame запускает функцию myAnimation() в виде коллбэка. Когда позиция элемента станет меньше 300, то нужно запустить анимацию, а если больше, то остановить анимацию. За запуск анимации отвечает requestAnimationFrame, таким образом происходит зацикливание анимации. И когда анимация выполнится 300 раз, то перестанет запускаться.
function myAnimation() {
pos++; // Увеличиваем позицию квадрата на единицу
elem.style.top = pos + "px"; // Устанавливаем значение top для инлайн стилей у квадрата
elem.style.left = pos + 'px'; // // Устанавливаем значение left для инлайн стилей у квадрата
if (pos < 300) {
requestAnimationFrame(myAnimation); // Запуск анимации
}
}
Для запуска анимации в первый раз, отслеживаем клик по кнопке btn и вызываем requestAnimationFrame. Для отмены анимации, нужно вызвать метод cancelAnimationFrame с уникальным идентификатором.
btn.addEventListener('click', () => requestAnimationFrame(myAnimation));
let id = requestAnimationFrame(myAnimation);
cancelAnimationFrame(id);
rAF не регулируются автоматически, если их выполнение занимает слишком много времени.
Допустим, в очереди 5 обратных вызовов, и каждый из них займет ~ 100 мс. Браузер не будет распределять их по множеству фреймов. Он попытается запустить их все в заданном кадре, даже если это займет 500 мс. Это означает довольно значительный рывок (притормаживание).
Вы можете спросить: «Почему внутри одного кадра запрашивается ПЯТЬ обратных вызовов animationFrame?» Это может случайно произойти, если вы сделаете 2 очень разумные вещи. 1) Вы запрашиваете новый обратный вызов в конце rAF. 2) Вы запрашиваете обратные вызовы rAF от обработчиков ввода. В результате вы удваиваете/утроите свою работу в каждом кадре.
Вы сами должны объединить RAF. Итак: если есть вероятность того, что в одном кадре сработает несколько «одних и тех же» обратных вызовов, вы должны управлять планированием/объединением.
Тем не менее, Chrome (по крайней мере) попытается кому-то помочь. Если rAF перегружают основной поток, браузер начнет блокировать события ввода, так что, будем надеяться, конфликт будет устранен.
15. Что такое requestIdleCallback?
Метод requestIdleCallback позволяет выполнять низкоприоритетные операции в период простоя браузера (отсюда idle) внутри фрейма (обычно, это происходит после вычисления браузером макета и его перерисовки, когда осталось какое-то время перед синхронизацией) или во время бездействия пользователя.
Даже если с точки зрения пользователя страница "подвисает", могут быть периоды, когда браузер находится в режиме ожидания. Максимальная продолжительность времени, формально предоставляемая rIC для выполнения задачи, составляет 50 мс. Фактически же в нашем распоряжении имеется всего 0.5-10 мс. Поэтому, если внутри rIC вызывается функция для изменения DOM, ее следует вызывать с помощью rAF. Это объясняется тем, что модификация DOM — это потенциально продолжительная операция, на выполнение которой в rIC может не хватить времени.
Браузер точно знает, сколько времени доступно в конце кадра и взаимодействует ли пользователь со страницей, поэтому с помощью requestIdleCallback мы получаем API, который позволяет нам максимально использовать любое свободное время, максимально возможным эффективным способом.
Это первые дни для requestIdleCallback, поэтому перед его использованием вы должны убедиться, что он доступен для использования:
if ('requestIdleCallback' in window) {
// Use requestIdleCallback to schedule work.
} else {
// Do what you’d do today.
}
Вызов requestIdleCallback очень похож на requestAnimationFrame тем, что он принимает функцию обратного вызова в качестве первого параметра:
requestIdleCallback(myNonEssentialWork);
При вызове myNonEssentialWork ему будет предоставлен объект deadline, который содержит функцию, возвращающую число, указывающее, сколько времени осталось для вашей работы:
function myNonEssentialWork (deadline) {
while (deadline.timeRemaining() > 0)
doWorkIfNeeded();
}
Функцию timeRemaining можно вызвать для получения последнего значения. Когда timeRemaining() возвращает ноль, вы можете запланировать еще один requestIdleCallback, если у вас еще есть работа:
Вы можете вызвать requestIdleCallback() в функции обратного вызова бездействия, чтобы запланировать другой обратный вызов не раньше, чем следующий проход через event loop.
function myNonEssentialWork (deadline) {
while (deadline.timeRemaining() > 0 && tasks.length > 0)
doWorkIfNeeded();
if (tasks.length > 0)
requestIdleCallback(myNonEssentialWork);
}
Что делать, если браузер все время заняты? Вы можете быть обеспокоены тем, что ваш обратный вызов никогда не будет вызван. Что ж, хотя requestIdleCallback похож на requestAnimationFrame, есть и отличия. Он принимает необязательный второй параметр: объект параметров со свойством тайм-аута. Этот тайм-аут, если он установлен, дает браузеру время в миллисекундах, в течение которого он должен выполнить обратный вызов:
// Подождите не более двух секунд перед обработкой событий.
requestIdleCallback(processPendingAnalyticsEvents, { timeout: 2000 });
Если ваш обратный вызов выполняется из-за срабатывания тайм-аута, вы заметите две вещи:
- timeRemaining() вернет ноль.
- Свойство didTimeout объекта крайнего срока будет иметь значение true.
Если вы видите, что didTimeout имеет значение true, вы, скорее всего, просто захотите запустить работу и покончить с ней:
function myNonEssentialWork (deadline) {
// Use any remaining time, or, if timed out, just run through the tasks.
while ((deadline.timeRemaining() > 0 || deadline.didTimeout) &&
tasks.length > 0)
doWorkIfNeeded();
if (tasks.length > 0)
requestIdleCallback(myNonEssentialWork);
}
Из-за потенциального сбоя, который этот тайм-аут может вызвать у ваших пользователей (работа может привести к тому, что ваше приложение перестанет отвечать или зависать), будьте осторожны с настройкой этого параметра. Там, где вы можете, пусть браузер решает, когда вызывать обратный вызов.
1. Можно использовать requestIdleCallback для отправки аналитических данных. В этом случае мы, вероятно, хотели бы отслеживать событие, например, касание меню навигации. Однако, поскольку они обычно анимируются на экране, нам следует избегать немедленной отправки этого события в Google Analytics. Мы создадим массив событий для отправки и запросим их отправку в какой-то момент в будущем.
2. Использование requestIdleCallback для внесения изменений в DOM. Нужно внести несущественные изменения DOM, например добавить элементы в конец постоянно растущего списка с ленивой загрузкой. Давайте посмотрим, как requestIdleCallback на самом деле вписывается в типичный фрейм.
16. Что такое замыкание?
Область видимости — это политика пространства, которая управляет доступностью переменных. Область видимости изолирует переменные.
Области видимости могут быть вложенными, внешняя область доступна из внутренней.
Лексическая область видимости состоит из внешних областей видимости, определенных статически.
Замыкание — это функция, которая обращается к своей лексической области видимости, даже если она выполняется вне ее лексической области видимости.
Замыкание — это комбинация функции и лексического окружения, в котором эта функция была объявлена. Это окружение состоит из произвольного количества локальных переменных, которые были в области действия функции во время создания замыкания.
Простыми словами, замыкание запоминает переменные из того места где оно было обьявлено и не важно где оно было запущено.
Event handlers - Когда кнопка нажата, handleClick() выполняется где-то внутри кода DOM. Исполнение происходит далеко от места определения.
Functional programming - carrying. Каррирование происходит, когда функция возвращает другую функцию до тех пор, пока аргументы не будут предоставлены полностью.
function multiply(a) {
return function executeMultiply(b) {
return a * b;
}
}
const double = multiply(2);
double(3); // => 6
double(5); // => 10
const triple = multiply(3);
triple(4); // => 12
17. Что такое прототип объекта в JavaScript?
В JavaScript объект может наследовать свойства другого объекта. Объект, от которого наследуются свойства, называется прототипом.
Прототип даёт нам немного «магии». Когда мы хотим прочитать свойство из object, а оно отсутствует, JavaScript автоматически берёт его из прототипа. В программировании такой механизм называется «прототипным наследованием». Многие интересные возможности языка и техники программирования основываются на нём.
JavaScript ищет унаследованные свойства в прототипе объекта, а также в прототипе прототипа и так далее в цепочке прототипов.
Суть прототипного наследования в JavaScript: объекты могут наследовать свойства от других объектов - прототипов. Связующим звеном выступает специальное свойство __proto__
Если один объект имеет специальную ссылку __proto__ на другой объект, то при чтении свойства из него, если свойство отсутствует в самом объекте, оно ищется в объекте __proto__. Недостаток этого подхода – он не работает в IE10-.
К счастью, в JavaScript с древнейших времён существует альтернативный, встроенный в язык и полностью кросс-браузерный способ. Чтобы новым объектам автоматически ставить прототип, конструктору ставится свойство prototype.
Чтобы новым объектам автоматически ставить прототип, конструктору ставится свойство prototype.
При создании объекта через new, в его прототип __proto__ записывается ссылка из prototype функции-конструктора.
var animal = {
eats: true
};
function Rabbit(name) {
this.name = name;
}
Rabbit.prototype = animal;
var rabbit = new Rabbit("Кроль"); // rabbit.__proto__ == animal
alert( rabbit.eats ); // true
Установка Rabbit.prototype = animal буквально говорит интерпретатору следующее: "При создании объекта через new Rabbit запиши ему __proto__ = animal".
Null это самый верхний прототип, у нуля уже нет прототипов.
Свойство __proto__ — исторически обусловленный геттер/сеттер для [[Prototype]] Обратите внимание, что __proto__ — не то же самое, что [[Prototype]]. Это геттер/сеттер для него.
Есть только два ограничения прототипирования:
- Ссылки не могут идти по кругу. JavaScript выдаст ошибку, если мы попытаемся назначить __proto__ по кругу.
- Значение __proto__ может быть объектом или null. Другие типы игнорируются.
прототипы никак не влияют на this. Неважно, где находится метод: в объекте или его прототипе. При вызове метода this — всегда объект перед точкой. Это на самом деле очень важная деталь, потому что у нас может быть большой объект со множеством методов, от которого можно наследовать. Затем наследующие объекты могут вызывать его методы, но они будут изменять своё состояние, а не состояние объекта-родителя.
Собственные и унаследованные свойства обьекта.
let animal = {
eats: true
};
let rabbit = {
jumps: true,
__proto__: animal
};
// Object.keys возвращает только собственные ключи
alert(Object.keys(rabbit)); // jumps
// for..in проходит и по своим, и по унаследованным ключам
for(let prop in rabbit) alert(prop); // jumps, затем eats
Если унаследованные свойства нам не нужны, то мы можем отфильтровать их при помощи встроенного метода obj.hasOwnProperty(key): он возвращает true, если у obj есть собственное, не унаследованное, свойство с именем key. Но почему hasOwnProperty не появляется в цикле for..in в отличие от eats и jumps? Он ведь перечисляет все унаследованные свойства.
Ответ простой: оно не перечислимо. То есть, у него внутренний флаг enumerable стоит false, как и у других свойств Object.prototype. Поэтому оно и не появляется в цикле.
- В JavaScript все объекты имеют скрытое свойство [[Prototype]], которое является либо другим объектом, либо null.
- Мы можем использовать obj.__proto__ для доступа к нему (исторически обусловленный геттер/сеттер, есть другие способы, которые скоро будут рассмотрены).
- Объект, на который ссылается [[Prototype]], называется «прототипом».
- Если мы хотим прочитать свойство obj или вызвать метод, которого не существует у obj, тогда JavaScript попытается найти его в прототипе.
- Операции записи/удаления работают непосредственно с объектом, они не используют прототип (если это обычное свойство, а не сеттер).
- Если мы вызываем obj.method(), а метод при этом взят из прототипа, то this всё равно ссылается на obj. Таким образом, методы всегда работают с текущим объектом, даже если они наследуются.
- Цикл for..in перебирает как свои, так и унаследованные свойства. Остальные методы получения ключей/значений работают только с собственными свойствами объекта.
use cases:
Одной из причин использования встроенного объекта-прототипа является многократное дублирование объекта, который будет иметь общую функциональность. Прикрепляя методы к прототипу, вы можете сэкономить на дублировании методов, создаваемых для каждого нового экземпляра. Но когда вы присоединяете метод к прототипу, все экземпляры будут иметь доступ к этим методам.
18. Расскажите про классы в JavaScript?
В JavaScript класс – это разновидность функции.
class User {
constructor(name) { this.name = name; }
sayHi() { alert(this.name); }
}
// доказательство: User - это функция
alert(typeof User); // function
Вот что на самом деле делает конструкция class User {...}:
- Создаёт функцию с именем User, которая становится результатом объявления класса. Код функции берётся из метода constructor (она будет пустой, если такого метода нет).
- Сохраняет все методы, такие как sayHi, в User.prototype. При вызове метода объекта new User он будет взят из прототипа. Таким образом, объекты new User имеют доступ к методам класса.
Можно проверить вышесказанное и при помощи кода:
class User {
constructor(name) { this.name = name; }
sayHi() { alert(this.name); }
}
// класс - это функция
alert(typeof User); // function
// ...или, если точнее, это метод constructor
alert(User === User.prototype.constructor); // true
// Методы находятся в User.prototype, например:
alert(User.prototype.sayHi); // alert(this.name);
// в прототипе ровно 2 метода
alert(Object.getOwnPropertyNames(User.prototype)); // constructor, sayHi
Классы всегда используют use strict. Весь код внутри класса автоматически находится в строгом режиме.
Поля класса — это переменные, содержащие информацию. Поля могут быть прикреплены к 2 объектам:
- Поля экземпляра класса
- Поля в самом классе (static)
Поля также имеют 2 уровня доступности:
- Public: поле доступно в любом месте
- Private: поле доступно только внутри тела класса
Приватные поля доступны только внутри тела класса.
class User {
#name;
constructor(name) {
this.#name = name;
}
getName() {
return this.#name;
}
}
const user = new User('Jon Snow');
user.getName(); // => 'Jon Snow'
user.#name; // SyntaxError is thrown
Вы также можете определить поля в самом классе: статические поля. Они полезны для определения констант класса или хранения информации, относящейся к классу.
class User {
static TYPE_ADMIN = 'admin';
static TYPE_REGULAR = 'regular';
name;
type;
constructor(name, type) {
this.name = name;
this.type = type;
}
}
const admin = new User('Site Admin', User.TYPE_ADMIN);
admin.type === User.TYPE_ADMIN; // => true
Чтобы сделать статическое поле приватным, поставьте перед именем поля специальный символ #: static #myPrivateStaticField.
Допустим, вы хотите ограничить количество экземпляров класса User. Чтобы скрыть детали лимитов экземпляров, вы можете создать приватные статические поля pattern singleton:
class User {
static #MAX_INSTANCES = 1;
static #instances = 0;
name;
constructor(name) {
User.#instances++;
if (User.#instances > User.#MAX_INSTANCES) {
throw new Error('Unable to create User instance');
}
this.name = name;
}
}
new User('Jon Snow');
new User('Sansa Stark'); // throws Error
Эти закрытые статические поля доступны только в классе User. Ничто из внешнего мира не может помешать механизму ограничений: в этом преимущество инкапсуляции.
Статические методы — это функции, прикрепленные непосредственно к классу. Они содержат логику, связанную с классом, а не с экземпляром класса.
static myStaticMethod() { ... }.
При работе со статическими методами следует помнить 2 простых правила:
- Статический метод может обращаться к статическим полям
- Статический метод не может получить доступ к полям экземпляра.
class User {
static #takenNames = [];
static isNameTaken(name) {
return User.#takenNames.includes(name);
}
name = 'Unknown';
constructor(name) {
this.name = name;
User.#takenNames.push(name);
}
}
const user = new User('Jon Snow');
User.isNameTaken('Jon Snow'); // => true
User.isNameTaken('Arya Stark'); // => false
isNameTaken() — это статический метод, который использует статическое приватное поле User.#takenNames для проверки занятых имен.
Статические методы могут быть приватными: static #staticFunction() {...}. Опять же, они соблюдают правила приватности: вы можете вызывать приватный статический метод только внутри тела класса.
Классы в JavaScript поддерживают одиночное наследование с использованием ключевого слова extends.
В выражении class Child extends Parent { } класс Child наследует от Parent конструктор, поля и методы.
class User {
name;
constructor(name) {
this.name = name;
}
getName() {
return this.name;
}
}
class ContentWriter extends User {
posts = [];
}
const writer = new ContentWriter('John Smith');
writer.name; // => 'John Smith'
writer.getName(); // => 'John Smith'
writer.posts; // => []
ContentWriter наследует от пользователя конструктор, метод getName() и имя поля. Кроме того, класс ContentWriter объявляет новое поле posts.
Обратите внимание, что частные члены родительского класса не наследуются дочерним классом.
Если вы хотите вызвать родительский конструктор в дочернем классе, вам нужно использовать специальную функцию super(), доступную в дочернем конструкторе.
Например, сделаем так, чтобы конструктор ContentWriter вызывал родительский конструктор User, а также инициализировал поле posts:
class User {
name;
constructor(name) {
this.name = name;
}
getName() {
return this.name;
}
}
class ContentWriter extends User {
posts = [];
constructor(name, posts) {
super(name);
this.posts = posts;
}
}
const writer = new ContentWriter('John Smith', ['Why I like JS']);
writer.name; // => 'John Smith'
writer.posts // => ['Why I like JS']
super(name) inside the child class ContentWriter executes the constructor of the parent class User
Если вы хотите получить доступ к родительскому методу внутри дочернего метода, вы можете использовать специальный ярлык super.
class User {
name;
constructor(name) {
this.name = name;
}
getName() {
return this.name;
}
}
class ContentWriter extends User {
posts = [];
constructor(name, posts) {
super(name);
this.posts = posts;
}
getName() {
const name = super.getName();
if (name === '') {
return 'Unknwon';
}
return name;
}
}
const writer = new ContentWriter('', ['Why I like JS']);
writer.getName(); // => 'Unknwon'
getName() дочернего класса ContentWriter обращается к методу super.getName() непосредственно из родительского класса User.
Эта функция называется переопределением метода.
Обратите внимание, что вы также можете использовать super со статическими методами, чтобы получить доступ к статическим методам родителя.
Итого:
class MyClass {
prop = value; // свойство
constructor(...) { // конструктор
// ...
}
method(...) {} // метод
get something(...) {} // геттер
set something(...) {} // сеттер
[Symbol.iterator]() {} // метод с вычисляемым именем (здесь - символом)
// ...
}
MyClass технически является функцией (той, которую мы определяем как constructor), в то время как методы, геттеры и сеттеры записываются в MyClass.prototype.
Давайте обобщим, какие методы для проверки типа мы знаем:
| работает | для | возвращает |
|---|---|---|
| typeof | примитивов | строка |
| {}.toString | примитивов, встроенных объектов, объектов с Symbol.toStringTag | строка |
| instanceof | объектов | true/false |
Как мы можем видеть, технически {}.toString «более продвинут», чем typeof.
А оператор instanceof – отличный выбор, когда мы работаем с иерархией классов и хотим делать проверки с учётом наследования. instanceof является полиморфным: оператор определяет дочерний элемент как экземпляр родительского класса.
Классы JavaScript инициализируют экземпляры с помощью конструкторов, определяют поля и методы. Вы можете присоединять поля и методы даже к самому классу, используя ключевое слово static.
Наследование достигается с помощью ключевого слова extends: вы можете легко создать дочерний класс из родительского. ключевое слово super используется для доступа к родительскому классу из дочернего класса.
Чтобы воспользоваться преимуществами инкапсуляции, сделайте поля и методы закрытыми, чтобы скрыть внутренние детали ваших классов. Имена приватных полей и методов должны начинаться с #.
19. Как работают методы apply(), call() и bind()?
Функции в JavaScript никак не привязаны к своему контексту this, с одной стороны, здорово – это позволяет быть максимально гибкими, одалживать методы и так далее.
Но с другой стороны – в некоторых случаях контекст может быть потерян. Способы явно указать this - методы bind, call и apply.
-
Синтаксис метода call: func.call(context, arg1, arg2, ...)
При этом вызывается функция func, первый аргумент call становится её this, а остальные передаются «как есть». Вызов func.call(context, a, b...) – то же, что обычный вызов func(a, b...), но с явно указанным this(=context).
-
Если нам неизвестно, с каким количеством аргументов понадобится вызвать функцию, можно использовать более мощный метод: apply. Вызов функции при помощи func.apply работает аналогично func.call, но принимает массив аргументов вместо списка.
func.call(context, arg1, arg2) идентичен вызову func.apply(context, [arg1, arg2]);
-
Синтаксис встроенного bind: var wrapper = func.bind(context[, arg1, arg2...])
Методы bind и call/apply близки по синтаксису, но есть важнейшее отличие. Методы call/apply вызывают функцию с заданным контекстом и аргументами. А bind не вызывает функцию. Он только возвращает «обёртку», которую мы можем вызвать позже, и которая передаст вызов в исходную функцию, с привязанным контекстом.
20. Что такое Promise (Промис)?
Promise – это специальный объект, который содержит состояние выполнения асинхронной функции. Вначале pending («ожидание»), затем – одно из: fulfilled («выполнено успешно») или rejected («выполнено с ошибкой»).
Промисы, оборачивающие результат асинхронной операции, могут быть возвращены синхронно из функции, присвоены переменным или использованы в качестве аргументов. В этом и заключается идея промисов: инкапсулировать асинхронность и позволить функциям, обрабатывающим асинхронные операции, по-прежнему выглядеть синхронно.
Синтаксис создания Promise:
var promise = new Promise(function(resolve, reject) {
// Эта функция будет вызвана автоматически
// В ней можно делать любые асинхронные операции,
// А когда они завершатся — нужно вызвать одно из:
// resolve(результат) при успешном выполнении
// reject(ошибка) при ошибке
if (asyncOperationSuccess) {
resolve(value); // async operation successful
} else {
reject(error); // async operation error
}
})
Большинство асинхронных функций популярных библиотек (например, axios) или веб-API (например, fetch()) возвращают уже созданные промисы.
Универсальный метод для навешивания обработчиков:
promise.then(onFulfilled, onRejected)
- onFulfilled – функция, которая будет вызвана с результатом при resolve.
- onRejected – функция, которая будет вызвана с ошибкой при reject.
Возьмём setTimeout в качестве асинхронной операции, которая должна через некоторое время успешно завершиться с результатом «result»:
// Создаётся объект promise
let promise = new Promise((resolve, reject) => {
setTimeout(() => {
// переведёт промис в состояние fulfilled с результатом "result"
resolve("result");
}, 1000);
});
// promise.then навешивает обработчики на успешный результат или ошибку
promise
.then(
result => {
// первая функция-обработчик - запустится при вызове resolve
alert("Fulfilled: " + result); // result - аргумент resolve
},
error => {
// вторая функция - запустится при вызове reject
alert("Rejected: " + error); // error - аргумент reject
}
);
В результате запуска кода выше – через 1 секунду выведется «Fulfilled: result».
Второе большое преимущество заключается в том, что промисы могут создавать цепочки для обработки нескольких зависимых асинхронных операций.
Техническая сторона цепочки состоит в том, что методы promise.then(successCallback) и даже promise.catch(errorCallback) сами по себе возвращают промис, к которому можно присоединить методы .then() или .catch(), и т.д. на.
Ожидает исполнения всех промисов или отклонения любого из них. Возвращает промис, который исполнится после исполнения всех промисов в iterable. В случае, если любой из промисов будет отклонён, Promise.all будет также отклонён.
Ожидает завершения всех полученных промисов (как исполнения так и отклонения). Возвращает промис, который исполняется когда все полученные промисы завершены (исполнены или отклонены), содержащий массив результатов исполнения полученных промисов.
const statusesPromise = Promise.allSettled([
resolveTimeout(['potatoes', 'tomatoes'], 1000),
resolveTimeout(['oranges', 'apples'], 1000)
]);
// wait...
const statuses = await statusesPromise;
// after 1 second
console.log(statuses);
// [
// { status: 'fulfilled', value: ['potatoes', 'tomatoes'] },
// { status: 'fulfilled', value: ['oranges', 'apples'] }
// ]
Ожидает исполнения или отклонения любого из полученных промисов. Возвращает промис, который будет исполнен или отклонён с результатом исполнения первого исполненного или отклонённого промиса из .iterable. Promise.race устанавливается, как только выполняется любое из promise, которые вы ему передаете, независимо от того, выполнены они или отклонены. Promise.any(promises) — это вспомогательная функция, которая выполняет промисы параллельно и разрешает значение первого успешно разрешенного промиса из списка промисов.Полезно выполнять независимые асинхронные операции параллельно и в режиме гонки, чтобы получить значение любого первого выполненного обещания. Promise, возвращаемый Promise.any(), выполняется с любым первым выполненным promise. Даже если некоторые promise будут rejected, эти reject будут проигнорированы. Promise.any решится, как только любое из переданных вами promise выполнено или все они отклонены, и в этом случае они отклоняются с AggregateError.
Promise.race реджектится, когда реджектиться первый promise, который вы ей даете; c any процесс происходит по другому, она обработает promise до первого успешного или пока все не отклонятся
причиной отклонения promise.any может быть только AggregateError, но причиной отклонения promise.race может быть отклонение любого promise.
Объект AggregateError представляет ошибку, когда несколько ошибок необходимо обернуть одной ошибкой. Он вызывается, когда операция должна сообщить о нескольких ошибках, например, Promise.any(), когда все промисы, переданные ему, отклоняются.
Возвращает промис, исполненный с результатом value.
Использование промисов по-прежнему требует обратных вызовов и относительно большого количества шаблонного кода, такого как .then(), .catch().
Синхронно вернуть результат из Promise не получится, promise всегда возвращает promise.
К счастью, JavaScript сделал еще один шаг вперед в улучшении асинхронного кода, предоставив синтаксис async/await — действительно полезный синтаксический сахар поверх промисов.
Когда это возможно, я настоятельно рекомендую работать с синтаксисом async/await, а не с необработанными промисами.
Применение синтаксиса async/await поверх промисов относительно просто:
- Отметьте функции, использующие промисы, ключевым словом async.
- Внутри тела асинхронной функции всякий раз, когда вы хотите дождаться разрешения промиса, используйте синтаксис await promiseExpression
- Асинхронная функция всегда возвращает обещание, что позволяет вызывать асинхронные функции внутри асинхронных функций.
const address = fetch("https://jsonplaceholder.typicode.com/users/1")
.then((response) => response.json())
.then((user) => {
return user.address;
});
const printAddress = async () => {
const a = await address;
console.log(a);
};
printAddress();
21. Promise.all?
Например, параллельно загрузить несколько файлов и обработать результат, когда он готов.
Для этого как раз и пригодится Promise.all.
Синтаксис:
let promise = Promise.all([...промисы...]); Метод Promise.all принимает массив промисов (может принимать любой перебираемый объект, но обычно используется массив) и возвращает новый промис.
Новый промис завершится, когда завершится весь переданный список промисов, и его результатом будет массив их результатов. Часто применяемый трюк – пропустить массив данных через map-функцию, которая для каждого элемента создаст задачу-промис, и затем обернёт получившийся массив в Promise.all.
Например, если у нас есть массив ссылок, то мы можем загрузить их вот так:
let urls = [
'https://api.github.com/users/iliakan',
'https://api.github.com/users/remy',
'https://api.github.com/users/jeresig'
];
// Преобразуем каждый URL в промис, возвращённый fetch
let requests = urls.map(url => fetch(url));
// Promise.all будет ожидать выполнения всех промисов
Promise.all(requests)
.then(responses => responses.forEach(
response => alert(`${response.url}: ${response.status}`)
));
В случае ошибки, остальные результаты игнорируются Если один промис завершается с ошибкой, то весь Promise.all завершается с ней, полностью забывая про остальные промисы в списке. Их результаты игнорируются.
Например, если сделано несколько вызовов fetch, как в примере выше, и один не прошёл, то остальные будут всё ещё выполняться, но Promise.all за ними уже не смотрит. Скорее всего, они так или иначе завершатся, но их результаты будут проигнорированы.
Promise.all ничего не делает для их отмены, так как в промисах вообще нет концепции «отмены».
22. Async/await?
Внутри функции используется ключевое слово await, которое ставится перед вызовом функций, которые, в свою очередь, тоже возвращают промисы. Если результат этого вызова присваивается переменной или константе, то в них записывается результат вызова. Если присвоения нет, как в последнем вызове await, то происходит ожидание выполнения операции без использования её результата.
Асинхронность в данном случае (как и в промисах) гарантирует нам, что программа не блокируется ожидая завершения вызовов, она может продолжать делать что-то еще (но не в этой функции). Но она не гарантирует параллельности. Более того, подряд идущие await в рамках одной функции всегда выполняются строго друг за другом. Не сколько параллельно запущенных функций будут выполняться параллельно, но внутри при наличии await они все равно выполняются последовательно.
Асинхронные функции создают, пользуясь при объявлении функции ключевым словом async. Выглядит это так:
const asyncFunction = async () => {
// Код
}
Выполнение асинхронной функции можно приостановить с использованием ключевого слова await. Его можно использовать только в асинхронных функциях. Оно позволяет возвратить результат работы асинхронной функции, который будет доступен после того, как такая функция завершит выполнение некоей задачи.
Сравним работу асинхронной функции и промиса, которые возвращают строку:
// Асинхронная функция
const asyncGreeting = async () => 'Greetings';
// Промис
const promiseGreeting = () => new Promise(((resolve) => {
resolve('Greetings');
}));
asyncGreeting().then(result => console.log(result));
promiseGreeting().then(result => console.log(result));
Несложно заметить, что использование ключевого слова async позволяет писать асинхронный код, который выглядит как синхронный. С таким кодом гораздо легче работать.
async/await имеет 4 простых правила:
- Функция, обрабатывающая асинхронную задачу, должна быть помечена ключевым словом async.
- Оператор await promise приостанавливает выполнение функции до тех пор, пока обещание не будет успешно разрешено или отклонено.
- Если обещание разрешается успешно, оператор await возвращает разрешенное значение: const resolveValue = await обещание. В противном случае вы можете поймать отклоненное обещание внутри try/catch.
- Асинхронная функция всегда возвращает обещание, что дает возможность вкладывать асинхронные функции.
23. Что такое Set, Map, WeakSet и WeakMap?
В ES-2015 появились новые типы коллекций в JavaScript: Set, Map, WeakSet и WeakMap.
Map – коллекция для хранения записей вида ключ:значение. В отличие от объектов, в которых ключами могут быть только строки, в Map ключом может быть произвольное значение.
Использование объектов в качестве ключей – это одна из известных и часто применяемых возможностей объекта Map. При строковых ключах обычный объект Object может подойти, но для ключей-объектов – уже нет.
Методы и свойства:
new Map([iterable]) – создаёт коллекцию, можно указать перебираемый объект (обычно массив) из пар [ключ,значение] для инициализации.
map.set(key, value) – записывает по ключу key значение value.
map.get(key) – возвращает значение по ключу или undefined, если ключ key отсутствует.
map.has(key) – возвращает true, если ключ key присутствует в коллекции, иначе false.
map.delete(key) – удаляет элемент по ключу key.
map.clear() – очищает коллекцию от всех элементов.
map.size – возвращает текущее количество элементов.
Отличия от обычного объекта Object:
Что угодно может быть ключом, в том числе и объекты.
Есть дополнительные методы, свойство size.
Set – коллекция для хранения множества значений, причём каждое значение может встречаться лишь один раз. Например, к нам приходят посетители, и мы хотели бы сохранять всех, кто пришёл. При этом повторные визиты не должны приводить к дубликатам, то есть каждого посетителя нужно «посчитать» ровно один раз. Set для этого отлично подходит:
Методы и свойства:
new Set([iterable]) – создаёт Set, можно указать перебираемый объект со значениями для инициализации.
set.add(value) – добавляет значение (если оно уже есть, то ничего не делает), возвращает тот же объект set.
set.delete(value) – удаляет значение, возвращает true если value было в множестве на момент вызова, иначе false.
set.has(value) – возвращает true, если значение присутствует в множестве, иначе false.
set.clear() – удаляет все имеющиеся значения.
set.size – возвращает количество элементов в множестве.
Перебор Map и Set всегда осуществляется в порядке добавления элементов, так что нельзя сказать, что это – неупорядоченные коллекции, но поменять порядок элементов или получить элемент напрямую по его номеру нельзя.
WeakMap - принципиально другая структура в этом аспекте. Она не предотвращает удаление объектов сборщиком мусора, когда эти объекты выступают в качестве ключей.
Первое его отличие от Map в том, что ключи в WeakMap должны быть объектами, а не примитивными значениями. Теперь, если мы используем объект в качестве ключа и если больше нет ссылок на этот объект, то он будет удалён из памяти (и из объекта WeakMap) автоматически. Количество элементов в коллекции WeakMap неизвестно. Движок может произвести очистку сразу или потом, или сделать это частично. По этой причине методы для доступа ко всем сразу ключам/значениям недоступны.
Примеры использования weakMap: Другая частая сфера применения – это кеширование, когда результат вызова функции должен где-то запоминаться («кешироваться») для того, чтобы дальнейшие её вызовы на том же объекте могли просто брать уже готовый результат, повторно используя его. Многократные вызовы process(obj) с тем же самым объектом в качестве аргумента ведут к тому, что результат вычисляется только в первый раз, а затем последующие вызовы берут его из кеша. Недостатком является то, что необходимо вручную очищать cache от ставших ненужными объектов.
Но если мы будем использовать WeakMap вместо Map, то эта проблема исчезнет: закешированные результаты будут автоматически удалены из памяти сборщиком мусора.
В WeakMap присутствуют только следующие методы:
weakMap.get(key)
weakMap.set(key, value)
weakMap.delete(key)
weakMap.has(key)
// текущие активные пользователи
let activeUsers = [
{name: "Вася"},
{name: "Петя"},
{name: "Маша"}
];
// вспомогательная информация о них,
// которая напрямую не входит в объект юзера,
// и потому хранится отдельно
let weakMap = new WeakMap();
weakMap.set(activeUsers[0], 1);
weakMap.set(activeUsers[1], 2);
weakMap.set(activeUsers[2], 3);
weakMap.set('Katya', 4); //Будет ошибка TypeError: "Katya" is not a non-null object
alert( weakMap.get(activeUsers[0]) ); // 1
activeUsers.splice(0, 1); // Вася более не активный пользователь
// weakMap теперь содержит только 2 элемента
activeUsers.splice(0, 1); // Петя более не активный пользователь
// weakMap теперь содержит только 1 элемент
WeakSet – особый вид Set, не препятствующий сборщику мусора удалять свои элементы. То же самое – WeakMap для Map.
- Она аналогична Set, но мы можем добавлять в WeakSet только объекты (не примитивные значения).
- Объект присутствует в множестве только до тех пор, пока доступен где-то ещё.
- Как и Set, она поддерживает add, has и delete, но не size, keys() и не является перебираемой.
Будучи «слабой» версией оригинальной структуры данных, она тоже служит в качестве дополнительного хранилища. Но не для произвольных данных, а скорее для значений типа «да/нет». Присутствие во множестве WeakSet может что-то сказать нам об объекте.
Например, мы можем добавлять пользователей в WeakSet для учёта тех, кто посещал наш сайт
Наиболее значительным ограничением WeakMap и WeakSet является то, что их нельзя перебрать или взять всё содержимое. Это может доставлять неудобства, но не мешает WeakMap/WeakSet выполнять их главную задачу – быть дополнительным хранилищем данных для объектов, управляемых из каких-то других мест в коде.
WeakMaps позволяют расширять объекты извне, не мешая сборке мусора.
24. Что делает строчка “use strict”;? Какие достоинства и недостатки от ее использования?
ECMAScript 5 (ES5) добавил новые возможности в язык и изменил некоторые из существующих. Чтобы устаревший код работал, как и раньше, по умолчанию подобные изменения не применяются. Поэтому нам нужно явно их активировать с помощью специальной директивы: "use strict".
‘use strict’ это директива, используемая для включения строгого режима во всем скрипте или отдельных функциях.
Не позволяет случайно создавать глобальные переменные.
Любое присваивание, которое в обычном режиме завершается неудачей, в строгом режиме выдаст исключение.
При попытке удалить неудаляемые свойства, выдаст исключение (в то время как в нестрогом режиме никакого действия бы не произошло).
Требует, чтобы имена параметров функции были уникальными.
this в глобальной области видимости равно undefined.
Перехватывает распространенные ошибки, выдавая исключения.
Нельзя использовать некоторые особенности языка, к которым привыкли некоторые разработчики.
Нет доступа к function.caller и function.arguments.
Объединение скриптов, написанных в строгом режиме может вызвать проблемы.
В целом, я думаю, что преимущества перевешивают недостатки, и мне никогда не приходилось полагаться на функции, которые заблокированы в строгом режиме. Я бы порекомендовал использовать строгий режим.
25. Расскажите про методы массивов forEach, filter, map, reduce?
forEach ни чего не возвращает. Прервать итерирование по массиву не возможно.
items.forEach((elem, index, array) => {
// console.log(elem, index , array)
})
filer - возвращает ту часть массива которая прошла по какому то критерию
let check = array.filter(
function(elem, index, array) {
return index > 1
})
map - проходимся по всем элементам нашего массива и точно так же вызываем функцию callback, вернет массив, каждый элемент массива будет содержать новое значение которое вернула функция callback.
let result = array.map(function(elem){
return elem*2
})
reduce - выполняет функцию callback на каждом элементе массива передавая результат выполнения на следующую итерацию. Из массива в итоге получиться 1 значение. Внутри acc аккамулируется сумма которую мы считаем в примере.
let total = array.reduce(function(acc, elem) {
return acc + elem
}, initValue)
const array2 = ['apple', 'banana', 'peach', 'orange']
let fruits = array2.reduce((acc, elem) => {
acc[elem] = 1
return acc
}, {})
26. Что такое "чистая функция"?
Чистые функции — строительные блоки в функциональном программировании. Их обожают за простоту и тестируемость. Функция должна удовлетворять двум условиям, чтобы считаться «чистой»:
— Каждый раз функция возвращает одинаковый результат, когда она вызывается с тем же набором аргументов
— Нет побочных эффектов
1. Нечистые функции = непостоянные результаты
2. Нет побочных эффектов
Примеры побочных эффектов:
Видоизменение входных параметров
console.log
HTTP вызовы (AJAX/fetch)
Изменение в файловой системе
Запросы DOM
По сути, любая работа, выполняемая функцией, не связана с вычислением конечного результата.
27. Что такое лямбда- или стрелочные функции?
Стрелочные функции — это сокращенный способ записи функциональных выражений. Они не имеют собственных this, arguments, super и new.target. Эти функции служат хорошей альтернативой функциям, не имеющим методов, но не могут использоваться как конструкторы.
28. Что такое запоминание или мемоизация?
const memoizAddition = () => {
let cache = {}
return value => {
if (value in cache) {
console.log('Получение данных из кэша')
return cache[value] // в данном случае, cache.value не может быть использовано в качестве названия свойства, поскольку названия свойств в JS не могут начинаться с числа. Поэтому используется скобочная нотация
} else {
console.log('Результат вычисляется')
let result = value + 20
cache[value] = result
return result
}
}
}
// возвращаем функцию из memoizAddition
const addition = memoizAddition()
console.log(addition(20)) // Результат вычисляется 40
console.log(addition(20)) // Получения данных из кэша 40
const multiplayer = (x, y, str) =>{
console.log(x , y, str + ' args in fn')
return x*y
}
const memoize = (fn) => {
const cache = {}
return (...args) => {
if(cache[args]){
console.log('result from cache')
return cache[args]
} else {
const results = fn(...args)
cache[args] = results
console.log('memoized')
return results
}
}
}
const memFn = memoize(multiplayer)
console.log(memFn(3, 4, 'vlad'))
console.log(memFn(3, 4, 'vlad'))
29. Расскажите про сборку мусора в JavaScript?
Если упростить, то «достижимые» значения – это те, которые доступны или используются. Они гарантированно находятся в памяти.
Существует базовое множество достижимых значений, которые не могут быть удалены.
Например:
- Локальные переменные и параметры текущей функции.
- Переменные и параметры других функций в текущей цепочке вложенных вызовов.
- Глобальные переменные.
- Сборщик мусора «помечает» (запоминает) все корневые объекты.
- Затем он идёт по их ссылкам и помечает все найденные объекты.
- Затем он идёт по ссылкам помеченных объектов и помечает объекты, на которые есть ссылка от них. Все объекты запоминаются, чтобы в будущем не посещать один и тот же объект дважды.
- …И так далее, пока не будут посещены все ссылки (достижимые от корней). Все непомеченные объекты удаляются.
- Сборка по поколениям (Generational collection) – объекты делятся на «новые» и «старые». Многие объекты появляются, выполняют свою задачу и быстро умирают, их можно удалять более агрессивно. Те, которые живут достаточно долго, становятся «старыми» и проверяются реже.
- Инкрементальная сборка (Incremental collection) – если объектов много, то обход всех ссылок и пометка достижимых объектов может занять значительное время и привести к видимым задержкам выполнения скрипта. Поэтому интерпретатор пытается организовать сборку мусора поэтапно. Этапы выполняются по отдельности один за другим. Это требует дополнительного учёта для отслеживания изменений между этапами, но зато теперь у нас есть много крошечных задержек вместо одной большой.
- Сборка в свободное время (Idle-time collection) – чтобы уменьшить возможное влияние на производительность, сборщик мусора старается работать только во время простоя процессора.
- Сборка мусора выполняется автоматически. Мы не можем ускорить или предотвратить её.
- Объекты сохраняются в памяти, пока они достижимы.
- Наличие ссылки не гарантирует, что объект достижим (от корня): несколько взаимосвязанных объектов могут стать недостижимыми как единое целое.
Эти значения мы будем называть корнями.
Любое другое значение считается достижимым, если оно доступно из корня по ссылке или по цепочке ссылок.
Например, если в локальной переменной есть объект, и он имеет свойство, в котором хранится ссылка на другой объект, то этот объект считается достижимым. И те, на которые он ссылается, тоже достижимы.
В интерпретаторе JavaScript есть фоновый процесс, который называется сборщик мусора. Он следит за всеми объектами и удаляет те, которые стали недостижимы.
Основной алгоритм сборки мусора – «алгоритм пометок» (англ. «mark-and-sweep»).Согласно этому алгоритму, сборщик мусора регулярно выполняет следующие шаги:
Вот некоторые из оптимизаций:
30. Как сравнивать обьекты в JavaScript?
JavaScript предоставляет 3 способа сравнения значений:
- 1. Оператор строго равенства ===
- 2. Оператор свободного равенста ==
- 3. Object.is() функция
Когда вы сравниваете обьекты одним из превиденных выше способов, сравнение выдаст true только в том случае если сравниваемые значения ссылаются на один и тот же экземпляр обьекта.
const hero1 = {
name: 'Batman'
};
const hero2 = {
name: 'Batman'
};
hero1 === hero1; // => true
hero1 === hero2; // => false
hero1 == hero1; // => true
hero1 == hero2; // => false
Object.is(hero1, hero1); // => true
Object.is(hero1, hero2); // => false
Еще раз. Сравниваются ссылки, а не сами обьекты
Очевидный способ сравнить объекты по содержимому — прочитать свойства и сравнить их вручную.
function isHeroEqual(object1, object2) {
return object1.name === object2.name;
}
const hero1 = {
name: 'Batman'
};
const hero2 = {
name: 'Batman'
};
const hero3 = {
name: 'Joker'
};
isHeroEqual(hero1, hero2); // => true
isHeroEqual(hero1, hero3); // => false
Ручное сравнение требует ручного извлечения свойств — для простых объектов это не проблема. Но для сравнения больших объектов (или объектов с неизвестной структурой) ручное сравнение неудобно, так как требует большого количества шаблонного кода.
Во время поверхностной проверки на равенство объектов вы получаете список свойств (используя Object.keys()) обоих объектов, затем проверяете значения свойств на равенство
function shallowEqual(object1, object2) {
const keys1 = Object.keys(object1);
const keys2 = Object.keys(object2);
if (keys1.length !== keys2.length) {
return false;
}
for (let key of keys1) {
if (object1[key] !== object2[key]) {
return false;
}
}
return true;
}
Но объекты в JavaScript могут быть вложенными. В таком случае, к сожалению, поверхностное равенство не работает.
Глубокое равенство похоже на поверхностное равенство, но с одним отличием. Во время неглубокой проверки, если сравниваемые свойства являются объектами, для этих вложенных объектов выполняется рекурсивная неглубокая проверка на равенство.
function deepEqual(object1, object2) {
const keys1 = Object.keys(object1);
const keys2 = Object.keys(object2);
if (keys1.length !== keys2.length) {
return false;
}
for (const key of keys1) {
const val1 = object1[key];
const val2 = object2[key];
const areObjects = isObject(val1) && isObject(val2);
if (
areObjects && !deepEqual(val1, val2) ||
!areObjects && val1 !== val2
) {
return false;
}
}
return true;
}
function isObject(object) {
return object != null && typeof object === 'object';
}
Выделенная строка areObjects && !deepEqual(val1, val2) указывает на то, что, как только сравниваемые свойства становятся объектами, начинается рекурсивный вызов для проверки равенства вложенных объектов.
Для глубокого сравнения рекомендуется использовать:
isDeepStrictEqual(object1, object2) of Node built-in util module
or _.isEqual(object1, object2) of lodash library.
Простой путь:
function deep_clone(a){
return JSON.parse(JSON.stringify(a));
};
function is_equal(a,b){
return JSON.stringify(a) === JSON.stringify(b);
};
JavaScript не гарантирует порядок ключей. Если они вводятся в одном и том же порядке, этот подход будет работать в большинстве случаев, но он не будет надежным. Кроме того, он вернет false для объектов, которые глубоко равны, но чьи ключи вводятся в другом порядке:
JSON.stringify({ a: 1, b: 2}) === "{"a":1,"b":2}"
JSON.stringify({ b: 2, a: 1}) === "{"b":2,"a":1}"
Обьект с сортировкой:
JSON.stringify(sortMyObj, Object.keys(sortMyObj).sort());
Однако этот метод удаляет любые вложенные объекты, на которые нет ссылок, и не применяется к объектам внутри массивов. Вы также захотите сделать flat для объекта сортировки.
- Ссылочное равенство (с использованием ===, == или Object.is()) определяет, являются ли операнды одним и тем же экземпляром объекта.
- Ручная проверка на равенство требует ручного сравнения значений свойств. Хотя эта проверка требует написания свойств для сравнения вручную, я нахожу этот подход удобным из-за его простоты.
- Когда сравниваемые объекты имеют много свойств или структура объектов определяется во время выполнения, лучшим подходом является использование поверхностной проверки.
- Наконец, если у сравниваемых объектов есть вложенные объекты, лучше всего использовать глубокую проверку на равенство.
31. Модули в JS?
Модуль – это просто файл. Один скрипт – это один модуль.
Так как модули поддерживают ряд специальных ключевых слов, и у них есть ряд особенностей, то необходимо явно сказать браузеру, что скрипт является модулем, при помощи атрибута .
<script type="module">
Браузер автоматически загрузит и запустит импортированный модуль (и те, которые он импортирует, если надо), а затем запустит скрипт.
В модулях всегда используется режим use strict. Например, присваивание к необъявленной переменной вызовет ошибку.
В модуле на верхнем уровне this не определён (undefined).
<script type="module">
a = 5; // ошибка
</script>
Каждый модуль имеет свою собственную область видимости. Другими словами, переменные и функции, объявленные в модуле, не видны в других скриптах.
Модули должны экспортировать функциональность, предназначенную для использования извне. А другие модули могут её импортировать.
В браузере также существует независимая область видимости для каждого скрипта
Если один и тот же модуль используется в нескольких местах, то его код выполнится только один раз, после чего экспортируемая функциональность передаётся всем импортёрам.
На практике, задача кода модуля – это обычно инициализация, создание внутренних структур данных, а если мы хотим, чтобы что-то можно было использовать много раз, то экспортируем это.
Такое поведение позволяет конфигурировать модули при первом импорте. Мы можем установить его свойства один раз, и в дальнейших импортах он будет уже настроенным.
Модули всегда выполняются в отложенном (deferred) режиме, точно так же, как скрипты с атрибутом defer
- загрузка внешних модулей, таких как <'script type="module" src="...">, не блокирует обработку HTML.
- модули, даже если загрузились быстро, ожидают полной загрузки HTML документа, и только затем выполняются.
- сохраняется относительный порядок скриптов: скрипты, которые идут раньше в документе, выполняются раньше.
Как побочный эффект, модули всегда видят полностью загруженную HTML-страницу, включая элементы под ними.
Для не-модульных скриптов атрибут async работает только на внешних скриптах. Скрипты с ним запускаются сразу по готовности, они не ждут другие скрипты или HTML-документ.
Для модулей атрибут async работает на любых скриптах.
Например, в скрипте ниже есть async, поэтому он выполнится сразу после загрузки, не ожидая других скриптов.
Скрипт выполнит импорт (загрузит ./analytics.js) и сразу запустится, когда будет готов, даже если HTML документ ещё не будет загружен, или если другие скрипты ещё загружаются.
Это очень полезно, когда модуль ни с чем не связан, например для счётчиков, рекламы, обработчиков событий.
<!-- загружаются зависимости (analytics.js) и скрипт запускается -->
<!-- модуль не ожидает загрузки документа или других тэгов <script> -->
<script async type="module">
import {counter} from './analytics.js';
counter.count();
</script>
Внешние скрипты с атрибутом type="module" имеют два отличия:
1. Внешние скрипты с одинаковым атрибутом src запускаются только один раз:
<!-- скрипт my.js загрузится и будет выполнен только один раз -->
<script type="module" src="my.js"></script>
<script type="module" src="my.js"></script>
2. Внешний скрипт, который загружается с другого домена, требует указания заголовков CORS. Другими словами, если модульный скрипт загружается с другого домена, то удалённый сервер должен установить заголовок Access-Control-Allow-Origin означающий, что загрузка скрипта разрешена.
<!-- another-site.com должен указать заголовок Access-Control-Allow-Origin -->
<!-- иначе, скрипт не выполнится -->
<script type="module" src="http://another-site.com/their.js"></script>
Это обеспечивает лучшую безопасность по умолчанию.
В реальной жизни модули в браузерах редко используются в «сыром» виде. Обычно, мы объединяем модули вместе, используя специальный инструмент, например Webpack и после выкладываем код на рабочий сервер.
Одно из преимуществ использования сборщика – он предоставляет больший контроль над тем, как модули ищутся, позволяет использовать «голые» модули и многое другое «своё», например CSS/HTML-модули.
Сборщик делает следующее:
- 1. Берёт «основной» модуль, который мы собираемся поместить в <'script type="module"> в HTML.
- 2. Анализирует зависимости (импорты, импорты импортов и так далее)
- 3. Собирает один файл со всеми модулями (или несколько файлов, это можно настроить), перезаписывает встроенный import функцией импорта от сборщика, чтобы всё работало. «Специальные» типы модулей, такие как HTML/CSS тоже поддерживаются.
- В процессе могут происходить и другие трансформации и оптимизации кода:
- Недостижимый код удаляется.
- Неиспользуемые экспорты удаляются («tree-shaking»)
- Специфические операторы для разработки, такие как console и debugger, удаляются.
- Современный синтаксис JavaScript также может быть трансформирован в предыдущий стандарт, с похожей функциональностью, например, с помощью Babel.
- Полученный файл можно минимизировать (удалить пробелы, заменить названия переменных на более короткие и т.д.).
Если мы используем инструменты сборки, то они объединяют модули вместе в один или несколько файлов, и заменяют import/export на свои вызовы. Поэтому итоговую сборку можно подключать и без атрибута type="module", как обычный скрипт:
<!-- Предположим, что мы собрали bundle.js, используя например утилиту Webpack -->
<script src="bundle.js"></script>
Хотя и «как есть» модули тоже можно использовать, а сборщик настроить позже при необходимости.
Атрибут defer сообщает браузеру, что он должен продолжать обрабатывать страницу и загружать скрипт в фоновом режиме, а затем запустить этот скрипт, когда DOM дерево будет полностью построено.
- Скрипты с defer никогда не блокируют страницу.
- Скрипты с defer всегда выполняются, когда дерево DOM готово, но до события DOMContentLoaded.
Атрибут async означает, что скрипт абсолютно независим:
- Страница не ждёт асинхронных скриптов, содержимое обрабатывается и отображается.
- Событие DOMContentLoaded и асинхронные скрипты не ждут друг друга:
- DOMContentLoaded может произойти как до асинхронного скрипта (если асинхронный скрипт завершит загрузку после того, как страница будет готова)
- …так и после асинхронного скрипта (если он короткий или уже содержится в HTTP-кеше)
- Остальные скрипты не ждут async, и скрипты casync не ждут другие скрипты.
- Так что если у нас есть несколько скриптов с async, они могут выполняться в любом порядке. То, что первое загрузится – запустится в первую очередь
У async и defer есть кое-что общее: они не блокируют отрисовку страницы. Так что пользователь может просмотреть содержимое страницы и ознакомиться с ней сразу же.
Но есть и значимые различия:
| Тип | Порядрк | DOMContentLoaded |
|---|---|---|
| async | Порядок загрузки (кто загрузится первым, тот и сработает). | Не имеет значения. Может загрузиться и выполниться до того, как страница полностью загрузится. Такое случается, если скрипты маленькие или хранятся в кеше, а документ достаточно большой. |
| defer | Порядок документа (как расположены в документе). | Выполняется после того, как документ загружен и обработан (ждёт), непосредственно перед DOMContentLoaded. |
- 1. Модуль – это файл. Чтобы работал import/export, нужно для браузеров указывать атрибут <'script type="module">. У модулей есть ряд особенностей:
- Отложенное (deferred) выполнение по умолчанию.
- Атрибут async работает во встроенных скриптах.
- Для загрузки внешних модулей с другого источника, он должен ставить заголовки CORS.
- Дублирующиеся внешние скрипты игнорируются.
- 2. У модулей есть своя область видимости, обмениваться функциональностью можно через import/export.
- 3. В модулях всегда включена директива use strict.
- 4. Код в модулях выполняется только один раз. Экспортируемая функциональность создаётся один раз и передаётся всем импортёрам.
Когда мы используем модули, каждый модуль реализует свою функциональность и экспортирует её. Затем мы используем import, чтобы напрямую импортировать её туда, куда необходимо. Браузер загружает и анализирует скрипты автоматически.
В реальной жизни часто используется сборщик Webpack, чтобы объединить модули: для производительности и других «плюшек»
DOM content loaded32. OOP в JavaScript?
Инкапсуляция включает в себя идею о том, что данные объекта не должны быть напрямую доступны. Нужно вызывать методы вместо прямого доступа к данным. Инкапсуляция позволяет нам скрывать/показывать свойства функций.
Инкапсуляция с использованием замыкания
const createCounter = () => {
// Переменная, определенная в области действия фабрики или конструктора
// является приватной для этой функции.
let count = 0;
return ({
// Любые другие функции, определенные в той же области, являются привилегированными:
// Они имеют доступ к закрытой переменной `count`
// определенной в любом месте их цепочки областей видимости (содержащей области действия функции).
click: () => count += 1,
getCount: () => count.toLocaleString()
});
};
const counter = createCounter();
counter.click();
Абстракция - это способ создания простой модели, которая содержит только важные свойства с точки зрения контекста приложения, из более сложной модели. Иными словами - это способ скрыть детали реализации и показать пользователям только функциональность. Абстракция игнорирует нерелевантные детали и показывает только необходимые. Важно помнить, что мы не можем создать экземпляр абстрактного класса.
Всё программное обеспечение - это абстракция, скрывающая всю тяжелую работу и бездумные детали.
Процесс декомпозиции - это процесс абстракции. Успешная абстракция подразумевает, что результатом является набор независимо полезных и перекомпонованных компонентов.
Само слово означает много форм. Существует много толкований того, что именно оно означает, но идея заключается в способности вызывать один и тот же метод для разных объектов, и при этом каждый объект реагирует по-своему.
Чтобы это произошло полиморфизм использует наследование
Способность объекта принимать различные формы. Например, функция может быть перегружена с тем же именем, но разными параметрами.
Наследование - это механизм базирования объекта или class на другом объекте (наследование на основе прототипа) или class (наследование на основе класса). Мы избегаем необходимости переписывать один и тот же код, а также экономим пространство памяти, используя общие методы.
33. Разница между Prototypal Inheritance vs Classical Inheritance?
Экземпляры обычно создаются с помощью функций-конструкторов с ключевым словом `new`. Наследование классов может использовать или не использовать ключевое слово class из ES6. Классы, какими вы их знаете из таких языков, как Java, технически не существуют в JavaScript. Вместо этого используются функции конструктора. Ключевое слово `class` ES6 преобразует сахар в функцию-конструктор:
class Foo {}
typeof Foo // 'function'
При наследовании классов экземпляры наследуются от схемы (класса) и создают отношения подклассов. Другими словами, вы не можете использовать класс так же, как экземпляр. Вы не можете вызывать методы экземпляра для самого определения класса. Вы должны сначала создать экземпляр, а затем вызывать методы для этого экземпляра.
При прототипном наследовании экземпляры наследуются от других экземпляров. Использование prototype delegate (установка прототипа одного экземпляра для ссылки на другой объект) буквально означает «связывание объектов с другими объектами», или OLOO, как это называет Кайл Симпсон. Используя concatenative inheritance, вы просто копируете свойства объекта-экземпляра в новый экземпляр.
Очень важно, чтобы вы понимали эти различия. Наследование классов благодаря своим механизмам создает иерархию классов как побочный эффект создания подклассов. Эти иерархии приводят к артритическому коду (трудно изменить) и хрупкости (легко сломать из-за побочных эффектов при изменении базовых классов).
- Класс — это "план" обьекта.
- Прототип — это экземпляр объекта.
В JavaScript наследование классов реализовано поверх прототипного наследования, но это не значит, что оно делает то же самое:
Программирование на основе прототипов - это стиль объектно-ориентированного программирования, в котором повторное использование поведения (известное как наследование) выполняется через процесс повторного использования существующих объектов посредством делегирования, которые служат как prototypes. Сторонники программирования на основе прототипов утверждают, что данный стиль поощряет программиста сосредоточиться на поведении некоторого набора примеров и лишь позднее, беспокоиться о классификации этих объектов в архетипические объекты, которые впоследствии используются аналогично классам.
Экземпляры могут состоять из множества различных исходных объектов, что обеспечивает простое выборочное наследование и плоскую иерархию делегирования [[Prototype]].
Наследование классов: класс подобен чертежу — описанию создаваемого объекта. Классы наследуются от классов и создают отношения подклассов
Программирование на основе классов, или же, ориентация на классы, - это стиль объектно-ориентированного программирования (ООП), в котором наследование происходит через определение классов объектов, вместо наследования, которое происходит только через объекты.
Tight Coupling (сильная связанность) относится к волновым эффектам, которые могут произойти с подклассами (дочерние классы), когда вносится изменение в суперкласс (родительский класс).
Наследование классов JavaScript использует цепочку прототипов, чтобы связать дочерний `Constructor.prototype` с родительским `Constructor.prototype` для делегирования. Обычно также вызывается конструктор `super()`. Эти шаги формируют иерархию родитель/потомок с одним предком и создают самую тесную связь, доступную в объектно-ориентированном дизайне.
Object instanceOf function
// class = function
// class это function потому что обращаясь к классу мы обращаемся к конструктору
Наследование — это, по сути, механизм повторного использования кода: способ для разных типов объектов совместно использовать код. То, как вы делитесь кодом, имеет значение, потому что если вы ошибетесь, это может создать много проблем, в частности:
Наследование классов создает таксономии родительских/дочерних объектов в качестве побочного эффекта.
Эти таксономии практически невозможно правильно подобрать для всех новых вариантов использования, а широкое использование базового класса приводит к проблеме хрупкого базового класса, из-за которой их трудно исправить, если вы ошибаетесь. На самом деле наследование классов вызывает много хорошо известных проблем в объектно-ориентированном проектировании:
- Проблема хрупкого базового класса. В двух словах, базовый класс — это класс, от которого вы наследуете, и его часто называют хрупким, потому что изменения в этом классе могут привести к неожиданным результатам в классах, которые от него наследуются.
- Проблема негибкой иерархии (в конце концов, все развивающиеся иерархии не годятся для новых целей)
- Проблема дублирования по необходимости (из-за негибкой иерархии новые варианты использования часто внедряются путем дублирования, а не адаптации существующего кода)
- Проблема гориллы с бананом. Суть в том что класс зависит от задач класса. Вы хотите использовать «Banana», но в конечном итоге импортируете Gorilla, а затем все вещи, от которых зависит Gorilla, такие как другие состояния объектов и окружающая среда, то есть джунгли.
- Классическое наследование требует превосходного предвидения, чтобы избежать проблем неправильного наследования.
34. Проблемы наследования?
Проблема заключается в том, что при использовании наследования классов вы покупаете всю существующую таксономию классов.
Если вы хотите немного адаптироваться к новому варианту использования, вы либо дублируете части существующей таксономии (проблема дублирования по необходимости), либо реорганизуете все, что зависит от существующей таксономии, чтобы адаптировать таксономию к новому использованию. Это является случаем проблемы хрупкого базового класса.
Композиция невосприимчива к обоим.
Наиболее важное различие между наследованием на основе классов и прототипов заключается в том, что класс определяет тип, экземпляр которого может быть создан во время выполнения, тогда как прототип сам по себе является экземпляром объекта.
Дочерний класс ES6 — это еще одно определение типа, которое расширяет родителя новыми свойствами и методами, которые, в свою очередь, могут быть созданы во время выполнения. Дочерний элемент прототипа — это другой экземпляр объекта, который делегирует родительскому элементу любые свойства, не реализованные в дочернем элементе.
Конструктор класса создает экземпляр класса. Конструктор в JavaScript — это обычная старая функция, которая возвращает объект. Единственная особенность конструктора JavaScript заключается в том, что при вызове с ключевым словом new он назначает свой прототип в качестве прототипа возвращаемого объекта. Если это звучит для вас немного запутанно, вы не одиноки — это так, и это большая часть того, почему прототипы плохо изучены.
Чтобы подчеркнуть это, дочерний элемент прототипа не является копией своего прототипа и не является объектом той же формы, что и его прототип. У дочернего элемента есть живая ссылка на прототип, и любое свойство прототипа, не существующее в дочернем элементе, является односторонней ссылкой на свойство прототипа с тем же именем.
let parent = { foo: 'foo' }
let child = { }
Object.setPrototypeOf(child, parent)
console.log(child.foo) // 'foo'
child.foo = 'bar'
console.log(child.foo) // 'bar'
console.log(parent.foo) // 'foo'
delete child.foo
console.log(child.foo) // 'foo'
parent.foo = 'baz'
console.log(child.foo) // 'baz'
В предыдущем примере, хотя child.foo не был определен, он ссылался на parent.foo. Как только мы определили foo для дочернего элемента, child.foo имел значение 'bar', но parent.foo сохранил свое исходное значение. Как только мы удаляем child.foo, он снова ссылается на parent.foo, что означает, что когда мы меняем значение родителя, child.foo ссылается на новое значение.
Ключевым выводом является то, что прототипы не определяют тип; они сами являются экземплярами и могут изменяться во время выполнения со всеми вытекающими последствиями.
// Пример функции конструктора с замыканием
function SecretiveProto() {
const secret = "The Class is a lie!"
this.spillTheBeans = function() {
console.log(secret)
}
}
const blabbermouth = new SecretiveProto()
try {
console.log(blabbermouth.secret)
}
catch(e) {
// TypeError: SecretiveClass.secret is not defined
}
blabbermouth.spillTheBeans() // "The Class is a lie!"
Для сравнения то как это бы проблемно выглядело в классах:
class SecretiveClass {
constructor() {
const secret = "I am a lie!"
this.spillTheBeans = function() {
console.log(secret)
}
}
looseLips() {
console.log(secret)
}
}
const liar = new SecretiveClass()
try {
console.log(liar.secret)
}
catch(e) {
console.log(e) // TypeError: SecretiveClass.secret is not defined
}
liar.spillTheBeans() // "I am a lie!"
И еще одна проблема...
try {
liar.looseLips()
}
catch(e) {
// ReferenceError: secret is not defined
}
Как вы уже догадались, это еще один вопрос с подвохом — опытные разработчики JavaScript стараются избегать и того, и другого, когда могут. Вот хороший способ сделать это с помощью идиоматического JavaScript:
function secretFactory() {
const secret = "Favor composition over inheritance, `new` is considered harmful, and the end is near!"
const spillTheBeans = () => console.log(secret)
return {
spillTheBeans
}
}
const leaker = secretFactory()
leaker.spillTheBeans()
Речь идет не только о том, чтобы избежать уродства, присущего наследованию, или о принудительной инкапсуляции. Подумайте, что еще вы могли бы сделать с помощью secretFactory и лейкера, чего не могли бы легко сделать с помощью прототипа или класса.
Во-первых, вы можете деструктурировать его, потому что вам не нужно беспокоиться о контексте этого:
const { spillTheBeans } = secretFactory()
spillTheBeans() // Favor composition over inheritance, (...)
Обьектно ориентированный ли язык Javascript?
JavaScript имеет мощную поддержку объектно-ориентированного программирования, но использует другую модель наследования (прототипную) по сравнению с большинством популярных объектно-ориентированных языков (использующих классическое наследование). Он также поддерживает процедурный и функциональный стили программирования.
35. Виды наследования в JavaScript? (L)
JavaScript — один из самых выразительных языков программирования, когда-либо созданных. Одной из его лучших особенностей является возможность создавать объекты и наследовать от них без классов и наследования классов.
Delegate prototype — это объект, который служит базой для другого объекта. Когда вы наследуете от delegate prototype, новый объект получает ссылку на прототип.
Когда вы пытаетесь получить доступ к свойству нового объекта, он сначала проверяет собственные свойства объекта. Если он не находит его там, он проверяет `[[Prototype]]` и так далее по цепочке прототипов, пока не вернется к `Object.prototype`, который является корневым delegate для большинства объектов.
Делегирование методов может сохранить ресурсы памяти, потому что вам нужна только одна копия каждого метода, которая будет использоваться всеми экземплярами.
class Greeter {
constructor (name) {
this.name = name || 'John Doe';
}
hello () {
return `Hello, my name is ${ this.name }`;
}
}
const george = new Greeter('George');
const msg = george.hello();
console.log(msg); // Hello, my name is George
Одним из основных недостатков делегирования является то, что оно не очень хорошо подходит для хранения состояния. Если вы попытаетесь сохранить состояние в виде объектов или массивов, изменение любого члена объекта или массива приведет к изменению члена для каждого экземпляра, который разделяет прототип. Чтобы сохранить безопасность экземпляра, вам нужно сделать копию состояния для каждого объекта.
const proto = {
hello () {
return `Hello, my name is ${ this.name }`;
}
};
const greeter = (name) => Object.assign(Object.create(proto), {
name
});
const george = greeter('george');
const msg = george.hello();
console.log(msg);
Конкатенативное наследование — это процесс копирования свойств одного объекта в другой без сохранения ссылки между двумя объектами. Он основан на динамическом расширении объекта JavaScript.
Клонирование — отличный способ сохранить состояние объектов по умолчанию: этот процесс обычно достигается с помощью `Object.assign()`.
const proto = {
hello: function hello() {
return `Hello, my name is ${ this.name }`;
}
};
const george = Object.assign({}, proto, {name: 'George'});
const msg = george.hello();
console.log(msg); // Hello, my name is George
Обычно этот стиль используется для миксинов. Например, вы можете превратить любой объект в генератор событий, смешав прототип `EventEmitter3`:
import Events from 'eventemitter3';
const object = {};
Object.assign(object, Events.prototype);
object.on('event', payload => console.log(payload));
object.emit('event', 'some data'); // 'some data'
Concatenative Inheritance очень мощное, но оно становится еще лучше, когда вы комбинируете его с замыканиями.
Функциональное наследование использует фабричную функцию, а затем добавляет новые свойства с помощью конкатенации.
Функции, созданные с целью расширения существующих объектов, обычно называют функциональными mixins. Основное преимущество использования функций для расширения заключается в том, что оно позволяет использовать замыкание функции для инкапсуляции частных данных. Другими словами, вы можете применять частное состояние.
Немного неудобно навешивать атрибуты на общедоступное свойство, где пользователь может установить или получить их без вызова соответствующих методов. Что мы действительно хотим сделать, так это скрыть атрибуты в приватном режиме.
import Events from 'eventemitter3';
const rawMixin = function () {
const attrs = {};
return Object.assign(this, {
set (name, value) {
attrs[name] = value;
this.emit('change', {
prop: name,
value: value
});
},
get (name) {
return attrs[name];
}
}, Events.prototype);
};
const mixinModel = (target) => rawMixin.call(target);
const george = { name: 'george' };
const model = mixinModel(george);
model.on('change', data => console.log(data));
model.set('name', 'Sam');
/*
{
prop: 'name',
value: 'Sam'
}
*/
Перемещая attrs из общедоступного свойства в частный идентификатор, мы удаляем все его следы из общедоступного API. Единственный способ использовать его сейчас — через привилегированные методы (getters and setter). Привилегированные методы — это любые методы, определенные в области действия замыкания, что дает им доступ к закрытым данным.
Обратите внимание, что в приведенном выше примере у нас есть оболочка `mixinModel()` вокруг фактического функционального миксина, `rawMixin()`. Причина, по которой нам это нужно, заключается в том, что нам нужно установить значение `this` внутри функции, что мы и делаем с помощью `Function.prototype.call()`. Мы могли бы пропустить оболочку и позволить вызывающей стороне сделать это, но это было бы неудобно.
“Favor object composition over class inheritance.” ~ The Gang of Four, “Design Patterns: Elements of Reusable Object Oriented Software”
Наследование классов создает отношения is-a с ограничительными таксономиями, которые в конечном итоге не подходят для новых вариантов использования. Но оказывается, мы обычно используем наследование для отношений «имеет», «использует» или «может сделать».
Композиция больше похожа на педаль эффектов гитары. Хотите что-то, что может делать задержку, тонкое искажение и голос робота? Без проблем! Просто подключите их все:
const effect = compose(delay, distortion, robovoice);
Как вы, вероятно, начинаете понимать, конкатенативное наследование — это секретный соус, который делает возможной композицию объектов в JavaScript, что делает как делегирование прототипов, так и функциональное наследование намного более интересными. Когда большинство людей думают о прототипном ОО в JavaScript, они думают о делегировании прототипа. К настоящему времени вы должны увидеть, что они многое упускают. Delegate prototype — не лучшая альтернатива наследованию классов, лучшаяя это композиция объектов.
36. Способы создания обьектов JavaScript? (L)
// ES6 / ES2015, because 2015.
let mouse = {
furColor: 'brown',
legs: 4,
tail: 'long, skinny',
describe () {
return `A mouse with ${this.furColor} fur,
${this.legs} legs, and a ${this.tail} tail.`;
}
};
Метод Object.create() используется для создания нового объекта с указанным объектом-прототипом и свойствами. Метод Object.create() возвращает новый объект с указанным объектом-прототипом и свойствами.
Object.create(prototype[, propertiesObject])
Используемые параметры:
1. Прототип: это объект-прототип, из которого должен быть создан новый объект.
2. propertiesObject : это необязательный параметр. Он указывает перечисляемые свойства, которые будут добавлены к вновь созданному объекту.
Давайте немного разберем пример. "animal" — это prototype delegate. `mouse` является экземпляром. Когда вы пытаетесь получить доступ к свойству mouse, которого там нет, среда выполнения JavaScript будет искать свойство «animal» (делегат).
let animal = {
animalType: 'animal',
describe () {
return `An ${this.animalType}, with ${this.furColor} fur,
${this.legs} legs, and a ${this.tail} tail.`;
}
};
let mouse = Object.assign(Object.create(animal), {
animalType: 'mouse',
furColor: 'brown',
legs: 4,
tail: 'long, skinny'
});
Object.assing() - Вы передаете целевой объект и любое количество исходных объектов, разделенных запятыми. Он скопирует все перечисляемые собственные свойства путем присвоения из исходных объектов целевым объектам с последним в приоритете. Если есть какие-либо конфликты имен свойств, версия из последнего переданного объекта побеждает.
Object.create() — это функция ES5, которая может присоединять прототипы делегатов без использования конструкторов и ключевого слова new.
Функция-конструктор необязательна для указания поведения инициализации экземпляра объекта и ее обработатки.
Любая функция может создавать и возвращать обьекты. Если это не функция конструктор то она будет называться factory function.
Улучшенные пример приведенного выше примера:
let animal = {
animalType: 'animal',
describe () {
return `An ${this.animalType} with ${this.furColor} fur,
${this.legs} legs, and a ${this.tail} tail.`;
}
};
let mouseFactory = function mouseFactory () {
return Object.assign(Object.create(animal), {
animalType: 'mouse',
furColor: 'brown',
legs: 4,
tail: 'long, skinny'
});
};
let mickey = mouseFactory();
37. Слово new в JavaScript (L)?
Ключевое слово `new` используется для вызова конструктора. Что он на самом деле делает, так это:
- Создает новый экземпляр
- Привязывает this к новому экземпляру
- Устанавливает новому объекту ссылку на [[Prototype]] объект, на который ссылается свойство `prototype` функции-конструктора.
- Устанавливает свойства .constructor нового объекта на конструктор, который был вызван.
- Устанавливает тип объекта после конструктора, который вы заметите в основном в консоли отладки. Например, вы увидите `[Object Foo]` вместо `[Object object]`.
- Позволяет instanceof проверять, является ли ссылка на прототип объекта тем же объектом, на который ссылается constructor.prototype. Оператор JavaScript instanceof используется для проверки типа объекта во время выполнения. Он возвращает логическое значение (true или false). Если возвращаемое значение истинно, то это указывает на то, что объект является экземпляром определенного класса, а если возвращаемое значение ложно, то это не так.
38. Слово instanceof в JavaScript (L)?
Оператор instanceof проверяет, принадлежит ли объект к определённому классу. Другими словами, object instanceof constructor проверяет, присутствует ли объект constructor.prototype в цепочке прототипов object.
> function foo() {}
> var bar = { a: ‘a’};
> foo.prototype = bar; // Object {a: “a”}
> baz = Object.create(bar); // Object {a: “a”}
> baz instanceof foo // true. oops
Последний результат полностью соответствует спецификации JavaScript. Ничего не сломано — просто instanceof не может гарантировать безопасность типов. Его легко обмануть, заставив сообщать как о ложных срабатываниях, так и о ложноотрицательных.
`instanceof` ограничивает возможность повторного использования вашего кода и потенциально может вносить ошибки в программы, использующие ваш код.
39. JavaScript ES6? (L)
ECMAScript 2015 — это шестая редакция стандарта спецификации языка ECMAScript, который используется при реализации JavaScript. Чтобы запустить код ES6 в современном браузере, мы используем BABEL. BABEL — это транспилятор для JavaScript, который позволяет запускать код ES6 в любом браузере.
Одна из самых больших проблем с ключевым словом var заключается в том, что оно позволяет перезаписывать значения переменной.
Если вы не хотите переприсваивать значение переменной, в этом случае мы выбираем ключевое слово let. ключевое слово let в ES6 поддерживает облочную область видимости, в которой область действия переменной ограничена блоком. В случае, если вы объявляете переменную с помощью ключевого слова var, область действия переменной может быть либо внутри функции, либо она может быть глобальной.
Добавлена read-only переменная const.
В ES6 появилась новая функция, называемая литералами шаблонов, для объединения или создания новых строк. Мы используем обратные галочки (``) для встраивания выражения. Placeholders представлены с помощью ${expression}
Оператор Rest позволяет нам представить количество аргументов в виде массива. Оператор Rest обозначается как (...args)
function sum(...theArgs) {
return theArgs.reduce((previous, current) => {
return previous + current;
});
}
console.log(sum(1, 2, 3)); // output 6
Или такой пример:
// Use rest to enclose the rest of specific user-supplied values into an array:
function myBio(firstName, lastName, ...otherInfo) {
return otherInfo;
}
// Invoke myBio function while passing five arguments to its parameters:
myBio("Oluwatobi", "Sofela", "CodeSweetly", "Web Developer", "Male");
// The invocation above will return:
["CodeSweetly", "Web Developer", "Male"]
Текст после оператора rest ссылается на значения, которые вы хотите заключить в массив. Вы можете использовать его только перед последним параметром в определении функции.
Оператор spread (...) помогает вам разложить итерации на отдельные элементы.
Синтаксис spread работает с литералами массивов, вызовами функций и инициализированными объектами свойств для распределения значений итерируемых объектов по отдельным элементам. Таким образом, он делает противоположное оператору rest.
const arrValue = ['My', 'name', 'is', 'Jack'];
console.log(arrValue); // ["My", "name", "is", "Jack"]
console.log(...arrValue); // My name is Jack
Деструктуризация позволяет нам извлекать значения в отдельные переменные из массива или объекта. Это позволяет нам присваивать значение левой стороне присваивания из массива или объекта.
const point = [10, 25, -34];
const [x, y, z] = point;
console.log(x, y, z); // 10 25 -34
const [a, b,,, c] = [1, 2, 3, 4, 5, 6];
console.log(a, b, c); // 1, 2, 5
Стрелочные функции — одна из примечательных особенностей ES6. Его поведение чем-то похоже на обычные функции, но синтаксически отличается. Это делает код очень четким и лаконичным.
this и аргументы внутри стрелочной функции разрешаются лексически, что означает, что они берутся из внешней области действия функции.
У стрелочной функции есть несколько ограничений: ее нельзя использовать в качестве метода объекта, конструктора или функции-генератора.
ES6 представил параметры по умолчанию для создания более гибких функций. Параметры по умолчанию возвращаются, когда параметр отсутствует для функции
function greeting(name = "Anonymous") {
return "Hello " + name;
}
console.log(greeting("Dave")); // Hello Dave
console.log(greeting()); // Hello Anonymous
ES6 предоставляет новый способ создания объектов с использованием ключевого слова class. Использование синтаксиса класса предназначено только для синтаксиса, и это не традиционные классы, это просто функция.
\class myclass{
constructor(){
this.a = 2
}
}
var myobj = new myclass();
console.log(typeof myclass)// function
Модули — одна из самых важных функций в ES6. В ES6 каждый модуль определяется в собственном файле. функции и переменные в одном модуле не видны другому модулю, если они не были экспортированы.
Чтобы экспортировать определенные переменные из модуля, вы просто используете экспорт ключевого слова. Точно так же, чтобы использовать экспортированные переменные в другом модуле, вы используете import.
//random.js
function generateRandom() {
return Math.random();
}
function multiply(a, b) {
return a * b;
}
export { generateRandom, multiply}
//app.js
import { generateRandom, multiply} from 'utility';
console.log(generateRandom()); //logs a random number
console.log(multiply(1, 2)); //2
Promise — это объект, который даст одно значение, которое может быть либо разрешенным, либо неразрешенным значением. Он находится в трех состояниях: выполнено, отклонено или ожидает выполнения. Обратные вызовы могут быть прикреплены к promise, чтобы узнать причину разрешенного или неразрешенного значения.
var wait1000 = new Promise(function(resolve, reject) {
setTimeout(resolve, 1000);
}).then(function() {
console.log("Yay!");
});
40. В чем преимущества использования spread оператора и чем он отличается от rest оператора? (L)
function putDookieInAnyArray(arr) {
return […arr, ‘dookie’];
}
const result = putDookieInAnyArray([‘I’, ‘really’, “don’t”, ‘like’]); // [“I”, “really”, “don’t”, “like”, “dookie”]
const person = {
name: ‘Todd’,
age: 29,
};
const copyOfTodd = { …person };
В свою очередь, rest оператор синтаксиса ES6 позволяет в сокращенном виде указывать неопределенное количество аргументов, передаваемых в функцию. Можно сказать, что он противоположен spread оператору: собирает данные и добавляет их в массив, вместо разделения массива данных. Он используется в аргументах функций, а также при деструктуризации массивов и объектов.
function addFiveToABunchOfNumbers(…numbers) {
return numbers.map(x => x + 5);
}
const result = addFiveToABunchOfNumbers(4, 5, 6, 7, 8, 9, 10); // [9, 10, 11, 12, 13, 14, 15]
const [a, b, …rest] = [1, 2, 3, 4]; // a: 1, b: 2, rest: [3, 4]
const { e, f, …others } = {
e: 1,
f: 2,
g: 3,
h: 4,
}; // e: 1, f: 2, others: { g: 3, h: 4 }
41. Что такое compose?
Композиция - создание сложной функциональности за счет объединения более простых функций. В некотором смысле, композиция - это вложение функций, каждая из которых передает свой результат в качестве входных данных для другой функции. Но вместо того, чтобы создавать неразборчивое количество вложений, мы создадим функцию более высокого порядка - compose(), которая принимает все функции, которые мы хотим объединить, и возвращает нам новую функцию для использования.
Это самый базовый вариант реализации. Обратите внимание: функции в аргументах будут выполняться справа налево. То есть сначала выполняется функция, расположенная справа, и ее результат передается в функцию слева от нее.
var compose = function(f, g) {
return function(x) {
return f(g(x));
};
};
Функция reverseAndUpper сначала переворачивает заданную строку, а затем переводит ее в верхний регистр. Мы можем переписать этот код, используя базовую функцию compose:
var reverseAndUpper = compose(upperCase, reverse);
Давайте реализуем более гибкую функцию compose, которая может включать любое количество других функций и аргументов. Предыдущая функция compose, которую мы рассмотрели, работает только с двумя функциями и принимает только первый переданный аргумент. Мы можем переписать ее так:
var compose = function() {
var funcs = Array.prototype.slice.call(аргументы);
return funcs.reduce(function(f,g) {
return function() {
return f(g.apply(this, аргументы));
};
});
};
Var doSometing = compose(upperCase, reverse, doSomethingInitial);
Например, мы создадим функцию compose, которая получит любое количество функций в качестве аргументов. Для этого мы будем использовать оператор rest.
const compose = (...fns) => x =>
fns.reduceRight(())
Функция compose возвращает функцию, которая ожидает своего начального значения - назовем её x. Отсюда у нас есть массив функций fns. Важно обратить внимание на порядок, в котором мы хотим их вызвать: он идет справа налево. Сначала мы вызываем upperCase, exclaim, а потом repeat. Чтобы сделать это, мы используем reduceRight метод.
const compose = (...fns) => x => fns.reduceRight((acc, fn) => fn(acc), x);
Первым аргументом функции reduceRight являются аккумулятор и текущий элемент. Элемент является нашей функцией. С каждой итерацией мы возвращаем результат вызова накопленного значения текущей функции. Второй аргумент для reduceRight - это наше начальное значение, которым является наш x. Теперь мы можем использовать эту функцию, чтобы легко создавать новые композиции.
Давай создадим функцию withСompose, которая будет составлять наши функции upperCase, exclaim, и repeat. Порядок аргументов нашей функции compose идет справа налево или снизу вверх.
const upperCase = str => str.toUpperCase();
const exclaim = str => `${str}!`;
const repeat = str => `${str} `.repeat(3);
const compose = (...fns) => x => fns.reduceRight((acc, fn) => fn(acc), x);
const withСompose = compose(
repeat,
exclaim,
upperCase
);
console.log(withСompose("I love coding")); // I LOVE CODING! I LOVE CODING! I LOVE CODING!
Функция pipe, которая аналогична compose, но порядок аргументов обратный. pipe также принимает любое количество функций и начальное значение, но на этот раз вызывает reduce. Таким образом, чтобы сделать функцию withСompose с pipe, мы просто меняем порядок аргументов.
const upperCase = str => str.toUpperCase();
const exclaim = str => `${str}!`;
const repeat = str => `${str} `.repeat(3);
const pipe = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x);
const withСompose2 = pipe(
upperCase,
exclaim,
repeat
);
console.log(withСompose2("I love coding")); // I LOVE CODING! I LOVE CODING! I LOVE CODING!
Хоть функция pipe и существует, всё же чаще используется функция compose, применяемая в функциональных языках, поскольку она следует математической модели композиции.
42. Расскажите про Object.defineProperties?
Метод Object.defineProperty - позволяет устанавливать свойствам некоторые настройки (можно ли свойство изменять, удалять и др.)
Object.defineProperty(объект, 'имя свойства', дескриптор);
Дескриптор - это объект, который описывает поведение свойства. В нем могут быть следующие свойства (в скобках указаны значения по умолчанию):
value //значение свойства (undefined)
writable //если true - свойство можно перезаписывать (false)
configurable // если true, то свойство можно удалять (false)
enumerable //если true, то свойство видно в цикле for..in (false)
get //Функции, которая возвращает значение свойства (undefined)
set //Функции, которая записывает значение свойства (undefined);
Если со свойством произвести запрещенное действие, например, попытаться изменить в то время как writable = false, то ничего не произойдет (а в строгом режиме (при указании 'use strict') - будет ошибка). Также запрещено указывать value/writeble если указаны get/set.
var obj = {
val1: 10, //Обычное свойство
}
//Создадим свойство через defineProperty
Object.defineProperty(obj, 'val2', {
value: 10,
configurable: false //нельзя удалять
});
alert(obj.val1) //10
alert(obj.val2) //10
delete obj.val1
delete obj.val2
alert(obj.val1) //undefined
alert(obj.val2) //10
Object.defineProperty в целом полезен для копирования дескрипторов свойств из одного объекта в другой с помощью связанных методов Object.getOwnPropertyNames() и Object.getOwnPropertyDescriptor(), например. при объединении вещей в прототип.
Object.getOwnPropertyDescriptor как раз позволяет достать свойства writable/configurable/enumerable etc.
И, как вы уже упоминали, их можно использовать для геттеров и сеттеров. Синтаксис литерала объекта работает только при создании новых объектов. Чтобы создать новые геттеры/сеттеры в существующем объекте (например, прототипе), вы должны использовать Object.defineProperty() или скопировать дескрипторы, как упоминалось выше.
Полезно, чтобы избежать перечисления через Object.keys(), for... in циклы, добавление свойств в подклассы массива и тому подобное. Это очень важно при добавлении полифилов во встроенные прототипы, особенно Object.prototype, поскольку вы не хотите, чтобы ваши добавленные методы внезапно отображались в циклах, поскольку это может нарушить работу другого кода, который не выполняет проверку .hasOwnProperty().
[[Writable]], [[Configurable]] позволяют создавать свойства только для чтения, которые нельзя случайно перезаписать или удалить. Это отлично подходит для библиотек.
Object.freeze() / .seal() / .preventExtensions() расширяют этот тип защиты до такой степени, что вы можете защитить объекты в достаточной степени, чтобы создать несколько безопасных песочниц javascript eval, защищая прототипы встроенных объектов.
Метод Object.freeze() замораживает объект: это значит, что он предотвращает добавление новых свойств к объекту, удаление старых свойств из объекта и изменение существующих свойств или значения их атрибутов перечисляемости, настраиваемости и записываемости. В сущности, объект становится эффективно неизменным. Метод возвращает замороженный объект.
Метод Object.seal() запечатывает объект, предотвращая добавление новых свойств к объекту и делая все существующие свойства не настраиваемыми. Значения представленных свойств всё ещё могут изменяться, поскольку они остаются записываемыми.
Метод Object.preventExtensions() предотвращает добавление новых свойств к объекту (то есть, предотвращает расширение этого объекта в будущем).
43. Что такое IIFE?
IIFE — это хороший способ защитить область действия вашей функции и переменные внутри нее. Любые переменные внутри IIFE не видимы для внешнего мира. Cуть паттерна в том, чтобы взять функцию и превратить её в выражение, а затем тут же его запустить.
Реально важной и полезной фичей IIFE является то, что с их помощью вы можете отдавать значение, которое будет назначено переменной.
var result = (function() {
return "From IIFE";
}());
alert(result); // alerts "From IIFE"
IIFE не только могут отдавать значения, но ещё и брать аргументы во время своего вызова.
(function IIFE(msg, times) {
for (var i = 1; i <= times; i++) {
console.log(msg);
}
}("Hello!", 5));
Пример модульного паттерна, который раскрывают всю мощь совместного применения замыканий и IIFE функций.
var Sequence = (function sequenceIIFE() { //обьект синглтон sequence
// приватная переменная для хранения значения счетчика
var current = 0;
// объект, возвращаемый IIFE
return {
getCurrentValue: function() {
return current;
},
getNextValue: function() {
current = current + 1;
return current;
}
};
}());
console.log(Sequence.getNextValue()); // 1
console.log(Sequence.getNextValue()); // 2
console.log(Sequence.getCurrentValue()); // 2
Так как переменная current является приватной в IIFE, то никто кроме функций, имеющих доступ к IIFE через замыкание, не имеет к ней доступа.
Указывает webpack добавить оболочку IIFE вокруг выпускаемого кода.
module.exports = {
//...
output: {
iife: true,
},
};
как указано в ссылке ниже. Function declarations загружаются до выполнения любого кода, в то время как Function expressions загружаются только тогда, когда интерпретатор достигает этой строки кода.
webpack обертывает все модули функциональными выражениями и объединяет их в файлы IIFE, чтобы убедиться, что после загрузки файла IIFE выполнит и подпишет модули как function expressions, «чтобы избежать их выполнения и перегрузить браузер ненужными процессами в дополнение к получение выгоды от повторного использования».
Да, функциональные выражения могут иметь имена. Самое обыденное и всюду объясняемое использование проименованных функциональных выражений — это рекурсия. Пока что не беспокойтесь о них, вы вполне можете понять IIFE и так.
var fibo = function fibonacci() {
// тут вы можете использовать "fibonacci()"
};
// fibonacci() не сработает, а fibo() да.
Разница тут в том, что функциональное выражение имеет имя “fibonacci”, которое можно использовать внутри самого выражения рекурсивным способом. (Тут вообще есть много всего интересного, например то, что имя функции всплывает в стектрейсе и т.п.).
Любое функциональное выражение с IIFE автоматически и единожды запускается и сразу исчезает.
let vlad = function() {
alert("Hello from IIFE!"); //allert
}();
console.log(vlad) // undefined
Анонимная функция - функция, которая была объявлена без какой-либо функции с именем идентификатора, чтобы ссылаться на нее. Обычно она недоступна после ее первоначального создания. Эти функции создаются во время выполнения.
44. Что такое Object.assign()?
Object.assign(target, ...sources)
- target — это Объект, к которому вы хотите присвоить. В нашем случае экземпляр объекта класса.
- sources — это те источники), из которых будут поступать данные. В нашем случае один источник. Запись базы данных (набор).
Свойства в целевом объекте будут перезаписаны свойствами в источниках, если они имеют одинаковый ключ. Свойства более поздних источников аналогичным образом перезапишут более ранние.
Метод Object.assign() копирует только перечисляемые и собственные свойства из исходного объекта в целевой объект. Он использует [[Get]] для источника и [[Set]] для цели, поэтому он будет вызывать геттеры и сеттеры. Поэтому он присваивает свойства, а не просто копирует или определяет новые свойства.
Patterns and algorithms:
1. Big O notation?
Big O нотация определяет эффективность алгоритма
- O - относится к порядку функции или скорости ее роста
- n — длина сортируемого массива
При рассмотрении многих наиболее часто используемых алгоритмов сортировки рейтинг O (n log n) в целом является лучшим, которого можно достичь. Алгоритмы, которые работают с этим рейтингом, включают быструю сортировку, сортировку кучей и сортировку слиянием. Быстрая сортировка является стандартом и используется по умолчанию почти во всех языках программного обеспечения.
Поиск значения, когда вы знаете, что ключ (объекты) или индекс (массивы) всегда выполняется за один шаг и является постоянным временем.
Если вы знаете, на какой стороне массива искать элемент, вы экономите время, срезая другую половину.
//You decrease the amount of work you have to do with each step
function thisOld(num, array){
var midPoint = Math.floor( array.length /2 );
if( array[midPoint] === num) return true;
if( array[midPoint] < num ) --> only look at second half of the array
if( array[midpoint] > num ) --> only look at first half of the array
//recursively repeat until you arrive at your solution
}
thisOld(29, sortedAges) // returns true
//Notes
//There are a bunch of other checks that should go into this example for it to be truly functional, but not necessary for this explanation.
//This solution works because our Array is sorted
//Recursive solutions are often logarithmic
//We'll get into recursion in another post!
Простейший пример — бинарный поиск. Если массив отсортирован, мы можем проверить, есть ли в нём какое-то конкретное значение, методом деления пополам. Проверим средний элемент, если он больше искомого, то отбросим вторую половину массива — там его точно нет. Если же меньше, то наоборот — отбросим начальную половину. И так будем продолжать делить пополам, в итоге проверим log n элементов.
Сколько раз нужно разделить n на 2, чтобы получить 1? log(n). Итак, у нас есть уровни log(n). Таким образом, наше общее время выполнения равно O(n (работа на уровень) * log(n) (количество уровней)), n log(n) раз.
Вы должны просмотреть каждый элемент в массиве или списке, чтобы выполнить задачу. Одиночные циклы for почти всегда имеют линейное время. Кроме того, методы массива, такие как indexOf, также являются линейными по времени. Вы просто абстрагировались от процесса зацикливания
//The number of steps you take is directly correlated to the your input size
function addAges(array){
var sum = 0;
for (let i=0 ; i < array.length; i++){ //has to go through each value
sum += array[i]
}
return sum;
}
addAges(sortedAges) //133
Чтобы получить такую сложность вложенный цикл должен идти по независимой структуре данных.
В текущем примере считается сумма чисел внутренних под массивов, размер и содержимое которых не зависят от внешнего массива. Поэтому сложность O(A+B).
Вложенные циклы for имеют квадратичное время, потому что вы выполняете линейную операцию внутри другой линейной операции (или n*n = n²).
//The number of steps you take is your input size squared
function addedAges(array){
var addedAge = [];
for (let i=0 ; i < array.length; i++){ //has to go through each value
for(let j=i+1 ; j < array.length ; j++){ //and go through them again
addedAge.push(array[i] + array[j]);
}
}
return addedAge;
}
addedAges(sortedAges); //[ 46, 49, 51, 53, 51, 53, 55, 56, 58, 60 ]
//Notes
//Nested for loops. If one for loop is linear time (n)
//Then two nested for loops are (n * n) or (n^2) Quadratic!
Экспоненциальное время обычно подходит для ситуаций, когда вы не так много знаете, и вам нужно попробовать все возможные комбинации или перестановки.
//The number of steps it takes to accomplish a task is a constant to the n power
//Thought example
//Trying to find every combination of letters for a password of length n
Доп информация по времени
O(1) - затраты времени не зависят от размера задачи
O(log(n)) - при увеличении размера задачи вдвое, затраты времени меняются на постоянную величину
O(n) - при увеличении размера задачи в 2 раза, затраты времени возрастут тоже в два раза
O(n^2) - при увеличении размера задачи в 2 раза, затраты времени возрастут примерно в четыре раза
O(n*log(n)) - при увеличении задачи в два раза, затраты времени возрастут в два раза, плюс некоторая прибавка, относительный вклад которой уменьшается с ростом n. При малых n может вносить очень большой вклад. O(n*log(n)) начинает расти как квадрат при малых n, но потом рост замедляется почти до линейного
O(n^p) - полиномиальный алгоритмы, остающиеся мечтой для некоторых задач.
O(a^n), O(n!), O(n^n) - неполиномиальные алгоритмы, в порядке ускорения увеличения затрат времени
2. Decorator pattern?
Декоратор – это обёртка вокруг функции, которая изменяет поведение последней. Основная работа по-прежнему выполняется функцией.
Обычно безопасно заменить функцию или метод декорированным, за исключением одной мелочи. Если исходная функция предоставляет свойства, такие как func.calledCount или типа того, то декорированная функция их не предоставит. Потому что это обёртка. Так что нужно быть осторожным в их использовании. Некоторые декораторы предоставляют свои собственные свойства.
Декораторы можно рассматривать как «дополнительные возможности» или «аспекты», которые можно добавить в функцию. Мы можем добавить один или несколько декораторов. И всё это без изменения кода оригинальной функции!
Для реализации cachingDecorator мы изучили методы:
func.call(context, arg1, arg2…) – вызывает func с данным контекстом и аргументами. func.apply(context, args) – вызывает func, передавая context как this и псевдомассив args как список аргументов.В основном переадресация вызова выполняется с помощью apply:
let wrapper = function(original, arguments) {
return original.apply(this, arguments);
};
Мы также рассмотрели пример заимствования метода, когда мы вызываем метод у объекта в контексте другого объекта. Весьма распространено заимствовать методы массива и применять их к arguments. В качестве альтернативы можно использовать объект с остаточными параметрами ...args, который является реальным массивом.
3. Observer pattern?
В JavaScript часто возникает проблема. Вам нужен способ обновлять части страницы в ответ на определенные события с данными, которые они предоставляют. Скажем, например, пользовательский ввод, который вы затем проецируете на один или несколько компонентов. Это приводит к большому количеству push-and-pull в коде, чтобы все было синхронно.
EventObserver
│
├── subscribe: adds new observable events
│
├── unsubscribe: removes observable events
|
└── broadcast: executes all events with bound data
Начните с пустого списка наблюдаемых событий и делайте это для каждого нового экземпляра.
class EventObserver {
constructor() {
this.observers = [];
}
}
subscribe(fn) {
this.observers.push(fn);
}
Возьмите список наблюдаемых событий и поместите новый элемент в массив. Список событий — это список функций обратного вызова.
unsubscribe(fn) {
this.observers = this.observers.filter((subscriber) => subscriber !== fn);
}
Отфильтруйте из списка все, что соответствует функции обратного вызова. Если совпадений нет, обратный вызов остается в списке. Фильтр возвращает новый список и переназначает список наблюдателей.
Для вызова всех events:
broadcast(data) {
this.observers.forEach((subscriber) => subscriber(data));
}
Перебираем список наблюдаемых событий и выполняем все обратные вызовы. При этом мы получаем необходимое отношение «один ко многим» с подписанными событиями. Мы передаем параметр data, который связывает колбэки.
class EventObserver {
constructor() {
this.observers = [];
}
subscribe(fn) {
this.observers.push(fn);
}
unsubscribe(fn) {
this.observers = this.observers.filter((subscriber) => subscriber !== fn);
}
broadcast(data) {
this.observers.forEach((subscriber) => subscriber(data));
}
}
const getWordCount = (text) => text ? text.trim().split(/\s+/).length : 0;
const wordCountElement = document.createElement('p');
wordCountElement.className = 'wordCount';
wordCountElement.innerHTML = 'Word Count: <strong id="blogWordCount">0</strong>';
document.body.appendChild(wordCountElement);
const blogObserver = new EventObserver();
blogObserver.subscribe((text) => {
const blogCount = document.getElementById('blogWordCount');
blogCount.textContent = getWordCount(text);
});
const blogPost = document.getElementById('blogPost');
blogPost.addEventListener('keyup', () => blogObserver.broadcast(blogPost.value));
The observer pattern может помочь вам решить реальные проблемы в JavaScript. Он решает извечную проблему синхронизации множества элементов с одними и теми же данными. Как это часто бывает, когда браузер запускает определенные события.
4. Pub/sub pattern?
Шаблон Pub/sub включает промежуточное программное обеспечение, которое также называется брокером Pub/sub. Брокер Pub/sub обрабатывает взаимодействие между издателями и подписчиками. Издатели публикуют содержимое или публикации брокеру Pub/sub, и он обрабатывает доставку этого содержимого соответствующему подписчику.
Брокер Pub/sub также обеспечивает свободную развязку издателей и подписчиков и поддерживает отношения «многие ко многим» между издателями и подписчиками.
Таким образом, в отличие от шаблона наблюдателя, шаблон Pub/sub допускает наличие нескольких издателей и нескольких подписчиков.
В шаблоне Pub/sub издатель публикует содержимое в теме, и заинтересованные подписчики получают доступ к этому содержимому, отправляя подписки брокеру Pub/sub, чтобы подписаться на эту тему. Кроме того, в отличие от шаблона наблюдателя, и издателям, и подписчикам не нужно знать друг о друге.
Существует множество реализаций шаблонов pub/sub, например, IBM Websphere MQ, RabbitMQ и RocketMQ, Apache Kafka, Google Cloud Pub/Sub и Pushy.
Наша реализация pub/sub состоит из класса pub/sub, который содержит массив событий, который используется для хранения списка всех опубликованных событий.
Кроме того, класс pub/sub имеет метод подписки, который обрабатывает все взаимодействия между издателями и подписчиками.
class Pubsub {
constructor() {
this.events = {};
}
subscription (eventName, func) {
return {
subscribe: () => {
if (this.events[eventName]) {
this.events[eventName].push(func);
console.log(`${func.name} has subscribed to ${eventName} Topic!`)
} else {
this.events[eventName] = [func];
console.log(`${func.name} has subscribed to ${eventName} Topic!`)
}
},
unsubscribe: () => {
if(this.events[eventName]){
this.events[eventName] = this.events[eventName].filter((subscriber) => subscriber !== func);
console.log(`${func.name} has unsubscribed from ${eventName} Topic!`)
}
}
}
}
publish(eventName, ...args) {
const funcs = this.events[eventName];
if (Array.isArray(funcs)) {
funcs.forEach((func) => {
func.apply(null, args);
});
}
}
}
const speak = (param) => {
console.log(`I am ${param}`);
};
const greetAll = (x, y, z) => {
console.log(`Hello ${x}, ${y}, ${z}`);
};
const pubsub = new Pubsub();
pubsub.subscription("greet", greetAll).subscribe() // prints greetAll has subscribed to greet Topic!
pubsub.subscription("sayName", speak).subscribe() // prints speak has subscribed to sayName Topic!
pubsub.subscription("sayName", greetAll).unsubscribe() // prints greetAll has unsubscribed from sayName Topic!
pubsub.publish("greet", "Lawrence Eagles", "John Doe", "Jane Doe"); // prints Hello Lawrence Eagles, John Doe, Jane Doe
pubsub.publish("sayName", "Lawrence Eagles"); // prints I am Lawrence Eagles
В нашем небольшом примере выше метод подписки возвращает объект, содержащий метод подписки, используемый для обработки подписок, и метод отмены подписки, который обрабатывает отказы от подписки.
Наконец, наш класс pub/sub содержит метод публикации, который принимает переменное количество аргументов и вызывает все функции, которые подписаны на указанное событие с этими аргументами, с помощью применения.
- Слабая развязка шаблона Pub/sub делает его пригодным для решения многих задач разработки программного обеспечения. Он обладает высокой масштабируемостью и хорошо подходит для распределенных архитектур, таких как микросервисы.
- Pub/sub отлично подходит для создания уведомлений о событиях, распределенного кэширования, распределенного ведения журналов и систем с несколькими источниками данных.
5. Отличия Observable от Pub/sub pattern?
Pub-sub паттерн является одной из вариаций паттерна Observer. Исходя из названия в паттерне выделяют два компонента Publisher (издатель) и Subscriber (подписчик). В отличие от Observer, связь между объектами осуществляется посредством канала связи Event Channel (шины событий).
Publisher кидает свои события в Event Channel, а Subscriber подписывается на нужное событие и слушает его на шине, что обеспечивает отсутствие прямой связи между подписчиком и издателем.
- отсутствие прямой связи между объектами
- объекты сигнализируют друг другу событиями, а не состояниями объекта
- возможность подписываться на различные события на одном объекте с различными обработчиками
Одной из наиболее известных реализаций паттерна pub-sub является Backbone, AmplifyJs и др. DOM, в некоторой степени тоже реализует модель pub-sub.
6. Mediator pattern?
На основе pub-sub строится работа паттерна Mediator, который позволяет наладить коммуникацию между различными компонентами системы. Mediator представляет собой глобальный объект в системе, о котором знают все компоненты системы, при этом компонент может выступать как слушателем события, так и издателем другого события, таким образом налаживая коммуникацию между объектами системы.
Если провести аналогию, то Mediator это городская АТС, в которую приходят входящие и исходящие вызовы от абонентов, а доходят они строго до нужного абонента. Но как мы знаем у телефонной сети есть недостаток — на новый год она может оказаться перегруженной огромным количеством звонков и перестать доставлять вызова абонентам. Тоже самое может произойти и с Mediator, когда он не справится с потоком событий.
Mediator особенно полезен в тех случаях, когда наблюдаются множественные однонаправленные или двунаправленные связи между различными компонентами системы. Особенно паттерн полезен, когда в приложении имеются вложенные друг в друга компоненты системы (например, дочерние композиционные элементы), чтобы не было необходимости пробрасывать callbacks используя модель всплытия события изнутри наружу. Достаточно предоставить Mediator внутреннему компоненту, который опубликует свое событие, а другие компоненты узнают об этом событии.
Mediator паттерн довольно успешно реализован в Backbone — сам глобальный объект Backbone можно использовать в качестве Mediator, либо унаследоваться от Backbone.Events.
TypeScript:
1. Что такое TypeScript и зачем использовать его вместо JavaScript??
TypeScript (TS) – это надмножество JavaScript (JS), среди основных особенностей которого можно отметить возможность явного статического назначения типов, поддержку классов и интерфейсов. Одним из серьёзных преимуществ TS перед JS является возможность создания, в различных IDE, такой среды разработки, которая позволяет, прямо в процессе ввода кода, выявлять распространённые ошибки. Применение TypeScript в больших проектах может вести к повышению надёжности программ, которые, при этом, можно разворачивать в тех же средах, где работают обычные JS-приложения.
- TypeScript поддерживает современные редакции стандартов ECMAScript, код, написанный с использованием которых, компилируется с учётом возможности его выполнения на платформах, поддерживающих более старые версии стандартов. Это означает, что TS-программист может использовать возможности ES2015 и более новых стандартов, наподобие модулей, стрелочных функций, классов, оператора spread, деструктурирования, и выполнять то, что у него получается, в существующих средах, которые пока этих стандартов не поддерживают.
- TypeScript – это надстройка над JavaScript. Код, написанный на чистом JavaScript, является действительным TypeScript-кодом.
- TypeScript расширяет JavaScript возможностью статического назначения типов. А именно, она включает в себя:
- интерфейсы
- перечисления
- гибридные типы
- обобщённые типы (generics)
- типы-объединения
- типы-пересечения
- модификаторы доступа
- Применение TypeScript, в сравнении с JavaScript, значительно улучшает процесс разработки. Дело в том, что IDE, в реальном времени, получает сведения о типах от TS-компилятора.
- При использовании режима строгой проверки на null (для этого применяется флаг компилятора --strictNullChecks), компилятор TypeScript не разрешает присвоение null и undefined переменным тех типов, в которых, в таком режиме, использование этих значений не допускается.
- Для использования TypeScript нужно организовать процесс сборки проекта, включающий в себя этап компиляции TS-кода в JavaScript. Компилятор может встроить карту кода (source map) в сгенерированные им JS-файлы, или создавать отдельные .map-файлы. Это позволяет устанавливать точки останова и исследовать значения переменных во время выполнения программ, работая непосредственно с TypeScript-кодом.
- TypeScript — это опенсорсный проект Microsoft, выпущенный под лицензией Apache 2. Инициатором разработки TypeScript является Андерс Хейлсберг. Он причастен к созданию Turbo Pascal, Delphi и C#.
- Объектно-ориентированное программирование: TypeScript поддерживает концепции объектно-ориентированного программирования, такие как интерфейсы, наследование, классы и многое другое.
- Компиляция: в отличие от JavaScript, который является интерпретативным языком, TypeScript компилирует код для вас и находит ошибки компиляции, что упрощает отладку.
TypeScript добавляет необязательную статическую типизацию и языковые функции, такие как классы и модули. Важно знать, что все эти расширенные функции добавляют JavaScript нулевую стоимость. TypeScript — это исключительно инструмент времени компиляции. После компиляции у вас остается простой идиоматический JavaScript. TypeScript — это язык для разработки JavaScript в масштабе приложения.
2. Unknown type TS?
Неизвестный тип является типобезопасным аналогом любого типа. Неизвестному типу можно присвоить что угодно, но неизвестный тип нельзя присвоить ничему, кроме самого себя и any, без выполнения утверждения типа сужения на основе потока управления. Вы не можете выполнять какие-либо операции над переменной неизвестного типа без предварительного утверждения или сужения ее до более конкретного типа.
Рассмотрим следующий пример. Мы создаем переменную foo неизвестного типа и присваиваем ей строковое значение. Если мы попытаемся присвоить эту неизвестную переменную строковой переменной bar, компилятор выдаст ошибку.
let foo: unknown = "Akshay";
let bar: string = foo; // Type 'unknown' is not assignable to type 'string'.(2322)
Вы можете сузить переменную неизвестного типа до чего-то определенного, выполнив проверки typeof или сравнения или используя type guards. Например, мы можем избавиться от вышеуказанной ошибки,
let foo: unknown = "Akshay";
let bar: string = foo as string;
Другой пример
let vakr:unknown = 1011
let vakr2:any = vakr
let vakr3:number = vakr as number
let vakr4:number;
if( typeof vakr == 'number'){
vakr4 = vakr
console.log(vakr4)
}
Вы должны использовать unknown, если вы не знаете, какой тип ожидать заранее, но хотите назначить его позже, и any не будет работать.
3. Never VS Void type TS?
Как следует из названия, тип never представляет собой тип значений, которые никогда не встречаются. Например, функция, которая никогда не возвращает значение или всегда выдает исключение, может пометить тип возвращаемого значения как never.
function error(message: string): never {
throw new Error(message);
}
Вы можете задаться вопросом, зачем нам нужен тип «never», когда у нас уже есть «void». Хотя оба типа выглядят одинаково, они представляют собой два совершенно разных понятия.
Функция, которая не возвращает значение, неявно возвращает значение undefined в JavaScript. Следовательно, даже если мы говорим, что он ничего не возвращает, он возвращает «undefined». Обычно в таких случаях мы игнорируем возвращаемое значение. Предполагается, что такая функция имеет возвращаемый тип void в TypeScript.
// This function returns undefined
function greet(name: string) {
console.log(`Hello, ${name}`);
}
let greeting = greet("David");
console.log(greeting); // undefined
Напротив, функция, имеющая тип never, никогда не возвращает значений. Она также не возвращает значение undefined. Есть 2 случая, когда функции должны никогда не возвращать тип:
В случае бесконечного цикла - while(true){} .
В случае если функция прокидывает ошибку function foo(){throw new Exception('Error message')}
4. Enums TS?
Перечисления позволяют нам создавать именованные константы. Это простой способ дать более понятные имена числовым значениям констант. Перечисление определяется ключевым словом enum, за которым следует его имя и члены.
Рассмотрим следующий пример, определяющий перечисление Team с четырьмя значениями.
enum Team {
Alpha,
Beta,
Gamma,
Delta
}
let t: Team = Team.Delta;
По умолчанию перечисления начинают нумерацию с 0. Вы можете переопределить нумерацию по умолчанию, явно назначив значения ее элементам.
TypeScript также позволяет создавать перечисления со строковыми значениями следующим образом:
enum Author {
Anders = "Anders",
Hejlsberg = "Hejlsberg"
};
Тело перечисления состоит из нуля или более элементов. Элементы перечисления имеют численное значение ассоциированное с именем, и могут быть либо константой, либо могут быть вычислены. Элемент перечисления считается константой, если:
- Он не имеет инициализатора, предшествующий элемент перечисления был константой. В этом случае значение текущего элемента перечисления будет равняться значению предшествующего элемента перечисления плюс единица. Исключением является первый элемент перечисления. Если элемент не имеет инициализатора, ему присваивается значение 0.
- Элемент перечисления инициализирован с константным выражением перечисления. Константное выражение перечисления - это подмножество TypeScript выражений, которое может быть полностью вычислено во время компиляции.
Во всех остальных случаях считается, что элемент перечисления вычисляем.
enum FileAccess {
// константные элементы
None,
Read = 1 << 1,
Write = 1 << 2,
ReadWrite = Read | Write,
// вычисляемые элементы
G = "123".length
}
5. Optional chaining и Nullish coalescing в TS?
Необязательная цепочка позволяет вам обращаться к свойствам и вызывать методы для них в цепочке. Вы можете сделать это с помощью оператора ‘?.’.
TypeScript немедленно прекращает выполнение какого-либо выражения, если оно сталкивается со значением «null» или «undefined», и возвращает «undefined» для всей цепочки выражений.
Используя необязательную цепочку, опишем следующее выражение:
let x = foo === null || foo === undefined ? undefined : foo.bar.baz();
Можно записать как:
let x = foo?.bar.baz();
Если person.firstName отсутствует, мы вернемся к значению John.
const name = person.firstName ?? 'John';
6. Типы в TS?
TypeScript добавляет слой статических типов. Если тип у переменной указан, то в приложении в дальнейшем он не меняется.
Типы существую при компиляции или проверке исходного кода.
Каждое место хранения данных имеет статический тип. (Аргумент, переменная...)
let str: string;
str = 'abc';
В большинств случаев TypeScript корректно определяет тип. Это избовляет от необходимости везде писать аннотацию.
- Аннотация бывает сложно составной
- При необходимости использовании нескольких варинтов типов для переменной используется оператор |
let myScore: number | string //union
При частном использовании составного типа, ему задают псевдоним. Имена псевдонимов, по соглашению, начинаются с большой буквы.
type Score = number | string
const myScore: Score = 7
Когда функция возвращает тип any и мы хотим утчонить значение.
Когда мы обьявляем и присваеваем переменную в разных местах.
Когда мы хотим, чтобы тип был сложносоставной и не определялся автоматически.
7. Типизация массивов в TS?
const arr: Array<number> = []
const arr: number[] = []
const arr2: string[][] = [] //Массив массивов или вложенный массив
arr2.push(['a', 'b'])
type MyType = (string | number)
const arr3: (string | number)[] = []
Кортеж - сущность с фиксированным числом элементов. Не может иметь большее или меньшее число элементов, чем зафиксированное.
const typle1: [string, boolean, number] = ['abc', true, 0] //в массиве будет только 3 элемента, их типы соотвествуют по указанному порядку
Tuple хорошо подходит под работу с CSV файлами. (Coma separated value)
type SimpleCsv = [string, string, number]
const example: SimpleCsv[] = [
['str', 'str', 32]
]
8. Работа с обьектами в TS?
Type для обьекта.
type MyObj = {
a: number;
b: number;
c: string;
}
const obj: MyObj = { a: 1, b: 2, c: 'sdsd' }
const obj1: object = { };
Interface для обьекта
inteface MyObject {
readonly a: number; // <--- readonly указывает, что ключ только для чтения и не может быть перезаписан
b: number;
c?: string; // <--- необязательное поле благодаря ?
print?: () => number; // <--- при такой декларации можно делать опциональной
print2(): number; // более современный метод декларации
[key: string]: string | number;
}
Обьединение interface
interface Person {
name: stirng;
}
interface Person {
age: number;
}
const john: Person = {
name: 'John',
age: 40
}
// Подводные камне с обьединением
inteface Account {
login: string;
}
const myAcc: Account = {
login: 'michey'
}
// выдастся ошибка Type '{login: string;}' is missing the following properties from type 'Account': displayName ,id, rpDisplayName
// потому что в lib.dom.d.ts есть свое определение типа Account (DOM API)
// Из за одинаковых имен интерфейсов возможны проблемы с пересечением.
// Решение проблемы добавлять I в началае имени интерфейсов IAccount
Обьединение разных интерфейсов в друг друга
interface IAccount {
email: string;
login: string;
active: boolean;
}
interface IDeveloper extends IAccount, IPerson .... etc { // обьединит все ключи из указанных интерфейсов
skills: string[],
level: string;
}
const john: IDeveloper = {
name: 'John',
age: 40,
email: 'pet@mail.ru',
login: 'pet14log',
active: true,
skills: ['tomador', 'pomador', 'JavaSquirt'],
level: '80lvl',
}
Обьединять можно и типы
type Person = {
}
type MyAccount = {
}
type MyDeveloper = {
}
type FrontendDeveloper = Person & MyAccount & MyDeveloper;
const devArr: FrontendDeveloper[] = []
9. Работа с функциями в TS?
Обьявление функций
const fn1 = (num: number): string => {
return String(num)
}
function fn2(cb: () => string) {}
type Callback = (num: number) => string;
function fn2(cb?: Callback) { // обязательна проверка на то передается параметр или нет
if(cb === undefined){
cb = String;
}
cb(12)
}
Параметры по умолчанию
function createPoint(x = 0, y = 0) { }
function createPoint(x: number = 0, y: number = 0): [number, number] {
return [x, y]
}
Список аргументов
function createPoint(...nums: number[]): string {
return nums.join('-')
}
Входной аргумент обьект
interface Printable {
label: string;
}
// Если функция ни чего не возвращает или в какой то момент явно указывается return undefined то указываем :void
function printReport(obj: Printable): void {
return undefined
}
// drink переданны в printReport будет работать если он не расширяется Printable, потому что входящий obj: Printable это как минимальный сценарий для входного обьекта. В drink есть label. Этого достаточно.
// так функции становятся более универсальными и не надо создавать отдельную функцию под каждый входящий обьект
const drink = {
label: 'pepsi',
price: 90,
}
printReport(drink)
Повторное обьявление функций для указания разных входный аргументов для одной и той же функции. (overload)
- Имя функции такое же
- Количество параметров отличается в каждой перегрузке
- Если количество параметров одинаковое, их тип должен быть разным.
- Все перегрузки должны иметь одинаковый тип возвращаемого значения.
- Наличие не общих аргументов надо проверять.
function pickCard(x: number): {suit: string; card: number} // нет описания самой функции
function pickCard(x: {suit: string; card: number}[]): number // нет описания самой функции
function pickCard(x): any {
if(typeof x === 'object') {
return number
} else if(typeof x === 'number') {
return {}
}
}
В TypeScript мы можем указать функцию, которую можно вызывать по-разному, написав сигнатуры перегрузки.
- При определении функции мы пишем несколько сигнатур перегрузки, которые клиенты могут вызывать для вызова функции.
- После этих сигнатур перегрузки мы пишем сигнатуру реализации непосредственно перед реализацией функции. Клиент не может вызвать функцию, используя сигнатуру реализации.
- Сигнатура реализации должна быть совместима с реализацией функции, а также сигнатурами перегрузки.
- Сигнатура реализации всегда должна быть последней в списке и может принимать любой тип или тип объединения в качестве типа своих параметров.
function add(first: number, second: number): number; //Overload signature with two parameters
function add(first: number, second: number, third:number): number; //Overload signature with three parameters
function add(first: number, second: number, third?: number, fourth?: number): number { //Implementation signature
if (first !== undefined && second !== undefined && third !== undefined) {
return first + second + third;
} else {
return first + second;
}
}
const r1 = add(1, 2, 3);
const r2 = add(1, 2);
const r3 = add(1, 2, 3, 4); // Expect 2-3 args but got 4
10. Работа с generics в TS?
Обобщённый тип (обобщение, дженерик) позволяет резервировать место для типа, который будет заменён на конкретный, переданный пользователем, при вызове функции или метода, а также при работе с классами. Рассмотрим пример:
const valueFactory = (x: number) => x
const myValue = valueFactory(11)
type TypeFactory<X> = x;
type MyType = TypeFactory<string>
// тип MyType в примере выше получит однозначное значение, в данном случае это тип string
Пример с intefrace
interface ValueContainer<Value> {
value: Value;
}
type StringContainer = ValueContainer<string> // теперь создавая переменную с таким типом, обязательно что бы этой переменной было поле value c типом String
const x: StringContainer = {
value: 'sdsd'
}
Пример с class
class ArrayOfNumbers {
constructor(public collection: number[]){
this.collection = collection //в TypeScript эту часть можно не писать, она произойдет автоматически
}
get(index: number): number {
return this.collection[index]
}
}
class ArrayOfString {
constructor(public collection: string[]) {}
get(index: number): string {
return this.collection[index]
}
}
// Создадим общий класс который может заменить два верхних по отдельности
class ArrayOfAnything<Type> {
constructor(public collection: Type[]){ }
get(index: number): Type {
return this.collection[index]
}
}
new ArrayOfAnything<string>(['1','2','3sd3'])
new ArrayOfAnything<number>([1, 2, 3])
function fillArray<T>(len: number, elem: T): T[] {
return new Array<T>(len).fill(elem);
}
const arr1 = fillArray<number>(10, 0);
const arr2 = fillArray<string>(10, '*');
interface Array<T> {
concat(...items: Array<T[] | T>): T[]; //метод concat может принимать на вход либо массив элементов Т либо массив массивом элементов Т и вернет массив элементов Т
reduce<U>(
callback:(state: U, element: T, index: number, array: T[]) => U,
firstState:? U
): U;
}
Создание требований для шаблона
что здесь может пойти не так?
function getLength<T>(arg: T): number {
return arg.length;
}
Ответ достаточно прост: компилятор ничего не знает про тип аргумента. По умолчанию, вместо обобщённого параметра можно подставить любой тип, поэтому компилятор не знает, что это за тип такой, – T и есть ли у него свойство length. Такая проблема решается на уровне разработчика с помощью ограничений. В нашем случае мы хотим ограничить принимаемое множество типов T условием: наличием свойства length. Для этого нужно создать некоторый интерфейс, где указано нужное свойство и расширить его, используя обобщённый тип T.
interface Lengthwise {
length: number;
}
function printLength<T extends Lenghtwise>(arg: T): number {
return arg.length;
}
printLength(1) // отработает не корректо. у числа нет длинны, синтаксис будет подсвечен. Использовать можно только типы с параметром length
// можно передать обьект, но только тот у которого будет ключ длинны
// но можно передать null и undefined, но это не правильно
В коде выше мы оповещаем компилятор о том, что на вход функции getLength могут подаваться аргументы лишь того типа, что имеют свойство length. Такая запись защитит вас от передачи в функции, например, аргумента типа number.
Здесь T – это некоторый параметр-тип T, который будет захвачен при вызове функции. Конструкция после имени функции указывает на то, что эта функция собирается захватить тип и подменить им все T.
Создание требований к ключу. "keyof T" на выходе преобразует все ключи обьекта в union.
function getProperty<T, K extends keyof T>(obj: T, key: K ) { //Ключ К обязательно должен присутствывать в обьекте
return obj[key]
}
const myObj = {
a: 1,
b: 2,
c: 3,
}
// K === 'a' | 'b' | 'c'
getProperty(myObj, 'd') // TS предупредит, что так делать нельзя! Ведь ключа 'd' нет в наборе union K. Как результат, функция будет работать с ключами только того обьекта, который был ему передан
Объединения или union не являются собственно типом данных, но они позволяют комбинировать или объединить другие типы. Так, с помощью объединений можно определить переменную, которая может хранить значение двух или более типов.
11. Чем отличается interface от type?
Единственная дополнительная функция, которую интерфейсы привносят в таблицу (чего нет у псевдонимов типов), — это «слияние объявлений», что означает, что вы можете определять один и тот же интерфейс несколько раз, и при каждом определении свойства объединяются.
interface Point { x: number; } //declaration #1
interface Point { y: number; } //declaration #2
// These two declarations become:
// interface Point { x: number; y: number; }
const point: Point = { x: 1, y: 2 };
12. Как в TypeScript реализовать свойства класса, являющиеся константами?
В TypeScript, при объявлении свойств классов, нельзя использовать ключевое слово const. При попытке использования этого ключевого слова выводится следующее сообщение об ошибке: A class member cannot have the ‘const’ keyword. В TypeScript 2.0 имеется модификатор readonly, позволяющий создавать свойства класса, предназначенные только для чтения:
MyClass {
readonly myReadonlyProperty = 1;
myMethod() {
console.log(this.myReadonlyProperty);
}
}
new MyClass().myReadonlyProperty = 5; // ошибка, так как свойство предназначено только для чтения
13. Для чего нужны Omit и Pick?
Это новый тип, в котором можно указать свойства, которые будут исключены из исходного типа.
type Person = { name: string; age: number; location: string; };
type QuantumPerson = Omit<Person, 'location'>; // Аналогично следующей строке
QuantumPerson = { name: string; age: number; };
Pick помогает вам использовать уже определенный интерфейс, но брать из объекта только те ключи, которые вам нужны.
interface Dog {
id?: number;
name: string;
weight: number;
age: number;
}
type HomelessDog = Pick<Dog, 'id' | 'name' | 'weight'>
const hoboDog: HomelessDog = {
name: 'hobodog',
weight: 50,
id: 1488
}
14. Для чего нужен тип «Record»?
Он позволяет создавать типизированную мапу.
let Person : Record<string, number> = {};
Person.age = 25;
Наш набор данных имеет идентификатор для своего ключа, который имеет строковый тип. Все значения имеют одинаковый формат, то есть у них есть firstName и lastName.
Для этой структуры данных лучше всего использовать служебный тип Record. Мы можем определить тип нашей структуры данных следующим образом:
type User = {
firstName: string,
lastName: string
}
const myData:Record<string, User> = {
"123-123-123" : { firstName: "John", lastName: "Doe" },
"124-124-124" : { firstName: "Sarah", lastName: "Doe" },
"125-125-125" : { firstName: "Jane", lastName: "Smith" }
}
15. Модификаторы доступа в TypeScript?
TypeScript предоставляет три ключевых слова для управления видимостью членов класса, таких как свойства или методы.
- public: вы можете получить доступ к общедоступному члену в любом месте за пределами класса. Все члены класса по умолчанию общедоступны.
- protected: защищенный член виден только подклассам класса, содержащего этот член. Внешний код, который не расширяет класс контейнера, не может получить доступ к защищенному члену.
- private: закрытый член виден только внутри класса. Никакой внешний код не может получить доступ к закрытым членам класса.
16. Абстрактные классы в TypeScript?
Абстрактные классы похожи на интерфейсы тем, что они определяют контракт для объектов, и вы не можете создавать их экземпляры напрямую. Однако, в отличие от интерфейсов, абстрактный класс может предоставлять сведения о реализации для одного или нескольких своих членов.
Абстрактный класс помечает один или несколько своих членов как абстрактные. Любые классы, которые расширяют абстрактный класс, должны предоставлять реализацию для абстрактных членов суперкласса.
Вот пример абстрактного класса Writer с двумя функциями-членами. Метод write() помечен как абстрактный, тогда как метод приветствия() имеет реализацию. И классы FictionWriter, и RomanceWriter, являющиеся наследниками Writer, должны предоставлять свою конкретную реализацию для метода записи.
abstract class Writer {
abstract write(): void;
greet(): void {
console.log("Hello, there. I am a writer.");
}
}
class FictionWriter extends Writer {
write(): void {
console.log("Writing a fiction.");
}
}
class RomanceWriter extends Writer {
write(): void {
console.log("Writing a romance novel.");
}
}
const john = new FictionWriter();
john.greet(); // "Hello, there. I am a writer."
john.write(); // "Writing a fiction."
const mary = new RomanceWriter();
mary.greet(); // "Hello, there. I am a writer."
mary.write(); // "Writing a romance novel."
17. Что такое пересечение типов в TypeScript?
Типы пересечения позволяют объединять элементы двух или более типов с помощью оператора «&». Это позволяет комбинировать существующие типы, чтобы получить единый тип со всеми необходимыми функциями.
В следующем примере создается новый тип Supervisor, содержащий члены типов Employee и Manager.
interface Employee { work: () => string; }
interface Manager { manage: () => string; }
type Supervisor = Employee & Manager;
// john can both work and manage let john: Supervisor;
18. Как работает и где используется typeof?
TypeScript добавляет оператор typeof, который вы можете использовать в контексте типа для определения типа переменной или свойства:
let s = "hello";
let n: typeof s;
(( let n: string ))
Помните, что значения и типы — это не одно и то же. Чтобы указать тип, который имеет значение f, мы используем typeof:
function f() {
return { x: 10, y: 3 };
}
type P = ReturnType<typeof f>;
** type P = {
x: number;
y: number;
}
19. Что такое type guards в TypeScript?
Защита типа — это некоторое выражение, которое выполняет проверку во время выполнения, гарантирующую тип в некоторой области.
Пример без использования type guards:
В примере сверху есть ошибки типов, потому что мы можем получить доступ только к членам union которые принадлежат к обоим типам.
Мы могли бы использовать приведения типо для исправления ошибок:
function makeNoise(creature: Human | Dog) {
if (typeof (creature as Human).speak === 'function') {
(creature as Human).speak();
}
if (typeof (creature as Dog).bark === 'function') {
(creature as Dog).bark();
}
}
Но это усложнит запись кода. Для облегчения понимания кода рекомендуется использовать type guards. Есть два основных способа определения type guards:
- Используя предикаты типов
- Используя оператор in
Чтобы определить определяемую пользователем защиту типа, нам просто нужно определить функцию, возвращаемый тип которой является предикатом типа:
function isHuman(creature: Human | Dog): creature is Human {
return typeof (creature as Human).speak === 'function';
}
Мы создали функцию type guard isHuman(creature), которая возвращает предикат типа created is Human. Любой предикат принимает параметр и тип. Параметр должен быть частью текущей сигнатуры функции. В нашем случае это существо.
Предикат вернет true/false в зависимости от того является ли creature обьектом класса Human и имеет ли creature метод speak.
С использованием in оператора, функция будет выглядеть следующим образом:
function isDog(creature: Human | Dog): creature is Dog {
return 'bark' in creature;
}
Оператор in возвращает значение true, если указанное свойство находится в указанном объекте.
В нашем примере кода первое решение с использованием предикатов типов лучше, потому что мы не только проверяем, существует ли свойство в объекте, но также и является ли свойство функцией.
Итоговый код будет выглядеть следующим образом.
function makeNoise(creature: Human | Dog) {
if (isHuman(creature)) {
creature.speak();
}
if (isDog(creature)) {
creature.bark();
}
}
Используя нашу type guard, Typescript может гарантировать, что наш параметр creature является либо Human, либо Dog, и больше не будет показывать ошибки.
Еще один пример
function entityIsUser(entity: Entity): entity is User {
return !!(entity as User).name;
}
function entityIsProduct(entity: Entity): entity is Product {
return !!(entity as Product).description;
}
function handleEntities(entities: Entity[]) {
entities.forEach(entity => {
if (entityIsUser(entity)) {
handleUser(entity);
}
if (entityIsProduct(entity)) {
handleProduct(entity);
}
});
}
20. Ключевое слово Declare?
Declare используется, чтобы сообщить компилятору о чем-то, объявленном в другом фрагменте кода. Это особенно полезно при использовании сторонних библиотек. Он также указывает компилятору не включать этот код при транспиляции TypeScript.
interface Interface {
run: () => void;
}
declare let plugin: Interface;
Вы добавляете ссылку на свою веб-страницу файл JavaScript, о котором компилятор ничего не знает. возможно, это скрипт из какого-то другого домена, например foo.com. при оценке сценарий создаст объект с некоторыми полезными методами API и назначит его идентификатору «fooSdk» в глобальной области.
Вы хотите, чтобы ваш код TypeScript мог вызывать fooSdk.doSomething(), но поскольку ваш компилятор не знает о существовании переменной fooSdk, вы получите ошибку компиляции.
Затем вы используете ключевое слово declare как способ сообщить компилятору: «Поверьте мне, эта переменная существует и имеет этот тип». Компилятор будет использовать этот оператор для статической проверки другого кода, но не будет транскомпилировать его в какой-либо JavaScript в выходных данных.
declare const fooSdk = { doSomething: () => boolean }
21. Что такое type predicate?
Предикаты типа — это специальный возвращаемый тип, который сигнализирует компилятору Typescript, к какому типу относится конкретное значение. Предикаты типа всегда присоединяются к функции, которая принимает один аргумент и возвращает логическое значение. Предикаты типа выражаются как argumentName is Type.
interface Cat {
numberOfLives: number;
}
interface Dog {
isAGoodBoy: boolean;
}
function isCat(animal: Cat | Dog): animal is Cat {
return (animal as Cat).numberOfLive !== undefined;
}
Например, функция isCat выполняется во время выполнения, как и все другие type guards. Поскольку эта функция возвращает логическое значение и включает в себя предикат типа animal is Cat, компилятор Typescript правильно преобразует Animal в Cat, если isCat оценивается как true. Он также отобразит Animal как Dog, если isCat оценивается как ложное.
let animal: Cat | Dog = ...
if (isCat(animal)) {
// animal successfully cast as a Cat
} else {
// animal successfully cast as a Dog
}
interface Person {
name: string;
}
interface House {
address: string;
}
const values: (Person | House | string)[] = [
{ name: "Kim" },
{ address: "123 street" },
"hello world",
];
function isPerson(value: Person | House): value is Person {
return (value as Person).name !== undefined;
}
function isHouse(value: Person | House | string): value is House {
return (value as House).address !== undefined;
}
for (const v of values) {
// v is `Person | House | string`
if (isPerson(v)) {
// v is `Person`
} else {
// v is `House | string`
if (isHouse(v)) {
// v is `House`
} else {
// v is `string`
}
}
// v is `Person | House | string`
}
Use in Arrow Function:
values
.filter((v): v is Person => (v as Person).name !== undefined)
.forEach((v) => {
// v is `Person`
});
22. Что такое decorator?
Хотя определение может различаться для разных языков программирования, причина существования декораторов во многом одинакова. В двух словах, декоратор — это шаблон в программировании, в который вы оборачиваете что-то, чтобы изменить его поведение.
Когда вы присоединяете функцию к классу в качестве декоратора, вы получаете конструктор класса в качестве первого параметра.
const classDecorator = (target: Function) => {
// do something with your class
}
@classDecorator
class Rocket {}
Если вы хотите переопределить свойства внутри класса, вы можете вернуть новый класс, расширяющий его конструктор, и установить свойства.
const addFuelToRocket = (target: Function) => {
return class extends target {
fuel = 100
}
}
@addFuelToRocket
class Rocket {}
Теперь ваш класс Rocket будет иметь свойство fuel со значением по умолчанию 100.
const rocket = new Rocket()
console.log((rocket).fuel) // 100
Еще одно хорошее место для присоединения декоратора — это метод класса. Здесь вы получаете три параметра в своей функции: target, propertyKey и descriptor.
const myDecorator = (target: Object, propertyKey: string, descriptor: PropertyDescriptor) => {
// do something with your method
}
class Rocket {
@myDecorator
launch() {
console.log("Launching rocket in 3... 2... 1... 🚀")
}
}
Первый параметр содержит класс, в котором живет этот метод, в данном случае это класс Rocket. Второй параметр содержит имя вашего метода в строковом формате, а последний параметр — это дескриптор свойства, набор информации, определяющий поведение свойства. Это можно использовать для наблюдения, изменения или замены определения метода.
Декоратор метода может быть очень полезен, если вы хотите расширить функциональность своего метода, о чем мы поговорим позже.
Как и в декораторе метода, вы получите параметр target и propertyKey. Единственная разница в том, что вы не получаете дескриптор свойства.
const propertyDecorator = (target: Object, propertyKey: string) => {
// do something with your property
}
Guide to typescript decorators
23. Что такое namespace?
Пространство имен используется для логической группировки функций. Чтобы включить одну или группу связанных функций, пространство имен может включать интерфейсы, классы, функции и переменные.
Ключевое слово namespace, за которым следует имя пространства имен, можно использовать для создания пространства имен. Фигурные скобки можно использовать для определения всех интерфейсов, классов и других объектов.
namespace <name>
{
}
24. Что такое Conditional Types?
На основе условия, заданного в качестве проверки отношения типов, условный тип выбирает один из двух альтернативных типов:
T extends U ? X : Y
Когда T можно присвоить U, тип — X, а когда нельзя — тип Y.
Поскольку условие зависит от одной или нескольких переменных типа, условный тип T расширяет U? X: Y и либо разрешается в X или Y, либо задерживается. Следует ли разрешать X или Y или откладывать, когда T или U содержат переменные типа, определяется тем, достаточно ли в системе типов информации, чтобы заключить, что T всегда можно присвоить U.
React:
1. Что такое React?
Когда React был впервые представлен, он коренным образом изменил работу фреймворков JavaScript. В то время как все остальные продвигали MVC, MVVM (Model-View-ViewModel) и т. д., React решил изолировать рендеринг представления от представления модели и внедрить совершенно новую архитектуру в экосистему JavaScript: Flux.
В 2013 году Facebook потратил немало усилий на интеграцию функции чата: функция, которая будет работать и доступна через приложение, интегрируясь практически на каждой странице сайта. Это было сложное приложение внутри уже сложного приложения, и неконтролируемая мутация модели DOM, наряду с параллельным и асинхронным характером многопользовательского ввода-вывода, создавала трудности для команды Facebook.
Например, как вы можете предсказать, что будет отображаться на экране, когда что угодно может захватить DOM и изменить его в любое время по любой причине, и как вы можете доказать, что то, что было отображено, было правильным?
Вы не могли дать такие гарантии ни с одной из популярных интерфейсных сред до React. Условия race-DOM были одной из самых распространенных ошибок в ранних веб-приложениях.
Недетерминизм = изменяемое состояние + параллельные процессы
Работой номер 1 для реакта стало решение этих проблем. Сделать это удалось с помощью следующих двух ключевых инноваций.
- Однонаправленный поток данных с помощью flux архитектуры.
- Состояние компонента иммутабельно. После установки состояние компонента не может быть изменено. Изменения состояния не меняют текущее представление. Вместо этого они запускают ререндер представления уже с новым состоянием.
«Самый простой способ, который мы нашли, концептуально, чтобы структурировать и отображать наши представления (view), — это просто попытаться полностью избежать мутаций». — Tom Occhino, JSConfUS 2013
С помощью Flux, React решил проблему неконтролируемой мутации. Вместо того, чтобы прикреплять eventlisteners к любому произвольному количеству произвольных объектов (моделей) для запуска обновлений DOM, React представил единственный способ управления состоянием компонента: dispatch в store. Когда состояние store изменится, store запросит у компонента повторный рендеринг.
Одна из главных причин для того что бы использовать реакт, это детерминированный подход к рендеру представлений
Это антишаблон читать данные из DOM с целью реализации доменной логики. Это противоречит цели использования React. Вместо этого считывайте данные из своего store и управляйте началом рендеринга.
До появления JSX, если вы хотели написать декларативный код пользовательского интерфейса, вам приходилось использовать HTML-шаблоны, а в то время для этого не существовало хорошего стандарта. Каждый фреймворк использовал свой собственный специальный синтаксис, который вам нужно было изучить, чтобы делать такие вещи, как цикл по данным, интерполировать переменные или выполнять условное ветвление.
Вы можете перебирать элементы с помощью Array.prototype.map, использовать логические операторы, пользоваться тернарным оператором, вызывать чистые функции, использовать литералы шаблонов (Шаблон строки) или вообще делать что-либо еще, что может делать выражение JavaScript. На мой взгляд, это огромное преимущество перед конкурирующими UI-фреймворками.
React предоставляет оболочку для событий DOM, называемую синтетическими событиями.
- Сглаживают межплатформенные различия в обработке событий, упрощая работу кода JS в любом браузере.
- Автоматически управляют памятью. Если бы вы собирались создать список с бесконечной прокруткой в необработанном HTML-коде JavaScript, вам нужно было бы делегировать события или перехватывать и отсоединять прослушиватели событий, когда элементы прокручиваются на экране и за его пределами, чтобы избежать утечек памяти. Синтетические события автоматически делегируются корневому узлу, что означает, что разработчики React получают управление памятью событий просто так.
Начиная с React 0.14, React представил синтаксис класса для подключения к жизненному циклу компонентов React. У React есть два разных жизненных цикла: монтирование, обновление и размонтирование:
Затем в жизненном цикле обновления есть еще три фазы:
- Рендеринг — помимо вызова хуков, ваша функция рендеринга должна быть детерминированной и не иметь побочных эффектов. Обычно вы должны думать об этом как о чистой функции от props до JSX.
- Pre-Commit — здесь вы можете читать из DOM, используя метод жизненного цикла getSnapShotBeforeUpdate. Полезно, если вам нужно прочитать такие вещи, как положение прокрутки или отображаемый размер элемента перед повторным рендерингом DOM.
- Commit — на этапе commit React обновляет DOM и ссылки. Вы можете подключиться к нему с помощью componentDidUpdate или хука useEffect. Здесь можно запускать эффекты, планировать обновления, использовать DOM и т. д.
На мой взгляд, представлять компонент как долгоживущий класс — не лучшая ментальная модель того, как работает React. Помните: состояние компонента React не предназначено для изменения. Он предназначен для замены, и каждая замена текущего состояния вызывает повторный рендеринг. Это позволяет реализовать, пожалуй, лучшую функцию React: упростить создание детерминированных view render.
Лучшей ментальной моделью такого поведения является то, что каждый раз, когда React выполняет рендеринг, он вызывает детерминированную функцию, которая возвращает JSX. Эта функция не должна напрямую вызывать свои побочные эффекты, но может ставить эффекты в очередь для запуска React.
Другими словами вам стоит думать о большинстве React компонентов как о чистых функциях от пропсов до JSX.
- Почему React? Детерминированное представление визуализируется благодаря однонаправленному потоку данных и неизменному состоянию компонента.
- JSX обеспечивает простую декларативную разметку в вашем JavaScript.
- Синтетические события сглаживают межплатформенные отличия в работе с событиями и уменьшают головную боль при управлении памятью.
- Жизненный цикл компонента существует для защиты состояния компонента. Он состоит из mount, update и unmount, а этап обновления состоит из этапов render, pre-commit и commit.
- Хуки React позволяют вам подключиться к жизненному циклу компонента без использования синтаксиса класса, а также упрощают обмен поведением между компонентами.
- Контейнеры и компоненты представления позволяют изолировать проблемы представления от состояния и эффектов, делая как ваши компоненты, так и бизнес-логику более пригодными для повторного использования и тестирования.
- Компоненты более высокого порядка упрощают совместное использование компонуемых поведений на многих страницах вашего приложения таким образом, что вашим компонентам не нужно знать о них (или быть тесно связанными с ними).
2. Основные принципы работы с React JS?
Ключевые особенности React: декларативность, универсальность, компонентный подход, виртуальный DOM, JSX.
Декларативность:
Декларати́вное программи́рование — парадигма программирования, в которой задаётся спецификация решения задачи, то есть описывается ожидаемый результат, а не способ его получения. Декларативное программирование — это как попросить вашего друга нарисовать пейзаж. Тебе все равно, как он рисует, это зависит от него.
Универсальность:
Одна из ключевых особенностей React — универсальность. Эту библиотеку можно использовать на сервере и на мобильных платформах с помощью React Native. Это принцип Learn Once, Write Anywhere или «Научитесь один раз, пишите где угодно».
Composition:
Композиция это обьединения частей или елементов в единое целое. Композиция в React JS используется вместо наследования.
Модель композиции React:
1. Родитель может знать, а может и не знать кто будет компонентом ребенком в будущем.
2. Ребенок ни когда не знает кто родитель.
3. Ребенок ни когда не знает кто его братья и сестры.
4. Отношения между компонентами проиходят через всем понятный интерфейс (ПРОПСЫ).
3. Что такое JSX(Javascript Syntax Expression)?
В приведенном ниже примере, текст внутри тега `h1` в методе `render()` возвращается в виде JavaScript-функции:
class App extends React.Component {
render() {
return (
<h1 className="greeting">
Привет, мир!
</h1>
</div>
)
}
}
const element = React.createElement(
'h1',
{className: 'greeting'},
'Привет, мир!'
);
4. Что такое Lifting State Up (поднятие) в React??
5. Что такое props в React?
*Props* - это входные данные для компонента. Это простые значения (примитивы) или объект, содержащий несколько значений, которые передаются компонентам при их создании с помощью синтаксиса, похожего на атрибуты HTML-тегов.
Основное назначение пропов в React заключается в предоставлении компоненту следующего функционала:
1. Передача данных компоненту
2. Вызов изменения состояния
Создадим элемент со свойством `reactProp`:
<Element reactProp={'1'} />
props.reactProp
6. Как работает виртуальный DOM?
🔸 В React каждый пользовательский интерфейс — это отдельный компонент, и у каждого компонента есть свое состояние.
🔸 React следует observable pattern и наблюдает за изменениями состояний.
🔸 Всякий раз, когда в состояние какого-либо компонента вносится изменение, React обновляет виртуальное дерево DOM, но не изменяет реальное дерево DOM.
🔸 После обновления React сравнивает текущую версию виртуального DOM с предыдущей версией.
🔸 React знает, какие объекты изменяются внутри виртуальном DOM, основываясь на том, что он изменяет только эти объекты в реальном DOM, совершая минимальные манипуляционные операции.
🔸 Этот процесс отмечен как "diffing". Изображение ниже прояснит концепцию больше.
Эффективный diff алгоритм:
Для дерева с числом элементов «n» сложность порядка O(n3). React реализует оптимизированный алгоритм O(n) на основе двух предположений:
- Элементы разных типов будут создавать разные деревья.
React анализирует дерево с помощью поиска в ширину (Breadth-first search - один из методов обхода графа). Для узла дерева, если тип элемента изменен, например, с «section» на «div». React уничтожит все поддерево под этим элементом и восстановит его с нуля.
- Разработчик указывает с помощью ключа (key prop), на то какие элементы должны быть стабильны в разных рендерах.
React будет использовать этот ключ для сопоставления элементов между двумя деревьями и минимизации ненужных мутаций.
7. Что такое React Fibers?
Fiber - это новый механизм согласования или повторной реализации основного алгоритма в React v16. Цель React Fiber - увеличении производительности при разработке таких задач как анимация, организация элементов на странице и движение элементов. Ее главная особенность это инкрементный рендеринг: способность разделять работу рендера на единицы и распределять их между множественными фреймами.
8. Согласование в React?
Каждый раз, когда происходит изменение в состоянии компонента, запускается механизм, именуемый "согласование" (reconciliation), который вычисляет разницу (дифф) между прошлым состоянием и новым. С алгоритмической точки зрения происходит поиск отличий в двух деревьях. В общем случае алгоритм, выполняющий это вычисление, работает со сложностью O(n3).
9. В чем разница между теневым (Shadow) и виртуальным DOM?
*Shadow DOM* - это браузерная технология, спроектированная для ограничения области видимости переменных и CSS в *веб-компонентах*. Используется для инкапсуляции.
10. В чем разница между настоящим и виртуальным DOM?
| Реальный DOM | Виртуальный DOM |
|---|---|
| Обновления медленные | Обновления быстрые |
| Манипуляции с DOM очень дорогостоящие | Манипуляции с DOM не очень дорогие |
| Вы можете обновлять HTML напрямую | Вы не можете обновлять HTML напрямую |
| Активная работа с DOM часто приводит к утечкам памяти | Утечки памяти практически полностью исключены |
| При обновлении элемента создается новая DOM | При изменении элемента обновляется только JSX |
11. Что такое фрагмент (Fragment)?
Это распространенный паттерн в React, который используется в компонентах, возвращающих несколько элементов. *Fragments* позволяют группировать дочерние элементы без создания лишних DOM-узлов:
render() {
return (
<React.Fragment>
<ChildA />
<ChildB />
<ChildC />
</React.Fragment>
)
}
Также существует *сокращенный синтаксис*, но он не поддерживается в некоторых инструментах:
render() {
return (
<>
<ChildA />
<ChildB />
<ChildC />
</>
)
}
1. Фрагменты немного быстрее и используют меньше памяти. Реальная польза от этого ощущается в очень больших и глубоких деревьях элементов
2. Некоторые механизмы CSS, например, *Flexbox* и *Grid* используют связь родитель-ребенок (предок-потомок, если угодно), поэтому добавление дополнительных `div` может сломать макет страницы.
3. Удобнее пользоваться инспектором DOM
12. Что такое `React.memo()`?
Когда компонент заключен в React.memo(), React рендерит компонент и запоминает результат. Перед следующим рендерингом, если новые пропсы одинаковы, React повторно использует memoизированный результат, пропуская следующий рендеринг.
React всегда перерисовывает компонент при изменении состояния, даже если компонент обернут в React.memo().
export function Movie({ title, releaseDate }) {
return (
<div>
<div>Movie title: {title}</div>
<div>Release date: {releaseDate}</div>
</div>
);
}
export const MemoizedMovie = React.memo(Movie);Похожий функционал предоставляет хук `useMemo()`.
Использование кастомного сравнения props:
По-умолчанию мемо делает поверхностное сравнение props. Функцию для кастомного сравнения можно передать вторым параметром:
React.memo(Component, [areEqual(prevProps, nextProps)]);Функция areEqual должна вернуть true если пропсы равны.
13. Как реализовать рендеринг на стороне сервера или SSR?
React поддерживает рендеринг на стороне Node-сервера из коробки. Для этого используется специальная версия DOM-рендерера, которая реализует такой же паттерн, что и клиентская версия:
import ReactDOMServer from 'react-dom/server'
import App from './App'
ReactDOMServer.renderToString(<App />)
Этот метод возвращает обычный HTML в виде строки, которая затем может быть помещена в тело (body) ответа сервера. На стороне клиента React определяет предварительно отрендеренный контент и просто вставляет его в существующее дерево компонентов.
14. Какие методы жизненного цикла компонента существуют в React?
-
constructor() - Конструктор компонента React вызывается до того, как компонент будет примонтирован. В начале конструктора необходимо вызывать super(props). Если это не сделать, this.props не будет определён.
Конструкторы в React обычно используют для двух целей: Инициализация внутреннего состояния через присвоение объекта this.state. Привязка обработчиков событий к экземпляру.
useState - заменяет конструктор. Служит для управление локальным state у компонента. -
componentDidMount() - вызывается сразу после монтирования. В этом методе должны происходить действия, которые требуют наличия DOM-узлов. Это хорошее место для создания сетевых запросов.
Этот метод подходит для настройки подписок. Но не забудьте отписаться от них в componentWillUnmount().
useEffect(callBack, []) - Инициализируется немедленно после первого рендера, инициализирует state которому могут понадобиться DOM ноды, Network запросы и другие side effects. -
componentDidUpdate(prevProps, prevState, snapshot) - вызывается сразу после обновления. Не вызывается при первом рендере. Метод позволяет работать с DOM при обновлении компонента. Также он подходит для выполнения таких сетевых запросов, которые выполняются на основании результата сравнения текущих пропсов с предыдущими. Если пропсы не изменились, новый запрос может и не требоваться.
useEffect(callBack) - вызывается и при первом рендере и после обновления. Для эмуляции componentDidUpdate нужна проверка на первый рендер и useRef.const mounted = useRef() useEffect(() => { if(!mounted.current) { mounted.current = true } else { console.log('componentDidUpdate !) } }) - componentWillUnmount() - вызывается непосредственно перед размонтированием и удалением компонента. В этом методе выполняется необходимый сброс: отмена таймеров, сетевых запросов и подписок, созданных в componentDidMount().
-
useRef - предоставляет доступ к DOM елементам созданным при рендере, помогает useEffect симитировать работу componentDidUpdate.
useRef() - это хук принимающий в качестве аргумента какое то значение и возвращающий референс. Референс это специальный обьект у которого есть свойство current.
reference.current - предоставляет доступ к значению, reference.current = newValue обновляет значение.
1. Значение референса остается не изменным между ререндерами.
2. Обновление референса не вызывает ререндер.
-
useMemo() - сохраняет результат полученной функции между ререндерами и не перезапускает ее до тех пор пока не изменятся зависимости.
useMemo(compute, dependencies) - принимает на вход два параметра. Compute - функия результат которой надо запомнить. Dependencies - зависимости без изменения которых не будет перезапущена функция compute.
Если сравнивать useMemo и useCallback то useCallback более специфическая функция для запоминания callback, но useMemo тоже умеет запоминать callback.
-
useCallback() - при не изменяющемся deps, хук вернет тот же самый instance функции между ререндерами.
useCallback(callbackFnc, deps) - принимает на вход два параметра. CallbackFnc - функия incstance которой надо запомнить. Deps - зависимости без изменения которых instance функции останется тем же самым.
Кейсы использования useCallback():
1. Функциональная компонента обернута React.memo() и принимает функцию как аргумент.
2. Когда функция является зависимостью для других hooks, useEffect(..., [callback)]
3. Когда у функции есть внутреннее состояние, когда функция debounced или throttled.
useEffect(callBack) - return функция. Возвращает функцию которая будет запущена после unmount'a компоненты. Подходит для отмены таймеров, сетевых запросов и подписок.
15. Что такое Context в React и для чего он используется?
Контекст разработан для передачи данных, которые можно назвать «глобальными» для всего дерева React-компонентов (например, текущий аутентифицированный пользователь, UI-тема или выбранный язык).
Контекст позволяет избежать передачи пропсов в промежуточные компоненты:
// Контекст позволяет передавать значение глубоко
// в дерево компонентов без явной передачи пропсов
// на каждом уровне. Создадим контекст для текущей
// UI-темы (со значением "light" по умолчанию).
const ThemeContext = React.createContext('light');
class App extends React.Component {
render() {
// Компонент Provider используется для передачи текущей
// UI-темы вниз по дереву. Любой компонент может использовать
// этот контекст и не важно, как глубоко он находится.
// В этом примере мы передаём "dark" в качестве значения контекста.
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
}
// Компонент, который находится в середине,
// теперь не должен явно передавать UI-тему вниз.
function Toolbar(props) {
return (
<div>
<ThemedButton />
</div>
);
}
class ThemedButton extends React.Component {
// Определяем contextType, чтобы получить значение контекста.
// React найдёт (выше по дереву) ближайший Provider-компонент,
// предоставляющий этот контекст, и использует его значение.
// В этом примере значение UI-темы будет "dark".
static contextType = ThemeContext;
render() {
return <Button theme={this.context} />;
}
}
Обычно контекст используется, если необходимо обеспечить доступ данных во многих компонентах на разных уровнях вложенности. По возможности не используйте его, так как это усложняет переиспользование компонентов.
16. Для чего нужен атрибут key при рендере списков?
Ключи (keys) помогают React определять, какие элементы были изменены, добавлены или удалены. Их необходимо указывать, чтобы React мог сопоставлять элементы массива с течением времени.
Лучший способ выбрать ключ — это использовать строку, которая будет явно отличать элемент списка от его соседей. Чаще всего вы будете использовать ID из ваших данных как ключи. Главное что бы ключи были уникальные и не смещались.
Чаще всего с этой задачей не возникает проблем, так как у любой сущности, с которой мы работаем, есть свой идентификатор (например, primary key из базы данных). Так же можно использовать UUID.
Кстати, key не обрабатывается как обычный проп и его нельзя получить внутри компонента как this.props.key. Если вам нужны данные, которые были переданы в key внутри компонента, то просто передайте их отдельным пропом (например, id):
17. Как работает проп children?
Некоторые компоненты не знают своих потомков заранее. Это особенно характерно для таких компонентов, как Sidebar или Dialog, которые представляют из себя как бы «коробку», в которую можно что-то положить. Для таких компонентов мы рекомендуем использовать специальный проп children, который передаст дочерние элементы сразу на вывод:
function FancyBorder(props) {
return (
<div className={'FancyBorder FancyBorder-' + props.color}>
{props.children}
</div>
);
}
Если внимательно посмотреть на документацию React, то можно увидеть следующее определение children: "children are an opaque data structure" (свойство children – непрозрачная структура данных). Другими словами, нельзя однозначно полагаться на тип этого пропса, так как снаружи можно передать всё что угодно. Подобное поведение может приводить к трудноотловимым ошибкам. Например проверка this.props.children.length это не всегда количество детей. Если children это одиночный элемент, например строка, то свойство length вернет длину этой строки.
Это позволит передать компоненту произвольные дочерние элементы, вложив их в JSX:
function WelcomeDialog() {
return (
<FancyBorder color="blue">
<h1 className="Dialog-title">
Добро пожаловать
</h1>
<p className="Dialog-message">
Спасибо, что посетили наш космический корабль!
</p>
</FancyBorder>
);
}
Всё, что находится внутри JSX-тега , передаётся в компонент FancyBorder через проп children. Поскольку FancyBorder рендерит {props.children} внутри div, все переданные элементы отображаются в конечном выводе.
18. В чем разница между управляемыми (controlled) и не управляемыми (uncontrolled) компонентами?
В HTML элементы формы, такие как input, textarea и select, обычно сами управляют своим состоянием и обновляют его когда пользователь вводит данные. В React мутабельное состояние обычно содержится в свойстве компонентов state и обновляется только через вызов setState().
В управляемом компоненте с каждой мутацией состояния связана функция-обработчик. Благодаря этому валидация или изменение введённого значения становится простой задачей. Например, если мы хотим, чтобы имя обязательно было набрано заглавными буквами, можно написать такой handleChange:
handleChange(event) {
this.setState({value: event.target.value.toUpperCase()});
}
Вместо того, чтобы писать обработчик события для каждого обновления состояния, вы можете использовать неуправляемый компонент и читать значения из DOM через реф.
class NameForm extends React.Component {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
this.input = React.createRef();
}
handleSubmit(event) {
alert('Отправленное имя: ' + this.input.current.value);
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Имя:
<input type="text" ref={this.input} />
</label>
<input type="submit" value="Отправить" />
</form>
);
}
}
Неуправляемые компоненты опираются на DOM в качестве источника данных и могут быть удобны при интеграции React с кодом, не связанным с React. Количество кода может уменьшиться, правда, за счёт потери в его чистоте. Поэтому в обычных ситуациях мы рекомендуем использовать управляемые компоненты.
Настройка контролируемого input включает в себя 3 шага:
1. Создать стейт для хранения значения input:
[val, setVal] = useState('')
onChange = event => setVal(event.target.value)
<input onChange={onChange} value={val} />.
debouncedQuery = useDebouncedValue(value, wait).
19. Что такое PureComponent?
React.PureComponent похож на React.Component. Отличие заключается в том, что React.Component не реализует shouldComponentUpdate(), а React.PureComponent реализует его поверхностным сравнением пропсов и состояния.
Если метод render() вашего React-компонента всегда рендерит одинаковый результат при одних и тех же пропсах и состояниях, для повышения производительности в некоторых случаях вы можете использовать React.PureComponent.
Метод shouldComponentUpdate() базового класса React.PureComponent делает только поверхностное сравнение объектов. Если они содержат сложные структуры данных, это может привести к неправильной работе для более глубоких различий (то есть, различий, не выраженных на поверхности структуры). Наследуйте класс PureComponent только тогда, когда вы ожидаете использовать простые пропсы и состояние
20. Что такое state в React?
State это JavaScript объект, который хранит динамические данные компонента и позволяет компоненту отслеживать изменения между рендерами(render).
Ещё раз: крайне важно не изменять state напрямую. Для установки нового состояния в React предусмотрена функция setState. setState() планирует обновления объекта состояния компонента. Когда состояние изменяется, компонент отвечает повторным рендерингом. Вызовы setState является асинхронными, когда они находятся внутри обработчиков событий, и когда вы не полагаетесь на this.state, чтобы отобразить новое значение сразу после вызова setState.
Общая рекомендация, которую дают разработчики React, это делать структуру максимально плоской, похожей на то, как хранятся данные в базе данных. Причём желательно в хорошо нормализованном виде. Другими словами, не нужно дублировать данные в состоянии.
21. События в React?
В любой обработчик события при вызове передаётся объект типа SyntheticEvent, кроссбраузерный "враппер" (обёртка) над нативным объектом события. Интерфейсно он не отличается от нативного, кроме того, что работает одинаково во всех браузерах.
class Component extends React.Component {
onClick = (event) => {
console.log(event); // => SyntheticBaseEvent
console.log(event.type); // => "click"
}
// ...
}
SyntheticEvent хранит в себе оригинальный объект события и предоставляет интерфейс для доступа к его свойствам и методам. Этот интерфейс одинаков для всех браузеров, что крайне удобно с точки зрения разработки. К примеру, необходимо отменить действие по умолчанию (перезагрузку страницы) при отправке формы:
handleSubmit = (e) => {
e.preventDefault();
this.setState({ count: this.state.count + 1 });
};
Точно так же нужно поступать при необходимости предотвратить всплытие события. Только вместо preventDefault вызывается функция stopPropagation.
React нормализует события так, что они имеют консистентные свойства в различных браузерах. Кроме того, в формах добавляется событие onChange, которое ведёт себя в соответствии со своим названием и сильно упрощает работу.
stopPropagation предотвращает дальнейшее распространение текущего события на этапах захвата и всплытия.
preventDefault предотвращает действие браузера по умолчанию для этого события.
22. Что такое Компонент высшего порядка (Higher-Order Component, HOC)?
Компонент высшего порядка — это функция, которая принимает компонент и возвращает новый компонент. React Higher Order Component — это паттерн, который проистекает из природы React, которая отдает предпочтение композиции, а не наследованию. HOC часто встречаются в сторонних библиотеках, например connect в Redux и createFragmentContainer в Relay.
HOC — это чистая функция с нулевыми побочными эффектами.
Помогает для Воспроизведения DRY. Прим. memo.
const EnhancedComponent = higherOrderComponent(WrappedComponent);
Заметьте, что HOC ничего не меняет и не наследует поведение оборачиваемого компонента, вместо этого HOC оборачивает оригинальный компонент в контейнер посредством композиции. HOC является чистой функцией без побочных эффектов. Оборачиваемый компонент получает все пропсы, переданные контейнеру, а также проп data. Для HOC не важно, как будут использоваться данные, а оборачиваемому компоненту не важно, откуда они берутся.
1. Обеспечения возможности переиспользования кода, логики, а так же начальная загрузка
2. Перехват рендеринга (RenderHighjacking)
3. Абстрагирования и манипулирования состоянием
4. Манипулирования пропами
Примеры использования:
1. Для отображения лоадера пока компонент ждет данные. Отслеживает props и пока они пусты будет показывать значек загрузки.
2. Компоненты которые рендерятся по условию. Когда их props достигают определенных значений.
23. Хук useState?
Хук состояния - useState. ASYNC. Возвращает массив где первой значение это начальное значение стейт, а второе функция сеттер.
Говорит реакту запустить следующую итерацию рендера. Однако реакт так же может оптимизировать процесс. Несколько вызовов useState приведут к одному рендеру.
// Toggle a boolean
const [toggled, setToggled] = useState(false);
setToggled(toggled => !toggled);
Ленивая инициализация state: Каждый раз при ре-рендере компонента реакт запускает useState(initialState). Когда туда передается примитивный тип данных проблем не возникает. Когда initialState требует тяжелых вычислительных операций выгодно передавать в useState саму функцию.
function MyComponent({ bigJsonData }) {
const [value, setValue] = useState(function getInitialState() {
const object = JSON.parse(bigJsonData); // expensive operation
return object.initialValue;
});
// ...
}
getInitialState() выполняется только один раз при начальном рендеринге, чтобы получить начальное состояние.
При более последующих ререндерах компонента getInitialState() не вызывается, пропуская дорогостоящую операцию.
24. Хук useEffect?
Для выполнения побочных эффектов, например, прямой работы с DOM в React используется встроенный хук useEffect(). Он заменяет собой три колбека жизненного цикла:
• componentDidMount() • componentDidUpdate() • componentWillUnmount()Колбек, переданный в useEffect(), отрабатывает после того как изменения будут запущены в дом (Асинхронно). То есть произошло объединение методов componentDidUpdate() и componentDidMount(). Такое изменение было сделано ради удобства. Мировая практика использования React показала, что, в основном, эффекты происходят после каждого рендера, независимо от того, первая эта отрисовка или все последующие.
Второй параметр передаваемый в useEffect это массив. Отсуствующий массив - useEffect запускается после каждого ререндера, пустой массив [] - useEffect запускается единождый после инициализирующего рендера, Массив с зависимостями [state/props] - useEffect запускается при изменении значений переданных в массив state/props.
Сброс эффекта:
В некоторых случаях эффект нужно сбрасывать. Например, когда эффект после изменения пропсов перестает быть актуальным, его нужно "зачистить". Для этого достаточно вернуть функцию из useEffect(), внутри которой выполняется очистка:
// Предположим, что этот эффект зависит от пропса userId
useEffect(() => {
const id = setTimeout(/* какой-то код с userId */);
return () => clearTimeout(id);
}, [userId]);
25. Хук ref?
useRef() – хранение любых данных между вызовами компонента. Этот хук возвращает обычный объект со свойством current внутри. Единственное отличие этого объекта, от создаваемого вручную { current: ... }, в том, что хук возвращает один и тот же объект при каждом вызове компонента. По своему поведению useRef() похож на использование обычного свойства внутри классового компонента (this.someproperty).
26. Хук useMemo?
Встроенный в реакт хук, который запоминает результат выполнения функции. На вход принимается сама функция и массив ее зависимостей. Во время иницилизирующего рендера хук запоминает результат выполнения функции. Если во время следующих ререндеров зависимости не изменяются useMemo не запустит функцию, но вернет ее результат.
const memoizedResult = useMemo(compute, dependencies);
Но если зависимости изменились useMemo запустит функцию и сохранит ее результат. Если callback использует state или props нужно не забыть указать их как зависимости.
const memoizedResult = useMemo(() => {
return expensiveFunction(propA, propB);
} , [propA, propB]);
useCallback по сравнению с useMemo более специализированный хук для сохранения callbacks. useMemo может заменить useCallback:
const callback = () => {
return 'Result';
};
const memoizedCallback = useMemo(() => callback, [prop]);
Просто обернув callback в стрелочную функцию.
27. Хук useCallback?
useCallback возвращает один и тот же экземпляр передаваемой функции (параметр 1) вместо создания нового при каждом повторном рендеринге компонента. Новый экземпляр передаваемой функции (параметр 1) может быть создан только при изменении массива зависимостей (параметр 2).
function MyComponent() {
// handleClick is the same function object
const handleClick = useCallback(() => {
console.log('Clicked!');
}, []);
// ...
}
28. Разница useCallback vs useMemo?
useCallback(fn, deps)` это просто `useMemo(() => fn, deps)`
Когда компонент собирается повторно визуализироваться, React сравнивает каждый созданный под его исходным компонентом объект с новой версией этого объекта. И хотя объекты точно такие же, они не указывают на один и тот же объект. React идентифицирует их как разные объекты, позволяя воссоздавать снова и снова при каждом повторном рендеринге.
Если useCallback и useMemo используются правильно, они необходимы, чтобы предотвратить повторные рендеры и сделать код более эффективным.
useMemo используется вместо того, чтобы возвращать невызванную функцию, как это делает useCallback – он работает с передаваемой функцией и возвращает результирующее значение только при изменении массива параметров. Другими словами, useMemo вызывает функцию только при необходимости и возвращает кэшированное значение для других визуализаций.
Когда компонент повторно визуализируется, он создает новые экземпляры всех объектов, включая все функции в нем:
useCallback – кэширует экземпляр функции между визуализациями. useMemo – кэширует значение между визуализациями.
29. Расскажите про хуки и их ценность в React JS?
Вызов Хука обычно приводит к побочным эффектам — эффектам, которые позволяют вашему компоненту подключаться к таким вещам, как состояние компонента и ввод-вывод. Побочный эффект — это любое изменение состояния, наблюдаемое вне функции, кроме возвращаемого функцией значения.
useEffect позволяет ставить эффекты в очередь для запуска в соответствующее время жизненного цикла компонента, что может быть сразу после монтирования компонента (например, componentDidMount), во время фазы фиксации (например, componentDidUpdate) или непосредственно перед размонтированием компонента (например, componentWillUnmount).
Хуки позволяют вам:
- Пишите свои компоненты как функции, а не классы.
- Организуйте свой код лучше.
- Разделяйте многократно используемую логику между различными компонентами
- Составьте хуки, чтобы создать свои собственные пользовательские хуки (вызов хука из другого хука).
Вообще говоря, вы должны отдавать предпочтение функциональным компонентам и хукам React, а не компонентам на основе классов. Обычно они содержат меньше кода, лучше организованы, более удобочитаемы, более пригодны для повторного использования и более проверяемы.
1. Хуки не должны вызываться внутри циклов, условий или вложенных функций. Это позволяет обеспечить одинаковый порядок вызова хуков при повторном рендеринге и сохранять состояние хуков между несколькими вызовами `useState()` и `useEffect()`
2. Хуки можно вызывать только внутри функциональных компонентов React и других хуков, вы не должны вызывать их внутри обычных функций
30. Что такое порталы в React?
Порталы позволяют рендерить дочерние элементы в DOM-узел, который находится вне DOM-иерархии родительского компонента.
ReactDOM.createPortal(child, container)
Первый аргумент (child) — это любой React-компонент, который может быть отрендерен, такой как элемент, строка или фрагмент. Следующий аргумент (container) — это DOM-элемент.
Типовой случай применения порталов — когда в родительском компоненте заданы стили overflow: hidden или z-index, но вам нужно чтобы дочерний элемент визуально выходил за рамки своего контейнера. Например, диалоги, всплывающие карточки и всплывающие подсказки.
Источник: ru.reactjs.org
31. Что такое debounce и throttle?
Это две похожие но разные техники контроля за тем сколько раз будет вызвана функция за определенное время.
Debounce - возвращает функцию которая может быть вызвана любое количество раз, но будет вызвана только после N ms после вызова идущей перед ней функции.
Throttle - возвращает функцию которая может быть вызвана любое количество раз, но будет вызывать callback не чаще одного раза в N ms.
Use case: Поиск города в огромном массиве из всех городов по мере ввода названия города. Каждый поиск является тяжеловестным, что влечет за собой зависания. Это означает, что в нашем фильтре городов мы не сможем попросить наш компьютер отфильтровать города с буквами «Sa», пока он все еще пытается сделать то же самое для городов с буквой «S» в названии. Нам придется либо предотвратить второй процесс, либо остановить первый, а затем запустить второй.
32. Что такое React Reconciliation (Cверка) и как он работает?
Reconciliation (Cверка) - это процесс, посредством которого React обновляет DOM. В этот процесс входит и Virtual DOM и Diffing алгоритм. Когда состояние компонента изменяется, React должен рассчитать необходимость обновления DOM. Это делается путем создания виртуального DOM и сравнения его с текущим DOM. В этом контексте виртуальный DOM будет содержать новое состояние компонента.
При сравнении двух деревьев первым делом React сравнивает два корневых элемента. Поведение различается в зависимости от типов корневых элементов.
Всякий раз, когда корневые элементы имеют различные типы, React уничтожает старое дерево и строит новое с нуля.
При сравнении двух React DOM-элементов одного типа, React смотрит на атрибуты обоих, сохраняет лежащий в основе этих элементов DOM-узел и обновляет только изменённые атрибуты.
По умолчанию при рекурсивном обходе дочерних элементов DOM-узла React проходит по обоим спискам потомков одновременно и создаёт мутацию, когда находит отличие. Эта неэффективность может стать проблемой. Когда у дочерних элементов есть ключи, React использует их, чтобы сопоставить потомков исходного дерева с потомками последующего дерева.
33. Можно создавать анимации в React?
React может использоваться для создания крутых анимаций! В качестве примера посмотрите библиотеки React Transition Group и React Motion.
Источник: ru.reactjs.org
34. Что такое DRY?
Don’t Repeat Yourself
Не повторять свой код. Главный из паттернов для поддержания этого принципа High-Order Component.
Также важно понимать, что DRY нужен не для того, чтобы писать код быстрее. Его задача — упростить чтение и поддержку нашего решения. Поэтому не спешите создавать абстракции сразу. Лучше сделать простую реализацию какой-то части кода, а потом проанализировать, какие абстракции нужно создать, чтобы упростить чтение кода и уменьшить количество мест для изменений, когда они понадобятся.
Чеклист правильной абстракции:
1. Имя абстракции полностью соответствует ее назначению
2. Абстракция выполняет конкретную, понятную задачу
3. Чтение кода, из которого была выделена абстракция, улучшилось
Если у вас есть одна и та же логика (или достаточно похожая, чтобы вы могли обобщить ее без особых усилий) в разных местах, вам следует подумать о перемещении этой логики внутри функции, и если у вас есть функция, которую вы определили в нескольких местах, то переместите в общий модуль.
И опять же, если вам случится копировать модуль с места на место, рассмотрите возможность превращения этого модуля в общую библиотеку.
35. Что такое KISS?
KISS — «Оставьте код простым и тупым». Думаю, с понятием простого кода вы знакомы. Но что значит «тупой» код? В моем понимании, это код, который решает задачу, используя минимальное количество абстракций, при этом вложенность этих абстракций друг в друга также минимальна.
36. Что такое YAGNI?
YAGNI — «Вам это не понадобится». Код должен уметь делать только то, для чего он написан. Мы не создаем никакой функционал, который может понадобиться потом или который делает приложение лучше на наш взгляд. Делаем только то, что нужно конкретно для реализации поставленной задачи.
37. Хук useLayoutEffect?
Согласно официальной документации хук useLayoutEffect по своим параметрам (сигнатуре) полностью идентичен хуку useEffect. Главное же отличие заключается в том, что useLayoutEffect вызывается синхронно, после всех изменений в DOM. Также сами разработчики React рекомендуют использовать useLayoutEffect только в случае острой необходимости, чтобы вдруг не возникло проблем с правильным рендерингом компонентов. Хук useLayoutEffect можно использовать в случаях, когда необходимо произвести какие-то вычисления либо замеры в реальном DOM или провести синхронно мутацию (изменения).
У useEffect одна загвоздка в том, что он запускается после того, как React отрисовывает ваш компонент и гарантирует, что ваш callback эффекта не блокирует отрисовку браузера. Отличается от componentDidMount и componentDidUpdate тем что они запускаютс синхронно после рендера компонента. Однако, если ваш эффект мутирует DOM (например через ссылку узла DOM) и мутация DOM изменит внешний вид узла DOM между моментом его рендеринга и изменениями от вашего useEffect, тогда вам не стоит использовать useEffect . Вам лучше использовать useLayoutEffect. В противном случае пользователь может увидеть мерцание, когда ваши мутации DOM вступят в силу. Это почти единственный случай, когда вы хотите избежать использования useEffect и вместо этого использовать useLayoutEffect.
useEffect запускается асинхронно и после рендеринга на экране. Итак, это выглядит:
1. Вы каким-то образом вызываете рендеринг (изменение состояния или повторный рендеринг родителя)
2. React отображает ваш компонент (вызывает его)
3. Экран визуально обновлен
4. ТОГДА запускается useEffect
useLayoutEffect, с другой стороны, запускается синхронно после рендеринга, но до обновления экрана. Это выглядит вот так:
1. Вы каким-то образом вызываете рендеринг (изменение состояния или повторный рендеринг родителя)
2. React отображает ваш компонент (вызывает его)
3. useLayoutEffect запускается, и React ждет его завершения.
4. Экран визуально обновлен
Если ваш компонент мерцает при обновлении состояния — например, он сначала рендерится в частично готовом состоянии, а затем сразу же повторно рендерится в своем конечном состоянии — это хороший признак того, что пришло время поменять местами в useLayoutEffect.
Так же useLayoutEffect будет иметь место, когда ваше обновление представляет собой двухэтапный (или многоэтапный) процесс. Вы хотите «сгруппировать» пару обновлений вместе перед перерисовкой экрана? Попробуйте использовать этот хук.
38. Что такое React.lazy(), suspense?
Функция React.lazy позволяет рендерить динамический импорт как обычный. Проще говоря, она позволяет загружать дополнительные компоненты в наш общий файл bundle (для браузера) только тогда, когда они понадобятся. Это дает возможность облегчить загрузку страницы сразу, но с другой стороны увеличивает ожидание, когда приходит время подгрузки. Поэтому тут важно построить «цепочку поведения пользователей» и правильно разделить компоненты на «пачки».
Suspense («задержка») позволяет показать запсное содержание, пока подгружается компонента черзе React.lazy. Меняем обычный импорт на импорт с «ленивой загрузкой». Например, отложим загрузку двух компонент в файле App.js.
Было:
import DialogsContainer from './components/Dialogs/DialogsContainer';
import ProfileContainer from './components/Profile/ProfileContainer';
....
Стало:
const DialogsContainer = React.lazy(() => import('./components/Dialogs/DialogsContainer'));
const ProfileContainer = React.lazy(() => import('./components/Profile/ProfileContainer'));
Теперь сами ленивые компоненты в том же файле нам в месте вызова нужно обернуть в компоненту Suspense, которая позволяет нам показать запасное содержимое (например, индикатор загрузки) пока происходит загрузка ленивой компоненты. Сам компонент Suspense можно разместить где угодно выше над этими компонентами. Например:
return (
<div className="app-wrapper">
<HeaderContainer />
<Navbar />
<div className="app-wrapper-content">
<Suspense fallback={<Preloader />}> //вот начало оборачивания
<Route path='/dialogs' render={ () => <DialogsContainer /> } />
<Route path='/profile/:userId?' render={ () => <ProfileContainer /> } />
</Suspense>
<Route path='/users' render={ () => <UsersContainer /> } />
<Route path='/news' render={ () => <News /> } />
<Route path='/music' render={ () => <Music /> } />
<Route path='/setings' render={ () => <Setings /> } />
<Route path='/login' render={ () => <Login /> } />
</div>
</div>
);
Можно в качестве альтернативы создать подобие ХОКа, как показывал Димыч у видео. Для этого создаем файл src/components/hoc/withSuspense.js:
import React, { Suspense } from 'react';
import Preloader from '../common/Preloader/Preloader';
export const withSuspense = (Component) => {
return (props) => {
return <Suspense fallback={<Preloader />}>
<Component {...props} />
</Suspense>
}
}
И теперь оборачиваем те же наши компоненты этим ХОКом, а не самой компонентой Suspense:
return (
<div className="app-wrapper">
<HeaderContainer />
<Navbar />
<div className="app-wrapper-content">
<Route path='/dialogs' render={ withSuspense(DialogsContainer) } /> //вот
<Route path='/profile/:userId?' render={ withSuspense(ProfileContainer) } /> //и вот
<Route path='/users' render={ () => <UsersContainer /> } />
<Route path='/news' render={ () => <News /> } />
<Route path='/music' render={ () => <Music /> } />
<Route path='/setings' render={ () => <Setings /> } />
<Route path='/login' render={ () => <Login /> } />
</div>
</div>
);
39. React 18 в кратце об изменениях?
До версии 18, React уже объединял/группировал (batched) несколько обновлений состояния в одно, чтобы уменьшить количество ненужных повторных отрисовок. Однако это происходило только в обработчиках событий DOM, поэтому промисы, тайм-ауты или другие обработчики этим не могли воспользоваться. Дело в том, что ранее каждый вызов useState (установка нового значения) приводил к перерисовке компонентов. Чуть позже движок оптимизировали и такие вызовы начали группироваться и выполняться за один раз, что должно было сократить количество перерисовок. Теперь данный функционал еще больше оптимизировали.
ReactDOM.flushSync() может принудительно заставить компонент перерисовываться при каждом вызове useState (установка нового значения). Это может иногда понадобиться.
Strict Mode
Напомню что Strict Mode был добавлен в React 16.3. Данный режим позволяет реакту производить дополнительные проверки что бы исключить возможные проблемы приложения. Далее, некоторые дополнения к строгому режиму (Strict Mode), включая новое поведение, называемое «строгими эффектами» (“strict effects”). Данный режим будет вызывать двойные эффекты — в частности, монтирование и размонтирование (mount и unmount). Добавление в приложение React добавляет особое поведение (только в режиме DEV) ко всем компонентам, вокруг которых оно выполняется. Например, при работе в «строгом режиме» React намеренно выполняет двойной рендеринг компонентов, чтобы избавиться от небезопасных побочных эффектов. Эти дополнительные проверки предназначены для проверки нескольких циклов монтирования/размонтирования. Он обеспечивает не только более устойчивые компоненты, но и правильное поведение с технологией Fast Refresh во время разработки (когда компоненты монтируются/размонтируются для обновления) и новый «Offscreen API», который в настоящее время находится в разработке.
Offscreen API
Обеспечивает лучшую производительность, скрывая компоненты вместо их размонтирования, сохраняя состояние и по-прежнему вызывая эффекты монтирования/размонтирования. Это сыграет решающую роль в оптимизации таких компонентов, как вкладки, виртуализированные списки и т. д.
Root API
Немного про новое API, ориентированных на пользователя. Функция рендеринга (render) — та, которая находится в корне каждого приложения React, будет заменена на createRoot. Новый API — это шлюз для доступа к новым функциям React 18. createRoot предоставляется вместе с устаревшим API, чтобы способствовать постепенному внедрению и упрощению возможных сравнений производительности.
import ReactDOM from "react-dom";
import App from "App";
const container = document.getElementById("app");
// Old
ReactDOM.render(<App />, container);
// New
const root = ReactDOM.createRoot(container);
root.render(<App />);
Параллельный рендеринг — Concurrent Rendering
Если совсем просто про concurrent rendering то так: В обычном поведении, если React начал перерисовывать DOM, все остальные обновления в очереди блокируются и дожидаются окончания обновления. Concurrent rendering должен решить эту проблему. В конкурентном режиме рендеринг не блокируется. Он прерывается. Это улучшает UX и открывает новые возможности.
40. Что такое forward ref?
React forwardRef — это метод, который позволяет родительским компонентам передавать ссылки своим дочерним элементам. Использование forwardRef в React дает дочернему компоненту ссылку на элемент DOM, созданный его родительским компонентом. Затем это позволяет дочернему элементу читать и изменять этот элемент везде, где он используется.
В React родительские компоненты обычно используют props для передачи данных своим дочерним элементам. Представьте, что вы создаете дочерний компонент с новым набором props, чтобы изменить его поведение. Нам нужен способ изменить поведение дочернего компонента без необходимости искать состояние или повторно отображать компонент. Мы можем сделать это, используя refs. Мы можем получить доступ к узлу DOM, который представлен элементом, используя refs. В результате мы внесем в него изменения, не влияя на его состояние и не перерисовывая его.
Когда дочернему компоненту необходимо сослаться на текущий узел своего родителя, у родительского компонента должен быть способ, которым дочерний компонент может получить свою ref. Этот метод известен как forward ref.
Syntax:
React.forwardRef((props, ref) => {})
Parameters: Принимает функцию с аргументами props и ref.
Return Value: Возвращает JSX элемент.
В отличие от компонентов класса, функциональные компоненты не имеют связанного экземпляра компонента при их монтировании. Другими словами, нет никакого способа получить доступ к некоторому «экземпляру» функционального компонента через свойство ref по умолчанию.
Просто добавить ref в качестве входного аргумента для функционального компонента тоже будет не правильно. Вместо этого правильно будет использовать forwardRef.
Давайте начнем с примера: допустим, у нас есть компонент ввода в нашем приложении React, который выглядит следующим образом:
const Input = (props) => {
return <input {...props} />;
};
Теперь мы также хотим сфокусировать компонент ввода на нажатие кнопки.
Для этого нам просто нужно создать новую ссылку в нашем приложении, передать ее в Input и вызвать .focus() для элемента, верно? Неправильно!
По умолчанию свойство ref работает только с элементами HTML, а не с компонентами React.Когда мы хотим передать ссылку на компонент React, нам нужно указать React, на какой элемент HTML он должен ссылаться, поскольку в нашем компоненте может быть более одного элемента.
Вот где forwardRef становится полезным. Это позволяет нам указать, на какой именно элемент HTML мы хотим ссылаться.
Наша задача — перенаправить ссылку, которую получает компонент Input, на элемент ввода HTML.
Мы делаем это, заключая компонент в forwardRef. Функция принимает обратный вызов с двумя параметрами:
- The component props
- The ref to be forwarded
Этот callback вызов является нашим функциональным компонентом, который теперь принимает второе свойство.
const Input = forwardRef((props, ref) => {
// Here goes the content of our component
});
В возвращенном коде JSX нам теперь нужно передать ссылку, которую мы получаем в функции, в правильный компонент HTML, который в нашем случае является input элементом.
const Input = forwardRef((props, ref) => {
return (
<input
ref={ref}
{...props}
/>
);
});
import React, { useState, useRef, forwardRef } from 'React';
const Input = forwardRef((props, ref) => {
return <input ref={ref} {...props} />;
});
const App = () => {
const inputRef = useRef(null);
const [value, setValue] = useState('');
const onInputChange = (e) => {
e.preventDefault();
setValue(e.target.value);
};
const focus = () => {
inputRef.current?.focus();
};
return (
<>
<Input value={value} onChange={onInputChange} ref={inputRef} />
<button onClick={focus}>Focus</button>
</>
);
};
Функция forwardRef берет имя компонента из функции, которую она принимает в качестве параметра. Если вы используете анонимные или стрелочные функции, ваши DevTools отобразят компонент следующим образом.
Есть несколько способов избежать этого. Во-первых, вы можете использовать выражение регулярной функции с таким именем:
const Input = forwardRef(function Input(props, ref) {
return <input ref={ref} {...props} />;
});
Вы также можете установить отображаемое имя вручную, добавив следующую строку в конец файла:
Input.displayName = 'Input';
useImperativeHandle настраивает значение экземпляра, которое предоставляется родительским компонентам при использовании ref. Как всегда, в большинстве случаев следует избегать императивного кода с использованием ссылок. useImperativeHandle следует использовать с forwardRef:
useImperativeHandle(ref, createHandle, [deps])
function FancyInput(props, ref) {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
}
}));
return <input ref={inputRef} ... />;
}
FancyInput = forwardRef(FancyInput);
В этом примере родительский компонент, отображающий <'Fancyinput ref="{inputRef}" />, сможет вызвать inputRef.current.focus().
forwardRef Explained forwardRef and useImperativeHandle()41. Отличие элемента от компонента?
- Элемент — это простой объект JavaScript, который описывает состояние компонента и узел DOM, а также его желаемые свойства.
- Он содержит только информацию о типе компонента, его свойствах и любых дочерних элементах внутри него.
- immutable
- Мы не можем применять какие-либо методы к элементам.
Пример:
const element = React.createElement(
'div',
{id: 'login-btn'},
'Login'
)
- Компонент — это основной строительный блок приложения React. Это класс или функция, которая принимает ввод и возвращает элемент React.
- Он может содержать состояние и свойства и имеет доступ к методам жизненного цикла React.
- mutable
- Мы можем применять методы к компонентам.
Пример:
function Button ({ onLogin }) {
return React.createElement(
'div',
{id: 'login-btn', onClick: onLogin},
'Login'
)
}
42. Назовите основные способы оптимизации React JS?
-
useMemo()
- Это хук React, который используется для кэширования ресурсоемких функций.
- Иногда в приложении React функция, требующая больших ресурсов процессора, вызывается повторно из-за повторного рендеринга компонента, что может привести к замедлению рендеринга. Хук useMemo() можно использовать для кэширования таких функций. При использовании useMemo() функция CPU-Expensive вызывается только тогда, когда это необходимо.
-
React.PureComponent
- Это базовый классовый компонент, который проверяет состояние и props компонента, чтобы узнать, следует ли обновлять компонент.
- Вместо использования простого React.Component мы можем использовать React.PureComponent, чтобы уменьшить количество ненужных повторных рендеров компонента.
-
Управление раположением state
- это процесс перемещения состояния как можно ближе к тому месту, где он будет использован.
- Иногда в приложении React у нас есть много ненужных состояний внутри родительского компонента, что делает код менее читаемым и трудным в обслуживании. Не забывайте, что наличие множества состояний внутри одного компонента приводит к ненужному повторному рендерингу компонента.
- Состояния, менее ценные для родительского компонента, лучше перенести в отдельный компонент.
-
Ленивая загрузка
Это метод, используемый для сокращения времени загрузки приложения React. Ленивая загрузка помогает свести риск производительности веб-приложения к минимуму.
43. Strict mode в React JS?
StrictMode — это инструмент, добавленный в версии 16.3 React для выявления потенциальных проблем в приложении. Он выполняет дополнительные проверки приложения.
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
const rootElement = document.getElementById("root");
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
rootElement
);
StrictMode не отображает ничего визуального (похожего на Fragment), но обнаруживает потенциальные проблемы в коде и даёт полезные предупреждения. Строгий режим работает только в режиме разработки (development) и не влияет на сборку.
StrictMode помогает обнаружить:
- Устаревший код и устаревшие API;
- Небезопасные методы жизненного цикла;
- Неожиданные побочные эффекты
Как и всё остальное, что сделано из кода, React со временем меняется, и то, что когда-то считалось современным, в конечном итоге становится устаревшим и заменяется. Например, findDOMNode, который устарел в строгом режиме и, возможно, будет удален в будущей версии React. Другими примерами являются использование String Refs и устаревшего context API, которые вызывают некоторые “проблемы”.
Начиная с версии 16.9 React выдаёт предупреждение при использовании любого из методов жизненного цикла componentWillMount, componentWillReceiveProps и componentWillUpdate. Надеюсь, что ты уже преобразовал эти методы в более безопасные альтернативы (если нет, тебе следует как минимум добавить префикс «UNSAFE_»). Строгий режим может помочь определить небезопасные методы жизненного цикла в собственном коде и в сторонних библиотеках, а также предложит альтернативу.
В Concurrent Mode React может запускать метод рендеринга несколько раз, прежде чем вносить изменения (например, изменить DOM). Поэтому важно, чтобы этот метод не содержал побочных эффектов, которые могут привести к утечкам памяти и неверному состоянию. Строгий режим не может обнаружить побочные эффекты автоматически. Но он использует хоть простой, но умный трюк - методы constructor, render, setState и getDerivedStateFromProps запускаются дважды, тем самым облегчая обнаружение побочных эффектов. Если это приводит к какому-либо странному поведению в приложении, ты знаешь, что искать.
44. Что такое error boundaries?
Границы ошибок, представленные в версии 16 React, позволяют нам отлавливать ошибки, возникающие на этапе рендеринга.
Любой компонент, который использует один из следующих методов жизненного цикла, имеет error boundaries. В каких местах граница ошибки может обнаружить ошибку?
- Render phase
- Inside a lifecycle method
- Inside the constructor
class CounterComponent extends React.Component{
constructor(props){
super(props);
this.state = {
counterValue: 0
}
this.incrementCounter = this.incrementCounter.bind(this);
}
incrementCounter(){
this.setState(prevState => counterValue = prevState+1);
}
render(){
if(this.state.counter === 2){
throw new Error('Crashed');
}
return(
<div>
<button onClick={this.incrementCounter}>Increment Value</button>
<p>Value of counter: {this.state.counterValue}</p>
</div>
)
}
}
В приведенном выше коде, когда counterValue равно 2, мы выдаем ошибку внутри метода рендеринга.
Когда мы не используем границу ошибки, вместо ошибки мы видим пустую страницу. Так как любая ошибка внутри метода рендера приводит к размонтированию компонента. Чтобы отобразить ошибку, возникающую внутри метода рендеринга, мы используем границы ошибок.
С границами ошибок: как упоминалось выше, граница ошибок — это компонент, использующий один или оба из следующих методов: статический getDerivedStateFromError и componentDidCatch.
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
logErrorToMyService(error, errorInfo);
}
render() {
if (this.state.hasError) {
return <h4>Something went wrong</h4>
}
return this.props.children;
}
}
В приведенном выше коде функция getDerivedStateFromError отображает резервный интерфейс пользовательского интерфейса, когда метод рендеринга имеет ошибку.
componentDidCatch записывает информацию об ошибке в службу отслеживания ошибок.
Теперь с границей ошибки мы можем визуализировать CounterComponent следующим образом:
<ErrorBoundary>
<CounterComponent/>
</ErrorBoundary>
Flux-архитектура, Redux и MobX:
1. Что такое Flux - архитектура? Какие сущности она имеет?
Flux-архитектура — архитектурный подход или набор шаблонов программирования для построения пользовательского интерфейса веб-приложений, сочетающийся с реактивным программированием и построенный на однонаправленных потоках данных.
Основной отличительной особенностью Flux является односторонняя направленность передачи данных между компонентами Flux-архитектуры. Архитектура накладывает ограничения на поток данных, в частности, исключая возможность обновления состояния компонентов самими собой. Такой подход делает поток данных предсказуемым и позволяет легче проследить причины возможных ошибок в программном обеспечении.
2. Назовите ключевые принципы Redux
Redux следует трем фундаментальным принципам:
1. Единственный источник истины:** состояние всего приложения хранится в древовидном объекта - в одном хранилище. Единственное состояние-дерево облегчает наблюдение за изменениями и отладку или инспектирование приложения
2. Состояние доступно только для чтения:** единственный способ изменить состояние заключается в запуске операции - объекте, описывающем произошедшее. Это позволяет гарантировать, что ни представления, ни сетевые коллбеки не буду иметь возможности изменять состояние напрямую
3. Изменения производятся с помощью "чистых" функций:** для определения того, как изменяется состояние в зависимости от операции, создаются редукторы (reducers). Редукторы - это "чистые" функции, принимающие предыдущее состояние в качестве аргумента и возвращающие новое. Благодаря чистым функциям доступен time travel в RTK.
3. Store?
Все, что хранится в хранилище, мы называем состоянием, но не все состояния одинаково полезны. Вот какую классификацию вводит документация Redux:
Domain data — данные приложения, которые нужно отображать, использовать и модифицировать. Например, список пользователей, загруженный с сервера.
App state — данные, определяющие поведение приложения. Например, текущий открытый URL.
UI state — данные, определяющие то, как выглядит UI. Например, вывод списка в плиточном виде.
Так как стор представляет собой ядро приложения, данные внутри него должны описываться в терминах domain data и app state, но не как дерево компонентов UI. Например, такой способ формирования состояния state.leftPane.todoList.todos — плохая идея. Крайне редко дерево компонентов отражается напрямую на структуру состояния, и это нормально. Представление зависит от данных, а не данные от представления.
const store = new Store(reducers, initialState);
Метод dispatch позволит передать инструкцию для Store, что мы хотим изменить дерево состояния. Это обрабатывается с помощью только что рассмотренного редюсера. Функция reduce для хранилища будет вызываться с текущим результатом getState() и заданным действием синхронно. Его возвращаемое значение будет считаться следующим состоянием. С этого момента он будет возвращаться из getState(), и listeners будут немедленно уведомлены.
Метод subscribe позволит передать в Store функцию-подписчик, которой, когда наше дерево состояния изменится, мы сможем передать эти его новые изменения с помощью аргумента в обратном вызове .subscribe().
Заменяет редюсер, используемый в настоящее время хранилищем для вычисления состояния. Это расширенный API. Это может понадобиться, если ваше приложение реализует разделение кода и вы хотите динамически загружать некоторые редюсеры. Вам также может понадобиться это, если вы реализуете механизм горячей перезагрузки для Redux.
Возвращает текущее дерево состояний вашего приложения. Оно равно последнему значению, возвращенному редьюсером хранилища.
4. Reducers?
(reducer) — это чистая функция, которая вычисляет следующее состояние дерева на основании его предыдущего состояния и применяемого действия.
(currentState, action) => newState
Чистая функция работает независимо от состояния программы и выдаёт выходное значение, принимая входное и не меняя ничего в нём и в остальной программе. Получается, что редуктор возвращает совершенно новый объект дерева состояний, которым заменяется предыдущий.
Чего не должен делать редуктор Редуктор — это всегда чистая функция, поэтому он не должен:
Редуктор для каждой части состояния
const title = (state = '', action) => {
if (action.type === 'CHANGE_LIST_TITLE') {
return action.title
} else {
return state
}
}
const list = (state = [], action) => {
switch (action.type) {
case 'ADD_ITEM':
return state.concat([{ title: action.title }])
case 'REMOVE_ITEM':
return state.map((item, index) =>
action.index === index
? { title: item.title }
: item
default:
return state
}
}
5. Dispatcher?
Что бы обновить store необходимо вызвать метод dispatch(). Он вызывается у объекта store который вы создаёте в store.js. Этот объект принято называть store поэтому обновление состояния в моём случае выглядит так:
store.dispatch({ type: ACTION_1, value_1: "Some text" });
ACTION_1 это константа события о которой речь пойдет дальше (см. Actions).
Эта функция вызовет функцию reducer который обработает событие и обновит соответствующие поля хранилища.
6. Что такое селекторы (selectors) Redux и зачем их использовать?
*Selectors* - это функции, принимающие состояние Redux в качестве аргумента и возвращающие некоторые данные для передачи компоненту.
Например, так можно извлечь данные пользователя из состояния:
const getUserData = state => state.user.dataСелекторы имеют два главных преимущества:
1. Селектор забирает из стора конкретные нужные данные.
2. Селектор не выполняет повторных вычислений до тех пор, пока не изменится один из его аргументов
const Foo = () => {
const value = useSelector(state => state.value);
return (
<Bar value={value} />
);
};
export default Foo;
7. Что такое операция (action) в Redux?
*Actions* - это обычные JavaScript-объекты, содержащие данные приложения, которые отправляются в хранилище. Операции должны иметь свойство `type`, указывающее какой тип операции необходимо выполнить. Операции также могут содержать полезную нагрузку (payload) - данные для обновления состояния.
Вот как может выглядеть операция по добавлению новой задачи в список:
// здесь используется константа
{
type: ADD_TODO,
text: 'Добавление задачи в список'
}
8. Проведите сравнение Redux и Flux
Отличия между Redux и Flux можно свести к следующему:
1. **Недопустимость мутаций:** во Flux состояние может быть изменяемым, а Redux требует, чтобы состояние было иммутабельным, и многие библиотеки для Redux исходят из предположения, что вы никогда не будете менять состояние напрямую. Вы можете обеспечить иммутабельность состояния с помощью таких пакетов, как `redux-immutable-state-invariant`, `Immutable.js` или условившись с другими членами команды о написании иммутабельного кода
2. **Осторожность в выборе библиотек:** Flux не пытается решать такие проблемы, как повторное выполнение/отмена выполнения, стабильность (постоянство) кода или проблемы, связанные с обработкой форм, явно, а Redux имеет возможность к расширению с помощью промежуточного программного обеспечения (middleware) и предохранителей хранилища, что породило богатую экосистему
3. **Отсутствие интеграции с Flow:** Flux позволяет осуществлять очень выразительную статическую проверку типов, а Redux пока не поддерживает такой возможности
9. В чем разница между `mapStateToProps()` и `mapDispatchToProps()`?
`mapStateToProps()` - это утилита, помогающая компонентам получать обновленное состояние (которое было обновлено другим компонентом):
const mapStateToProps = (state) => {
return {
todos: getVisibleTodos(state.todos, state.visibilityFilter)
}
}
`mapDispatchToProps()` - утилита, помогающая компонентам вызывать операции (которые могут привести к обновлению состояния приложения):
const mapDispatchToProps = (dispatch) => {
return {
onTodoClick: (id) => {
dispatch(toggleTodo(id))
}
}
}
Ваши компоненты должны быть связаны только с отображением информации. Единственное место, откуда они должны получать информацию, — это пропсы.
Отдельно от «отображения материала» (компонентов) стоит: как вы получаете данные для отображения, и как вы обрабатываете события. Для этого есть контейнеры.
class FancyAlerter extends Component {
sendAlert = () => {
this.props.sendTheAlert()
}
render() {
<div>
<h1>Today's Fancy Alert is {this.props.fancyInfo}</h1>
<Button onClick={sendAlert}/>
</div>
}
}
Вот где появляется mapDispatchToProps: в соответствующем контейнере:
function mapDispatchToProps(dispatch) {
return({
sendTheAlert: () => {dispatch(ALERT_ACTION)}
})
}
function mapStateToProps(state) {
return({fancyInfo: "Fancy this:" + state.currentFunnyString})
}
export const FancyButtonContainer = connect(
mapStateToProps, mapDispatchToProps)(
FancyAlerter
)
Теперь FancyButtonContainer знает о redux, dispatch, store, state и... прочем.
Компонент в шаблоне, FancyAlerter, который выполняет рендеринг, не должен знать ни о чем из этого: он получает свой метод для вызова при нажатии кнопки через свои props.
И... mapDispatchToProps был полезным средством, которое предоставляет Redux, позволяющим контейнеру легко передавать эту функцию в обернутый компонент в своих реквизитах.
10. Обязательно ли хранить состояние всех компонентов в хранилище Redux?
Данные приложения следует хранить в хранилище Redux, а состояние компонентов пользовательского интерфейса в соответствующих компонентах. У создателя Redux Дэна Абрамова по этому поводу есть статья под названием "Следует ли вам использовать Redux?"
11. Как рекомендуется получать доступ к хранилищу Redux?
Лучшим способом получить хранилище в компоненте является использование функции `connect()`, которая создает новый компонент, оборачивающий существующий. Этот паттерн называется *компоненты высшего порядка*, он является предпочтительным способом расширения функциональности компонента в React. �



