Объекты позволяют хранить наборы значений с ключами. Это нормально.
Но довольно часто мы обнаруживаем, что нам нужна упорядоченная коллекция , где у нас есть 1-й, 2-й, 3-й элемент и так далее. Например, нам нужно это для хранения списка чего-либо: пользователей, товаров, HTML-элементов и т. д.
Здесь неудобно использовать объект, поскольку он не предоставляет методов управления порядком элементов. Мы не можем вставить новое свойство «между» существующими. Объекты просто не предназначены для такого использования.
Существует специальная структура данных с именем Array
для хранения упорядоченных коллекций.
Существует два синтаксиса создания пустого массива:
пусть arr = новый массив(); пусть arr = [];
Почти всегда используется второй синтаксис. Мы можем поставить начальные элементы в скобках:
let Fruits = ["Яблоко", "Апельсин", "Слива"];
Элементы массива нумеруются, начиная с нуля.
Мы можем получить элемент по его номеру в квадратных скобках:
let Fruits = ["Яблоко", "Апельсин", "Слива"]; оповещение(фрукты[0]); // Яблоко оповещение(фрукты[1]); // Апельсин предупреждение(фрукты[2]); // Слива
Мы можем заменить элемент:
Fruits[2] = 'Груша'; // сейчас ["Яблоко", "Апельсин", "Груша"]
…Или добавьте в массив новый:
Fruits[3] = 'Лимон'; // сейчас ["Яблоко", "Апельсин", "Груша", "Лимон"]
Общее количество элементов массива равно его length
:
let Fruits = ["Яблоко", "Апельсин", "Слива"]; предупреждение(фрукты.длина); // 3
Мы также можем использовать alert
, чтобы показать весь массив.
let Fruits = ["Яблоко", "Апельсин", "Слива"]; оповещение(фрукты); // Яблоко, Оранжевый, Слива
Массив может хранить элементы любого типа.
Например:
// сочетание значений let arr = [ 'Apple', { name: 'John' }, true, function() { alert('hello'); } ]; // получаем объект с индексом 1 и затем показываем его имя предупреждение(прибытие[1].имя); // Джон // получаем функцию с индексом 3 и запускаем ее обр[3](); // привет
Завершающая запятая
Массив, как и объект, может заканчиваться запятой:
пусть фрукты = [ "Яблоко", "Апельсин", "Слива", ];
Стиль «замыкающая запятая» упрощает вставку/удаление элементов, поскольку все строки становятся одинаковыми.
Недавнее дополнение
Это недавнее дополнение к языку. Старым браузерам могут потребоваться полифилы.
Допустим, нам нужен последний элемент массива.
Некоторые языки программирования позволяют использовать для той же цели отрицательные индексы, напримерfruits fruits[-1]
.
Хотя в JavaScript это не сработает. Результат будет undefined
, поскольку индекс в квадратных скобках трактуется буквально.
Мы можем явно вычислить индекс последнего элемента и затем получить к нему доступ: fruits[fruits.length - 1]
.
let Fruits = ["Яблоко", "Апельсин", "Слива"]; предупреждение(фрукты[фрукты.длина-1]); // Слива
Немного громоздко, не так ли? Нам нужно написать имя переменной дважды.
К счастью, есть более короткий синтаксис: fruits.at(-1)
:
let Fruits = ["Яблоко", "Апельсин", "Слива"]; // то же, что и Fruits[fruits.length-1] предупреждение(фрукты.at(-1)); // Слива
Другими словами, arr.at(i)
:
точно так же, как arr[i]
, если i >= 0
.
для отрицательных значений i
он отступает от конца массива.
Очередь — одно из наиболее распространенных применений массива. В информатике это означает упорядоченный набор элементов, который поддерживает две операции:
push
добавляет элемент в конец.
shift
получить элемент с начала, продвигая очередь так, чтобы 2-й элемент стал 1-м.
Массивы поддерживают обе операции.
На практике нам это нужно очень часто. Например, очередь сообщений, которые необходимо отобразить на экране.
Есть еще один вариант использования массивов — структура данных под названием stack.
Он поддерживает две операции:
push
добавляет элемент в конец.
pop
берет элемент с конца.
Таким образом, новые элементы добавляются или берутся всегда с «конца».
Стопку обычно изображают в виде колоды карт: новые карты добавляются сверху или берутся сверху:
Для стеков первым принимается последний отправленный элемент, это также называется принципом LIFO (Last-In-First-Out). Для очередей у нас есть FIFO (первым пришел — первым обслужен).
Массивы в JavaScript могут работать как в виде очереди, так и в виде стека. Они позволяют добавлять/удалять элементы как в начало, так и в конец.
В информатике структура данных, позволяющая это сделать, называется deque.
Методы, работающие с концом массива:
pop
Извлекает последний элемент массива и возвращает его:
let Fruits = ["Яблоко", "Апельсин", "Груша"]; оповещение(фрукты.поп()); // удаляем "Грушу" и предупреждаем об этом оповещение(фрукты); // Яблоко, Апельсин
И fruits.pop()
, и fruits.at(-1)
возвращают последний элемент массива, но fruits.pop()
также модифицирует массив, удаляя его.
push
Добавьте элемент в конец массива:
let Fruits = ["Яблоко", "Апельсин"]; Fruits.push("Груша"); оповещение(фрукты); // Яблоко, Апельсин, Груша
Вызов fruits.push(...)
равен fruits[fruits.length] = ...
.
Методы, работающие с началом массива:
shift
Извлекает первый элемент массива и возвращает его:
let Fruits = ["Яблоко", "Апельсин", "Груша"]; оповещение(фрукты.shift()); // удаляем Apple и предупреждаем его оповещение(фрукты); // Апельсин, Груша
unshift
Добавьте элемент в начало массива:
let Fruits = ["Апельсин", "Груша"]; Fruits.unshift('Яблоко'); оповещение(фрукты); // Яблоко, Апельсин, Груша
Методы push
и unshift
позволяют добавлять сразу несколько элементов:
let Fruits = ["Яблоко"]; Fruits.push("Апельсин", "Персик"); Fruits.unshift("Ананас", "Лимон"); // ["Ананас", "Лимон", "Яблоко", "Апельсин", "Персик"] оповещение(фрукты);
Массив — это особый тип объекта. Квадратные скобки, используемые для доступа к свойству arr[0]
на самом деле взяты из синтаксиса объекта. По сути, это то же самое, что и obj[key]
, где arr
— это объект, а в качестве ключей используются числа.
Они расширяют объекты, предоставляя специальные методы для работы с упорядоченными коллекциями данных, а также свойство length
. Но по сути это все еще объект.
Помните, что в JavaScript существует только восемь основных типов данных (дополнительную информацию см. в главе «Типы данных»). Массив является объектом и, следовательно, ведет себя как объект.
Например, он копируется по ссылке:
пусть фрукты = ["Банан"] пусть обр = фрукты; // копирование по ссылке (две переменные ссылаются на один и тот же массив) alert(arr === фрукты); // истинный arr.push("Груша"); // модифицируем массив по ссылке оповещение(фрукты); // Банан, Груша — теперь 2 шт.
…Но что делает массивы действительно особенными, так это их внутреннее представление. Движок пытается хранить свои элементы в непрерывной области памяти один за другим, как показано на иллюстрациях в этой главе; существуют и другие оптимизации, позволяющие массивам работать очень быстро.
Но все они ломаются, если мы перестанем работать с массивом как с «упорядоченной коллекцией» и начнем работать с ним, как с обычным объектом.
Например, технически мы можем сделать это:
пусть фрукты = []; // создаем массив фрукты[99999] = 5; // присваиваем свойству индекс, намного превышающий его длину фрукты.возраст = 25; // создаем свойство с произвольным именем
Это возможно, потому что массивы в своей основе являются объектами. Мы можем добавить к ним любые свойства.
Но движок увидит, что мы работаем с массивом как с обычным объектом. Оптимизации, специфичные для массива, не подходят для таких случаев и будут отключены, а их преимущества исчезнут.
Способы неправильного использования массива:
Добавьте нечисловое свойство, например arr.test = 5
.
Сделайте дырки, например: добавьте arr[0]
, а затем arr[1000]
(и между ними ничего).
Заполните массив в обратном порядке, например arr[1000]
, arr[999]
и так далее.
Пожалуйста, думайте о массивах как о специальных структурах для работы с упорядоченными данными . Для этого они предоставляют специальные методы. Массивы тщательно настраиваются внутри движков JavaScript для работы с непрерывными упорядоченными данными, используйте их таким образом. А если вам нужны произвольные ключи, велика вероятность, что вам действительно понадобится обычный объект {}
.
Методы push/pop
выполняются быстро, а shift/unshift
— медленно.
Почему с концом массива работать быстрее, чем с его началом? Посмотрим, что происходит во время выполнения:
фрукты.сдвиг(); // берем 1 элемент с начала
Недостаточно взять и удалить элемент с индексом 0
. Остальные элементы также необходимо перенумеровать.
Операция shift
должна делать 3 вещи:
Удалите элемент с индексом 0
.
Переместите все элементы влево, перенумеруйте их с индекса 1
на 0
, с 2
на 1
и так далее.
Обновите свойство length
.
Чем больше элементов в массиве, тем больше времени на их перемещение, больше операций в памяти.
Аналогичное происходит и с unshift
: чтобы добавить элемент в начало массива, нам нужно сначала переместить существующие элементы вправо, увеличив их индексы.
А что насчет push/pop
? Им не нужно ничего перемещать. Чтобы извлечь элемент с конца, метод pop
очищает индекс и сокращает length
.
Действия для операции pop
:
фрукты.поп(); // берем 1 элемент с конца
Методу pop
не нужно ничего перемещать, поскольку другие элементы сохраняют свои индексы. Вот почему это невероятно быстро.
То же самое и с методом push
.
Одним из старейших способов циклического перемещения элементов массива является цикл for
по индексам:
let arr = ["Яблоко", "Апельсин", "Груша"]; for (пусть я = 0; я <arr.length; i++) { оповещение(прибытие[я]); }
Но для массивов есть другая форма цикла for..of
:
let Fruits = ["Яблоко", "Апельсин", "Слива"]; // перебираем элементы массива ибо (пусть плод плодов) { оповещение(фрукты); }
for..of
не предоставляет доступ к номеру текущего элемента, а только к его значению, но в большинстве случаев этого достаточно. И он короче.
Технически, поскольку массивы являются объектами, их также можно использовать for..in
:
let arr = ["Яблоко", "Апельсин", "Груша"]; for (введите arr) { предупреждение(прибытие[ключ]); // Яблоко, Апельсин, Груша }
Но на самом деле это плохая идея. С этим могут возникнуть потенциальные проблемы:
Цикл for..in
перебирает все свойства , а не только числовые.
В браузере и других средах существуют так называемые «массивные» объекты, которые выглядят как массивы . То есть у них есть свойства length
и индексов, но они также могут иметь и другие нечисловые свойства и методы, которые нам обычно не нужны. Однако цикл for..in
выведет их список. Так что если нам нужно работать с объектами, подобными массивам, то эти «лишние» свойства могут стать проблемой.
Цикл for..in
оптимизирован для общих объектов, а не массивов, и поэтому работает в 10–100 раз медленнее. Конечно, это все еще очень быстро. Ускорение может иметь значение только в узких местах. Но все же мы должны осознавать разницу.
Как правило, нам не следует использовать for..in
для массивов.
Свойство length
автоматически обновляется при изменении массива. Точнее, на самом деле это не количество значений в массиве, а наибольший числовой индекс плюс единица.
Например, один элемент с большим индексом имеет большую длину:
пусть фрукты = []; Fruits[123] = "Яблоко"; предупреждение(фрукты.длина); // 124
Обратите внимание, что мы обычно не используем такие массивы.
Еще одна интересная особенность свойства length
заключается в том, что оно доступно для записи.
Если увеличить вручную, ничего интересного не произойдет. Но если мы уменьшим его, массив усекается. Процесс необратим, вот пример:
пусть arr = [1, 2, 3, 4, 5]; длина аранж. = 2; // усекаем до 2 элементов оповещение (прибытие); // [1, 2] длина аранжировки = 5; // возвращаем длину назад предупреждение(прибытие[3]); // не определено: значения не возвращаются
Итак, самый простой способ очистить массив: arr.length = 0;
.
Существует еще один синтаксис для создания массива:
let arr = new Array("Яблоко", "Груша", "и т.д");
Он используется редко, поскольку квадратные скобки []
короче. Кроме того, у него есть одна хитрая особенность.
Если new Array
вызывается с единственным аргументом, который является числом, он создает массив без элементов, но с заданной длиной .
Давайте посмотрим, как можно выстрелить себе в ногу:
пусть arr = новый массив (2); // создаст ли массив [2]? предупреждение(прибытие[0]); // неопределенный! никаких элементов. оповещение(длина сообщения); // длина 2
Чтобы избежать таких неожиданностей, мы обычно используем квадратные скобки, если только мы действительно не знаем, что делаем.
Массивы могут содержать элементы, которые также являются массивами. Мы можем использовать его для многомерных массивов, например, для хранения матриц:
пусть матрица = [ [1, 2, 3], [4, 5, 6], [7, 8, 9] ]; предупреждение(матрица[0][1]); // 2, второе значение первого внутреннего массива
Массивы имеют собственную реализацию метода toString
, который возвращает список элементов, разделенных запятыми.
Например:
пусть arr = [1, 2, 3]; оповещение (прибытие); // 1,2,3 Предупреждение(String(arr) === '1,2,3'); // истинный
Также давайте попробуем следующее:
предупреждение ([] + 1); // "1" предупреждение([1] + 1); // "11" предупреждение([1,2] + 1); // "1,21"
Массивы не имеют ни Symbol.toPrimitive
, ни жизнеспособного valueOf
, они реализуют только преобразование toString
, поэтому здесь []
становится пустой строкой, [1]
становится "1"
а [1,2]
становится "1,2"
.
Когда двоичный оператор плюс "+"
добавляет что-то к строке, он также преобразует это в строку, поэтому следующий шаг выглядит следующим образом:
предупреждение( "" + 1); // "1" Предупреждение( "1" + 1); // "11" предупреждение( "1,2" + 1); // "1,21"
Массивы в JavaScript, в отличие от некоторых других языков программирования, не следует сравнивать с помощью оператора ==
.
Этот оператор не имеет специального подхода к массивам, он работает с ними как с любыми объектами.
Напомним правила:
Два объекта равны ==
только в том случае, если они являются ссылками на один и тот же объект.
Если один из аргументов ==
является объектом, а другой — примитивом, то объект преобразуется в примитив, как описано в главе «Преобразование объекта в примитив».
…За исключением null
и undefined
, которые равны ==
друг другу и ничему больше.
Строгое сравнение ===
еще проще, поскольку оно не преобразует типы.
Итак, если мы сравниваем массивы с помощью ==
, они никогда не будут одинаковыми, если только мы не сравним две переменные, которые ссылаются на один и тот же массив.
Например:
Предупреждение([] == []); // ЛОЖЬ Предупреждение([0] == [0]); // ЛОЖЬ
Эти массивы технически являются разными объектами. Так что они не равны. Оператор ==
не выполняет поэлементное сравнение.
Сравнение с примитивами также может дать, казалось бы, странные результаты:
предупреждение (0 == []); // истинный Оповещение('0' == []); // ЛОЖЬ
Здесь в обоих случаях мы сравниваем примитив с объектом массива. Таким образом, массив []
преобразуется в примитив для целей сравнения и становится пустой строкой ''
.
Затем продолжается процесс сравнения примитивов, как описано в главе «Преобразования типов»:
// после того, как [] было преобразовано в '' оповещение (0 == ''); // true, поскольку '' преобразуется в число 0 Оповещение('0' == ''); // false, без преобразования типов, разные строки
Итак, как сравнивать массивы?
Это просто: не используйте оператор ==
. Вместо этого сравните их поэлементно в цикле или с помощью методов итерации, описанных в следующей главе.
Массив — это особый тип объекта, подходящий для хранения упорядоченных элементов данных и управления ими.
Декларация:
// квадратные скобки (обычные) let arr = [item1, item2...]; // новый массив (исключительно редко) пусть arr = новый массив (элемент1, элемент2...);
Вызов new Array(number)
создает массив заданной длины, но без элементов.
Свойство length
— это длина массива, а точнее, его последний числовой индекс плюс единица. Он автоматически настраивается методами массива.
Если мы укоротим length
вручную, массив будет усечен.
Получение элементов:
мы можем получить элемент по его индексу, например arr[0]
также мы можем использовать метод at(i)
который допускает отрицательные индексы. Для отрицательных значений i
выполняется шаг назад от конца массива. Если i >= 0
, это работает так же, как arr[i]
.
Мы можем использовать массив как дек со следующими операциями:
push(...items)
добавляет items
в конец.
pop()
удаляет элемент с конца и возвращает его.
shift()
удаляет элемент с начала и возвращает его.
unshift(...items)
добавляет items
в начало.
Чтобы перебрать элементы массива:
for (let i=0; i<arr.length; i++)
– работает быстрее всего, совместим со старыми браузерами.
for (let item of arr)
– современный синтаксис только для элементов,
for (let i in arr)
– никогда не использую.
Для сравнения массивов не используйте оператор ==
(а также >
, <
и другие), поскольку они не имеют специальной обработки для массивов. Они обращаются с ними как с любыми объектами, а это не то, чего нам обычно хочется.
Вместо этого вы можете использовать цикл for..of
для сравнения массивов поэлементно.
Мы продолжим изучение массивов и изучим дополнительные методы добавления, удаления, извлечения элементов и сортировки массивов в следующей главе. Методы массивов.
важность: 3
Что этот код покажет?
let Fruits = ["Яблоки", "Груша", "Апельсин"]; // помещаем новое значение в «копию» пусть ShoppingCart = Fruits; ShoppingCart.push("Банан"); // что во фруктах? предупреждение(фрукты.длина); // ?
Результат 4
:
let Fruits = ["Яблоки", "Груша", "Апельсин"]; пусть ShoppingCart = Fruits; ShoppingCart.push("Банан"); предупреждение(фрукты.длина); // 4
Это потому, что массивы являются объектами. Таким образом, и shoppingCart
, и fruits
являются ссылками на один и тот же массив.
важность: 5
Давайте попробуем 5 операций с массивом.
Создайте массив styles
с элементами «Джаз» и «Блюз».
В конце добавьте «Rock-n-Roll».
Замените значение посередине на «Классика». Ваш код для поиска среднего значения должен работать для любых массивов нечетной длины.
Удалите первое значение массива и покажите его.
Добавьте к массиву Rap
и Reggae
.
Массив в процессе:
Джаз, Блюз джаз, блюз, рок-н-ролл Джаз, Классика, Рок-н-Ролл Классика, Рок-н-Ролл Рэп, Регги, Классика, Рок-н-Ролл
let Styles = ["Джаз", "Блюз"]; Styles.push("Рок-н-Ролл"); стили[Math.floor((styles.length - 1) / 2)] = "Классика"; оповещение(styles.shift()); Styles.unshift("Рэп", "Регги");
важность: 5
Каков результат? Почему?
let arr = ["a", "b"]; arr.push(function() { предупреждение(это); }); обр[2](); // ?
Вызов arr[2]()
синтаксически является старым добрым obj[method]()
, в роли obj
у нас есть arr
, а в роли method
— 2
.
Итак, у нас есть вызов функции arr[2]
как метода объекта. Естественно, он получает this
на объект arr
и выводит массив:
let arr = ["a", "b"]; arr.push(function() { предупреждение(это); }) обр[2](); // a,b,function(){...}
Массив имеет 3 значения: изначально их было два плюс функция.
важность: 4
Напишите функцию sumInput()
которая:
Запрашивает у пользователя значения с помощью prompt
и сохраняет значения в массиве.
Завершает запрос, когда пользователь вводит нечисловое значение, пустую строку или нажимает «Отмена».
Вычисляет и возвращает сумму элементов массива.
PS Ноль 0
— допустимое число, не останавливайте ввод на нуле.
Запустить демо-версию
Обратите внимание на тонкую, но важную деталь решения. Мы не преобразуем value
в число сразу после prompt
, потому что после value = +value
мы не сможем отличить пустую строку (знак остановки) от нуля (действительного числа). Вместо этого мы сделаем это позже.
функция sumInput() { пусть числа = []; в то время как (истина) { let value = Prompt("Пожалуйста, укажите номер?", 0); // стоит ли нам отменить? if (value === "" || value === null || !isFinite(value)) Break; числа.push(+значение); } пусть сумма = 0; for (пусть количество чисел) { сумма += число; } сумма возврата; } Оповещение (суммаВвод());
важность: 2
Входные данные представляют собой массив чисел, например arr = [1, -2, 3, 4, -9, 6]
.
Задача: найти непрерывный подмассив arr
с максимальной суммой элементов.
Напишите функцию getMaxSubSum(arr)
которая будет возвращать эту сумму.
Например:
getMaxSubSum([-1, 2, 3, -9]) == 5 (сумма выделенных элементов) getMaxSubSum([2, -1, 2, 3, -9]) == 6 getMaxSubSum([-1, 2, 3, -9, 11]) == 11 getMaxSubSum([-2, -1, 1, 2]) == 3 getMaxSubSum([100, -9, 2, -3, 5]) == 100 getMaxSubSum([1, 2, 3]) == 6 (забрать все)
Если все элементы отрицательны, это означает, что мы не берем ни одного (подмассив пуст), поэтому сумма равна нулю:
getMaxSubSum([-1, -2, -3]) = 0
Пожалуйста, попробуйте придумать быстрое решение: O(n 2 ) или даже O(n), если можете.
Откройте песочницу с тестами.
Мы можем вычислить все возможные подсуммы.
Самый простой способ — взять каждый элемент и вычислить суммы всех подмассивов, начиная с него.
Например, для [-1, 2, 3, -9, 11]
:
// Начиная с -1: -1 -1 + 2 -1 + 2 + 3 -1 + 2 + 3 + (-9) -1 + 2 + 3 + (-9) + 11 // Начиная с 2: 2 2 + 3 2 + 3 + (-9) 2 + 3 + (-9) + 11 // Начиная с 3: 3 3 + (-9) 3 + (-9) + 11 // Начиная с -9 -9 -9 + 11 // Начиная с 11 11
На самом деле код представляет собой вложенный цикл: внешний цикл по элементам массива, а внутренние подсчеты суммируются, начиная с текущего элемента.
функция getMaxSubSum(arr) { пусть maxSum = 0; // если мы не возьмем ни одного элемента, будет возвращен ноль for (пусть я = 0; я <arr.length; i++) { пусть sumFixedStart = 0; for (let j = i; j <arr.length; j++) { sumFixedStart += arr[j]; maxSum = Math.max(maxSum, sumFixedStart); } } вернуть максимальную сумму; } alert( getMaxSubSum([-1, 2, 3, -9]) ); // 5 alert( getMaxSubSum([-1, 2, 3, -9, 11]) ); // 11 alert( getMaxSubSum([-2, -1, 1, 2]) ); // 3 предупреждение( getMaxSubSum([1, 2, 3])); // 6 alert( getMaxSubSum([100, -9, 2, -3, 5]) ); // 100
Решение имеет временную сложность O(n 2 ). Другими словами, если мы увеличим размер массива в 2 раза, алгоритм будет работать в 4 раза дольше.
Для больших массивов (1000, 10000 и более элементов) такие алгоритмы могут привести к серьезной медлительности.
Давайте пройдемся по массиву и сохраним текущую частичную сумму элементов в переменной s
. Если в какой-то момент s
становится отрицательным, присвойте s=0
. Максимум из всех таких s
и будет ответом.
Если описание слишком расплывчатое, посмотрите код, он достаточно короткий:
функция getMaxSubSum(arr) { пусть maxSum = 0; пусть частичная сумма = 0; for (let item of arr) { // для каждого элемента arr частичная сумма += элемент; // добавляем его в частичную сумму maxSum = Math.max(maxSum, частичнаяSum); // запоминаем максимум если (partialSum < 0) частичная сумма = 0; // ноль, если отрицательно } вернуть максимальную сумму; } alert( getMaxSubSum([-1, 2, 3, -9]) ); // 5 alert( getMaxSubSum([-1, 2, 3, -9, 11]) ); // 11 alert( getMaxSubSum([-2, -1, 1, 2]) ); // 3 alert( getMaxSubSum([100, -9, 2, -3, 5]) ); // 100 предупреждение( getMaxSubSum([1, 2, 3])); // 6 alert( getMaxSubSum([-1, -2, -3]) ); // 0
Алгоритму требуется ровно 1 проход массива, поэтому временная сложность равна O(n).
Более подробную информацию об алгоритме можно найти здесь: Задача о максимальном подмассиве. Если все еще не очевидно, почему это работает, то проследите алгоритм на примерах выше, посмотрите, как он работает, это лучше любых слов.
Откройте решение с тестами в песочнице.