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



Всем привет! В конце сентября в 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, нужно изучить несколько пунктов:

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



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


Сразу скажем: массив по сути является объектом и в 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 это привело к известным примерам, которые считаются недостаточно логичными:

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 


И вопросы, которые несколько сложнее:

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('отправка формы должна быть остановлена. Например, для валидации')
        }
      )

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

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




На этом все. Ждём вас на бесплатном вебинаре, который пройдет уже 12 сентября.

Вы можете помочь и перевести немного средств на развитие сайта



Комментарии (33):

  1. dss_kalika
    /#20606221

    Это не вброс, просто капелька негодования.
    Зачем нужны динамические типы, если всё равно надо в уме держать схему их неявных преобразований?! )

    • Aingis
      /#20606315

      Что там запоминать-то? Если отбросить объекты, которые преобразовывать обычно плохая идея (а если и делать, то явно), и булевые типы (работать с ними нестрого тоже, скорее всего, плохая идея), то оставшиеся преобразования все тривиальны и даже удобны.

      • dss_kalika
        /#20606431

        Строки, числа и даты.
        Неявные преобразования — явное зло и не очень понятен выгоды этого действа над строгой типизацией.

        Но, подозреваю, эта тема мучает обе стороны с момента появления нестрогой типизации, так что не думаю что стоит об этом спорить.

        • Aingis
          /#20606891

          Даты — это тоже объекты, хоть и вполне удобно приводимые к числу при сравнении больше-меньше. Могут приводиться в строки, но тоже лучше явно и в клиентоориентированном формате.


          Выгода как раз понятна. Сравниваете вы, например, число со строкой a === b. При строгом сравнении данные разных типов никогда не будут равны (2 !== '2'). Надо приводить одно в другое и писать более громоздкий код, например: String(a) === b.


          Гораздо проще использовать нестрогое сравнение чтобы не ловить ошибок из-за несоответствия типов a == b (2 == '2', реально имел дело с такими ошибками при бездумном навязывании строгого сравнении). Кстати, при сравнении больше или меньше a >= b приведения избежать вообще нельзя.


          Есть хорошая презентация на тему Дмитрия Барановского (автора Snap.svg): Zen of JavaScript.

          • dss_kalika
            /#20606921

            Ну так и не надо сравнивать разнородные сущности. А если сравнивать — то явно указывать правила сравнения, а не полагаться на «подразумевающийся» алгоритм приведения типов… который и приходится держать в уме.

            Это не проще. Это — скрывает логику и порождает ошибку )

            ЗЫ: Спасибо за презентацию, но я и так это всё уже давно знаю )

            • iluxa1810
              /#20609379

              +1.
              Вот я тоже не понимаю преимущества в этом.
              Зачем давать неявную возможность все что угодно сравнивать между собой?
              Если ты реально хочешь сделать сравнение, то сделай приведение типа.

    • rboots
      /#20607873

      Нет никакой неявности в преобразовании типов, типы преобразуются по строгим правилам, которые можно выучить за 15 минут с запасом. Как и при перегрузке операторов в C++, правила преобразования определяются типом первого аргумента, поэтому «1» + 1 и 1 + 1 дают разные результаты. Перегрузки операторов в самом JavaScript нет, поэтому выражения с участием объектов бессмысленны, рассматривать стоит только примитивы. Строки всё приводят к строке, остальные типы к своему типу, кроме опратора "+", он приводит к строке даже если строка только второй аргумент, это сделано для удобства. Поздравляю, вы знаете приведение типов в JavaScript.

      • dss_kalika
        /#20609243

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

        Удобно, но минимально.

      • pawlo16
        /#20611621

        == это сделано для удобства

        Это сделано по глупости и является неиссякаемым источником самых идиотских багов. Удобство — это строго на строго запретить 1 + '1' или кидать на него эксепшен. В абсолютно всех более или менее нормальных языках так и сделано

        • Aingis
          /#20611835

          Никогда не видел баги из-за ==. Может потому что дело не в языке, а в том кто им пользуется. Зато видел баги из-за навязывания ===.
          'Количество: ' + num — офигенно удобно, не пользоваться фичами языка из-за невежества просто глупо. Причём там не rocket science, правила просты, состоят из нескольких пунктов, и вполне логичны.

          • dss_kalika
            /#20611915

            Когда это твой код или маленький фрагмент — возможно.
            Если это чужой код и большой фрагмент кода то SomeVar1 + SomeVar2 может выдать… всё что угодно.
            И неявные правила — это всегда риски багов. =)

            • Aingis
              /#20612087 / +1

              Если может быть вообще всё что угодно, то проблема далеко не в коде.
              Непонятно, про какие неявные правила вы говорите. Все правила приведения в JS строго описаны. Их легко разобрать и можно вполне удобно использовать.

              • dss_kalika
                /#20613711

                Те, которые ты не пишешь руками.

                SomeVar1 + SomeVar2
                Вот пример. Без разбирательства что в переменных и в каком виде храниться даже зная правила неявных преобразований типов сложно гарантировать что получим на выходе.

                • Zenitchik
                  /#20615021

                  Без разбирательства что в переменных и в каком виде храниться

                  … нельзя их складывать. Просто совсем нельзя.

                • Aingis
                  /#20615799

                  Почему-то все всегда приводят в пример какой-то лютый говнокод, как будто это довод. Говнокод можно написать на любом языке. Даже если компилятор с линтером ругаются на каждый чих, это будет просто красиво оформленный, замаскированный говнокод, особенно когда речь о большом фрагменте кода. Разруха, она в головах.

                  Обычно из контекста вполне ясно что складывается. Если идут математические операции, вряд ли складываются строки. Если складывается строка, обычно какая-то часть явно присутствует.

                  Если из кода совсем непонятно что он делает, то это код плохой, а не язык. Гарантировать вообще ничего нельзя, даже если у вас будет явно типизировано. Потому что типизация говорит «аргументы такие, мамой клянусь!», но никак не может гарантировать, что извне придёт именно то, что ожидалось. Однако, ничто не мешает сделать проверки и привести в нужный тип, когда надо. Поэтому я и говорю, что дело не в языке, а в том кто им пользуется.

                  • dss_kalika
                    /#20616017

                    Конечно дело в том кто им пользуется. И большинство тех кто им пользуется — пишут так себе код. Поэтому надеяться на читабельность доставшегося куска кода, адекватность того кто это писал, отсутствие других факторов (надо было костыль для конкретного случая нафигачить) как то… наивно.
                    В таком случае лучше иметь строгий язык, не позволяющий стрелять себе в ногу и скрывать эту боль до момента, когда не будет поздно ) Да и не так уж и много это требует действий на самом деле.

                    • Aingis
                      /#20616139

                      Чтобы не стрелять себе в ногу, нужен отлаженный процесс разработки. Такой как код-ревью, в котором обязательно принимает участие сеньор. Тесты тоже помогают (хорошие конечно). Это не техническая задача, она не решается техническими методами.

                      Даже если компилятор с линтером ругаются на каждый чих, это будет просто красиво оформленный, замаскированный говнокод, особенно когда речь о большом фрагменте кода.

                      • dss_kalika
                        /#20616443

                        т.е. проверку чёткости работы с типизацией вы хотите решать изменением процесса разработки и найму лишних людей? )

                        интересный подход.

                        Но тем не менее, это классический пример скрытой логики, которая может приводить к ошибкам, созданной для удобства. Да, это удобно и да у этого есть некоторые очевидные минусы.

                        Собственно — это не спор, что лучше, а просто было высказанное мнение ) я знаю за и против.

                        • Aingis
                          /#20616597

                          проверку чёткости работы с типизацией
                          Если работа с кодом для вас сводится только к этому, то понятно откуда берутся проблемы. Я говорю о том, как писать надёжный работающий код, в противовес говнокоду, где проблемы с типизаций лишь частный симптом, один из запашков. Плохому кодеру слабая типизация мешает, как плохому танцору.

                          • dss_kalika
                            /#20618435

                            Ну, мы говорим не о том, как не писать говнокод, а о том, что неявные преобразования в виде отсутствия строгой типизации могут приносить проблемы. С большей вероятностью, чем их отсутствие.
                            Более ярко это выражено в проектах с так себе кодом, менее ярко — где код хороший.

                            Это то небольшое удобство, которое приносит и небольшие риски. =)

                            • Zenitchik
                              /#20620017

                              Более ярко это выражено в проектах с так себе кодом, менее ярко — где код хороший.

                              Неправда ваша.
                              Ярко выражено — где так себе код, совершенно не является проблемой — где код хороший. Риски околонулевые.

                              Хотя, честно говоря, я бы предпочёл статическую типизацию с неявными преобразованиями.

                              • dss_kalika
                                /#20620289

                                Ярко выражено — где так себе код, совершенно не является проблемой — где код хороший. Риски околонулевые.
                                О чём я и писал. Чем лучше код — тем меньше это может вызвать проблем.
                                Но во-первых — а так ли мало этого не очень кода? (много!)
                                во-вторых — риски есть риски. и хотелось бы их совсем избежать, особенно когда их привносят просто фишечки порядка «удобно» и «не надо писать лишнее слово». ) Но вот это уже часть ИМХО, конечно, о чём я сразу и сказал.

                                Всё неявное рано или поздно становится явной проблемой. =) Вам так лень написать одну директиву для преобразования в нужный тип?)

                                • Zenitchik
                                  /#20620999

                                  Но во-первых — а так ли мало этого не очень кода? (много!)

                                  А есть ли нам дело до чужого кода? Наш код — нормальный.

                                  • dss_kalika
                                    /#20621133

                                    Блажен кто верует… )

                                    • Zenitchik
                                      /#20621383

                                      Во что верует?
                                      В то, что наш код нормальный? Это объективная реальность, поддаётся проверке.
                                      Что мне нет дела до чужого кода? Аналогично, хотя, доказать потруднее.

          • pawlo16
            /#20612567

            Слушайте, из этих ваших простых правил могут быть и бывают сложные, запутанные и ни разу не очевидные следствия. Во всех языках на такую дичаюшую дичь, как неявное преобразование любого типа в строку в выражении с оператором "+", наложено жесточайшее табу. И только в джаваскрипте это считается фичей. В данном случае следствием этого вашего якобы офигенного удобства написания примитивного кода является то, что программисту приходится выполнять работу компилятора/интерпретатора в нормальных ЯП. Чуть менее тривиальный код, где производятся вычисления с number, зачастую приходится обвешивать тестами и костылями, проверяющими тип входных параметров. Потому если туда внезапно приходит строка, получаем на выходе упс, на который интерпретатору плевать.

            == дело не в языке, а в том кто им пользуется

            так можно оправдать любой косяк любой технологии — во всём виноваты те, кто пользуется технологией, а сама технология хорошая, она не при чём. Суперджет офигенно удобный самолёт, во всём виноваты пилоты и пассажиры, которые его не правильно используют, ага.

            == 'Количество: ' + num — офигенно удобно,

            `Количество: ${num}` ещё более офигенно удобно.

    • justhabrauser
      /#20607941

      Чтобы потом, после прочтения K&R, жизнь этого новичка заиграла новыми, яркими и радостными красками.
      После удивленного «а что, так можно было?».

  2. sultan99
    /#20607035

    КОМММЕНТАРИЙ АВТОРА ПОТОМ УДАЛИТЬ: здесь могла быть картинка с эйнштейном: cs11.pikabu.ru/post_img/2019/02/07/6/1549532414127869234.jpg


    Забыли удалить?

    • MaxRokatansky
      /#20607771 / +1

      Вообще авторский юмор, но видимо придется удалить, так как шутку не оценили и в личку пришло несколько таких же вопросов)

  3. TheShock
    /#20608093

    И конечно, на собеседованиях часто попадается такая задача, чья особенность связана с динамической типизацией:

    console.log(null==undefined);//true
    console.log(null===undefined);// а вот тут уже false


    С приведением типов при сравнении в JS связано большое количество фокусов, всем мы их здесь привести физически не сможем. Рекомендуем обратиться к «Что за черт JavaScript „.
    Распространенная ошибка новичка. Именно в этом примере НЕТ приведения типов и динамическая типизация — тоже ни при чем.

    • TheShock
      /#20612717

      Я знал, что найдется человек, который плохо знает JS и меня минусанёт. Этого человека я хочу спросить — что в этом примере приводится к чему?

  4. Fen1kz
    /#20610335

    Ужасная статья. Будь я новичком — я бы тоже перестал понимать эти 5 вещей после такой статьи.


    Для иллюстрации, представьте, что вам нужно запомнить имена трех новых коллег по работе — совершенно новые имена, и для усиления сравнения, ваши новые коллеги из Индии или Китая с необычными для вас именами. А теперь представьте, что коллег зовут также, как вас, и двух ваших лучших друзей в школе. В какой ситуации вам будет запомнить легче? Здесь память человека и компьютера работает схоже.

    Компьютеру легче запомнить имя его школьных друзей? Компьютеру легче запомнить переменные, названные привычными ему обозначениями? Мне кажется новичков он только запутает.


    Приведем несколько конкретных примеров

    Приводит один пример без верного ответа.


    Давным-давно, еще в ES5 все было достаточно просто: было только объявление переменной с помощью var, которое при объявлении в потоке выполнения программы считалось глобальным

    WAT? А function scoped объявления тоже не было? На фоне такой жести продвижение let, в отличие от const смотрится как детская шалость.


    Стрелочные функции, которые появились в ES6, достаточно быстро завоевали популярность — их очень быстро взяли на вооружение программисты на Node.js и React

    Записал эту фразу, скажу ее на следующем собеседовании. Я хочу увидеть их глаза :D


    Пример со стрелочными функциями очень странный. Автор объясняет что такое стрелочные функции, но делает это с помощью двух новых концептов — this и биндинга функций


    Остальная такая же бредовая — рандомные обрывки сведений в рамках одного пункта. Фиг с ними с "особенностями", но вот события — такая благодатная тема, перескажи в кратце суть, например, вот этого поста.


    Так нет же, надо в двух разных абзацах про разные вещи использовать термин "отмена события". "отмена события" у нас теперь и preventDefault и stopPropagation. Пойду создам предложение переименовать оба метода в cancelEvent. А при вызове выбирать рандомно.




    И я понимаю, статья подается не как справочник, а как 5 рандомных фактов, но в рамках этих 5 пунктов — можно же объяснить более менее системно, чтобы не закидывать "новичка" кучей не связанных между собой фактов, типа "зубри ска"


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


    tl;dr дорогие новички, бегите из этого центра пока вам ещё есть чем бежать.