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

const length = 12;
const num = length(54);
Сначала мы создали константу. Помните, что это как давать чему-то название: в нашем случае — числу 12 даётся название length. В следующей строке мы вызываем функцию length и передаём ей аргумент — число 54. Но подождите! length — это не функция! Это всего лишь число. Числа — это не функции, не ящики, которые производят какие-то действия. И JavaScript пожалуется именно на это:

→ node test.js
/Users/rakhim/test.js:2
const num = length(-54);
            ^

TypeError: length is not a function
    at Object.<anonymous> (/Users/rakhim/test.js:2:13)
    at Module._compile (module.js:571:32)
    at Object.Module._extensions..js (module.js:580:10)
    at Module.load (module.js:488:32)
    at tryModuleLoad (module.js:447:12)
    at Function.Module._load (module.js:439:3)
    at Module.runMain (module.js:605:10)
    at run (bootstrap_node.js:420:7)
    at startup (bootstrap_node.js:139:9)
    at bootstrap_node.js:535:3
Это Ошибка типизации: тип объекта, который вы использовали, неверный. Интерпретатор JavaScript не скажет чем что-то является, но точно скажет чем оно не является. length — это не функция.

Ошибка типизации — это как просить кошку постирать бельё. Возможно, вы хотели попросить об этом вашего друга.

В программировании "типизация" — это классификация информации. Это общий термин и разные языки программирования справляются с типизацией по-разному. Как вы уже знаете, JavaScript умеет отличать типы. Функция — это один тип, Число — другой, и вы не можете просто использовать число как функцию.

typeof — это специальный оператор, который возвращает строку, в которой написан тип.

typeof 42;      // 'number'
typeof 3.14;    // 'number'
typeof NaN;     // 'number'

typeof 'Berry'; // 'string'
typeof true;    // 'boolean'
typeof false;   // 'boolean'
42 и 3.14, очевидно, числа, несколько комбинаций букв в кавычках — строка, а true и false — булево значение. Всё это — типы в JavaScript — число, строка и булево значение.

NaN означает — "не число", но тип NaN — это "число". Да, я знаю. Еще одна странность JavaScript. Такие правила в этом языке.

Типизация полезна. Когда мы попытаемся запустить число, как будто это функция, JavaScript начнёт жаловаться и мы увидим ошибку и починим её. Если бы никакого обозначения типов в JavaScript не было, мы бы сталкивались либо с каким-нибудь аномальным поведением, либо с мистической ошибкой. Вместо чёткого "length — это не функция", мы бы видели что-то вроде "I'm sorry Dave, I'm afraid I can't do that".

А что, если создать переменную, но не задать ей никакого значения? Какой в этом случае будет тип? Это ни число, ни строка, ничто... Потому что нет значения, правильно?

JavaScript в этом случае кое-что делает в тайне от вас. Переменная без значения на самом деле имеет специальное значение — "undefined". И тип такой переменной называется "undefined".

let a;
console.log(a);   // undefined
typeof a;         // 'undefined'
Например, тип number имеет множество потенциальных значений: 1, 2, -10, 69000 и другие числа. А тип undefined только одно — undefined.

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

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

Код, который вы пишете, обычно конвертируется в понятную для запуска компьютером форму. Этот процесс называется компиляцией, а промежуток времени, за который это происходит — "стадией компиляции" или compile time.

После того, как компиляция закончена и программа запущена, начинается отсчёт времени, который называется "стадией исполнения" или run time.

Некоторые языки проверяют типы и ищут ошибки типизации на стадии компиляции. У них статическая типизация.

Другие языки проверяют типы и ищут ошибки типизации на стадии исполнения. Такая типизация — динамическая.

Иными словами: статическая типизация означает проверку типов перед запуском программы, динамическая — проверку типов, когда программа запущена.

C#, C++, Java, Go — статически типизированные языки. Если в одном из этих языков вы создадите число и попытаетесь проводить с ним операции, как с функцией, вы получите ошибку во время компиляции, а программа не станет запускаться — она даже не дойдёт до этой стадии, потому что ошибка типизации будет обнаружена перед исполнением, в период компиляции.

JavaScript, Ruby, PHP — динамически типизированные языки. Как вы видели раньше, если использовать неверную типизацию, ваша программа запустится, а ошибка обнаружится только когда будет исполняться конкретная строчка кода. Здесь типы проверяются в период исполнения.

Вообще-то, в JavaScript обычно нет никакой компиляции, но это тема другого урока.

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

Теперь давайте поговорим о слабой и сильной типизации. Посмотрите на этот JavaScript код:

4 + '7';      // '47'
4 * '7';      // 28
2 + true;     // 3
false - 3;    // -3
М-да… Это… Ок, что тут происходит? Сложение числа 4 со строкой "7" даёт нам строку "47". JavaScript конвертирует число 4 в строку "4" и конкатенирует две строки — склеивает их друг с другом. JavaScript просто берёт на себя ответственность предположить, что это то, что мы хотели. Глупо обвинять его — чего мы действительно хотели? Складывать число со строкой не имеет никакого смысла. Какой-нибудь другой язык, вроде Ruby или Python просто бы пожаловался и ничего не сделал.

Произведение числа 4 со строкой "7", это, как видите, 28, по мнению JavaScript. В этом случае он сконвертировал строку "7" в число 7 и произвёл обычное умножение.

JavaScript постоянно так делает. Он знает о типах разных значений, но когда типы не соответствуют, он пытается предположить и сконвертировать один тип в другой, не предупреждая вас. Иногда это полезно, иногда мозгодробяще. Такое происходит потому что JavaScript — язык со слабой типизацией. У него есть представление о типах, но он типа "это всего лишь игра, чего ты злишься?"

У этой концепции нет ничего общего с динамической и статической типизацией, смысл которых — КОГДА проверять типы. Сильная против слабой — это НАСКОЛЬКО СЕРЬЁЗНО проверять типы.

Вы можете считать, что слабая — это нестрогая типизация, а сильная — это требовательная.

В отличие от динамичности-статичности, сила типизации это спектр. У PHP типизация немного сильнее. У Python ещё сильнее. И все они динамически типизированные языки.

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

// Конвертация числа в строку
String(44843);  // '44843'

// Конвертация строки в число
Number('590');    // 590
Number('aaa!!');  // NaN

// Конвертация числа в булево значение
Boolean(1);       // true
Boolean(0);       // false

// Конвертация булева значения в строку
String(true);     // 'true'
String(false);    // 'false'
Можно предположить, что неявная конверсия из типа в тип — не самая лучшая идея. Неявный, значит скрытый, а скрытый — значит трудно понимаемый и предрасположенный к ошибкам. Поведение программы становится менее очевидным. Вы пишете меньше кода, да, но код более хрупкий и менее понятный.
Дополнение к уроку
null
В JavaScript кроме undefined существует null. Оно означает, что «значение отсутствует». Например, если создать переменную, но не задавать ей значения, то у нее будет значение undefined:

let a;
console.log(a);     // undefined
Тут значения не оказалось ненамеренно. Видимо, просто еще не пришло время дать этой переменной значение.

null нужен для явного, намеренного указания, что значения нет. Можно сказать let a = null;. Например, вы попросили пользователя ввести информацию, но он ничего не ввел. В таком случае уместно записать в результат null.

null, в отличие от undefined, можно задавать вручную, передавать как аргумент в функцию и в целом использовать как любое другое явное значение.

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

При сравнении null и undefined нужно быть осторожным:

typeof null;          // "object" (не "null" по историческим причинам)
typeof undefined;     // "undefined"
null === undefined;   // false

null == undefined;    // true
null === null;        // true
null == null;         // true
!null;                // true

isNaN(1 + null);      //false
isNaN(1 + undefined); //true
Сравнение
В этом курсе мы сравниваем данные, используя три знака равенства:

a === b;
12 === 12;
Это сравнение прямое: являются ли эти данные абсолютно идентичными?

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

1 === '1';    // false
1 == '1';     // true

true === 1;   // false
true == 1;    // true
Выводы
Типизация в JavaScript
JavaScript имеет представление о типах: числах, строках, функциях, логических значениях и так далее. typeof возвращает строку, в которой записан тип:

typeof 42;      // 'number'
typeof 3.14;    // 'number'
typeof NaN;     // 'number'

typeof 'Berry'; // 'string'
typeof true;    // 'boolean'
typeof false;   // 'boolean'
NaN означает "не число", но тип этого значения — number.
Переменная без значения имеет специальное значение undefined. Тип такой переменной — undefined:

let a;
console.log(a);   // undefined
typeof a;         // 'undefined'
Динамическая и статическая типизация
Код конвертируется в другую форму, которую компьютер может запустить. Этот процесс называется компиляцией, а период времени, за который этот процесс происходит — стадией компиляции (compile time).

После того, как компиляция закончена, запускается программа и период, пока она запущена, называется стадией исполнения (run time).

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

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

Иными словами: статическое типизирование означает проверку типов перед запуском программы; динамическое — проверку типов пока программа запущена.
Слабая и сильная типизация
JavaScript часто конвертирует типы автоматически:

4 + '7';      // '47'
4 * '7';      // 28
2 + true;     // 3
false - 3;    // -3
JavaScript — это язык со слабой типизацией. У него есть представление о типах, но он расслаблено к ним относится и может оперировать значениями, можно сказать, произвольно. Чем сильнее система типизации, тем строже правила.
Явные конверсии в JavaScript

Number('590');    // 590
Number('aaa!!');  // NaN

Boolean(1);       // true
Boolean(0);       // false

String(true);     // 'true'
String(false);    // 'false'

String(44843);  // '44843'
Дополнительные материалы
Упражнение
Реализуйте и экспортируйте по умолчанию функцию, которая работает следующим образом:

Дано неотрицательное целое число num. Складывать все входящие в него цифры до тех пор, пока не останется одна цифра.
Для числа 38 процесс будет выглядеть так:

  1. 3+ 8 = 11
  2. 1 + 1 = 2
Результат: 2

Примеры

addDigits(10); // 1
addDigits(19); // 1
addDigits(38); // 2
addDigits(1259); // 8
Подсказки

  • Выделите процесс суммирования цифр в числе в отдельную функцию
  • Для преобразования типов используйте функции из урока