Каждая новая версия JavaScript добавляет некоторые дополнительные возможности, которые облегчают программирование. EcmaScript 5 добавил некоторые очень необходимые методы к типу данных Array
, и, хотя вы можете найти ресурсы, которые научат вас, как использовать эти методы, они, как правило, не обсуждают использование их с чем-то, кроме скучной пользовательской функции.
Все дополнения массива игнорируют отверстия в массивах.
Новые методы массива, добавленные в ES5, обычно называются дополнительными массивами . Они облегчают процесс работы с массивами, предоставляя методы для выполнения общих операций. Вот почти полный список новых методов:
-
Array.prototype.map
-
Array.prototype.reduce
-
Array.prototype.reduceRight
-
Array.prototype.filter
-
Array.prototype.forEach
-
Array.prototype.every
-
Array.prototype.some
Array.prototype.indexOf
и Array.prototype.lastIndexOf
также являются частью этого списка, но в этом руководстве будут обсуждаться только семь вышеуказанных методов.
Что они сказали тебе
Эти методы довольно просты в использовании. Они выполняют функцию, которую вы указали в качестве первого аргумента для каждого элемента в массиве. Как правило, предоставляемая функция должна иметь три параметра: элемент, индекс элемента и весь массив. Вот несколько примеров:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
|
[1, 2, 3].map(function(elem, index, arr){
return elem * elem;
});
//returns [1, 4, 9]
[1, 2, 3, 4, 5].filter(function(elem, index, arr){
return elem % 2 === 0;
});
//returns [2, 4]
[1, 2, 3, 4, 5].some(function(elem, index, arr){
return elem >= 3;
});
//returns true
[1, 2, 3, 4, 5].every(function(elem, index, arr){
return elem >= 3;
});
//returns false
|
Методы reduceRight
и reduceRight
имеют другой список параметров. Как следует из их имен, они сводят массив к одному значению. Начальным значением результата по умолчанию является первый элемент в массиве, но вы можете передать второй аргумент этим методам, чтобы он служил в качестве начального значения.
Функция обратного вызова для этих методов принимает четыре аргумента. Текущее состояние является первым аргументом, а остальные аргументы — это элемент, индекс и массив. Следующие фрагменты демонстрируют использование этих двух методов:
1
2
3
4
5
6
7
8
9
|
[1, 2, 3, 4, 5].reduce(function(sum, elem, index, arr){
return sum + elem;
});
//returns 15
[1, 2, 3, 4, 5].reduce(function(sum, elem, index, arr){
return sum + elem;
}, 10);
//returns 25
|
Но вы, наверное, уже знали все это, не так ли? Итак, давайте перейдем к тому, с чем вы, возможно, не знакомы.
Функциональное программирование на помощь
Удивительно, что все больше людей не знают этого: вам не нужно создавать новую функцию и передавать ее .map()
и друзьям. Более того, вы можете передавать встроенные функции, такие как parseFloat
без использования оболочки!
1
|
[«1», «2», «3», «4»].map(parseFloat);
|
Обратите внимание, что некоторые функции не будут работать должным образом. Например, parseInt
принимает основание в качестве второго аргумента. Теперь помните, что индекс элемента передается функции в качестве второго аргумента. Так что же вернет следующее?
1
|
[«1», «2», «3», «4»].map(parseInt);
|
Точно: [1, NaN, NaN, NaN]
. В качестве объяснения: база 0 игнорируется; Итак, первое значение анализируется, как и ожидалось. Следующие базы не включают число, переданное в качестве первого аргумента (например, база 2 не включает 3), что приводит к NaN
. Поэтому обязательно проверяйте Mozilla Developer Network заранее, прежде чем использовать функцию, и все будет в порядке.
Совет : вы даже можете использовать встроенные конструкторы в качестве аргументов, так как они не должны вызываться с new
. В результате простое преобразование в логическое значение может быть выполнено с использованием Boolean
, например так:
1
|
[«yes», 0, «no», «», «true», «false»].filter(Boolean);
|
Несколько других приятных функций — это encodeURIComponent
, Date.parse
(обратите внимание, что вы не можете использовать конструктор Date
поскольку он всегда возвращает текущую дату при вызове без new
), Array.isArray
и JSON.parse
.
Не забудьте .apply()
Хотя использование встроенных функций в качестве аргументов для методов массива может привести к хорошему синтаксису, вы также должны помнить, что вы можете передать массив в качестве второго аргумента Function.prototype.apply
. Это удобно при вызове методов, таких как Math.max
или String.fromCharCode
. Обе функции принимают переменное число аргументов, поэтому вам нужно будет обернуть их в функцию при использовании дополнительных функций массива. Так что вместо:
1
2
3
4
5
|
var arr = [1, 2, 4, 5, 3];
var max = arr.reduce(function(a, b) {
return Math.max(a, b);
});
|
Вы можете написать следующее:
1
2
3
|
var arr = [1, 2, 4, 5, 3];
var max = Math.max.apply(null, arr);
|
Этот код также имеет хороший выигрыш в производительности. Как примечание: в EcmaScript 6 вы сможете просто написать:
1
2
|
var arr = [1, 2, 4, 5, 3];
var max = Math.max(…arr);
|
Массивы без отверстий
Все дополнения массива игнорируют отверстия в массивах. Пример:
1
2
3
4
5
|
var a = [«hello», , , , , «world»];
var count = a.reduce(function(count){ return count + 1; }, 0);
console.log(count);
|
Такое поведение, вероятно, дает выигрыш в производительности, но бывают случаи, когда это может быть настоящей болью в заднице. Одним из таких примеров может быть случай, когда вам нужен массив случайных чисел; невозможно просто написать это:
1
|
var randomNums = new Array(5).map(Math.random);
|
Но помните, что вы можете вызывать все нативные конструкторы без new
. И еще одна полезная новость: Function.prototype.apply
не игнорирует дыры. Сочетая их, этот код возвращает правильный результат:
1
|
var randomNums = Array.apply(null, new Array(5)).map(Math.random);
|
Неизвестный второй аргумент
Большинство из вышеперечисленного известно и используется многими программистами на регулярной основе. То, что большинство из них не знает (или, по крайней мере, не использует), является вторым аргументом большинства дополнительных функций массива (только функции reduce*
не поддерживают его).
Используя второй аргумент, вы можете передать значение this
функции. В результате вы можете использовать методы- prototype
. Например, фильтрация массива с помощью регулярного выражения становится однострочной:
1
2
|
[«foo», «bar», «baz»].filter(RegExp.prototype.test, /^b/);
//returns [«bar», «baz»]
|
Кроме того, проверка, имеет ли объект определенные свойства, становится проще:
1
2
|
[«foo», «isArray», «create»].some(Object.prototype.hasOwnProperty, Object);
//returns true (because of Object.create)
|
В конце концов, вы можете использовать любой метод, который хотели бы:
1
2
3
4
5
6
7
|
//lets do something crazy
[
function(a) { return a * a;
function(b) { return b * b * b;
]
.map(Array.prototype.map, [1, 2, 3]);
//returns [[1, 4, 9], [1, 8, 27]]
|
Это становится безумным при использовании Function.prototype.call
. Смотри:
1
2
3
4
5
|
[» foo «, «\n\tbar», «\r\nbaz\t «].map(Function.prototype.call, String.prototype.trim);
//returns [«foo», «bar», «baz»]
[true, 0, null, []].map(Function.prototype.call, Object.prototype.toString);
//returns [«[object Boolean]», «[object Number]», «[object Null]», «[object Array]»]
|
Конечно, чтобы порадовать своего внутреннего гика, вы также можете использовать Function.prototype.call
в качестве второго параметра. При этом каждый элемент массива вызывается с его индексом в качестве первого аргумента и целым массивом в качестве второго:
1
2
3
|
[function(index, arr){
//whatever you might want to do with it
}].forEach(Function.prototype.call, Function.prototype.call);
|
Позволяет построить что-то полезное
Учитывая все сказанное, давайте создадим простой калькулятор. Мы хотим поддерживать только базовые операторы ( +
, -
, *
, /
), и нам нужно соблюдать процедуру оператора. Таким образом, умножение ( *
) и деление ( /
) необходимо оценить перед сложением ( +
) и вычитанием ( -
).
Во-первых, мы определяем функцию, которая принимает строку, представляющую вычисление, как первый и единственный аргумент.
1
|
function calculate (calculation) {
|
В теле функции мы начинаем преобразовывать вычисление в массив с помощью регулярного выражения. Затем мы гарантируем, что проанализировали весь расчет, соединив части с помощью Array.prototype.join
и сравнив результат с исходным расчетом.
1
2
3
4
5
6
7
8
|
var parts = calculation.match(
// digits |operators|whitespace
/(?:\-?[\d\.]+)|[-\+\*\/]|\s+/g
);
if( calculation !== parts.join(«») ) {
throw new Error(«couldn’t parse calculation»)
}
|
После этого мы вызываем String.prototype.trim
для каждого элемента, чтобы устранить пробелы. Затем мы фильтруем массив и удаляем фальшивые элементы (т. Е. F пустых строк).
1
2
|
parts = parts.map(Function.prototype.call, String.prototype.trim);
parts = parts.filter(Boolean);
|
Теперь мы создаем отдельный массив, который содержит проанализированные числа.
1
|
var nums = parts.map(parseFloat);
|
Вы можете передавать встроенные функции, такие как
parseFloat
без использования оболочки!
На данный момент самый простой способ продолжить это простой -loop. В нем мы создаем еще один массив (названный processed
) с уже примененными умножением и делением. Основная идея состоит в том, чтобы свести каждую операцию к сложению, чтобы последний шаг стал довольно тривиальным.
Мы проверяем каждый элемент массива nums
чтобы убедиться, что это не NaN
; если это не число, то это оператор. Самый простой способ сделать это — воспользоваться тем, что в JavaScript NaN !== NaN
. Когда мы находим число, мы добавляем его в массив результатов. Когда мы находим оператора, мы применяем его. Мы пропускаем операции сложения и меняем только знак следующего числа для вычитания.
Умножение и деление должны быть рассчитаны с использованием двух окружающих чисел. Поскольку мы уже добавили предыдущее число к массиву, его необходимо удалить с помощью Array.prototype.pop
. Результат расчета добавляется в массив результатов и готов к добавлению.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
var processed = [];
for(var i = 0; i < parts.length; i++){
if( nums[i] === nums[i] ){
processed.push( nums[i] );
} else {
switch( parts[i] ) {
case «+»:
continue;
case «-«:
processed.push(nums[++i] * -1);
break;
case «*»:
processed.push(processed.pop() * nums[++i]);
break;
case «/»:
processed.push(processed.pop() / nums[++i]);
break;
default:
throw new Error(«unknown operation: » + parts[i]);
}
}
}
|
Последний шаг довольно прост: мы просто добавляем все числа и возвращаем наш конечный результат.
1
2
3
|
return processed.reduce(function(result, elem){
return result + elem;
});
|
Завершенная функция должна выглядеть так:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
|
function calculate (calculation) {
//build an array containing the individual parts
var parts = calculation.match(
// digits |operators|whitespace
/(?:\-?[\d\.]+)|[-\+\*\/]|\s+/g
);
//test if everything was matched
if( calculation !== parts.join(«») ) {
throw new Error(«couldn’t parse calculation»)
}
//remove all whitespace
parts = parts.map(Function.prototype.call, String.prototype.trim);
parts = parts.filter(Boolean);
//build a separate array containing parsed numbers
var nums = parts.map(parseFloat);
//build another array with all operations reduced to additions
var processed = [];
for(var i = 0; i < parts.length; i++){
if( nums[i] === nums[i] ){ //nums[i] isn’t NaN
processed.push( nums[i] );
} else {
switch( parts[i] ) {
case «+»:
continue;
case «-«:
processed.push(nums[++i] * -1);
break;
case «*»:
processed.push(processed.pop() * nums[++i]);
break;
case «/»:
processed.push(processed.pop() / nums[++i]);
break;
default:
throw new Error(«unknown operation: » + parts[i]);
}
}
}
//add all numbers and return the result
return processed.reduce(function(result, elem){
return result + elem;
});
}
|
Хорошо, давайте проверим это:
1
2
|
calculate(» 2 + 2.5 * 2 «) // returns 7
calculate(«12 / 6 + 4 * 3») // returns 14
|
Похоже, работает! Есть еще некоторые крайние случаи, которые не обрабатываются, такие как вычисления с оператором или числа, содержащие несколько точек. Поддержка скобок была бы хорошей, но мы не будем беспокоиться о том, чтобы углубиться в подробности этого простого примера.
Завершение
Хотя дополнения к массиву ES5 на первый взгляд могут показаться довольно тривиальными, они раскрывают немного глубины, как только вы дадите им шанс. Внезапно функциональное программирование на JavaScript становится не просто адом обратного вызова и кодом спагетти. Осознание этого стало для меня настоящим откровением и повлияло на мой способ написания программ.
Конечно, как показано выше, всегда есть случаи, когда вы хотите вместо этого использовать обычный цикл. Но, и это хорошая часть, вам не нужно.