Переменные, декларативное и императивное программирование
Транскрипт урока
Мы уже знакомы со способом называть что-то, используя константы. Например, это константа pi со значением 3.14.

const pi = 3.14;
После этой строки, каждый раз, когда мы видим pi, мы знаем, что её значение 3.14. Она называется константой, потому что, ээм, она неизменна, постоянна. После этой строки pi всегда будет 3.14, это никогда не изменится. Именно поэтому по аналогии с бумагой я использую ручку.

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

Тем не менее, иногда вам может потребоваться изменить уже существующие данные или, другими словами, их значения. Допустим, вы хотите повторить что-то 5 раз. Один способ — выполнять повторы, отсчитывая до пяти, а затем остановиться. Для этого вам потребуется что-нибудь для хранения счётчика этого меняющегося числа. Константа не будет в таком случае работать — она неизменна, вы не можете изменить её значение после того, как уже создали её.

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

let factorial = 1;

factorial = factorial * 2; // 2
factorial = factorial * 3; // 6
factorial = factorial * 4; // 24
factorial = factorial * 5; // 120
Создать переменную — просто, она выглядит как константа, только вместо const мы пишем let. Мы позволяем ей быть чем-то и это не навсегда.
Затем мы изменяем значение factorial. Мы бы такого не смогли сделать, если бы factorial был константой. Эта строка означает "изменить значение переменной факториал, на результат умножения факториала на 2". Теперь JavaScript умножает factorial на 2 и хранит этот результат в переменной factorial. Раньше factorial был 1, а теперь это 2.

Мы повторяем это ещё трижды, каждый раз умножая полученное значение на следующее целое число: на 3, на 4 и на 5. Это то, что вы возможно делаете в уме, когда умножаете числа. Вероятно, вы не думали об этом так чётко, но это неплохой способ описания процесса вычисления, если бы вам было нужно объяснить его.

Идея использования счётчика для повторения чего-то множество раз — распространённая в программировании, и большинство языков программирования имеют для этого "циклы". Давайте рассмотрим один тип цикла — "цикл while". Это блок кода, который повторяется, пока удовлетворяется какое-то условие.

Представьте фермера, который работает от рассвета до заката. Другими словами, он работает пока солнце в небе. Вы можете записать:

while (sun is up) {
  work
}
Конечно, это не настоящий JavaScript, это просто чтобы показать идею. Эта строка "work" будет повторяться снова и снова, пока солнце над горизонтом. Это значит после каждого повторения нам нужно проверять, действительно ли солнце в небе, и остановиться если это не так. Другими словами: проверить — исполнить, проверить — исполнить, и так далее.

Вот функция факториала с переменными и циклом вместо рекурсии.

const factorial = (n) => {
  let counter = 1;
  let result  = 1;

  while (counter <= n) {
    result = result * counter;
    counter = counter + 1;
  }

  return result;
}
О-о, что тут происходит? Во-первых, мы создали две переменные: одна для счётчика, чтобы считать от 1 до верхнего предела, а вторая для текущего результата.

Затем начинается главная часть: цикл while, который повторяется, пока счётчик меньше или равен n — числу, переданному в эту функцию. Код, который повторяется, простой: мы меняем значения наших двух переменных. Текущий результат умножается на счётчик, а счётчик увеличивается на 1.
В какой-то момент это условие — "счётчик меньше или равен n" — станет ложным, цикл больше не будет повторяться, а программа перейдёт к следующему этапу — return result. К этому моменту результат станет ответом, потому что за время всех повторов в цикле, результат умножался на 1, затем на 2, 3 и так далее, пока не достиг значения n, каким бы оно ни было.
Давайте посмотрим, что компьютер делает шаг за шагом, когда мы вызываем факториал 3.

  1. Взять один аргумент — 3, известный внутри, как n
  2. Создать переменную counter, установить значение 1
  3. Создать переменную result, установить значение 1
  4. Проверить: в счётчике — 1, это меньше или равно n, поэтому
  5. Умножить result на counter и положить ответ — 1 — в result
  6. Добавить 1 к counter и положить ответ — 2 — в counter
  7. Вернуться и проверить: counter — 2, это меньше или равно n, поэтому
  8. Умножить result на counter и положить ответ — 2 — в result
  9. Добавить 1 к counter и положить ответ — 3 — в counter
  10. Вернуться и проверить: counter — 3, это меньше или равно n, поэтому
  11. Умножить result на counter и положить ответ — 6 — в result
  12. Добавить 1 к counter и положить ответ — 4 — в counter
  13. Вернуться и проверить: counter — 4, это не меньше и не равно n, поэтому остановить повтор и перейти к следующей строке
  14. Вернуть result — 6
Компьютер выполняет такие операции в миллиарды раз быстрее, но по сути это выглядит именно так. Общее название такого вида сформулированных повторений — "итерация". Наша программа использует итерацию, чтобы рассчитать факториал.

В прошлый раз мы рассматривали итеративный процесс с рекурсией, а в этот — итеративный процесс без рекурсии.

Оба используют технику итерации, но с рекурсивными вызовами нам не нужно менять значения, мы просто передаём новые значения в следующий вызов функции. А эта функция факториала не имеет рекурсивных вызовов вообще, поэтому все трансформации должны происходить внутри единственного экземпляра, единственной функциональной коробки. У нас нет выбора, кроме как менять значения содержимого.

Стиль программирования, который вы видели в предыдущих уроках, называется "декларативным". Сегодняшний стиль, с изменением значений, называется "императивным".

Сравните рекурсивный и нерекурсивный факториалы:

const recursiveFactorial = (n) => {
  if (n === 1) {
    return 1;
  }
  return n * recursiveFactorial(n-1);
}

const factorial = (n) => {
  let counter = 1;
  let result  = 1;

  while (counter <= n) {
    result = result * counter;
    counter = counter + 1;
  }

  return result;
}
Эта рекурсивная функция — декларативная — она как описание факториала. Она объясняет, что такое факториал.

Это нерекурсивная итеративная функция, и она императивная — она описывает, что делать, чтобы найти факториал.

Слово декларативный происходит от латинского "clarare" — разъяснять, заявлять, делать объявление. Вы разъясняете: я хочу, чтобы факториал n был n умножить на факториал n-1.

Слово императивный происходит от латинского "imperare", что значит "командовать". Вы приказываете чётко передвигаться по шагам — умножать это на это, пока идёт отсчёт и запоминать какие-то числа.
Декларативное — это что. Императивное — это как.

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

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

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

От изменения состояния появляется гора багов, а оператор присваивания (assignment statements), который создает изменения, часто является причиной всего зла во вселенной.
Дополнение к уроку
var vs let
Кроме let существует другой способ определения переменных: var a = 5. Этот способ был единственным до появления стандарта ES6, но в современном JavaScript он является устаревшим, и let полностью заменил его.

let и var по-разному влияют на видимость переменной, и использование var сегодня нежелательно. let был создан как более правильная альтернатива старому способу.
Отладка: Как найти и исправить ошибки
Наделать ошибок очень легко, когда приходится справляться с переменными, изменять их, отслеживать и всякое такое. Особенно в зацикленном процессе. Прекрасный способ разобраться с тем, что происходит — использовать простейшую технику для отладки — console.log. Вспомните, эта функция выводит в консоль то, что ей передают.

Например, я пытаюсь разобраться, что происходит внутри цикла while:

while (counter <= n) {
  result = result * result;
  counter = counter + 1;
}
Добавлю сюда console.log:

while (counter <= n) {
  result = result * result;
  counter = counter + 1;
  console.log(result)
}
Теперь, каждый раз, когда повторяется этот блок кода, переменная result будет выводиться на экран. Давайте посмотрим:

1
1
1
(Я запускаю функцию с n равным 3)

В каждом шаге result — это 1. Не верно — result должен возрастать на каждом шаге… Ок, значит вероятная проблема, в строке, где result меняется. Так и есть! У меня result = result * result, а мне нужно умножить result на counter, а не на result.

1
2
6
Теперь всё работает! Теперь я вижу шаги и последний шаг выдал верный ответ: 3! это 6. Используйте console.log абсолютно везде. Это ваш лучший друг :-)
Бесконечные циклы
Поскольку цикл while только проверяет состояние, мы можем создать бесконечные циклы, если сделаем состояние всегда истинным.

while (10 > 5) {
  console.log("Ten is still larger than 5");
}
Этот код будет выводить на экран "Десять всё ещё больше чем 5" пока не сгорит вселенная или пока вы не закроете программу, ну или пока компьютер не израсходует всю свою память — что бы ни случилось первым. Поскольку состояние 10 > 5 всегда истинно.

Даже ещё более простой способ сделать бесконечный цикл — while (true) { ... }. true всегда истинный.

Иногда ваши циклы будут бесконечными, даже если вы этого не планировали. Это общая проблема, и она указывает только на то, что внутри цикла вы забыли изменить проверяемую деталь. Например, если я удалю строку counter = counter + 1 из цикла в сегодняшнем уроке

while (counter <= n) {
  result = result * result;
}
… у меня получится бесконечный цикл: counter никогда не меняется, поэтому если counter <= n, условие всегда будет истинно.
Выводы
Переменные подобны константам, но вы можете изменить их значения в любой момент.

let age = 21;

age = 22;         // now age is 22
age = age + 10;   // now age is 32
Циклы — это повторяющиеся блоки кода. Цикл while — это блок, повторяющийся пока какое-то состояние истинно.

while (condition) {
  do_stuff;
}
Вот факториал функции с циклом while:

const factorial = (n) => {
  let counter = 1;
  let result  = 1;

  while (counter <= n) {
    result = result * counter;
    counter = counter + 1;
  }

  return result;
}
Идея: сделать counter = 1, затем умножать result на counter повторно, пока идёт отсчёт до n (число, передаваемое функции). Когда counter станет больше, чем n — остановиться. К тому моменту result будет ответом.

Это итерация — сформулированное повторение кода. Разные языки по-разному выполняют итерацию. Цикл while — это один из способов, который предлагает JavaScript.
Декларативное vs. императивное программирование
Сравните рекурсивный факториал (из 9 урока) и нерекурсивный факториал (из сегодняшнего):

const recursiveFactorial = (n) => {
  if (n === 1) {
    return 1;
  }
  return n * recursiveFactorial(n-1);
}

const factorial = (n) => {
  let counter = 1;
  let result  = 1;

  while (counter <= n) {
    result = result * counter;
    counter = counter + 1;
  }

  return result;
}
Эта рекурсивная функция — декларативная — она как бы определение (трактование, характеристика) факториала. Она декларирует, что такое факториал.

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

Слово декларативный происходит от латинского "clarare" — разъяснять, заявлять, делать объявление. Вы разъясняете: я хочу, чтобы факториал n был n раз факториалом n-1.

Слово императивный происходит от латинского "imperare", что значит "командовать". Вы приказываете чётко передвигаться по шагам — умножать это на это, пока идёт отсчёт и запоминаются какие-то числа.
Эта рекурсивная функция — декларативная — она как бы определение (трактование, характеристика) факториала. Она декларирует, что такое факториал.

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

Слово декларативный происходит от латинского "clarare" — разъяснять, заявлять, делать объявление. Вы разъясняете: я хочу, чтобы факториал n был n раз факториалом n-1.

Слово императивный происходит от латинского "imperare", что значит "командовать". Вы приказываете чётко передвигаться по шагам — умножать это на это, пока идёт отсчёт и запоминаются какие-то числа.
Декларативное — это что. Императивное — это как.
Писать декларативный код, в целом, лучший выбор. Ваш код будет легче читать, понимать и делать что-то новое опираясь на него. Но иногда у вас нет выбора.

От изменения состояния* появляется гора багов, а инструкции (операторы) присваивания (assignment statements), которые создают изменения, часто являются коренными причинами всего зла во вселенной.
Поэтому, когда дело доходит до инструкций присваивания, действуйте осторожно.
Дополнительные материалы
Упражнение
Напишите функцию smallestDivisor(). Она должна находить наименьший целый делитель числа. Поведение у функции такое же, как в предыдущем уроке, но реализация (код функции) должна быть другой. На этот раз реализуйте императивный итеративный процесс, что означает:
  • не используйте рекурсию
  • используйте переменные
  • используйте цикл while
Например, наименьший делитель числа 15 это 3.

smallestDivisor(15); // 3
smallestDivisor(17); // 17
 
smallestDivisor(0); // NaN
smallestDivisor(-3); // NaN
Примечания
  • Если переданное в smallestDivisor() число меньше единицы, возвращайте NaN.
Алгоритм
Идея алгоритма:
  1. Попробуйте разделить число на 2.
  2. Если число делится без остатка, то это наименьший делитель.
  3. Если нет, то попробуйте следующий делитель.
  4. Если ничего не делит число без остатка, то переданное число является простым, так что его наименьший делитель — оно само (не считая 1)
Подсказки
  • Вспомните про оператор % (modulus or остаток от деления) из урока 5. Он вычисляет остаток от деления одного операнда на другой. Например, 11 % 5 это 1, а 10 % 2 это 0. Так что если x % y это 0, то y делит x без остатка.