Як на самай працэ ў JavaScript апрацоўваецца ланцуг дыялектараў для вычыслення зменных
Адказвае пра механізм лексычнага спектру, які стоіць за пошукам зменных, пра тое, чаму выкарыстоўванне var вызывае бягі ў цыклах, і пра тое, як закрыцці натуральна вынікаюць з гэтага.
Пошук зменных не завісіць ад круглых скляеў
Тыповы спосаб, якім люди першыя даведаюцься пра дыямэнт расповсюджэння, — гаворыць, што «дыямэнт расповсюджэння — гэта тое, што знаходзіцца ў скляеў». Такой апіс даўа можлівасць перайсці тэст, але не дапамагае прыпускаць, што на самай працэ практыкуе код. Рэальны механізм є болей механічны і прыглядней для прыкладзення, чым парадоксальнае скрацэнне, і калі вы яго зрозумеете, клоузыры перестануць вярцацца магіяй.
Дыямэнт расповсюджэння вядомы з таго, дзе вы пішаце код, а не з таго, як ён выкананы
JavaScript раз’яўляе зменныя лексычна. Гэта значыць, што дыямэнт расповсюджэння зменной фіксуецца на адвычайнае яго месца ў файле-выканавіцэ на момент напісання, а не завісіць ад таго, якая функцыя ў момент роботы програмы вызывае іншую функцыю.
const value = "outer";
function readValue() {
console.log(value);
}
function runWithDifferentValue() {
const value = "inner";
readValue(); // still logs "outer", not "inner"
}
runWithDifferentValue();
readValue не заінтэресаваны тым, хто яго вызывае і якія зменныя знаходзяцца ў средовышчы вызываючага коду. Яго цікавіць толькі тое, дзе ён фізычна быў заданы — проста побачліва ўсередзіне const value = „outer“. Гэта ўсё, што ён можа бачыць, незалежна ад таго, з якога месца вашага програмы ён вызываецца. Самэлькі разоў самэлькі разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоў разоѭ
Асалодны механізм пошуку: пераходжэнне па ланцужку сферы дасягнення
Калі двахарукаеўскі прыстрой павінен разгледзець абяву пра змэнную, ён не шукае ў всім кодавым базе. Ён пачынае ад самага месца, дзе викорыстоўваецца змэнная, і праходзіць даўжэй, па аднаму кожны замкнуты прастор, зупінаючыся як толькі знаходзіць падходящы варыянт:
const a = "global";
function outer() {
const b = "outer";
function inner() {
const c = "inner";
console.log(a, b, c); // "global outer inner"
}
inner();
}
outer();
inner спачатку пераглядае c і знаходзіць яго прымусова, без неабяжнага пошуку. Потым яна пераглядае b – яго няма ў локальнай області значэнняў, таму пошук пераходзіць у області значэнняў outer, дзе яго і знаходзяць. Далей яна пераглядае a – яго няма ні ў inner, ні ў outer, таму пошук продовжваецца, пакуль не дасягне глобальнай області значэнняў, дзе яго нарэшце знаходзяць. Гэтыя этапы – перагляд локальнай області, потым аблака, якая яе абгортае, і так да глобальнай області – і ўсё гэта ёсць весь алгорытм пошуку. Няма нічога сложнейшага, чым «паглядзіце тут, потым паглядзіце на адны ўровень вышэй, павтараць, пакуль не будзе знайдзена».
Гэты ж механізм адказвае на падыянне засенення змэнных без адзінай-толькі дапаможнай правілы. Якщо inner абявіць свой сабе const b, пошук завершыцца як толькі будзе знайдзена гэта локальная b, і нават не дася дае ўвесь час до b, абявленага в outer. У гэтым сценарыі нічога не падменяецца; проста спачатку знаходзіцца болей ближы рэшт, таму пошук проста не мае прычыны продавацца.
Чаму var паводзіцца інакше, і чаму гэта вызывае вядомы баг
let і const прыўязуюцца да найбліжэйшага абгорнучага блока, тое ў значэнні да будь-якай пары круглых скобак, незалежна ад таго, це ўмова if чы корпус ціклу. var зовсім не следуе гэтай правілу. У замен var прыўязуюцца да найбліжэйшай абгорнучай функцыі, цалкам ігнаруючы межы блакоў.
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// logs: 3, 3, 3
У гэтым фрагменте коду ёсць роўна адна зменная i, якая ўтворена ў межах функцыі, і кожная ітерацыя циклу выкарыстоўвае гэтую ж зменную. Калі запускаюцца калебэкі setTimeout, цикл уже завершыў свою роботу, а зменная i вже набрала значэнне 3. Усе тры калебэкі выкарыстоўваюць тую ж зменную, а не кожны сваю сабственную копію, таму ўсе яны прадаюць канечнае значэнне, якое ў яё засталося.
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// logs: 0, 1, 2
Пакалькі let мае блак-скоуп, і таму што язык спецыяльна стварае новую прыязню i для кожнага прайходу ціклу, кожны калебэк у падзею заканчваецца зі сваім адзінственным i, які застаецца незменным на значэнне, якое ён меў у той конкрэтны ітерацыі. Код выглядае практычна так сама, як і у пакульнам прыкладзе, але базовая правіла скоупу разныя, і гэта дае набліжна прыгляднае рашытак.
Калебэкі — не аднае засобная функцыя, а проста наследкі ланцуга скоупу
Калі вы разумеете, як працюе ланцуг контекстаў, закрытыя функціі практычна не трэбуе дадатковага адказвання. Закрытая функцыя — гэта не якісь дадатковы механізм, дапамагаючый працаваць з контекстамі. Це проста тое, што выклікаецца сама сабою кожны раз, калі вы визначаеце функцыю ўнутрь іншай функцыі, а гэтая внутранняя функцыя пасля таго выкарыстоўваецца там, дзе звычайна бы ўжо не застаўся зовнішній контекст.
function debounce(fn, delayMs) {
let timeoutId;
return function (...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn(...args), delayMs);
};
}
const debouncedSearch = debounce((query) => runSearch(query), 300);
debounce выконваецца роўна адназначы. Якщо вы прывыклі да мов без клаўзураў, ваша інстынктывная думка можа быць такой, што timeoutId павінен зникнуць як толькі debounce завершыць сваю роботу. Аднак ён не зникае, таму што функцыя debounce, якая вяртаецца, была запрацоўвана ўсередзіне яго, што значыць, што ланцуг області працы вярнутай функцыі стойкава уключае сабе область працы самога debounce, уключаючы timeoutId. Кожны наступны вызов debouncedSearch, незалежна ад таго, насколькі пазней ён выканаўся, праходзіць па тым самым ланцугу областей працы, ўсё да таго самога timeoutId. Самэ гэтая прычына, чаму логіка debounce работае: патрэбная єдыная, стойкая зменна, якая фіксуе чакаючы таймаут праз кожны вызов, і клаўзура ёсць тым, што гарантуе гэта.
Тая ж сама функцыя, якая задае клаўзуры, можа таксама спрычыніць вытэк памяці
Тое, што робіць клозуры корыстнымі, — гэта тая ж самая асоблівасць, якая стварае конкрэтную, павторюючуюся проблему: клозура зберагае ўсю свою цэлую абрамляючую сферу, а не толькі зменныя, на якія вона фактычна сыходзіць, і ў яе є жывая ссылка на саму зменную, а не зашчытаная копія ў значэнні, якое было у момент стварэння клозуры.
function createHandlers() {
let clickCount = 0;
const massiveDataset = loadHugeArray(); // large, no longer needed after setup
return {
onClick: () => {
clickCount++; // only this variable is actually used
console.log(clickCount);
},
};
}
onClick-аўскія клаузуры захопляюць усё прастора імен, які належыць createHandlers, уключаючы massiveDataset, нават які onClick на самай працы ніколи не чытае. Колькі onClick застаецца актыўным, усё іншае, што ён захопіў, таксама застаецца актыўным, і гэта ёсць справжняя, хоць і часта незначная, проблема з памяцюю, калі дыярант з дзейнасцю на дужо длігі час застаецца з аб’ектамі, які ўтримваюць канкэрэнты да вялікых або непатрэбных дадзенняў. А так как клаузура утримвае саму зменную, а не ўскопіяваную яе значэнне, яна завжды бачыць текущы, актуальны стан. Гэта тая ж самая асновная правіла, якая дазволіла прыкладу з debounce працаваць правільна і якая змусіла цыкл з var выдаваць 3, 3, 3; гэта адна і тая ж механізм, якая ў однам контэксте выступае як корыстная функцыя, а ў іншам — як баг.
Розумэнне механізма краща, чым запам’ятовванне яго апісу
Тэкст у падручніку „Закрыцце — это функцыя, яка асоціюецца зі сваёй лексычной средой“ тэхнічна правільны, але ён рэдкая час з’яўляецца ў свядомасці, пакуль вы калькольваеце ланцюг дыяпазонаў кілька разоў і не пабачыце, дзе самэ ўсё завершаецца. Калі калькольванне гэтага ланцюга стане прыродным, закрыцця больш не патрабуюць спецыяльных пояснэнняў. Яны проста ёсць звычайным наследкам таго, як завжды працаваў дыяпазон, прыкладзеным да функцыі, якая, як правило, жыве дольш за среду, у якой была створана.
Спаднёйшыя матэрыялы
- Што насправды робіць і чаго не робіць натыўнае падтрымкі TypeScript у Node.js — У гэтым артыкуле пояснюецца, як Node.js запускае файлы .ts натыўна за дапамогою адключэння типаў, чымі ён прыменшуе перакананне типаў, і калі вам все ж патрэбны рэальныя крокі будовы.