Глава 3
Функции

Хората си мислят, че компютърните науки са изкуство за гении, но реалната действителност е точно обратна, само много хора, които правят неща, които се натрупват едно върху друго, като стена от малки камъни”

Donald Knuth

Вие видяхте функции, като alert и как се извикват. Функциите са хлябът и маслото на JavaScript програмирането. Концепцията за опаковане на парче програма в стойност има много приложения. Това е инструмент за асоцииране на имена с програми, да се структурират по-големи програми, да се намали повторението и да се изолират тези програми една от друга.

Най-общо, прилагането на функции е дефиниране на нови думи. В обикновената проза на човешкия език, създаването на нови думи е лош стил. Но в програмирането е належащо.

Типично възрастни англо-говорящи имат около 20 000 думи в речника си. Малко програмни езици идват с 20 000 команди в тях. И речника, който им е на разположение има тенденцията да бъде точно определен, по този начин са по-малко гъвкави, отколкото човешкия език. Ето защо трябва да добавим някои неща от собствения си речник.

Дефиниране на функция

Дефинирането на функция, е като дефиниране на променлива, където стойността дадена на тази променлива се случи да е функция. Например, следния код дефинира променливата square, която се отнася към функция, която произвежда квадрата на дадено число:

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

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

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

Функцията може да няма или има множество параметри. В следващия пример, makeNoise няма списък с имена на параметри, докато power има два:

var makeNoise = function() {
  console.log("Pling!");
};

makeNoise();
// → Pling!

var power = function(base, exponent) {
  var result = 1;
  for (var count = 0; count < exponent; count++)
    result *= base;
  return result;
};

console.log(power(2, 10));
// → 1024

Някои функции произвеждат стойност, като например power и square, а други не, като makeNoise, която произвежда само страничен ефект. Изявлението return определя стойността, която функцията връща. Когато контрол се натъкне на подобно изявление, той веднага изкача от текущата функция и дава върната стойност на кода, който извикава функцията. Ключовата дума return без израз след нея, ще накара функцията да върне undefined.

Параметри и обхват

Параметрите на функцията се държат, като обикновени променливи, но техните първоначални стойности са зададени от извикващия функцията, а не от код в самата функция.

Важно свойство на функциите, е че променливите създадени вътре в тях, включително и техните параметри, са локални за функцията. Това означава, например, че променливата result в power ще бъде на ново създадена всеки път, когато се извика функцията и тези отделни инкарнации не си пречат една на друга.

Тези локални променливи се отнасят само за параметри и променливи декларирани с ключовата дума var вътре в тялото на функцията. Променливи декларирани извън функцията се наричат глобални, защото те са видими в рамките на програмата. Възможно е да се получи достъп до тези променливи вътре във функцията до тогава, докато не се декларират локални променливи със същото име.

Следният код демонстрира това. Той определя и извиква две функции, като и двете присвояват стойност на променливата x. Първата декларира променливата x локално и по-този начин променя само локалната променлива. Втората не декларира x локално и така препратката вътре в нея се отнася до глобалната променлива x, която е дефинирана в горната част на примера.

var x = "outside";

var f1 = function() {
  var x = "inside f1";
};
f1();
console.log(x);
// → outside

var f2 = function() {
  x = "inside f2";
};
f2();
console.log(x);
// → inside f2

Това поведение помага за предотвратяване на случайно вмешателство между функциите. Ако всички променливи бяха споделени от цялата програма, щеше да отнеме много време, за да се уверите, че няма име, което да се използва за две различни цели. И ако се използва повторно името на променливата, може да видите странни ефекти от несвързан код със стойността на променливата. Чрез обработка на функционалните локални променливи, които съществуват само в рамките на функцията, езикът дава възможност да се четат и разбират функциите, като малки вселени, без да се налага да се притесняваме за целия код.

Вложен обхват

JavaScript различава не само глобални и локални променливи. Функциите могат да бъдат създадени в рамките на други функции, продукт на няколко степени на местоположението.

Например, това е по-скоро безсмислен израз, да има функция и още две функции вътре в нея:

var landscape = function() {
  var result = "";
  var flat = function(size) {
    for (var count = 0; count < size; count++)
      result += "_";
  };
  var mountain = function(size) {
    result += "/";
    for (var count = 0; count < size; count++)
      result += "'";
    result += "\\";
  };

  flat(3);
  mountain(4);
  flat(6);
  mountain(1);
  flat(1);
  return result;
};

console.log(landscape());
// → ___/''''\______/'\_

Функциите flat и mountain могат да виждат променливата наречена result, тъй като те са в рамките на функцията, която го определя. Но те не могат да видят другата променлива count, тъй като тя е извън техния обхват. Средата извън функцията landscape не вижда нито една от променливите, дефинирани вътре в landscape.

Накратко, всеки локален обхват може да вижда всички локални обхвати, които се съдържат в него. Наборът от променливи видими вътре във функцията се определя от мястото на тази функция в текста на програмата. Всички променливи от блоковете около дефиницията на функцията са видими - което означава, както тези в тялото на функцията, които да приложи, така и тези от най- високото ниво на програмата. Този подход към променливата видимост се нарича - лексикално определяне на обхвата.

Хората, които имат опит с други езици за програмиране, може да очакват, че всеки блок код между скоби произвежда нова локална среда. Но в JavaScript, функциите са единственото нещо, което създава нов обхват. Вие имате право да ползвате свободно стоящите блокове по долу.

var something = 1;
{
  var something = 2;
  // Do stuff with variable something...
}
// Outside of the block again...

Но something вътре в блока се отнася до една и съща променлива, като тази извън блока. В действителност въпреки, че блокове, като този са разрешени те са полезни само за групиране на тялото на if изявления или цикли.

Ако това ви се струва странно, знайте, че не сте сами. Следващата версия на JavaScript ще въведе думата let, която работи, като var, но създава променлива, която е локална за обхващащия блок, а не за обхващащата функция.

Функциите като стойности

Променливите на функциите обикновено действат, като имена за конкретна част от програмата. Такива променливи се определят веднъж и никога не се променят. Това ги прави лесни за стартиране и трудни за объркване.

Но те са различни. Със стойността на функцията може да се направи всичко, което може да се направи с другите стойности - можете да я използвате в произволни изрази, а не само да я извиквате. Възможно е да съхранявате стойност на функция на ново място, да я давате като аргумент към функция и т.н. По същия начин, променлива, която притежава функция, все още е обикновена променлива и може да присвои нова стойност, като това:

var launchMissiles = function(value) {
  missileSystem.launch("now");
};
if (safeMode)
  launchMissiles = function(value) {/* do nothing */};

В Глава 5 ще обсъдим чудесните неща, които могат да се направят чрез подаване на стойности към функция от други функции.

Декларация нотация

Налице е малко по-къс начин да се каже “var square = function…”. Думата function може да се използва в началото на протокола, както в следното:

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

Това е функция декларация. Изявлението дефинира променливата square да сочи към тази функция. До тук добре. Има една тънкост с тази форма на декларация на функция, обаче.

console.log("The future says:", future());

function future() {
  return "We STILL have no flying cars.";
}

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

Какво се случва, когато поставим такава дефиниция на функция вътре в блока на (if) условие или цикъл? Е не правете това. Различните платформи на JavaScript в различните браузъри традиционно правят различни неща в такива ситуации и най- новите стандарти в действителност не го позволяват. Ако искате вашите програми да се държат последователно, използвайте само тези форми на дефиниране на функции в най-външния блок на функция или програма.

function example() {
  function a() {} // Okay
  if (something) {
    function b() {} // Danger!
  }
}

Стек на извикване

Ще бъде от полза да разгледаме по-отблизо начина на контрола на потока посредством функция. Ето една проста програма, която прави няколко извиквания на функции:

function greet(who) {
  console.log("Hello " + who);
}
greet("Harry");
console.log("Bye");

Изпълнението на тази програма изглежда приблизително така: извиква greet за да накара контрола да скочи в началото на тази функция (линия 2). Той призовава console.log (вградена функция във браузъра), който поема контрола и си върши работата, след това връща контрола към линия 2. После той достига края на функцията greet и се връща на мястото откъдето се извиква на линия 4. Линията след това призовава console.log отново.

Можем да покажем потока на контрола схематично по следния начин:

top
   greet
        console.log
   greet
top
   console.log
top

Тъй като функцията трябва да скочи обратно до мястото на извикването, когато тя се връща, компютъра трябва да помни контекста, с който е извикана функцията. В един от случаите console.log трябва да скочи обратно към greet функцията. В другия случай той се връща обратно в края на програмата.

Мястото, където компютъра съхранява този контекст е в стека на извикване. Всеки път, когато дадена функция се извиква, настоящия контекст се поставя на върха на стека. Когато функцията се връща, тя премахва горния контекст от стека и го използва за да продължи изпълнението.

Съхраняването на този стек изисква пространство в паметта на компютъра. Когато стека порасне твърде голям, компютъра ще даде грешка от рода на “out of stack space” или “too much recursion”. Следният код илюстрира това, чрез задаване на един наистина труден въпрос към компютъра, който причинява безкрайно движение напред и назад между две функции. Това щеше да е безкрайно, ако компютъра имаше един безкраен стек. Както и да е, ние ще изчерпим пространството или “взривим стека” (както се казва).

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

Допълнителни аргументи

Следният код е разрешен и се изпълнява без всякакъв проблем:

alert("Hello", "Good Evening", "How do you do?");

Функцията alert официално приема само един аргумент. И все пак извикана по този начин, тя не се оплаква. Тя просто игнорира останалите аргументи и показва само “Hello”.

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

Недостатъкът на това е, че е възможно да приеме случайно грешен брой аргументи и никой няма да ви каже за това.

Обърнато, това поведение може да се използва за да се направи функция, която да взема по избор аргументи. Например следната версия на power може да се извика с един или два аргумента, като в този случай се приема експонентата да бъде две и функцията се държи, като square.

function power(base, exponent) {
  if (exponent == undefined)
    exponent = 2;
  var result = 1;
  for (var count = 0; count < exponent; count++)
    result *= base;
  return result;
}

console.log(power(4));
// → 16
console.log(power(4, 3));
// → 64

В следващата глава ще видим начин, по който тялото на функцията може да получи най-точния списък с аргументи, които може да приеме. Това е полезно, тъй като дава възможност функцията да приема произволен брой аргументи. Например, console.log се възползва от това да извежда всички стойности, които и се подадат.

console.log("R", 2, "D", 2);
// → R 2 D 2

Закриване

Възможността да се третират функциите, като стойности, съчетано с факта, че локалните променливи се пресъздават всеки път, когато се извиква дадена функция, води до интересен въпрос. Какво се случва с локалните променливи, когато функцията се извиква и ги създава, но не е активна?

Следният код показва пример на това. Той определя функцията wrapValue, която създава локална променлива. След това връща функция, която има достъп и връща тази локална променлива:

function wrapValue(n) {
  var localVariable = n;
  return function() { return localVariable; };
}

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

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

Това в което, функцията е в състояние на референция към конкретен случай на локална променлива в обхващата функция, се нарича закриване. Функцията, която затваря няколко локални променливи се нарича закриваща. Това поведение не само ви освобождава от необходимостта да се притеснявате за живота на променливите, но също така дава възможност за някое творческо използване на функционалните стойности.

С лека промяна, можем да превърнем предишния пример в начин за създаване на функции, които се размножават от произволен номер.

function multiplier(factor) {
  return function(number) {
    return number * factor;
  };
}

var twice = multiplier(2);
console.log(twice(5));
// → 10

Изричното localVariable от wrapValue - примера, не е необходимо, тъй като параметър е самата локална променлива.

Мисля, че програми като тази изискват известна практика. Един добър мисловен модел е да се мисли за ключовата дума function, като “замразяване” на код в тялото и, и опаковането му в пакет (стойност на функция). Така че, когато четете return function(...) {...}, мислете за него, като връщане на манипулатор за парче от изчисления, замразени за по-нататъшна употреба.

В примера, multiplier връща замразено парче код, който получава съхраняваната в twice променлива. Последният ред код извиква стойността на тази променлива, вкарвайки я на замразения код (return number * factor;) за да се активира. Функцията все още има достъп до променливата factor от извикването на multiplier, която го създава, и в допълнение, тя получава достъп до аргумента, когато става размразяването - 5 , чрез своя number параметър.

Рекурсия

Много е добре за една функция да се извиква, толкова дълго, докато се грижи да не препълни стека. Функцията, която извиква себе си се нарича рекурсивна. Рекурсията позволява някои функции да бъдат написани в различен стил. Вземете, например, това алтернативно изпълнение на power:

function power(base, exponent) {
  if (exponent == 0)
    return 1;
  else
    return base * power(base, exponent - 1);
}

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

Това е по-скоро близо до математическото степенуване и безспорно описва концепцията по по-елегантен начин, отколкото един цикъл може да го направи. Функцията извиква себе си няколко пъти с различни аргументи, за да се постигне повтаряемост на размножаването.

Но това изпълнение има един важен проблем: в типични JavaScript приложения, това е около 10 пъти по-бавно от цикъл версия. Работата чрез цикъл е много по-бърза, отколкото извикването на функция няколко пъти.

Дилемата на скорост спрямо елегантност е интересна. Можете да я погледнете, като едно продължение между човека и машината, за лекотата на употребата. Почти всяка програма може да се направи по-бърза, което я прави по-голяма и по-сложна. Програмиста трябва да вземе решение за подходящо равновесие.

В случая по-рано на функцията power, грубоватата (цикъл) версия все още е сравнително проста и лесна за четене. Тогава няма много смисъл да се заменя с рекурсивна версия. Често обаче, една програма се занимава с такива сложни понятия, че се налага да се отказва някоя ефективност за да се направи програмата по-ясна, това е атрактивен избор.

Основно правило на много програмисти и с което съм съгласен с цялото си сърце, е да не се тревожим за ефективността, докато не знаем със сигурност, че програмата е твърде бавна. Ако е така, разберете кои части отнемат най-много време и започнете да обменяте елегантност за ефективност в тези части.

Разбира се, това не означава, че човек трябва да игнорира производителноста напълно. В много случаи, като например функцията power, не е много ясно дали се печели от елегантен подход. И понякога опитен програмист може да види веднага, че един прост подход никога няма да бъде достатъчно бърз.

Причината, както съм подчертавал е, че изненадващо много програмисти фанатично започват да се съсредоточават върху ефективността, дори в най-малките подробности. Резултатът е по-големи, по-сложни, а често и по-малко правилни програми, които обикновено работят незначително по-бързо и отнемат повече време да се пишат, отколкото техните по-ясни еквиваленти.

Но рекурсията не винаги е просто по-ефективна алтернатива от цикъла. Някои от проблемите са много по-лесни за решаване с рекурсия, отколкото с цикъл. Най-често това са проблеми, които изискват проучване или преработване на някои “разклонения”, всяко от които може да се разклони отново в други разклонения.

Помислете за този пъзел: като се започне от цифрата 1 и на няколко пъти добавяте 5 или умножавате по 3, могат да бъдат произведени безкрайно количество от нови номера. Как бихте написали функция, която по един номер, се опитва да намери поредицата от такива събирания и умножения, които произвеждат този номер? Например номер 13 може да бъде постигнат, като първо се умножава по 3 и след това на два пъти се добавя 5, като се има в предвид, че не може да се стигне до номер 15.

Това е рекурсивното решение:

function findSolution(target) {
  function find(start, history) {
    if (start == target)
      return history;
    else if (start > target)
      return null;
    else
      return find(start + 5, "(" + history + " + 5)") ||
             find(start * 3, "(" + history + " * 3)");
  }
  return find(1, "1");
}

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

Имайте в предвид, че тази програма няма претенциите да намери най-кратката последователност на операциите. Ние сме доволни, ако намери всяка последователност за всички.

Не е задължително да очакваме веднага да разберем как работи . Но нека да поработим с нея, тъй като тя дава възможност за чудесно упражнение в рекурсивното мислене.

Вътрешната функция find прави действителна рекурсия. Тя взема два аргумента- текущия номер и string, който записва как се достига до този номер и връща string, който показва как се стига до целта или null.

За да направи това, функцията изпълнява едно от следните три действия. Ако текущия (current) номер е номера на целта (target), текущата история (history) е просто начин за постигане на тази цел, така че просто се връща. Ако текущия номер е по-голям от целта, няма смисъл от по-нататъшното разглеждане на тази история, тъй като събирането и умножението, ще направят номера по-голям. И на края, ако все още сме далеч от целта, функцията се опитва от двете възможности, които започват от сегашния брой, извикваща себе си два пъти по веднъж за всяка от следващите стъпки. Ако първото извикване връща нещо, което не е null, то се връща. В противен случай, на второто извикване се връща - независимо от това дали тя произвежда string или null.

За да разберете по-добре как функцията произвежда ефекта, който търсим, нека да разгледаме всички извиквания към find, които се правят при търсене на решението за броя 13.

find(1, "1")
  find(6, "(1 + 5)")
    find(11, "((1 + 5) + 5)")
      find(16, "(((1 + 5) + 5) + 5)")
        too big
      find(33, "(((1 + 5) + 5) * 3)")
        too big
    find(18, "((1 + 5) * 3)")
      too big
  find(3, "(1 * 3)")
    find(8, "((1 * 3) + 5)")
      find(13, "(((1 * 3) + 5) + 5)")
        found!

Отместването на вътре предполага, дълбочината на стека за повикване. Първият find извиква себе си два пъти, за да проучи решенията, които започват с (1 + 5) и (1 * 3). Първото извикване се опитва да намери решение, което започва с (1 + 5) и с помощта на рекурсия, изследва всеки отговор, който се получава - число по-малко или равно на целевия номер. Тъй като не намира решение, което да улучи целта, той връща null обратно, към първото извикване. Там оператора || води извикването към (1 * 3). Това търсене има повече късмет, защото първото рекурсивно извикване, чрез поредното рекурсивно повикване улучва целевия брой 13. Това най-вътрешно рекурсивно извикване връща string и всеки един от || операторите в междинните нива подадени в този string заедно и в крайна сметка връща нашето решение.

Нарастване на функции

Има две повече или по-малко естествени начина функциите да бъдат въведени в програмите.

Първият е написването на много подобен код няколко пъти. Ние искаме да избегнем това, тъй като повече код означава повече възможности за грешки и повече материал за четене от хора опитващи се да разберат програмата. Така че, ако се повтаря функционалност, намери добро име за нея и я сложи във една функция.

Вторият начин е, да видите дали имате нужда от някоя функция, която не е написана още и напишете своя собствена функция. Ще започнете от названието на функцията, а след това напишете нейното тяло. Може дори да започнете да пишете код, който използва функцията преди да определите самата функция.

Колко трудно е да се намери подходящо име за функция е добър показател за това, колко ясна е концепцията, която се опитвате да приключите. Нека минем през един пример.

Ние искаме да напишем програма, която отпечатва две числа - броят на кравите и пилетата във ферма, с думите Cows и Chickens и нули преди числото, така че винаги да е три цифри.

007 Cows
011 Chickens

Ясно е, че се пита за функция с два аргумента. Да кодираме:

function printFarmInventory(cows, chickens) {
  var cowString = String(cows);
  while (cowString.length < 3)
    cowString = "0" + cowString;
  console.log(cowString + " Cows");
  var chickenString = String(chickens);
  while (chickenString.length < 3)
    chickenString = "0" + chickenString;
  console.log(chickenString + " Chickens");
}
printFarmInventory(7, 11);

Добавянето на .length след стойността на string, ще ни даде дължината на този string. По този начин while цикъла поддържа добавянето на нули пред номера на string, докато той е дълъг най- малко три знака.

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

Сигурно можем. Но точно, когато сме в процес на копиране и вмъкване на тези четири линии отново, да спрем и се замислим. Дали има по-добър начин. Ето един първи опит:

function printZeroPaddedWithLabel(number, label) {
  var numberString = String(number);
  while (numberString.length < 3)
    numberString = "0" + numberString;
  console.log(numberString + " " + label);
}

function printFarmInventory(cows, chickens, pigs) {
  printZeroPaddedWithLabel(cows, "Cows");
  printZeroPaddedWithLabel(chickens, "Chickens");
  printZeroPaddedWithLabel(pigs, "Pigs");
}

printFarmInventory(7, 11, 3);

Работи! Но това име printZeroPaddedWithLabel е малко неудобно. То смесва три неща - печат, нула-подплънка и добавяне на етикет в една функция.

Вместо премахване на повтарящата се част от нашата програма за търговия на едро, да опитаме да изберем една единствена концепция.

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

function printFarmInventory(cows, chickens, pigs) {
  console.log(zeroPad(cows, 3) + " Cows");
  console.log(zeroPad(chickens, 3) + " Chickens");
  console.log(zeroPad(pigs, 3) + " Pigs");
}

printFarmInventory(7, 16, 3);

Функция с очевидно хубаво име, като zeroPad прави по-лесно за някой, който чете кода, да разбере какво прави той. И това е полезно в повече ситуации, отколкото само в тази конкретна програма. Например може да я използвате за да отпечатате добре подравнени таблици от номера.

Колко интелигентна и гъвкава трябва да бъде вашата функция? Можете да напишете ужасно проста функция, която просто подава номера, широки три знака на сложен форматиращ генератор за номера, който се занимава с дробни и отрицателни числа, подравняване на точки, попълва различни характери и т.н.

Полезен принцип е да не се добавя интелигентност, освен ако не сте абсолютно сигурни, че се нуждаете от нея. Може да се изкушите да пишете общи рамки за всяка малка функционалност на която попаднете. Не се поддавайте на това желание. Ще свършите някаква реална работа, но вие ще се окажете пред написването на много код, който никога няма да използвате.

Функции и странични ефекти

Функциите могат грубо да бъдат разделени, на тези които се извикват за техните странични ефекти и онези, които се извикват за връщане на стойност. (Въпреки, че това определено е възможно, една функция да има странични ефекти и да връща стойност.)

Функцията в примера с фермата, printZeroPaddedWithLabel извиква нейния страничен ефект: тя отпечатва ред. Във втората версия, zeroPad се извиква нейното връщане на стойност. Не е съвпадение, че втория вариант е полезен в повече ситуации от първия. Функциите, които създават стойности са по-лесни за комбиниране по нов начин, отколкото функции, които пряко изпълняват странични ефекти.

Чиста функция е специфичен вид функция за производство на стойност, която не само няма странични ефекти, но и не разчита на странични ефекти от други кодове, например, тя не чете глобални променливи, които по някога се променят от други кодове. Чистата функция има хубавото качество, когато е извикана със същите аргументи, тя винаги да произвежда една и съща стойност( и не прави нищо друго). Това се прави за да разсъждава лесно. Извикването на такава функция може да бъде мислено заменена от нейния резултат, без да се променя смисъла на кода. Когато не сте сигурни, че чистата функция работи правилно, може да я тествате, като просто я извикате и ако тя работи в този контекст, ще работи във всеки друг контекст. Нечистите функциите могат да връщат различни стойности на базата на всички видове фактори и имат странични ефекти, които могат да бъдат трудни за тестване и измисляне.

И все пак, няма нужда да се чувствате зле, когато пишете функции, които не са чисти или водят свещена война срещу вашия код. Страничните ефекти често са полезни. Сигурно има начин да се напише чиста версия на console.log, но console.log със сигурност е полезна. Някои операции също са по-лесни за изразяване по ефективен начин, когато използваме странични ефекти, така че скоростта на изчисленията може да бъде причина за избягване на чистотата.

Резюме

Тази глава ви научи как да пишете свои собствени функции. Функция е ключова дума, която когато се използва като израз, може да създаде стойност функция. Когато се използва, като изявление, тя може да се използва за деклариране на променлива, която да се даде на функция, като нейна стойност.

// Create a function value f
var f = function(a) {
  console.log(a + 2);
};

// Declare g to be a function
function g(a, b) {
  return a * b * 3.5;
}

Ключов аспект за разбирането на функции е разбирането на локалните обхвати. Параметри и променливи, декларирани в тялото на функцията са локални за функцията, пресъздадени всеки път, когато функцията се извиква и не се виждат от вън. Функции обявени в други функции имат достъп до локалния обхват на външната функция.

Разделянето на задачите, които изпълнява програмата в различни функции е от полза. Няма нужда от много повтаряне за да разберете, че функциите могат да направят програмата, по-разбираема чрез групиране на код в концептуални парчета, по същия начин, както глави и раздели помагат за организиране на обикновен текст.

Упражнения

Минимум

В предишната глава въведохме стандартната функция Math.min която връща най-малкия аргумент. Можем да направим това сами сега. Напишете функция min, която взема два аргумента и връща по-малкия.

// Your code here.

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

Ако имате проблеми с поставянето на скоби или със скоби на точното място, за да получите валидна дефиниция на функция, започнете, като копирате един от примерите в тази глава и го променете.

Функцията може да съдържа множество return изявления.

Рекурсия

Видяхме, че % (оператора за остатък) може да се използва, за да се провери дали даден номер е четен или с % 2 дали се дели на две. Ето още един начин, по който може да се определи дали дадено положително цяло число е четно или нечетно.

Направете рекурсивна функция isEven съответстваща на това описание. Функцията трябва да приема number параметър и да връща булева стойност.

Пробвайте с 50 и 75. Вижте как се държи с -1. Защо? Сещате ли се начин да се оправи това?

// Your code here.

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

Вашата функция вероятно ще изглежда подобно на вътрешната функция find в рекурсивния findSolution пример на тази глава, с if/else if/else последователност за тестване, на кой от трите случая се прилага. Финалът на else, който съответства на третия случай, прави рекурсивно извикване. Всяка от бримките на тестовете трябва да съдържа return изявление или някакъв друг начин, за осигуряване на връщане на определена стойност.

Когато е дадено отрицателно число, функцията ще се самоизвика отново и отново, като си подава все повече отрицателни числа и по този начин все по-далеч и по-далеч от крайния си резултат. Това в крайна сметка ще изчерпи пространството на стека и ще се откаже.

Bean броене

Можете да получите N-тия харктер или буква от string с написването на "string".charAt(N), подобно на начина, по който вземаме дължината му с "s".length. Върнатата стойност ще бъде string, съдържащ само един характер (например "b"). Първият характер има позиция нула, което означава, че последният ще бъде намерен в позиция string.length - 1. С други думи, string от два характера, ще има позиции 0 и 1.

Напишете функция countBs, която взема string, като единствен аргумент и връща число, което показва колко много главни “B” характера има в този string.

На следващо място, напишете функция, наречена countChar, която се държи като countBs, освен това взема втори аргумент, който показва характера, който трябва да се брои (а не само да брои главни “B”). Пренапишете countBs да се възползва от тази нова функция.

// Your code here.

console.log(countBs("BBC"));
// → 2
console.log(countChar("kakkerlak", "k"));
// → 4

Цикъла във вашата функция ще трябва да разгледа всеки характер в string започвайки от индекс нула до един по-малко от дължината му (< string.length). Ако характера на текущата позиция е същия, като този, който функцията търси, се добавя 1 към променливата на брояча. След като цикъла приключи, броячът може да бъде returned.

Погрижете се да направите всички променливи, използвани във функцията локални за функцията с помощта на ключовата дума var.