Всем привет! В конце сентября в OTUS стартует новый поток курса «Fullstack разработчик JavaScript». В преддверии начала занятий хотим поделиться с вами авторской статьей, подготовленной специально для студентов курса.
Автор статьи: Павел Якупов
Превью. Хочу сразу отметить, что в данной статье разбираются темы, хорошо знакомые «ниндзям», и больше статья нацелена на то, чтобы новички лучше поняли некоторые нюансы языка, и могли не потеряться в задачах, которые часто дают на собеседовании — ведь на самом деле подобные таски никакого отношения к реальной разработке не имеют, а те, кто их дают, чаще всего таким способом пытаются понять, насколько хорошо вы знаете JavaScript.
Ссылочные типы памяти
Как именно данные хранятся в JavaScript? Многие курсы обучения программированию начинают объяснения с классического: переменная это некая «коробка» в которой у нас хранятся какие-то данные. Какие именно, для языков с динамической типизацией вроде как оказывается неважно: интерпретатор сам «проглотит» любые типы данных и динамически поменяет тип, если надо, и задумываться над типами переменных, и как они обрабатываются, не стоит. Что конечно, неправильно, и поэтому мы начнем сегодняшнее обсуждение с особенностей, которые часто ускользают: как сохраняются переменные в JavaScript — в виде примитивов(копий) или в виде ссылок.
Сразу перечислим виды переменных, которые могут храниться в виде примитивов: это boolean
, null
, undefined
, Number
, String
, Symbol
, BigInt
. Когда мы встречаем отдельно объявленные переменные с данным типом данных, мы должны помнить, что во время первичной инициализации они создают ячейку памяти — и что они могут присваиваться, копироваться, передаваться и возвращаться по значению.
В остальном JavaScript опирается на ссылочные области памяти. Зачем они нужны? Создатели языка старались создать язык, в котором память использовалась бы максимально экономно(и это было совершенно не ново на тот момент). Для иллюстрации, представьте, что вам нужно запомнить имена трех новых коллег по работе — совершенно новые имена, и для усиления сравнения, ваши новые коллеги из Индии или Китая с необычными для вас именами. А теперь представьте, что коллег зовут также, как вас, и двух ваших лучших друзей в школе. В какой ситуации вам будет запомнить легче? Здесь память человека и компьютера работает схоже. Приведем несколько конкретных примеров:
let x = 15; //создаем переменную x
x = 17;// произошла перезапись
console.log(x)// тут все понятно
//и маленькая задачка с собеседований
let obj = {x:1, y:2} // создаем объект
let obj1 = obj; // присвоем obj к obj1
obj1.x = 2; // поменяем значение у "младшего"
console.log(obj1.x); // тут понятно, только присвоили
console.log(obj.x) // и чему же сейчас равен obj.x ?
Таким образом, если вы встретите подобную задачу на собеседовании, постарайтесь сразу понять, какой перед вами тип данных — откуда и как он получил значение, как примитивный тип, или как ссылочный.
Работа контекста
Для того, чтобы понять, как именно работает контекст в JS, нужно изучить несколько пунктов:
- Глобальный/локальный уровень видимости.
- Разница в работе контекста при при инициализации переменных в глобальной/локальной области видимости.
- Стрелочные функции.
Давным-давно, еще в ES5 все было достаточно просто: было только объявление переменной с помощью var, которое при объявлении в потоке выполнения программы считалось глобальным (что означало, что переменная приписывается как свойство к глобальному объекту, такому как window
или global
). Далее на сцену пожаловали let
и const
, которые ведут себя несколько по другому: к глобальному объекту они не приписываются, и в памяти сохраняются по другому, ориентируясь на блочную область видимости. Сейчас уже var считается устаревшим, потому как его использование может привести к засорению глобальной области видимости, и кроме того, let
выглядит куда более предсказуемо.
1. Итак, для понимания стоит твердо уяснить что такое области видимости в JavaScript(scope). Если переменная объявлена в глобальной области видимости с помощью директивы let
, тогда она не приписывается к объекту window
, но сохраняется глобально.
Перейдем к задачам, которые чаще всего получают новички по вопросам контекста на собеседовании.
//задание: что же выведется в консоль?
let x = 15;
function foo(){
let x = 13;
return x;
}
console.log(x)// 15 из глобальной области видимости
foo();
console.log(x)// ответ все тот же
x = foo();
console.log(x)// а вот сейчас return поменял наше переменную, вернув другое значение
2. В тоже время не все новички в курсе, как интерпретатор JavaScript считывает код: на самом деле он читает его два раза, в первый раз он считывает код функций, объявленных как Function Declaration(и готов их выполнить при втором, настоящем считывании и выполнении). Ещё один маленький фокус связан с var
и let
: при первом чтении переменной с директивой var
присваивается значение undefined
. А вот с let
её преждевременный вызов вообще невозможен:
console.log(x);
console.log(y)
var x = 42;
let y = 38;
//что будет в консоли?
// а будет undefined и error!
3. Стрелочные функции, которые появились в ES6, достаточно быстро завоевали популярность — их очень быстро взяли на вооружение программисты на Node.js (за счет быстрого обновления движка) и React (из-за особенностей библиотеки и неизбежного использования Babel). В отношении контекста стрелочные функции соблюдают следующее правило: они не привязываются к this
. Проиллюстрируем это:
var x = 4;
var y = 4;
function mult(){
return this.x * this.y;
}
let foo = mult.bind(this);
console.log(foo());
let muliply = ()=>x*y;
console.log(muliply());
/* стрелочная функция здесь выглядит куда лаконичнее и логичнее
если бы x и y были инициализированы через литерал let, то function declaration вообще бы не сработал таким способом */
Типы данных и что к чему относится
Сразу скажем: массив по сути является объектом и в JavaScript это не первая вариация объекта — Map, WeakSet, Set и коллекции тому подтверждение.
Итак, массив является объектом, а его отличие от обычного объекта в JS, заключается в первую очередь в большей скорости работы за счет оптимизации индексации, а во-вторых в наследовании от Array.prototype, которые предоставляет бoльший набор методов, чего его «старший брат» Object.prototype
.
console.log(typeof({}))
console.log(typeof([]))
console.log(typeof(new Set))
console.log(typeof(new Map))
//и все это будет один и тот тип объекта
Далее на очереди странностей в типах данных идет null
. Если спросить у JavaScript, к какому типу данных относится null, то мы получим достаточно однозначный ответ. Однако и здесь не обойдется без некоторых фокусов:
let x = null;
console.log(typeof(x));
//Отлично! Следовательно, null происходит от objet, логично?
console.log(x instanceof Object.prototype.constructor); //false
//А вот и нет! Видимо это просто придется просто запомнить)
Стоит запомнить, что null
является специальным типом данных — хотя начало предыдущего примера и указывало строго на другое. Для лучшего понимания, зачем именно данный тип был добавлен в язык, мне кажется, стоит изучить основы синтаксиса C++ или С#.
И конечно, на собеседованиях часто попадается такая задача, чья особенность связана с динамической типизацией:
console.log(null==undefined);//true
console.log(null===undefined);// а вот тут уже false
С приведением типов при сравнении в JS связано большое количество фокусов, всем мы их здесь привести физически не сможем. Рекомендуем обратиться к «Что за черт JavaScript „.
Нелогичные особенности, оставленные в языке в процессе разработки
Сложение строк. На самом деле сложение строк с числами нельзя отнести к ошибкам в разработке языка, однако в контексте JavaScript это привело к известным примерам, которые считаются недостаточно логичными:
let x = 15;
let y = "15";
console.log(x+y);//здесь происходит "склеивание"
console.log(x-y); // а здесь у нас происходит нормальное вычитание
То, что плюс просто складывает строки с числами — относительно нелогично, но это нужно просто запомнить. Особенно непривычно это может быть потому, что другие два интерпретируемых языка, которые популярны и широко используются и в веб-разработке — PHP и Python — подобных фокусов со сложением строк и чисел не выкидывают и ведут себя куда более предсказуемо в подобных операциях.
Менее известны подобные примеры, например c NaN:
console.log(NaN == NaN); //false
console.log(NaN > NaN); //false
console.log(NaN < NaN); //false … ничего не сходится... стоп, а какой тип данных у NaN?
console.log(typeof(NaN)); // number
Часто NaN приносит неприятные неожиданности, если вы, например, неправильно настроили проверку на тип.
Куда более известен пример с 0.1 +0.2 — потому как эта ошибка связана с форматом IEEE 754, который используется также, к примеру, в столь “математичном» Python.
Так же включим менее известный баг с числом Epsilon, причина которого лежит в том же русле:
console.log(0.1+0.2)// 0.30000000000000004
console.log(Number.EPSILON);// 2.220446049250313e-16
console.log(Number.EPSILON + 2.1) // 2.1000000000000005
КОМММЕНТАРИЙ АВТОРА ПОТОМ УДАЛИТЬ: здесь могла быть картинка с эйнштейном: https://cs11.pikabu.ru/post_img/2019/02/07/6/1549532414127869234.jpg
И вопросы, которые несколько сложнее:
Object.prototype.toString.call([])// эта конструкция вообще сработает?
// -> вернет '[object Array]'
Object.prototype.toString.call(new Date) // сработает ли это с Date?
// -> '[object Date]' да тоже самое
Стадии обработки событий
Многим новичкам непонятны браузерные события. Часто даже незнакомы самые основные принципы, по которым работают браузерные события — перехват, всплытие и события по умолчанию. Самая загадочная с точки зрения новичка вещь — это всплытие события, который, вне сомнения, обосновано в начале вызывает вопросы. Всплытие работает следующим образом: когда вы кликаете по вложенному DOM — элементу, событие срабатывает не только на нем, но и на родителе, если на родителе также был установлен обработчик с таким событием.
В случае, если у нас происходит всплытие события, нам может понадобится его отмена.
//недопущение смены цвета всех элементов, которые находятся выше по иерархии
function MouseOn(e){
this.style.color = "red";
e.stopPropagation(); // вот тут остановочка
}
Кроме того, для новичков часто представляет проблему отмена событий, которые происходят по умолчанию. В особенности это важно при разработке форм — потому как валидацию формы, к примеру, нужно проводить как на стороне клиента, так и на стороне сервера:
codepen.io/isakura313/pen/GRKMdaR?editors=0010
document.querySelector(".button-form").addEventListener(
'click', function(e){
e.preventDefault();
console.log('отправка формы должна быть остановлена. Например, для валидации')
}
)
Отмена всплытия события может нести в себе и некоторые неприятности: к примеру, вы можете создать так называемую «мертвую зону», в которой не сработает необходимая вещь — к примеру, событие элемента, которому «не посчастливилось» оказаться рядом.
Всем спасибо за внимание! Здесь несколько полезных ссылок, с которых вы можете черпать множество полезной информации:
developer.mozilla.org/ru/docs/Web/JavaScript/Reference
learn.javascript.ru/event-bubbling
learn.javascript.ru/bind#reshenie-2-privyazat-kontekst-s-pomoschyu-bind
medium.com/@KucherDev/когда-и-почему-стоит-использовать-стрелочные-функции-es6-3135a973490b
github.com/denysdovhan/wtfjs/blob/master/README.md
habr.com/ru/company/otus/blog/456124
habr.com/ru/company/mailru/blog/335292
habr.com/ru/company/otus/blog/457616
habr.com/ru/company/otus/blog/456724
На этом все. Ждём вас на бесплатном вебинаре, который пройдет уже 12 сентября.
Автор: Дмитрий