Акт 1: «Ядро» — Часть 5 (Неизменяемость и массивы)
Это продолжение серии о JavaScript, начатой здесь.
Неизменяемость и операции на месте — две важные концепции, когда речь идет о сопровождении кода и соображениях производительности. Нам часто приходится иметь дело с массивами, и именно здесь эти вещи играют поразительную роль.
Неизменяемые и операции на месте:
Важным понятием, которое нужно знать, является неизменность. Как мы знаем, переменные относятся к данным в памяти. Если вы можете изменить эти данные в памяти через переменную, то она изменчива, иначе неизменяема. Например, строки неизменяемы:
var a = ''; a = a + 'x'; // creates a new object in memory
Массивы изменяются с помощью так называемых операций на месте:
var a = []; a.push('x'); console.log(a); // ['x']
Это показывает, что метод push
изменял объект в памяти.
Не все операторы над массивами изменяемы. Например:
var a = []; var b = a.concat(['x']); console.log(a); // [] console.log(b); // ['x']
Как видите, метод concat
не изменяет данные в памяти. Он создает в памяти новый массив, соответствующий конкатенации, и возвращает ссылку на этот объект.
Неизменяемые операции в целом должны быть предпочтительным методом, поскольку они обеспечивают более простое сопровождение кода, но в сценариях, где задействованы тяжелые вычисления или большие объемы данных, изменяемая версия часто обеспечивает гигантское ускорение.
Некоторые менее известные методы Array:
Массивами можно манипулировать различными способами. Важно знать, работает ли метод на месте или неизменен (см. предыдущий раздел).
Метод unshift
добавляет новый элемент в начало на месте:
var a = []; a.unshift(1); console.log(a); // [1]
Метод shift
удаляет первый элемент на месте:
var a = [1, 2]; a.shift(1); console.log(a); [2]
Array.isArray
— это официальный метод проверки того, является ли объект массивом. Напомним, что typeof []
вернет только 'object'
:
Array.isArray([]); // true
Самый простой способ очистить массив — просто установить его length
в 0
:
var a = [1, 2]; a.length = 0; console.log(a); // []
Метод from
позволяет создать новый массив из заданного итерируемого объекта. Что такое итерации, будет рассмотрено позже, а пока достаточно знать, что сами массивы являются итерируемыми:
var a = [1, 2]; var b = Array.from(a); console.log(b); // [1, 2]
Аналогичным вышеприведенному методу является of
, который создает массив из заданного списка параметров:
var a = Array.of(1, 2, 3); console.log(a); // [ 1, 2, 3 ]
Быстрый способ создать массив с предварительно заполненными значениями — использовать конструктор Array
и метод fill
:
var a = Array(3); // constructs an array of length 3 a.fill(0); console.log(a); // [0,0,0]
Массивы в JavaScript по умолчанию разреженные. То есть вы можете назначать определенные индексы, не назначая никаких других:
var a = []; a[1000] = 1; a[2000] = 2; console.log(a); // [ <1000 empty items>, 1, <999 empty items>, 2 ]
Вместо удаления элементов можно воспользоваться методом splice
:
var a = [0, 1, 2, 3]; a.splice(1, 2); console.log(a); // [0,3]
Этот метод удаляет 2
элементов, начиная с индекса 1
. Итак, первый параметр указывает индекс начала удаления, а второй количество удаляемых элементов.
Основные функциональные концепции массивов:
some
, filter
, every
, find
Все эти методы схожи в своем использовании. Они ожидают предикат, которому передается каждый элемент массива. some
и find
вырываются наружу, когда предикат выполняется некоторым элементом:
var a = [1, 2, 3]; console.log(a.some(e => e === 2)); // true console.log(a.every(e => e > 0)); // true console.log(a.filter(e => e > 1)); // [2,3] console.log(a.find(e => e === 3)); // 3
reduce
, forEach
, map
reduce
и map
работают с данными и возвращают новый массив, тогда как forEach
просто перебирает массив и позволяет выполнять побочные эффекты с помощью заданной функции:
[1, 2, 3].reduce((prev, e) => fn(prev, e), init);
reduce
перебирает массив и вызывает для каждого элемента заданную функцию fn
. В качестве первого параметра принимает возвращаемое значение предыдущего вызова fn
или, если предыдущего вызова не было, использует заданный параметр init
. В качестве второго параметра он передает текущий итерируемый элемент.
Это хорошее упражнение — попытаться реализовать этот метод самостоятельно или, по крайней мере, попытаться понять следующий код:
function reduce(a, f, next, idx = 0) { return idx < a.length ? reduce(a, f, f(next, a[idx]), ++idx) : next; } console.log( reduce([1, 2, 3], (prev, e) => ({ ...prev, [e]: e }), {}) ); // { '1': 1, '2': 2, '3': 3 } // this is equivalent to: console.log([1, 2, 3].reduce((prev, e) => ({ ...prev, [e]: e }), {})); // { '1': 1, '2': 2, '3': 3 }
Эта функция рекурсивно вызывает себя и каждый раз предоставляет в качестве параметра возвращаемое значение вызова f
. Сам f
получает текущее значение next
и элемент массива a
, соответствующий текущему индексу итерации. Это дает f
на каждой итерации возможность изменять значение next
в зависимости от текущего элемента массива.
Метод flat
рекурсивно сглаживает все элементы. Это,
var a = [1, 2, [3, 4, [5, 6]]]; console.log(a.flat()); // [ 1, 2, 3, 4, [ 5, 6 ] ]
Вы можете управлять параметром, насколько глубоким должно быть это сглаживание:
var a = [1, 2, [3, 4, [5, 6]]]; console.log(a.flat(2)); // [ 1, 2, 3, 4, 5, 6 ]
Обратите внимание, что для этого параметра можно установить значение Infinity
, что указывает на отсутствие ограничений по глубине уровня.
Это хорошее упражнение для самостоятельной реализации такого метода:
function flat(a) { return a.reduce((p, e) => { if (!Array.isArray(e)) { p.push(e); } else { p.push(...flat(e)); } return p; }, []); } var a = [1, 2, [3, 4, [5, 6]]]; console.log(flat(a)); // [ 1, 2, 3, 4, [ 5, 6 ] ]
В этом примере показано хорошее применение функции reduce
. Хотя для начинающих этот стиль кода и, в частности, рекурсивные вызовы могут показаться довольно сложными, вам следует ознакомиться с этим как можно раньше. Часто используется на практике.
На этом пока все, и не забывайте оставлять комментарии по вопросам или ошибкам, которые вы обнаружите. Давайте посмотрим еще раз в следующей истории, если хотите.
Спасибо за чтение!