Это перевод статьи Притти Кассириди »Let’s Learn JavaScript Closures».

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

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

Разработчикам понимание принципов работы тех или иных особенностей языка, даст больше осознанности в их применении, поэтому этот пост посвящён внутреннему устройству замыканий: как они работают и почему.

Надеюсь, после этой статьи вы будете лучше подготовлены к использованию замыканий в своей повседневной разработке. Давайте начнём.

Что такое замыкания?

Замыкания являются мощным инструментом в JavaScript и других языках программирования. Вот определение с MDN:

Замыкания — это функции, ссылающиеся на независимые (свободные) переменные. Другими словами, функция, определённая в замыкании, «запоминает» окружение, в котором она была создана.

Заметка: свободные переменные — это переменные, которые не объявлены локально и не передаются в качестве параметра.

Давайте посмотрим на несколько примеров:

Пример 1

function numberGenerator() {
  // Локальная «свободная» переменная, которая доступна только в замыкании
  var num = 1;
  function checkNumber() {
    console.log(num);
  }
  num++;
  return checkNumber;
}

var number = numberGenerator();
number(); // 2

В примере функция numberGenerator создаёт локальную «свободную» переменную num (число) и checkNumber (функция, которая выводит число в консоль). Функция checkNumber не содержит собственной локальной переменной, но благодаря замыканию она имеет доступ к переменным внутри внешней функции, numberGenerator. Поэтому объявленная в numberGenerator переменная num будет успешно выведена в консоль, даже после того, как numberGenerator вернёт результат выполнения.

Пример 2

В этом примере видно, что замыкания содержат в себе все локальные переменные, которые были объявлены внутри внешней замкнутой функции — enclosing function.

function sayHello() {
  var say = function() { console.log(hello); }
  // Локальная переменная, которая доступна только в замыкании
  var hello = 'Hello, world!';
  return say;
}
var sayHelloClosure = sayHello();
sayHelloClosure(); // ‘Hello, world!’

Обратите внимание, как переменная hello определяется после анонимной функции, но эта функция всё равно может получить доступ к этой переменной hello. Это происходит из-за того, что переменная hello во время создания уже была определена в области видимости (scope), тем самым сделав её доступной на тот момент, когда анонимная функция будет выполнена. Не беспокойтесь, позже я объясню, что такое «область видимости». А пока просто смиритесь с этим.

Понимаем высокий уровень

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

Чтобы понять, как это работает, давайте рассмотрим несколько связанных между собой идей. Мы зайдём издалека и постепенно вернёмся к замыканиям. Начнём наш путь с общего контекста, в котором выполняется функция, и известного как контекст выполнения — execution context.

Контекст выполнения

Контекст выполнения — это абстрактное понятие, которое используется в спецификации ECMAScript для оценки времени выполнения кода. Это может быть глобальный контекст — global context, в котором ваш код выполнится первым, или когда поток выполнения переходит в тело функции.

В любой момент времени выполняется только один контекст функции (тело функции). Вот почему JavaScript является однопотоковым, так как единовременно может выполняться только одна команда. Обычно браузеры поддерживают этот контекст с помощью стека — stack. Стек — структура данных, выполняемая в обратном порядке: LIFO — «последним пришёл — первым вышел». Последнее, что вы добавили в стек, будет удалено первым из него. Это происходит из-за того, что мы можем только добавить или удалить элементы из верхушки стека. Текущий или «выполняющийся» контекст исполнения — всегда верхний элемент стека. Он выскакивает из стека, когда код в текущем контексте полностью разобран, позволяя следующему верхнему элементу стека взять на себя контекст выполнения.

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

Для наглядности запустите в консоли код, который вы видите ниже:

var x = 10;
function foo(a) {
  var b = 20;

  function bar(c) {
    var d = 30;
    return boop(x + a + b + c + d);
  }

  function boop(e) {
    return e * -1;
  }

  return bar;
}

var moar = foo(5); // Замыкание
/*
  Функция ниже выполняет функцию bar, которая была возвращена в тот момент,
  когда мы выполнили функцию foo в строке выше. Функция bar
  вызывает boop, в этот момент bar получает приостановку и boop перемещается
  на вершину стека вызовов(call stack) (см. скриншот ниже)
*/
moar(15);

Затем, когда boop возвратится, он удалится из стека, и bar продолжит работу:

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

  • Оценка состояния кода — любое состояние необходимо выполнить, приостановить и возобновить определение кода, связанного с этим контекстом выполнения.
  • Функция — объект функции, который оценивает контекст выполнения или null, если контекст был определён как script или модуль.
  • Область — набор внутренних объектов, глобальное окружение ECMAScript, весь код ECMAScript, который находится в пределах этого глобального окружения и другие связанные с ним состояния и ресурсы.
  • Лексическое окружение — используется для разрешения ссылок идентификатора кода в этом контексте исполнения.
  • Переменное окружение — лексическое окружение, чья запись окружения — EnvironmentRecord имеет связи, созданные заявленными переменными — VariableStatements в этом контексте выполнения.

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

Заметка: c технической точки зрения, окружение переменных и лексическая область видимости используются для реализации замыканий. Но для простоты мы заменим его на «окружение». Для детального объяснения разницы между лексическим и переменным окружением читайте статью Акселя Раушмайера.

Лексическая область видимости

Дадим определение: лексическое окружение — специфичный тип, используемый для связи идентификаторов с определёнными переменными и функциями на основе лексической структуры вложенности кода ECMAScript. Лексическое окружение состоит из записи окружения и, возможно, нулевой ссылки на внешнее лексическое окружение. Обычно лексическое окружение связано с определённой синтаксической структурой, например: FunctionDeclaration — объявление функции, BlockStatement — оператор блока, Catch clause — условный оператор, TryStatement — перехват ошибок и новым лексическим окружением, которое создавалось каждый раз при разборе кода. — ECMAScript-262/6.0

Давайте разберём это.

  • **«используемый для связи идентификаторов»: целью лексического окружения является управление данными, то есть идентификаторами в коде. Говоря иначе, это придаёт им смысл. Например, у нас есть такая строка в консоли: console.log (x / 10), x** здесь бессмысленная переменная или идентификатор без чего-либо, что придавало бы ей смысл. Лексическое окружение обеспечивает смысл или «ассоциацию» через запись окружения. Смотрите ниже.
  • Лексическое окружение состоит из записи окружения: запись окружения — причудливый способ сказать, что она хранит записи всех идентификаторов и связей, которые существуют в лексической области видимости. Каждая лексическая область видимости имеет собственную запись окружения.
  • Лексическая структура вложенности: самый интересный момент, который говорит, что внутреннее окружение ссылается на внешнее окружение, и это внешнее окружение может иметь собственное внешнее окружение. В результате окружение может быть внешним окружением для более чем одного внутреннего окружения. Глобальное окружение является единственным лексическим окружением, которое не имеет внешнего окружения. Это сложно описать словами, поэтому давайте использовать метафоры и представим лексическое окружение как слои лука: глобальная среда — внешний слой луковицы, где каждый последующий слой находится ниже.

Так выглядит окружение в псевдокоде:

LexicalEnvironment = {
  EnvironmentRecord: {
  // Здесь идёт привязка идентификатора
  },
  // Ссылка на внешнее окружение
  outer: < >
};
  • Новое лексическое окружение, которое создавалось каждый раз при разборе кода — каждый раз, когда вызываются внешние вложенные функции, создаётся новое лексическое окружение. Это важно, и мы вернёмся к этому моменту в конце. Примечание: функции — не единственный способ создать лексическое окружение. Другие типы содержат в себе оператор блока — block statement или условный оператор — catch clause__. Для простоты, я сосредоточусь на окружении созданной нами функции на протяжении всего поста.

Каждый контекст исполнения имеет собственное лексическое окружение. Это лексическое окружение содержит переменные и связанные с ними значения, а также имеет ссылку на него во внешнем окружении. Лексическое окружение может быть глобальным окружением, модульным окружением, которое содержит привязку для объявлений модуля на высшем уровне, или окружением функций, где окружение создаётся за счёт вызова функции.

Цепочки областей видимости

Исходя из приведённого выше определения, мы знаем, что окружение имеет доступ к окружению своего родителя, его родительское окружение имеет доступ к своему родительскому окружению и так далее. Этот набор идентификаторов, к которому каждое окружение имеет доступ, называется область видимости — scope. Мы можем вложить их в иерархические цепочки окружения, известные как цепочки областей видимости.

Давайте рассмотрим эту структуру вложенности:

var x = 10;

function foo() {
  var y = 20; // свободная переменная
  function bar() {
    var z = 15; // свободная переменная
    return x + y + z;
  }
  return bar;
}

Как вы можете видеть, bar вложен в foo. Чтобы всё это представить посмотрите на диаграмму ниже:

Мы вернёмся позже к этому примеру.

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

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

Идём в обход: динамическая область видимости против статической области видимости

У динамических языков программирования существует стековая архитектура — stack-based implementations, локальные переменные и функции хранятся в стеке. Поэтому, во время выполнения стека, программа определяет какую переменную вы имеете в виду. С другой стороны, статическая область видимости — это когда переменные ссылаются на контекст и фиксируются на момент создания. Другими словами, структура исходного кода программы определяет к каким переменным вы обращаетесь.

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

Пример 1

var x = 10;

function foo() {
  var y = x + 5;
  return y;
}

function bar() {
  var x = 2;
  return foo();
}

function main() {
  foo(); // Статическая область: 15; Динамическая область: 15
  bar(); // Статическая область: 15; Динамическая область: 7
  return 0;
}

Как мы видим из примера выше, статическая и динамическая область видимости возвращают разные значения при вызове функции bar.

В статической области видимости возврат значения bar зависит от значения x. Это происходит из-за того, что статическая и лексическая структура исходного кода приводит x и к 10, и к 15.

Динамическая область видимости даёт нам стек определённых переменных, которые отслеживаются во время выполнения. Поэтому x, которую мы используем, зависит от того, что находится в её области видимости и как она была динамично определена во время выполнения. Выполнение функции bar выталкивает x = 2 на верхушку стека, заставляя foo вернуть 7.

Пример 2

var myVar = 100;

function foo() {
  console.log(myVar);
}

foo(); // Статическая область: 100; Динамическая область: 100

(function () {
  var myVar = 50;
  foo(); // Статическая область: 100; Динамическая область: 50
})();

// Функция высшего порядка
(function (arg) {
  var myVar = 1500;
  arg();  // Статическая область: 100; Динамическая область: 1500
})(foo);

Аналогично и в динамической области видимости. Переменная myVar решает, какое использовать значение myVar в зависимости от того, где была вызвана функция. Статическая область видимости приводит myVar к переменной, которая была сохранена в рамках двух немедленно вызываемых функций при их создании.

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

Замыкания

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

Каждая функция имеет контекст выполнения, который состоит из окружения и передаёт смысл в переменные этой функции и ссылку на окружение своего родителя. Эта ссылка делает все переменные в родительской области доступными для всех внутренних функций, независимо от того, вызывается ли внутренняя функция (или функции) вне или внутри области видимости, в которой они были созданы.

Кажется, как будто функция «запоминает» это окружение, поскольку функция буквально имеет ссылку к области видимости и переменным, определённым в этой среде.

Возвратимся к примеру вложенной структуры:

var x = 10;

function foo() {
  var y = 20;
  function bar() {
    var z = 15;
    return x + y + z;
  }
  return bar;
}

var test = foo();

test(); // 45

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

GlobalEnvironment = {
  EnvironmentRecord: {
    // встроенные идентификаторы
    Array: '<func>',
    Object: '<func>',
    // и т.д.

    // пользовательские идентификаторы
    x: 10
  },
  outer: null
};

fooEnvironment = {
  EnvironmentRecord: {
    y: 20,
    bar: '<func>'
  }
  outer: GlobalEnvironment
};

barEnvironment = {
  EnvironmentRecord: {
    z: 15
  }
  outer: fooEnvironment
};

Когда мы вызываем функцию test, мы получаем 45, и она возвращает значение из вызова функции bar (потому что foo возвращает bar). bar имеет доступ к свободной переменной y даже после того, как функция foo вернётся, так как bar имеет ссылку на y через его внешнее окружение, которое является окружением foobar так же имеет доступ к глобальной переменной x потому, что у окружения foo есть доступ к глобальному окружению. Это называют «поиск цепочки области видимости».

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

Теперь, когда мы понимаем внутренности на абстрактном уровне, давайте рассмотрим ещё пару примеров:

Пример 1

Вот типичное заблуждение: в цикле for мы пробуем связать переменную счётчика с какой-либо функцией.

var result = [];

for (var i = 0; i < 5; i++) {
  result[i] = function () {
    console.log(i);
  };
}

result[0](); // 5, ожидалось 0
result[1](); // 5, ожидалось 1
result[2](); // 5, ожидалось 2
result[3](); // 5, ожидалось 3
result[4](); // 5, ожидалось 4

На основе всего что было ранее, мы можем с лёгкостью найти ошибку здесь. Абстрактно говоря, вот так выглядит окружение, во время выхода из цикла for:

environment: {
  EnvironmentRecord: {
    result: [...],
    i: 5
  },
  outer: null,
}

Было бы неверно предполагать, что область видимости отличается для всех пяти функций в результирующем массиве. Вместо того, что происходит по факту, окружение или контекст/область видимости является тем же самым для всех пяти функций в пределах результирующего массива. Поэтому каждый раз, когда переменная i увеличивается, обновляется область видимости, а она является общей для всех функций. Из-за этого любая из пяти функций, пытающихся получить доступ к i, возвращает 5, i равна 5, когда цикл завершается.

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

var result = [];

for (var i = 0; i < 5; i++) {
  result[i] = (function inner(x) {
    // дополнительный вызываемый контекст
    return function() {
      console.log(x);
    }
  })(i);
}

result[0](); // 0, ожидалось 0
result[1](); // 1, ожидалось 1
result[2](); // 2, ожидалось 2
result[3](); // 3, ожидалось 3
result[4](); // 4, ожидалось 4

Ура, мы исправили это.

Есть ещё одно решение, в котором мы используем let вместо varlet находится в операторе блока и поэтому новая привязка идентификатора замыкания создаётся для каждой итерации в цикле for.

var result = [];

for (let i = 0; i < 5; i++) {
  result[i] = function () {
    console.log(i);
  };
}

result[0](); // 0, ожидалось 0
result[1](); // 1, ожидалось 1
result[2](); // 2, ожидалось 2
result[3](); // 3, ожидалось 3
result[4](); // 4, ожидалось 4

Та-дам!

Пример 2

В этом примере мы покажем как каждый вызов в функции создаёт новое отдельное замыкание:

function iCantThinkOfAName(num, obj) {
  // Это массив переменных, вместе с 2 параметрами, передаваемых
  // "захваченными" в замкнутую функцию 'doSomething'
  var array = [1, 2, 3];
  function doSomething(i) {
    num += i;
    array.push(num);
    console.log('num: ' + num);
    console.log('array: ' + array);
    console.log('obj.value: ' + obj.value);
  }

  return doSomething;
}

var referenceObject = { value: 10 };
var foo = iCantThinkOfAName(2, referenceObject); // замыкание #1
var bar = iCantThinkOfAName(6, referenceObject); // замыкание #2

foo(2);
/*
  num: 4
  array: 1,2,3,4
  obj.value: 10
*/

bar(2);
/*
  num: 8
  array: 1,2,3,8
  obj.value: 10
*/

referenceObject.value++;

foo(4);
/*
  num: 8
  array: 1,2,3,4,8
  obj.value: 11
*/

bar(4);
/*
  num: 12
  array: 1,2,3,8,12
  obj.value: 11
*/

Мы видим что каждый вызов в функции iCantThinkOfAName создаёт новое замыкание, а именно foo и bar. Последующие вызовы каждой замкнутой функции обновляют замкнутые переменные в пределах самого замыкания, демонстрируя, что переменные в каждом замыкании используются функции iCantThinkOfAName’s doSomething после того, как вернулась iCantThinkOfAName.

Пример 3

function mysteriousCalculator(a, b) {
	var mysteriousVariable = 3;
	return {
		add: function() {
			var result = a + b + mysteriousVariable;
			return toFixedTwoPlaces(result);
		},

		subtract: function() {
			var result = a - b - mysteriousVariable;
			return toFixedTwoPlaces(result);
		}
	}
}

function toFixedTwoPlaces(value) {
	return value.toFixed(2);
}

var myCalculator = mysteriousCalculator(10.01, 2.01);
myCalculator.add() // 15.02
myCalculator.subtract() // 5.00

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

GlobalEnvironment = {
  EnvironmentRecord: {
    // встроенные идентификаторы
    Array: '<func>',
    Object: '<func>',
    // и т.д.

    // пользовательские идентификаторы
    mysteriousCalculator: '<func>',
    toFixedTwoPlaces: '<func>',
  },
  outer: null,
};

mysteriousCalculatorEnvironment = {
  EnvironmentRecord: {
    a: 10.01,
    b: 2.01,
    mysteriousVariable: 3,
  }
  outer: GlobalEnvironment,
};

addEnvironment = {
  EnvironmentRecord: {
    result: 15.02
  }
  outer: mysteriousCalculatorEnvironment,
};

subtractEnvironment = {
  EnvironmentRecord: {
    result: 5.00
  }
  outer: mysteriousCalculatorEnvironment,
};

Это происходит из-за того, что наши функции add и substract ссылаются на среду mysteriousCalculator, и они в состоянии использовать переменные этой среды для расчёта результата.

Пример 4

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

function secretPassword() {
  var password = 'xh38sk';
  return {
    guessPassword: function(guess) {
      if (guess === password) {
        return true;
      } else {
        return false;
      }
    }
  }
}

var passwordGame = secretPassword();
passwordGame.guessPassword('heyisthisit?'); // false
passwordGame.guessPassword('xh38sk'); // true

Это очень мощная техника — она даёт замыкающей функции guessPassword исключительный доступ к переменной password, делая невозможным доступ к password снаружи.

Tl; dr

  • Контекст выполнения — это абстрактный контекст, использовавшийся в спецификации ECMAScript для отслеживания времени выполнения кода. В любое время может быть только один контекст выполнения, который выполняет код.
  • Каждый контекст исполнения имеет лексическое окружение. Оно содержит связи идентификаторов, то есть переменные и их значения и имеет ссылку на него во внешнем окружении.
  • Набор идентификаторов, к которым у каждого окружения есть доступ, называют «область видимости». Мы можем вложить эти области в иерархическую цепь окружения, известной как «цепочки области видимости».
  • Каждая функция имеет контекст выполнения, который включает в себя лексическое окружение. Это придаёт смысл переменным в пределах этой функции и ссылку на родительское окружение. И это означает, что функции «запоминают» окружение или область видимости, так как они буквально ссылаются на это окружение. Это и есть замыкание.
  • Замыкания создаются каждый раз при вызове внешней функции. Другими словами, внутренняя функция не будет возвращена для замыкания, в котором была создана.
  • Область видимости замыканий в JavaScript лексическая, её смысл определяется статично в зависимости от нахождения в исходном коде.
  • Есть множество практических случаев использования замыканий. Один из важных случаев использования — это сохранение приватных ссылок к переменным во внешней среде.

Замыкающая ремарка

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

Дополнительная литература

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

Есть что-то ещё? Предлагайте.


«Доктайп» — журнал о фронтенде. Читайте, слушайте и учитесь с нами.

ТелеграмПодкастБесплатные учебники

Читать дальше

Ошибка JavaScript «Uncaught TypeError: Cannot read property of undefined». Что делать?

Описание проблемы
Эта ошибка возникает, когда вы пытаетесь получить доступ к свойству объекта, который в данный момент имеет значение undefined. Например:


let obj = undefined;
console.log(obj.property); // Uncaught TypeError: Cannot read property 'property' of undefined
  

Возможные причины

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

Шаги по исправлению

1. Проверка объекта перед доступом к свойствам

Убедитесь, что объект существует, прежде чем пытаться получить его свойства.

if (obj !== undefined && obj !== null) {
    console.log(obj.property);
}

// Или современный способ:
console.log(obj?.property); // Вернет undefined, если obj равен null или undefined
  

2. Инициализация объекта перед использованием

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

Неверный код:

let data;
console.log(data.user.name); // Ошибка
  

Исправление:


let data = { user: { name: "Иван" } };
console.log(data.user.name); // "Иван"
  

3. Проверка API или данных с сервера

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

fetch('https://api.example.com/data')
    .then(response => response.json())
    .then(data => {
        if (data?.user?.name) {
            console.log(data.user.name);
        } else {
            console.error('Данные пользователя отсутствуют');
        }
    });

4. Использование значений по умолчанию

Для предотвращения ошибок можно задавать значения по умолчанию.

let user = undefined;
let userName = user?.name || "Гость";
console.log(userName); // "Гость"

5. Отладка с помощью console.log

Проверяйте промежуточные значения переменных в консоли браузера.

console.log(obj); // Убедитесь, что объект определен 

Пример из жизни
Вы пишете код для отображения имени пользователя из объекта user:

let user = null; // Данные еще не загружены
console.log(user.name); // Uncaught TypeError: Cannot read property 'name' of null

Исправление:

let user = null;
console.log(user?.name || "Имя пользователя не найдено"); // "Имя пользователя не найдено"

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

JS
  • 15 ноября 2024

Как исправить ошибку JavaScript «Uncaught ReferenceError: variable is not defined»

Эта ошибка возникает, когда вы пытаетесь обратиться к переменной, которая не была объявлена в текущей области видимости. Например:

console.log(myVariable); // Uncaught ReferenceError: myVariable is not defined

Возможные причины

  • Вы пытаетесь обратиться к переменной до ее объявления.
  • Переменная объявлена внутри функции и недоступна за ее пределами.
  • Ошибка в написании имени переменной (опечатка).

Шаги по исправлению

1. Проверка объявления переменной

Убедитесь, что переменная объявлена перед ее использованием.

Неверный код:

console.log(myVariable); // Ошибка

Исправление:

let myVariable = 10;
console.log(myVariable); // 10

2. Правильное определение области видимости переменной

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

Неверный код:

function myFunction() {
    let localVariable = "Привет";
}
console.log(localVariable); // Ошибка

Исправление:

function myFunction() {
    let localVariable = "Привет";
    console.log(localVariable); // "Привет"
}
myFunction();

3. Использование глобальных переменных

Если переменная должна быть доступна везде, объявляйте ее глобально (но избегайте этого, если возможно).

let globalVariable = "Глобальная переменная";
function showVariable() {
    console.log(globalVariable);
}
showVariable(); // "Глобальная переменная"

4. Проверка имени переменной на ошибки

Проверьте, нет ли опечаток в имени переменной.

Неверный код:

let myVariable = "Данные";
console.log(myVarible); // Ошибка

Исправление:

let myVariable = "Данные";
console.log(myVariable); // "Данные"

5. Использование typeof для проверки существования переменной

Вы можете использовать оператор typeof, чтобы проверить, объявлена ли переменная.

if (typeof myVariable !== "undefined") {
    console.log(myVariable);
} else {
    console.log("Переменная не определена");
}

Пример из жизни
Вы пишете функцию для проверки данных пользователя:

function checkUser() {
    if (userName) {
        console.log(`Добро пожаловать, ${userName}!`);
    }
}
checkUser(); // Ошибка: userName не определен

Исправление:

let userName = "Иван";
function checkUser() {
    if (userName) {
        console.log(`Добро пожаловать, ${userName}!`);
    }
}
checkUser(); // "Добро пожаловать, Иван!"

Итог
Эта ошибка часто возникает из-за опечаток или неверной области видимости. Убедитесь, что переменные объявлены и доступны в нужных местах. Если необходимо, используйте typeof для проверки их существования.

JS
  • 15 ноября 2024

Как исправить ошибку JavaScript «Uncaught TypeError: Cannot set property of undefined»

Эта ошибка возникает, когда вы пытаетесь присвоить значение свойству объекта, который не существует (имеет значение undefined или null). Например:

let obj;
obj.property = "value"; // Uncaught TypeError: Cannot set property 'property' of undefined

Возможные причины

  • Объект не был инициализирован.
  • Вы обращаетесь к объекту, который уже удален или еще не создан.
  • Ошибка в цепочке данных, например, промежуточное свойство объекта равно undefined.

Шаги по исправлению

1. Инициализация объекта перед использованием

Убедитесь, что объект объявлен и инициализирован перед тем, как вы присваиваете его свойствам значения. Неверный код:

let obj;
obj.property = "value"; // Ошибка

Исправление:

let obj = {};
obj.property = "value"; // Работает

2. Проверка существования объекта перед доступом

Проверяйте объект на null или undefined, чтобы избежать ошибок. Пример:

let obj;
if (obj) {
    obj.property = "value";
} else {
    console.log("Объект не определен");
}

3. Использование опциональной цепочки

Если ошибка возникает в глубоко вложенных объектах, используйте опциональную цепочку. Пример:

let obj;
obj?.nestedProperty?.subProperty = "value"; // Не вызовет ошибку

4. Проверка асинхронных данных

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

let obj;
setTimeout(() => {
    obj = { property: "value" };
    console.log(obj.property); // Работает
}, 1000);

// Нельзя обращаться к obj до инициализации:
console.log(obj.property); // Ошибка

Пример из жизни
Вы загружаете данные пользователя с сервера и пытаетесь установить свойство объекта:

let user;
user.name = "Иван"; // Ошибка

Исправление:

let user = {};
user.name = "Иван"; // Работает

Итог
Чтобы избежать ошибки «Cannot set property of undefined», убедитесь, что объект существует и инициализирован. Используйте проверки на null, опциональную цепочку или инициализируйте объект перед работой с его свойствами.

JS
  • 15 ноября 2024

Как исправить ошибку JavaScript «Uncaught SyntaxError: Unexpected token»

Эта ошибка возникает, когда интерпретатор JavaScript сталкивается с неожиданным токеном (символом) в коде, который нарушает синтаксические правила. Например:

let obj = { key: "value", }; // Uncaught SyntaxError: Unexpected token }

Возможные причины

  • Лишние или пропущенные символы, такие как запятые, точки с запятой, кавычки или скобки.
  • Неправильное использование синтаксиса, например, забытые конструкции или операторы.
  • Смешение разных типов кавычек (например, двойных и одинарных) без соблюдения правил их вложенности.

Шаги по исправлению

1. Проверка корректности синтаксиса

Убедитесь, что все запятые, точки с запятой и кавычки находятся на своих местах. Неверный код:

let obj = { key: "value", }; // Лишняя запятая

Исправление:

let obj = { key: "value" }; // Правильный синтаксис

2. Проверка закрытия скобок

Проверьте, закрыты ли все фигурные, круглые и квадратные скобки. Неверный код:

if (true {
    console.log("Ошибка!");
}

Исправление:

if (true) {
    console.log("Исправлено!");
}

3. Проверка типов кавычек

Не смешивайте одинарные и двойные кавычки. Неверный код:

let str = "Привет'; // Ошибка

Исправление:

let str = "Привет"; // Или let str = 'Привет';

4. Использование отладчика

Откройте консоль браузера или отладчик в вашей IDE, чтобы найти строку с ошибкой. Она будет указана в сообщении об ошибке.

// В консоли может быть указано:
Uncaught SyntaxError: Unexpected token } at script.js:10

Исправьте строку, где произошла ошибка.

5. Проверка работы кода в строгом режиме

Добавьте "use strict"; в начало вашего файла или функции, чтобы браузер проверял ваш код на соответствие строгому синтаксису.

"use strict";
let obj = { key: "value" };
console.log(obj);

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

const add = (a, b => {
    return a + b;
};

Исправление:

const add = (a, b) => {
    return a + b;
};

Итог
Чтобы избежать ошибок «Unexpected token», всегда проверяйте синтаксис вашего кода на наличие лишних или пропущенных символов, используйте инструменты отладки и следите за правильным использованием конструкций JavaScript.

JS
  • 15 ноября 2024
9 книг по JavaScript для начинающих в 2024

9 книг по JavaScript для начинающих в 2024

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

Мы опросили знакомых разработчиков, узнали, что читают они сами, и предлагаем вам подборку хороших книг по JavaScript.

Читать дальше
JS
  • 6 марта 2024
Объект URL в JavaScript: полный разбор

Объект URL в JavaScript: полный разбор

Объект URL в JavaScript представляет URL-адрес и предоставляет удобные методы для работы с ним. Он позволяет анализировать, конструировать и декодировать URL-адреса.

Создать объект URL можно двумя способами:

Конструктор URL() — самый распространённый способ, в котором вы передаёте любой URL в виде строки в качестве аргумента.

const url = new URL("https://www.example.com/path?query=123#hash");

Использование window.location — это глобальный объект в браузерах, который содержит информацию о текущем URL.

const currentUrl = new URL(window.location.href);
Читать дальше
JS
  • 23 января 2024
Знакомство с JavaScript

Знакомство с JavaScript

Теперь, когда вы знаете, как создать структуру веб-страницы с помощью HTML и оформить ее стилями с помощью CSS, пришло время оживить её с помощью JavaScript (JS). JavaScript — это мощный язык программирования, который используется для создания интерактивных и динамических веб-сайтов.

Вы можете добавить JavaScript в ваш HTML-документ двумя способами:

Встроенный JavaScript: непосредственно в HTML-документ, в тегах <script>:

<script>
  alert("Привет, мир!");
</script>

Внешний JavaScript: подключение внешнего .js файла к HTML-документу:

<script src="script.js"></script>
Читать дальше
JS
  • 1 ноября 2023