ECMAScript 5 Часть 2: дополнительные возможности работы с массивами

В прошлый раз в нашей серии, посвященной поддержке IE9 спецификации ES5, мы говорили о новых функциях вроде Object.create и Object.defineProperty, помогающих спроектировать компоненты многократного использования. В этом сообщении мы рассмотрим другой набор новых функций, ориентированных на более базовые возможности: циклы и массивы.

Циклы, обрабатывающие массивы, – одна из самых общих форм программной логики, и программы JavaScript в браузере не являются исключением. Общие задачи поиска узла в DOM, итерирование по записям в объекте JSON с сервера и фильтрация массивов элементов на основании ввода пользователя – все включают итерирование по массивам.

ES5 включает набор новых функций, которые облегчают эти общие задачи и делают их менее подверженными ошибкам. Мы рассмотрим несколько примеров и увидим как эти новые функции позволят использовать более функциональный или декларативный стиль программирования.

Цикл For Each

Вы когда-нибудь пытались написать код JavaScript, подобный следующему?

 var people = ["Bob", "Jane", "Mary", "Chris"];

for(var person in people) {
  processPerson( person );
}

Если да, то вероятно вы заметили, что цикл for…in возвращает индексы элементов массива, а не сами элементы. Вместо этого, программисты JavaScript используют следующий цикл:

 for(var i = 0; i < people.length; i++) {
  processPerson(people[i]);
}

Это очень общий шаблон и ES5 вводит новую функцию Array – Array.prototype.forEach, – чтобы сделать цикл действительно простым:

 people.forEach(processPerson);

Использование forEach делает код немного короче, но есть и другие важные причины, по которым он зачастую является лучшим выбором. Во-первых, вам не надо самостоятельно писать логику перебора элементов, которая предоставляет некоторые дополнительные возможности для внесения ошибок, и в этом случае меньше шансов на случайную ошибку диапазона или неверное вычисление длины. Во-вторых, цикл forEach автоматически управляет массивами с пустыми элементами, опуская индексы, у которых нет значений. И в-третьих, помещая тело цикла внутри функции, можно быть уверенным, что любая определенная переменная будет иметь область видимости лишь внутри тела цикла, уменьшая риск конфликтов переменных цикла с остальными переменными в коде.

Функция forEach дает имя общему шаблону и обеспечивает удобный способ перебора элементов массива. С некоторыми примерами использования forEach можно познакомиться в игре ES5 Tile Switch Game на сайте Internet Explorer Test Drive.

Другие общие циклы

Существуют и другие общие шаблоны циклической переборки массивов. Вот один из них, с которым вы возможно, встречались:

 var newData = []
for(var i = 0; i < data.length; i++) {
  var value = data[i];
  var newValue = processValue(value);
  newData.push(newValue);
}

Здесь мы создаем новый массив путем преобразования каждого элемента существующего массива. Опять, здесь много стандартной логики. Используя функцию ES5 Array.prototype.map, мы можем написать это гораздо проще:

 var newData = data.map(processValue)

Вот другой общий шаблон:

 var i;
for(i = data.Length - 1; i >= 0; i--) {
  if(data[i] === searchValue) 
    break;
}

Код такого типа используется при поиске последнего элемента с заданным значением в массиве – в этом случае поиск осуществляется от конца массива. Используя функцию ES5 Array.prototype.lastIndexOf, мы можем выразить это следующим образом:

 var i = data.lastIndexOf(searchValue)

Общая мысль здесь следующая – существует ряд общих шаблонов при переборке элементов массивов и новые функции ES5 Array делают это проще, более эффективно и более надежно.

Объединение

Другим общим способом использования массивов является сведение данных массива в одно значение. Наиболее частым примером этого процесса является вычисление суммы, среднего значения и превращение списка в красиво отформатированную строку.

Пример ниже перебирает список для вычисления суммы его элементов:

 var sum = 0;
for(int i = 0; i < data.length; i++) {
  sum = sum + data[i];
}

Используя новую функцию ES5 Array.prototype.reduce не нужно следить за индексом цикла и можно проводить объединительные операции по целому массиву.

 var sum = data.reduce(function(soFar, next) { return soFar + next; })

В этом примере, передаваемая в reduce функция вызывается по одному разу для каждого элемента массива и каждый раз ей передается накопленная сумма вместе со следующим элементом.

Функция reduce может иметь дополнительный параметр в качестве начального значения для объединения. Этот необязательный параметр также можно использовать для отслеживания многих состояний. В следующем примере вычисляется среднее значение по массиву чисел:

 var result = 
  data.reduce(function(soFar, next) { 
    return { total: soFar.total + next, 
             count: soFar.count + 1   }; 
    },
    {total: 0, count: 0 }
  );

var mean = result.total / result.count;

Для работы с массивом в порядке убывания индексов, от последнего элемента к первому, используйте функцию Array.prototype.reduceRight.

Еще одной формой объединительной операции с массивом является проверка того, что каждый элемент массива (или хотя бы один) удовлетворяет заданному требованию. Простой способ проверки этих условий обеспечивают функции Array.prototype.every и Array.prototype.some. Важной характеристикой этих функций является их способность завершить работу, не перебирая все элементы, если результат выполнения цикла будет ясен досрочно.

 var allNamesStartWithJ = 
  data.every(function(person) { return person.name[0] === 'J'; })

Как и в предыдущих примерах функции reduce, reduceRight, every и some делают конструирование циклов, перебирающих списки, более простым и менее подверженным ошибкам, и, в тоже время, позволяют строить сложные композиции, как мы увидим далее.

Собирая вместе

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

Далее идет пример совместного использования функций filter, map и reduce. Представьте, что мы создаем сайт, обрабатывающий записи транзакций из кассового аппарата. Эти записи транзакций могут содержать входные данные, разделенные запятыми, указывающие на покупку (‘P’), возвращение денег (‘R’) или отмену транзакции (‘C’). Данные могут выглядеть следующим образом:

 var transactions = "P 130.56, C, P 12.37 , P 6.00, R 75.53, P 1.32"

Мы можем вычислить сумму всех покупок, комбинируя некоторые функции обработки массивов, как показано ниже:

 transactions
  // Break the string into an array on commas
  .split(",")
  // Keep just the purchase transactions ('P')
  .filter(function(s) { return s.trim()[0] === 'P' })
  // Get the price associated with the purchase
  .map   (function(s) { return Number(s.trim().substring(1).trim()) })
  // Sum up the quantities of purchases
  .reduce(function(acc, v) { return acc + v; });

Такой стиль программирования обычно называется функциональным программированием и является типовым для языков, подобных Lisp и Scheme, оба из которых оказали влияние на исходный дизайн JavaScript. Этот стиль также связан с концепциями, заложенными в Language Integrated Query (LINQ) в .NET и в модель Map-Reduce для обработки и генерации очень больших наборов данных в распределенных вычислениях.

Новые функции обработки массивов в ES5

В целом добавлено девять новых функций для поиска и обработки массивов в ES5, и все они поддерживаются в IE9:

Каждая из этих функций также поддерживает добавочный необязательный параметр, не показанный выше, который увеличивает ее гибкость. Кроме того, эти функции можно использовать не только с массивами, но и с любыми другими JavaScript-объектами, имеющими целочисленный индекс и свойство length (длина).

Эти функции работы с массивами можно самостоятельно попробовать в демонстрационном примере ECMAScript 5 Arraysна сайте IE9 Test Drive.

Функции, описанные в данном сообщении, представляют собой один из способов использования ES5 для того, чтобы писать более простой код, который можно применять повторно. Помните, чтобы воспользоваться этими функциями браузер необходимо перевести в режим IE9 Standards Document Mode, который является режимом по умолчанию в IE9 для стандартных документов. Мы будем рады видеть, как разработчики будут использовать возможности ES5 в IE9. Мы хотели бы услышать, как вы планируете использовать эти возможности. В следующих сообщениях мы продолжим рассматривать добавления в ES5. Оставайтесь с нами!

Люк Хобан (Luke Hoban)

Руководитель проекта JavaScript