One place for hosting & domains

      Знакомство

      Знакомство с реляционными базами данных


      Введение

      Системы управления базами данных (СУБД) — это компьютерные программы, которые позволяют пользователям взаимодействовать с базой данных. СУБД позволяет пользователям контролировать доступ к базе данных, записывать данные, запускать запросы и выполнять любые другие задачи, связанные с управлением базами данных.

      Однако для выполнения любой из этих задач СУБД должна иметь в основе модель, определяющую организацию данных. Реляционная модель — это один из подходов к организации данных, который широко используется в программном обеспечении баз данных с момента своего появления в конце 60-х годов. Этот подход настолько распространен, что на момент написания данной статьи четыре из пяти самых популярных систем управления базами данных являются реляционными.

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

      История реляционной модели

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

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

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

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

      Пример иерархической базы данных: классификация животных

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

      В конце 60-х годов Эдгар Ф. Кодд (Edgar F. Codd), программист из IBM, разработал реляционную модель управления базами данных. Реляционная модель Кодда позволила связать отдельные записи с несколькими таблицами, что дало возможность устанавливать между точками данных отношения «много ко многим» в дополнение к «один ко многим». Это обеспечило большую гибкость по сравнению с другими существующими моделями, если говорить о разработке структур баз данных, а значит реляционные системы управления базами данных (РСУБД) могли удовлетворить гораздо более широкий спектр бизнес-задач.

      Кодд предложил язык для управления реляционными данными, известный как Alpha , оказавший влияние на разработку более поздних языков баз данных. Коллеги Кодда из IBM, Дональд Чемберлен (Donald Chamberlin) и Рэймонд Бойс (Raymond Boyce), создали один из языков под влиянием языка Alpha. Они назвали свой язык SEQUEL, сокращенное название от Structured English Query Language (структурированный английский язык запросов), но из-за существующего товарного знака сократили название до SQL (более формальное название — структурированный язык запросов).

      Из-за ограниченных возможностей аппаратного обеспечения ранние реляционные базы данных были все еще непозволительно медленными, и потребовалось некоторое время, прежде чем технология получила широкое распространение. Но к середине 80-х годов реляционная модель Кодда была внедрена в ряд коммерческих продуктов по управлению базами данных от компании IBM и ее конкурентов. Вслед за IBM, эти поставщики также стали разрабатывать и применять свои собственные диалекты SQL. К 1987 году Американский национальный институт стандартов и Международная организация по стандартизации ратифицировали и опубликовали стандарты SQL, укрепив его статус признанного языка для управления РСУБД.

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

      Как реляционные базы данных структурируют данные

      Теперь, когда у вас есть общее понимание истории реляционной модели, давайте более подробно рассмотрим, как данная модель структурирует данные.

      Наиболее значимыми элементами реляционной модели являются отношения, которые известны пользователям и современным РСУБД как таблицы. Отношения — это набор кортежей, или строк в таблице, где каждый кортеж имеет набор атрибутов, или столбцов:

      Пример диаграммы, отражающей связь отношений, кортежей и атрибутов друг с другом

      Столбец — это наименьшая организационная структура реляционной базы данных, представляющая различные ячейки, которые определяют записи в таблице. Отсюда происходит более формальное название — атрибуты. Вы можете рассматривать каждый кортеж в качестве уникального экземпляра чего-либо, что может находиться в таблице: категории людей, предметов, событий или ассоциаций. Такими экземплярами могут быть сотрудники компаний, продажи в онлайн-бизнесе или результаты лабораторных тестов. Например, в таблице с трудовыми записями учителей в школе кортежи могут иметь такие атрибуты, как name, subjects, start_date и т. д.

      При создании столбцов вы указываете тип данных, определяющий, какие записи могут вноситься в данный столбец. РСУБД часто используют свои собственные уникальные типы данных, которые могут не быть напрямую взаимозаменяемы с аналогичными типами данных из других систем. Некоторые распространенные типы данных включают даты, строки, целые числа и логические значения.

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

      Если у вас есть две таблицы, которые вы хотите связать друг с другом, можно сделать это с помощью внешнего ключа. Внешний ключ — это, по сути, копия основного ключа одной таблицы (таблицы «предка»), вставленная в столбец другой таблицы («потомка»). Следующий пример показывает отношения между двумя таблицами: одна используется для записи информации о сотрудниках компании, а другая — для отслеживания продаж компании. В этом примере первичный ключ таблицы EMPLOYEES используется в качестве внешнего ключа таблицы SALES:

      Пример диаграммы, показывающей, как первичный ключ таблицы EMPLOYEE действует в качестве внешнего ключа таблицы SALES

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

      Структурные элементы реляционной модели помогают хранить данные в структурированном виде, но хранение имеет значение только в том случае, если вы можете извлечь эти данные. Для извлечения информации из РСУБД вы можете создать запрос, т. е. структурированный запрос на набор информации. Как уже упоминалось ранее, большинство реляционных баз данных используют язык SQL для управления данными и отправки запросов. SQL позволяет фильтровать результаты и обрабатывать их с помощью различных пунктов, предикатов и выражений, позволяя вам контролировать, какие данные появятся в результате.

      Преимущества и ограничения реляционных баз данных

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

      Сегодня как SQL, так и базы данных, которые ее используют, несколько отклоняются от реляционной модели Кодда. Например, модель Кодда предписывает, что каждая строка в таблице должна быть уникальной, а по соображениям практической целесообразности большинство современных реляционных баз данных допускают дублирование строк. Есть и те, кто не считает базы данных на основе SQL истинными реляционными базами данных, если они не соответствуют каждому критерию реляционной модели по версии Кодда. Но на практике любая СУБД, которая использует SQL и в какой-то мере соответствует реляционной модели, может быть отнесена к реляционным системам управления базами данных.

      Хотя популярность реляционных баз данных стремительно росла, некоторое недостатки реляционной модели стали проявляться по мере того, как увеличивались ценность и объемы хранящихся данных. К примеру, трудно масштабировать реляционную базу данных горизонтально. Горизонтальное масштабирование или масштабирование по горизонтали — это практика добавления большего количества машин к существующему стеку, что позволяет распределить нагрузку, увеличить трафик и ускорить обработку. Часто это контрастирует с вертикальным масштабированием, которое предполагает модернизацию аппаратного обеспечения существующего сервера, как правило, с помощью добавления оперативной памяти или центрального процессора.

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

      Еще одно ограничение, существующее в РСУБД, заключается в том, что реляционная модель была разработана для управления структурированными данными, или данными, которые соответствуют заранее определенному типу данных, или, по крайней мере, каким-либо образом предварительно организованы. Однако с распространением персональных компьютеров и развитием сети Интернет в начале 90-х годов появились неструктурированные данные, такие как электронные сообщения, фотографии, видео и пр.

      Но все это не означает, что реляционные базы данных бесполезны. Напротив, спустя более 40 лет, реляционная модель все еще является доминирующей основой для управления данными. Распространенность и долголетие реляционных баз данных свидетельствуют о том, что это зрелая технология, которая сама по себе является главным преимуществом. Существует много приложений, предназначенных для работы с реляционной моделью, а также много карьерных администраторов баз данных, которые являются экспертами, когда дело доходит до реляционных баз данных. Также существует широкий спектр доступных печатных и онлайн-ресурсов для тех, кто хочет начать работу с реляционными базами данных.

      Еще одно преимущество реляционных баз данных заключается в том, что почти все РСУБД поддерживают транзакции. Транзакция состоит из одного или более индивидуального выражения SQL, выполняемого последовательно, как один блок работы. Транзакции представляют подход «все или ничего», означающий, что все операторы SQL в транзакции должны быть действительными. В противном случае вся транзакция не будет выполнена. Это очень полезно для обеспечения целостности данных при внесении изменений в несколько строк или в таблицы.

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

      Заключение

      Благодаря гибкости и проектному решению, направленному на сохранение целостности данных, спустя пятьдесят лет после появления такого замысла, реляционные базы данных все еще являются основным способом управления данными и их хранения. Даже с увеличением в последние годы числа разнообразных баз данных NoSQL понимание реляционной модели и принципов ее работы с РСУБД является ключевым моментом для всех, кто хочет создавать приложения, использующие возможности данных.

      Чтобы узнать больше о нескольких популярных РСУБД с открытым исходным кодом, мы рекомендуем вам ознакомиться с нашим сравнением различных реляционных баз данных с открытым исходным кодом. Если вам интересно узнать больше о базах данных в целом, мы рекомендуем вам ознакомиться с нашей полной библиотекой материалов о базах данных.



      Source link

      Знакомство с параметрами JavaScript по умолчанию


      Автор выбрал COVID-19 Relief Fund для получения пожертвования в рамках программы Write for DOnations.

      Введение

      В ECMAScript 2015 были введены параметры функций по умолчанию для языка JavaScript. Они позволяют разработчикам инициализировать функции со значениями по умолчанию, если при вызове функции не указываются аргументы. Такая инициализация параметров функций упрощает чтение функций, снижает вероятность ошибок и задает поведение функций по умолчанию. Это позволит избежать ошибок, вытекающих из передачи неопределенных (undefined​​) аргументов и деструктуризации несуществующих объектов.

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

      Аргументы и параметры

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

      В следующем блоке кода мы создадим функцию, возвращающую куб заданного числа, определяемого как x:

      // Define a function to cube a number
      function cube(x) {
        return x * x * x
      }
      

      Переменная x в этом примере является параметром, то есть переменной с именем, которая передается функции. Параметр всегда должен содержаться в переменной и никогда не может иметь прямого значения.

      Теперь рассмотрим следующий блок кода, вызывающий созданную нами функцию cube:

      // Invoke cube function
      cube(10)
      

      Результат будет выглядеть следующим образом:

      Output

      1000

      В данном случае 10 — это аргумент, то есть значение, передаваемое функции при ее вызове. Значение часто может также содержаться в переменной, как показано в следующем примере:

      // Assign a number to a variable
      const number = 10
      
      // Invoke cube function
      cube(number)
      

      Результат будет таким же:

      Output

      1000

      Если вы не передадите аргумент в функцию, которая ожидает его получить, функция будет использовать подразумеваемое значение undefined:

      // Invoke the cube function without passing an argument
      cube()
      

      Результат будет выглядеть следующим образом:

      Output

      NaN

      В данном случае cube() пытается рассчитать значение undefined * undefined * undefined и получает результат NaN («не число»). Дополнительную информацию можно найти в посвященном числам разделе статьи Типы данных в JavaScript.

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

      Синтаксис параметра по умолчанию

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

      Без параметров по умолчанию нам нужно будет явно проверять наличие значений undefined, чтобы задать значения по умолчанию, как показано в этом примере:

      // Check for undefined manually
      function cube(x) {
        if (typeof x === 'undefined') {
          x = 5
        }
      
        return x * x * x
      }
      
      cube()
      

      Здесь используется условное выражение для проверки автоматической передачи значения undefined, а затем задается значение x, равное 5. Результат выглядит следующим образом:

      Output

      125

      Использование параметров по умолчанию позволяет добиться того же результата с намного меньшим количеством кода. Вы можете задать значение по умолчанию для параметра функции cube, используя оператор равенства (=), как показано здесь:

      // Define a cube function with a default value
      function cube(x = 5) {
        return x * x * x
      }
      

      Теперь при вызове функции cube без аргумента она будет присваивать значение 5 переменной x и выводить результат расчета вместо NaN:

      // Invoke cube function without an argument
      cube()
      

      Output

      125

      При наличии аргумента функция будет выполняться обычным образом, игнорируя значение по умолчанию:

      // Invoke cube function with an argument
      cube(2)
      

      Output

      8

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

      // Invoke cube function with undefined
      cube(undefined)
      

      В результате будет провизведен расчет с переменной x, равной 5:

      Output

      125

      В этом случае были рассчитаны значения для параметра по умолчанию, и явно переданное значение undefined не заменило этот параметр.

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

      Типы данных параметров по умолчанию

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

      Задайте параметры, используя число, строку, логическое значение, объект, массив и нулевое значение в качестве значения по умолчанию. В этом примере будет использоваться синтаксис функции arrow:

      // Create functions with a default value for each data type
      const defaultNumber = (number = 42) => console.log(number)
      const defaultString = (string = 'Shark') => console.log(string)
      const defaultBoolean = (boolean = true) => console.log(boolean)
      const defaultObject = (object = { id: 7 }) => console.log(object)
      const defaultArray = (array = [1, 2, 3]) => console.log(array)
      const defaultNull = (nullValue = null) => console.log(nullValue)
      

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

      // Invoke each function
      defaultNumber()
      defaultString()
      defaultBoolean()
      defaultObject()
      defaultArray()
      defaultNull()
      

      Output

      42 "Shark" true {id: 7} (3) [1, 2, 3] null

      Любой объект, создаваемый в параметре по умолчанию, будет создаваться при каждом вызове функции. Часто такое поведение параметров по умолчанию используется для получения значений из объекта. Если вы пытаетесь провести деструктурирование или получить значение из несуществующего объекта, будет выведена ошибка. Если же параметр по умолчанию представляет собой пустой объект, он просто выдаст значения undefined вместо ошибки:

      // Define a settings function with a default object
      function settings(options = {}) {
        const { theme, debug } = options
      
        // Do something with settings
      }
      

      Это позволяет избежать ошибок, вызванных деструктурированием несуществующих объектов.

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

      Использование нескольких параметров по умолчанию

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

      Вначале декларируем функцию sum() с несколькими параметрами по умолчанию:

      // Define a function to add two values
      function sum(a = 1, b = 2) {
        return a + b
      }
      
      sum()
      

      По умолчанию производится следующий расчет:

      Output

      3

      Кроме того, значение параметра может использоваться в любом последующем параметре по умолчанию, слева направо. Например, данная функция createUser создает пользовательский объект userObj как третий параметр, и функция просто возвращает userObj с первыми двумя параметрами:

      // Define a function to create a user object using parameters
      function createUser(name, rank, userObj = { name, rank }) {
        return userObj
      }
      
      // Create user
      const user = createUser('Jean-Luc Picard', 'Captain')
      

      Если вы вызовете user, вы получите следующий результат:

      Output

      {name: "Jean-Luc Picard", rank: "Captain"}

      Обычно рекомендуется размещать все параметры по умолчанию в конце списка параметров, чтобы можно было легко опускать необязательные значения. Если вы используете параметр по умолчанию первым, вам нужно будет явно передать undefined для использования значения по умолчанию.

      Вот пример с параметром по умолчанию в начале списка:

      // Define a function with a default parameter at the start of the list
      function defaultFirst(a = 1, b) {
        return a + b
      }
      

      При вызове этой функции нужно вызвать defaultFirst() с двумя аргументами:

      defaultFirst(undefined, 2)
      

      Результат будет выглядеть следующим образом:

      Output

      3

      Вот пример с параметром по умолчанию в конце списка:

      // Define a function with a default parameter at the end of the list
      function defaultLast(a, b = 1) {
        return a + b
      }
      
      defaultLast(2)
      

      В результате будет получено то же значение:

      Output

      3

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

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

      // Define function to create an element
      function createNewElement(tag, text, classNames = []) {
        const el = document.createElement(tag)
        el.textContent = text
      
        classNames.forEach(className => {
          el.classList.add(className)
        })
      
        return el
      }
      

      Вы можете вызвать функцию с несколькими классами в массиве:

      const greeting = createNewElement('p', 'Hello!', ['greeting', 'active'])
      

      При вызове greeting значение будет следующим:

      Output

      <p class="greeting active">Hello!</p>

      Если вы оставите массив classNames вне вызова функции, функция все равно сработает.

      const greeting2 = createNewElement('p', 'Hello!')
      

      greeting2 теперь имеет следующее значение:

      Output

      <p>Hello!</p>

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

      Output

      VM2673:5 Uncaught TypeError: Cannot read property 'forEach' of undefined at createNewElement (<anonymous>:5:14) at <anonymous>:12:18

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

      Вызовы функций как параметры по умолчанию

      Помимо примитивов и объектов, в качестве параметра по умолчанию можно использовать результат вызова функции.

      В этом блоке кода мы создадим функцию, возвращающую случайное число, и используем результат как значение параметра по умолчанию для функции cube:

      // Define a function to return a random number from 1 to 10
      function getRandomNumber() {
        return Math.floor(Math.random() * 10)
      }
      
      // Use the random number function as a default parameter for the cube function
      function cube(x = getRandomNumber()) {
        return x * x * x
      }
      

      Теперь при каждом вызове функции cube без параметра результаты могут отличаться:

      // Invoke cube function twice for two potentially different results
      cube()
      cube()
      

      Вывод вызова этих функций будет отличаться:

      Output

      512 64

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

      В следующем примере в качестве значения x назначается случайное число, которое используется как параметр для созданной нами функции cube. Параметр y рассчитывает кубический корень числа и проверяет равенство x и y:

      // Assign a random number to x
      // Assign the cube root of the result of the cube function and x to y
      function doesXEqualY(x = getRandomNumber(), y = Math.cbrt(cube(x))) {
        return x === y
      }
      
      doesXEqualY()
      

      В результате вы получите следующий вывод:

      Output

      true

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

      // Define a function with a default parameter that is an anonymous function
      function outer(
        parameter = function inner() {
          return 100
        }
      ) {
        return parameter()
      }
      
      // Invoke outer function
      outer()
      

      Output

      100

      Внутренняя функция создается с нуля каждый раз при вызове внешней функции.

      Заключение

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

      Если вы хотите узнать больше о JavaScript, перейдите на главную страницу нашей серии материалов Написание кода на JavaScript или перейдите к нашей серии Написание кода на Node.js для изучения материалов по серверной разработке.



      Source link

      Знакомство с объектами map и set в JavaScript


      Автор выбрал фонд Open Internet/Free Speech для получения пожертвования в рамках программы Write for DOnations.

      В JavaScript разработчики часто тратят много времени на решение того, какую структуру данных следует использовать. Это вызвано тем, что выбор правильной структуры данных упрощает последующее управление этими данными, экономя время и упрощая понимание кода. Двумя преобладающими структурами для хранения коллекций данных являются объекты и массивы (тип объекта). Разработчики используют объекты для хранения пары ключ/значение и массивы для хранения индексированных списков. Однако, чтобы предоставить разработчикам больше гибкости, в спецификации ECMAScript 2015 появились два новых типа итерируемых объектов: карты (map), которые являются упорядоченными коллекциями пар ключ/значение, и сеты (set), которые содержат список уникальных значений.

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

      Карты

      Map — это коллекция пар ключ/значение, которая может использовать любой тип данных в качестве ключа и поддерживает порядок своих записей. Карты содержат как характеристики объектов (коллекция пар ключ/значение), так и массивов (упорядоченная коллекция), но имеют больше сходства с объектами. Это связано с тем, что хотя размер и порядок записей сохраняется, как и в массиве, сами по себе записи являются парами ключ/значение, как объекты.

      Карты можно инициализировать с помощью синтаксиса new Map():

      const map = new Map()
      

      В результате будет создана пустая карта:

      Output

      Map(0) {}

      Добавление значений в карту

      Вы можете добавить значения в карту с помощью метода set(). Первый аргумент будет ключом, а второй — значением.

      Представленный синтаксис добавляет в карту три пары ключ/значение:

      map.set('firstName', 'Luke')
      map.set('lastName', 'Skywalker')
      map.set('occupation', 'Jedi Knight')
      

      Здесь мы начинаем понимать, что карты имеют черты объектов и массивов. Как и в случае массива, у нас есть индексированная коллекция, и мы можем увидеть количество элементов в карте по умолчанию. Карты используют синтаксис => для обозначения пар ключ/значение как key => value:

      Output

      Map(3) 0: {"firstName" => "Luke"} 1: {"lastName" => "Skywalker"} 2: {"occupation" => "Jedi Knight"}

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

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

      [ [ 'key1', 'value1'], ['key2', 'value2'] ]
      

      Используя следующий синтаксис, мы можем воссоздать ту же самую карту:

      const map = new Map([
        ['firstName', 'Luke'],
        ['lastName', 'Skywalker'],
        ['occupation', 'Jedi Knight'],
      ])
      

      Примечание. Данный пример использует завершающие запятые, также именуемые как подвешенные запятые. Это практика форматирования JavaScript, когда конечный элемент в серии при объявлении коллекции данных имеет запятую в конце. Хотя это форматирование можно использовать для получения более чистых версий кода и облегчения работы с кодом, использовать его или нет решает разработчик. Дополнительную информацию о завершающих запятых см. в статье о завершающих запятых в веб-документации MDN.

      Кстати, этот синтаксис выглядит так же, как и результат вызова Object.entries() для объекта. Это дает готовый способ для преобразования объекта в карту, как показано в следующем блоке кода:

      const luke = {
        firstName: 'Luke',
        lastName: 'Skywalker',
        occupation: 'Jedi Knight',
      }
      
      const map = new Map(Object.entries(luke))
      

      Также вы можете превратить карту обратно в объект или массив с помощью одной строки кода.

      Код ниже преобразует карту в объект:

      const obj = Object.fromEntries(map)
      

      В результате будет получено следующее значение obj:

      Output

      {firstName: "Luke", lastName: "Skywalker", occupation: "Jedi Knight"}

      Теперь мы преобразуем карту в массив:

      const arr = Array.from(map)
      

      В результате будет получен следующий массив arr:

      Output

      [ ['firstName', 'Luke'], ['lastName', 'Skywalker'], ['occupation', 'Jedi Knight'] ]

      Ключи карты

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

      Давайте проинициализируем карту нестроковыми ключами:

      const map = new Map()
      
      map.set('1', 'String one')
      map.set(1, 'This will be overwritten')
      map.set(1, 'Number one')
      map.set(true, 'A Boolean')
      

      В этом примере будет переопределен первый ключ 1 на последующий ключ и будет рассмотрено использование строки '1' и числа 1 в качестве уникальных ключей:

      Output

      0: {"1" => "String one"} 1: {1 => "Number one"} 2: {true => "A Boolean"}

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

      В качестве примера выполните инициализацию объекта с числовым ключом и сравните числовой ключ 1 со строковым ключом "1":

      // Initialize an object with a numerical key
      const obj = { 1: 'One' }
      
      // The key is actually a string
      obj[1] === obj['1']  // true
      

      Поэтому, если вы попробуете использовать объект в качестве ключа, он будет отображать строку object Object.

      В качестве примера создайте объект и используйте его в качестве ключа другого объекта:

      // Create an object
      const objAsKey = { foo: 'bar' }
      
      // Use this object as the key of another object
      const obj = {
        [objAsKey]: 'What will happen?'
      }
      

      В результате вы получите следующее:

      Output

      {[object Object]: "What will happen?"}

      Такой подход с картой не работает. Попробуйте создать объект и задать его как ключ для карты:

      // Create an object
      const objAsKey = { foo: 'bar' }
      
      const map = new Map()
      
      // Set this object as the key of a Map
      map.set(objAsKey, 'What will happen?')
      

      Ключом элемента карты теперь является созданный нами объект.

      Output

      key: {foo: "bar"} value: "What will happen?"

      Необходимо отметить один важный момент, касающийся использования объекта или массива в качестве ключа: карта использует ссылку на объект для сравнения, а не литеральное значение объекта. В JavaScript {} === {} возвращает false, поскольку оба объекта не являются одинаковыми объектами, несмотря на то, что у них одно (пустое) значение.

      Это означает, что при добавлении двух уникальных объектов с одинаковым значением будет создана карта с двумя записями:

      // Add two unique but similar objects as keys to a Map
      map.set({}, 'One')
      map.set({}, 'Two')
      

      В результате вы получите следующее:

      Output

      Map(2) {{…} => "One", {…} => "Two"}

      Но при использовании одной ссылки на объект дважды будет создана карта с одной записью.

      // Add the same exact object twice as keys to a Map
      const obj = {}
      
      map.set(obj, 'One')
      map.set(obj, 'Two')
      

      Результат будет выглядеть следующим образом:

      Output

      Map(1) {{…} => "Two"}

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

      Получение и удаление элементов карты

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

      Мы можем проинициализировать новую карту для демонстрации следующих методов и свойств: delete(), has(), get() и size.

      // Initialize a new Map
      const map = new Map([
        ['animal', 'otter'],
        ['shape', 'triangle'],
        ['city', 'New York'],
        ['country', 'Bulgaria'],
      ])
      

      Используйте метод has() для проверки наличия элемента в карте. has() будет возвращать булево значение.

      // Check if a key exists in a Map
      map.has('shark') // false
      map.has('country') // true
      

      Используйте метод get() для получения значения по ключу.

      // Get an item from a Map
      map.get('animal') // "otter"
      

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

      // Get the count of items in a Map
      map.size // 4
      

      Используйте метод delete(), чтобы удалить элемент карты по ключу. Метод будет возвращать булево значение — true, если элемент существовал и был удален, и false, если такой элемент не найден.

      // Delete an item from a Map by key
      map.delete('city') // true
      

      В результате будет получена следующая карта:

      Output

      Map(3) {"animal" => "otter", "shape" => "triangle", "country" => "Bulgaria"}

      Наконец, вы можете удалить все значения карты с помощью map.clear().

      // Empty a Map
      map.clear()
      

      Результат будет выглядеть следующим образом:

      Output

      Map(0) {}

      Ключи, значения и записи для карт

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

      Методы keys(), values() и entries() возвращают MapIterator, который похож на массив, где вы можете использовать цикл for...of для прохождения по всем значениям.

      Здесь представлен другой пример карты, который мы можем использовать для демонстрации этих методов:

      const map = new Map([
        [1970, 'bell bottoms'],
        [1980, 'leg warmers'],
        [1990, 'flannel'],
      ])
      

      Метод keys() возвращает ключи:

      map.keys()
      

      Output

      MapIterator {1970, 1980, 1990}

      Метод values() возвращает значения:

      map.values()
      

      Output

      MapIterator {"bell bottoms", "leg warmers", "flannel"}

      Метод entries() возвращает массив пар ключ/значение:

      map.entries()
      

      Output

      MapIterator {1970 => "bell bottoms", 1980 => "leg warmers", 1990 => "flannel"}

      Итерация по карте

      Карта имеет встроенный метод forEach, как и в случае с массивом, для встроенной итерации по карте. Однако есть определенное количество отличий в том, как они выполняют итерацию. Обратный вызов forEach карты проходит по value, key и map, в то время как версия для массива проходит по item, index и array.

      // Map
      Map.prototype.forEach((value, key, map) = () => {})
      
      // Array
      Array.prototype.forEach((item, index, array) = () => {})
      

      Это огромное преимущество карты над объектом, так как объект нужно преобразовывать для использования keys(), values() или entries(), а простого способа получения свойств объекта без преобразования не существует.

      Чтобы продемонстрировать это, давайте пробежимся по нашей карте и выведем пары ключ/значение в консоль:

      // Log the keys and values of the Map with forEach
      map.forEach((value, key) => {
        console.log(`${key}: ${value}`)
      })
      

      Это даст нам следующее:

      Output

      1970: bell bottoms 1980: leg warmers 1990: flannel

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

      // Destructure the key and value out of the Map item
      for (const [key, value] of map) {
        // Log the keys and values of the Map with for...of
        console.log(`${key}: ${value}`)
      }
      

      Свойства и методы карты

      В нижеследующей таблице содержится список свойств и методов карты для быстрого просмотра:

      Свойства/методыОписаниеВозвращает
      set(key, value)Добавляет пару ключ/значение в картуКарта Объект
      delete(key)Удаляет пару ключ/значение из карты по ключуБулево значение
      get(key)Возвращает значение по ключузначение
      has(key)Проверяет наличие элемента в карте по ключуБулево значение
      clear()Удаляет все элементы из картыНет данных
      keys()Возвращает все ключи в картеОбъект MapIterator
      values()Возвращает все значения в картеОбъект MapIterator
      entries()Возвращает все ключи и значения в карте в виде [key, value]Объект MapIterator
      forEach()Выполняет итерацию по карте в порядке добавленияНет данных
      sizeВозвращает количество элементов в картеЧисло

      Когда нужно использовать карту

      В целом карты напоминают объекты внутри, которые хранят пары ключ/значение, но карты обладают рядом преимуществ над объектами:

      • Размер — карты имеют свойство size, в то время как объекты не имеют встроенного способа получения размера.
      • Итерация — карты являются итерируемыми, а объекты нет.
      • Гибкость — карты могут содержать любой тип данных (примитивы или объекты) в качестве ключа для значения, а объекты могут хранить только строки.
      • Упорядоченность — карты сохраняют порядок добавления, а объекты не имеют гарантированного порядка.

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

      • JSON — объекты отлично работают с JSON.parse() и JSON.stringify(), двумя основными функциями для работы с JSON, стандартным форматом данных, с которым работает множество REST API.
      • Работа с одним элементом — работая с известным значением в объекте, вы можете получить к нему доступ напрямую с помощью ключа без необходимости использования метода, например метода get() карты.

      Этот список поможет вам решить, подходит ли в вашем конкретном случае карта или объект.

      Сет

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

      Вы можете инициализировать сеты с помощью синтаксиса new Set().

      const set = new Set()
      

      В результате будет создан пустой сет:

      Output

      Set(0) {}

      Элементы можно добавить в сет с помощью метода add() (не путайте с методом set(), доступным для карты, хотя они и похожи).

      // Add items to a Set
      set.add('Beethoven')
      set.add('Mozart')
      set.add('Chopin')
      

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

      set.add('Chopin') // Set will still contain 3 unique values
      

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

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

      // Initialize a Set from an Array
      const set = new Set(['Beethoven', 'Mozart', 'Chopin', 'Chopin'])
      

      Output

      Set(3) {"Beethoven", "Mozart", "Chopin"}

      Аналогично сет можно преобразовывать в массив с помощью одной строки кода:

      const arr = [...set]
      

      Output

      (3) ["Beethoven", "Mozart", "Chopin"]

      Сеты имеют множество общих с картой методов и свойств, включая delete(), has(), clear() и size.

      // Delete an item
      set.delete('Beethoven') // true
      
      // Check for the existence of an item
      set.has('Beethoven') // false
      
      // Clear a Set
      set.clear()
      
      // Check the size of a Set
      set.size // 0
      

      Обратите внимание, что сет не позволяет получить доступ к значению по ключу или индексу, как, например, Map.get(key) или arr[index].

      Ключи, значения и записи для сетов

      Карта и сет имеют методы keys(), values() и entries(), которые возвращают итератор. Однако, хотя каждый из этих методов имеет конкретную функцию в карте, у сетов нет ключей, поэтому ключи являются псевдонимами для значений. Это означает, что keys() и values() будут возвращать один и тот же итератор, а entries() будет возвращать значение дважды. В случае с сетом только метод values()​​​ имеет смысл, а два других метода существуют для обеспечения согласованности и кросс-совместимости с картой.

      const set = new Set([1, 2, 3])
      // Get the values of a set
      set.values()
      

      Output

      SetIterator {1, 2, 3}

      Итерация сета

      Как и карта, сет имеет встроенный метод forEach(). Поскольку у сетов нет ключей, первый и второй параметр обратного вызова forEach() возвращает одно и то же значение, так что нет необходимости его использования без совместимости с картой. Параметрами forEach() являются (value, key, set).

      При работе с сетом можно использовать forEach() и for...of. Вначале рассмотрим итерацию с помощью forEach():

      const set = new Set(['hi', 'hello', 'good day'])
      
      // Iterate a Set with forEach
      set.forEach((value) => console.log(value))
      

      Затем мы сможем написать версию for...of:

      // Iterate a Set with for...of
      for (const value of set) {  
          console.log(value);
      }
      

      В обоих случаях нам потребуется следующее:

      Output

      hi hello good day

      Свойства и методы сета

      В нижеследующей таблице содержится список свойств и методов сета для быстрого просмотра:

      Свойства/методыОписаниеВозвращает
      add(value)Добавляет новый элемент в сетСет Объект
      delete(value)Удаляет заданный элемент из сетаБулево значение
      has()Проверяет наличие элемента в сетеБулево значение
      clear()Удаляет все элементы из сетаНет данных
      keys()Возвращает все значения сета (аналогично values())Объект SetIterator
      values()Возвращает все значения сета (аналогично keys())Объект SetIterator
      entries()Возвращает все значения сета в виде [value, value]Объект SetIterator
      forEach()Выполняет итерацию по сету в порядке вставкиНет данных
      sizeВозвращает количество элементов сетаЧисло

      Когда нужно использовать сет

      Сет — это полезное дополнение для вашего набора инструментов JavaScript, особенно при работе с дублирующими значениями в данных.

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

      const uniqueArray = [ ...new Set([1, 1, 2, 2, 2, 3])] // (3) [1, 2, 3]
      

      Это даст нам следующее:

      Output

      (3) [1, 2, 3]

      Сет можно использовать для поиска идентичных, пересекающихся и отличных элементов двух наборов данных. Однако массивы имеют ряд преимуществ, включая дополнительные инструменты манипуляции данными благодаря методам sort(), map(), filter() и reduce(), а также прямую совместимость с методами JSON.

      Заключение

      Из этой статьи мы узнали, что карта представляет собой коллекцию упорядоченных пар ключ/значение, а сет — это коллекция уникальных значений. Обе эти структуры дают JavaScript дополнительные возможности и упрощают выполнение стандартных задач, например поиск длины коллекции пар ключ/значение и удаление дублирующих элементов из набора данных соответственно. С другой стороны, объекты и массивы традиционно используются для хранения и обработки данных в JavaScript, а также имеют прямую совместимость с JSON, что позволяет им оставаться самыми важными структурами данных, особенно при работе с REST API. Карты и сеты часто используются в качестве вспомогательных структур данных для объектов и массивов.

      Если вы хотите узнать больше о JavaScript, перейдите на главную страницу нашей серии материалов Написание кода на JavaScript или перейдите к нашей серии Написание кода на Node.js для изучения материалов по серверной разработке.



      Source link