Немного о Javascript

Это — очень краткая справка по языку. К тому же, немного устаревшая (она писалась в 2016 году; сейчас я её обновил, но только в совсем неактуальных моментах).

Где взять?

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

В качестве интерактивного режима может выступать Javascript-консоль, находящаяся среди Инструментов Разработчика (Developer Tools) браузера.

Выполнить программу целиком можно одим из следующих способов:

  1. Включить внутрь html-документа тег script. Внутри этого тега должна находиться программа на Javascript. Распознавание html для содержимого тега script отключено!

  2. Использовать пустой тег script с аттрибутом src=путь_к_файлу. Например, так:

<script src="foobar.js"></script>

Отметим также, что существует популярная альтернатива браузерам как Javascript-платформам — Node.js. Обычно её используют тогда, когда по тем или иным причинам удобно исполнять один и тот же кусок кода как на стороне пользователя, просматривающего веб-страницу, так и на стороне сервера, обслуживающего эту страницу. Впрочем, последнее время Node.js для чего только не используют (в том числе — используют как цель для кросскомпиляции программ на других языках).

Основные характеристики языка

Javascript является слабо динамически типизированным языком. Динамическая типизация означает, что типы проверяются только на этапе выполнения программы, но не на этапе компиляции (более того, переменные в Javascript не имеют типов).

Слабая же типизация означает, что даже выражения, которые не типизируются никаким адекватным образом (например, сумма числа и текста) всё равно вычисляются после неявного преобразование каких-то частей выражения к другому типу.

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

Основные синтаксические конструкции

Javascript получил своё название от языка Java, у которого он позаимствовал существенную часть синтаксиса (но не семантики). Язык Java, в свою очередь, синтаксис позаимствовал у C++.

Начнём с объявления переменных. Как ни странно, но это — весьма сложная часть языка. В Javascript есть четыре вида объявления переменных:

foo = 3;       // глобальная переменная
var foo = 4;   // переменная, локальная для функции
let foo = 5;   // переменная, локальная для блока
const foo = 6; // неизменяемая переменная, локальная для блока

Обратите внимание! Объявление глобальной переменной по своему виду совпадает с операцией присваивания. Это — распространённый источник ошибок!

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

Наконец, let и constпредпочтительный способ объявления переменных. Остальные два существуют только для обратной совместимости с кодом, написанным до 2015 года. Причём настоятельно рекомендуется использовать const везде, кроме тех случаев, когда let необходим.

Функции в Javascript задаются при помощи лямбда-выражений. Например:

const sayHello = (name) => { alert("Hello, " + name); };

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

const addNumbers = (x,y) => {
  return   // точка-с-запятой будет поставлена здесь!
    x+y;
};

или же

const something = x => {   // единственный вход можно не брать в скобки
  const foo = 2 
  ( generateFoo(foo) )(x)  // эта строчка является потенциальным продолжением предыдущей
                           // поэтому точка-с-запятой в конце предыдущей строчки поставлена не будет
}

Также допустимы объявления функций вида

function foo(x,y,z) { ... }

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

sayHello();  // код может встречаться и вне функций!

function sayHello() { ... }  // функция объявлена после её использования

Управляющие конструкции if, while, for, switch, break, continue, return в Javascript работают почти полностью аналогично соответствующим конструкциям из C. Команда goto отсутствует, зато есть возможность помечать циклы метками и выходить сразу из нескольких циклических уровней:

outer: for (let i = 0; i < 10; i += 1) {
  for (let j = 0; j < 10; j += 1) {
    console.log(i+j);

    if (i > j) { break outer; }
  }
}

Различные виды данных

В первом приближении в Javascript есть 8 «типов» данных: boolean, null, undefined, number, string, object, bigint, symbol.

Тип boolean содержит два значения: true и false. Тем не менее, любой объект может быть преобразован к этому типу. Ложными считаются: false, число 0, число -0, число NaN, null, undefined, пустой текст. Всё остальное считается истинным.

Типы null и undefined содержат по одному (одноимённому) значению. Различие между ними следующее: предполагается, что null — вполне корректное значение, которое программист может использовать в своих целях, а undefined — обычно признак ошибки в коде. Впрочем, иногда у них есть и другие назначения. Например, при преобразовании объектов в формат JSON поля со значением null остаются, а со значением undefined — нет.

Числа в Javascript обычно имеют тип number — аналог double из языка C. А именно, это — числа с плавающей точкой двойной точности. В связи с этим с ними нужно соблюдать осторожность: арифметика с плавающей точкой порой значительно отличается по поведению от арифметики целых чисел.

Есть ещё тип bigint. Литералы этого типа имеют суффикс n. Например, 123n. Это — целые числа произвольного размера. Аналогичны целым числам в Python.

Тексты типа string представляют собой неизменяемые массивы символов в кодировке UTF-16 (это означает, что некоторые символы занимают по два «места»). К отдельным символам можно обращаться при помощи квадратных скобок или же метода charAt (например "foobar".charAt(2)). Длина текста хранится в поле length ("foobar".length равно 6). Текстовые литералы можно обозначать как одинарными, так и двойными кавычками — разницы между ними нет. Также есть обратные кавычки, внутри которых:

  • можно переходить на новую строчки — ошибки компиляции не будет
  • можно вставить текстовое представление значения любой формулы — эту формулу нужно взять в доллар с парой фигурных скобок: ${формула}

Текст можно преобразовать в число функциями parseInt, parseFloat и BigInt.

Тип symbol используется не слишком часто, поэтому мы не будем на нём отдельно останавливаться.

Всё остальное (в том числе функции и массивы) считаются представителями типа object. Поскольку объекты бывают изменяемыми, очень важно понимать семантику операции присваивания. В Javascript присваивание даёт объекту новое имя (в точности как в Python):

a = [1,2,3];
b = a;
b[1] = 4;  // теперь a[1] -- тоже 4, поскольку b и a именуют один и тот же массив

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

Например, так:

const someObject = {
  foo: {  // ключи не обязательно брать в кавычки
    bar: 123, 
    baz: 456 
  },
  "quuz": 789,
};

Получить доступ к полям такого объекта можно как при помощи квадратных скобок (например, someObject["foo"]), так и при помощи точки (например, someObject.foo). Квадратные скобки более универсальны (работают, например, для ключей, состоящих из цифр), но никак не обрабатываются различными оптимизаторами и минификаторами кода (впрочем, оптимизаторы и минификаторы не являются частью языка, да и используются зачастую только для того, чтобы убрать из кода все пробелы и комментарии).

В заключение пара слов о массивах. Массивы ведут себя так, как будто они являются объектами с ключами "0", "1", "2" и так далее.

Тем не менее, для них есть специальная поддержка со стороны языка. Литералы массивов выделяются с двух сторон квадратными скобками. Длина массива хранится в поле length. Доступ к элементам осуществляется при помощи квадратных скобок. Наиболее простой способ скопировать массив: воспользоваться методом slice (foobar.slice() создаёт копию массива, именуемого переменной foobar).

Метод push позволяет добавить элементы в конец массива (например, foobar.push(1,2,3) добавляет элементы 1, 2, 3 в конец массива, именуемого foobar). Можно добавлять элементы и просто при помощи квадратных скобок и присваивания:

foobar[foobar.length] = 1;
foobar[foobar.length] = 2;
foobar[foobar.length] = 3;
// эквивалентно foobar.push(1,2,3)

Циклы «foreach»

Кроме сиподобного цикла for в Javascript есть более лакончиные

  • for (let/const переменная in формула)
  • for (let/const переменная of формула)

Оба они работают примерно как for из Python: сначала вычисляется значение формулы, а затем, если это значение имеет тип object:

  • for in проходится по всем ключам объекта (кроме тех, которые помечены как «неперечисляемые»)
  • for of запускает специальный механизм итерации (для произвольно взятого объекта этот механизм не определён; зато определён, например, для массивов, для которых for of в основном и используется)

Сравнение объектов

Отдельный раздел мы посвятим операторам сравнения == и === (и их отрицаниям != и !==). Различие между ними простое: === проверяет только равенство внутри одного типа; объекты разного типа он считает разными; == дополнительно производит преобразование типов. Поскольку запомнить семантику оператора == весьма сложно, не рекомендуется его использовать кроме как для сравнения чисел с текстами, состоящими из цифр. Поэтому мы опишем только действие ===.

  • булевы значения сравниваются естественным образом
  • null равно null, то же самое — с undefined
  • тексты сравниваются посимвольно
  • числа сравниваются в соответствии с правилами арифметики с плавающей точкой
  • объекты равны только при равенстве указателей (т.е. когда это физически один и тот же объект в памяти компьютера); в частности, обычно [1,2,3] не равен [1,2,3]!

Оператор typeof

В Javascript есть оператор typeof, который позволяет узнать тип объекта. У него есть две особенности:

  • typeof null === 'object', несмотря на то, что настоящий тип null называется null
  • typeof функция === 'function', хотя функции имеют тип object

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

JSON

Javascript породил такой текстовый формат представления данных как JSON. Каждая единица данных в JSON — это одно из:

  • число (оформляется стандартным образом)
  • текст (оформляется при помощи двойных кавычек)
  • список (оформляется при помощи квадратных скобок и запятых)
  • объект (оформляется при помощи фигурных скобок и запятых; запятые разделяют пары ключ : значение, где ключ — это текст, а значение — произвольный JSON)
  • булево значение: false или true
  • значение null

Сам Javascript поддерживает в качестве литералов некоторое надмножество JSON.

Например, объект, соответствующий JSON

{ "foo" : "bar", "baz": { "quuz": 42 } }

в Javascript можно записать как

{ foo: `bar`, baz: { quuz: 42, }, }

Для преобразования между Javascript-объектами и JSON существуют функции JSON.parse (из JSON в JS) и JSON.stringify (из JS в JSON).

Зачастую JSON.stringify — наиболее простой способ сравнить между собой сложные объекты (например, два массива). К сожалению, числа типа bigint не поддерживаются этой функцией.

Функции как замыкания

Если функция объявляется в контексте каких-то переменных и их использует, то эти переменные остаются живы всё то время, пока жива эта функция.

Например, работает следующий код:

function makeAdder(toAdd) {
  return x => toAdd + x;  // тело вида { return формула; } можно заменить просто на эту формулу
}

const foo = makeAdder(3);

console.log(foo(1));  // печатает 4
console.log(foo(2));  // печатает 5

Более того, подобным образом можно сделать «объект» с изменяемым состоянием:

function makeVar(init) {
  let myVar = init;

  return [
    () => myVar,
    (newVal) => { myVar = newVal },
  ];
}

const [ getFoo, setFoo ] = makeVar(42);
console.log(getFoo());  // 42
setFoo(24);
console.log(getFoo());  // 24

И даже — «модуль первого класса»:

function makeLibrary(/*какие-то параметры*/) {

const FOO = /*...*/;
const BAR = /*...*/;

function someFunc1(/*...*/) { /*...*/ }
function someFunc2(/*...*/) { /*...*/ }
function someFunc3(/*...*/) { /*...*/ }

return { someFunc1, someFunc2, someFunc3 };
}

Можно для каждого набора параметров создать свой экземпляр «библиотеки» и одновременно пользоваться несколькими такими экземплярами. Иногда это бывает очень удобно!