Глава 5
По-високо ниво функции

Дзъ-ли и Дзъ-су се хвалят с размерите на най-новите си програми. „Двеста хиляди реда код”, каза Дзъ-ли „без да броим коментарите!” Дзъ-су му отговорил „Пфу, моята е почти с милион линии повече.” А майстор Ян-Ма, казал „Моята най-добра програма има петстотин реда.” Като чули това, Дзъ-ли и Дзъ-су били просветени.”

Master Yuan-Ma, Книга за програмиране

Има два начина за конструиране на софтуерен дизайн: Единият от начините е да се направи това толкова просто, че да има очевидни недостатъци, а другият начин е да се направи това, толкова сложно, че да не се виждат явните недостатъци.”

C.A.R. Hoare, 1980 ACM Turing Award Lecture

Голямата програма е скъпа програма и то не само заради времето необходимо да се изгради. Размера почти винаги включва сложност и комплексност, която обърква програмистите. Обърканите програмисти, от своя страна, са склонни да въвеждат грешки (bugs) в програмите. Голямата програма предлага много място, където могат да се крият тези грешки, което ги прави трудни за намиране.

За кратко да се върнем към последните две примерни програми в увода. Първата е автономна и е дълга 6 реда.

var total = 0, count = 1;
while (count <= 10) {
  total += count;
  count += 1;
}
console.log(total);

Втората се позовава на две външни функции и е с дължина един ред.

console.log(sum(range(1, 10)));

Коя е по-вероятно да съдържа бъг?

Ако се позовем на размера на определенията за sum и range, втората програма е по-голяма от първата. Но все пак, мога да твърдя, че е по-вероятно да бъде вярна.

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

Определенията на тази лексика (функциите sum и range), все още включват цикли, броячи и други съпътстващи подробности. Но тъй като са по-прости понятия за изразяване на програмата, като цяло, те са по-вероятно да са правилни.

Абстракция

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

Като аналогия, да сравним тези две рецепти за грахова супа:

Сложете 1 чаша сушен грах на човек в контейнер. Добавете вода, докато се покрие добре. Оставете граха във водата в продължение на най-малко 12 часа. Вземете граха от контейнера и го сложете в тенджера за готвене. Добавете 4 чаши вода на човек. Покрийте тенджерата с капак и варете граха в продължение на два часа. Вземете половин глава лук на човек. Нарежете го на парчета с нож. Добавете го към граха. Вземете един морков на човек. Нарежете го на парчета. С нож! Добавете го към граха. Гответе още 10 минути.”

И втората рецепта:

На човек: 1 чаша изсушен грах, половин нарязан лук, стрък целина и един морков.

Накиснете граха в продължение на 12 часа. Оставете го да къкри 2 часа в 4 чаши вода (на човек). Добавете нарязаните лук и зеленчуци. Гответе още 10 минути.”

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

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

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

Абстрактно прекосяване на масив

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

В предишната глава, този вид for цикъл направи няколко завъртания:

var array = [1, 2, 3];
for (var i = 0; i < array.length; i++) {
  var current = array[i];
  console.log(current);
}

Цикъла се опитва да каже: “Вземи всеки елемент от масива и го изведи в конзолата”. Но той използва по-заобиколен начин, който включва брояч i, проверка за дължината на масива и допълнителна променлива-декларация за избиране на текущия елемент. Освен, че е малко грозно, това ни дава много място за потенциални грешки. Ние може случайно да използваме повторно променливата i, да сбъркаме length с lenght, да объркаме i с current и т.н.

Така, че нека се опитаме да направим това, абстрактно под нова функция. Сещате ли се за начин?

Е, това е лесно да се напише функция, която преминава през масив и извиква console.log на всеки елемент.

function logEach(array) {
  for (var i = 0; i < array.length; i++)
    console.log(array[i]);
}

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

function forEach(array, action) {
  for (var i = 0; i < array.length; i++)
    action(array[i]);
}

forEach(["Wampeter", "Foma", "Granfalloon"], console.log);
// → Wampeter
// → Foma
// → Granfalloon

(В някои браузъри, извикана console.log по този начин не работи. Можете да използвате alert вместо console.log, ако този пример не работи.)

Често не подаваме предварително определена функция, като forEach, но можем да създадем стойност на функцията на място.

var numbers = [1, 2, 3, 4, 5], sum = 0;
forEach(numbers, function(number) {
  sum += number;
});
console.log(sum);
// → 15

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

Използвайки този модел, можем да зададем име на променлива за текущия елемент (number), а не да се налага да го вземаме от масива ръчно.

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

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

function gatherCorrelations(journal) {
  var phis = {};
  for (var entry = 0; entry < journal.length; entry++) {
    var events = journal[entry].events;
    for (var i = 0; i < events.length; i++) {
      var event = events[i];
      if (!(event in phis))
        phis[event] = phi(tableFor(event, journal));
    }
  }
  return phis;
}

Работата с forEach я прави малко по-къса и малко по-чиста.

function gatherCorrelations(journal) {
  var phis = {};
  journal.forEach(function(entry) {
    entry.events.forEach(function(event) {
      if (!(event in phis))
        phis[event] = phi(tableFor(event, journal));
    });
  });
  return phis;
}

По-високо ниво функции

Функции, които работят в други функции или ги вземат, като аргументи или се връщат, като аргументи, се наричат "higher-order functions". Ако вече сте приели факта, че функциите са обикновени стойности, няма нищо особено и забележимо във факта, че съществуват такива функции. Терминът идва от математиката, където вземат разграничаването между функциите и другите стойности по-сериозно.

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

function greaterThan(n) {
  return function(m) { return m > n; };
}
var greaterThan10 = greaterThan(10);
console.log(greaterThan10(11));
// → true

И може да имаме функции, които променят други функции.

function noisy(f) {
  return function(arg) {
    console.log("calling with", arg);
    var val = f(arg);
    console.log("called with", arg, "- got", val);
    return val;
  };
}
noisy(Boolean)(0);
// → calling with 0
// → called with 0 - got false

Можем да пишем функции, които осигуряват нови видове контрол на потока.

function unless(test, then) {
  if (!test) then();
}
function repeat(times, body) {
  for (var i = 0; i < times; i++) body(i);
}

repeat(3, function(n) {
  unless(n % 2, function() {
    console.log(n, "is even");
  });
});
// → 0 is even
// → 2 is even

Лексикалното определяне на обхвата от правила, което обсъдихме в Глава 3, работи в наша полза, когато използваме функциите по този начин. В предишния пример, променливата n е параметър към външната функция. Но вътрешната функция живее в средата на външната и тя може да използва n. Телата на такива вътрешни функции могат да получат достъп до променливите около тях. Те могат да играят роля подобна на {} блокове, използвани в редовни цикли и условни конструкции. Важна разлика е, че променливи декларирани вътре във вътрешните функции, не попадат в заобикалящата среда на външната функция. И това обикновено е нещо добро.

Подаване на аргументи

Функцията noisy определена по-рано, която увива своя аргумент в друга функция, има доста сериозен пропуск.

function noisy(f) {
  return function(arg) {
    console.log("calling with", arg);
    var val = f(arg);
    console.log("called with", arg, "- got", val);
    return val;
  };
}

Ако f взема повече от един аргумент, получава само първия. Бихме могли да добавим един куп аргументи към вътрешната функция (arg1, arg2 и т.н.) и да ги прехвърлим към f, но не е ясно, колко от тях ще бъдат достъпни. Това решение също би лишило f от част от информацията в arguments.length. Тъй като винаги ще взема същия брой аргументи, колкото първоначално са му били зададени.

За тези видове ситуации, функциите в JavaScript имат метода apply. Може да го сложите в масив (или масиво-подобен обект) от аргументи и да извикате функцията с тези аргументи.

function transparentWrapping(f) {
  return function() {
    return f.apply(null, arguments);
  };
}

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

JSON

По-високото ниво функции, които по някакъв начин прилагат функция върху елементите на даден масив, са широко използвани в JavaScript. Метода forEach е най-примитивната такава функция. Има редица други варианти на методи за масиви. За да се запознаем с тях, нека си поиграем с друг набор от данни.

Преди няколко години, попаднах на много архиви събрани и поставени на едно място в книга, посветена на историята на моето семейство (Haverbeke—meaning Oatbrook). Отворих я с надеждата да намеря рицари, пирати и алхимици...., но книгата се оказа пълна най-вече с фламандски фермери. За забавление, извлякох данните на моите преки предци и ги поставих в електронен формат.

Файлът изглежда нещо такова:

[
  {"name": "Emma de Milliano", "sex": "f",
   "born": 1876, "died": 1956,
   "father": "Petrus de Milliano",
   "mother": "Sophia van Damme"},
  {"name": "Carolus Haverbeke", "sex": "m",
   "born": 1832, "died": 1905,
   "father": "Carel Haverbeke",
   "mother": "Maria van Brussel"},
   and so on
]

Този формат се нарича JSON (произнася се”Джейсън”), което е съкращение от JavaScript Object Notation. Той се използва широко, както за съхранение на данни, така и за комуникационен формат в Web.

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

JavaScript ни дава функциите JSON.parse и JSON.stringify, които преобразуват данни от и към този формат. Първата взема стойност от JavaScript и връща кодиран JSON string формат. Втората взема този string и го преобразува в кодирана стойност.

var string = JSON.stringify({name: "X", born: 1980});
console.log(string);
// → {"name":"X","born":1980}
console.log(JSON.parse(string).born);
// → 1980

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

var ancestry = JSON.parse(ANCESTRY_FILE);
console.log(ancestry.length);
// → 39

Филтриране на масив

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

function filter(array, test) {
  var passed = [];
  for (var i = 0; i < array.length; i++) {
    if (test(array[i]))
      passed.push(array[i]);
  }
  return passed;
}

console.log(filter(ancestry, function(person) {
  return person.born > 1900 && person.born < 1925;
}));
// → [{name: "Philibert Haverbeke", …}, …]

Функцията използва аргумент с име test, като стойност на функция за да попълни “разминаването” в изчислението. Test функцията се извиква за всеки елемент и върнатата и стойност определя дали даден елемент е включен във върнатия масив.

Трима души са били млади и живи през 1924г: дядо ми, баба ми и пра-леля ми.

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

Подобно на forEach, така и filter също е стандартен метод за масиви. Примерът дефинира функция само за да покаже това, което тя прави вътрешно. От сега на татък, ще я използваме така:

console.log(ancestry.filter(function(person) {
  return person.father == "Carel Haverbeke";
}));
// → [{name: "Carolus Haverbeke", …}]

Трансформиране с карта (map)

Да кажем, че имаме масив от обекти представляващи хора, направени чрез филтриране на масива ancestry по някакъв начин. Но ние искаме масив от имена, който е по-лесен за четене.

Метода map трансформира масив чрез прилагане на функция върху всички свои елементи и изгражда нов масив от върнатите стойности. Новият масив има същата дължина, като входящия масив, но съдържанието му е “mapped” към новата форма на функцията.

function map(array, transform) {
  var mapped = [];
  for (var i = 0; i < array.length; i++)
    mapped.push(transform(array[i]));
  return mapped;
}

var overNinety = ancestry.filter(function(person) {
  return person.died - person.born > 90;
});
console.log(map(overNinety, function(person) {
  return person.name;
}));
// → ["Clara Aernoudts", "Emile Haverbeke",
//    "Maria Haverbeke"]

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

Подобно на forEach и filter, map също е стандартен метод за масиви.

Обобщаване с намаляване

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

По-високото ниво на операцията, която представлява този модел се нарича reduce (намаляване или понякога fold). Можете да мислите за него, като сгъване на масива в един елемент в даден момент. Когато сумираме номерата, ще започнем с числото нула и за всеки елемент го комбинираме с текущата сума чрез добавяне на две.

Параметрите на функцията reduce са освен масива, функция за комбиниране и начална стойност. Тази функция е малко по-малка от filter и map, така че и обърнете особено внимание.

function reduce(array, combine, start) {
  var current = start;
  for (var i = 0; i < array.length; i++)
    current = combine(current, array[i]);
  return current;
}

console.log(reduce([1, 2, 3, 4], function(a, b) {
  return a + b;
}, 0));
// → 10

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

За да използваме reduce, за да намерим най-древния известен мой предшественик, можем да напишем нещо такова:

console.log(ancestry.reduce(function(min, cur) {
  if (cur.born < min.born) return cur;
  else return min;
}));
// → {name: "Pauwels van Haverbeke", born: 1535, …}

Composability

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

var min = ancestry[0];
for (var i = 1; i < ancestry.length; i++) {
  var cur = ancestry[i];
  if (cur.born < min.born)
    min = cur;
}
console.log(min);
// → {name: "Pauwels van Haverbeke", born: 1535, …}

Има още няколко променливи и програмата е с два реда повече, но все още е доста лесно да се разбере.

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

function average(array) {
  function plus(a, b) { return a + b; }
  return array.reduce(plus) / array.length;
}
function age(p) { return p.died - p.born; }
function male(p) { return p.sex == "m"; }
function female(p) { return p.sex == "f"; }

console.log(average(ancestry.filter(male).map(age)));
// → 61.67
console.log(average(ancestry.filter(female).map(age)));
// → 54.56

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

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

Това е страхотно за написване на ясен код. За съжаление, тази яснота има цена.

Разходите

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

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

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

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

За щастие повечето компютри са безумно бързи. Ако обработвате скромен набор от данни или правите нещо, което трябва да се случи само по скалата на човешкото време (да речем, всеки път, когато потребителя кликне на бутон), тогава няма значение дали пишете решение, което отнема половин милисекунда или супер - оптимизирано решение, което отнема една десета от секундата.

Полезно е грубо да следите, колко често парче от вашата програма се стартира. Ако имате един цикъл вътре в друг цикъл (или директно, чрез външния цикъл извиквате функция, която завършва работата на вътрешния цикъл), кодът вътре във вътрешния цикъл ще приключи с N×M пъти, където N е броя на повторенията на външният цикъл, а M е броят пъти, когато вътрешният цикъл се повтаря в рамките на всяка итерация на външния цикъл. Ако този вътрешен цикъл съдържа друг цикъл, който прави P завъртания, тялото му ще се стартира M×N×P пъти и т. н. Това може да бъде в големи количества и когато дадена програма е бавна, проблемът често може да бъде проследен до само една малка част от кода, който стои вътре във вътрешния цикъл.

Пра-пра-пра-пра...

Дядо ми, Philibert Haverbeke, е включен в базата данни. Като се започне с него, мога да проследя родословието си, за да разбера дали най-древния човек в данните - Pauwels van Haverbeke, е моя прародител. И ако е той, бих искал да знам колко ДНК теоретично споделям с него.

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

var byName = {};
ancestry.forEach(function(person) {
  byName[person.name] = person;
});

console.log(byName["Philibert Haverbeke"]);
// → {name: "Philibert Haverbeke", …}

Сега проблемът не е прост, трябва да се следват father свойствата, които трябва да преброим за да стигнем до Pauwels. Има няколко случая в родословното дърво, където хората се женели за вторите си братовчеди. Това е причина клоните на родословното дърво да се съединяват на няколко места, което означава, че аз споделям повече от 1/2G от гените на този човек, като броя G е за броя на поколенията между Pauwels и мен. Тази формула идва от идеята, че всяко поколение разделя генетичния фонд на две.

Разумен начин да мислим за този проблем е да гледаме на него, като аналогичен на reduce, който свива масива до една стойност, чрез многократно съкращаване на стойности от ляво на дясно. В този случай, ние също искаме да съкратим нашата структура от данни до една стойност по начин, който следва семейните линии. Формата на данните е на родословно дърво , а не плосък списък.

Начинът, по който искаме да намалим тази форма е с изчисляване на стойност за даден човек, чрез комбиниране на стойности от неговите предци. Това може да стане рекурсивно: ако ние сме заинтересованото лице A, то трябва да изчислим стойностите на родителите на A, което от своя страна трябва да изчисли стойностите на техните родители-“баба и дядо” и т.н. По принцип, това изисква да прегледаме един безкраен брой от хора, но тъй като нашия набор от данни е ограничен, ще трябва да спрем някъде. Ще приемем стойност по подразбиране, която да дадем на нашата функция за намаляване, която ще се използва за хора, които не са в данните. В нашия случай, тази стойност е просто нула за предположението, че хората, които не са в списъка не споделят ДНК с прародителя, който търсим.

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

function reduceAncestors(person, f, defaultValue) {
  function valueFor(person) {
    if (person == null)
      return defaultValue;
    else
      return f(person, valueFor(byName[person.mother]),
                       valueFor(byName[person.father]));
  }
  return valueFor(person);
}

Вътрешната функция (valueFor) обработва един човек. Чрез магията на рекурсията, тя просто извиква себе си, за да обработи майката и бащата на този човек. Резултатите, заедно със самия обект на лицето се подават на f, който връща действителната стойност за този човек.

След това можем да използваме това за да изчислим количеството ДНК, което дядо ми споделя с Pauwels van Haverbeke и го разделим на четири.

function sharedDNA(person, fromMother, fromFather) {
  if (person.name == "Pauwels van Haverbeke")
    return 1;
  else
    return (fromMother + fromFather) / 2;
}
var ph = byName["Philibert Haverbeke"];
console.log(reduceAncestors(ph, sharedDNA, 0) / 4);
// → 0.00049

Лицето Pauwels van Haverbeke очевидно има 100% от ДНК-то си с Pauwels van Haverbeke ( ако не съществуват хора , които да споделят същото име в масива от данни), така че функцията връща 1 за него. Всички други хора споделят средната стойност от сумите, които техните родители споделят.

Така че, статистически погледнато аз споделям около 0.05% от моето ДНК с този човек от 16-ти век. Трябва да се отбележи, че това е само статистическо приближение, а не точен резултат. Това е по-скоро малък брой в предвид, колко генетичен материал носим ( около 3 милиарда база двойки), но може би е някакъв аспект, който приближава произхода ми с Pauwels.

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

function countAncestors(person, test) {
  function combine(current, fromMother, fromFather) {
    var thisOneCounts = current != person && test(current);
    return fromMother + fromFather + (thisOneCounts ? 1 : 0);
  }
  return reduceAncestors(person, combine, 0);
}
function longLivingPercentage(person) {
  var all = countAncestors(person, function(person) {
    return true;
  });
  var longLiving = countAncestors(person, function(person) {
    return (person.died - person.born) >= 70;
  });
  return longLiving / all;
}
console.log(longLivingPercentage(byName["Emile Haverbeke"]));
// → 0.129

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

Обвързване

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

Следният код показва пример на bind в употреба. Той дефинира функция isInSet, която ни казва дали даден човек е в наборa от strings. За да извикаме filter да събере тези обекти на лица, чиито имена са в специфичния набор от данни, можем да напишем функционален израз, който прави извикване на isInSet с нашия набор от данни, като първи аргумент или частично прилага isInSet функцията.

var theSet = ["Carel Haverbeke", "Maria van Brussel",
              "Donald Duck"];
function isInSet(set, person) {
  return set.indexOf(person.name) > -1;
}

console.log(ancestry.filter(function(person) {
  return isInSet(theSet, person);
}));
// → [{name: "Maria van Brussel", …},
//    {name: "Carel Haverbeke", …}]
console.log(ancestry.filter(isInSet.bind(null, theSet)));
// → … same result

Извикването на bind връща функция, която извиква isInSet с theSet, като първи аргумент, последван от всички останали аргументи, подадени на свързващата функция.

Първият аргумент, който е подаден в примера е null, той се използва за извикване на метод, подобно на първия аргумент на apply. Ще опишем това по-подробно в следващата глава.

Резюме

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

Масивите предоставят редица полезни по-високо ниво методи: forEach, който да направи нещо с всеки елемент в масива, filter, който да изгради нов масив с филтрираните елементи, map, който да изгради нов масив, където всеки елемент е преминал през функция и reduce който да комбинира всички елементи на масива в една стойност.

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

Упражнения

Сплескване

Използвайте метода reduce в комбинация с метода concat за да “изгладите” масив от масиви в един масив, който има всички елементи на входящите масиви.

var arrays = [[1, 2, 3], [4, 5], [6]];
// Your code here.
// → [1, 2, 3, 4, 5, 6]

Възрастова разлика между майка и дете

Използвайте данните от настоящата глава, за да изчислите средно-възрастовата разликата между майки и деца (това е възрастта на майката, когато детето се ражда). Можете да използвате функцията average дефинирана по-рано в тази глава.

Имайте в предвид, че не всички майки посочени в данните присъстват в масива. Обекта byName може да ви е полезен, понеже прави лесно намирането на обекта на едно лице, само по името му.

function average(array) {
  function plus(a, b) { return a + b; }
  return array.reduce(plus) / array.length;
}

var byName = {};
ancestry.forEach(function(person) {
  byName[person.name] = person;
});

// Your code here.

// → 31.2

Тъй като, не всички елементи в ancestry масива произвеждат полезни данни (не можем да изчислим разликата във възрастта, освен ако не сме сигурни в датата на раждане на майката), ние трябва да приложим filter по някакъв начин преди да извикаме average. Можете да го направите, като първо го подадете, чрез дефиниране на hasKnownMother функция и филтрирате там първо. Алтернативно, можете да извикате map и във вашата mapping функция да върнете разликата във възрастта или null, ако майката не е известна. След това можете да извикате filter за премахване на null елементите преди да подадете масива към average.

Продължителноста на живота исторически

Когато прегледаме всички хора в нашия набор от данни, които са живели повече от 90г., излиза само последното поколение в данните. Нека разгледаме по-подробно това явление.

Изчисляват се изходящите данни на средната възраст на хората в потеклото за един век. Лице се определя на един век, когато годината на смъртта се дели на 100 и се закръгля на горе, както в Math.ceil(person.died / 100).

function average(array) {
  function plus(a, b) { return a + b; }
  return array.reduce(plus) / array.length;
}

// Your code here.

// → 16: 43.5
//   17: 51.2
//   18: 52.8
//   19: 54.8
//   20: 84.7
//   21: 94

Същността на този пример се крие в групирането на елементите от колекцията с някои аспекти на тяхното разделяне в масива на предците, на по-малки масиви с предци за един век.

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

И на края може да използвате for/in цикъл за отпечатване на средната възраст за отделните векове.

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

Всеки и след това някои

Масивите също идват със стандартните методи: every и some. И двете са predicate функции, които когато бъдат извикани с елемент от масив, като аргумент, връщат истина или лъжа. Точно, както && връща истина, само когато всеки от изразите от двете му страни са верни, така и every връща истина, само когато predicate връща вярно за всички елементи в масива. По същия начин, some връща истина, веднага след като, predicate върне вярно за някой от елементите в масива. Те не обработват повече елементи отколкото е необходимо, например, ако some открие, че predicate взема първия елемент на масива за верен, той няма да разглежда стойности след това.

Напишете две функции every и some, които се държат като тези методи с изключение на това, че те вземат масива, като първи аргумент, а не метод.

// Your code here.

console.log(every([NaN, NaN, NaN], isNaN));
// → true
console.log(every([NaN, NaN, 4], isNaN));
// → false
console.log(some([NaN, 3, 4], isNaN));
// → true
console.log(some([2, 3, 4], isNaN));
// → false

Функциите могат да следват подобен модел на поведение, като дефиницията на forEach в началото на тази глава, с изключение на това, че трябва да се върнат незабавно (с правилната стойност), когато predicate функцията връща false или true. Не забравяйте да поставите друго return изявление след цикъла, така че функцията също да връща правилната стойност, когато достигне края на масива.