undefined является специальным значением. Согласно спецификации ECMAScript, undefined в JavaScript можно получить при доступе к неинициализированным переменным, несуществующим свойствам объекта, несуществующим элементам массива, etc. Пример:
let number;
number; // => undefined
let movie = { name: 'Interstellar' };
movie.year; // => undefined
let movies = ['Interstellar', 'Alexander'];
movies[3]; // => undefined
Как видим, undefined выводится при попытке доступа к:
неинициализированной переменной number;
несуществующему свойству объекта movie.year;
несуществующему элементу массива movies[3].
Оператор typeof возвращает строку undefined для неопределённого значения:
typeof undefined === 'undefined'; // => true
Оператор typeof отлично подходит для проверки значения undefined в JavaScript:
let nothing;
typeof nothing === 'undefined'; // => true
Как избежать undefined в JavaScript
Неинициализированная переменная
Объявленная переменная, которая еще не имеет значения (не инициализирована), по умолчанию undefined. Пример:
let myVariable;
myVariable; // => undefined
Переменная myVariable уже объявлена, но ещё не имеет присвоенного значения. Попытка обратиться к ней закончится выводом undefined. Чтобы это исправить, достаточно присвоить переменной значение. Чем меньше переменная существует в неинициализированном состоянии, тем лучше.
Ниже приведены способы решения проблемы.
1. const и let вместо var
Объявленные таким образом объекты и переменные находятся в области видимости, ограниченной текущим блоком кода, и находятся во временной мёртвой зоне до момента присвоения им значения.
При использовании неизменяемых данных (констант) рекомендуется инициализировать их как const:
const myVariable = 'initial'
Константа не подвергается неинициализированному состоянию, и получить значение undefined в этом случае невозможно.
Если вам нужно менять значение переменной, то обозначьте её как let и также присваивайте ей начальное значение:
let index = 0
Проблема var заключается в поднятии переменных: где бы ни находилось объявление, это равнозначно тому, что переменную объявили в начале кода.
function bigFunction() {
// код...
myVariable; // => undefined
// код...
var myVariable = 'Initial value';
// код...
myVariable; // => 'Initial value'
}
bigFunction();
В этом случае переменная myVariable содержит undefined до получения значения:
myVariable = 'Initial value'
Если же переменную объявить как let, она останется недоступной до момента присвоения ей значения. Таким образом, использование const или let снизит риск получения значения undefined в JavaScript.
2. Усиление связности
Связность характеризует степень взаимосвязи элементов модуля (пространства имён, класса, метода, блока кода). Сильная связность предпочтительнее, поскольку предполагает, что элементы модуля должны фокусироваться исключительно на одной задаче. Это поможет модулю быть:
сфокусированным и понятным;
легко поддерживаемым и поддающимся рефакторингу;
многоразовым;
простым для тестирования.
undefined в JavaScript и связность
Блок кода сам по себе может считаться небольшим модулем. Чтобы извлечь выгоду из преимуществ сильной связности, нужно держать переменные как можно ближе к блоку кода, который их использует.
Вот классический пример того, как не надо делать:
function someFunc(array) {
var index, item, length = array.length;
// некоторый код...
// некоторый код...
for (index = 0; index < length; index++) {
item = array[index];
// некоторый код...
}
return 'some result';
}
index, item и length объявляются в начале функции, но используются они лишь ближе к концу. Всё время между объявлением переменной в начале и до использования её в цикле index и item не инициализируются и выводят undefined. Разумнее переместить переменные ближе к месту их применения:
function someFunc(array) {
// некоторый код...
// некоторый код...
const length = array.length;
for (let index = 0; index < length; index++) {
const item = array[index];
// некоторый код
}
return 'some result';
}
Доступ к несуществующему свойству
Попытка получить доступ к несуществующему свойству объекта JavaScript заканчивается undefined. Пример:
let favoriteMovie = {
title: 'Blade Runner'
};
favoriteMovie.actors; // => undefined
favoriteMovie — объект с одним значением title. Доступ к несуществующему свойству actors приведёт к выводу undefined.
Сам по себе доступ к не вызовет ошибку, а вот при попытке получить значение из несуществующего свойства выведется ошибка:
TypeError: Cannot read property
Проблема в особенностях JavaScript: свойство может быть установлено или отсутствовать. Хорошее решение — установка правил, которые обязывают задать свойствам значения.
Но не всегда возможно контролировать объекты, с которыми приходится работать. Такие объекты могут иметь разный набор свойств в различных сценариях, и каждый из них нужно обрабатывать вручную.
Реализуем функцию append(array, toAppend), которая добавляет в начале и/или в конце массива новые элементы:
function append(array, toAppend) {
const arrayCopy = array.slice();
if (toAppend.first) {
arrayCopy.unshift(toAppend.first);
}
if (toAppend.last) {
arrayCopy.push(toAppend.last);
}
return arrayCopy;
}
append([2, 3, 4], { first: 1, last: 5 }); // => [1, 2, 3, 4, 5]
append(['Hello'], { last: 'World' }); // => ['Hello', 'World']
append([8, 16], { first: 4 }); // => [4, 8, 16]
Поскольку объект toAppend может удалять первые или последние свойства, необходимо проверить их существование с помощью условий if(toAppend.first){} и if(toAppend.last){}.
Вот только undefined, как false, null, 0, NaN и ' ', является ложными значением, а в текущей реализации функция append() не позволяет вставлять ложные элементы:
append([10], { first: 0, last: false }); // => [10]
0 и false — ложные значения, потому что if (toAppend.first){} и if (toAppend.last){} фактически сравниваются с ложными значениями, и эти элементы не вставляются в массив. Функция возвращает исходный массив [10] без изменений.
1. Наличие свойства
К счастью, JavaScript предлагает множество способов определить, имеет ли объект определённое свойство:
obj.prop !== undefined в JavaScript позволяет проверить undefined, сравнив с ним объект;
typeof obj.prop !== 'undefined' проверяет тип значения свойства;
obj.hasOwnProperty('prop') проверяет объект на наличие собственного свойства;
'prop' in obj проверяет объект на наличие собственного или унаследованного свойства.
Рекомендацией в этом случае будет использование оператора in , чтобы проверить, имеет ли объект определённое свойство, не обращаясь к фактическому значению этого свойства:
function append(array, toAppend) {
const arrayCopy = array.slice();
if ('first' in toAppend) {
arrayCopy.unshift(toAppend.first);
}
if ('last' in toAppend) {
arrayCopy.push(toAppend.last);
}
return arrayCopy;
}
append([2, 3, 4], { first: 1, last: 5 }); // => [1, 2, 3, 4, 5]
append([10], { first: 0, last: false }); // => [0, 10, false]
'first' in toAppend ( как и 'last' in toAppend) выводит true, независимо от существующего свойства. В других случаях выводится — false.
Использование оператора in устраняет проблему со вставкой ложных элементов 0 и false. Теперь добавление элементов в начале и в конце массива [10] приводит к ожидаемому результату: [0, 10, false].
2. Деструктуризация доступа к свойствам объекта
Деструктуризация объекта позволяет устанавливать значение по умолчанию, если свойство не существует: удобно для исключения прямого контакта с undefined:
const object = { };
const { prop = 'default' } = object;
prop; // => 'default'
Применяя преимущества деструктуризации объекта, реализуем quote():
function quote(str, config) {
const { char = '"', skipIfQuoted = true } = config;
const length = str.length;
if (skipIfQuoted
&& str[0] === char
&& str[length - 1] === char) {
return str;
}
return char + str + char;
}
quote('Hello World', { char: '*' }); // => '*Hello World*'
quote('"Welcome"', { skipIfQuoted: true }); // => '"Welcome"'
const { char = '"', skipIfQuoted = true } = config в одной строкe извлекает свойства char и skipIfQuoted из объекта config.
Если некоторые свойства недоступны в объекте config, деструктуризация задаёт значения по умолчанию: '"' для char и false для skipIfQuoted. К счастью, функцию можно улучшить.
Переместим деструктуризацию в раздел параметров и установим значение по умолчанию (пустой объект { }) для параметра config, чтобы пропустить второй аргумент, когда будет достаточно значений по умолчанию:
function quote(str, { char = '"', skipIfQuoted = true } = {}) {
const length = str.length;
if (skipIfQuoted
&& str[0] === char
&& str[length - 1] === char) {
return str;
}
return char + str + char;
}
quote('Hello World', { char: '*' }); // => '*Hello World*'
quote('Sunny day'); // => '"Sunny day"'
Деструктурирующее присваивание гарантирует, что используется пустой объект, если второй аргумент не указан вообще. В результате вы избегаете возникновения значения undefined в JavaScript.
3. Свойство по умолчанию
Есть простой способ установить значения по умолчанию для свойств объекта, и имя ему Spread syntax:
const unsafeOptions = {
fontSize: 18
};
const defaults = {
fontSize: 16,
color: 'black'
};
const options = {
...defaults,
...unsafeOptions
};
options.fontSize; // => 18
options.color; // => 'black'
Инициализатор объекта распространяет свойства из исходных объектов defaults и unsafeOptions. Важен порядок, в котором указаны исходные объекты: свойства более позднего исходного объекта перезаписывают более ранние. Независимо от ситуации, объект всегда содержит полный набор свойств, и появление undefined невозможно.
Параметры функции
Функция, имеющая определённые параметры, должна вызываться с одинаковым количеством аргументов. В таком случае параметры получают ожидаемые значения:
function multiply(a, b) {
a; // => 5
b; // => 3
return a * b;
}
multiply(5, 3); // => 15
При вызове функции multiply(5, 3) параметры a и b получают соответствующие значения 5 и 3. Умножение рассчитывается как ожидаемое: 5 * 3 = 15.
Но что происходит, когда пропускается аргумент при вызове? Параметр внутри функции получает значение undefined. Как этого избежать?
Лучшим подходом является использование параметров по умолчанию из ES2015:
function multiply(a, b = 2) {
a; // => 5
b; // => 2
return a * b;
}
multiply(5); // => 10
multiply(5, undefined); // => 10
Значение b = 2 в сигнатуре функции гарантирует, что если b получит значение undefined, то по умолчанию параметр изменится на 2.
Возвращаемое значение функции
В JavaScript функция, которая не имеет оператора return, возвращает значение undefined:
function square(x) {
const res = x * x;
}
square(2); // => undefined
То же происходит, если return присутствует, но без какого-либо выражения рядом:
function square(x) {
const res = x * x;
return;
}
square(2); // => undefined
Указывая значение для return, можно получить желаемый результат:
function square(x) {
const res = x * x;
return res;
}
square(2); // => 4
Теперь вызов функции выведет нужное значение.
Оператор void
Оператор void выполняет выражение и возвращает undefined вне зависимости от результата:
void 1; // => undefined
void (false); // => undefined
void {name: 'John Smith'}; // => undefined
void Math.min(1, 3); // => undefined
Одним из вариантов использования оператора void является переопределение результата выполнения выражения и возврат undefined в случае возникновения неожиданных результатов выполнения функции.
Значение undefined в массивах
Вы получаете undefined при попытке доступа к элементу массива с индексом вне пределов массива.
const colors = ['blue', 'white', 'red'];
colors[5]; // => undefined
colors[-1]; // => undefined
Массив colors имеет 3 элемента, поэтому корректные индексы равны 0, 1 и 2. Поскольку в индексах массива 5 и -1 нет элементов, значения colors[5] и colors[-1] получают значение undefined.
В JavaScript вы можете столкнуться с так называемыми разрежёнными массивами. Эти массивы имеют пробелы, то есть на некоторых индексах не определены никакие элементы. Когда делаем попытку получить доступ к пустому значению в разрежённом массиве, на выходе получаем undefined:
const sparse1 = new Array(3);
sparse1; // => [
sparse1[0]; // => undefined
sparse1[1]; // => undefined
const sparse2 = ['white', ,'blue']
sparse2; // => ['white',
sparse2[1]; // => undefined
sparse1 создается путем вызова конструктора Array с числовым первым аргументом. Он имеет 3 пустых элемента. sparse2 создается с литералом массива, второй элемент которого отсутствует. В любом из этих массивов доступ к пустому элементу оценивается как undefined.
Отличие null и undefined в JavaScript
Основное отличие в том, что undefined представляет значение переменной, которая ещё не была инициализирована, а null — намеренное отсутствие объекта.
Допустим, переменная number определена, но ей не назначено начальное значение:
let number;
number; // => undefined
То же самое произойдёт при попытке доступа к несуществующему свойству объекта:
const obj = { firstName: 'Dmitri' };
obj.lastName; // => undefined
Или переменная должна ожидать возвращение объекта функции, но по какой-то причине создание объекта невозможно. В этом случае null является значимым индикатором недостающего объекта. Например, clone() — это функция, которая клонирует простой объект JavaScript. Ожидается, что функция вернёт объект:
function clone(obj) {
if (typeof obj === 'object' && obj !== null) {
return Object.assign({}, obj);
}
return null;
}
clone({name: 'John'}); // => {name: 'John'}
clone(15); // => null
clone(null); // => null
Но clone() может быть вызван с пустым аргументом: 15 или null. В этом случае функция не может создать клон, поэтому возвращает null — индикатор отсутствующего объекта.
В JavaScript существуют проверки на null и undefined. Оператор typeof демонстрирует различие между двумя значениями:
typeof undefined; // => 'undefined'
typeof null; // => 'object'
Строгий оператор равенства === также отличает undefined от null:
let nothing = undefined;
let missingObject = null;
nothing === missingObject; // => false
Вам также может быть интересна наша статья про обработку ошибок в JavaScript.
Заключение
Стратегия борьбы с undefined в JavaScript:
Уменьшить использование неинициализированных переменных.
Сделать жизненный цикл переменных коротким и близким к источнику их использования.
Назначить начальное значение переменным.
Использовать const или let.
Проверять наличие свойств или заполнить небезопасные объекты по умолчанию.
Использовать значения по умолчанию для некритичных параметров функции.
Избегать использования разрежённых массивов.