Думаєте, ви знаєте JavaScript?

14 хв. читання

Усі ми знаємо, що браузер — «дім» для JavaScript. Вони працюють пліч-о-пліч за допомогою розробників. Тож дуже важливо розбиратися в концепціях JS, аби ця робота проходила максимально злагоджено. Однак такі теми як прототипи, замикання та цикл подій часто спричиняють труднощі, тому потребують більш детального розбору. Як відомо, обмежені знання — небезпечна річ, адже це прямий шлях до помилок.

Спробуймо пограти в мінігру, де треба відповісти на всі запитання. Якщо не знаєте відповіді, спробуйте здогадатися. Запам'ятайте чи запишіть свої відповіді та звірте їх з правильним варіантом нижче. За кожну правильну відповідь — 1 бал. Почнемо.

В деяких фрагментах коду для оголошення змінної спеціально використовувалось var, щоб ви без проблем могли копіпастити код в консоль браузера і не отримати SyntaxError. Проте автор статті не підтримує використання var, адже з let і const код буде більш надійним та стійким до помилок.

Запитання #1: Що буде виведено в консоль браузера?

var a = 10;
function foo() {
    console.log(a); // ??
    var a = 20;
}
foo();

Запитання #2: Чи буде відрізнятися результат, якщо ми використаємо let чи const замість var?

var a = 10;
function foo() {
    console.log(a); // ??
    let a = 20;
}
foo();

Запитання #3: З яких елементів складатиметься newArray?

var array = [];
for(var i = 0; i <3; i++) {
 array.push(() => i);
}
var newArray = array.map(el => el());
console.log(newArray); // ??

Запитання #4: Чи буде переповнено стек викликів, якщо ми виконаємо foo() в браузері?

function foo() {
  setTimeout(foo, 0); // тут буде помилка stack overflow?
};

Запитання #5: Чи буде реагувати UI сторінки, якщо ми запустимо таку функцію в консолі?

function foo() {
  return Promise.resolve().then(foo);
};

Запитання #6: Чи можна якось використати синтаксис spread з цим виразом, щоб не отримати TypeError?

var obj = { x: 1, y: 2, z: 3 };
[...obj]; // TypeError

Запитання #7: Який результат отримаємо в консолі після запуску цього фрагмента коду?

var obj = { a: 1, b: 2 };
Object.setPrototypeOf(obj, {c: 3});
Object.defineProperty(obj, 'd', { value: 4, enumerable: false });

// які властивості буде роздруковано, коли ми запустимо цикл for-in?
for(let prop in obj) {
    console.log(prop);
}

Запитання #8: Який результат отримаємо після виклику xGetter()?


var x = 10;
var foo = {
  x: 90,
  getX: function() {
    return this.x;
  }
};
foo.getX(); // виведе 90
var xGetter = foo.getX;
xGetter(); // виведе ??

Відповіді

Тепер спробуймо дати правильну відповідь на кожне запитання. У поясненнях будуть посилання на додаткові матеріали.

Відповідь #1: undefined

Пояснення

Змінні, оголошені за допомогою ключового слова var, спливають в JavaScript і їм присвоюється значення undefined. Однак ініціалізація відбувається в місці, де ви присвоюєте цій змінній значення. До того ж змінні, оголошені з var, мають область видимості всередині функції, а let та const мають блокову область видимості. Ось який вигляд має цей процес:

var a = 10; // глобальна область видимості
function foo() {
// Оголошення var a спливе на початок функції
// Щось на зразок: var a;

console.log(a); // виведе undefined

// справжня ініціалізація значенням 20 відбувається лише тут
   var a = 20; // локальна область видимості
}

Відповідь #2: ReferenceError: a is not defined

Пояснення

let та const дозволяють оголошувати змінні, область видимості яких обмежена блоком або виразом, де вони використовуються. На відміну від var такі змінні не спливають і мають так звану тимчасово мертву зону (Temporal Dead Zone). Якщо ви спробуєте отримати доступ до таких змінних в цій зоні, виникне ReferenceError, оскільки в цьому випадку змінні можна використовувати лише після оголошення. Більше про лексичну область видимості, а також Контекст та Стек виконання в JavaScript за посиланнями.

var a = 10; // глобальна область видимості
function foo() { // вводимо нову область видимості, початок TDZ 

// Створено неініціалізоване прив'язування для 'a' 
    console.log(a); // ReferenceError

// TDZ закінчується, 'a' проініціалізовано значенням 20 тільки тут
    let a = 20;
}

Ця таблиця демонструє поведінку спливання та областей видимості, залежно від способу оголошення змінної в JavaScript.

Ключове слово Спливання Область видимості Створює глобальні властивості
var Оголошення Функція Так
let Тимчасово мертва зона Блок Ні
const Тимчасово мертва зона Блок Ні
function Повне Блок Так
class Немає Блок Ні
import Повне Глобальна відносно модуля Ні

Відповідь #3: [3, 3, 3]

Пояснення

Оголосивши змінну за допомогою ключового слова var на початку циклу for, ми створили одиничну прив'язку (місце для зберігання) цієї змінної. Читайте детальніше про замикання. Поглянемо на цикл for ще раз:

// Будемо вважати, що тут блокова область видимості
var array = [];
for (var i = 0; i < 3; i++) {
  // Кожне значення 'i' в тілі трьох стрілкових функцій 
  // посилається на те саме зв'язування, саме тому
  // наприкінці циклу кожен раз повертається значення '3' 
  array.push(() => i);
}
var newArray = array.map(el => el());
console.log(newArray); // [3, 3, 3]

Якщо ви перевизначите змінну, використавши let, що має блокову область видимості, для кожної ітерації створюватиметься нове зв'язування.

// Використовуємо зв'язування блокової області видимості ES6
var array = [];
for (let i = 0; i < 3; i++) {
  // Цього разу кожне значення 'i' посилається на зв'язування однієї певної ітерації 
  // і зберігає значення, яке було поточним в той момент.
  // Так кожна стрілкова функція повертатиме різні значення 
  array.push(() => i);
}
var newArray = array.map(el => el());
console.log(newArray); // [0, 1, 2]

Інший спосіб розв'язати таку проблему — використати замикання.


// Без статичної області видимості не існувало б поняття замикань.
let array = [];
for (var i = 0; i < 3; i++) {
  // викликаємо функцію, щоб захопити (закрити) поточне значення змінної в циклі.
  array[i] = (function(x) {
    return function() {
      return x;
    };
  })(i);
}
const newArray = array.map(el => el());
console.log(newArray); // [0, 1, 2]

Відповідь #4: Ні

Пояснення

Паралелізм в JavaScript базується на моделі «циклу подій». Коли ми на початку статті порівняли браузер з домом для JavaScript, малося на увазі, що браузери передбачають середовище виконання для JavaScript-коду. Основні компоненти браузера містять Стек викликів (Call stack), Цикл подій (Event loop), Чергу завдань (Task Queue) та Web API. Глобальні функції на зразок setTimeout, setInterval та Promise не є частиною самого JavaScript, а стосуються Web API. Наочно середовище JavaScript можна представити так:

Думаєте, ви знаєте JavaScript?

Стек викликів в JS утворює LIFO (Last In First Out). Рушій бере за раз одну функцію зі стека та запускає код послідовно зверху вниз. Кожного разу, коли він зустрічає асинхронний код на зразок setTimeout, він передає його Web API (стрілка №1). Тож коли виникає певна подія, колбек відправляється в чергу завдань (стрілка №2).

Цикл подій постійно відстежує завдання та обробляє один колбек за раз в порядку черги. Кожного разу, коли стек викликів стає порожнім, цикл бере новий колбек та переміщує його в стек (стрілка №3) для обробки. Пам'ятайте, що цикл подій не буде заносити колбеки в стек, якщо він не пустий.

Для більш детального пояснення роботи Циклу подій в JavaScript — відео за посиланням. Ви також можете візуалізувати та зрозуміти стек викликів з цим інструментом. Переходьте за посиланням, запускайте функцію foo і спостерігайте за результатом.

Тепер ви озброєні теорією, тому спробуймо все ж відповісти на поставлене запитання:

Кроки

  1. Викликаючи foo(), ми поміщаємо функцію в стек викликів;
  2. Під час обробки коду в тілі функції, рушій JS натрапляє на setTimeout;
  3. Потім він передає колбек foo WebAPI (стрілка №1) та завершує виконання функції. Стек викликів тепер знову пустий.
  4. Значення таймера встановлено як 0, тому foo буде відправлено в чергу завдань (стрілка №2).
  5. Оскільки наш стек викликів був порожнім, цикл подій візьме колбек foo та передасть його в стек викликів для обробки.
  6. Процес повторюється знову, але стек не переповнюється.

Відповідь #5: Ні

Пояснення

Більшість розробників думають, що на схемі з циклом подій існує лише одна черга завдань. Насправді це не так. У нас є декілька черг завдань. Браузер сам вирішує, яку чергу обрати та обробити колбек, котрий вона містить.

На більш високому рівні в JavaScript існують макротаски та мікротаски. Колбеки setTimeout належать до макротасків, а колбеки Promise — до мікротасків. Основна розбіжність в особливостях їх виконання. Макротаски заносяться в стек по одному в єдиному циклі. Натомість черга мікротасків завжди вивільняється до того, як виконання повернеться до циклу подій, включно з будь-якими додатковими елементами в черзі. Тож якщо ви додаєте елементи в цю чергу так само швидко, як обробляєте їх, ви нескінченно будете обробляти мікротаски. Для глибшого розуміння подивіться статтю та відео.

Черга мікротасків завжди вивільняється перед тим, як виконання повертається до циклу подій.

Тепер, коли ви запустите цей фрагмент коду у вашій консолі:

function foo() {
  return Promise.resolve().then(foo);
};

Кожен виклик foo продовжуватиме додавати інший колбек foo до черги мікротасків, тому цикл подій не зможе обробляти всі ваші наступні дії (прокрутку, клік та інше), поки черга остаточно не звільниться. Як наслідок, блокується рендеринг.

Відповідь #6: Так, зробивши об'єкт ітерованим

Пояснення

Синтаксис spread та вираз for-of використовуються для ітерації даними, визначеними ітерованим об'єктом. Array та Map ітеровані за замовчуванням. Об'єкти самі по собі не ітеровані, однак це можна виправити за допомогою протоколів iterable та iterator.

В документації Mozilla зазначено, що об'єкт стає ітерованим, якщо він імплементує метод @@iterator, тобто об'єкт (або один з об'єктів в ланцюжку прототипів) повинен мати властивість з ключем @@iterator, яка доступна через константу Symbol.iterator.

Таке пояснення може здатися заплутаним, тому розглянемо як все працює на практиці:

var obj = { x: 1, y: 2, z: 3 };
obj[Symbol.iterator] = function() {
  // Ітератор – це об'єкт, який має метод next,
  // який повертає також об'єкт, який має як мінімум 
  // одну з двох властивостей: value & done.

  // повертає об'єкт ітератор
  return {
    next: function() {
      if (this._countDown === 3) {
        const lastValue = this._countDown;
        return { value: this._countDown, done: true };
      }
      this._countDown = this._countDown + 1;
      return { value: this._countDown, done: false };
    },
    _countDown: 0
  };
};
[...obj]; // виведе [1, 2, 3]

Ви також можете використовувати функцію-генератор, щоб налаштувати поведінку ітерації для об'єкта.

var obj = { x: 1, y: 2, z: 3 };
obj[Symbol.iterator] = function*() {
  yield 1;
  yield 2;
  yield 3;
};
[...obj]; // виведе [1, 2, 3]

Відповідь #7: a, b, c

Пояснення

Цикл for-in ітерується enumerable-властивостями самого об'єкта або об'єктів, успадкованих від його прототипа. Такі властивості можна обходити в циклах for-in.

var obj = { a: 1, b: 2 };
var descriptor = Object.getOwnPropertyDescriptor(obj, "a");
console.log(descriptor.enumerable); // true
console.log(descriptor);
// { value: 1, writable: true, enumerable: true, configurable: true }

З отриманими знаннями нам буде легко зрозуміти, чому ми отримуємо саме такий результат:

var obj = { a: 1, b: 2 }; // властивості a, b є enumerables

// встановлюємо об'єкт {c: 3} як прототип 'obj', а як ми знаємо,
// цикл for-in loop також ітерується властивостями, які успадковує obj 
// від свого прототипу, тому цикл також пройдеться об'єктом 'c'
Object.setPrototypeOf(obj, { c: 3 });

// Визначаємо ще одну властивість 'd' для нашого об'єкта 'obj', але
// встановлюємо 'enumerable' як false. Це означає, що 'd' буде проігноровано
Object.defineProperty(obj, "d", { value: 4, enumerable: false });

for (let prop in obj) {
  console.log(prop);
}
// Отримаємо результат
// a
// b
// c

Відповідь #8: 10

Пояснення

Коли ми ініціалізуємо x в глобальній області видимості, він стає властивістю об'єкта window (якщо припустимо, що це середовище браузера та не режим strict). Поглянемо на такий код:

var x = 10; // глобальна область видимості
var foo = {
  x: 90,
  getX: function() {
    return this.x;
  }
};
foo.getX(); // виводить 90
let xGetter = foo.getX;
xGetter(); // виводить 10

Ми можемо припустити, що:

window.x === 10; // true

this завжди вказує на об'єкт, методи якого викликаються. Тож у випадку з foo.getX(), this вказує на об'єкт foo, повертаючи нам значення 90. Тимчасом як у випадку з xGetter() this вказує на об'єкт window, який повертає нам значення 10.

Щоб отримати значення x об'єкта foo, ми можемо створити нову функцію і прив'язати this до об'єкта, використовуючи Function.prototype.bind.

let getFooX = foo.getX.bind(foo);
getFooX(); // виведе 90
Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Codeguida 5.6K
Приєднався: 8 місяців тому
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід / Реєстрація