5 вещей, которые чаще всего не понимают новички в JavaScript

Всем привет! В конце сентября в OTUS стартует новый поток курса «Fullstack разработчик JavaScript». В преддверии начала занятий хотим поделиться с вами авторской статьей, подготовленной специально для студентов курса.

Автор статьи: Павел Якупов

uuibccg0qbeeizngnzwp8dcq-ho.jpeg

Превью. Хочу сразу отметить, что в данной статье разбираются темы, хорошо знакомые «ниндзям», и больше статья нацелена на то, чтобы новички лучше поняли некоторые нюансы языка, и могли не потеряться в задачах, которые часто дают на собеседовании — ведь на самом деле подобные таски никакого отношения к реальной разработке не имеют, а те, кто их дают, чаще всего таким способом пытаются понять, насколько хорошо вы знаете JavaScript.


203c8rmbxnavnfjgl5d26lfem0m.png

Ссылочные типы памяти


Как именно данные хранятся в 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 ?

Таким образом, если вы встретите подобную задачу на собеседовании, постарайтесь сразу понять, какой перед вами тип данных — откуда и как он получил значение, как примитивный тип, или как ссылочный.

a6g-1d8m1v3kkjrga-ow-ldt0zy.png

Работа контекста


Для того, чтобы понять, как именно работает контекст в JS, нужно изучить несколько пунктов:

  1. Глобальный/локальный уровень видимости.
  2. Разница в работе контекста при при инициализации переменных в глобальной/локальной области видимости.
  3. Стрелочные функции.

Давным-давно, еще в 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 вообще бы не сработал таким способом */

qmxvupa5cwiscgphsr0htpoujqc.png

Типы данных и что к чему относится


Сразу скажем: массив по сути является объектом и в 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 ».

6nm0qmsth_xjh0fohk1fwf-kvc8.png

Нелогичные особенности, оставленные в языке в процессе разработки


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

codepen.io/pen/? editors=0011

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]' да тоже самое

zpumi-pdkzkib1c9wgfgh5fdjme.png

Стадии обработки событий


Многим новичкам непонятны браузерные события. Часто даже незнакомы самые основные принципы, по которым работают браузерные события — перехват, всплытие и события по умолчанию. Самая загадочная с точки зрения новичка вещь — это всплытие события, который, вне сомнения, обосновано в начале вызывает вопросы. Всплытие работает следующим образом: когда вы кликаете по вложенному 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('отправка формы должна быть остановлена. Например, для валидации')
        }
      )

Отмена всплытия события может нести в себе и некоторые неприятности: к примеру, вы можете создать так называемую «мертвую зону», в которой не сработает необходимая вещь — к примеру, событие элемента, которому «не посчастливилось» оказаться рядом.

Всем спасибо за внимание! Здесь несколько полезных ссылок, с которых вы можете черпать множество полезной информации:


learn.javascript.ru
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 сентября.

© Habrahabr.ru