Skip to content

Latest commit

 

History

History
328 lines (222 loc) · 19.8 KB

File metadata and controls

328 lines (222 loc) · 19.8 KB

Серія "Ти поки що не знаєш JS". Книга 1: "Перші кроки". Друге видання

Додаток A: Продовжуємо дослідження

У цьому додатку ми подивимося уважніше на деякі теми з основних глав. Розглядайте цей матеріал як необов’язковий попередній перегляд деяких деталей, висвітлених у решті книг серії.

Значення та посилання: в чому різниця

У главі 2 ми представили два основних типи значень: примітивні значення та значення об’єктного типу. Проте ми ще не торкалися важливої різниці між ними: як ці значення призначаються та передаються.

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

Якщо ви призначаєте або передаєте саме значення, воно копіюється. Наприклад:

var myName = "Кайл";

var yourName = myName;

Тут змінна yourName містить окрему копію рядка Кайл, який зберігається в myName. Значення Кайл є примітивним, а примітивні значення завжди призначаються або передаються як копії.

Ось як можна довести, що залучені два окремих значення:

var myName = "Кайл";

var yourName = myName;

myName = "Френк";

console.log(myName);
// Френк

console.log(yourName);
// Кайл

Ви помітили, що призначення змінній myName значення "Френк" не вплинуло на yourName? Це тому, що кожна змінна має власну копію значення.

Натомість ідея посилання у тому, що дві або більше змінних вказують на одне і те саме значення таким чином, що зміна цього спільного значення буде помітною при доступі за будь-яким з цих посилань. У JS лише значення об’єктного типу (масиви, об’єкти, функції тощо) поводяться як посилання.

Наприклад:

var myAddress = {
    street: "123 JS Blvd",
    city: "Austin",
    state: "TX"
};

var yourAddress = myAddress;

// Я переїжджаю!
myAddress.street = "456 TS Ave";

console.log(yourAddress.street);
// 456 TS Ave

Оскільки значення, присвоєне myAddress, є об'єктом, воно утримується або призначається за посиланням, і, таким чином, присвоєння змінної yourAddress є копією посилання, а не самого значення об'єкта. Ось чому оновлення значення myAddress.street помітне, коли ми звертаємося до yourAddress.street. myAddress та yourAddress містять копії посилання на один об’єкт, тому оновлення одного означає оновлення обох.

Знову ж таки, JS обирає між копіюванням значення та копіюванням посилання за типом значення. Примітиви утримуються за значенням, об’єкти - за посиланням. В JS неможливо змінити це в жодному напрямку.

Так багато видів функцій

Згадаймо цей фрагмент з розділу "Функції" у главі 2:

var awesomeFunction = function(coolThings) {
    // ..
    return amazingStuff;
};

Такий функційний вираз називається анонімним функційним виразом, оскільки між ключовим словом function та списком параметрів (..) відсутній ідентифікатор імені. Цей момент бентежить багатьох JS-розробників, оскільки, починаючи з ES6, JS виконує "виведення імені" для анонімної функції:

awesomeFunction.name;
// "awesomeFunction"

Властивість name у випадку оголошення покаже задане ім'я функції, а у випадку анонімного функційного виразу – виведене ім'я. Це значення зазвичай використовується інструментами розробника під час перевірки значення функції або для виведення стек-трейсу помилок.

Тож навіть анонімний функційний вираз може мати ім'я. Однак виведення імені відбувається лише в обмежених випадках, наприклад, коли функцій вираз призначається змінній або властивості (з =). Але, скажімо, якщо ви передаєте функційний вираз як аргумент під час виклику функції, виведення імені не відбувається; властивість name буде порожнім рядком, а консоль розробника зазвичай повідомляє про анонімну функцію так: "(anonymous function)".

Навіть якщо ім’я виводиться, це все одно анонімна функція. Чому? Оскільки виведене ім'я є значенням рядка метаданих, а не ідентифікатором за яким можна звернутися до функції. Анонімна функція не має ідентифікатора, який би використовувався для посилання на себе зсередини для рекурсії або скасування обробки подій (event unbinding).

Порівняйте форму анонімний функційний вираз з таким кодом:

// let awesomeFunction = ..
// const awesomeFunction = ..
var awesomeFunction = function someName(coolThings) {
    // ..
    return amazingStuff;
};

awesomeFunction.name;
// "someName"

Цей функційний вираз є іменованим функційним виразом, оскільки ідентифікатор someName під час компіляції безпосередньо пов'язується з функційним виразом. Щодо ідентифікатора awesomeFunction, то прив'язка до нього відбувається вже під час виконання. Ці два ідентифікатори не обов'язково мають збігатися; часом потрібно, щоб вони були різними, а в інших випадках краще, щоб вони були однаковими.

Зауважте, що явне ім'я функції, ідентифікатор someName, має пріоритет при призначенні властивості name.

Чи повинні функційні вирази бути іменованими або анонімними? Думки щодо цього дуже різні. Більшість розробників, як правило, не турбує використання анонімних функцій. Вони коротші і, безперечно, більш поширені в широкій сфері JS-коду.

Моя думка така. Якщо функція існує у вашій програмі, вона має мету. Інакше просто видаліть її! А якщо у функції є мета, вона має природну назву, яка цю мету описує.

Якщо функція має ім'я, ви як автор коду, повинні включити це ім'я до коду, щоб читачеві не довелося здогадуватися про це ім'я під час читання та виконання вихідного коду цієї функції подумки. Навіть таке тривіальне тіло функції, як x * 2, доведеться прочитати, щоб вивести ім'я типу "double" або "multBy2"; цієї незначної надлишкової розумової роботи можна позбутися, якщо витратити кілька секунд та назвати функцію "double" або "multBy2" один раз та зекономити час читача, який повторюватиме розумову роботу кожного разу, коли читатиме цю функцію.

Станом на початок 2020 року в JS існує багато інших форм визначення функцій; про деякі з них варто пошкодувати. Можливо, в майбутньому способів оголосити функцію буде ще більше.

Ось ще кілька форм оголошення:

// оголошення функції-генератора
function *two() { .. }

// оголошення асинхронної функції
async function three() { .. }

// оголошення асинхронної функції-генератора
async function *four() { .. }

// іменований експорт функції (модулі ES6)
export function five() { .. }

І ще декілька (чимало, насправді) форм виразів функції:

// IIFE
(function(){ .. })();
(function namedIIFE(){ .. })();

// asynchronous IIFE
(async function(){ .. })();
(async function namedAIIFE(){ .. })();

// arrow function expressions
var f;
f = () => 42;
f = x => x * 2;
f = (x) => x * 2;
f = (x,y) => x * y;
f = x => ({ x: x * 2 });
f = x => { return x * 2; };
f = async x => {
    var y = await doSomethingAsync(x);
    return y * 2;
};
someOperation( x => x * 2 );
// ..

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

Оскільки я вважаю, що анонімні функції не слід часто використовувати у ваших програмах, я не прихильник використання форми функції =>. Цей тип функції насправді має певне призначення, а саме вони призначені для лексичного розв'язання ключового слова this, але це не означає, що ми повинні використовувати його для кожної написаної нами функції. Використовуйте найбільш слушний для певної задачі інструмент.

Функції також можуть бути вказані у визначеннях класів та визначеннях літеральних об’єктів. У цих формах їх зазвичай називають "методами", хоча в JS цей термін не має помітної різниці від звичайних функцій:

class SomethingKindaGreat {
    // методи класу
    coolMethod() { .. }   // кому не ставимо!
    boringMethod() { .. }
}

var EntirelyDifferent = {
    // методи об'єкту
    coolMethod() { .. },   // ставимо кому!
    boringMethod() { .. },

    // (анонімний) функційний вираз у властивості об'єкта
    oldSchool: function() { .. }
};

Фух! Багато різних способів визначення функцій набралося.

Зрізати кут тут не вийде; потрібно познайомитися з усіма формами функцій, щоб ви могли їх розпізнати в наявному коді та використовувати належним чином у коді, який ви пишете. Уважно вивчайте їх і тренуйтеся!

Умовне порівняння із приведенням типів

Англійською назву цього розділу – Coercive Conditional Comparison – з першого разу і не виговориш. То про що ми говоримо? Ми говоримо про умовні вирази, які мають приводити типи своїх операндів для здійснення порівняння.

if та тернарний оператор, а також перевірка умови у циклах while і for виконують неявне порівняння значень. Але яке саме порівняння? Суворе чи з приведенням типів? Насправді обидва.

Розглянемо:

var x = 1;

if (x) {
    // буде виконано!
}

while (x) {
    // буде виконано один раз!
    x = false;
}

Про такі умовні вирази ви можете думати наступним чином:

var x = 1;

if (x == true) {
    // буде виконано!
}

while (x == true) {
    // буде виконано один раз!
    x = false;
}

У цьому конкретному випадку ми можемо подумки замінити x на 1, але цей підхід не універсальний. Наприклад:

var x = "hello";

if (x) {
    // буде виконано!
}

if (x == true) {
    // не буде виконано :(
}

Отакої. То що ж робить той if? Ось точніша ментальна модель:

var x = "hello";

if (Boolean(x) == true) {
    // буде виконано
}

// це те ж саме:

if (Boolean(x) === true) {
    // буде виконано
}

Оскільки функція Boolean(..) завжди повертає значення типу boolean, немає сенсу розрізняти == та ===; у цьому фрагменті вони будуть робити те саме. Але найголовніше – побачити, що перед порівнянням відбувається приведення типів, а саме з будь-якого типу x до булевого.

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

Прототипні "класи"

У главі 3 ми поговорили про прототипи та показали, як можна пов’язати об’єкти через ланцюжок прототипів.

Існує ще один (досить потворний, правду кажучи) спосіб поєднання прототипів, який також став попередником елегантної системи класів у ES6 (див. Главу 2, "Класи") і називається прототипними класами.

| ПОРАДА: | | : --- | | Хоча цей стиль коду в JS є досить рідкісним явищем, з незрозумілих причин про це дуже часто запитують на співбесіді! |

Давайте спочатку згадаємо стиль написання коду зі створенням об'єктів за допомогою Object.create(..):

var Classroom = {
    welcome() {
        console.log("Welcome, students!");
    }
};

var mathClass = Object.create(Classroom);

mathClass.welcome();
// Welcome, students!

Тут об'єкт mathClass через свій прототип пов'язаний з об'єктом Classroom. Через цей зв'язок виклик функції mathClass.welcome() делегується методу, визначеному в Classroom.

Відштовхуючись від шаблона прототипного класу, ми могли б назвати цю поведінку делегування "успадкуванням", а як альтернативу визначили б ту саму поведінку наступним чином:

function Classroom() {
    // ..
}

Classroom.prototype.welcome = function hello() {
    console.log("Welcome, students!");
};

var mathClass = new Classroom();

mathClass.welcome();
// Welcome, students!

Усі функції за замовчуванням посилаються через властивість з назвою prototype на порожній об'єкт. Всупереч назві, це не прототип функції (тобто, не об'єкт, на який посилається прототип функції), а об'єкт прототипу, до якого буде створене посилання, від нових об'єктів, створених при виклику цієї функції з new .

Ми додаємо до цього порожнього об'єкта Classroom.prototype властивість welcome, що вказує на функцію hello().

Потім new Classroom() створює новий об'єкт (присвоєний mathClass), а прототип пов'язує його з наявним об'єктом Classroom.prototype.

Хоча у mathClass немає властивості чи функції welcome(), він успішно делегує виклик функції Classroom.prototype.welcome().

Шаблон "прототипного класу" зараз настійно не рекомендується. Натомість слід обрати механізм class з ES6:

class Classroom {
    constructor() {
        // ..
    }

    welcome() {
        console.log("Welcome, students!");
    }
}

var mathClass = new Classroom();

mathClass.welcome();
// Welcome, students!

За лаштунками створюється той самий прототипний зв’язок, але цей синтаксис набагато чіткіше відповідає класово-орієнтованому шаблону проєктування, ніж «прототипні класи».