Галоўная / Артыкулы

Закрыцце, падняць і гэта: правілы JavaScript, якія з’являюцца ў багах

Падзеянне, клазуры і іншыя аспекты ў JavaScript, якія выкладваюцца чераз багі, якія спрыяюць ўсвайомленню: памылкі TDZ, спяльныя прыяўненні зменных у цікле і аддзерваваныя методы.

Выдавец: REACTAPP.TOP5 хв чытання1108 слоў

Крыніцы: Closures, Hoisting, and `this`: The JavaScript Concepts That Finally Clicked for Me

Закрыцці, паднесенне і this: Канцэпцыі JavaScript, якія продовжваюць з’являцца

Багато разработчыкаў месцыяўныяся на роботу з JavaScript для всіх слоёў аплыцення пра месцыяўныяся, аднак далей вважаюць тры аспекты мовы простымі фактамі для спрабаў на адбор: паднесенне, закрыцці і this. Запам’ятаваныя вазначэння можуць дапамогчы праўільна адпавесці на запытанні пад час спрабаў. Аднак баги ў продакшэне адкрываюць гэтыя прасоўніны — таймауты, якія фіксуюць некоректны індэкс, хендлеры, якія трапляюцца без свайго экземпляра, і зменныя, якія застаюцца undefined у заместо таго, каб выклікаць паўтарныя прычыны. Цена гэтага зазвычай — час: трэба дзеўнастацца на стак, які выглядае правільным, пакуль правілы месца вызыва нарэшце не поясняюць прычыну.

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

Паднесенне

Першыя неспадкі выглядаюць так:

console.log(greet()); // "Hello!"
function greet() {
  return "Hello!";
}

Выклік знаходзится вышэй за декларацыю, пры тым як ён вяртае "Hello!". За стандартным ментальным моделюваннем гэта не можа быць. Декларацыі функцыйяў адрозненыя: перш чым запускаюцца оператывы, двайчоракі працоўнік паднімае всю декларацыю, уключаючы ўсё тэло функцыі, таму greet вже можа быць выкліканы.

Адно з суседніх патэранаў працюе іначы:

console.log(name); // undefined
var name = "Prashant";

Няма ReferenceError, толькі undefined. Двайчоракі працоўнік знаў, што name існуе ўжо з самага пачатку, але прызначэнне значэння яшчо не было выканана. У обох случаях існуе адны і той самы механізм: декларацыі паднімаюцца на верх свайго дыапазону дасягнема, а декларацыі функцыйяў прыводзяць свое тэла разам з сабой. var прыводзіць толькі інформацыю пра зв’язку; ініцыялайзер застаецца там, дзе ён быў напісаны, таму пачатковае значэння є порожнім.

Змена спосабу прыяжоўкі на let зменяе спосаб выйсця з працы:

console.log(name); // ReferenceError: Cannot access 'name' before initialization
let name = "Prashant";

Это вызывае адмоўку. Часты інстынкт — сказаць, што let “не выкарыстоўвае хойстінг”. Гэта некалькітабельна. Прыяжоўка ствараецца рана, а потым зачынена ў часовай “мертвой зоне” аж да рэчы прызнання. Імя існуе; яго зміна прычынай ініцыялізацыі є незаконной. Гэты стандартная настройка безпечней, чым var, який таямна выдае undefined і дазволяе багу распростраńвацца даўжэй.

Закрыцці

Закрыцці часта дапамагаюць распазнаць баг у цікле, а не простая рэч з падручніка. Разглядзім прыклад з запазджаным логам, які павінен вывести 1, 2, 3:

for (var i = 1; i <= 3; i++) {
  setTimeout(() => {
    console.log(i);
  }, 1000);
}
// what I expected: 1, 2, 3
// what I got: 4, 4, 4

Апэктаваецься парадныя цыфры. Рэзультат — 4, 4, 4. Змянная var мае дыяпазон функцыі, а не блока, таму цикл выкарыстоўвае адну і тую ж змянную i. Калі таймеры запускаюцца пасля завершэння циклу, гэтае спільнае значэнне i вялічыцца ўжо 4. Кожны калбэк чытае тое ж завершанае значэнне.

Є адна ключоўая слова, якая гэта рашыяе:

for (let i = 1; i <= 3; i++) {
  setTimeout(() => {
    console.log(i);
  }, 1000);
}
// 1, 2, 3

let мае дыяпазон блока, таму кожная ітерація мае аднае значэнне i. Кожны калбэк выкарыстоўвае сваё сопственнае значэнне i.

Гэтая пара — функцыя плюс запам’ятаваныя значэння — являе сабою практычную дыферэнціяцыю калбэка. Калбэк setTimeout — это не толькі функцыя; ён таксама несе сабой атмосферу, якая ў яго патрэбна пасля таго, як дыяпазон циклу завершыўся. Калі вы спакоюватэсе на гэтым прытрымліванні значэння, рэзультат 4, 4, 4 перестае выглядаць як баг платформы і становіцца прыкладам спільнага зменяючагася стану.

Калечэннія стаюць виднымі ў звычным коде React і Node: хукі, які вяртаюць функцыі, обрабоўчыкі змян, які чытаюць стан компонента, і практычна кожны апдэйтар useState. Гэта панясць не толькі для спаканаў; так працуе вяліка частка коду UI, які базуецца на калечэннях.

this

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

Прыклад класовага кантэйнера ілюструе гэтыя проблемы:

class Counter {
  constructor() {
    this.count = 0;
  }
  increment() {
    this.count++;
    console.log(this.count);
  }
}const counter = new Counter();
const btn = document.querySelector("button");btn.addEventListener("click", counter.increment); // this is undefined

Выкліканне методу increment через экземпляр працюе. Перадача counter.increment безпосередня ў addEventListener часта выклікае проблемы, калі змэнная this усередзіне методу є undefined. Ад’ёнаванне методу прыводзіць да выкліку простай функцыі; у режыме строгасці такі выклік не отрымае первачальны об’ект як this.

Две палекі дапамагаюць легка запам’ятаваць гэта правілу:

// bind it explicitly
btn.addEventListener("click", counter.increment.bind(counter));
// or wrap it in an arrow function
btn.addEventListener("click", () => counter.increment());

.bind(counter) фіксуе об’ект-прыймальнік. Функцыя-стрэлка выклікае метод через экземпляр, таму форма выкліку методу застаецца незменнай. Функцыі-стрэлкі не ствараюць сабе власную змэнную this; яны успадковуюць прыязанасці з абоўсюды навколле, таму другая палка не трэбуе явнага bind.

Чатыры правілы для месца выкліку пакрываюць большасць повсякдзенных ситуацый:

  1. Звычны ўзыв функцыі → у режыме strict значэнне this ўзначаецца як undefined, інакш — глобальны об’ект.
  2. obj.method() → this ўзначаецца як obj.
  3. .call(), .apply() або .bind() → this ўзначаецца тым, што было явна перадана.
  4. Функцыі-стрэлкі → не маюць сасвейшаг this; яны спадчынаюць яго з абрамляючага контексту.

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

Што насправды дапамагало

Аднальне чытанне апісаў рэдка калі застаецца ў памяці. Асьцераганні, якія дапамагаюць:

Запісваць маленькія, навмысна зламаныя фрагменты коду — пяць-шасць ліній, прызначаных для адной конкрэтной проблемы — так кожны брак будзе лёгкі да адазначэння і аналізу.

З’явіце прогназ выходу console.log даўна перад запускам файлу. Пры прымусовай працэўке прогназу запамятаваная правіла ператвараецца ў гіпотэзу, якую можна пераканаць.

Паспяшна адказайце на пытанні, як гэты механізм працюе. Прасочынкі выяўляюцца адразу, калі трэба пераказаць прынцыпы hoisting або closures на новыя словы.

Вывад

Hoisting, closures і this — гэта не дробныя деталі. Яны адказваюць за працэсавыя неудачы: спакушаныя зв’язкі ў цыклах, памылкі TDZ і обрабнікі, якія трацяюць свой экземпляр. Кроме таго, яны не падаюць на чысты лад у багато іншых мов, таму пачатковая плутанасць — гэта нормальна. Свяжаныя несправныя фрагменты, прогназы і пераказы скрацуюць час, пакуль поведзенне стане прыемна працоўным. Зберагайце файлы з несправнымі фрагментамі; перагляд іх чыраз за тыдзень часта ўжоць корыстней, чым перачытанне дзяліны кнігі.