Паспрацаванне з JavaScript у прыгледах: абаранты, хіп, вытэкі, закрыцція
Структураваны аптак для падготовкі да спэктываў: стак вызываў, калекцыі задач і мікрозадач, хіп і процес збору сметлівых дадзенняў, вытэкі памяці, дэлегаванне, протатыпы і замкнутасці.
Крыніцы: #1 FrontEnd Concepts For Interviews — Javascript Fundament
У спэктарыях на праблемах фронт-енда рэдкасцю бывае, калі цэнююцца заучаныя фразы. Адзінавальнікі выявляюць, чы можаце вы поясніць, што робіць двайчык з вашым кодам: дзе чакае каллбэк, чаму об’ект жыве дыягональна сваёй функцыі, чаму адзін слухальнік у контэйнеры можа заменіць сотні. Шырокі падчытак раскрывае гэтыя основы ў той парад, у якім яны выступаюць адна на другой, тады вы зможаеце логічна разглядаць незнайомыя пытанні, а не проста дакладваць адказ. Хорашы саматэст: пасля кожнага раздзелу поясніце ідею вогулом, як бы для колегі, які ніколи пра ёй не чуў.
Стак каллаў: дзе выконваецца ваш код
Кожны ўраджык JavaScript выкананае кода на стаке вызваў, яны ёсць структурой типу «пасляльгаванне — перша выйма», якую можна уявіць як масэвы элемент, дзе данні дадаюцца толькі з канца і выкарыстоўваюцца таксама з канца. Калі вызываецца функцыя, для ўмов яе выканання дадаёцца фрэйм; калі функцыя вяртаеся, фрэйм выкарыстоўваецца. На галоўнай ніткі існуе толькі адны такі стак, таму пакуль фрэйм выкананае, нічога іншага не можа выканацца. Дзейсны сінхронны цыкл замарожвае адрасаванне і ввод пакуль не завершыцца.
Чаму запит да сеті не замарожвае сторанцы
Якшто за раз выкананае толькі адна задача, чаму сторанцы не зупіняецца пакуль яна чакае на адпаведны сервер? Таму што не JavaScript чакае. Уявіце экран, які обрабоўвае великі набор дадзеных, а потым па спэрыядзе вызывае калькі API. Вызыв fetch() перадае задачу Web API, які знаходзіцца ў браузеры і караце сетевыя запыткі па званню стака. Функцыя негайна вяртае обявленую задачу, і корыстнік можа продаваць работу з сторанцай.
Калі прыходзіць адпаведна адказ, обява выканана і ўсі яе .then() функцыі-выкліканні запланаваны як мікрозадачы, якія запускаюцца пасля таго, як стак працы спрагнуў. Тэчнае значэнне такое, што завершэння сіткавай адпаведзі выдаецца через механізм задач браузера, а ў чергу мікрозадач уходзяць рэакцыі обявы. Галоўная мысль для інтэрв’ю — ваша прыемнае дзейство запланавана ў чергу, а не выконваецца безпосередна.
Чэрга задач проты чэргі мікрозадач
Таймеры праходзяць іншым шляхам: функцыя-выкліканні setTimeout() падае ў чэргу задач, якая таксама называецца чэргай макрозадач. Правілам цыклу змаганняў є тое, што пасля завершэння чынной задачы і спрагнення стака ён спачатку адмахвае всю чэргу мікрозадач, перш чым выбраць наступную задачу. Цяя функцыя запланавае по адной задачы кожага типу; номеры ў паведамленнях паказваюць порядак выканання.
function triggerBothQueues() {
console.log("1. Synchronous: Code starts on Call Stack");
// Enqueue a Macrotask (Task Queue)
setTimeout(() => {
console.log("4. Task Queue (Macrotask): setTimeout callback executed");
}, 0);
// Enqueue a Microtask (Microtask Queue)
Promise.resolve().then(() => {
console.log("3. Microtask Queue: Promise.then callback executed");
});
console.log("2. Synchronous: Code ends on Call Stack");
}
triggerBothQueues();
Две сінхронныя лініі спачатку выведаюць даны, пасля чаго — калебэк функціі promise, а потым — таймер. Задзейсненне з запаздоўкай 0 означае „працаваць якомога шырэй“, а не „працаваць зараз“, і мікрозадача все равно будзе выканана першай. Частая наступна ситуацыя: мікрозадача, якая плануе іншую мікрозадачу, прыўязываецца да той самай чергі і выкананая раней за будзь-які таймер, таму безканечны ланцуг мікрозадач можа завяліць процес атрыбутавання элементаў.
Heap: дзе жывуць об’екты
Стак парадуе контэксты выканання; хіп — данні, на якія вони паляцуюць. Об’екты, масівы, функцыі і ўсё, што керуецца па ссылцы, знаходзяцца на хіпе, тады як локальныя прымітывы, такія як числы і булевы значэння, концэптуальна зберагаюцца разам з контэкстам выканання функцыі. У інтэрв’ю можна сказаць, што гэта спрасцавленне: кампіляторы зазвычай выделяюць строкі на хіпе і могу ўзаўмна іх выкарыстоўваць, а прымітыва, які зберагаецца як атрыбут об’екта, таксама знаходзіцца на хіпе разам з тым об’ектам. Гэтыя моделі є псыхалагічнымі атрыбутамі, а не тэхнічнымі спецыфікаціямі.
Сбор шматку і досяжнасць
Хіп не є безканечным. Фонавы сборшчык шматку вывалюе памяць, выкарыстоўваючы ідэю досяжнасці:
- Кораны за сваёй прыродай є досяжныя: глобальныя зменны, а таксама параметры і локальныя зменны функцый, якія зараз знаходзяцца на стаке.
Усё, да чаго нельга дасягнуць з жадного корана, ўважаецца мусорам. Калектар не пакушаецца з’ясавіць, чыры вы ўсё яшчэ намераваліся вядомы об’ект; як толькі існуе дасяжны рэферэнс, ён застаецца. Самэ так і выклікаюцца вытеканні памяці. Для глыбокага аналізу, спрямованага на інструменты, адзірніце як ловіць вытеканні памяці ў JavaScript за дапамогою прынцыпаў дасяжнасці і рэтэйнероў.
Два класычныя спосабы вытэкання памяці
Таймер, які ніколі не зупіняецца
У гэтым тракеры на панелі керування вялікі масіў ствараецца ўнутрь функцыі, а калбэк інтэравала чытае яго дужыну кожную секунду.
function startDashboardTracker() {
const hugeDataPayload = new Array(1000000).fill("data");
// Timer is launched
setInterval(() => {
// Because hugeDataPayload is referenced here,
// it stays trapped in memory forever!
console.log("Tracker pinging... Payload length:", hugeDataPayload.length);
}, 1000);
}
startDashboardTracker();
// Even after startDashboardTracker finishes executing and leaves the Call Stack,
// hugeDataPayload CANNOT be garbage collected because the timer's closure holds a reference to it.
Калі функцыя startDashboardTracker вяртаеся, можна спадзявацца, што массив стане некорыстным. Аднак гэтага не выходзіць: прыгледар захоўвае актыўны інтэрвал, гэты інтэрвал зберагае свой калебак, а калебак захоўвае hugeDataPayload. Зберагаюце ID, які вяртае setInterval(), і вызывайце clearInterval(), калі робота завершылася або калі компонент UI быў зняты; разрыхленне гэтага першага зв’язку вызвалівае звалечэнне всей ланцюга.
Аслухальнік на глобальным об’екте
Гэты прыклад ўскладнены, таму што, здаецца, нічога не працюе. Віджет для пошуку стварае вялікі індэкс і рэгіструе функцыю обработкі змены размера на window.
function setupSearchWidget() {
// Heavy data tied to the widget scope
const heavySearchIndex = new Array(1000000).fill("search_index_item");
function handleResize() {
console.log("Window resized. Index size:", heavySearchIndex.length);
}
// Listener attached to the global 'window' object (a Root)
window.addEventListener("resize", handleResize);
}
setupSearchWidget();
// Later, user navigates away and the Search Widget DOM element is deleted.
// BUT 'window' still holds a reference to 'handleResize',
// which holds a reference to 'heavySearchIndex'.
Калі корыстнік ад’ехае і DOM-элемент віджета усуваецца, window, які ўстаўляеся як коран, все ж такі мае у сабе элемент handleResize, які зберагае кэшаваны экземпляр heavySearchIndex. Неабяжна вызваць removeEventListener, калі віджет усуваецца, і паслаць той самы аб’ект функціі, які вы зарэгістравалі; новая функцыя-стрэлка є адным зусім іншым об’ектам і нічога не усувае.
Розпространенне і делегаванне змаганняў
React і Angular скрываюць велікую частку такога калектавання інформацыі. Адна з прычын — делегаванне змаганняў: уместо аднарозовых слухачаў на кожным вузле, React слухае ў корановым контейнеры і распрацоўвае змаганняя, калі яны распространяюцца вгору, а його сінтэтычная система змаганняя прыўязуе і ад’юнае слухачаў, калі компаненты запускаюцца і завершаюць свою роботу.
Дэлегаванне выконвачаецца за дапамою механізма „бублінг“: клік адправляецца да цэльнага элемента, а пасля праходзіць вышэй праз кожны яго предка. На гэтай старонцы ўтвореныя тры колькаваныя рамкі, кожная з якіх мае аб’ект для пераслухвання клікаў.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Event Bubbling Example</title>
<style>
#grandparent {
background-color: lightcoral;
padding: 20px;
}
#parent {
background-color: lightblue;
padding: 20px;
}
#child {
background-color: lightgreen;
padding: 20px;
}
</style>
</head>
<body>
<div id="grandparent">
<h1>Grandparent - only one event listener</h1>
<div id="parent">
Parent
<div id="child">
Child
</div>
</div>
</div>
<script>
document.getElementById('grandparent').addEventListener('click', function(event) {
console.log('Grandparent clicked!');
console.log('target: ', event.target.getAttribute("id"));
// if (event.target.getAttribute("id") === 'child') {
// //
// }
});
document.getElementById('parent').addEventListener('click', function(event) {
console.log('Parent clicked!');
// event.stopPropagation();
});
document.getElementById('child').addEventListener('click', function(event) {
console.log('Child clicked!');
// event.stopPropagation();
});
</script>
</body>
</html>
Калі нажмаць на самую внутранюю рамку, фіксуецца інформацыя пра дзецю, ягоякі, а пасля пра дадзеякага. Дадзеякі чытае event.target, ўбачаючы, який элемент на самай працоўкі быў нажаты, а прымечаны if паказвае, як адны аб’ект пераслухвання можа робіць разлік на ID, тады дзецям не патрабуецца аднародны аб’ект. Гэта і є дэлегаванне у мініятуры: адны аб’ект пераслухвання, расположаны высока ў дрэвае элементаў, карыстаецца для контролю ўсіх наследнікаў, уклучаючы тыя, якія былі даданы пазнейша, і пры тым викорыстоўваецца значна менша колькасць памяці.
Коментаваныя вызначэнні event.stopPropagation() паказваюць працоўнае правілу наадворс: калі яны ўвімкнутыя ў дзецячым элементах, яны зупіняюць запуск падзеі прычым яна не дасягне абяцелых элементаў (дывісь документацыю да stopPropagation). Іспользуйце іх адзячна, адтуды што яны таксама блакуе дэлегаваныя працоўнікі вышэй у іерархіі, уключаючы корневага аслухальніка фрэймворку. Цякі эксперыменты з гэтым файлам безпосередна маюць сенс; звычайны addEventListener должен стаць знакомым раней, чым абстракціі фрэймворку.
Функцыональная програмаванне ў стислым відзе
Функцыональная програмаванне (FP) часта абразуецца як лінія зборкі із аўтаномных машын: кожная з яых прымеўа вхідныя данні, стварае выходныя і не вплывае на што-небудзь іншае. Якраз калі код аб’ектно-орыентаванага падходу часта мутуюць стан аб’ектаў, функцыональная програмаванне ухіляецца ад мутацый і спакоюеся на пяці прынцыпах:
- Чыстыя функцыі: выходны результат залежы толькі ад вхідных дадзеных, без жадных зовнішняйх парадоксальных наследкав.
map і filter, замест таго, каб кераваць лічыльнікамі і станом.Класы і протатыпы
З ES2022 класы падтрымляюць справжнія прыватныя поля з прыметкай #. Тут #balance доступны толькі ўнутры корпусу класа.
// 1. PURE JAVASCRIPT (Native ES2022+)
class BankAccount {
#balance = 0; // Native JS private field (using #)
constructor(owner, initialBalance) {
this.owner = owner;
this.#balance = initialBalance;
}
deposit(amount) {
this.#balance += amount;
}
}
const account = new BankAccount("Alex", 1000);
// account.#balance; ❌ Uncaught SyntaxError (Hard runtime privacy)
Чытанне account.#balance зза меж класа вызывае SyntaxError, таму прыватнасць гарантуецца самым языкам, а не традыцыямі.
Падае, JavaScript яшчэ выкарыстоўвае спадчына на аднойчынных пратыпах: об’екты давяраюць пошук атрыбутаў пратыпу. До ES6 структуры, падобныя класам, былі ствараны вручную за дапамой функцый-канстрактараў, як у гэтым варыянце рачунку з падкласам для захоўвання грошаў.
// 1. BASE CONSTRUCTOR FUNCTION (Parent Class)
function BankAccount(owner, initialBalance) {
this.owner = owner; // Public property
// PRIVATE VARIABLE (Data Privacy via Closure)
// Lives in function scope, completely hidden from the outside world!
var balance = initialBalance;
// Public method privileged to access the private 'balance' variable
this.deposit = function (amount) {
balance += amount;
this._logTransaction("Deposited quot; + amount + ". New Balance: quot; + balance);
};
}
// PROTECTED / SHARED METHODS (Prototypal Inheritance)
// Attached to the prototype so methods aren't re-created for every single instance
BankAccount.prototype._logTransaction = function (action) {
console.log("[" + this.owner + "]: " + action);
};
// 2. DERIVED CONSTRUCTOR FUNCTION (Child Class)
function SavingsAccount(owner, initialBalance, interestRate) {
// Call Parent Constructor with 'this' context (Super Call)
BankAccount.call(this, owner, initialBalance);
this.interestRate = interestRate;
}
// Inherit parent prototype methods on the child
SavingsAccount.prototype = Object.create(BankAccount.prototype);
SavingsAccount.prototype.constructor = SavingsAccount; // Reset constructor reference
// Child Public Method
SavingsAccount.prototype.addInterest = function () {
// Calls the inherited prototype method
this._logTransaction("Applied " + this.interestRate + "% interest");
};
// 3. USAGE
var mySavings = new SavingsAccount("Alex", 1000, 5);
mySavings.deposit(200); // -> [Alex]: Deposited $200. New Balance: $1200
mySavings.addInterest(); // -> [Alex]: Applied 5% interest
Заазначыце, што balance ўтримваецца як прыватны за дапамой клозуры: ён знаходзіцца ў дыапазоне канстрактара і толькі deposit можа яго бачыць. _logTransaction знаходзіцца на пратыпе, таму всі экземпляры дзеляцца адной функцыяй, а ўсілака падчыркі значыць толькі „захаваны“ за прыметам. BankAccount.call(this, ...) — это ручны эквівалент super(), а Object.create запоўнюе пратыпы перш чым будзе вярнута замененае значэнне атрыбута constructor.
Сучасныя коды фронт-энда ў будзь-якім случае вяртаюць прыоритэт композіцыі над спадчынай. Якщо вам трэба строгіяя пераканання пры выкарыстоўванні парадакту OOP, такія як модифікаторы доступу і інтэрфейсы, тады TypeScript яўляецца кращым выбрам; гэты болей дзялёны аналіз праглядае ўсе дэталі.
Закрыцця
Є ўсё, што можна дакладней розглядзець — ад Canvas і іншых API прыгледачаў да модуляў, ітератароў і генератораў, але закрыцця з’являюцца практычна ў кожным інтэрв’ю, і вы вже сталкнуліся з ямі ў прыкладах з вытэкамі. Закрыцце — это функцыя, якая асоціюецца з лексычным сераўаком, у якім яна была створаная. Паколькі внутрэшняя функцыя зберагае канект да гэтага сераўака, яна можа чытаць і адкладвати змяні ў зменных зовнішней функцыі нават пасля таго, як зовнішняя функцыя вярнулася.
function createCounter() {
let count = 0; // Outer variable
return function() {
count++; // Inner function accesses 'count'
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
count застаецца між вызвамі, таму што функцыя, яка вяртаеся, заставае свое сераўакі доступнымі, што безпасэчна зв’язана з правіламі сбору сметлі. Кожны вызов createCounter стварае незалежны лічылак.
Практычны прыклад закрыцця: debounce
Рэалістычны прыклад — debounce: функцыя выконваецца толькі пасля таго, як вызвы завяршыліся пры заданай затрымцы, як у функцыі пошуку пад час напісання.
function debounce(func, delay = 300) {
let timer; // 1. The closure variable that holds the timeout reference
return function (...args) {
// 2. If the function is called again before the delay finishes,
// cancel the previous scheduled run
clearTimeout(timer);
// 3. Schedule a new run after the delay
timer = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}
// --- Usage Example ---
function handleSearch(query) {
console.log(`Searching API for: ${query}`);
}
// Wrap our search function in the debounce function
const debouncedSearch = debounce(handleSearch, 500);
// Simulating fast typing into an input field:
debouncedSearch("a"); // Timer starts...
debouncedSearch("ap"); // Cancels "a", resets timer...
debouncedSearch("app"); // Cancels "ap", resets timer...
debouncedSearch("apple");// Cancels "app", resets timer...
// Only "Searching API for: apple" will log to the console
// 500ms after the last keypress!
Закрыты ўрадок timer являецца станам. Кожны вызов абнімае чакаючы тайм-аут і плануе новы, таму запускаецца толькі последній вызов у серыі: быстрая абаротка "a", "ap", "app", "apple" прыносіць адна выкарыстоўванне для пошуку "apple" за 500 мс пасля последней натыкання. func.apply(this, args) перадае як аргументы, так і контекст вызову. Па вопытам прызнікання і спецыяльным случаям, адзірніце як напісаць утыліту debounce з нуля.
Галоўныя выводы
- Ваш код запускаецца за дапамою однага стака вызоваў; API прыгледача адпавядае за чаканне і вяртае рэзультаты через калейкі.
- Усе чакаючыя мікразадачы запускаюцца раней за наступную задачу, напрыклад, калебраны ўрадок таймера.
- Колектар зберагае все, што ўсё ж можна дацягнуць з корана, незалежна ад таго, чы яго трэба, чы ні.