Проверка существования свойства

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

hasOwnProperty

obj.hasOwnProperty('propertyName')

Метод hasOwnProperty принимает на вход имя проверяемого свойства и возвращает булево значение: true — если такое свойство есть в объекте, и false — если нет.

const obj = {
  color: 'red',
  height: 10,
};

obj.hasOwnProperty('color'); // true

const propertyName = 'height';
obj.hasOwnProperty(propertyName); // true

obj.hasOwnProperty('nonexistent'); // false

Свойства с именем nonexistent в объекте obj нет, поэтому метод hasOwnProperty вернул значение false.

Давайте попробуем решить задачку подсчёта количества продуктов в сумке :) Допустим, наша сумка представлена вот таким массивом:

const bag = [
  'apple', 'banana', 'pear',
  'apricot', 'apple', 'banana',
  'apple', 'orange', 'pear',
];

Напишем функцию fruitsCounter, подсчитывающую количество каждого вида фруктов в сумке. То есть функция на основе массива должна сформировать объект, каждый ключ которого — это определённый фрукт, а значение — количество:

fruitsCounter(bag); // { apple: 3, banana: 2, pear: 2, apricot: 1, orange: 1 }

Алгоритм следующий:

  1. Подготавливаем объект-результат — аккумулятор, в котором будем хранить фрукты и их количество.
  2. Перебираем массив фруктов. Для каждого фрукта определяем, есть ли соответствующее ему свойство в формируемом объекте-результате:
    • Если свойства нет (то есть фрукт ранее не подсчитывали), то добавляем его в объект и присваиваем значение 1 (единица).
    • Если свойство есть (то есть ранее фрукт уже учитывали), то увеличиваем его количество на единицу.
  3. Возвращаем итоговый объект.
const fruitsCounter = (fruits) => {
  const statistics = {};

  for (const fruit of fruits) {
    if (statistics.hasOwnProperty(fruit)) {
      statistics[fruit] += 1;
    } else {
      statistics[fruit] = 1;
    }
  }

  return statistics;
};

Новички часто совершают ошибку: вместо проверки существования свойства пытаются взять значение у проверяемого свойства и проверить это значение.

// расчёт на особенность JS, что
// обращение к несуществующему свойству
// вернёт значение undefined
if (statistics[fruit] === undefined)

или

// расчёт на то, что undefined
// в логическом контексте — false
if (!statistics[fruit])

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

В примере с фруктами логической ошибки не произойдёт, потому что это очень простой пример. Но будет нарушена семантика кода, ведь мы делаем косвенные, опосредованные проверки, то есть работаем со следствиями, а не причинами. Таким образом, мы, как создатели кода, "скрываем" свои истинные намерения от тех, кто будет читать и анализировать его. Проверка if (statistics.hasOwnProperty(fruit)) явно (насколько это позволяют выразительные средства языка) даёт понять, что проверяется существование свойства. Проверка, наподобие, if (statistics[fruit]) может вызвать непонимание и вопросы "зачем так делается?". Да, опытный программист скорее всего разгадает истинный смысл проверки, но в любом случае такой код окажется тяжелее для чтения и восприятия.

Рассмотрим пример с логической ошибкой, она часто появляется при сравнении объектов. Допустим, у нас есть две карты университетских дисциплин для первого и второго семестров:

const firstSemesterSubjects = {
  chemistry: {
    faculty: 'Chemistry faculty',
    teacher: 'Ivanov',
  },
  law: {
    // какие-то характеристики
  },
  informatics: null,
  microeconomics: {
    // какие-то характеристики
  },
};

const secondSemesterSubjects = {
  microeconomics: null,
  chemistry: {
    faculty: 'Chemistry faculty',
    teacher: 'Ivanov',
  },
  informatics: {
    // какие-то характеристики
  },
};

Задача состоит в том, чтобы определить, какие дисциплины первого семестра были убраны во втором семестре. Опять же, это упрощённая задача, но её достаточно, чтобы схватить суть. Условимся, если дисциплина имеет значение null, это означает, что она существует, но на момент создания карты данные по ней не были определены или уточнены (возможно, это произойдёт позже) — реальная жизнь накладывает свои сложности. Алгоритм может быть таким:

  1. Подготавливаем массив-результат — аккумулятор, в котором будем вести список убранных дисциплин.
  2. Получаем все дисциплины первого семестра. Дисциплины — это ключи, для получения массива ключей у объекта есть специальный метод Object.keys().
  3. Перебираем дисциплины первого семестра. Для каждой дисциплины определяем, есть ли она в карте дисциплин второго семестра:
    • Если свойства нет (то есть дисциплина была убрана во втором семестре), то добавляем её в массив-результат.
  4. Возвращаем результирующий массив.
const getRemovedSubjects = (first, second) => {
  const removedSubjects = [];
  const firstSemesterSubjects = Object.keys(first);

  for (const subject of firstSemesterSubjects) {
    if (!second.hasOwnProperty(subject)) {
      removedSubjects.push(subject);
    }
  }

  return removedSubjects;
};

getRemovedSubjects(firstSemesterSubjects, secondSemesterSubjects);
// ['law']

Во втором семестре была убрана дисциплина 'law', функция правильно отработала и вернула массив с единственным элементом: ['law'].

Теперь давайтe поэкспериментируем и заменим прямую проверку свойства на косвенную проверку по значению:

const getRemovedSubjects = (first, second) => {
  const removedSubjects = [];
  const firstSemesterSubjects = Object.keys(first);

  for (const subject of firstSemesterSubjects) {
    if (!second[subject]) {
      removedSubjects.push(subject);
    }
  }

  return removedSubjects;
};

getRemovedSubjects(firstSemesterSubjects, secondSemesterSubjects);
// ['law', 'microeconomics']

Как видно, алгоритм отработал неправильно — он посчитал, что, помимо дисциплины law, во втором семестре была убрана микроэкономика microeconomics. Это произошло из-за того, что в логическом контексте false-значением является не только undefined, но и null (в js существует ещё куча других false-значений). Таким образом наша косвенная проверка противоречит допущению реальной жизни, что в ключе объекта может быть записано значение null.

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

Функция has из библиотеки lodash

У встроенного метода hasOwnProperty есть хорошая альтернатива — функция has из библиотеки lodash.

_.has(object, path);

Функция has первым параметром принимает объект, у которого проверяется свойство, вторым параметром — путь до свойства (строка или массив). Возвращает булево значение: true — если такое свойство по указанному пути есть в объекте, и false — если нет.

Её преимущества:

  • Она более надёжна. Дело в том, что в JavaScript программист может переопределять свойства объекта, в том числе свойство hasOwnProperty. Причины для такого переопределения могут быть разные: от невнимательности и ошибки до необходимости в имплементации сложной логики. Но, если уж так произошло, то код, использующий прямой вызов метода hasOwnProperty на проверяемом объекте, будет работать некорректно или вовсе упадёт с ошибкой:

    const object = { a: { b: 2 } };
    
    // вызываем встроенный метод
    console.log(object.hasOwnProperty('a')); // => true
    
    // переопределяем: подменяем встроенный метод другой функцией
    object.hasOwnProperty = () => 'lalala!!!';
    console.log(object.hasOwnProperty('a')); // => 'lalala!!!'
    
    // переопределяем: вместо функции пишем значение другого типа
    object.hasOwnProperty = false;
    console.log(object.hasOwnProperty('a'));
    // TypeError: object.hasOwnProperty is not a function
    

    Функция has лишена этого недостатка:

    import _ from 'lodash';
    
    const object = { a: { b: 2 } };
    
    // вызываем функцию has
    console.log(_.has(object, 'a')); // true
    
    // переопределяем: подменяем встроенный метод другой функцией
    object.hasOwnProperty = () => 'lalala!!!';
    console.log(object.hasOwnProperty('a')); // => 'lalala!!!'
    
    // снова вызываем функцию has
    console.log(_.has(object, 'a')); // true
    

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

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

  • Она более гибкая, позволяет проверять цепочки свойств:

    const object = { a: { b: { b: 2 } } };
    
    // вызываем функцию has
    console.log(_.has(object, 'a')); // => true
    
    // вторым параметром передаём путь до вложенного свойства
    console.log(_.has(object, 'a.b')); // => true
    // вместо строки, путь можно передать в форме массива
    console.log(_.has(object, ['a', 'b'])); // => true
    
    // путь в форме строки
    console.log(_.has(object, 'a.b.b')); // => true
    // путь в форме массива
    console.log(_.has(object, ['a', 'b', 'b'])); // => true
    

Дополнительные материалы

  1. Функция has из библиотеки Lodash

Для полного доступа к курсу, нужна профессиональная подписка

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

Получить доступ
115
курсов
892
упражнения
2241
час теории
3196
тестов

Зарегистрироваться

или войти в аккаунт

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно.

  • 115 курсов, 2000+ часов теории
  • 800 практических заданий в браузере
  • 250 000 студентов

Нажимая кнопку «Зарегистрироваться», вы даёте своё согласие на обработку персональных данных в соответствии с «Политикой конфиденциальности» и соглашаетесь с «Условиями оказания услуг».

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Rambler
Логотип компании Bookmate
Логотип компании Botmother

Есть вопрос или хотите участвовать в обсуждении?

Зарегистрируйтесь или войдите в свой аккаунт

Нажимая кнопку «Зарегистрироваться», вы даёте своё согласие на обработку персональных данных в соответствии с «Политикой конфиденциальности» и соглашаетесь с «Условиями оказания услуг».