Перейти к основному содержимому
Блокчейн21ГЛАВА 21Пусто: null, undefined и строгие проверкиundefinednullвозникло самоположилизначения нет

Пусто: null, undefined и строгие проверки

Прочитано0%
Квизы0/5
Тренажёрыне пройден0/1
Уровень 1 · Новичок

Зачем это нужно​

Пятнадцать глав подряд язык гарантировал: значение есть всегда. Несуществующий ключ словаря отдавал нули, нулевой адрес, структуру из нулей — поэтому в контракте и заводили отдельное поле-флаг «а записан ли пользователь вообще».

Здесь всё иначе. Значения может не быть, и это отдельное состояние — причём в двух разных обличьях. А самая частая ошибка выполнения в этом языке звучит так: Cannot read properties of undefined. Эта глава — про то, откуда она берётся и как сделать так, чтобы её ловил компилятор, а не пользователь.

Две пустоты: одна возникает сама, другую кладут руками​

undefined — «сюда не клали». Никто его не пишет; оно возникает само в четырёх случаях:

  • переменная объявлена без значения;
  • у объекта нет такого поля;
  • функция ничего не вернула;
  • элемента массива не существует.

null — «положили пусто». Само не появляется никогда: его пишет человек или присылает внешняя система, и означает оно осознанное «здесь ничего».

ДВЕ ПУСТОТЫ — И ВОСЕМЬ ЗНАЧЕНИЙ, КОТОРЫЕ ТОЛЬКО ПРИКИДЫВАЮТСЯundefined — «сюда не клали»· переменная объявлена без значения· у объекта нет такого поля· функция ничего не вернула· элемента массива не существуетnull — «положили пусто»· пишет человек или присылает сервис· само не появляется никогдаtypeof null === "object"ошибка языка, которой тридцать летв условии «ложными» оказываются восемь: false, 0, −0, 0n, '', null, undefined, NaNпоэтому проверка «если значение есть» отвергает настоящий нулевой баланс и пустую строку
Одна пустота возникает сама, другую кладут руками; и восемь значений только прикидываются пустыми
Интересный факт

Проверка типа у null отвечает "object" — это ошибка языка, которой тридцать лет, и чинить её не станут: слишком много кода на неё опирается. Поэтому null проверяют строгим сравнением, а не проверкой типа.

Условие принимает что угодно — и это ловушка​

В Solidity if принимал только логическое значение. Здесь в условие пускают любое, и «ложными» оказываются восемь значений: false, ноль, минус ноль, ноль в больших числах, пустая строка, null, undefined и «не число».

Значит, проверка «если значение есть» отвергает настоящий нулевой баланс и пустую строку. Для протокола, где ноль — законная сумма, это прямая ошибка в логике.

Первая модель прогоняет четырнадцать значений через четыре проверки:

значение typeof if(x) x ?? def x || def x?.length
-----------------------------------------------------------
0 number нет 0 'def' undefined
-0 number нет -0 'def' undefined
0n bigint нет 0n 'def' undefined
'' string нет '' 'def' 0
'0' string да '0' '0' 1
' ' string да ' ' ' ' 1
NaN number нет NaN 'def' undefined
false boolean нет false 'def' undefined
true boolean да true true undefined
null object нет 'def' 'def' undefined
undefined undefined нет 'def' 'def' undefined
[] object да [] [] 0
{} object да {} {} undefined
42 number да 42 42 undefined

|| и ?? разошлись на 6 значениях: 0, -0, 0n, '', NaN, false
typeof null === "object": true
null == undefined: true, null === undefined: false
ГДЕ ?? И || ДАЮТ РАЗНОЕx ?? «по умолч.»x || «по умолч.»00«по умолч.»разошлись''''«по умолч.»разошлисьfalsefalse«по умолч.»разошлисьnull«по умолч.»«по умолч.»совпали424242совпалина четырнадцати значениях они разошлись на шести: 0, −0, 0n, пустая строка, NaN и false?? подставляет запасное только вместо настоящей пустоты — для сумм и ставок нужен именно он
Расхождение на шести значениях из четырнадцати: для сумм и ставок нужен именно ??

Три строки вывода стоят отдельного внимания.

Пустой массив и пустой объект — истинны. Проверка «если массив есть» не отвечает на вопрос «есть ли в нём что-нибудь». Для этого смотрят длину.

Строка «0» истинна, а число 0 — нет. Отсюда классическая ошибка при разборе данных, пришедших текстом.

|| и ?? расходятся на шести значениях. Первый подставляет запасное значение при любом «ложном», второй — только при настоящей пустоте. Для суммы, ставки, процента нужен именно второй: иначе законный ноль молча превратится в значение по умолчанию.

Отвечено вопросов: 0 из 2
В коде написано `const fee = input || 100`. Пользователь передал 0. Что окажется в fee?
Чем undefined отличается от null?
Отвечено верно: 0 из 2

Строгий режим: один флаг меняет язык​

Без строгой проверки пустоты типы «строка» и «строка или пусто» неотличимы: компилятор считает пустоту допустимым значением любого типа и молчит. С флагом — пустота становится частью типа, и обращаться к ней без проверки нельзя.

Вторая модель компилирует один и тот же файл дважды:

── без строгой проверки: компилятор промолчал, программа запущена ──
упало: TypeError: Cannot read properties of undefined (reading 'height')
упало: TypeError: Cannot read properties of undefined (reading 'generator')
упало: TypeError: Cannot read properties of undefined (reading 'length')
упало: TypeError: Cannot read properties of undefined (reading 'height')
blocks[10] сам по себе: undefined
blocks.length = 2

── с --strict: до запуска дело не дошло ──
model2.ts(20,37): error TS18048: 'found' is possibly 'undefined'.
model2.ts(27,29): error TS2532: Object is possibly 'undefined'.
model2.ts(35,42): error TS18048: 'g' is possibly 'undefined'.

Четыре падения превратились в три сообщения компилятора — до запуска. Это и есть ответ на вопрос «зачем вообще нужен TypeScript, если Node прекрасно запускает .js».

Сужение типа: как убедить компилятора​

Компилятор читает условия и после них сам убирает пустоту из типа — это называется сужением.

const found = findBlock(height); // тип: Block | undefined

if (found === undefined) return "не найден";
return found.generator; // здесь тип уже Block
Нажми на часть выражения

Работают четыре формы: сравнение с пустотой, проверка типа, проверка наличия поля и проверка «это массив». Не работает одна — та самая проверка «если значение есть»: она отсекает заодно и ноль с пустой строкой.

Обращение, которое не падает​

Знак ?. возвращает пустоту вместо падения, если слева пусто:

const gen = block?.generator; // undefined, если block пуст
const first = list?.[0]; // то же для элемента
const res = handler?.(); // и для вызова

Вся цепочка обрывается сразу: если пусто в начале, остальное не вычисляется вовсе.

Это удобно для чтения и опасно для записи: ?. превращает падение в тихий undefined, который поедет дальше по программе и всплывёт в другом месте. Правило: ?. — там, где пустота ожидаема и обработана; проверка — там, где пустота означает ошибку.

Массив не защищён даже в строгом режиме​

В Solidity выход за границу массива давал панику и откат всей транзакции. Здесь — тихую пустоту:

blocks[10] сам по себе: undefined
blocks.length = 2

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

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

Как читать самую частую ошибку​

TypeError: Cannot read properties of undefined (reading 'height')

Читается по одному правилу: в скобках — имя того, что пытались прочитать, а undefined — это то, что стояло слева от точки. То есть виновата не height, а выражение перед ней.

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

Цена восклицательного знака​

Знак ! после выражения означает «я знаю, что здесь не пусто» — и убирает пустоту из типа без всякой проверки. Третья модель ставит рядом три способа:

(tsc --strict: ошибок нет)

--- пользователь существует ---
withCheck ('lender1') -> lender1 / 0xA1 / 1000
withOptional('lender1') -> 0xA1
withBang ('lender1') -> lender1 / 0xA1 / 1000

--- пользователя нет ---
withCheck ('ghost') -> нет такого пользователя
withOptional('ghost') -> адрес неизвестен
withBang ('ghost') -> УПАЛО: TypeError: Cannot read properties of undefined (reading 'login')

--- что осталось от `!` в рантайме ---
текст функции withBang: const u = findUser(login) ;
ТРИ СПОСОБА ОБРАТИТЬСЯ К ТОМУ, ЧЕГО МОЖЕТ НЕ БЫТЬпользователь естьпользователя нетс проверкой ifвернул данныевернул «нет такого»через ?.вернул адресвернул «неизвестен»через ! — «я знаю, что не пусто»вернул данныеУПАЛОвесь файл прошёл строгую проверку без единой ошибки: про восклицательный знак компилятор молчитв готовом коде от него не остаётся ничего — это обещание компилятору, а не проверка
Весь файл прошёл строгую проверку без единой ошибки — и один из трёх вариантов упал

Последняя строка объясняет всё: в готовом коде от восклицательного знака не остаётся ничего. Это обещание компилятору, а не проверка. Компилятор поверил на слово и замолчал — а пользователь получил падение.

Правило: ! уместен там, где невозможность пустоты видна из соседних строк и не зависит от внешних данных. Во всех остальных случаях — проверка.

Отвечено вопросов: 0 из 2
Файл со знаком ! прошёл строгую проверку типов. Что это доказывает?
Как читать «Cannot read properties of undefined (reading 'height')»?
Отвечено верно: 0 из 2

Две пустоты по-разному уезжают наружу​

При превращении объекта в текст для передачи разница перестаёт быть теоретической: поле с undefined исчезает целиком, а поле с null остаётся со значением «пусто».

Отсюда практическое следствие для скриптов, которые разговаривают с узлом: если сервис ждёт поле и получает объект без него — это не то же самое, что получить поле со значением «пусто». Первое обычно означает «параметр не передан», второе — «передан и он пустой».

Типичные ошибки​

|| значение_по_умолчанию для суммы. Законный ноль превращается в значение по умолчанию.

Проверка «если значение есть» вместо сравнения с пустотой. Отсекает ноль и пустую строку заодно.

Проверка типа для null. Отвечает "object" — сравнивать нужно строго.

Обращение по индексу без проверки. Даже в строгом режиме компилятор считает элемент существующим.

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

! вместо проверки. Компилятор молчит, программа падает.

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

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

Пустота, приехавшая из ответа узла. Поля, которого нет в ответе, не существует — и типизировать ответ как «точно есть» значит обмануть себя.

Слова главы​

  • undefined — «не клали»; возникает само в четырёх случаях.
  • null — «положили пусто»; пишется руками.
  • «Ложные» значения — восемь: false, 0, -0, 0n, пустая строка, null, undefined, «не число».
  • Строгая проверка пустоты — флаг компилятора, делающий пустоту частью типа.
  • Сужение типа — компилятор убирает пустоту после проверки.
  • ?. — обращение, возвращающее пустоту вместо падения.
  • ?? — запасное значение только вместо настоящей пустоты.
  • ? в типе — необязательное поле или параметр.
  • ! — «я знаю, что не пусто»; в готовом коде исчезает.

Тренажёр: набрать вслепую​

Цель: скорость ≥ 100 зн/мин, точность ≥ 88%

if (found === undefined) return "блок не найден";

На чемпионате​

Модуль А. Дашборд из задания показывает больше десятка метрик: общий объём средств, статистика ликвидаций, текущее отношение долга к залогу каждой позиции, накопленные награды, цена доли. Каждая метрика — величина, которой может не быть: позиций ноль, ликвидаций не было, доход не начислялся.

Практические выводы:

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

Модуль В. Хотя аудит идёт по контракту, привычка думать о пустоте пригодится и там: в Solidity её роль играют нули, и отличить «не записано» от «записано нулём» можно только своим полем-флагом — ровно та же задача, другими средствами.

Отвечено вопросов: 0 из 2
На дашборде метрика «ликвидаций не было». Что показать?
Ответ узла разбирают и берут поле, которого в нём может не быть. Как типизировать?
Отвечено верно: 0 из 2

Что дальше​

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

Отвечено вопросов: 0 из 1
Обращение к элементу за границей массива в строгом режиме. Что скажет компилятор?
Отвечено верно: 0 из 1

Углубиться​

  • Почему проверку типа у null не чинят — и что предлагали взамен.
  • Строгая проверка индексов — отдельная настройка: что она даёт и почему её не включили в стандартный набор.
  • Утверждения типа — другие способы сказать компилятору «я знаю лучше» и чем они опасны.
  • Функции-предикаты — как написать свою проверку, после которой компилятор сузит тип.
  • Пустота в чужих данных — приёмы разбора ответа, о которых пойдёт речь в главе про свои типы.

Сводная проверка по всей главе: 8 вопросов, 5:00 на всё, без подсказок по ходу. Вопросы показываются по одному, ответ изменить нельзя. Если время выйдет — экзамен завершится с тем, что успел ответить. Пересдавать можно сколько угодно раз.

Куда дальше: проверенные ресурсы​

Видео по теме

Видеоnull и undefined / Как проверить значение на null (для новичков)Игорь Антонов — про JavaScript и разработку
ВидеоОпциональная цепочка в JavaScript / Optional ChainingИгорь Антонов — про JavaScript и разработку
ВидеоОпциональная цепочка в JavaScriptOpen JS
ВидеоTypeScript 5.6 - Nullish and Truthy ChecksPurpleSchool | Anton Larichev
ВидеоAre You Using Null And Undefined Wrong?Web Dev Simplified

Комментарии

Обсуждение живёт в GitHub Discussions и грузится с чужого домена — поэтому только по нажатию.