Досліджуючи 8 основ JavaScript

Тут ви знайдете;

  • Підняття (Hoisting).
  • Замикання (Closure).
  • Обіцянки (Promise).
  • Карі (Function Currying).
  • Контекст виконання (Execution Context).
  • Call, Apply, та Bind.
  • Полифіли для звичайних методів масивів та рядків.
  • Прототипне наслідування (Prototypal Inheritance).

Підняття (Hoisting)

Підняття (Hoisting) — це процес, коли JavaScript всередині переміщає ваші оголошення змінних і функцій на початок їхнього простору видимості під час компіляції, до того, як буде виконано будь-який код. Однак ефект підняття різний, залежно від того, чи використовуєте ви var, let, const чи function declaration:

var

  • Піднімається з початковим значенням undefined.
  • Якщо ви звертаєтесь до змінної var до її оголошення, JavaScript просто повертає undefined, замість того, щоб викидати помилку.
  // var піднімається: за замовчуванням стає undefined, якщо використовується до оголошення  
  console.log(grape);   
  // Виведеться: undefined (немає помилки ReferenceError; воно піднято з "undefined")  

  var grape = "Зелений виноград";  
  console.log(grape);   
  // Виведеться: "Зелений виноград"

let та const

  • Також піднімаються, але перебувають у Темпоральній Мертвій Зоні (TDZ) від початку їхнього зовнішнього простору видимості до лінії оголошення.
  • Спроба звернутися до них до цієї лінії викликає ReferenceError.
  • const вимагає початкового значення і не може бути переназначена пізніше.
  // let також піднімається, але залишається в Темпоральній Мертвій Зоні (TDZ):  
  try {  
    console.log(berry);  
    // ReferenceError: Не можна звернутися до 'berry' до ініціалізації  
  } catch (error) {  
    console.error("Помилка доступу до berry:", error.message);  
  }  

  let berry = "Полуниця";  
  console.log(berry);   
  // Виведеться: "Полуниця"
  // const поводиться як let стосовно TDZ, але повинен бути ініціалізований одразу.  
  try {  
    console.log(mango);  
    // ReferenceError: Не можна звернутися до 'mango' до ініціалізації  
  } catch (error) {  
    console.error("Помилка доступу до mango:", error.message);  
  }  

  const mango = "Медовий манго";  
  console.log(mango);  
  // Виведеться: "Медовий манго"  

  // Спроба переназначити const викликає TypeError  
  // mango = "Зелений манго"; // TypeError: Призначення значення константній змінній.

Оголошення функцій (Function Declarations)

  • Повністю піднімаються разом з їхнім виконанням, що дозволяє викликати їх до того, як вони з'являються в коді.
  // Оголошення функцій повністю піднімаються:  
  blendFruits();   
  // Виведеться: "Змішуємо фрукти у смузі!"  

  function blendFruits() {  
    console.log("Змішуємо фрукти у смузі!");  
  }

Замикання (Closure)

Замикання (Closure) створюється, коли функція має доступ до змінних з зовнішнього простору її виконання. Цей доступ зберігається навіть після того, як зовнішня функція завершить виконання. Замикання працюють завдяки правилам лексичного скопування (lexical scoping) JavaScript: середовище, в якому функція визначена, зберігається всередині цієї функції, що дозволяє їй зберігати посилання на будь-які змінні з того середовища.

Чому замикання корисні?

  1. Інкапсуляція даних: Замикання дозволяють створювати приватні дані або методи, до яких не можна звертатися ззовні функції.
  2. Збереження стану: Вони допомагають зберігати стан між кількома викликами функцій.
  3. Функції-фабрики (Factory Functions): Ви можете створювати спеціалізовані функції на льоту, кожна з яких матиме своє власне постійне середовище.

Як працюють замикання всередині?

Коли функція оголошується, вона зберігає посилання на своє лексичне середовище. Навіть після того, як зовнішня функція повернеться, це посилання зберігається.
Отже, кожного разу, коли ви викликаєте внутрішню функцію, вона все одно може доступатись до змінних зовнішнього простору та змінювати їх.

Просте замикання (Simple Closure)

function createFruitVault() {  
  const secretFruit = "Драконовий плід";  

  return function revealSecretFruit() {  
    console.log("Секретний фрукт:", secretFruit);  
  };  
}  

const openVault = createFruitVault();  
openVault();   
// Виведеться: "Секретний фрукт: Драконовий плід"
  • Внутрішня функція revealSecretFruit закривається на змінній secretFruit.
  • Хоча createFruitVault завершила виконання, revealSecretFruit все ще має доступ до secretFruit завдяки замиканню.

Збереження стану за допомогою замикань (Maintaining State with Closures)

function fruitCounter() {  
  let count = 0;  

  return function addFruit() {  
    count++;  
    console.log("Загальна кількість фруктів:", count);  
  };  
}  

const addOneFruit = fruitCounter();  

addOneFruit();   
// Виведеться: "Загальна кількість фруктів: 1"  

addOneFruit();   
// Виведеться: "Загальна кількість фруктів: 2"
  • Кожен виклик addOneFruit збільшує лічильник count.
  • Змінна count знаходиться у зовнішньому просторі функції fruitCounter, і лише повернута функція може її змінювати.

Конфіденційність даних (Data Privacy)

function secretFruitStore() {  
  let inventory = ["Яблуко", "Банан"];  

  return {  
    getInventory: function() {  
      return [...inventory]; // повертає копію  
    },  
    addFruit: function(fruit) {  
      inventory.push(fruit);  
      console.log("Фрукт додано:", fruit);  
    }  
  };  
}  

const store = secretFruitStore();  
store.addFruit("Манго");   
// Виведеться: "Фрукт додано: Манго"  

console.log(store.getInventory());   
// Виведеться: ["Яблуко", "Банан", "Манго"]  

// Прямий доступ до інвентарю неможливий, він приватний в замиканні!
  • Масив inventory є приватним для зовнішньої функції.
  • Тільки методи getInventory та addFruit можуть взаємодіяти з ним.
  • Цей шаблон надає контрольований доступ до даних.

Обіцянка (Promise)

Обіцянка (Promise) — це об'єкт JavaScript, який виступає як заповнювач для майбутнього завершення (або невдачі) асинхронної операції. Вона може знаходитися в одному з трьох станів:

  1. В очікуванні (Pending): Початковий стан, не виконано і не відхилено.
  2. Виконано (Fulfilled): Операція успішно завершена, і обіцянка тепер має вирішене значення.
  3. Відхилено (Rejected): Операція зазнала невдачі, і обіцянка має причину невдачі.

Обіцянки спрощують обробку асинхронних операцій, надаючи методи .then() та .catch():

  • .then() викликається, якщо обіцянка виконана, і ви отримуєте доступ до вирішеного значення.
  • .catch() викликається, якщо обіцянка була відхилена, що дозволяє коректно обробляти помилки.

Також можна використовувати .finally(), щоб виконати код, незалежно від того, чи була обіцянка виконана чи відхилена.

Коли використовувати обіцянки (When to Use Promises)

  • Обробка мережевих запитів, таких як отримання даних з API.
  • Виконання довготривалих завдань або операцій, які повинні бути завершені перед тим, як перейти до наступних.
  • Заміна кодів, що використовують зворотні виклики, на більш узгоджений підхід з обробкою помилок.

Обіцянки є однією з основ сучасного асинхронного програмування в JavaScript, відкриваючи шлях для синтаксису async/await та більш читабельних асинхронних потоків.

Основна обіцянка (Basic Promise)

const plantMango = new Promise((resolve, reject) => {  
  setTimeout(() => {  
    const successfulGrowth = true; // змінити на false, щоб перевірити відхилення  
    if (successfulGrowth) {  
      resolve("Свіже манго виросло!");  
    } else {  
      reject("Не вдалося виростити мангове насіння.");  
    }  
  }, 1000);  
});  

plantMango  
  .then((mangoMessage) => {  
    console.log("Успіх:", mangoMessage);  
  })  
  .catch((error) => {  
    console.error("Невдача:", error);  
  });
  • Обіцянка починається в стані очікування (pending).
    Після затримки обіцянка або виконується з повідомленням про успіх, або відхиляється з помилкою.
  • .then() обробляє випадок виконання, а .catch() обробляє відхилення.

Ланцюжок обіцянок (Chaining Promises)

function washBananas() {  
  return new Promise((resolve) => {  
    console.log("Миття бананів...");  
    setTimeout(() => resolve("Банани вимиті"), 500);  
  });  
}  

function peelBananas(previousMessage) {  
  return new Promise((resolve) => {  
    console.log(previousMessage);  
    console.log("Чищення бананів...");  
    setTimeout(() => resolve("Банани очищені"), 500);  
  });  
}  

function sliceBananas(previousMessage) {  
  return new Promise((resolve) => {  
    console.log(previousMessage);  
    console.log("Різка бананів...");  
    setTimeout(() => resolve("Банани порізані"), 500);  
  });  
}  

// Ланцюжок обіцянок  
washBananas()  
  .then(result => peelBananas(result))  
  .then(result => sliceBananas(result))  
  .then(finalMessage => {  
    console.log(finalMessage);   
    // Виведеться: "Банани порізані"  
  })  
  .catch(error => {  
    console.error("Помилка в процесі обробки бананів:", error);  
  });
  • Кожна функція повертає нову обіцянку.
  • Повертаючи обіцянку в .then(), ви можете ланцюжити кілька асинхронних операцій у послідовності.
  • Якщо будь-яка обіцянка в ланцюжку буде відхилена, .catch() обробляє помилку.

Promise.all

function pickStrawberries() {  
  return new Promise((resolve) => {  
    console.log("Збір полуниць...");  
    setTimeout(() => resolve("Полуницю зібрано"), 700);  
  });  
}  

function pickBlueberries() {  
  return new Promise((resolve) => {  
    console.log("Збір чорниць...");  
    setTimeout(() => resolve("Чорницю зібрано"), 300);  
  });  
}  

Promise.all([pickStrawberries(), pickBlueberries()])  
  .then((messages) => {  
    console.log("Всі фрукти готові:", messages);  
    // Виведеться: "Всі фрукти готові: ['Полуницю зібрано', 'Чорницю зібрано']"  
  })  
  .catch((error) => {  
    console.error("Помилка під час збору фруктів:", error);  
  });
  • Promise.all() чекає, поки всі обіцянки будуть виконані, а потім повертає масив всіх вирішених значень.
  • Якщо будь-яка обіцянка буде відхилена, Promise.all() негайно відхилить усі обіцянки.

Функціональний каррінг (Function Currying)

Каррінг (Function Currying) перетворює функцію, яка приймає кілька параметрів, в послідовність функцій, кожна з яких приймає один або менше аргументів за раз. Замість того щоб викликати someFunction(a, b, c), ви викликаєте someFunction(a)(b)(c). Цей шаблон допомагає створювати більш спеціалізовані функції та спрощує композицію функцій.

Чому використовувати каррінг?

  1. Перевикористовуваність: Ви можете частково застосовувати функції з певними аргументами і повторно використовувати їх для конкретних завдань.
  2. Чіткість: Каррінгові функції часто виглядають як конвеєр, що робить потік логіки більш зрозумілим під час ланцюжного виклику операцій.
  3. Функціональне програмування: Каррінг часто використовується у функціональних парадигмах програмування для створення більш складних функцій із простіших.

Приклади коду

// Звичайна функція, яка приймає три параметри  
function makeFruitPunch(apple, orange, lemon) {  
  return `Фруктовий коктейль: ${apple} + ${orange} + ${lemon}`;  
}  

console.log(makeFruitPunch("Червоне яблуко", "Нейвел апельсин", "Мейер лимон"));  
// Виведеться: "Фруктовий коктейль: Червоне яблуко + Нейвел апельсин + Мейер лимон"  

// Каррінгова версія тієї ж функції  
function curryFruitPunch(apple) {  
  return function(orange) {  
    return function(lemon) {  
      return `Каррінговий фруктовий коктейль: ${apple} + ${orange} + ${lemon}`;  
    };  
  };  
}  

console.log(curryFruitPunch("Зелене яблуко")("Кровавий апельсин")("Еврика лимон"));  
// Виведеться: "Каррінговий фруктовий коктейль: Зелене яблуко + Кровавий апельсин + Еврика лимон"
  • Звичайна функція makeFruitPunch приймає всі параметри одразу.
  • Каррінгова функція curryFruitPunch приймає аргументи по черзі, повертаючи нову функцію кожного разу, поки не буде досягнуто останнього параметра.

Контекст виконання (Execution Context)

Контекст виконання (Execution Context) — це середовище, в якому виконується код JavaScript.
Існують два основних типи контекстів виконання:

  1. Глобальний контекст виконання (Global Execution Context)
  • Створюється, коли ваш JavaScript код починає виконуватись.
  • Обробляє глобальні змінні та глобально оголошені функції.
  • В браузері глобальний об'єкт — це window, який є найбільшим оточенням.
 // Глобальна змінна  
let globalFruit = "Полуниця";  

// Глобальна функція  
function showGlobalFruit() {  
  console.log("Глобальний фрукт:", globalFruit);  
}  

showGlobalFruit();  
// Виведеться: "Глобальний фрукт: Полуниця"
  1. Контекст виконання функції (Function Execution Context)
  • Створюється щоразу, коли функція викликається.
  • Кожен виклик функції має власний локальний контекст, власне зв'язування this та посилання на зовнішнє оточення.
function fruitParfait() {  
  let localFruit = "Чорниця";  
  console.log("В середині fruitParfait, localFruit:", localFruit);  

  function addIngredients() {  
    let addedFruit = "Банан";  
    console.log("Додаємо:", localFruit, "і", addedFruit);  
  }  

  addIngredients();  
}  

fruitParfait();  
// Виведеться:  
// "В середині fruitParfait, localFruit: Чорниця"  
// "Додаємо: Чорниця і Банан"

Фаза створення (Creation Phase) проти фази виконання (Execution Phase)

  1. Фаза створення (Creation Phase)
  • Двигун JavaScript сканує поточний контекст на наявність оголошених змінних та функцій.
  • Виділяється пам'ять для цих змінних та функцій (це місце, де відбувається підйом (hoisting)).
  1. Фаза виконання (Execution Phase)
  • Код виконується по рядках.
  • Змінні отримують свої початкові значення, а функції фактично викликаються.

Коли функція завершує виконання, її контекст зникає з стека викликів (call stack).
Двигун потім повертається до попереднього активного контексту.

let bowl = "Основна чаша для подачі";  

function prepareSalad() {  
  let bowl = "Локальна чаша для салату";  
  console.log("Готуємо салат в:", bowl);  
  mixFruit();  
}  

function mixFruit() {  
  let bowl = "Чаша для змішування";  
  console.log("Змішуємо фрукти в:", bowl);  
}  

prepareSalad();  
// Виведеться:  
// "Готуємо салат в: Локальна чаша для салату"  
// "Змішуємо фрукти в: Чаша для змішування"
  • Виклик prepareSalad() створює новий контекст.
  • Всередині prepareSalad() виклик mixFruit() додає ще один контекст до стека викликів.
  • Кожна функція має свою власну локальну змінну bowl, що демонструє різницю між локальними областями видимості.

Call, Apply та Bind

call(thisArg, ...args)

  • Викликає функцію, явно встановлюючи this на значення thisArg.
  • Додаткові аргументи передаються окремо.

apply(thisArg, [argsArray])

  • Схоже на call, але аргументи передаються як масив, а не окремо.

bind(thisArg, ...args)

  • Повертає нову функцію, яка постійно встановлює this на значення thisArg, разом з необов'язковими попередньо заданими аргументами.
  • Ви можете викликати цю нову функцію скільки завгодно разів.

Ці методи є важливими, коли потрібно керувати значенням this у функціях — особливо при запозиченні методів з одного об'єкта для використання в іншому або при роботі з обробниками подій (Event Handlers) в певних контекстах.

function showFavoriteFruit(prefix, suffix) {  
  console.log(`${prefix} улюблений фрукт ${this.fruit}${suffix}`);  
}  

const person = {  
  fruit: "Полуниця"  
};  

// Використання call  
showFavoriteFruit.call(person, "Мій", "!");  
// Виведеться: "Мій улюблений фрукт: Полуниця!"  


// Використання apply (аргументи передаються як масив)  
showFavoriteFruit.apply(person, ["Твій", "!!!"]);  
// Виведеться: "Твій улюблений фрукт: Полуниця!!!"  


// Використання bind (повертає нову функцію)  
const boundShowFruit = showFavoriteFruit.bind(person, "Арда's");  
boundShowFruit("!!!");  
// Виведеться: "Арда's улюблений фрукт: Полуниця!!!"
  • call безпосередньо викликає функцію з кастомізованим значенням this і переданими аргументами.
  • apply корисний, коли ваші аргументи вже знаходяться в масиві.
  • bind не виконує функцію негайно, а створює нову функцію з постійно встановленим значенням this, яку можна викликати пізніше.

Поліфіли для звичайних методів масивів та рядків

Поліфіл (Polyfill) — це шматок коду, який реалізує функціональність для середовищ (старі браузери), де ця функціональність не підтримується нативно. У JavaScript часто можна побачити поліфіли для нових методів, таких як Array.prototype.map, Array.prototype.filter, Array.prototype.reduce, та String.prototype.trim. Додаючи ці поліфіли, ви забезпечуєте роботу вашого коду в середовищах, де ці методи не підтримуються нативно.

Чому використовувати поліфіли?

  1. Сумісність з браузерами: Старі браузери можуть не підтримувати сучасні методи JavaScript.
  2. Узгодженість: Ви можете покладатися на те, що певні API існують без необхідності перевіряти їх наявність.
    3.
    Підтримка старих версій: Дозволяє запускати сучасний код в старіших середовищах без помилок.

Array.prototype.map

if (!Array.prototype.map) {  
  Array.prototype.map = function(callback, thisArg) {  
    if (this == null) {  
      throw new TypeError("Map не можна викликати для null або undefined!");  
    }  
    if (typeof callback !== "function") {  
      throw new TypeError("Callback має бути функцією!");  
    }  

    const fruitBasket = Object(this);  
    const length = fruitBasket.length >>> 0;  
    const result = new Array(length);  

    for (let i = 0; i < length; i++) {  
      if (i in fruitBasket) {  
        result[i] = callback.call(thisArg, fruitBasket[i], i, fruitBasket);  
      }  
    }  

    return result;  
  };  
}  

// Приклад використання:  
const fruits = ["Apple", "Banana", "Cherry"];  
const fruitLengths = fruits.map((item) => item.length);  
console.log(fruitLengths);  
// Можливий результат: [5, 6, 6]

Array.prototype.filter

if (!Array.prototype.filter) {  
  Array.prototype.filter = function(callback, thisArg) {  
    if (this == null) {  
      throw new TypeError("Filter не можна викликати для null або undefined!");  
    }  
    if (typeof callback !== "function") {  
      throw new TypeError("Callback має бути функцією!");  
    }  

    const fruitBasket = Object(this);  
    const length = fruitBasket.length >>> 0;  
    const filteredFruits = [];  

    for (let i = 0; i < length; i++) {  
      if (i in fruitBasket) {  
        const fruit = fruitBasket[i];  
        if (callback.call(thisArg, fruit, i, fruitBasket)) {  
          filteredFruits.push(fruit);  
        }  
      }  
    }  

    return filteredFruits;  
  };  
}  

// Приклад використання:  
const allFruits = ["Apple", "Banana", "Avocado", "Blueberry"];  
const fruitsThatStartWithA = allFruits.filter((item) => item.startsWith("A"));  
console.log(fruitsThatStartWithA);  
// Можливий результат: ["Apple", "Avocado"]

Array.prototype.reduce

if (!Array.prototype.reduce) {  
  Array.prototype.reduce = function(callback /*, initialValue*/) {  
    if (this == null) {  
      throw new TypeError("Reduce не можна викликати для null або undefined!");  
    }  
    if (typeof callback !== "function") {  
      throw new TypeError("Callback має бути функцією!");  
    }  

    const fruitBasket = Object(this);  
    const length = fruitBasket.length >>> 0;  
    let index = 0;  
    let accumulator;  

    if (arguments.length >= 2) {  
      accumulator = arguments[1];  
    } else {  
      // Знайти перший визначений індекс  
      while (index < length && !(index in fruitBasket)) {  
        index++;  
      }  
      if (index >= length) {  
        throw new TypeError("Reduce порожнього масиву без початкового значення");  
      }  
      accumulator = fruitBasket[index++];  
    }  

    for (; index < length; index++) {  
      if (index in fruitBasket) {  
        accumulator = callback(accumulator, fruitBasket[index], index, fruitBasket);  
      }  
    }  

    return accumulator;  
  };  
}  

// Приклад використання:  
const tropicalFruits = ["Mango", "Pineapple", "Papaya"];  
const totalLetters = tropicalFruits.reduce((sum, fruit) => sum + fruit.length, 0);  
console.log(totalLetters);  
// Можливий результат: 18

String.prototype.trim

if (!String.prototype.trim) {  
  String.prototype.trim = function() {  
    // Видаляє пробіли на початку та в кінці рядка  
    return this.replace(/^\s+|\s+$/g, "");  
  };  
}  

// Приклад використання:  
const messyFruit = " Watermelon ";  
const cleanFruit = messyFruit.trim();  
console.log(cleanFruit);  
// Можливий результат: "Watermelon"

Прототипне наслідування

Прототипне наслідування — це механізм у JavaScript, де об'єкти можуть успадковувати властивості та методи від об'єкта прототипу. Замість класів JavaScript будує відносини через ланцюг прототипів: якщо об'єкт не може знайти властивість чи метод у себе, він звертається до свого прототипу, потім до прототипу його прототипу і так далі, поки не знайде потрібне або не досягне кінця ланцюга.

Чому це корисно?

  1. Перевикористовуваність: Спільні властивості та методи можна розмістити в одному об'єкті прототипі, що зменшує використання пам'яті та дублювання.
    2.
    Динамічна розширюваність: Ви можете додавати властивості або методи до прототипу після створення об'єктів, і вони набувають цих нових можливостей.
  2. Гнучкість: Це відрізняється від наслідування, заснованого на класах, пропонуючи більш динамічний спосіб визначення відносин між об'єктами.
// Конструктор: Fruit  
function Fruit(name, color) {  
  this.name = name;  
  this.color = color;  
}  

// Додаємо метод до прототипу Fruit  
Fruit.prototype.describe = function() {  
  return `Це ${this.color} ${this.name}.`;  
};  

// Створюємо екземпляри (потомки)  
const apple = new Fruit("Apple", "Red");  
const mango = new Fruit("Mango", "Golden");  

// Доступ до методу describe через прототип  
console.log(apple.describe());  
// Вивід: "Це Red Apple."  

console.log(mango.describe());  
// Вивід: "Це Golden Mango."
  • Fruit — це конструктор, що використовується для створення нових об'єктів.
  • Метод describe додається до Fruit.prototype.
  • Екземпляри apple та mango автоматично успадковують метод describe через ланцюг прототипів.

Коротко,

Hoisting (Підняття): JavaScript переміщає оголошення змінних та функцій на початок їх області видимості.
Closure (Замикання): Внутрішня функція зберігає доступ до змінних своєї зовнішньої функції навіть після того, як зовнішня функція завершить виконання.
Promise (Приміс): Представляє ймовірний успіх або помилку асинхронної операції.
Function Currying (Каррінг функцій): Перетворює функцію, що приймає кілька параметрів, в вкладені функції, кожна з яких обробляє один параметр.
Execution Context (Контекст виконання): Середовище, в якому JavaScript код оцінюється та виконується.
Call, Apply, and Bind: Методи для явного встановлення контексту this функції та передачі аргументів.
Polyfills (Поліфіли): Заміщення реалізацій функцій JavaScript для старіших середовищ, що не підтримують їх нативно.
Prototypal Inheritance (Прототипне наслідування): Об'єкти можуть успадковувати властивості та методи від іншого об'єкта через ланцюг прототипів.

Це все щодо основ JavaScript.

Коли я зустрічатиму нові й цікаві теми, пов'язані з веб-розробкою, я продовжуватиму вигадувати нові історії.

Перекладено з: Exploring 8 JavaScript Fundamentals

Leave a Reply

Your email address will not be published. Required fields are marked *