пїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ
Слишком длинный поисковый запрос.
По вашему запросу ничего не найдено :(
Убедитесь, что запрос написан правильно, или посмотрите другие
наши статьи:
Сначала JavaScript может показаться довольно простым языком программирования. Однако он гораздо более сложный, чем можно предположить на первый взгляд. Многие тонкости JavaScript приводят к ряду распространенных проблем и мешают коду вести себя так, как задумано. В этой статье мы рассмотрим типичные сложности, с которыми сталкиваются JavaScript-разработчики.
Проблема №1. Неправильные ссылки на this
Среди разработчиков JavaScript часто возникает путаница с ключевым словом this.
По мере того как методы написания кода и шаблоны проектирования JavaScript становились всё более сложными, наблюдается увеличение количества самоссылочных областей видимости внутри колбэков и замыканий, которые являются довольно распространённым источником проблем с ключевым словом this, вызывая ошибки в JavaScript.
Рассмотрим следующий пример кода:
Выполнение данного кода приводит к следующей ошибке:
Почему? Всё дело в контексте. Ошибка возникает потому, что при вызове setTimeout() вы на самом деле вызываете window.setTimeout(). В результате анонимная функция, передаваемая в setTimeout(), определяется в контексте объекта window, у которого нет метода clearBoard().
Традиционное решение, совместимое со старыми браузерами, состоит в том, чтобы сохранить ссылку на this в переменной, которая затем будет унаследована замыканием, например:
Альтернативно, в новых браузерах можно использовать метод bind(), чтобы передать правильную ссылку:
Проблема №2. Предположение о наличии блочной области видимости
Как обсуждалось в нашем руководстве по найму JavaScript-разработчиков, распространённым источником путаницы среди JavaScript-разработчиков (а значит, и частой причиной ошибок) является предположение, что в JavaScript создаётся новая область видимости для каждого блока кода. Хотя это верно для многих других языков, в JavaScript это не так. Рассмотрим следующий пример кода:
Если вы подумали, что вызов console.log() выведет либо undefined, либо вызовет ошибку, вы ошиблись. Хотите верьте, хотите нет, но результатом будет 10. Почему?
В большинстве других языков приведённый выше код привёл бы к ошибке, так как "жизнь" (то есть область видимости) переменной i была бы ограничена циклом for. В JavaScript же это не так, и переменная i остаётся доступной даже после завершения цикла, сохраняя своё последнее значение после выхода из цикла. (Такое поведение называется подъёмом переменных, или variable hoisting.)
Поддержка блочных областей видимости в JavaScript возможна с помощью ключевого слова let. Ключевое слово let уже давно поддерживается браузерами и серверными движками JavaScript, такими как Node.js. Если это новость для вас, стоит потратить время на изучение областей видимости, прототипов и других важных аспектов.
Проблема №3. Создание утечек памяти
Утечки памяти в JavaScript практически неизбежны, если вы сознательно не пишете код, чтобы их избегать. Существует множество способов возникновения таких утечек, и мы рассмотрим два из самых распространенных примеров.
Пример утечки памяти 1: «Зависшие» ссылки на устаревшие объекты
Примечание: Этот пример относится только к устаревшим движкам JavaScript — современные имеют сборщики мусора (GC), которые достаточно умны, чтобы справляться с такими случаями.
Рассмотрим следующий код:
Если запустить этот код и отслеживать использование памяти, вы обнаружите значительную утечку — 1 мегабайт каждую секунду! Даже ручной сборщик мусора не помогает. Похоже, что при каждом вызове replaceThing утечка возникает из-за longStr. Но почему?
Давайте разберемся подробнее:
Каждый объект theThing содержит собственный объект longStr размером 1 МБ. Каждую секунду, при вызове replaceThing, сохраняется ссылка на предыдущий объект theThing в переменной priorThing. Но, на первый взгляд, это не должно быть проблемой, так как каждый раз предыдущий объект priorThing теряет свою ссылку (когда priorThing переназначается на theThing). Кроме того, он используется только в основном теле функции replaceThing и в функции unused, которая, фактически, никогда не используется.
Так почему же возникает утечка памяти?
Чтобы понять это, нужно глубже разобраться в работе JavaScript. Замыкания обычно реализуются так, что каждая функция ссылается на объект, представляющий её лексическую область видимости. Если обе функции внутри replaceThing используют priorThing, важно, чтобы они обе получили один и тот же объект, даже если priorThing переназначается снова и снова, чтобы обе функции имели общую лексическую область. Но как только переменная используется в замыкании, она попадает в общую лексическую область, используемую всеми замыканиями в этом контексте. Этот нюанс и приводит к этой сложной утечке памяти.
Пример утечки памяти 2: Циклические ссылки
Рассмотрим следующий фрагмент кода:
Здесь функция onClick имеет замыкание, которое сохраняет ссылку на element (через element.nodeName). При назначении onClick элементу element.click создается циклическая ссылка: element ? onClick ? element ? onClick ? element…
Интересно, что даже если элемент удален из DOM, эта циклическая ссылка предотвратит сборку мусора для element и onClick, что приведет к утечке памяти.
Как избежать утечек памяти: Основы
Управление памятью в JavaScript (и, в частности, сборка мусора) в основном основано на концепции достижимости объектов.
Следующие объекты считаются достижимыми и известны как "корни":
Объекты, на которые есть ссылки из текущего стека вызовов (то есть все локальные переменные и параметры в функциях, которые в данный момент выполняются, а также все переменные в области видимости замыкания).
Все глобальные переменные.
Объекты остаются в памяти до тех пор, пока они доступны из любого из корней через прямую ссылку или цепочку ссылок.
В браузерах есть сборщик мусора, который очищает память, занятую недостижимыми объектами. Иными словами, объекты удаляются из памяти только в том случае, если сборщик мусора считает, что они недостижимы. К сожалению, легко столкнуться с "зомби"-объектами, которые больше не используются, но сборщик мусора все еще считает их достижимыми.
Проблема №4. Путаница с равенством
Одним из удобств JavaScript является автоматическое приведение любого значения, используемого в логическом контексте, к булевому типу. Однако в некоторых случаях это может быть столь же запутанным, сколь и удобным. Например, следующие выражения часто вызывают трудности у многих разработчиков на JavaScript:
Что касается последних двух выражений, несмотря на то, что они пустые (что могло бы заставить вас думать, что они будут оценены как false), как {} (объект), так и [] (массив) на самом деле являются объектами. В соответствии со спецификацией ECMA-262, любое объектное значение в JavaScript будет приведено к булевому значению true.
Как показывают эти примеры, правила приведения типов иногда могут быть крайне запутанными. Соответственно, если явно не требуется приведение типов, обычно рекомендуется использовать операторы === и !== (вместо == и !=) для избежания непреднамеренных побочных эффектов приведения типов. (Операторы == и != автоматически выполняют приведение типов при сравнении, тогда как === и !== сравнивают без приведения типов.)
Поскольку речь идет о приведении типов и сравнении, стоит упомянуть, что сравнение NaN с чем-либо (даже с самим NaN!) всегда возвращает false. Следовательно, нельзя использовать операторы равенства (==, ===, !=, !==) для проверки, является ли значение NaN. Вместо этого используйте встроенную глобальную функцию isNaN():
Проблема №5. Неэффективная манипуляция с DOM
JavaScript делает манипуляции с DOM (т.е. добавление, изменение и удаление элементов) относительно простыми, но не способствует их эффективному выполнению.
Частый пример — код, который добавляет серию DOM-элементов по одному. Добавление DOM-элемента — это затратная операция, и код, который последовательно добавляет несколько элементов, работает неэффективно и может давать сбои.
Одной из эффективных альтернатив при необходимости добавить несколько элементов в DOM является использование document fragments (фрагментов документа), что улучшит производительность и эффективность.
Например:
Кроме явного улучшения эффективности такого подхода, создание прикрепленных DOM-элементов — это ресурсоемкая операция. Однако создание и изменение элементов, пока они отсоединены, а затем их прикрепление к документу значительно повышает производительность.
Проблема №6. Некорректное использование определений функций внутри циклов for
Рассмотрим следующий код:
В приведенном выше коде, если бы у нас было 10 элементов input, при клике на любой из них отображалось бы “Это элемент #10”! Это происходит потому, что к моменту вызова обработчика onclick значение переменной i уже будет равно 10 (для всех элементов).
Вот как можно исправить эту проблему в JavaScript, чтобы добиться желаемого поведения:
В этой исправленной версии кода функция makeHandler выполняется немедленно каждый раз, когда мы проходим через цикл, и каждый раз получает текущее значение i+1 и связывает его с переменной num, которая имеет область видимости. Внешняя функция возвращает внутреннюю функцию (которая также использует эту область видимости для переменной num), и обработчик onclick элемента устанавливается на эту внутреннюю функцию. Это гарантирует, что каждый обработчик onclick получает и использует правильное значение переменной i (через переменную num).
Проблема №7. Неправильное использование прототипного наследования
Необычно большое количество разработчиков JavaScript не полностью понимают и поэтому не используют возможности прототипного наследования.
Вот простой пример:
Это выглядит довольно просто. Если передан параметр name, используется его значение, в противном случае имя устанавливается в ‘default’. Например:
Но что, если мы сделаем так:
Тогда мы получим:
Но разве не было бы лучше, если бы значение вернулось к ‘default’? Это можно легко сделать, если изменить исходный код для использования прототипного наследования следующим образом:
В этой версии объекта BaseObject свойство name наследуется от прототипного объекта, где оно установлено (по умолчанию) на 'default'. Таким образом, если конструктор вызывается без параметра name, имя будет по умолчанию 'default'. Точно так же, если свойство name удаляется из экземпляра BaseObject, то будет выполнен поиск в цепочке прототипов, и свойство name будет получено из прототипного объекта, где его значение по-прежнему 'default'. Таким образом, теперь мы получаем:
Проблема №8. Создание некорректных ссылок на методы экземпляра
Определим простой объект и создадим его экземпляр следующим образом:
Теперь, для удобства, создадим ссылку на метод whoAmI, чтобы мы могли вызывать его просто как whoAmI(), а не как obj.whoAmI():
Чтобы убедиться, что мы сохранили ссылку на функцию, выведем значение нашей новой переменной whoAmI:
Результат:
Пока всё выглядит нормально.
Но обратите внимание на разницу при вызове obj.whoAmI() и нашей ссылки whoAmI():
Что пошло не так? Вызов whoAmI() происходит в глобальном контексте, поэтому this устанавливается в window (или, в строгом режиме, в undefined), а не в экземпляр MyObjectFactory! Другими словами, значение this обычно зависит от контекста вызова.
Стрелочные функции ((params) => {}), в отличие от обычных функций function(params) {}, имеют статическое значение this, которое не зависит от контекста вызова. Это даёт нам обходной путь:
Вы могли заметить, что, хотя вывод совпадает, this является ссылкой на фабрику, а не на экземпляр. Вместо того чтобы дальше исправлять эту проблему, стоит рассмотреть подходы в JavaScript, которые не зависят от this (или даже new), как объяснено в статье "Как JS-разработчику, это то, что не даёт мне спать ночью".
Проблема №9. Передача строки в качестве первого аргумента для setTimeout или setInterval
Для начала, давайте проясним один момент: передача строки в качестве первого аргумента для setTimeout или setInterval сама по себе не является ошибкой. Это вполне законный код JavaScript. Проблема заключается скорее в производительности и эффективности. Часто упускается из виду, что если вы передаете строку в качестве первого аргумента для setTimeout или setInterval, она будет передана конструктору функции для преобразования в новую функцию. Этот процесс может быть медленным и неэффективным, и редко бывает необходим.
Альтернативой передаче строки в качестве первого аргумента для этих методов является передача функции. Рассмотрим пример.
Вот типичное использование setInterval и setTimeout, где передается строка в качестве первого параметра:
Лучший выбор — передать функцию в качестве начального аргумента, например:
Проблема JavaScript № 10: Отсутствие использования «строгого режима»
Как объясняется в нашем Руководстве по найму JavaScript-разработчиков, «строгий режим» (то есть включение 'use strict'; в начале ваших файлов JavaScript) является способом добровольно применить более строгий анализ и обработку ошибок в вашем JavaScript-коде во время выполнения, а также сделать ваш код более безопасным.
Хотя, признаться, отсутствие использования строгого режима не является настоящей «ошибкой», его использование все больше поощряется, а его отсутствие все чаще считается плохим тоном.
Вот некоторые ключевые преимущества строгого режима:
Упрощает отладку. Ошибки в коде, которые в противном случае были бы проигнорированы или молчали бы, теперь будут генерировать ошибки или выбрасывать исключения, что позволяет быстрее обнаруживать проблемы с JavaScript в вашем коде и направляет вас к их источнику.
Предотвращает случайные глобальные переменные. Без строгого режима присвоение значения необъявленной переменной автоматически создает глобальную переменную с этим именем. Это одна из самых распространенных ошибок в JavaScript. В строгом режиме попытка сделать это вызывает ошибку.
Исключает преобразование this. Без строгого режима ссылка на значение this, равное null или undefined, автоматически преобразуется в глобальную переменную globalThis. Это может вызывать множество раздражающих ошибок. В строгом режиме ссылка на значение this, равное null или undefined, вызывает ошибку.
Не допускает дублирующие имена свойств или значения параметров. Строгий режим выбрасывает ошибку, когда обнаруживает дублирующее имя свойства в объекте (например, var object = {foo: "bar", foo: "baz"};) или дублирующий аргумент функции (например, function foo(val1, val2, val1){};), тем самым обнаруживая почти наверняка ошибку в вашем коде, которую вы могли бы потратить значительное время на её отслеживание.
Упрощает использование eval(). Есть некоторые различия в поведении eval() в строгом режиме и в нестрогом режиме. Наиболее значительное отличие заключается в том, что в строгом режиме переменные и функции, объявленные внутри eval(), не создаются в содержащей области видимости. (В нестрогом режиме они создаются в содержащей области видимости, что также может быть общим источником проблем в JavaScript.)
Выбрасывает ошибку при некорректном использовании delete. Оператор delete (используемый для удаления свойств из объектов) не может быть использован для неконфигурируемых свойств объекта. Нестрогий код будет молчаливо терпеть неудачу при попытке удалить неконфигурируемое свойство, в то время как строгий режим выбросит ошибку в таком случае.
Устранение проблем JavaScript с умным подходом
Как и в любой технологии, чем лучше вы понимаете, почему и как JavaScript работает и не работает, тем более надежным будет ваш код, и тем больше вы сможете эффективно использовать настоящую мощь языка. Напротив, недостаток правильного понимания парадигм и концепций JavaScript — это то, где скрывается множество проблем JavaScript. Тщательное знакомство с нюансами и тонкостями языка — это наиболее эффективная стратегия для повышения вашей квалификации и увеличения продуктивности.
Что такое события?
События – это действия, происходящие в момент взаимодействия пользователя со страницей, например, нажатие на элемент, ввод текста в поле или загрузка страницы.
Браузер сообщает системе о том, что что-то произошло и что это нужно обработать. Это что-то обрабатывается путем регистрации функции, которая называется обработчиком событий, и которая отслеживает событий определенного типа.
Что значит «обработать событие»?
Чтобы было проще, давайте предположим, что вы хотите посетить мероприятие по веб-разработке в вашем городе.
Для этого вы регистрируетесь на эту встречу под названием «Women Who Code» и подписываетесь на уведомления. В таком случае, когда бы ни была запланирована новая встреча, вы получите оповещение. Это и есть обработка событий!
Здесь «событие» - это новая встреча. При публикации новой встречи, веб-сайт meetup.com улавливает это изменение, и, как следствие, «обрабатывает» это событие. После чего он оповещает вас, тем самым совершая «действие», связанное с этим событием.
В браузере события обрабатываются точно так же. Браузер обнаруживает изменение и оповещает функцию (обработчик событий), которая прослушивает определенный тип события. Далее функция выполняет необходимые действия.
Давайте посмотрим на пример обработчика события
click
const buttonContainer = document.querySelector('.buttons');
console.log('buttonContainer', buttonContainer);
buttonContainer.addEventListener('click', event => {
console.log(event.target.value)
})
Какие есть типы событий?
Событие может произойти в любой момент взаимодействия пользователя со страницей. Это может быть прокрутка пользователем страницы, нажатие на элемент страницы или загрузка страницы.
Далее перечислены самые распространенные события:
onclick dblclick mousedown mouseup mousemove keydown keyup touchmove touchstart touchend onload onfocus onblur onerror onscroll
Различные фазы событий – фаза погружения, фаза цели и фаза всплытия
Когда событие перемещается по модели DOM, а именно всплывает наверх или просачивается вниз, это называется распространением события. Событие распространяется по DOM-дереву.
События происходят в две фазы: фаза всплытия (bubbling phase) и фаза погружения (capturing phase).
В фазе погружения, которую также называют фазой просачивания, событие «просачивается» к элементу, который вызвал событие.
Оно начинает с элемента корневого уровня и обработчика, а далее распространяется вниз к элементу. Фаза погружения завершается тогда, когда событие достигает цели -
target
.
В фазе всплытия событие «всплывает» по DOM-дереву. Сначала оно погружается и обрабатывается самым дальним обработчиком (тем, который находится ближе всего к элементу, на котором произошло событие). После чего оно всплывает (или распространяется) на более высокие уровни DOM-дерева, все выше к своим родительским узлам, а затем и к его корню.
Вот подсказка, которая поможет вам запомнить это:
trickle down, bubble up
А ниже представлена инфографика с quirkmode, которая поможет вам лучше разобраться:
/ \
---------------| |-----------------
| элемент1 | | |
| -----------| |----------- |
| |элемент2 | | | |
| ------------------------- |
| Событие ВСПЛЫВАЕТ |
-----------------------------------
| |
---------------| |-----------------
| элемент1 | | |
| -----------| |----------- |
| |элемент2 \ / | |
| ------------------------- |
| Событие ПОГРУЖАЕТСЯ |
-----------------------------------
Единственное, на что нужно обратить внимание, так это на то, что независимо от того, на какой фазе вы регистрируете обработчик событий, ВСЕГДА происходят обе фазы. Все событий всплывают по умолчанию.
Вы можете зарегистрировать обработчики событий для любой из фаз с помощью функции
addEventListener(type, listener, useCapture)
. Если параметр
useCapture
задается как
false
, то считаем, что событие находится в фазе всплытия. Иначе – в фазе погружения.
Порядок фаз зависит от браузера
Для того, чтобы проверить, какой браузер первым принимает погружение, попробуйте выполнить следующий код в JSFiddle:
const childOne = document.getElementById("child-one");
const childOneHandler = () => {
console.log('Captured on child one')
}
const childOneHandlerCatch = () => {
console.log('Captured on child one in capture phase')
}
childOne.addEventListener("click", childOneHandler);
childOne.addEventListener("click", childOneHandlerCatch, true);
В Firefox, Safari и Chrome результат будет такой:
(Первыми срабатывают события в фазе погружения)
Как прослушивать события?
Есть два способа прослушивания событий:
addEventListener
встраиваемые события, такие как
onclick
//addEventListener
document.getElementByTag('a').addEventListener('click', onClickHandler);
//inline using onclick
Click me
Что лучше – встраиваемое событие или
addEventListener
?
С помощью
addEventListener
можно регистрировать неограниченное количество обработчиков событий.
Также можно использовать
removeEventListener
для удаления обработчиков событий.
Флаг
useCapture
можно использовать для указания, в какой фазе должно обрабатываться событие – в фазе погружения или в фазе всплытия.
Примеры программного кода в реальном действии
Вы можете попробовать запустить эти события в JSFiddle, чтобы поэкспериментировать с ними.
const wrapperDiv = document.getElementById("wrapper-div");
const childOne = document.getElementById("child-one");
const childTwo = document.getElementById("child-two");
const childOneHandler = () => {
console.log('Captured on child one')
}
const childTwoHandler = () => {
console.log('Captured on child two')
}
const wrapperDivHandler = () => {
console.log('Captured on wrapper div')
}
const childOneHandlerCatch = () => {
console.log('Captured on child one in capture phase')
}
const childTwoHandlerCatch = () => {
console.log('Captured on child two in capture phase')
}
const wrapperDivHandlerCatch = () => {
console.log('Captured on wrapper div in capture phase')
}
childOne.addEventListener("click", childOneHandler);
childTwo.addEventListener("click", childTwoHandler);
wrapperDiv.addEventListener("click", wrapperDivHandler);
childOne.addEventListener("click", childOneHandlerCatch, true);
childTwo.addEventListener("click", childTwoHandlerCatch, true);
wrapperDiv.addEventListener("click", wrapperDivHandlerCatch, true);
Резюмируя
Фазы событий – это погружение (DOM -> target), всплытие (target -> DOM) и цель (target).
Cобытия можно прослушивать с помощью
addEventListener
или встраиваемых событий, таких как
onclick
.
С addEventListener можно добавлять несколько событий, а с onclick нельзя.
onclick можно добавить в качестве атрибута HTML, а addEventListener можно добавить только внутри элементов
