Pull to refresh

Вся боль undefined

Reading time4 min
Views10K

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

Отказ от ответственности

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

Неинициализированные переменные

Эту проблему решают статические анализаторы и TypeScript, но помнить о ней все же стоит.

/**
 * @param {number} op1
 * @param {number} op2
 * @returns {number}
 */
function sum(op1, op2) {
  return op1 + op2;
}

/**
 * @type {number}
 */
let a; // undefined

// ...

if (condition) { // это условие не выполнилось
  a = 10;
}

/**
 * @type {number}
 */
let b = 10;

const result = sum(a, b);

a = 1; // поздно

console.log('sum =', result); // sum = NaN

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

function sum(op1, op2) {
	return op1 + op2;
}

let a;

(async () => {
	a = await getRemoteAValue();
})();

let b = 10;

const result = sum(a, b); // очевидно выполнится до того, как переменная a будет инициализирована

console.log('sum =', result); // sum = NaN

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

Сериализация/десериализация

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

Возьмем очень простой объект, состоящий из трех полей (для удобства, структура объекта будет описана в виде интерфейса TypeScript):

interface SimpleObject {
  id: number;
  name: string | null;
  desc?: string;
}

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

const obj = { id: 1, name: null, desc: undefined };

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

for (const key in obj) {
  console.log(`key: ${key}, value: ${obj[key]}`);
}

Результат:

key: id, value: 1
key: name, value: null
key: desc, value: undefined

Прекрасно, теперь сериализуем его в JSON, передадим по сети, десериализуем и вновь выполним обход:

const serialized = JSON.stringify(obj);
// где-то здесь происходит передача
// код далее как бы выполняется на другой стороне соединения
const incoming = JSON.parse(serialized);

for (const key in incoming) {
  console.log(`key: ${key}, value: ${obj[key]}`);
}

Результат:

key: id, value: 1
key: name, value: null

Уппс! Погодите-ка, а куда же делось наше поле desc? Дело в том, что в JSON нет типа undefined, для него поле со значением undefined - это отсутствующее поле, если вывести значение serialized — получим следующий текст:

{"id":1,"name":null}

При этом десериализованный объект (incoming) все еще соответствует интерфейсу SimpleObject, так как попытка получить значение поля desc в любом случае даст значение undefined:

console.log(obj.desc); // undefined
console.log(incoming.desc); // undefined

В 90% случаев все будет нормально и не будет являться проблемой ровно до той поры, пока мы не попытаемся произвести сравнение этих объектов. Создадим копию объекта obj и, при помощи lodash.isEqual сравним копию и десериализованный результат с оригиналом:

import isEqual from 'lodash/isEqual';

const objCopy = { ...obj }; // не будем заморачиваться с глубоким копированием, так как все поля нашего объекта - примитивы

console.log(isEqual(obj, incoming)); // false
console.log(isEqual(obj, objCopy)); // true

Какая неприятная неожиданность! Эта неприятность может нас поджидать во многих случаях, например, если мы хотим сохранить сериализованное значение где-либо, а потом подгрузить и сравнить с текущим (redis, РСУБД, файл, localStorage).

Решается тривиально: использовать null для значений полей вместо undefined. К сожалению, это возможно не всегда. Например, если удаленный ресурс вам не принадлежит и уже использует опциональные поля. В таком случае, банального isEqual будет уже недостаточно — придется добавить дополнительных проверок, либо добавлять undefined-поле сразу после десериализации сохраненного значения. Другими словами, требуется провести валидацию десериализованных данных.

Вывод

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

Ссылка на репозиторий с примерами.

P.S.: Ничего не имею против минусов, но, пожалуйста, будьте добры пояснить - в чем именно заключается низкий технический уровень материала. Заранее, спасибо.

Tags:
Hubs:
Total votes 21: ↑7 and ↓14-7
Comments92

Articles