Статьи

Что они не сказали вам о массивах ES5

Каждая новая версия JavaScript добавляет некоторые дополнительные возможности, которые облегчают программирование. EcmaScript 5 добавил некоторые очень необходимые методы к типу данных Array , и, хотя вы можете найти ресурсы, которые научат вас, как использовать эти методы, они, как правило, не обсуждают использование их с чем-то, кроме скучной пользовательской функции.

Все дополнения массива игнорируют отверстия в массивах.

Новые методы массива, добавленные в ES5, обычно называются дополнительными массивами . Они облегчают процесс работы с массивами, предоставляя методы для выполнения общих операций. Вот почти полный список новых методов:

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 .


Хотя использование встроенных функций в качестве аргументов для методов массива может привести к хорошему синтаксису, вы также должны помнить, что вы можете передать массив в качестве второго аргумента 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 становится не просто адом обратного вызова и кодом спагетти. Осознание этого стало для меня настоящим откровением и повлияло на мой способ написания программ.

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