[Перевод] Что нужно знать о массивах JavaScript

Представляем вам перевод статьи автора Thomas Lombart, которая была опубликована на сайте medium.freecodecamp.org. Перевод публикуется с разрешения автора.

smgs6kbtqkaxsoz79qtt2on6m5c.png
Пример использования метода reduce для сокращения массива

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

Несмотря на эффективность, большинство этих методов все еще малоизвестны и не очень популярны. Я проделаю для вас трудную работу и расскажу о самых полезных. Считайте эту статью своим путеводителем по методам массивов JavaScript.

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

Изначально эта статья была опубликована на сайте thomlom.dev — там вы можете найти больше материалов по веб-разработке.

Основы


Есть четыре метода, о которых стоит знать, если вы работаете с массивами. Это map, filter, reduce и оператор spread. Они эффективны и полезны.

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

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

const numbers = [1, 2, 3, 4] 
const numbersPlusOne = numbers.map(n => n + 1) console.log(numbersPlusOne) // [2, 3, 4, 5]


Также вы можете создать новый массив, который хранит только одно определенное свойство объекта.

const allActivities = [
  { title: 'My activity', coordinates: [50.123, 3.291] }, 
  { title: 'Another activity', coordinates: [1.238, 4.292] }
] 
const allCoordinates = allActivities.map(activity => activity.coordinates) 
console.log(allCoordinates) // [[50.123, 3.291], [1.238, 4.292]]


Итак, запомните: когда вам нужно изменить массив, подумайте об использовании map.

filter
Название этого метода говорит само за себя: применяйте его, когда хотите отфильтровать массив.

Как и map, filter принимает в качестве единственного параметра функцию, которая вызывается на каждом элементе массива. Эта функция должна вернуть булево значение:

  • true — если вы хотите сохранить элемент в массиве;
  • false — если не хотите сохранять его.


В итоге у вас будет правильный новый массив с элементами, которые вы хотели оставить.

Например, в массиве можно сохранить только нечетные цифры.

const numbers = [1, 2, 3, 4, 5, 6] 
const oddNumbers = numbers.filter(n => n % 2 !== 0) console.log(oddNumbers) // [1, 3, 5]


Также можно использовать filter, чтобы убрать определенный элемент в массиве.

const participants = [
  { id: 'a3f47', username: 'john' }, 
  { id: 'fek28', username: 'mary' }, 
  { id: 'n3j44', username: 'sam' }, 
]
function removeParticipant(participants, id) { 
  return participants.filter(participant => participant.id !== id) 
} 
console.log(removeParticipant(participants, 'a3f47')) //  [{ id: 'fek28', username: 'mary' }, { id: 'n3j44', username: 'sam' }];


reduce
На мой взгляд, этот метод — самый сложный для понимания. Но как только вы его освоите, у вас появится куча возможностей.

Обычно метод reduce берет массив значений и соединяет их в одно значение. Он принимает два параметра, функцию callback (которая является редуктором) и опциональное начальное значение (которое является первым элементом массива по умолчанию). Сам редуктор принимает четыре параметра:

  • аккумулятор, собирающий возвращенные значения в редуктор;
  • текущее значение массива;
  • текущий индекс;
  • массив, для которого был вызван метод reduce.


В основном вы будете использовать только первые два параметра — аккумулятор и текущее значение.

Но давайте не будем сильно углубляться в теорию и рассмотрим самый распространенный пример применения reduce.

const numbers = [37, 12, 28, 4, 9] 
const total = numbers.reduce((total, n) => total + n) console.log(total) // 90


В первой итерации аккумулятор, являющийся суммой, принимает начальное значение 37. Возвращенное значение — 37 + n, где n = 12. Получаем 49.

Во время второй итерации аккумулятор равен 49, возвращенное значение — 49 + 28 = 77. И так далее.

Метод reduce настолько функциональный, что вы можете использовать его для построения множества методов массивов вроде map или filter.

const map = (arr, fn) => { 
  return arr.reduce((mappedArr, element) => { 
    return [...mappedArr, fn(element)] 
  }, []) 
} 
console.log(map([1, 2, 3, 4], n => n + 1)) // [2, 3, 4, 5]
const filter = (arr, fn) => { 
  return arr.reduce((filteredArr, element) => { 
    return fn(element) ? [...filteredArr] : [...filteredArr, element] 
  }, []) 
} 
console.log(filter([1, 2, 3, 4, 5, 6], n => n % 2 === 0)) // [1, 3, 5]


Как правило, мы присваиваем методу reduce начальное значение [] — аккумулятор. Для map мы запускаем функцию, результат которой добавляется в конец аккумулятора при помощи оператора spread (мы поговорим о нем ниже, не волнуйтесь). Для filter проделываем практически то же самое, только функцию filter запускаем на элементе. Если она принимает значение true, мы возвращаем предыдущий массив. В противном случае добавляем элемент в конец массива.

Давайте рассмотрим более сложный пример: сильно сократим массив [1, 2, 3, [4, [[[5, [6, 7]]]], 8]] до [1, 2, 3, 4, 5, 6, 7, 8].

function flatDeep(arr) { 
  return arr.reduce((flattenArray, element) => { 
    return Array.isArray(element) ? [...flattenArray, ...flatDeep(element)] : [...flattenArray, element] 
  }, []) 
} 
console.log(flatDeep([1, 2, 3, [4, [[[5, [6, 7]]]], 8]])) // [1, 2, 3, 4, 5, 6, 7, 8]


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

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

const numbers = [1, 2, 3] 
const numbersCopy = [...numbers]
console.log(numbersCopy) // [1, 2, 3]
const otherNumbers = [4, 5, 6]
const numbersConcatenated = [...numbers, ...otherNumbers]
console.log(numbersConcatenated) // [1, 2, 3, 4, 5, 6]


Внимание: оператор spread делает поверхностную копию оригинального массива. Но что значит «поверхностную»?

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

const arr = ['foo', 42, { name: 'Thomas' }]
let copy = [...arr]
copy[0] = 'bar'
console.log(arr) // No mutations: ["foo", 42, { name: "Thomas" }]
console.log(copy) // ["bar", 42, { name: "Thomas" }]
copy[2].name = 'Hello'
console.log(arr) // /!\ MUTATION ["foo", 42, { name: "Hello" }]
console.log(copy) // ["bar", 42, { name: "Hello" }]


Итак, если вы хотите создать реальную копию массива, который содержит объект или массивы, можете воспользоваться функцией lodash вроде cloneDeep. Но не нужно считать себя обязанным это сделать. Ваша цель — узнать, как все устроено под капотом.

Полезные методы


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

includes (ES2015)
Вы использовали когда-нибудь indexOf, чтобы узнать, есть элемент в массиве или нет? Ужасный способ проверки, правда?

К счастью, метод includes делает проверку за нас. Задайте параметр для includes, и он проведет поиск элемента по массиву.

const sports = ['football', 'archery', 'judo'] 
const hasFootball = sports.includes('football')
console.log(hasFootball) // true


concat
Метод concat можно применять для слияния двух или более массивов.

const numbers = [1, 2, 3]
const otherNumbers = [4, 5, 6]
const numbersConcatenated = numbers.concat(otherNumbers)
console.log(numbersConcatenated) // [1, 2, 3, 4, 5, 6]
// You can merge as many arrays as you want
function concatAll(arr, ...arrays) {
  return arr.concat(...arrays)
}
console.log(concatAll([1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12])) // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]


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

const numbers = [1, 2, 3, 4, 5] 
numbers.forEach(console.log)
// 1 0 [ 1, 2, 3 ]
// 2 1 [ 1, 2, 3 ]
// 3 2 [ 1, 2, 3 ]


indexOf
Этот метод используют, чтобы вернуть первый индекс, при котором элемент можно найти в массиве. Также с помощью indexOf часто проверяют наличие элемента в массиве. Честно говоря, сейчас я применяю его нечасто.

const sports = ['football', 'archery', 'judo'] 
const judoIndex = sports.indexOf('judo') 
console.log(judoIndex) // 2


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

const users = [
  { id: 'af35', name: 'john' },
  { id: '6gbe', name: 'mary' },
  { id: '932j', name: 'gary' },
]
const user = users.find(user => user.id === '6gbe')
console.log(user) // { id: '6gbe', name: 'mary' }


Итак, используйте метод filter, когда хотите отфильтровать весь массив, а метод find, когда уверены, что ищете уникальный элемент в массиве.
findIndex
Этот метод практически такой же, как find, но он возвращает индекс первого найденного элемента вместо самого элемента.

const users = [
  { id: 'af35', name: 'john' },
  { id: '6gbe', name: 'mary' },
  { id: '932j', name: 'gary' },
]
const user = users.findIndex(user => user.id === '6gbe')
console.log(user) // 1


Вам может показаться, что findIndex и indexOf — это одно и тоже. Не совсем. Первым параметром indexOf является примитивное значение (булево значение, номер, строка, неопределенное значение или символ), тогда как первый параметр findIndex — функция обратного вызова.

Поэтому, когда вам нужно найти индекс элемента в массиве примитивных значений, вы можете работать с indexOf. Если у вас больше сложных элементов, например объектов, используйте findIndex.

slice
Когда вам нужно взять часть массива или скопировать массив, вы можете обратиться к методу slice. Но будьте внимательны: как и оператор spread, slice возвращает поверхностную копию.

const numbers = [1, 2, 3, 4, 5] 
const copy = numbers.slice()


В начале статьи я упомянул, что циклы часто бывают бесполезными. Давайте я покажу, как от них можно избавиться.

Предположим, вы хотите вернуть определенное количество сообщений чата из API и вам нужно, чтобы отображались только пять из них. Ниже приведены два подхода: один с циклами, другой с методом slice.

// The "traditional way" to do it:
// Determine the number of messages to take and use a for loop
const nbMessages = messages.length < 5 ? messages.length : 5
let messagesToShow = []
for (let i = 0; i < nbMessages; i++) {
  messagesToShow.push(posts[i])
}
// Even if "arr" has less than 5 elements,
// slice will return an entire shallow copy of the original array
const messagesToShow = messages.slice(0, 5)


some
Если вы хотите проверить, пройдет ли тест хотя бы один элемент массива, можно воспользоваться some. Как и map, filter или find, метод some принимает функцию обратного вызова в качестве единственного параметра, а затем возвращает значение true, если по крайней мере один элемент проходит проверку, и значение false, если нет.

Также some подходит для работы с разрешениями.

const users = [
  {
    id: 'fe34',
    permissions: ['read', 'write'],
  },
  {
    id: 'a198',
    permissions: [],
  },
  {
    id: '18aa',
    permissions: ['delete', 'read', 'write'],
  }
]
const hasDeletePermission = users.some(user => 
  user.permissions.includes('delete')
)
console.log(hasDeletePermission) // true


every
Этот метод похож на some, за исключением того, что он проверяет, чтобы условию соответствовал каждый элемент (а не один).

const users = [
  {
    id: 'fe34',
    permissions: ['read', 'write'],
  },
  {
    id: 'a198',
    permissions: [],
  },
  {
    id: '18aa',
    permissions: ['delete', 'read', 'write'],
  }
]
const hasAllReadPermission = users.every(user =>
  user.permissions.includes('read')
)
console.log(hasAllReadPermission) // false


flat (ES2019)
Это совершенно новые методы в мире JavaScript. Обычно flat создает новый массив, соединяя все элементы вложенного массива. Он принимает один параметр — число, которое указывает, насколько сильно вы хотите уменьшить размерность массива.

const numbers = [1, 2, [3, 4, [5, [6, 7]], [[[[8]]]]]]
const numbersflattenOnce = numbers.flat()
console.log(numbersflattenOnce) // [1, 2, 3, 4, Array[2], Array[1]]
const numbersflattenTwice = numbers.flat(2)
console.log(numbersflattenTwice) // [1, 2, 3, 4, 5, Array[2], Array[1]]
const numbersFlattenInfinity = numbers.flat(Infinity)
console.log(numbersFlattenInfinity) // [1, 2, 3, 4, 5, 6, 7, 8]


flatMap (ES2019)
Угадаете, что делает этот метод? Могу поспорить, вы поймете по одному его названию.

Сначала он запускает функцию mapping для каждого элемента, а затем сокращает массив за один раз. Проще простого!

const sentences = [
  'This is a sentence',
  'This is another sentence',
  "I can't find any original phrases",
]
const allWords = sentences.flatMap(sentence => sentence.split(' '))
console.log(allWords) // ["This", "is", "a", "sentence", "This", "is", "another", "sentence", "I", "can't", "find", "any", "original", "phrases"]


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

Затем вы можете подсчитать количество слов с функцией reduce (это не относится к flatMap, просто я хочу показать вам другой пример использования метода reduce).

const wordsCount = allWords.reduce((count, word) => {
  count[word] = count[word] ? count[word] + 1 : 1
  return count
}, {})
console.log(wordsCount) // { This: 2, is: 2, a: 1, sentence: 2, another: 1, I: 1, "can't": 1, find: 1, any: 1, original: 1, phrases: 1, }


Метод flatMap также часто используется в реактивном программировании. Пример вы можете посмотреть здесь.

join
Если вам нужно создать строку, основанную на элементах массива, метод join — то, что вам нужно. Он позволяет создавать новую строку, соединяя все элементы массива, разделенные предоставленным разделителем.

Например, с помощью join можно визуально отобразить всех участников деятельности.

const participants = ['john', 'mary', 'gary']
const participantsFormatted = participants.join(', ')
console.log(participantsFormatted) // john, mary, gary


А это более реальный пример, где вы можете сначала отфильтровать участников и получить их имена.

const potentialParticipants = [
  { id: 'k38i', name: 'john', age: 17 },
  { id: 'baf3', name: 'mary', age: 13 },
  { id: 'a111', name: 'gary', age: 24 },
  { id: 'fx34', name: 'emma', age: 34 },
]
const participantsFormatted = potentialParticipants
  .filter(user => user.age > 18)
  .map(user => user.name)
  .join(', ')
console.log(participantsFormatted) // gary, emma


from
Это статический метод, который создает новый массив из массивоподобного или итерируемого объекта, например строки. Он может пригодиться, когда вы работаете с объектной моделью документа.

const nodes = document.querySelectorAll('.todo-item') // this is an instance of NodeList
const todoItems = Array.from(nodes) // now, you can use map, filter, etc. as you're workin with an array!


Вы увидели, что мы использовали тип массива вместо экземпляра массива? Вот почему этот метод называется статическим.

Затем вы можете поразвлекаться с узлами, например зарегистрировать слушателей события на каждый из них при помощи метода forEach.

todoItems.forEach(item => {
  item.addEventListener('click', function() {
    alert(`You clicked on ${item.innerHTML}`)
  })
})


Методы, изменяющие массив, о которых стоит знать


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

Если вы не хотите изменять оригинальный массив, работая с этими методами, сделайте его поверхностную или полную копию заранее.

const arr = [1, 2, 3, 4, 5] 
const copy = [...arr] // or arr.slice()


sort
Да, sort изменяет оригинальный массив. Фактически он сортирует элементы массива на месте. Метод сортировки по умолчанию трансформирует все элементы в строки и сортирует их в алфавитном порядке.

const names = ['john', 'mary', 'gary', 'anna']
names.sort()
console.log(names) // ['anna', 'gary', 'john', 'mary']


Будьте внимательны: если вы, например, перешли с языка Python, то метод sort при работе с массивом цифр не даст вам желаемого результата.

const numbers = [23, 12, 17, 187, 3, 90]
numbers.sort()
console.log(numbers) // [12, 17, 187, 23, 3, 90] 


Как же тогда отсортировать массив? Метод sort принимает одну функцию — функцию сравнения. Она принимает два параметра: первый элемент (а) и второй элемент для сравнения (b). Сравнение между этими двумя элементами требует возврата цифры:

  • если значение отрицательное — a сортируется перед b;
  • если значение положительное — b сортируется перед a;
  • если значение равно 0 — без изменений.


Затем можно отсортировать цифры.

const numbers = [23, 12, 17, 187, 3, 90]
numbers.sort((a, b) => a - b)
console.log(numbers) // [3, 12, 17, 23, 90, 187]


Или можно отсортировать даты от наиболее поздней.

const posts = [
  {
    title: 'Create a Discord bot under 15 minutes',
    date: new Date(2018, 11, 26),
  },
  {
    title: 'How to get better at writing CSS', 
    date: new Date(2018, 06, 17) },
  { 
    title: 'JavaScript arrays', 
    date: new Date() 
  },
]
posts.sort((a, b) => a.date - b.date) // Substracting two dates returns the difference in millisecond between them
console.log(posts)
// [ { title: 'How to get better at writing CSS',
//     date: 2018-07-17T00:00:00.000Z },
//   { title: 'Create a Discord bot under 15 minutes',
//     date: 2018-12-26T00:00:00.000Z },
//   { title: 'Learn Javascript arrays the functional way',
//     date: 2019-03-16T10:31:00.208Z } ]


fill
Метод fill изменяет или заполняет все элементы массива от начального индекса до конечного заданным значением. Пример отличного использования fill — заполнение нового массива начальными данными.

// Normally I would have called a function that generates ids and random names but let's not bother with that here.
function fakeUser() {
  return {
    id: 'fe38',
    name: 'thomas',
  }
}
const posts = Array(3).fill(fakeUser())
console.log(posts) // [{ id: "fe38", name: "thomas" }, { id: "fe38", name: "thomas" }, { id: "fe38", name: "thomas" }]


reverse
Мне кажется, название метода полностью объясняет его суть.

const numbers = [1, 2, 3, 4, 5]
numbers.reverse()
console.log(numbers) // [5, 4, 3, 2, 1]


pop
Этот метод убирает последний элемент из массива и возвращает его.

const messages = ['Hello', 'Hey', 'How are you?', "I'm fine"]
const lastMessage = messages.pop()
console.log(messages) // ['Hello', 'Hey', 'How are you?']
console.log(lastMessage) // I'm fine


Методы, которые можно заменить


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

push
Этот метод используется часто. Он позволяет добавлять один или более элементов в массив, а также строить новый массив, основанный на предыдущем.

const todoItems = [1, 2, 3, 4, 5]
const itemsIncremented = []
for (let i = 0; i < items.length; i++) {
  itemsIncremented.push(items[i] + 1)
}
console.log(itemsIncremented) // [2, 3, 4, 5, 6]
const todos = ['Write an article', 'Proofreading']
todos.push('Publish the article')
console.log(todos) // ['Write an article', 'Proofreading', 'Publish the article']


Если вам нужно построить массив на основе другого, как в методе itemsIncremented, есть подходящие для этого и уже знакомые нам map, filter или reduce. Например, мы можем взять map, чтобы сделать это.

const itemsIncremented = todoItems.map(x => x + 1)


А если вы хотите использовать push, когда нужно добавить новый элемент, то пригодится оператор spread.

const todos = ['Write an article', 'Proofreading'] console.log([...todos, 'Publish the article'])


splice
К splice часто обращаются, чтобы убрать элемент на определенном индексе. Вы можете сделать то же самое с методом filter.

const months = ['January', 'February', 'March', 'April', ' May']
// With splice
months.splice(2, 1) // remove one element at index 2
console.log(months) // ['January', 'February', 'April', 'May']
// Without splice
const monthsFiltered = months.filter((month, i) => i !== 3)
console.log(monthsFiltered) // ['January', 'February', 'April', 'May']


Вы спросите:, а если мне нужно убрать много элементов? Тогда используйте slice.


const months = ['January', 'February', 'March', 'April', ' May']
// With splice
months.splice(1, 3) // remove thirds element starting at index 1
console.log(months) // ['January', 'February', 'April', 'May']
// Without splice
const monthsFiltered = [...months.slice(0, 1), ...months.slice(4)]
console.log(monthsFiltered) // ['January', 'February', 'April', 'May']


shift
Метод shift убирает первый элемент массива и возвращает его. Чтобы сделать это в стиле функционального программирования, можно использовать оператор spread или rest.

const numbers = [1, 2, 3, 4, 5]
// With shift
const firstNumber = numbers.shift()
console.log(firstNumber) // 1
console.log(numbers) // [2, 3, 4, 5]
// Without shift
const [firstNumber, ...numbersWithoutOne] = numbers
console.log(firstNumber) // 1
console.log(numbersWithoutOne) // [2, 3, 4, 5]


unshift
Метод unshift позволяет добавлять один или более элементов в начало массива. Как и в shift, вы можете делать это с помощью оператора spread.

const numbers = [3, 4, 5]
// With unshift
numbers.unshift(1, 2)
console.log(numbers) // [1, 2, 3, 4, 5]
// Without unshift
const newNumbers = [1, 2, ...numbers]
console.log(newNumbers) // [1, 2, 3, 4, 5]


TL; DR


  • Когда вы хотите совершить какие-то операции с массивом, не используйте цикл for и не изобретайте велосипед, потому что, скорее всего, найдется метод из вышеперечисленных, который может сделать то, что вам нужно.
  • Чаще всего вы будете пользоваться методами map, filter, reduce и оператором spread — это важные инструменты для любого разработчика.
  • Существует также много методов массивов, которые хорошо бы знать: slice, some, flatMap, и т. д. Знакомьтесь с ними и применяйте при необходимости.
  • Побочные эффекты могут привести к нежелательным изменениям. Учитывайте, что некоторые методы изменяют ваш оригинальный массив.
  • Метод slice и оператор spread делают поверхностные копии. В результате объекты и подмассивы будут иметь одинаковые ссылки — это тоже стоит иметь в виду.
  • Старые методы, изменяющие массив, можно заменить новыми. Вы сами решаете, как поступать.


Теперь вы знаете все, что должны были знать о массивах JavaScript. Если вам понравилась эта статья, нажмите на кнопку «Похлопать» (до 50 раз, если захотите:-)) и поделитесь ей. И не стесняйтесь обмениваться впечатлениями в комментариях!

© Habrahabr.ru