Закрыцце, падняць і гэта: правілы JavaScript, якія з’являюцца ў багах
Падзеянне, клазуры і іншыя аспекты ў JavaScript, якія выкладваюцца чераз багі, якія спрыяюць ўсвайомленню: памылкі TDZ, спяльныя прыяўненні зменных у цікле і аддзерваваныя методы.
Крыніцы: 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.
Чатыры правілы для месца выкліку пакрываюць большасць повсякдзенных ситуацый:
- Звычны ўзыв функцыі → у режыме strict значэнне
thisўзначаецца якundefined, інакш — глобальны об’ект. obj.method()→thisўзначаецца якobj..call(),.apply()або.bind()→thisўзначаецца тым, што было явна перадана.- Функцыі-стрэлкі → не маюць сасвейшаг
this; яны спадчынаюць яго з абрамляючага контексту.
Самэўсёледзяючая прычына ўжывання функцый-стрэлакоў як стандартнага рашэння ў класах і компонентах, якія работаюць з атрыбутамі экзанпляра, — гэта запобежчанне випадковым проблемам з ад’ёмам контексту.
Што насправды дапамагало
Аднальне чытанне апісаў рэдка калі застаецца ў памяці. Асьцераганні, якія дапамагаюць:
Запісваць маленькія, навмысна зламаныя фрагменты коду — пяць-шасць ліній, прызначаных для адной конкрэтной проблемы — так кожны брак будзе лёгкі да адазначэння і аналізу.
З’явіце прогназ выходу console.log даўна перад запускам файлу. Пры прымусовай працэўке прогназу запамятаваная правіла ператвараецца ў гіпотэзу, якую можна пераканаць.
Паспяшна адказайце на пытанні, як гэты механізм працюе. Прасочынкі выяўляюцца адразу, калі трэба пераказаць прынцыпы hoisting або closures на новыя словы.
Вывад
Hoisting, closures і this — гэта не дробныя деталі. Яны адказваюць за працэсавыя неудачы: спакушаныя зв’язкі ў цыклах, памылкі TDZ і обрабнікі, якія трацяюць свой экземпляр. Кроме таго, яны не падаюць на чысты лад у багато іншых мов, таму пачатковая плутанасць — гэта нормальна. Свяжаныя несправныя фрагменты, прогназы і пераказы скрацуюць час, пакуль поведзенне стане прыемна працоўным. Зберагайце файлы з несправнымі фрагментамі; перагляд іх чыраз за тыдзень часта ўжоць корыстней, чым перачытанне дзяліны кнігі.