Skip to content

Latest commit

 

History

History
762 lines (504 loc) · 59.3 KB

03_functions.md

File metadata and controls

762 lines (504 loc) · 59.3 KB

Функції

{{quote {author: «Donald Knuth», “глава”: “істина”}}

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

quote}}

{{index «Knuth, Donald»}}

{{figure {url: «img/chapter_picture_3.jpg», alt: «Ілюстрація листя папороті з фрактальною формою, бджоли на задньому плані», chapter: framed}}}

{{index function, [code, «структура»]}}

Функції є одним з центральних інструментів у програмуванні на JavaScript. Концепція обгортання частини програми у значення має багато застосувань. Вона дає нам спосіб структурувати більші програми, зменшити повторення, пов'язати імена з підпрограмами та ізолювати ці підпрограми одна від одної.

Найбільш очевидним застосуванням функцій є визначення нового ((словникового запасу)). Створення нових слів у прозі зазвичай є поганим стилем, але у програмуванні воно є необхідним.

{Абстракція індексів, словниковий запас}}

Типовий словниковий запас дорослого носія англійської мови налічує близько 20 000 слів. Небагато мов програмування мають 20 000 вбудованих команд. А словниковий запас, який є в наявності, як правило, більш точно визначений, а отже, менш гнучкий, ніж у людській мові. Тому нам доводиться вводити нові слова, щоб уникнути надмірної багатослівності.

Визначення функції

{{індекс «квадратний приклад», [функція, визначення], [зв'язування, визначення]}}

Визначення функції - це звичайне зв'язування, де значенням зв'язування є функція. Наприклад, у цьому коді визначено квадрат для посилання на функцію, яка виводить квадрат заданого числа:

const square = function(x) {
  return x * x;
};

console.log(square(12));
// → 144

{{indexsee «фігурні дужки», дужки}} {{індекс [дужки, «тіло функції»], блок, [синтаксис, функція], «ключове слово функції», [функція, тіло], [функція, «як значення»], [дужки, аргументи]}}

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

{{index «roundTo example»}}

Функція може мати декілька параметрів або взагалі не мати параметрів. У наступному прикладі makeNoise не містить жодного параметра, тоді як roundTo (яка округлює n до найближчого кратного step) містить два параметри:

const makeNoise = function() {
  console.log(«Pling!»);
};

makeNoise();
// → Pling!

const roundTo = function(n, step) {
  let remainder = n % step;
  return n - remainder + (remainder < step / 2 ? 0 : step);
};

console.log(roundTo(23, 10));
// → 20

{{індекс «значення, що повертається», «ключове слово повернення», undefined}}

Деякі функції, такі як roundTo і square, повертають значення, а деякі ні, наприклад makeNoise, єдиним результатом якої є ((побічний ефект)). Оператор return визначає значення, яке повертає функція. Коли елемент керування зустрічає такий оператор, він негайно виходить з поточної функції і передає значення, що повертається, коду, який викликав функцію. Ключове слово return без виразу після нього призведе до того, що функція поверне значення undefined. Функції, які взагалі не мають оператора return, такі як makeNoise, також повертають значення undefined.

{{індекс параметра, [функція, додаток], [прив'язка, «від параметра»]}}

Параметри функції поводяться як звичайні зв'язування, але їх початкові значення задаються викликачем функції, а не кодом у самій функції.

Зв'язування та області видимості

{{indexsee «область видимості верхнього рівня», «глобальна область видимості»}} {{індекс «ключове слово var», «глобальна область видимості», [прив'язка, глобальна], [прив'язка, «область видимості»]}}

Кожне прив'язування має ((область видимості )), яка є частиною програми, в якій це прив'язування є видимим. Для прив'язок, визначених поза будь-якою функцією, блоком або модулем (див. Глава ?), областю видимості є вся програма - ви можете посилатися на такі прив'язки де завгодно. Такі прив'язки називаються глобальними.

{{index «local scope», [binding, local]}}

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

{{index «ключове слово let», «ключове слово const», «ключове слово var»}}

Зв'язки, оголошені за допомогою let і const, насправді є локальними для ((блоку)), в якому вони оголошені, тому якщо ви створите одну з них всередині циклу, код до і після циклу не зможе її «побачити». У JavaScript до 2015 року нові області видимості створювали лише функції, тому прив'язки старого стилю, створені за допомогою ключового слова var, видимі у всій функції, в якій вони з'являються - або у всій глобальній області видимості, якщо вони не знаходяться у функції.

let x = 10; // global
if (true) {
  let y = 20; // локально для блоку
  var z = 30; // також глобально
}

{{index [зв'язування, видимість]}}

Кожне ((область видимості)) може «виглядати» в область видимості навколо нього, тому x видно всередині блоку у прикладі. Винятком є випадки, коли декілька прив'язок мають однакове ім'я - у такому випадку код може бачити лише внутрішню прив'язку. Наприклад, коли код всередині функції halve посилається на n, він бачить своє власне n, а не глобальне n.

const halve = function(n) {
  return n / 2;
};

нехай n = 10;
console.log(halve(100));
// → 50
console.log(n);
// → 10

{{id scoping}}

Вкладена область видимості

{{index [nesting, «of functions»], [nesting, «of scope»], scope, «internal function», «lexical scoping»}}

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

{{index «приклад ландшафту»}}

Наприклад, ця функція, яка виводить інгредієнти, необхідні для приготування хумусу, має всередині іншу функцію:

const hummus = function(factor) {
  const ingredient = function(кількість, одиниця виміру, назва) {}.
    нехай ingredientAmount = кількість * фактор;
    if (ingredientAmount > 1) {
      unit += «s»;
    }
    console.log(`${ingredientAmount} ${unit} ${name}`);
  };
  ingredient(1, «банка», «нут»);
  ingredient(0.25, «cup», «tahini»);
  ingredient(0.25, «cup», «лимонний сік»);
  ingredient(1, «зубчик», «часник»);
  ingredient(2, «столова ложка», «оливкова олія»);
  ingredient(0.5, «чайна ложка», «кмин»);
};

{{index [function, scope], scope}}

Код всередині функції ingredient бачить прив'язку factor із зовнішньої функції, але її локальні прив'язки, такі як unit або ingredientAmount, не видно у зовнішній функції.

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

Функції як значення

{{index [функція, «як значення»], [зв'язування, визначення]}}

Прив'язка функції зазвичай просто діє як ім'я для певного фрагмента програми. Таке зв'язування визначається один раз і ніколи не змінюється. Це дозволяє легко переплутати функцію та її ім'я.

{{index [зв'язування, присвоєння]}}

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

let launchMissiles = function() {
  missileSystem.launch(«now»);
};
if (safeMode) {
  launchMissiles = function() {/* нічого не робити */};
}

{{index [function, «higher-order»]}}

У Главі ? ми обговоримо цікаві речі, які можна робити, передаючи значення функції іншим функціям.

Нотація оголошення

{{індекс [синтаксис, функція], «ключове слово функції», «квадратний приклад», [функція, визначення], [функція, оголошення]}}

Існує дещо коротший спосіб створення зв'язування функції. Коли ключове слово function використовується на початку оператора, він працює інакше:

function square(x) {
  return x * x;
}

{{index future, «порядок виконання»}}

Це оголошення функції . Вона визначає зв'язуючий квадрат і вказує його на задану функцію. Він трохи простіший у написанні і не вимагає крапки з комою після функції.

У цій формі визначення функції є один нюанс.

console.log(«Майбутнє говорить:», future());

function future() {
  return «У вас ніколи не буде літаючих автомобілів»;
}

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

Функції зі стрілками

{{index-функція, «функція-стрілка»}}

Існує третій спосіб позначення функцій, який дуже відрізняється від інших. Замість ключового слова function використовується стрілка (=>), що складається зі знака рівності та символу більше ніж (не плутати з оператором більше ніж або дорівнює, який записується >=):

const roundTo = (n, step) => {
  let remainder = n % step;
  return n - remainder + (remainder < step / 2 ? 0 : step);
};

{{index [function, body]}}

Стрілка стоїть після списку параметрів, а за нею йде тіло функції. Вона виражає щось на кшталт «ці вхідні дані (параметри) дають цей результат (тіло)».

{{індекс [дужки, «тіло функції»], «квадратний приклад», [дужки, аргументи]}}

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

const square1 = (x) => { return x * x; };
const square2 = x => x * x;

{{індекс [дужки, аргументи]}}

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

const horn = () => {
  console.log(«Toot»);
};

{{багатослівність індексів}}

Немає жодної глибокої причини для того, щоб мати у мові як функції-стрілки, так і вирази функції. За винятком невеликої деталі, яку ми обговоримо у Розділ ?, вони роблять те саме. Функції зі стрілками було додано у 2015 році, головним чином для того, щоб зробити можливим написання невеликих функціональних виразів у менш багатослівний спосіб. Ми часто використовуватимемо їх у Розділ ?.

{{id stack}}

Стек викликів

{{indexsee stack, «стек викликів»}} {{index «стек викликів», [функція, програма]}}

Спосіб передачі керування через функції є дещо складним. Давайте розглянемо його докладніше. Ось проста програма, яка виконує декілька викликів функцій:

function greet(who) {
  console.log("Hello » + who);
}
greet(«Гаррі»);
console.log(«Бувай»);

{{index [«control flow», functions], «execution order», «console.log»}}

Виконання цієї програми відбувається приблизно так: виклик greet призводить до переходу керування на початок цієї функції (рядок 2). Функція викликає console.log, який отримує керування, виконує свою роботу, а потім повертає керування на рядок 2. Там воно досягає кінця функції greet, тому повертається до місця, звідки його було викликано - до рядка 4. Наступний за ним рядок знову викликає console.log. Після повернення цього рядка програма завершується.

Схематично потік управління можна зобразити так:

не у функції
  у greet
    у console.log
  in greet
не у функції
  у console.log
not in function

{{індекс «ключове слово повернення», [пам'ять, стек викликів]}}

Оскільки функція повинна повертатися до місця, з якого її було викликано, комп'ютер повинен запам'ятати контекст, з якого було здійснено виклик. В одному випадку, console.log має повернутися до функції greet після завершення роботи. В іншому випадку він повертається у кінець програми.

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

{{index «infinite loop», «stack overflow», recursion}}

Зберігання цього стеку вимагає місця у пам'яті комп'ютера. Коли стек стає занадто великим, комп'ютер завершить роботу з повідомленням на кшталт «не вистачає місця у стеку» або «занадто багато рекурсії». Наступний код ілюструє це, ставлячи комп'ютеру дуже складне питання, яке викликає нескінченне циклічне перемикання між двома функціями. Точніше, воно було б нескінченним, якби комп'ютер мав нескінченний стек. А так, нам не вистачить місця, або ми «рознесемо стек».

function chicken() {
  return egg();
}
function egg() {
  return chicken();
}
console.log(chicken() + « came first.»);
// → ??

Необов'язкові аргументи

{{аргумент індексу, [функція, додаток]}}

Наступний код є допустимим і виконується без проблем:

function square(x) { return x * x; }
console.log(square(4, true, «hedgehog»));
// → 16

Ми визначили квадрат лише з одним ((параметром)). Але коли ми викликаємо його з трьома, мова не скаржиться. Вона ігнорує зайві аргументи і обчислює квадрат першого з них.

{{index undefined}}

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

Недоліком цього є те, що ви можете випадково передати функціям неправильну кількість аргументів, і це навіть ймовірно. І ніхто вам про це не скаже. Перевагою є те, що ви можете використовувати цю поведінку, щоб дозволити виклик функції з різною кількістю аргументів. Наприклад, ця функція minus намагається імітувати оператор -, діючи з одним або двома аргументами:

function minus(a, b) {
  if (b === undefined) return -a;
  else return a - b;
}

console.log(minus(10));
// → -10
console.log(minus(10, 5));
// → 5

{{id roundTo}} {{індекс «необов'язковий аргумент», «значення за замовчуванням», параметр, [«= оператор», «для значення за замовчуванням»] «roundTo приклад»}}

Якщо після параметра написати оператор =, а потім вираз, значення цього виразу замінить аргумент, якщо його не вказано. Наприклад, у цій версії roundTo другий аргумент є необов'язковим. Якщо ви не вкажете його або передасте значення undefined, він буде за замовчуванням дорівнювати одиниці:

function roundTo(n, step = 1) {
  let remainder = n % step;
  return n - remainder + (remainder < step / 2 ? 0 : step);
};

console.log(roundTo(4.5));
// → 5
console.log(roundTo(4.5, 2));
// → 4

{{index «console.log»}}

У наступному розділі буде описано спосіб, за допомогою якого тіло функції може отримати весь список аргументів, які їй було передано. Це корисно, оскільки дозволяє функції приймати будь-яку кількість аргументів. Наприклад, console.log робить це, виводячи усі значення, які їй було передано:

console.log(«C», «O», 2);
// → C O 2

Закриття

{{індекс «стек викликів», «локальне прив'язування», [функція, «як значення»], область видимості}}

Можливість поводитися з функціями як зі значеннями у поєднанні з тим, що локальні прив'язки створюються заново при кожному виклику функції, викликає цікаве питання: Що відбувається з локальними зв'язками, коли виклик функції, який їх створив, більше не є активним?

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

function wrapValue(n) {
  let local = n;
  return () => local;
}

let wrap1 = wrapValue(1)
let wrap2 = wrapValue(2);
console.log(wrap1());
// → 1
console.log(wrap2());
// → 2

Це дозволено і працює так, як ви сподіваєтесь - до обох екземплярів прив'язки все ще можна отримати доступ. Ця ситуація є гарною демонстрацією того, що локальні прив'язки створюються заново для кожного виклику, і різні виклики не впливають на локальні прив'язки один одного.

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

{{index «multiplier function»}}

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

function multiplier(factor) {
  return число => число * множник;
}

нехай twice = multiplier(2);
console.log(twice(5));
// → 10

{{index [binding, «from parameter»]}}

Явне local-зв'язування з прикладу wrapValue насправді не потрібне, оскільки параметр сам по собі є локальним зв'язуванням.

{{index [function, «model of»]}}

Щоб думати про такі програми, потрібно трохи потренуватися. Хороша ментальна модель полягає у тому, щоб думати про значення функції як такі, що містять як код у тілі функції, так і середовище, у якому вони створені. Під час виклику тіло функції бачить середовище, у якому її було створено, а не середовище, у якому її викликано.

У попередньому прикладі викликається multiplier і створює середовище, в якому його параметр factor прив'язується до 2. Значення функції, яке вона повертає, і яке зберігається у twice, запам'ятовує це середовище, щоб при наступному виклику помножити свій аргумент на 2.

Рекурсія

{{index «power example», «stack overflow», recursion, [function, application]}}

Для функції цілком нормально викликати саму себе, якщо вона не робить це так часто, що переповнює стек. Функція, яка викликає сама себе, називається рекурсивною. Рекурсія дозволяє деяким функціям бути написаними в іншому стилі. Візьмемо, наприклад, цю функцію power, яка робить те саме, що й оператор ** (піднесення до степеня):

function power(base, exponent) {
  if (exponent == 0) {
    повернути 1;
  } else {
    return base * power(base, exponent - 1);
  }
}

console.log(power(2, 3));
// → 8

{{індексний цикл, читабельність, математика}}

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

{{індекс [функція, застосування], ефективність}}

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

{Оптимізація індексів

Дилема швидкості проти ((елегантності)) є цікавою. Ви можете розглядати її як своєрідний континуум між зручністю для людини та зручністю для машини. Майже будь-яку програму можна зробити швидшою, зробивши її більшою та заплутанішою. Програміст повинен знайти відповідний баланс.

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

{{Профілювання індексів}}

Турбота про ефективність може відволікати увагу. Це ще один фактор, який ускладнює розробку програми, а коли ви робите щось і без того складне, додаткова турбота про ефективність може паралізувати вас.

{{index «передчасна оптимізація»}}

Тому, як правило, варто починати з написання чогось правильного і зрозумілого. Якщо вас турбує, що це занадто повільно - а зазвичай це не так, оскільки більшість коду просто не виконується достатньо часто, щоб займати значну кількість часу - ви можете виміряти його згодом і покращити, якщо це необхідно.

{{index «розгалужена рекурсія»}}

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

{{id recursive_puzzle}} {{index recursion, «приклад числової головоломки»}}

Розглянемо цю головоломку: починаючи з числа 1 і багаторазово додаючи 5 або множачи на 3, можна отримати нескінченну множину чисел. Як би ви написали функцію, яка за заданим числом намагається знайти послідовність таких додавань і множень, які дають це число? Наприклад, число 13 можна отримати, якщо спочатку помножити на 3, а потім двічі додати 5, тоді як число 15 взагалі неможливо отримати.

Ось рекурсивний розв'язок:

function findSolution(target) {
  function find(current, history) {
    if (current == target) {
      повернути історію;
    } else if (current > target) {
      повернути null;
    } else {
      return find(current + 5, `(${history} + 5)`) ??
             find(current * 3, `(${history} * 3)`);
    }
  }
  return find(1, «1»);
}

console.log(findSolution(24));
// → (((1 * 3) + 5) * 3)

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

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

Внутрішня функція find виконує власне рекурсію. Вона приймає два ((аргументи)): поточне число і рядок, який записує, як ми дійшли до цього числа. Якщо вона знаходить розв'язок, то повертає рядок, який показує, як дістатися до мети. Якщо не знайдено жодного розв'язку, починаючи з цього числа, повертається null.

{{індекс null, «?? оператор», «оцінка короткого замикання»}}

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

{{index «call stack»}}

Щоб краще зрозуміти, як ця функція дає потрібний нам ефект, давайте подивимося на всі виклики find, які здійснюються при пошуку розв'язку для числа 13:

find(1, «1»)
  find(6, «(1 + 5)»)
    find(11, «((1 + 5) + 5)»)
      find(16, «(((1 + 5) + 5) + 5)»)
        занадто велике
      find(33, «(((1 + 5) + 5) * 3)»)
        занадто велике
    find(18, «((1 + 5) * 3)»)
      занадто велике
  find(3, «(1 * 3)»)
    find(8, «((1 * 3) + 5)»)
      find(13, «(((1 * 3) + 5) + 5)»)
        Знайдено!

Відступ вказує на глибину стеку викликів. При першому виклику find функція починає з виклику самої себе для пошуку розв'язку, який починається з (1 + 5). Цей виклик буде повторюватися для перебору кожного подальшого розв'язку, який дає число, менше або рівне заданому числу. Оскільки не знайдено жодного розв'язку, який би збігався з заданим, програма повертає null назад до першого виклику. Там оператор ?? викликає виклик, який досліджує (1 * 3). Цьому пошуку пощастило більше - його перший рекурсивний виклик, через ще один рекурсивний виклик, натрапляє на цільове число. Цей внутрішній виклик повертає рядок, і кожен з операторів ?? у проміжних викликах передає цей рядок далі, зрештою повертаючи розв'язок.

Зростаючі функції

{{індекс [функції, визначення]}}

Існує два більш-менш природні способи введення функцій у програми.

{{індекс повторення}}

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

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

{{індекс [функція, назва], [зв'язування, назва]}}

Наскільки складно підібрати гарну назву для функції, є гарним показником того, наскільки зрозумілою є концепція, яку ви намагаєтесь обернути. Давайте розглянемо приклад.

{{index «farm example»}}

Ми хочемо написати програму, яка виводить два числа: кількість корів та курей на фермі, зі словами Cows та Chickens після них та нулями перед обома числами так, щоб вони завжди складалися з трьох цифр:

007 Корови
011 Кури

Тут запитується функція від двох аргументів - кількості корів та кількості курей. Приступимо до кодування.

function printFarmInventory(cows, chickens) {
  let cowString = String(cows);
  while (cowString.length < 3) {
    cowString = «0» + cowString;
  }
  console.log(`${cowString} Корови`);
  нехай chickenString = String(кури);
  while (chickenString.length < 3) {
    chickenString = «0» + chickenString;
  }
  console.log(`${chickenString} Кури`);
}
printFarmInventory(7, 11);

{{index [«властивість length», «for string»], «цикл while»}}

Написання .length після рядкового виразу дасть нам довжину цього рядка. Таким чином, цикл while продовжує додавати нулі перед числовими рядками до тих пір, поки вони не стануть довжиною не менше трьох символів.

Місія виконана! Але коли ми вже збиралися відправити фермеру код (разом з великим рахунком), він зателефонував і сказав, що також почав розводити свиней, і чи не могли б ми розширити програму, щоб вона могла друкувати і свиней?

{{index «copy-paste programming»}}

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

function printZeroPaddedWithLabel(number, label) {
  нехай numberString = String(number);
  while (numberString.length < 3) {
    numberString = «0» + numberString;
  }
  console.log(`${numberString} ${label}`);
}

function printFarmInventory(cows, chickens, pigs) {
  printZeroPaddedWithLabel(cows, «Корови»);
  printZeroPaddedWithLabel(chickens, «Кури»)
  printZeroPaddedWithLabel(pigs, «Свині»);
}

printFarmInventory(7, 11, 3);

{{index [function, naming]}}

Працює! Але ця назва, printZeroPaddedWithLabel, трохи незручна. Вона об'єднує три речі - друк, додавання нульового відступу і додавання мітки - в одну функцію.

{{index «функція zeroPad»}}

Замість того, щоб масово витягувати повторювану частину нашої програми, давайте спробуємо виокремити одну концепцію:

function zeroPad(number, width) {
  let string = String(number);
  while (string.length < width) {
    string = «0» + string;
  }
  return string;
}

function printFarmInventory(cows, chickens, pigs) {
  console.log(`${zeroPad(cows, 3)} Корови`);
  console.log(`${zeroPad(chickens, 3)} Кури`);
  console.log(`${zeroPad(pigs, 3)} Свині`);
}

printFarmInventory(7, 16, 3);

{{index readability, «pure function»}}

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

{{index [інтерфейс, дизайн]}}

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

Корисний принцип полягає в тому, щоб утримуватися від додавання хитрощів, якщо ви не впевнені, що вони вам знадобляться. Може виникнути спокуса писати загальні «((фреймворк))s» для кожного біта функціональності, з яким ви стикаєтесь. Не піддавайтеся цьому бажанню. Ви не зробите жодної реальної роботи - ви будете надто зайняті написанням коду, який ніколи не будете використовувати.

{{id pure}}

Функції та побічні ефекти

{{index «side effect», «pure function», [function, purity]}}

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

{{повторне використання індексів}}

Першу допоміжну функцію у прикладі ((ферма)), printZeroPaddedWithLabel, викликано за її побічним ефектом: вона виводить рядок. Друга версія, zeroPad, викликається за значенням, що повертається. Не випадково другий варіант корисний у більшій кількості ситуацій, ніж перший. Функції, які створюють значення, легше комбінувати у нові способи, ніж функції, які безпосередньо виконують побічні ефекти.

{{підстановка індексу}}

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

{{index optimization, «console.log»}}

Тим не менш, не потрібно відчувати себе погано, коли ви пишете функції, які не є чистими. Побічні ефекти часто бувають корисними. Наприклад, немає можливості написати чисту версію console.log, а console.log добре мати. Деякі операції також легше виразити ефективним способом, коли ми використовуємо побічні ефекти.

Підсумок

У цій главі ви навчилися писати власні функції. Ключове слово function, коли використовується як вираз, може створювати значення функції. Якщо воно використовується як оператор, то може бути використане для оголошення зв'язування і надання йому функції як значення. Функції зі стрілками - це ще один спосіб створення функцій.

// Описати f для зберігання значення функції
const f = function(a) {
  console.log(a + 2);
};

// Оголосити функцію g
function g(a, b) {
  return a * b * 3.5;
}

// Менш багатослівне значення функції
let h = a => a % 3;

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

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

Вправи

Мінімум

{{index «Math object», «minimum (exercise)», «Math.min function», minimum}}

У [попередньому розділі] (структура_програми#повернення_значень) було описано стандартну функцію Math.min, яка повертає найменший аргумент. Тепер ми можемо написати таку функцію самостійно. Опишемо функцію min, яка отримує два аргументи і повертає їх мінімальне значення.

{{якщо інтерактивно

// Ваш код тут.

console.log(min(0, 10));
// → 0
console.log(min(0, -10));
// → -10

if}}

{{hint

{{index «minimum (exercise)»}}

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

{{index «ключове слово повернення»}}

Функція може містити декілька інструкцій return.

підказка}}

Рекурсія

{{index recursion, «isEven (вправа)», «парне число»}}

Ми бачили, що можемо використовувати % (оператор залишку) для перевірки парності чи непарності числа, використовуючи % 2 для перевірки, чи ділиться воно на два. Ось ще один спосіб визначити парність або непарність додатного цілого числа:

  • Нуль - парне.

  • Одиниця - непарне.

  • Для будь-якого іншого числа N, його парність дорівнює N - 2.

Визначте рекурсивну функцію isEven, яка відповідає цьому опису. Функція повинна приймати один параметр (додатне ціле число) і повертати булеве значення.

{{index «stack overflow»}}

Протестуйте її на 50 і 75. Подивіться, як вона поводиться на -1. Чому? Чи можете ви придумати, як це виправити?

{{if інтерактивний

// Ваш код тут.

console.log(isEven(50));
// → true
console.log(isEven(75));
// → false
console.log(isEven(-1));
// → ??

if}}

{{hint

{{index «isEven (вправа)», [«ключове слово if», ланцюжок], рекурсія}}

Ваша функція, ймовірно, буде дещо схожа на внутрішню функцію find у рекурсивному прикладі findSolution example у цій главі, з ланцюжком if/else if/else, який перевіряє, який з трьох випадків застосовний. Останній else, що відповідає третьому випадку, виконує рекурсивний виклик. Кожна з гілок повинна містити інструкцію return або іншим чином організувати повернення певного значення.

{{index «stack overflow»}}

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

підказка}}

Підрахунок бобів

{{index «підрахунок бобів (вправа)», [рядок, індексація], «підрахунок з нуля», [«властивість довжини», «для рядка»]}}

Ви можете отримати N-йсимвол або літеру з рядка, дописавши [N] після рядка (наприклад, string[2]). Результатом буде рядок, що містить лише один символ (наприклад, «b»). Перший символ має позицію 0, що призводить до того, що останній символ буде знайдено у позиції string.length - 1. Іншими словами, двосимвольний рядок має довжину 2, а його символи мають позиції 0 і 1.

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

Далі напишіть функцію countChar, яка працює подібно до countBs, за винятком того, що вона отримує другий аргумент, який вказує символ, який потрібно порахувати (замість того, щоб рахувати тільки великі літери B). Перепишіть countBs, щоб скористатися цією новою функцією.

{{якщо інтерактивно

// Ваш код тут.

console.log(countBs(«BOB»));
// → 2
console.log(countChar(«kakkerlak», «k»));
// → 4

if}}

{{hint

{{index «підрахунок бобів (вправа)», [«властивість довжини», «для рядка»], «змінна-лічильник»}}

Вашій функції знадобиться ((цикл)), який переглядає кожен символ у рядку. Вона може перебирати індекс від нуля до одиниці нижче довжини рядка (< string.length). Якщо символ у поточній позиції збігається з тим, який шукає функція, вона додає 1 до змінної-лічильника. Після завершення циклу лічильник можна повернути.

{{index «local binding»}}

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

підказка}}