Lo que necesita saber sobre las matrices de JavaScript

Le presentamos una traducción de un artículo de Thomas Lombart, que se publicó en medium.freecodecamp.org. La traducción se publica con permiso del autor.


Un ejemplo del uso del método reduce para reducir una matriz

Permítanme hacer una declaración audaz: los bucles a menudo son inútiles y hacen que el código sea difícil de leer. Para iteraciones en matrices, búsqueda, clasificación de elementos y otras acciones similares, puede usar uno de los métodos a continuación.

A pesar de la efectividad, la mayoría de estos métodos aún son poco conocidos y no muy populares. Haré el trabajo duro por ti y hablaré sobre los más útiles. Lea este artículo como guía para los métodos de matriz de JavaScript.

Nota : Antes de comenzar, necesita saber una cosa: estoy sesgado hacia la programación funcional. Para evitar efectos secundarios, me esfuerzo por aplicar métodos que no modifiquen directamente la matriz original. No le estoy diciendo que se niegue a cambiar la matriz, pero vale la pena considerar que algunos métodos conducen a esto. Como resultado, aparecen efectos secundarios, cambios no deseados y, como resultado, errores.

Este artículo se publicó originalmente en thomlom.dev , donde puede encontrar más material de desarrollo web.

Los fundamentos


Hay cuatro métodos que vale la pena saber si está trabajando con matrices. Estos son el operador de map , filter , reduce y spread . Son efectivos y útiles.

mapa
A menudo utilizará el método de map . En general, cada vez que necesite cambiar los elementos de una matriz, considere esta opción.

Toma un parámetro: una función que se llama en cada elemento de la matriz y luego devuelve una nueva matriz para que no pueda haber efectos secundarios.

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

También puede crear una nueva matriz que almacene solo una propiedad específica del objeto.

 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]] 

Entonces, recuerde: cuando necesite cambiar una matriz, piense en usar el mapa .

filtro
El nombre de este método habla por sí mismo: úselo cuando desee filtrar una matriz.

Al igual que map , filter toma como parámetro único una función que se llama en cada elemento de la matriz. Esta función debería devolver un valor booleano:

  • true : si desea guardar el elemento en una matriz;
  • false : si no desea guardarlo.

Como resultado, tendrá la nueva matriz correcta con los elementos que desea dejar.

Por ejemplo, solo se pueden almacenar números impares en una matriz.

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

También puede usar el filtro para eliminar un elemento específico de la matriz.

 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' }]; 

reducir
En mi opinión, este método es el más difícil de entender. Pero tan pronto como lo domines, tendrás muchas oportunidades.

Por lo general, el método reduce toma una matriz de valores y los concatena en un solo valor. Se necesitan dos parámetros, una función de devolución de llamada (que es el reductor ) y un valor inicial opcional (que es el primer elemento de la matriz de forma predeterminada). La caja de cambios en sí toma cuatro parámetros:

  • una batería que recoge los valores devueltos en la caja de cambios ;
  • valor actual de la matriz;
  • índice actual;
  • la matriz para la que se llamó al método reduce .

Básicamente, usará solo los dos primeros parámetros: la batería y el valor actual.

Pero no profundicemos en la teoría y consideremos el ejemplo más común de aplicar reduce .

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

En la primera iteración, el acumulador, que es la suma, toma el valor inicial 37. El valor devuelto es 37 + n, donde n = 12. Obtenemos 49.

Durante la segunda iteración, el acumulador es 49, el valor devuelto es 49 + 28 = 77. Y así sucesivamente.

El método de reduce es tan funcional que puede usarlo para construir muchos métodos de matriz como map o 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] 

Como regla general, asignamos el valor inicial [] al método de reduce : el acumulador. Para el map ejecutamos una función cuyo resultado se agrega al final de la batería utilizando el operador de propagación (hablaremos de ello a continuación, no se preocupe). Para el filter haciendo casi lo mismo, solo ejecutamos la función de filtro en el elemento. Si es cierto, devolvemos la matriz anterior . De lo contrario, agregue el elemento al final de la matriz.

Veamos un ejemplo más complejo: reduzca considerablemente la matriz [1, 2, 3, [4, [[[5, [6, 7]]]], 8]] a [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] 

Este ejemplo es muy similar al map , excepto que usamos recursividad aquí. No me detendré en la recursión en detalle, porque esto está más allá del alcance de nuestro tema, pero si desea saber más, vaya a este excelente recurso .

Declaración de spread (ES2015)
Estoy de acuerdo, este no es un método. Sin embargo, el operador de propagación ayuda a lograr diferentes objetivos cuando se trabaja con matrices. Puede aplicarlo para expandir los valores de una matriz en otra y luego hacer una copia o vincular varias matrices.

 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] 

Nota : la declaración de propagación hace una copia superficial de la matriz original. Pero, ¿qué significa "superficial"?

Dicha copia duplicará los elementos originales lo menos posible. Si tiene una matriz con números, cadenas o valores booleanos ( tipos primitivos ), no hay problemas y los valores están realmente duplicados. Sin embargo, las cosas son diferentes con los objetos y las matrices : solo se copia una referencia al valor original. Por lo tanto, si realiza una copia superficial de la matriz, incluido el objeto, y cambia el objeto en la matriz copiada, también se cambiará en el original, ya que tienen la misma referencia .

 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" }] 

Entonces, si desea crear una copia real de una matriz que contenga un objeto o matrices, puede usar una función lodash como cloneDeep . Pero no te consideres obligado a hacer esto. Su objetivo es descubrir cómo funciona todo bajo el capó .

Metodos utiles


A continuación encontrará otros métodos que también son útiles para conocer y que pueden ser útiles para resolver problemas como encontrar un elemento en una matriz, eliminar parte de una matriz y mucho más.

incluye (ES2015)
¿Alguna vez ha usado indexOf para averiguar si hay un elemento en una matriz o no? Una forma terrible de verificar, ¿verdad?

Afortunadamente, el método includes la validación por nosotros. Establezca el parámetro para inclusiones, y buscará el elemento en la matriz.

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

concat
El método concat se puede usar para fusionar dos o más matrices.

 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] 

por cada
Si desea realizar una acción para cada elemento de la matriz, puede usar el método forEach . Toma una función como parámetro, que a su vez también toma tres parámetros: valor actual, índice y matriz.

 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
Este método se utiliza para devolver el primer índice en el que se puede encontrar el elemento en la matriz. Además, indexOf menudo verifica la presencia de un elemento en una matriz. Honestamente, ahora lo uso con poca frecuencia.

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

encontrar
El método de find es similar al filter . Debe proporcionarle una función que pruebe cada elemento de la matriz. Sin embargo, find detiene los elementos de prueba tan pronto como encuentra uno que pasó la prueba. Este no es un filter que itera sobre toda la matriz, independientemente de las circunstancias.

 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' } 

Por lo tanto, utilice el método de filter cuando desee filtrar toda la matriz y el método de find cuando esté seguro de que está buscando un elemento único en la matriz.
findIndex
Este método es casi lo mismo que find , pero devuelve el índice del primer elemento encontrado en lugar del elemento en sí.

 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 

Puede pensar que findIndex e indexOf son lo mismo. En realidad no El primer parámetro de indexOf es un valor primitivo (un valor booleano, número, cadena, valor o carácter indefinido), mientras que el primer parámetro findIndex es una función de devolución de llamada.

Por lo tanto, cuando necesite encontrar el índice de un elemento en una matriz de valores primitivos, puede trabajar con indexOf . Si tiene elementos más complejos, como objetos, use findIndex .

cortar
Cuando necesite formar parte de una matriz o copiar una matriz, puede consultar el método de slice . Pero tenga cuidado: al igual que el operador de propagación, la slice devuelve una copia superficial .

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

Al comienzo del artículo, mencioné que los bucles a menudo son inútiles. Déjame mostrarte cómo deshacerte de ellos.

Suponga que desea devolver una cierta cantidad de mensajes de chat desde la API y solo necesita ver cinco de ellos. A continuación hay dos enfoques: uno con bucles, el otro con el método de 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) 

algunos
Si desea verificar si al menos un elemento de la matriz pasa la prueba, puede usar some . Al igual que map , filter o find , el método some toma una función de devolución de llamada como el único parámetro, y luego devuelve true si al menos un elemento pasa la verificación, y false si no.

some también son adecuados para trabajar con permisos.

 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 

cada
Este método es similar a some , excepto que verifica que cada elemento (y no uno ) coincida con la condición.

 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 

plano (ES2019)
Estos son métodos completamente nuevos en el mundo de JavaScript. Por lo general, flat crea una nueva matriz, conectando todos los elementos de una matriz anidada. Se necesita un parámetro: un número que indica cuánto desea reducir la dimensión de la matriz.

 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)
¿Adivina qué hace este método? Apuesto a que entenderás un nombre.

Primero, ejecuta la función de mapeo para cada elemento, y luego reduce la matriz a la vez. Tan simple como eso!

 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"] 

En este ejemplo, tiene muchas oraciones en la matriz y desea obtener todas las palabras. En lugar de usar el método de map y dividir todas las oraciones en palabras y luego acortar la matriz, puede usar flatMap .

Luego puede contar el número de palabras con la función reduce (esto no se aplica a flatMap , solo quiero mostrarle otro ejemplo del uso del método 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, } 

El método flatMap también se usa a menudo en la programación reactiva. Puedes ver un ejemplo aquí .

unirse
Si necesita crear una cadena basada en elementos de matriz, el método de join es lo que necesita. Le permite crear una nueva línea conectando todos los elementos de la matriz, separados por el separador proporcionado.

Por ejemplo, usando join puede mostrar visualmente a todos los participantes en una actividad.

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

Y este es un ejemplo más realista en el que primero puede filtrar a los participantes y obtener sus nombres.

 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 

de
Este es un método estático que crea una nueva matriz a partir de un objeto iterativo o similar a una matriz, como una cadena. Puede ser útil cuando trabaja con el modelo de objeto de documento.

 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! 

¿Viste que usamos un tipo de matriz en lugar de una instancia de matriz? Es por eso que este método se llama estático.

Luego, puede divertirse con los nodos, por ejemplo, registrar oyentes de eventos para cada uno de ellos utilizando el método forEach .

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

Modificación de matriz que vale la pena conocer


Los siguientes son otros métodos estándar. Su diferencia es que modifican la matriz original. No hay nada de malo en el cambio, pero debe considerarlo cuando trabaje.

Si no desea modificar la matriz original mientras trabaja con estos métodos, haga una copia completa o de superficie por adelantado.

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

ordenar
Sí, sort modifica la matriz original. De hecho, ordena los elementos de la matriz en su lugar. El método de ordenación predeterminado transforma todos los elementos en cadenas y los ordena en orden alfabético.

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

Tenga cuidado: si, por ejemplo, cambió del lenguaje Python, entonces el método de sort al trabajar con una matriz de números no le dará el resultado deseado.

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

¿Cómo, entonces, ordenar una matriz? El método de sort toma una función: una función de comparación . Toma dos parámetros: el primer elemento ( ) y el segundo elemento para la comparación ( b ). Una comparación entre estos dos elementos requiere que se devuelva un dígito:

  • si el valor es negativo, a ordena antes que b ;
  • si el valor es positivo, b ordena antes que a ;
  • si el valor es 0, no hay cambio.

Entonces puedes ordenar los números.

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

O puede ordenar las fechas de la última.

 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 } ] 

llenar
El método de fill modifica o llena todos los elementos de la matriz desde el índice inicial hasta el final con el valor especificado. Un ejemplo del excelente uso del fill es llenar una nueva matriz con datos iniciales.

 // 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" }] 

revertir
Me parece que el nombre del método explica completamente su esencia.

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

pop
Este método elimina el último elemento de la matriz y lo devuelve.

 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 

Métodos que pueden ser reemplazados


En la última sección, encontrará métodos que modifican la matriz original y que son fáciles de encontrar una alternativa. No estoy diciendo que necesiten un descuento, solo quiero transmitirles que algunos métodos tienen efectos secundarios y pueden reemplazarse.

empujar
Este método se usa con frecuencia. Le permite agregar uno o más elementos a la matriz, así como crear una nueva matriz basada en la anterior.

 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'] 

Si necesita construir una matriz sobre la base de otra, como en el método itemsIncremented , existen map adecuados, filter o itemsIncremented reduce que ya nos son familiares. Por ejemplo, podemos tomar el map para hacer esto.

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

Y si desea usar push cuando necesita agregar un nuevo elemento, entonces el operador de propagación es útil.

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

empalme
splice menudo se accede al splice para limpiar un elemento en un índice específico. Puede hacer lo mismo con el método de 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'] 

Puede preguntar: ¿qué pasa si necesito eliminar muchos elementos? Luego usa 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'] 

turno
El método shift elimina el primer elemento de la matriz y lo devuelve. Para hacer esto en el estilo de la programación funcional, puede usar la declaración spread o 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] 

no cambiar
El método unshift le permite agregar uno o más elementos al comienzo de una matriz. Al igual que shift , puede hacer esto con el operador de propagación.

 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


  • Cuando desee realizar algunas operaciones con una matriz, no use el bucle for y no reinvente la rueda, porque lo más probable es que exista un método de lo anterior que pueda hacer lo que necesita.
  • La mayoría de las veces usará el map , el filter , reduce métodos de reduce y el operador de propagación: estas son herramientas importantes para cualquier desarrollador.
  • También hay muchos métodos de matriz que sería bueno saber: slice , some , flatMap , etc. flatMap y aplique si es necesario.
  • Los efectos secundarios pueden conducir a cambios no deseados. Tenga en cuenta que algunos métodos modifican su matriz original.
  • El método de slice y el operador de propagación hacen copias superficiales. Como resultado, los objetos y las submatrices tendrán los mismos enlaces; también vale la pena tenerlo en cuenta.
  • Los métodos antiguos que modifican la matriz se pueden reemplazar por otros nuevos. Tú decides qué hacer.

Ahora ya sabe todo lo que debe saber sobre las matrices de JavaScript. Si te gustó este artículo , haz clic en el botón "Pat" (hasta 50 veces, si quieres :-)) y compártelo. ¡Y siéntase libre de compartir sus impresiones en los comentarios!

Source: https://habr.com/ru/post/446902/


All Articles