- Опубліковано
- 8 хв читання
Цікаві запитання зі світу JavaScript
- Автори
- Ім’я
- Learna
Навіщо ця стаття?
Тут зібрані цікаві ситуації з JavaScript: одні важливо пам’ятати в роботі, інші просто допомагають краще зрозуміти мову. Це добірка неочевидних «JS-моментів», з якими ми стикаємося не щодня. Більшість завдань — у форматі «вгадайте результат» або «чому це не працює».
Порядок виконання в циклі подій: promises, queueMicrotask.
Вгадайте порядок виведення.
Promise.resolve().then(() => console.log(1));
queueMicrotask(() => console.log(2));
setTimeout(() => {
console.log(3);
}, 0);
console.log(4);
new Promise(() => {
console.log(5);
});
(async () => console.log(6))();
Відповідь
4, 5, 6, 1, 2, 3
Пояснення:
-
console.log(4)виконується синхронно. -
Тіло виконавця
new Promise()також виконується синхронно. -
Асинхронний функціональний вираз, який одразу викликається (IIFE):
(async () => console.log(1))(); new Promise(() => console.log(2)); console.log(3); // 1, 2, 3 -
Коли синхронний код завершився, обробляється черга мікрозавдань. Першим у ній стоїть обробник уже виконаного Promise:
then(() => console.log(1)). -
Далі виконується колбек
queueMicrotask(() => console.log(2))із тієї самої черги. -
Після спорожнення мікрозавдань цикл може перейти до наступного завдання — колбека
setTimeout(() => console.log(3), 0).
Що з наведеного не є мікрозавданням?
a. Завантаження скрипту
b. Колбек setTimeout
c. Подія mousemove
d. Колбек requestAnimationFrame
e. Рендеринг інтерфейсу
f. Колбек then
g. Функція-виконавець new Promise
h. Виклики fetch
Відповідь
Усі перелічені пункти, крім колбека then. Сам виклик fetch не є мікрозавданням, хоча обробники повернутого ним Promise використовують чергу мікрозавдань. Виконавець new Promise запускається синхронно.
Порядок виконання в циклі подій: promises, async/await.
Яке значення буде останнім?
async function asyncFunction() {
console.log(1);
new Promise(() => console.log(2));
await new Promise((res) => {
setTimeout(() => console.log(3), 0);
res();
});
}
new Promise((res) => {
console.log(4);
(async () => {
console.log(5);
await asyncFunction()
console.log(6);
})();
res();
}).then(() => console.log(7));
console.log(8);
Відповідь
4, 5, 1, 2, 8, 6, 7, 3
Що саме відбулося?

- Виконавець зовнішнього
new Promiseсинхронно виводить4. - IIFE доходить до
await: спочатку синхронно виводить5і викликаєasyncFunction. asyncFunctionвиводить1; виконавець вкладеного Promise —2.- Усередині Promise, якого очікує
asyncFunction, планується таймер із виведенням3і викликаєтьсяres(). - Продовження
asyncFunctionпісляawaitстає мікрозавданням. IIFE також очікує завершенняasyncFunction. - Зовнішній Promise завершується, його
.then()планує виведення7. - Решта синхронного скрипту виводить
8. - Продовження
asyncFunctionвиводить6і завершує функцію, після чого може продовжитися IIFE. - Виконується запланований
.then()із7, а згодом — таймер із3.
Важливо розрізняти виклик async-функції, який починається синхронно, і її продовження після await, яке планується окремо.
Порядок виконання в циклі подій: async, Promise.all.
Яке значення буде першим?
(async () => {
const asyncFunc = async () => "asyncFunc";
const promise = new Promise(() => {
console.log("promise");
}).then(() => console.log("then"));
console.log("asyncBody");
queueMicrotask(() => {
console.log("queueMicrotask");
});
const results = await Promise.all([asyncFunc(), promise]);
return results;
})();
console.log("script");
Відповідь
promise, asyncBody, script, queueMicrotask
Чому не виконується then із шостого рядка? Promise, створений у четвертому рядку, ніхто не завершує: він залишається у стані pending.
Області видимості та замикання
Основні області видимості JavaScript — глобальна, функціональна й блокова; модулі також мають власну область видимості.
const name = "John" ; // Global Scope
{
const name = "John"; // Block Scope
}
function foo() {
const name = "John"; // Function Scope
}
Якщо функція визначена всередині іншої, вона може звертатися до змінних зовнішньої області видимості. Замикання — це функція разом із доступом до її лексичного оточення. У JavaScript замикання формується під час створення функції.
Яким буде результат?
function createCounter() {
let global = 0;
function incrementCounter() {
const incremented = ++global;
return incremented;
}
return { incrementCounter };
}
const counter = createCounter();
console.log(counter.incrementCounter());
console.log(counter.incrementCounter());
console.log(createCounter().incrementCounter());
Відповідь
1, 2, 1
Чи рівні ці об’єкти?
function userFactory() {
let user = { name: "", createAt: Date.now() };
return (name) => {
"use strict";
user.name = name;
user.createAt = Date.now();
return user;
};
}
const createUser = userFactory();
console.log(createUser("John") === createUser("NotJohn"));
Відповідь
Так, це той самий об’єкт.
userFactory — помилкова реалізація фабрики: вона повертає той самий об’єкт, а мала б створювати новий. Водночас це наочний приклад замикання.
Порівнюючи об’єкти через ===, ми перевіряємо їхню ідентичність, а не рівність вмісту.
Що таке hoisting?
Слово hoisting («підняття оголошень») може вводити в оману. Рушій JavaScript не переміщує рядки на початок файлу. То що ж насправді означає цей термін?
Hoisting описує поведінку оголошень до того, як виконання дійде до їхнього місця в коді. До пояснення повернемося трохи нижче, а спочатку — запитання.
Які твердження правильні?
- a. Hoisting фізично переміщує функції та змінні на початок файлу.
- b. Оголошення
letіconstвпливають на область видимості ще до свого рядка, але до ініціалізації недоступні. - c. Змінні
varдо виконання присвоєння залишаються неініціалізованими. - d. Hoisting — це переставляння рядків під час виконання.
- e. Статичні
importдоступні в модулі незалежно від їхнього текстового розташування.
Відповідь
b and e
Для пояснення виконання коду часто використовують модель контексту виконання з двома етапами:
- Підготовка оточення.
- Виконання коду.
Під час підготовки оточення:
- Формуються зв’язки для змінних і функцій у відповідній області видимості. Глобальний об’єкт створює середовище виконання, а не кожен файл.
varініціалізується значеннямundefined.- Оголошення функцій пов’язуються з функціями;
letіconstдо своєї ініціалізації залишаються в temporal dead zone.
Під час виконання:
- Рушій обчислює вирази й виконує інструкції згідно з потоком керування.
Приклади hoisting
const framework = "Next.js";
let renderingPattern = "SSR";
var bestProgrammingLanguage = "HTML😁";
До виконання оголошень let і const їхні зв’язки вже існують, але ще не ініціалізовані. var bestProgrammingLanguage натомість має значення undefined. Присвоєння з прикладу відбуваються під час виконання відповідних рядків.
Розгляньмо ще один приклад:
var counter = 1;
function resetCounter() {
console.log(counter); // undefined
var counter = 0;
console.log(counter); // 0
};
resetCounter();
А тепер приклад із помилкою:
let counter = 1;
function resetCounter() {
console.log(counter);
const counter = 0;
console.log(counter);
};
resetCounter();
Цей код викине Uncaught ReferenceError: Cannot access 'counter' before initialization.
Виконання доходить до console.log(counter) раніше, ніж ініціалізується counter, тому виникає помилка. Зв’язок для змінної вже існує в лексичному оточенні функції, але читати його ще не можна. Це відрізняється від звернення до зовсім неоголошеної змінної. Порівняйте повідомлення на зображенні:
Отже, hoisting — узагальнена назва поведінки оголошень до їхнього текстового місця. Не всі оголошення отримують однакове початкове значення: var, функції, let, const та імпорти мають різні правила.
Що таке this?
Значення this залежить від контексту та способу виклику.
Верхній рівень: у звичайному браузерному скрипті це глобальний об’єкт, але у ES-модулі — undefined; CommonJS у Node.js має власні правила.
Звичайна функція: для виклику obj.method() значенням є obj. Його також можна явно задати через call, apply або bind.
Стрілкова функція: не створює власного this, а бере його з лексичного оточення.
Класи: у методі екземпляра, викликаному як instance.method(), це екземпляр; від’єднання методу від об’єкта може змінити результат.
Суворий режим: звичайний виклик функції без об’єкта дає undefined, а не глобальний об’єкт.
Приклади нижче передбачають звичайний несуворий скрипт. У ES-модулі або strict mode частина результатів буде іншою.
Приклади this
Чи зможете ви правильно відповісти на всі запитання?
function printContext() {
console.log(this);
}
const thatObj = {
printContext,
printContext_1() {
return printContext();
},
printContext_2() {
return thatObj.printContext();
},
};
thatObj.printContext();
thatObj.printContext_1();
thatObj.printContext_2();
Відповідь
thatObj, global, thatObj
thatObj.printContext()викликається як метод об’єкта, томуthis—thatObj.thatObj.printContext_1містить результат попереднього виклику функції, а не прив’язаний доthatObjметод.thatObj.printContext_2()усередині делегує виклик черезthis.printContext().
const thatObj_1 = {
foo() {
console.log(this);
},
bar: () => {
console.log(this);
},
};
const thatObj_2 = {
foo: thatObj_1.foo,
bar: () => thatObj_1.bar(),
baz() {
thatObj_1.foo();
},
bam() {
thatObj_1.foo.call(this);
},
bah() {
const nestedF = () => {
console.log(this);
};
nestedF();
},
};
thatObj_2.foo();
thatObj_2.bar();
thatObj_2.baz();
thatObj_2.bam();
thatObj_2.bah();
Відповідь
thatObj_2, global, thatObj_1, thatObj_2, thatObj_2
thatObj_2.bam()викликаєfooзthis, що дорівнюєthatObj_2. Для звичайного методу важливе не місце оголошення, а спосіб виклику.- Стрілка, яку повертає
thatObj_2.bah(), запам’ятовуєthisзовнішнього виклику —thatObj_2.
const thatObj = {
printContext() {
console.log(this);
},
printContext_1() {
function nestedF() {
console.log(this);
};
nestedF.apply(this);
},
};
const { printContext, printContext_1 } = thatObj;
printContext();
printContext_1();
thatObj.printContext();
thatObj.printContext_1();
Відповідь
global, global, thatObj, thatObj
- Після деструктуризації
printContext()іprintContext_1()викликаються як звичайні функції без об’єкта. У несуворому скрипті це дає глобальнийthis; у суворому режимі було бundefined.
Чи можна обійти масивоподібний об’єкт за допомогою Array.prototype.forEach?
Так — метод можна позичити через call 😊.
const arrayLike = {
length: 3,
0: 2,
1: 3,
2: 4,
3: 5, // ignored by forEach() since length is 3
};
Array.prototype.forEach.call(arrayLike, (x) => console.log(x)); // 2, 3, 4
Дякую за читання! Сподіваюся, було корисно й трохи весело. До зустрічі! 👋
