Вспомним несколько важных понятий из прошлого курса:
Выражение — код, выполнение которого возвращает значение. Инструкция — код, представляющий собой команду.
- Выражения — вычисляются.
- Инструкции — исполняются.
К выражениям относятся:
- Вызов функции
- Арифметические и логические операции
- Тернарный оператор
- и другие
К инструкциям относятся:
for
while
break
return
if
- и другие
Список выражений и инструкций на этом не заканчивается, с полным перечнем можно ознакомиться в документации (см. дополнительные материалы к уроку).
Для лучшего понимания, как могут взаимодействовать друг с другом различные конструкции языка, необходимо знать разницу между ними.
Возьмем условный оператор if
. В некоторых языках он представлен инструкцией, в некоторых — выражением. Посмотрите на код ниже и подумайте, возможно ли такое в JavaScript и почему:
const value = if (something) {
one
} else {
two
};
Такой код в JavaScript невозможен только по одной простой причине: if
— это инструкция, а не выражение.
Но такой код возможен и часто используется в языках вроде Ruby или Python. Зачем же нужен if
, если есть тернарный оператор, который как раз и является выражением?
Дело вот в чем. Если бы if
был выражением, то тернарный оператор стал бы попросту не нужен, несмотря на то, что он является более лаконичной заменой if
. Но и тернарный оператор бывает неудобен в тех ситуациях, когда вычисление слишком большое и не помещается в одну строку, а его результат должен быть записан в одну и ту же константу или переменную в каждой из веток. В такой ситуации как раз пригодился бы if
как выражение.
Этот пример ярко иллюстрирует тот факт, что конструкции языка, представленные выражением, делают язык гибче, а решения с их использованием делают код лаконичнее. Другими словами, язык становится выразительнее. Кроме описанного выше, у выражений есть еще одно огромное преимущество: они могут комбинироваться друг с другом и вкладываться друг в друга.
Ниже рассмотрим варианты комбинирования выражений, но с акцентом на функции:
Арифметические операции
const r1 = 5;
const r2 = 5 + 8;
const r3 = 5 + 8 - Math.PI;
const r4 = 5 + 8 - Math.PI * Math.sqrt(16);
const r5 = f1() + f2() * f3();
На последней строке может возникнуть вопрос: насколько допустимо складывать результаты функций? Вызов функции — это выражение, возвращающее результат, так что этот код допустим на 100%. Если функция вернет значение, неподходящее для сложения, то может возникнуть ошибка, но это будет логическая ошибка, а не синтаксическая.
Примечание: предполагается, что используемые в примерах этого урока функции и константы (например, f2
или isEditing
) ранее где-то были определены. Их определение мы убрали из примеров, чтобы не отвлекать от главного.
Логические операции
Все то же самое можно делать и с логическими выражениями:
const r1 = true;
const r2 = true || false;
const r3 = true || false && isEditing;
const r4 = true || false && isEditing || isEmpty(data);
const r5 = f1() || f2() && f3();
Из-за слабой типизации подобный код работать будет вообще всегда, даже если функции возвращают не true
или false
, но пользоваться этим не стоит. (Типизация рассматривалась в уроке предыдущего курса).
Аргументы
А теперь чуть более сложный пример. Когда мы вызываем функцию, то в аргументах ожидается выражение: func(<expression>, <expression>, ...)
. А из этого следует, что мы можем сделать так:
const r1 = f();
const r2 = f(5);
const r3 = f(5 + Math.PI);
const r4 = f(5 + Math.PI - cube(number));
const r5 = f(f1(f2(n3, f3(n1, n2))), f4());
Если последний вызов вам кажется сложным, то вспомните вот что. Когда мы изучали русский язык в школе, то постоянно делали грамматический разбор предложения, в котором выделяли существительное, подлежащее, сказуемое, различные обороты и многое другое. Здесь нужно сделать то же самое. Очень важно уметь разбивать в уме сложное выражение на составные.
Пройдемся по примеру выше. f(f1(f2(n3, f3(n1, n2))), f4())
содержит в аргументах два выражения:
f4()
f1(f2(n3, f3(n1, n2)))
— этот вызов содержит один аргумент — вызов функции:f2(n3, f3(n1, n2))
, - который в свою очередь содержит два аргумента:n3
и вызов функцииf3(n1, n2)
.
Порядок выполнения
Осталось понять, в какой последовательности происходят эти вызовы. JavaScript считается энергичным языком, то есть языком с аппликативным порядком вычисления, а это значит, что аргументы вычисляются до того, как попадают внутрь функций.
const f1 = () => console.log('called f1');
const f2 = () => console.log('called f2');
const f3 = () => console.log('called f3');
const f4 = () => console.log('called f4');
const f5 = () => console.log('called f5');
const f6 = () => console.log('called f6');
f1(f2(f4(), f5()), f3(f6()));
// => called f4
// => called f5
// => called f2
// => called f6
// => called f3
// => called f1
https://repl.it/@hexlet/js-functions-execution-nested-calls
Как видно, вычисление идет с самого глубокого уровня слева направо. Каждая функция попадает в стек вызовов, где работает принцип "первым вошел - последним вышел". Поскольку каждый аргумент - тоже функция, то они также добавляются за ней следом. Те функции, которые возвращают результат, сразу вычисляются. Так получается, что стек наполняется и очищается до тех пор, пока не выполнится f1
.
Дополнительные материалы
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.