https://habrahabr.ru/company/ruvds/blog/346022/- Разработка веб-сайтов
- JavaScript
- Блог компании RUVDS.com
JavaScript — это
потрясающий инструмент, который можно найти буквально в каждом углу современного интернета. Но даже несмотря на его невероятную распространённость, и профессионалам в области JS всегда будет чему поучиться. Всегда найдётся что-то такое, чего они не знают.
В этом материале вы найдёте разбор двенадцати вопросов о JavaScript, на которые нередко не могут ответить даже опытные разработчики. Сначала мы рассмотрим десять типичных вопросов, включая такие, которые часто всплывают на собеседованиях. Оставшиеся два вопроса посвящены более сложным и неоднозначным вещам, в частности, использованию JS для улучшения производительности веб-страниц и разработке приложений, которые не теряют актуальности с течением времени.
Вопрос №1. Что такое прототипное наследование?
Практически всё в JavaScript — это объекты. У каждого объекта есть прототип, от которого он наследует свойства и методы. Если объект не включает в себя запрошенное свойство, JavaScript выполнит поиск этого свойства в прототипе объекта. При этом поиск будет выполняться по цепочке прототипов до тех пор, пока не будет найдено то, что нужно. Если же поиск успехом не увенчается, будет возвращена ошибка.
Прототипы весьма полезны для создания объектов, которые имеют одинаковые свойства и методы. Когда всё это определено на уровне прототипа, требуется лишь одна копия подобных сущностей, что ведёт к эффективному использованию памяти.
var parent = {inherit: true}
var childA = Object.create(parent);
var childB = {};
Object.setPrototypeOf(childB, parent);
class childC extends parent {}
Прототипы можно добавлять к объектам при создании этих объектов, используя команду
Object.create()
, или после создания, командой
Object.setPrototypeOf()
. В стандарте ES2015 предусмотрено ключевое слово
class
, тут же имеется и команда
extends
, которая позволяет использовать заданное при её вызове значение как прототип объекта.
Вопрос №2. Как можно использовать JavaScript для повышения доступности веб-проектов?
Современные
средства для обеспечения доступности веб-сайтов для людей с ограниченными возможностями, часто умеют обрабатывать JavaScript и динамическое содержимое страниц. И JS и динамические данные можно состыковать со средствами обеспечения доступности, при этом стоит учитывать, что в данном случае лучше всего, когда, например, скрипты используются в роли средств расширения функциональности проекта а не неких возможностей, совершенно необходимых для его правильной работы.
Обычный способ помощи пользователям в работе с сайтом заключается в предоставлении удобных средств для навигации по объектам страниц, с которыми можно взаимодействовать. Речь идёт об управлении фокусом. Например, если на странице появляется календарь, у пользователя должна быть возможность пользоваться им без мыши, в частности — с помощью клавиш-стрелок. А именно, стрелки перехода влево и вправо можно использовать (при условии, что в одной строке экранного отображения календаря выводится семь дней) для перехода по дням, а клавиши для перехода вверх и вниз — для переключения между неделями. Реализуется подобное путём прослушивания событий клавиатуры в то время, когда календарь получает фокус.
Если изменение важных данных реализуется средствами JavaScript, например, при заполнении формы обратной связи, новые данные следует передавать скринридеру. Часто подобное реализуется путём маркировки соответствующего контейнера как интерактивной области.
Вопрос №3. Что такое всплытие событий и чем оно отличается от перехвата событий?
Всплытие событий используется при реализации делегирования событий. Если подписаться на события родительского элемента, можно получить сведения о событиях и для его потомков
И перехват, и всплытие событий являются частью процесса, который называется «распространение событий», в ходе которого браузер реагирует на события, происходящие на странице. Более старые браузеры выполняли либо одно, либо другое, но в наши дни все браузеры поддерживают и перехват, и всплытие событий.
Первая фаза — фаза перехвата — выполняется сразу после того, как происходит событие. Событие начинается на самом верхнем уровне, которым является либо объект
document
, либо объект
window
в зависимости от события. Отсюда оно опускается, проходя через тег
<html>
и через то, что находится в этом теге, до тех пор, пока не достигнет элемента, в пределах которого оно возникло.
Затем происходит вторая фаза — всплытие события. В её ходе повторяется тот же процесс, но наоборот. Всё начинается с элемента, который вызвал событие, оно «всплывает» до корневого элемента
<html>
. При добавлении прослушивателей событий ожидается именно такое поведение системы.
Вопрос №4. Как делегирование событий улучшает код на сайтах с множеством интерактивных элементов?
Веб-сайты нередко полны динамических элементов, которые постоянно меняются. Если подобные элементы должны быть ещё и интерактивными, понадобится некий способ наблюдения за событиями, которые возникают, когда пользователь с ними взаимодействует. Если каждому элементу понадобится собственный прослушиватель событий, это замусорит код и увеличит нагрузку на браузер.
Делегирование событий — это техника, которая использует механизм всплытия событий. Добавляя прослушиватель к родительскому элементу, разработчик может наладить обработку событий для его потомков.
parentEl.addEventListener('click', function(e) {
if(e.target && e.target.nodeName == 'BUTTON') {
// Щелчок по кнопке
} });
Внутри функции обратного вызова прослушивателя события целевой элемент события будет представлен параметром
target
, который можно использовать для принятия решения о дальнейших действиях. Например, атрибут этого параметра
data
может хранить идентификатор для доступа к свойствам объекта.
Вопрос №5. Что такое замыкания и как они могут помочь в организации кода?
Функции в JavaScript используют то, что называется «лексической областью видимости». Это означает, что у них есть доступ к переменным, определённым в области видимости, включающей их, но те переменные, которые объявлены внутри функций, недоступны извне.
function outer() {
let x = 'Web Designer';
function shout() {
alert(`I love ${x}!`);
}
shout(); }
Вызов функции
outer()
приведёт к показу сообщения «I love Web Designer!», но если к функции
shout()
или к переменной
x
попытаться обратиться за пределами функции
outer()
, окажется, что и та и другая не определены. Замыкание — это комбинация функции и её лексического окружения. В нашем примере замыкание — это функция
outer()
.
Замыкания полезны при создании больших наборов компонентов, так как всё, объявленное внутри одного замыкания, не затрагивает другие. Замыкания можно использовать для создания приватных функций и переменных способами, напоминающими те, что применяются в других объектно-ориентированных языках вроде Python. Шаблон «модуль» широко использует замыкания для обеспечения структурированных способов взаимодействия модулей.
Вопрос №6. Что означает строчка 'use strict' в верхней части блока кода?
В ES5 описан особый вариант JavaScript, называемый строгим режимом (strict mode). В строгом режиме использование неоднозначных конструкций из более ранних версий языка вызывает ошибки вместо того, чтобы приводить к незапланированному поведению.
function strictFunction() {
'use strict';
myVar = 4; //ReferenceError }
В вышеприведённом фрагменте кода мы пытаемся присвоить значение необъявленной переменной. За пределами строгого режима выполнение подобной команды приведёт к добавлению переменной
myVar
к глобальной области видимости, что, если не уделять подобным вещам достаточно внимания, способно полностью поменять функционал скрипта. В строгом режиме подобное приводит к выдаче сообщения об ошибке и, таким образом, предотвращает возможное нарушение функционирования программы. Модули ES2015 используют строгий режим по умолчанию, но в замыканиях, созданных с помощью функций, команда
'use strict'
может быть использована на уровне функции, так же, как и на уровне всего файла.
Вопрос №7. Что термин «поднятие переменных» означает в применении к JavaScript?
Одной из особенностей JavaScript является тот факт, что программы, написанные на нём, распространяются в нескомпилированном виде. Браузер компилирует скрипты, что называется, «на лету», и в ходе этого процесса делает «заметки» о функциях и переменных, объявленных в этих скриптах.
После первого просмотра кода, браузер выполняет второй проход, представляющий собой выполнение программы, уже зная о том, где применяются функции и переменные. При выполнении фрагмента кода объявления функций и переменных «поднимаются» в верхнюю часть этого фрагмента.
welcome("Matt"); //"Welcome, Matt."
function welcome(name) {
return `Welcome, ${name}.`;
}
В этом примере функцию
welcome()
можно использовать до её объявления в коде, так как она «поднимается» в верхнюю часть скрипта.
Вопрос №8. Чем стрелочные функции отличаются от обычных функций?
В ES2015 появилось множество изменений, и одним из них, довольно заметным, стало введение стрелочных функций.
function CountUp() {
this.x = 0;
setInterval(() => console.log(++this.x), 1000); }
var a = new CountUp();
Основное отличие стрелочных функций от обычных функций, даже если не смотреть на то, что они короче, заключается в том, что стрелочные функции не задают собственное значение для
this
. Вместо этого они используют значение
this
блока, в который они включены. В вышеприведённом примере при обращении к
this.x
каждую секунду будут выводиться числа 1, 2, 3, и так далее. При использовании в похожей ситуации обычной функции,
this
имело бы значение
undefined
, что привело бы к выводу
NaN
. Тело стрелочной функции представляет собой её возвращаемое значение. Это делает особенно удобным использование стрелочных функций в промисах. Обычные функции, в отличие от стрелочных, должны явно возвращать некое значение, иначе автоматически будет возвращено
undefined
.
Вопрос №9. В каких ситуациях следует использовать ключевые слова let и const?
Ещё одним фундаментальным новшеством в ES2015 стало введение ключевых слов
let
и
const
, как альтернативных способов объявления переменных. Область видимости таких переменных ограничена блоком, в котором они были объявлены. Это даёт больше уверенности в том, что переменные, созданные в различных блоках кода, не повлияют на то, что находится за пределами этих блоков.
for(let x=1; x<=3; x++) {
console.log(x); // 1, 2, 3}
console.log(x); // "x is not defined"
Если значение переменной в процессе выполнения программы не меняется, используйте
const
вместо
let
. При попытке переопределения подобной переменной, которую правильнее называть «константой», будет выдана ошибка. При этом надо учитывать, что при таком подходе внутреннее содержание объектов и массивов, ссылки на которые записаны в константы, может меняться, но они не могут быть заменены на новые объекты.
Переменные, объявленные с использованием
let
и
const
, не поднимаются, в отличие от переменных, объявленных с помощью ключевого слова
var
, поэтому к ним нельзя обращаться до их инициализации. Пространство между началом блока кода и местом инициализации переменной известно как «временная мёртвая зона», нередко это может быть причиной путаницы.
Вопрос №10. Что такое функциональное программирование и каковы его особенности?
Чистая функция
Функциональное программирование представляет собой подход к разработке программ, сущность которого заключается в том, что данные, представляющие собой состояние приложения, обрабатываются исключительно с помощью функций. Если такие функции не производят побочных эффектов, в итоге получается код, с которым легко работать, и который просто понять.
Обычно JS-проекты строят с использованием принципов объектно-ориентированного программирования. Информация о текущем состоянии программы хранится в объектах, а если на странице что-то меняется, сведения об изменениях записываются в эти объекты.
Функциональное программирование — это, фактически, другой стиль мышления. Надо сказать, что языки вроде F# используют подобные принципы уже очень давно. При этом в ES2015 появились некоторые важные механизмы, расширяющие возможности функционального программирования на JS.
Если, при разработке веб-проекта, придерживаться правил функционального программирования, то нужно учитывать, что все операции должны производиться внутри так называемых «чистых» функций. Это — функции, на которые не действуют данные, находящиеся за пределами области видимости этих функций. Другими словами, когда такой функции предают одни и те же данные, она всегда должна возвращать один и тот же результат.
Кроме того, это означает, что у функций не должно быть совместного доступа к неким внешним по отношению к ним данным, например, представляющим собой состояние приложения. Если приложению нужно изменить состояние, оно должно передать его функции в виде параметра.
И наконец, в коде нужно избегать изменения существующих значений. Например, при выполнении операций, подразумевающих изменение объектов, должны возвращаться копии этих объектов с изменёнными значениями. Это способствует избавлению от побочных эффектов, которые приводят к ошибкам и усложняют тестирование кода.
Вопрос №11. Как использовать JavaScript для улучшения производительности веб-страниц?
Сегодня основной объём просмотров веб-страниц выполняется со смартфонов или планшетов. При этом далеко не у всех есть самые современные устройства. Поэтому то, насколько быстро страницы реагируют на действия пользователя, очень важно. Любые «тормоза» в работе сайта могут вылиться в потерю клиента. К счастью, в JavaScript есть средства, которые помогают этого избежать.
▍Избегайте ненужных воздействий на прокрутку страницы
«Рваная» прокрутка — это явный признак того, что на странице выполняются какие-то программные действия. В некоторых случая браузер вынужден ждать из-за того, что на странице имеются некие прослушиватели событий. Так, события, такие, как
wheel
или
touchmove
могут отменять прокрутку, в результате страница вынуждена ждать до тех пор, пока событие завершится перед тем, как начнётся стандартное поведение прокрутки.
Это может привести к скачкам при прокрутке страницы и к её непостоянной скорости, что вызывает у пользователей плохие впечатления от работы со страницей.
document.addEventListener('touchmove', handler, {passive: true});
Для того, чтобы этого избежать, добавляя прослушиватель событий, передавайте ему, в качестве третьего параметра, объект со свойством
passive
, установленным в значение
true
. Браузер, работая с подобным событием, может считать, что оно не затрагивает скроллинг, в результате прокрутка страницы может начаться без ненужных задержек.
Третий параметр заменяет опцию
useCapture
в старых браузерах, поэтому, прежде чем применять тот или иной подход, надо проверить, что именно поддерживает браузер. Для того, чтобы намеренно отключить взаимодействие некоей области с пользователем, в большинстве браузеров можно использовать
touch-action: none
в CSS.
▍Дросселирование событий
События, вроде скроллинга или изменения размера элемента, возникают так быстро, как это возможно, для того, чтобы все прослушиватели получали актуальные данные. Если при обработке каждого события производятся какие-то ресурсоёмкие операции, это может быстро привести к «зависанию» страницы.
const resizeDebounce = debounce(() => {
// Код для обработки события изменения размера }, 200);
window.addEventListener('resize', resizeDebounce);
Устранение «дребезга» событий — это методика, которая позволяет снизить частоту вызова функций обработки событий, возникающих слишком часто. Реализация этого механизма, а также частота вызова функций в разных проектах варьируются, однако, можно заметить, что уменьшение частоты обработки событий до пяти раз в секунду, например, приводит к немедленному улучшению производительности страницы.
▍Видимая область страницы
Типичный способ использования событий скроллинга заключается в обнаружении того, когда некий элемент окажется видимым на странице. Даже с использованием техники устранения «дребезга», вызов
getBoundingClientRect()
требует от браузера анализа раскладки всей страницы. Существует новое браузерное API, которое называется
IntersectionObserver
, сообщающее об изменении состояния элементов, за которыми наблюдают его средствами, вызывая заданную функцию каждый раз, когда они входят в область просмотра или покидают её. Для страниц с бесконечным скроллингом это API можно применять для того, чтобы помечать устаревшие визуальные элементы как подходящие для удаления или повторного использования.
API
IntersectionObserver
доступно во всех свежих браузерах за исключением Safari. При этом разница между использованием новой методики работы с областью просмотра страницы и старых подходов к определению видимости элементов весьма заметна.
▍Выделение ресурсоёмких операций в отдельные потоки
При работе с большими наборами данных или в ходе обработки больших файлов, таких, как изображения, JavaScript может заблокировать окно браузера. По умолчанию все задачи выполняются в единственном потоке, поэтому, если этот поток окажется перегруженным, интерфейс приложения перестанет реагировать на воздействия пользователя.
Если вы знаете, что на выполнение некоей операции потребуется много времени, неплохо будет задуматься о вынесении её в веб-воркер. Так называются скрипты, которые выполняются в отдельных потоках, при этом, даже если эти скрипты выполняют ресурсоёмкие операции, пользовательский интерфейс веб-приложения продолжает работать. Подобные скрипты могут передавать друг другу данные с использованием специального механизма обмена сообщениями. У веб-воркеров нет доступа к DOM и к некоторым свойствам объекта
window
, механизмы обмена сообщениями используются и для передачи в главный поток данных, которые могут подействовать на интерфейс.
Вопрос №12. Как писать JS-код, который не потеряет актуальности со временем?
Один из базовых принципов JavaScript заключается в том, что в ходе его развития в него стараются не вносить, если это осуществимо, изменений, которые делают невозможным выполнение кода, написанного для его предыдущих версий. В результате, например, практически весь код, написанный сегодня, будет работать и десятилетие спустя, даже учитывая изменчивость мира веб-разработки.
Однако, то, что некий фрагмент кода выполняется, не значит, что он со временем не потеряет актуальности. Тут впору задаться вопросом о том, как будет смотреться через несколько лет то, что вы пишете сегодня. Вот несколько рекомендаций, которые позволят вам создавать программы, готовые к испытанию будущим.
▍Избегайте спагетти-кода
На начальных стадиях работы над неким проектом может показаться привлекательной идея, так сказать, «валить всё в одну кучу», не разбивая код на мелкие, достаточно независимые, части. Хотя при таком подходе легко понять, что делается в том или ином фрагменте кода, это ещё и означает, что функциональные возможности программы тесно связаны друг с другом. Так, если где-то в программе нужно будет то, что реализовано в другой её части, соответствующий участок кода копируют, и, возможно, переписывают с учётом новых потребностей. Если в проект добавляют новую возможность, или в нём обнаруживается ошибка, каждую такую копию, реализующую, по сути, одно и то же, нужно обновлять по-отдельности, что может потребовать немало времени.
Если, с самого начала работы над проектом, стремиться к модульности кода, уже реализованный в нём функционал можно без проблем использовать повторно. Любые изменения в некоем модуле тут же отражаются везде, где к нему обращаются. Такие приёмы программирования, как использование шаблона «модуль», можно применять постепенно, не переписывая весь остальной проект, что упрощает их использование.
▍Постарайтесь избежать зависимости проекта от фреймворков
Многие современные фреймворки, вроде React или Polymer, подталкивают разработчиков к разработке модульного кода через создание компонентов. При этом каждый фреймворк предусматривает собственные способы разработки компонентов и организации их взаимодействия.
Что произойдёт, когда на горизонте появится очередной «самый лучший фреймворк»? Перевод проекта на новый фреймворк, или даже на новую версию используемого, может оказаться непростым делом. Для того, чтобы заставить старые компоненты работать в новой среде, может понадобиться немало бесценного времени.
Поэтому везде, где это возможно, стоит, вместо того, чтобы прибегать к фреймворкам, пользоваться обычным JavaScript. При таком подходе при смене фреймворка вышеозначенные проблемы можно свести к минимуму. Если говорить о практической реализации подобного, то, например, перед передачей данных компоненту можно использовать объекты для обработки таких данных.
Кроме того, такой подход способствует написанию универсального кода. Избегая, там, где это возможно, использования браузерных API, код можно сделать пригодным для повторного использования и в браузерных, и в серверных проектах, при условии, что последние основаны на Node.
▍Поддерживайте чистоту кода
После того, как модули написаны, их код должен быть не только работоспособным, но и лёгким для восприятия. Чистый код означает, что даже тот, кто видит его впервые, способен этот код понять, что, в частности, способствует облегчению отладки такого кода.
let activeUsers = users.filter(user => user.active === true);
Среди средств, которые позволяют повысить вероятность того, что в будущем код не потеряет актуальности, можно назвать самодокументирование. В частности, например, использование описательных имён для переменных в итераторах, вместо чего-то вроде
i
и
j
, облегчает чтение соответствующих конструкций.
Кроме того, и это очень важно, нужно всеми силами стремиться к единообразию кода. Если весь проект написан с использованием единого стиля, с ним гораздо легче работать. Поэтому перед работой над проектом стоит разработать руководство по стилю и подобрать инструменты, вроде ESLint, которые помогут этого стиля придерживаться.
▍Разрабатывайте проекты с учётом их возможного роста
Код, с которым будет удобно работать, должен быть хорошо читаемым. Похожие идеи можно распространить и на структуру проекта. Если, при работе над проектом, не придерживаться разумных правил его структурирования, очень скоро управлять таким проектом будет очень непросто.
Если, в самом начале разработки, все файлы находятся в одной папке, это упрощает дело. Например, когда скрипты импортируют модули, не нужно думать о том, где именно находятся источники этих модулей.
Однако, по мере роста проекта, число файлов в его папке будет увеличиваться, что в итоге затруднит работу. Поэтому стоит, с самого начала, поддерживать структуру проекта, которая хорошо масштабируется. Например, все модули, которые отвечают за взаимодействие с пользователями, можно хранить в папке
users
, и так далее. Тут, конечно, нельзя дать универсальной рекомендации, подходящей для проектов всех типов. Скажем, при создании одностраничных приложений необходимо разделять логику моделей, видов и контроллеров. Однако, в другой ситуации может понадобиться что-то иное. Структура — это не навязываемая «сверху» жёсткая конструкция, а естественное отражение особенностей каждой конкретной разработки.
▍Пишите код, который пригоден для тестирования
Периодически выполняйте тесты с помощью средства вроде Jest для того, чтобы убедиться в том, что всё работает как надо
Использование фреймворков вроде React способствует созданию маленьких компонентов, подходящих для повторного использования. Однако, даже при использовании грамотной структуры проекта, может быть непросто проверить, что в ходе развития этого проекта все его части работают именно так, как ожидается. Чем лучше код проекта покрыт тестами — тем больше вероятность того, что, когда дело дойдёт до перехода этого проекта из стадии разработки в стадию практического использования, всё будет работать правильно.
Модульные тесты работают на уровне модулей. Инструменты вроде Mocha и Jest позволяют разработчикам проверять программы, задавая некие входные данные и ожидаемую реакцию системы на подачу в неё этих данных. Периодическое выполнение подобных тестов позволяет удостовериться в правильности работы программы и в отсутствии побочных эффектов при её выполнении.
Модули нужно писать так, чтобы их можно было протестировать в изоляции от остальных частей системы. Это означает, что у каждого из них должно быть как можно меньше внешних зависимостей, и то, что они не должны полагаться на некое глобальное состояние проекта.
Существуют, конечно, и другие подходы к тестированию программных проектов. В частности, это интеграционное и функциональное тестирование. Чем полнее подобные тесты охватывают проект — тем лучше перспективы такого проекта в будущем.
▍О языке завтрашнего дня
Обеспечьте работоспособность вашего кода в устаревших браузерах с использованием транспилятора вроде Babel
Стоить отметить, что лучший способ обеспечить актуальность кода в будущем — это писать его с использованием синтаксических конструкций будущего. Хотя звучать это может странновато, вроде призыва к «прыжку в неизвестное», существуют инструменты, которые, без особых сложностей, позволяют такой прыжок совершить.
Вот, например, транспилятор Babel — инструмент, который умеет конвертировать одни формы JavaScript в другие. Он применяется для преобразования современного кода в формат, который понимают даже не самые современные браузеры и другие среды выполнения JS.
ES2015 принёс в мир JS массу возможностей, которые способствуют написанию чистого, понятного кода. Среди них — стрелочные функции, промисы и встроенная поддержка модулей. Самый свежий стандарт языка, ES2017, даёт разработчику ещё больше удобств, благодаря, например, асинхронным функциям. Babel, при условии его правильной настройки, способен конвертировать всё это в код, который можно использовать уже сегодня.
В конечном счёте, по мере внедрения в среды выполнения JS новых стандартов, проекты смогут обходиться и без шага транспиляции, однако, для того, чтобы оставаться как можно более актуальными в будущем, они должны уже сегодня использовать инструменты завтрашнего дня.
Итоги
Сегодня мы рассмотрели двенадцать вопросов и ответов по JavaScript, многие из которых охватывают достаточно обширные пространства веб-разработки и направлены на то, чтобы привлечь внимание программистов к таким вещам, которые, в повседневной рутине, могут годами оставаться незамеченными. Надеемся, идеи, озвученные в этом материале, помогут вам улучшить ваши разработки.
Уважаемые читатели! В последнее время много говорят о том, что JS-проекты, по возможности, надо писать так, чтобы они как можно меньше зависели от фреймворков. Как по-вашему, стоит ли к этому стремиться?