Массивы

Массив представляет собой последовательность элементов, каждый из которых имеет значение любого типа. Массив можно определить, поместив список значений в скобки ([]):

> let myArray = [2, 'Pete', 2.99, 'another string']

В этом примере подчеркивается неоднородность массивов; myArray содержит числовые и текстовые значения. Массивы могут содержать что угодно, включая объекты и другие массивы. Каждый элемент в массиве имеет уникальный порядковый номер, указывающий его положение в массиве. В результате массивы представляют собой как индексированные списки, так и отсортированные списки. На элемент массива можно ссылаться по его порядковому номеру. Номер индекса помещается в скобки ([]) после имени или значения массива. Например, мы можем получить первый член myArray следующим образом:

> myArray[0]
= 2
/* An array's initial entry is always at index 0. As a result, the second element is at index 1, the third element is at index 2, and so on. To get to the third member of an array, type: */
> myArray[2]
= 2.99

Зная, что myArray состоит из четырех элементов, легко понять, что myArray[3] возвращает четвертый. Предположим, вы не знаете, сколько элементов в массиве. Как добраться до последнего элемента? Это не сложно, но немного тяжело. Атрибут длины массива выполняет трюк:

> myArray[myArray.length - 1]
= 'another string'

Мы должны использовать имя массива дважды, и поскольку мы должны учитывать элемент с индексом 0, мы должны вычесть 1 из длины, чтобы получить индекс последнего элемента. Поначалу индексация с отсчетом от нуля кажется необычной, усложняя математику, но как только вы к ней привыкнете — что требует практики — она быстро станет вашей второй натурой.

Изменение массивов

Массивы были бы бесполезны, если бы мы не могли их изменить. Требуется возможность добавлять, удалять и заменять элементы. Этим действиям помогает оператор [] и несколько функций массива.

Замена и добавление элементов с помощью []

/* To replace an element of an array, use brackets ([]) with the assignment operator:*/
> let array = [1, 2, 3]
> array[1] = 4
= 4
> array
= [ 1, 4, 3 ]
/* You can also use [] to add new elements to an array: */
> array[array.length] = 10
= 10
> array
= [ 1, 4, 3, 10 ]

Поскольку предыдущий конечный элемент находился в массиве[массив.длина — 1], массив[массив.длина] = значение добавляет новое значение в конец массива. Стоит отметить, что переменные, объявленные с помощью const и инициализированные массивом, ведут себя странно; хотя вы не можете изменить массив, на который ссылается переменная, вы можете изменить содержимое массива:

> const MyArray = [1, 2, 3]
> MyArray[1] = 5
> MyArray
= [1, 5, 3]
> MyArray = [4, 5, 6] // Uncaught TypeError: Assignment to constant variable.

Такое поведение может вызвать недоумение. Это происходит от понятия «переменные как указатели», к которому мы вернемся позже. Объявление const, по сути, запрещает изменять элемент, на который указывает const, но не запрещает изменять содержимое этой вещи. Таким образом, мы можем изменить один элемент в массиве const, но не массив, на который указывает const. Если вы хотите, чтобы элементы массива также были константными, вы можете использовать метод Object.freeze:

> const MyArray = Object.freeze([1, 2, 3])
> MyArray[1] = 5
> MyArray
= [1, 2, 3]
/* Take note of how the assignment on line 2 had no effect on the array's content. It is critical to understand that Object.freeze only operates one level deep in the array. If your array includes nested arrays or other objects, their values can still be modified unless they are likewise frozen:*/
> const MyArray = Object.freeze([1, 2, 3, [4, 5, 6]])
> MyArray[3][1] = 0
> MyArray
= [1, 2, 3, [4, 0, 6]]
/* You must freeze the sub-array if you want it to be constant as well: */
> const MyArray = Object.freeze([1, 2, 3, Object.freeze([4, 5, 6])])
> MyArray[3][1] = 0
> MyArray
= [1, 2, 3, [4, 5, 6]]

Добавление элементов с помощью push

Функция push добавляет в конец один или несколько элементов массива:

> array.push('a')
= 5               // the new length of the array
> array
= [ 1, 4, 3, 10, 'a' ]
> array.push(null, 'xyz')
= 7
> array
= [ 1, 4, 3, 10, 'a', null, 'xyz' ]

Функция push добавляет свои параметры к вызывающей стороне (массиву), вызывая изменение вызывающей стороны. Затем возвращается обновленная длина массива. Помните, что методы и функции выполняют действия и возвращают значения. Вы должны быть осторожны, чтобы различать между ними. Push добавляет элементы в конец массива вызывающего объекта, но возвращает обновленную длину массива. Важно отметить, что он не возвращает измененный массив! Новые программисты на JavaScript часто сбиты с толку этим различием и тратят часы на выяснение того, почему функция не возвращает ожидаемое значение. Если у вас есть какие-либо сомнения, обратитесь к документации.

Добавление элементов с помощью concat

Concat похож на push; однако это не меняет вызывающего абонента. Он объединяет два массива и создает новый массив со всеми элементами исходного массива, за которыми следуют все переданные ему параметры:

> array.concat(42, 'abc')
= [ 1, 4, 3, 10, 'a', null, 'xyz', 42, 'abc' ]
> array
= [ 1, 4, 3, 10, 'a', null, 'xyz' ]

Удаление элементов с помощью pop

Поп — это обратная сторона толчка. В то время как push добавляет элемент в конец массива, pop удаляет и возвращает последний элемент массива:

> array.pop()
= 'xyz'            // removed element value
> array
= [ 1, 4, 3, 10, 'a', null ]
// pop mutates the caller.

Удаление элементов с помощью splice

Функция splice позволяет вам удалить один или несколько элементов из массива, даже если они не находятся в конце:

> array.splice(3, 2)
[ 10, 'a' ]
> array
= [ 1, 4, 3, null ]

В этом примере мы удаляем два компонента, начиная с позиции индекса три. splice изменяет исходный массив и возвращает новый, включая исключенные элементы.

Методы итерации

Итерация с forEach

JavaScript включает несколько встроенных методов для перебора элементов массива. Функция forEach была представлена ​​в главе «Циклы и итерации». Это простой подход к перебору массивов; стандарты стиля часто предпочитают его циклу for. Для ForEach требуется функция обратного вызова, которая передается в качестве входных данных для forEach. Функция обратного вызова — это функция, которая передается в качестве аргумента другой функции. Когда вызываемая функция выполняется, она вызывает функцию обратного вызова. Метод forEach вызывает свой обратный вызов один раз для каждого элемента, передавая его значение в качестве параметра. forEach каждый раз возвращает undefined.

let array = [1, 2, 3];
array.forEach(function(num) {
  console.log(num); // on first iteration  => 1
                    // on second iteration => 2
                    // on third iteration  => 3
}); // returns `undefined`

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

let array = [1, 2, 3];
array.forEach(num => console.log(num));
// We can also perform more complex operations:
let array = [1, 2, 3];
array.forEach(num => console.log(num + 2)); // on first iteration  => 3
                                            // on second iteration => 4
                                            // on third iteration  => 5

Преобразование массивов с помощью map

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

> let numbers = [1, 2, 3, 4]
> let squares = [];
> numbers.forEach(num => squares.push(num * num));
> squares
= [ 1, 4, 9, 16 ]
> numbers
= [ 1, 2, 3, 4 ]

Это работает нормально, но обратный вызов теперь имеет непреднамеренный побочный эффект: он обновляет переменную Squares, которая не является частью метода обратного вызова. Иногда побочные эффекты вызывают проблемы. Подумайте, что произойдет, если вы повторите вызов forEach после выполнения приведенного выше кода:

> numbers.forEach(num => squares.push(num * num));
> squares
= [ 1, 4, 9, 16, 1, 4, 9, 16 ]
/* Because we failed to reset squares to an empty array, we now have two copies of the squares. This issue is handled more cleanly using the map method: */
> let numbers = [1, 2, 3, 4]
> let squares = numbers.map(num => num * num);
> squares
= [ 1, 4, 9, 16 ]
> squares = numbers.map(num => num * num);
= [ 1, 4, 9, 16 ]

Первые четыре строки этого кода дают тот же результат, что и в предыдущем примере forEach. Однако map создает новый массив с одним членом для каждого элемента в числах, где каждый элемент установлен в значение, возвращаемое обратным вызовом: в этом примере квадраты чисел. Этот код короче кода forEach и не имеет побочных эффектов; обратный вызов не изменяет квадраты (меняет возвращаемое значение карты), и нам не нужно сбрасывать переменную, если мы снова запустим тот же код. forEach и map — полезные подходы, но они могут запутать новичков. Основное отличие состоит в том, что forEach выполняет базовую итерацию и возвращает неопределенное значение, тогда как map изменяет элементы массива и возвращает новый массив с измененными значениями.

Фильтрация массивов с помощью filter

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

> let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2]
> numbers.filter(num => num > 4)
= [ 5, 6, 7, 8, 9, 10 ]
> numbers
= [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2 ]

filter перебирает элементы массива. Он вызывает функцию обратного вызова на каждой итерации, передавая значение текущего элемента в качестве аргумента. Если обратный вызов возвращает истинное значение, значение элемента добавляется к новому массиву. В противном случае он игнорирует значение элемента и ничего не делает. Когда фильтр завершает итерацию, он доставляет массив выбранных элементов: тех, для которых обратный вызов возвращает истинный результат. В этом случае фильтр выбирает все компоненты со значением больше 4. Фильтр не изменяет вызывающий объект.

Создание новых значений из массивов с помощью reduce

Метод сокращения эффективно уменьшает содержимое массива до одного значения. Это один из самых сложных для понимания алгоритмов итерации массива, но он также и один из самых важных. Уменьшение может использоваться для создания функций forEach, map и filter, а также множества других итерационных методов, указанных для массивов. reduce принимает два аргумента: функцию обратного вызова и значение, которое устанавливает аккумулятор в ноль. В своей самой простой форме функция обратного вызова принимает два аргумента: текущее значение аккумулятора и элемент массива. Он возвращает значение, которое будет использоваться в качестве аккумулятора при следующем вызове обратного вызова. Это кажется сложнее, чем есть на самом деле, поэтому давайте рассмотрим два простых варианта использования сокращения:

> let arr = [2, 3, 5, 7]
> arr.reduce((accumulator, element) => accumulator + element, 0)
= 17
> arr.reduce((accumulator, element) => accumulator * element, 1)
= 210

Первый вызов вычисляет сумму всех значений массива, например, 2 + 3 + 5 + 7. Для начала мы устанавливаем аккумулятор равным 0. В результате при первом вызове метода обратного вызова аккумулятор 0, а элемент равен 2. Когда мы снова вызываем обратный вызов, на этот раз с элементом 3, он возвращает 2, которое становится новым значением аккумулятора. В свою очередь, этот вызов дает 5. Эта процедура повторяется до тех пор, пока окончательное возвращаемое значение не будет равно 17. Второй вызов reduce вычисляет произведение целых чисел массива (2 * 3 * 5 * 7), где 1 является аккумулятором. (Если бы мы начали с 0, конечным возвращаемым значением был бы 0, потому что 0, умноженное на что-либо, равно 0.) Функция редукции предназначена не только для вычисления целых чисел; он также может вычислять тексты, объекты и массивы. Вот пример со строками:

> let strings = ['a', 'b', 'c', 'd']
> strings.reduce((accumulator, element) => {
...   return accumulator + element.toUpperCase()
... }, '');
= 'ABCD'

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

Массивы могут быть нечетными

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

  1. Вы уже заметили одну странность: индексы начинаются с 0. В этом нет ничего необычного, поскольку большинство компьютерных языков используют индексы, начинающиеся с нуля. Однако студенты часто допускают ошибки при использовании массивов.
  2. Свойство length всегда возвращает значение, на единицу большее, чем наиболее часто используемая индексная точка массива. Например, если один элемент находится в позиции индекса 124 и нет дополнительных элементов с более высокими значениями индекса, длина массива равна 125.
  3. Массивы — это объекты. В результате при применении к массиву оператор typeof не возвращает «массив»:
> let arr = [1, 2, 3]
> typeof arr
= 'object'
/* If you really need to detect whether a variable references an array, you need to use Array.isArray instead: */
> let arr = [1, 2, 3]
> Array.isArray(arr)
= true

4. Когда вы изменяете свойство длины массива на новое, более низкое значение, массив усекается; JavaScript удаляет все элементы после нового последнего элемента.

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

> let arr = []
> arr.length = 3
> arr
= [ <3 empty items> ]
> arr[0]
= undefined
> arr.filter(element => element === undefined)
= []
> arr.forEach(element => console.log(element)) // no output
= undefined
> arr[1] = 3
> arr
= [ <1 empty item>, 3, <1 empty item> ]
> arr.length
= 3
> arr.forEach(element => console.log(element))
= 3
= undefined
> Object.keys(arr)
= ['1']
/* Unset array members are seen as missing by JavaScript in general, although the length property includes them. */

6. Вы можете создавать «элементы» массива с отрицательными, нецелыми или даже нечисловыми индексами:

> arr = [1, 2, 3]
= [ 1, 2, 3 ]
> arr[-3] = 4
= 4
> arr
= [ 1, 2, 3, '-3': 4 ]
> arr[3.1415] = 'pi'
= 'pi'
> arr["cat"] = 'Fluffy'
= 'Fluffy'
> arr
= [ 1, 2, 3, '-3': 4, '3.1415': 'pi', cat: 'Fluffy' ]
/* These "elements" aren't genuine elements; they're array object characteristics that we'll go over later. Only index values (0, 1, 2, 3, and so on) contribute to the array's length. */
> arr.length
= 3

7. Поскольку массивы являются объектами, функция Object.keys может использоваться для возврата ключей массива (значений его индекса) в виде массива строк. Отрицательные индексы, нецелые или нечисловые, также включены.

> arr = [1, 2, 3]
> arr[-3] = 4
> arr[3.1415] = 'pi'
> arr["cat"] = 'Fluffy'
> arr
= [ 1, 2, 3, '-3': 4, '3.1415': 'pi', cat: 'Fluffy' ]
> Object.keys(arr)
= [ '0', '1', '2', '-3', '3.1415', 'cat' ]
/* One quirk of this method is that it treats unset values in arrays differently from those that merely have a value of undefined. Unset values are created when there are "gaps" in the array; they show up as empty items until you try to use their value: */
> let a = new Array(3);
> a
= [ <3 empty items> ]
> a[0] === undefined;
= true
> let b = [];
> b.length = 3;
> b
= [ <3 empty items> ]
> b[0] === undefined;
= true
> let c = [undefined, undefined, undefined]
> c
= [ undefined, undefined, undefined ]
> c[0] === undefined;
= true
/* Unlike the length property of Array, which counts unset values, Object.keys only counts items that have been set to a value: */
> let aKeys = Object.keys(a)
> a.length
= 3
> aKeys.length;
= 0
> let bKeys = Object.keys(b)
> b.length
= 3
> bKeys.length;
= 0
> let cKeys = Object.keys(c)
> c.length
= 3
> cKeys.length;
= 3
/* It is worthwhile to devote some time and effort to memorizing these habits. */

Вложенные массивы

Элементы массива могут содержать что угодно, даже другие массивы. Вы можете создавать массивы с массивами внутри них, а также массивы внутри этих внутренних массивов. Предположим, вы хотите отслеживать все команды, соревнующиеся в смешанных парных соревнованиях по теннису. Вы можете сделать такой массив.

> let teams = [['Joe', 'Jennifer'], ['Frank', 'Molly'], ['Dan', 'Sarah']]
// You can now find the teams by index.
> teams[2]
= [ 'Dan', 'Sarah' ]
/* If you want to retrieve the second element of teams[2], you can append [1] to the expression: */
> teams[2][1]
= 'Sarah'

Равенство массивов

Как вы думаете, что вернет следующая фраза?

> [1, 2, 3] === [1, 2, 3]
/* It returns true, which is a sensible answer. After all, the arrays appear to be similar. However, JavaScript is not appropriate in this case: the expression returns false. */
> [1, 2, 3] === [1, 2, 3]
= false
How about the following comparison?
> let a = [1, 2, 3]
> let b = a
> a === b

Удивительно, но эта скорость сравнения верна. Что тут происходит? JavaScript считает два массива равными, только если они являются одним и тем же массивом: они должны занимать одну и ту же ячейку памяти. Это правило применяется ко всем объектам JavaScript; объекты должны быть одинаковыми. В результате второй пример возвращает yes, а первый возвращает false. Когда a присваивается b, b ссылается на тот же массив, что и a; он не генерирует новый массив.

На первый взгляд массивы в первом примере кажутся «одним и тем же массивом», но это не так. Это два разных массива, которые содержат одинаковые значения. Однако, поскольку они занимают разные места в памяти, они не являются одним и тем же массивом и, следовательно, не равны. Учитывая такое поведение, как узнать, содержат ли два массива одинаковые элементы? Одна из возможностей — написать функцию, которая сравнивает элементы одного массива с элементами другого:

function arraysEqual(arr1, arr2) {
  if (arr1.length !== arr2.length) return false;
for (let i = 0; i < arr1.length; i += 1) {
    if (arr1[i] !== arr2[i]) {
      return false;
    }
  }
return true;
}
console.log(arraysEqual([1, 2, 3], [1, 2, 3]));    // => true
console.log(arraysEqual([1, 2, 3], [4, 5, 6]));    // => false
console.log(arraysEqual([1, 2, 3], [1, 2, 3, 4])); // => false

arraysEqual сравнивает два массива и возвращает false, если элемент в одном массиве не соответствует элементу в другом. Если нет, он возвращает true. Если массивы имеют разную длину, сразу возвращается false; работа с этим сценарием в первую очередь упрощает остальную часть процедуры. arraysEqual работает лучше всего, когда элементы обоих массивов являются примитивными значениями. arraysEqual может не дать ожидаемого результата, если любая пара элементов имеет не примитивное значение (массив или объект):

> arraysEqual([1, 2, [3, 4], 5], [1, 2, [3, 4], 5])
= false

Другие методы массива

включает

/* The includes method checks whether an array contains a given element: */
> let a = [1, 2, 3, 4, 5]
> a.includes(2)
= true
> a.includes(10)
= false

Сортировать

/* The sort method is a convenient technique to reorder the elements of an array in order. It yields a sorted array. */
> let a = ["e", "c", "h", "b", "d", "a"]
> a.sort()
= [ 'a', 'b', 'c', 'd', 'e', 'h' ]

кусочек

/* The slice method, unlike the splice method, extracts and returns a subset of the array. There are two optional arguments. The first is the index at which extraction begins, and the second is the index at which extraction concludes: */
> let fruits = ['mango', 'orange', 'banana', 'pear', 'apple']
> fruits.slice(1, 3)
= [ 'orange', 'banana' ]
> fruits.slice(2) // second argument defaults to rest of array
= [ 'banana', 'pear', 'apple' ]
> fruits.slice() // no arguments duplicates the array
= [ 'mango', 'orange', 'banana', 'pear', 'apple' ]
/* If the second parameter is omitted, slice returns the remainder of the array, beginning at the position specified by the first argument. It returns the entries up to but omitting that index with the second parameter. (Compare this to how splice handles its second parameter.) If no parameters are provided, slice produces a duplicate of the entire array: that is, a new array with the same items as the original. This comes in handy when you need to apply a destructive technique on an array that you don't want to change. */

обеспечить регресс

// An array's order is reversed using the reverse method.
> let numbers = [1, 2, 3, 4]
> numbers.reverse()
= [ 4, 3, 2, 1 ]
> numbers
= [ 4, 3, 2, 1 ]
/* The opposite is destructive: it changes the array. If you don't want to change the original array, as described in the preceding section, you can use slice with no arguments: */
> let numbers = [1, 2, 3, 4]
> let copyOfNumbers = numbers.slice();
> let reversedNumbers = copyOfNumbers.reverse()
> reversedNumbers
= [ 4, 3, 2, 1 ]
> numbers
= [ 1, 2, 3, 4 ]