Марейн Хавербеке - Выразительный JavaScript
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 – одна из самых примитивных подобных функций. В качестве методов массивов нам доступно много других вариантов функций. Для знакомства с ними давайте поиграем с ещё одним набором данных.
Несколько лет назад кто-то обследовал много архивов и сделал целую книгу по истории моей фамилии. Я открыл её, надеясь найти там рыцарей, пиратов и алхимиков… Но оказалось, что она заполнена в основном фламандскими фермерами. Для развлечения я извлёк информацию по моим непосредственным предкам и перевёл в формат, пригодный для чтения компьютером.
Файл выглядит примерно так:
[
{"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"},
… и так далее
]
Этот формат называется JSON, что означает JavaScript Object Notation (разметка объектов JavaScript). Он широко используется в хранении данных и сетевых коммуникациях.
JSON похож на JavaScript по способу записи массивов и объектов – с некоторыми ограничениями. Все имена свойств должны быть заключены в двойные кавычки, а также допускаются только простые величины – никаких вызовов функций, переменных, ничего что включало бы вычисления. Также не допускаются комментарии.
JavaScript предоставляет функции JSON.stringify и JSON.parse, которые преобразовывают данные из этого формата и в этот формат. Первая принимает значение и возвращает строчку с JSON. Вторая принимает такую строчку и возвращает значение.
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 файл в виде строки. Давайте её раскодируем и посчитаем количество упомянутых людей.
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 – это функция, которая производит вычисления проверки. Она вызывается для каждого элемента, а возвращаемое ею значение определяет, попадает ли этот элемент в возвращаемый массив.
В файле оказалось три человека, которые были молоды в 1924 – дедушка, бабушка и двоюродная бабушка.
Обратите внимание, функция filter не удаляет элементы из существующего массива, а строит новый, содержащий только прошедшие проверку элементы. Это чистая функция, потому что она не портит переданный ей массив.
Как и forEach, filter – это один из стандартных методов массива. В примере мы описали такую функцию, только чтобы показать, что она делает внутри. Отныне мы будем использовать её просто:
console.log(ancestry.filter(function(person) {
return person.father == "Carel Haverbeke";
}));
// → [{name: "Carolus Haverbeke", …}]
Преобразования при помощи map
Допустим, есть у нас массив объектов, представляющих людей, который был получен фильтрацией массива предков. Но нам нужен массив имён, который было бы проще прочесть.
Метод map преобразовывает массив, применяя функцию ко всем его элементам и строя новый массив из возвращаемых значений. У нового массива будет та же длина, что у входного, но его содержимое будет преобразовано в новый формат.
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
Другой популярный пример работы с массивами – получение одиночного значения на основе данных в массиве. Один пример – уже знакомое нам суммирование списка номеров. Другой – поиск человека, родившегося раньше всех.
Операция высшего порядка такого типа называется 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, …}
Компонуемость
Как бы мы могли написать предыдущий пример (поиск человека с самой ранней датой рождения) без функций высшего порядка? На самом деле, код не такой уж и ужасный:
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"; }