Типы данных

Тип данных Symbol

Символы нужны для создания уникальных идентификаторов. Использование:

  1. «Скрытые» свойства объектов.
  2. Системные символолы, доступные как Symbol.* , например:
    • Symbol.isConcatSpreadable
    • Symbol.iterator
    • Symbol.toPrimitive

Синтаксис:

let id = Symbol("id"); // Символ с описанием (именем) "id"

let user = { [id]: 12, }; user[id] = 123;

Символы уникальны (не равны друг другу), даже если имеют одинаковое описание.

Для любых символов доступно свойство description:

id.description; // id

Символы игнорируются циклом for…in

Символы не преобразуются автоматически в строки:

alert(id); // TypeError: Cannot convert a Symbol value to a string

Методы для получения символов:

  • Object.getOwnPropertySymbols(obj) возвращает массив символьных ключей объекта
  • Reflect.ownKeys(obj) возвращает массив всех собственных ключей объекта, включая символьные

Объекты

"key" in object // оператор "in" определяет существование свойста

Глубокое клонирование объектов можно осуществить с помощью рекурсии, воспользовавшись глобальным методом structuredClone(obj), либо использовать метод _.cloneDeep(obj) из JavaScript-библиотеки lodash.

Получение свойств объекта

Object.getOwnPropertyNames(obj) возвращает не-символьные ключи.

Object.getOwnPropertySymbols(obj) возвращает символьные ключи.

Object.keys/values() возвращают не-символьные ключи/значения с флагом enumerable.

for..in перебирает не-символьные ключи с флагом enumerable, а также ключи прототипов.

Reflect.ownKeys(obj) возвращает все собственные ключи объекта, включая символьные.

Сборка мусора

Основной алгоритм сборки мусора – «алгоритм пометок» («mark-and-sweep»).

  • Сборщик мусора «помечает» (запоминает) все корневые объекты.
  • Затем он идёт по их ссылкам и помечает все найденные объекты.
  • Затем он идёт по отмеченным объектам и отмечает их ссылки. Все посещённые объекты запоминаются, чтобы в будущем не посещать один и тот же объект дважды.
  • …И так далее, пока не будут посещены все достижимые (из корней) ссылки.
  • Все непомеченные объекты удаляются.

"this"

В JavaScript this является «свободным», его значение вычисляется в момент вызова метода и не зависит от того, где этот метод был объявлен, а зависит от того, какой объект вызывает метод (какой объект стоит «перед точкой»). Вызов функции без объекта означает, что this === undefined.

Значение this передаётся правильно, только если функция вызывается напрямую с использованием синтаксиса точки obj.method(). При любой другой операции, например, присваивании obj = obj.method, this теряется и дальнейший вызов происходит уже без this.

let user = { age: 20, f1() { return this.age; }, f2() { return 1; } }; (user.age == 20 ? user.f1 : user.f2)(); // TypeError: Cannot read properties of undefined

Конструкторы, создание объектов через "new"

Конструкторы являются обычными функциями. Но есть два соглашения:

  1. Имя такой функции должно начинаться с большой буквы.
  2. Функция должна вызываться только с помощью оператора new.
  3. const user = new User(...)

При вызове функции-конструктора при помощи оператора new происходит следующее:

  1. Создаётся новый пустой объект, и он присваивается this // this = {}; (неявно).
  2. Выполняется код функции. Обычно он модифицирует this, добавляет туда новые свойства.
  3. Возвращается значение this // return this; (неявно).

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

Если же возвращаемое значение - объект, то вместо this будет возвращён этот объект.

Проверка на вызов в режиме конструктора: new.target

В случае обычного вызова функции new.target === undefined. Если же она была вызвана при помощи new, new.target будет равен самой функции.

function User(name) { if (!new.target) { // в случае, если вы вызвали меня без оператора new return new User(name); // ...я добавлю new за вас } this.name = name; }

Опциональная цепочка

Опциональная цепочка ?. останавливает вычисление и возвращает undefined, если часть перед ?. имеет значение undefined или null.

{}.address.street // TypeError: Cannot read properties of undefined (reading 'street')

{}.address?.street // undefined (без ошибки)

Переменная перед ?. должна быть объявлена, иначе выражение выдаст ошибку.

Другие варианты применения: ?.(), ?.[].

Преобразование объектов в примитивы

Существует "три хинта", то есть 3 варианта преобразования объекта в примитив: "string", "number", "default".

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

  • obj[Symbol.toPrimitive](hint)
  • obj.toString()
  • obj.valueOf()

Порядок вызова:

  1. Вызывает obj[Symbol.toPrimitive](hint) если такой метод существует, и передаёт ему хинт.
  2. Иначе, если хинт равен "string"
    пытается вызвать obj.toString(), а если его нет, то obj.valueOf(), если он существует.
  3. В случае, если хинт равен "number" или "default"
    пытается вызвать obj.valueOf(), а если его нет, то obj.toString(), если он существует.

Symbol.toPrimitive - это универсальный подход:

let user = { name: "John", money: 1000, [Symbol.toPrimitive](hint) { return hint == "string" ? `{name: "${this.name}"}` : this.money; } }; alert(user); // {name: "John"} -> hint: string alert(+user); // 1000 -> hint: number alert(user + 500); // 1500 -> hint: default

На практике довольно часто достаточно реализовать только obj.toString().

Он обработает все случаи преобразований к примитивам.

Числа

Number.isInteger(num) возвращает true, если num - целое число, иначе false.

num.toFixed(n) округляет число до n знаков после запятой и возвращает строковое представление результата.

let num = 1.396; num.toFixed(2) === '1.40';
Шестнадцатеричные, двоичные и восьмеричные числа:

0x - 16-ые, 0b - 2-ые, 0o - 8-ые.

0xff === 255; 0b1111 === 15;

Метод num.toString(base) возвращает строковое представление числа в системе счисления base, где base от 2 до 36.

20..toString(2) === '10100'; // 2 точки для вызова метода (20).toString(2) === '10100'; // либо круглые скобки

Неточные вычисления

0.1 + 0.2 == 0.3; // false

Число хранится в памяти в бинарной форме.

Дроби, такие как 0.1 и 0.2 являются бесконечной дробью в двоичной форме.

В JavaScript нет возможности для хранения точных значений 0.1 или 0.2, используя двоичную систему, так же, как нет возможности хранить одну третью в десятичной системе счисления.

Из 64 бит, отведённых на число, сами цифры числа занимают до 52 бит, остальные 11 бит хранят позицию десятичной точки и один бит – знак. Eсли 52 бит не хватает на цифры, то при записи пропадут младшие разряды.

9999999999999999 === 1e16; // true

Проверка: isFinite и isNaN

Значение NaN уникально тем, что оно не равно ничему, даже самому себе.

NaN == NaN; // false

isNaN(value) преобразует значение в число и проверяет является ли оно NaN.

isFinite(value) преобразует значение в число и возвращает true, если оно не NaN/Infinity/-Infinity.

Number.isNaN(value) возвращает true в случае, если значение принадлежит к типу number и является NaN (без преобразования типа).

Number.isFinite(value) возвращает true в случае, если значение принадлежит к типу number и не является NaN/Infinity/-Infinity (без преобразования типа).

Object.is(a, b) проверяет на строгое равенство, может работать с NaN и нулем.

Object.is(NaN, NaN) === true; Object.is(-0, 0) === false; // технически эти значения разные

parseInt и parseFloat

parseInt(str, base) и parseFloat(str) «читают» число из строки. Если в процессе чтения возникает ошибка, они возвращают полученное до ошибки число. Eсли первый символ - не число, возвращают NaN.

parseInt возвращает целое число, второй параметр определяет систему счисления (от 2 до 36).

parseFloat возвращает число с плавающей точкой.

parseInt('100px') === 100 parseFloat('12.5em') === 12.5 parseFloat('12.3.4') === 12.3 parseInt('2n9c', 36) === 123456

Методы Math.*

Math.floor(num) - округляет в меньшую сторону

Math.ceil(num) - округляет в большую сторону

Math.round(num) - округляет до ближайшего целого

Math.trunc(num) - удаляет дробную часть без округления

Math.abs(num) - возвращает число по модулю

Math.random() - возвращает псевдослучайное число n, 0 <= n < 1

Math.max(...nums) - возвращает наибольшее число

Math.min(...nums) - возвращает наименьшее число

Math.pow(n, power) - возвращает число n, возведённое в степень power

Строки

Спецсимволы
  • \n - перевод строки
  • \t - знак табуляции
  • \', \" - кавычки
  • \\ - обратный слеш ( \ )

JavaScript позволяет вставить символ в строку, указав его шестнадцатеричный Юникод с помощью одной из трех нотаций:

  1. \xXX – можно использовать только для первых 256 символов Юникода
  2. \uXXXX – кодировка одного из первых 65536 символов Юникода
  3. \u{X…XXXXX} – любой символ от 0 до 10FFFF
"\xA9" === "©"; "\u{1F680}" === "🚀";

Основные методы

str.charAt(pos) - возвращает символ на позиции pos, при отсутствии возвращают пустую строку.

str.at(pos) - возвращает символ на позиции pos, при отсутствии возвращает undefined. Если задать отрицательное число, то отсчет ведется от конца строки.

"Hello".at(-1) === "o";

str.toLowerCase() и str.toUpperCase() возвращают строку в заданном регистре.

str.indexOf(substr, pos) ищет подстроку substr в строке str, начиная с позиции pos, и возвращает позицию совпадения, либо -1 при отсутствии совпадений.

str.lastIndexOf(substr, pos) ищет с конца строки (или с pos) до начала.

"some string".indexOf("s", 1) === 5; "some string".lastIndexOf("s") === 5;

str.includes(substr, pos) возвращает true, если в строке str есть подстрока substr, либо false, если нет.

"Midget".includes("id", 2) === false; // поиск начат с позиции 2

str.startsWith(substr) и str.endsWith(substr) проверяют, начинается ли и заканчивается ли строка определённой строкой.

"Widget".startsWith("Wid") === true;

str.slice(start, end) возвращает часть строки от start до (не включая) end.

"stringify".slice(0, 5) === "strin"; "stringify".slice(2) === "ringify"; "stringify".slice(-4, -1) === "gif";

str.substring(start, end) - почти такой же. Можно задавать start больше end, отрицательные значения интерпретируются как 0.

"stringify".substring(6, 2) === "ring"; "stringify".substring(2, -3) === "st";

str.split(regexp|delim, limit) - разбивает строку на массив по заданному разделителю delim. Если delim не указан, возвращает всю строку в первом элементе массива.

Имеет необязательный второй числовой аргумент – ограничение на количество элементов в массиве. Если их больше, чем указано, то остаток массива будет отброшен.

"12345".split('', 2); // ['1', '2']

str.trim() — убирает пробелы в начале и конце строки

str.repeat(n) — повторяет строку n раз

Стравнение строк

Строки кодируются в UTF-16. У любого символа есть соответствующий код.

str.codePointAt(pos) возвращает код для символа, находящегося на позиции pos.

"z".codePointAt(0) === 122; "a".codePointAt() === 97; // pos === 0 при отсутсвии аргумента "Z".codePointAt(0) === 90;

String.fromCodePoint(code) создаёт символ по его коду code.

String.fromCodePoint(90) === "Z"

str.codePointAt(pos) и String.fromCodePoint(code) правильно обрабатывают суррогатные пары.

'𝒳'.codePointAt(0) === 119987; "\u{1d4b3}" === '𝒳'; // (119987).toString(16) === "1d4b3" String.fromCodePoint(119987) === '𝒳';

str.localeCompare(str2) показывает, какая строка больше в соответствии с правилами языка.

Возвращает 1 если str больше str2, -1 если str меньше, 0 если строки равны.

Массивы

let arr = new Array(); // эквивалентно let arr = []; new Array("Яблоко", "Груша", "и тд"); // можно сразу добавить элементы

Однако, если new Array вызывается с одним аргументом, который представляет собой число, он создаёт массив без элементов, но с заданной длиной.

Методы

arr.at(pos) - возвращает элемент на позиции pos, pos может быть отрицательным.

[1, 2, 3].at(-1) === 3

arr.pop() - удаляет последний элемент из массива и возвращает его.

arr.push(elem1, elem2...) - добавляет элемент (или несколько элементов) в конец массива и возвращает длину получившегося массива.

arr.shift() - удаляет из массива первый элемент и возвращает его.

arr.unshift(elem1, elem2...) - добавляет элемент (или несколько элементов) в начало массива и возвращает длину получившегося массива.

arr.splice(index[, deleteCount, elem1, ..., elemN]) - удаляет deleteCount элементов начиная с index и заменяет на elemN эл-ты. Возвращает массив удаленных элементов. Может вставлять элементы без удаления если установить deleteCount в 0.

arr.slice(start, end) - возвращает новый массив, в который копирует элементы, начиная с индекса start и до (не включая) end. Без аргументов создаёт копию массива arr.

arr.concat(arg1, arg2...) - создаёт новый массив, в который распаковывает данные из других массивов и добавляет дополнительные значения. Другие объекты добавляются как есть.

Если псевдомассив имеет специальное свойство Symbol.isConcatSpreadable со значением truthy, он обрабатывается как массив.

[1, 2].concat([[3, 4], 5], 6, {7: 8}) // вернет [1, 2, [3, 4], 5, 6, {7: 8}]

arr.forEach(function(item, index, array) {...}) - позволяет запускать функцию для каждого элемента массива. Результат функции, если она что-то возвращает, игнорируется.

arr.indexOf(item, from) - ищет item в массиве, начиная с позиции from и возвращает индекс найденного элемента, либо -1 если ничего не найдено.

arr.lastIndexOf(item, from) - поиск выполняется начиная с from и заканчивая началом массива.

arr.includes(item, from) - ищет item начиная с индекса from и возвращает true, если поиск успешен, false в противном случае. Метод правильно обрабатывает NaN.

[NaN].includes(NaN) === true

arr.find(function(item, index, array) {...}) - возвращает item, при котором функция вернула truthy, иначе undefined.

arr.findIndex(function(item, index, array) {...}) - возвращает индекс, при котором функция вернула truthy, иначе -1.

arr.filter(function(item, index, array) {...}) - возвращает массив из всех подходящих элементов (для которых функция вернула truthy). Возвращается пустой массив в случае, если таких элементов нет.

arr.map(function(item, index, array) {...}) - вызывает функцию для каждого элемента массива и возвращает массив результатов выполнения этой функции.

["Bilbo", "Gandalf", "Nazgul"].map(item => item.length) // [5, 7, 6]

arr.sort(fn) - возвращает отсортированный массив (возвращенный массив и есть arr).

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

Эта функция принимает два параметра, назовем их a и b (a - элемент справа). Если функция сравнения этих двух элементов возвращает отрицательное число, элементы меняются местами.

[2, 3, 1].sort( (a, b) => a - b ); // [1, 2, 3] ['Österreich', 'Andorra', 'Vietnam'].sort( (a, b) => a.localeCompare(b) ); // правильная сортировка строк

arr.reverse() - меняет порядок элементов в arr на обратный (возвращенный массив и есть arr).

arr.join(glue) - возвращает строку из элементов arr, вставляя glue между ними (запятую если glue не указан).

arr.reduce(function(previousValue, item, index, array) {...}, [initial]) - используется для вычисления единого значения на основе всего массива.

  • метод перебирает массив
  • при вызове функции результат её вызова передается в previousValue
  • первоначально previousValue берется из initial
  • метод возвращает результат конечной функции

Если массив пустой, то возвращается initial, иначе, если не указан initial, вызов завершается ошибкой TypeError: Reduce of empty array with no initial value.

arr.reduceRight(function(previousValue, item, index, array) {...}, [initial]) - работает аналогично, проходя по массиву справа налево.

Array.isArray - используется чтобы отличить простой объект от массива

Array.isArray([]) === true

«thisArg»

arr[method](func, thisArg)

Почти все методы массива, которые вызывают функции, за исключением sort и reduce, принимают необязательный параметр thisArg. Значение параметра thisArg становится this для func.

let army = { minAge: 18, maxAge: 27, canJoin(user) { return user.age >= this.minAge && user.age < this.maxAge; } }; let users = [{age: 16}, {age: 20}, {age: 23}]; users.filter(army.canJoin, army); // [{age: 20}, {age: 23}]

Другие методы

arr.some(fn) и arr.every(fn) проверяют массив. Функция fn вызывается для каждого элемента массива. Если какие-либо/все результаты вызовов являются truthy, то метод возвращает true, иначе false.

arr.fill(value, start, end) – заполняет массив повторяющимися value, начиная с индекса start до end. Меняет только уже существующие элементы (возвращенный массив и есть arr).

arr.copyWithin(target, start, end) – копирует свои элементы, начиная со start и заканчивая end, на позиции, начиная с target (возвращенный массив и есть arr).

arr.flat(depth) - создаёт новый массив из всех подмассивов в нём. Он принимает один параметр — глубину «сглаживания» массива.

[1, [2, [3, [4]], 5]].flat(1); // [1, 2, [3, [4]], 5]; [1, [2, [3, [4]], 5]].flat(Infinity); // [1, 2, 3, 4, 5];

arr.flatMap(fn) - сначала он вызывает mapping-функцию fn для каждого элемента в массиве, а потом «выравнивает» их в один массив.

['Это предложение', 'Это другое предложение'].flatMap(sentence => sentence.split(' ')) // ['Это', 'предложение', 'Это', 'другое', 'предложение']

Перебираемые объекты

Итерируемые объекты – объекты, которые реализуют метод Symbol.iterator.

Псевдомассивы – объекты, у которых есть индексы и свойство length.

Symbol.iterator

Чтобы сделать объект итерируемым (и позволить for..of работать с ним), нужно добавить в объект метод с именем Symbol.iterator.

  1. Когда цикл for..of запускается, он вызывает этот метод один раз (или выдаёт ошибку, если метод не найден). Этот метод должен вернуть итератор – объект с методом next.
  2. Дальше for..of работает только с этим возвращённым объектом.
  3. Когда for..of хочет получить следующее значение, он вызывает метод next() этого объекта.
  4. Результат вызова next() должен иметь вид {done: Boolean, value: any}, где done: true означает, что итерация закончена, в противном случае value содержит очередное значение.
let range = { from: 1, to: 5 }; range[Symbol.iterator] = function() { return { current: this.from, last: this.to, next() { if (this.current <= this.last) { return { done: false, value: this.current++ }; } else { return { done: true }; } } }; }; for (let num of range) { console.log(num); // 1, затем 2, 3, 4, 5 }

Array.from

Array.from(obj[, mapFn, thisArg]) принимает итерируемый объект или псевдомассив и делает из него «настоящий» Array. Позволяет указать необязательную «трансформирующую» функцию.

Array.from(range, num => num * num); // [1, 4, 9, 16, 25] (range взят из примера выше)

Object.keys, values, entries

Object.keys(obj) – возвращает массив ключей.

Object.values(obj) – возвращает массив значений.

Object.entries(obj) – возвращает массив пар [ключ, значение].

Object.fromEntries(arr) - обратный метод. Преобразует массив пар [ключ, значение] в объект.

Object.fromEntries( Object.entries( {a: 1, b: 2} ).map(([key, value]) => [key, value * 2]) ); // {a: 2, b: 4}

Map, Set, WeakMap, WeakSet

Map

Map – это коллекция пар ключ/значение, как и Object. Позволяет использовать ключи любого типа.

При создании Map можно указать массив (или другой итерируемый объект) с парами ключ-значение.

new Map([ ['1', 'str1'], [1, 'num1'], [true, 'bool1'] ]); // создает коллекцию из трех пар [ключ, значение]

map.set(key, value) – записывает по ключу key значение value. Возвращает получившийся map.

map.get(key) – возвращает значение по ключу или undefined, если ключ key отсутствует.

map.has(key) – возвращает true, если ключ key присутствует в коллекции, иначе false.

map.delete(key) – удаляет элемент по ключу key. Возвращает true, если элемент был удален, иначе false.

map.clear() – очищает коллекцию от всех элементов.

map.size – возвращает текущее количество элементов.

map.set("1", "str1").set(1, "num1").set(true, "bool1"); // цепочка вызовов
Перебор Map

map.keys() – возвращает итерируемый объект по ключам.

map.values() – возвращает итерируемый объект по значениям.

map.entries() – возвращает итерируемый объект по парам вида [ключ, значение], используется по умолчанию в for..of.

map.forEach((value, key, map) => {...} ) - схож с одноименным методом для массивов for (let key of map.keys()) { console.log(key); // выводит все ключи map } for (let [key, value] of map) { ... } // получить и ключи, и значения
Object.fromEntries: Object из Map

Object.fromEntries(map.entries()); // возвращает объект

Object.fromEntries(map); // так тоже работает

Set

Объект Set – особый вид коллекции: «множество» уникальных значений без ключей, где каждое значение может появляться только один раз.

new Set(iterable) – создаёт Set, и если в качестве аргумента был предоставлен итерируемый объект (обычно это массив), то копирует его значения в новый Set.

set.add(value) – добавляет значение (если оно уже есть, то ничего не делает), возвращает получившийся set.

set.delete(value) – удаляет значение, возвращает true, если value было в множестве на момент вызова, иначе false.

set.has(value) – возвращает true, если значение присутствует в множестве, иначе false.

set.clear() – удаляет все имеющиеся значения.

set.size – возвращает количество элементов в множестве.

Перебор Set

set.keys(), set.values() – возвращают перебираемый объект для значений. Оба метода работают одинаково.

set.entries() – возвращает перебираемый объект с парами [значение, значение]

for (let value of set) {...} // используя for..of set.forEach((value, valueAgain, set) => { ... } ); // или forEach

WeakMap

WeakMap и WeakSet используются как вспомогательные структуры данных в дополнение к «основному» месту хранения объекта. Не являются перебираемыми.

Ключи в WeakMap должны быть объектами.

При использовании объекта в качестве ключа если больше нет ссылок на этот объект, он будет удалён из памяти (и из объекта WeakMap) автоматически вместе с соответствующим ему значением.

weakMap.get(key)

weakMap.set(key, value)

weakMap.delete(key)

weakMap.has(key)

let messages = [ {text: "Hello", from: "John"}, {text: "How goes?", from: "John"}, {text: "See you soon", from: "Alice"} ]; let readMap = new WeakMap(); readMap.set(messages[0], new Date(2017, 1, 1));

WeakSet

В WeakSet можно добавлять только объекты.

Объект присутствует в множестве только до тех пор, пока доступен где-то ещё.

weakSet.add(value)

weakSet.has(value)

weakSet.delete(value)

Деструктурирующее присваивание

Это специальный синтаксис, который позволяет «распаковать» перебираемые объекты в несколько переменных.

Деструктуризация массива

let [first, second] = [1, 2]; // first === 1, second === 2

Ненужные элементы массива могут быть отброшены через запятую.

let [first, , third] = [1, 2, 3, 4];

Можно использовать что угодно «присваивающее» с левой стороны.

let user = {}; // преобразуется в {name: 'John', surname: 'Smith'} [user.name, user.surname] = "John Smith".split(' ');

Остаточные параметры.

let [name1, name2, ...rest] = ["Julius", "Caesar", "Consul", "Me"]; console.log(rest); // ['Consul', 'Me']

Если в массиве меньше значений, чем в присваивании, они считаются неопределёнными.

let [firstName, surname] = []; // firstName === undefined

Значения по умолчанию.

let [name = "Guest", info = prompt('')] = []; // info - результат prompt

Деструктуризация объекта

Деструктурирующее присваивание также работает с объектами.

let {height = 100, width} = { width: 200 }; // height === 100, width === 200

Можно присвоить свойство объекта переменной с другим названием.

let { title: t } = { title: "Menu" }; // t === "Menu"

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

let {one, ...rest} = { one: 1, two: 2, three: 3 }; console.log(rest); // {two: 2, three: 3}

Вложенная деструктуризация

let options = { size: {width: 100, height: 200} }; let { size: { width: w, height, title = "Menu", }, } = options; console.log(height); // 200 console.log(w); // 100 console.log(title); // "Menu" console.log(size); // ReferenceError: size is not defined console.log(width); // ReferenceError: width is not defined

Умные параметры функций

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

function showMenu(title = "Untitled", width = 200, height = 100, items = []) {...}

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

function showMenu({title = "Untitled", width = 200, height = 100, items = []}) {...} showMenu({ title: "My menu", items: ["Item1", "Item2"] });

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

showMenu({}); // все значения - по умолчанию showMenu(); // TypeError: Cannot read properties of undefined (reading 'title')

Ошибку можно исправить, сделав {} значением по умолчанию для всего объекта параметров.

function showMenu({ title = "Menu", width = 100, height = 200 } = {}) { console.log( `${title} ${width} ${height}` ); } showMenu(); // Menu 100 200

Дата и время

Создание объекта Date

new Date() – создать с текущими датой и временем.

new Date(milliseconds) - таймстамп (кол-во миллисекунд, прошедших с 1 января 1970 года).

new Date(datestring) - строка в формате YYYY-MM-DDTHH:mm:ss.sssZ (обязателен только год).

  • T используется в качестве разделителя
  • Z обозначает часовой пояс в формате +-hh:mm. Если указать просто букву Z, получим UTC+0
new Date('2022-01-26T13:51:50.417-07:00'); // Wed Jan 26 2022 23:51:50 GMT+0300 (Москва, стандартное время)

new Date(year, month, date, hours, minutes, seconds, ms) - аргументы (обязательны первые два).

  • year должен состоять из четырёх цифр
  • month нумеруется от 0 (январь) по 11 (декабрь)
new Date(2021, 5, 6, 8, 49); // Sun Jun 06 2021 08:49:00 GMT+0300
Автоисправление даты

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

new Date(2013, 0, 32); // Fri Feb 01 2013 ...

Получение компонентов даты

Методы возвращают значения в соответствии с местным часовым поясом:

getFullYear() - возвращает год (4 цифры).

getMonth() - возвращает месяц, от 0 до 11.

getDate() - возвращает день месяца, от 1 до 31.

getHours(), getMinutes(), getSeconds(), getMilliseconds() - соответственно названиям.

getDay() - возвращает день недели от 0 (воскресенье) до 6 (суббота).

Их UTC-аналоги:

getUTCFullYear(), getUTCMonth(), getUTCDate(), getUTCHours(), getUTCMinutes(), getUTCSeconds(), getUTCMilliseconds(), getUTCDay()

new Date().getHours(); // 9 new Date().getUTCHours(); // 6

2 особых методы без UTC-варианта:

getTime() - для заданной даты возвращает таймстамп

new Date().getTime(); // 1688275690962

getTimezoneOffset() - возвращает разницу в минутах между местным часовым поясом и UTC.

new Date().getTimezoneOffset(); // -180

Установка компонентов даты

Методы изменяют дату в соответствии с местным часовым поясом и возвращают таймстамп.

setFullYear(year, [month], [date])

setMonth(month, [date])

setDate(date)

setHours(hour, [min], [sec], [ms])

setMinutes(min, [sec], [ms])

setSeconds(sec, [ms])

setMilliseconds(ms)

let date = new Date(2021, 5, 6, 8, 49); date.setHours(0); console.log( date ); // Sun Jun 06 2021 00:49:00 GMT+0300

setTime(milliseconds) - устанавливает дату в виде целого кол-во ms, прошедших с 01.01.1970 UTC

Если нужно изменить дату:

let date = new Date(2016, 1, 28); date.setDate(date.getDate() + 2); console.log(date); // Tue Mar 01 2016 ...

Преобразование к числу, разность дат.

Если объект Date преобразовать в число, то получим таймстамп по аналогии с date.getTime().

+new Date(); // 1688280813375

Поэтому даты можно вычитать, получая разность в миллисекундах.

Date.now() - метод для быстрого получения текущего времени в формате таймстампа. Семантически эквивалентен new Date().getTime(), но не создаёт промежуточный объект Date. Так что этот способ работает быстрее и не нагружает сборщик мусора.

Разбор строки с датой

Date.parse(str) считывает дату из строки.

Формат такой же, как в случае с new Date(datestring).

Вызов Date.parse(str) обрабатывает строку в заданном формате и возвращает таймстамп. Если формат неправильный, возвращается NaN.

performance.now()

В браузерах есть метод performance.now(), возвращающий количество миллисекунд с начала загрузки страницы с точностью до микросекунд (корректными являются только три цифры после точки):

performance.now() // 8232.939999899827 (8.232939 секунды)

Формат JSON, метод toJSON

Формат JSON (JavaScript Object Notation):

Ключи объектов заключаются в двойные кавычки. Значения, если это строки, также заключаются в двойные кавычки.

JSON поддерживает простые объекты, массивы, строки, числа, логические значения и null.

JSON.stringify

JSON.stringify(value[, replacer, space]) - метод для преобразования объектов в JSON.

value - объект для преобразования в JSON.

replacer - массив свойств для кодирования или функция соответствия function(key, value).

space - указывает на кол-во пробелов для удобного форматирования.

JSON.stringify(user, null, 2);

JSON является независимой от языка спецификацией для данных, поэтому JSON.stringify пропускает некоторые специфические свойства объектов JavaScript:

  • cвойства-функции (методы)
  • cимвольные ключи и значения
  • свойства со значением undefined

Ограничение: не должно быть циклических ссылок.

let meetup = { title: "Conference", participants: [{name: "John"}, {name: "Alice"}], }

Вместо replacer можно передать массив свойств, которые мы хотим записать в JSON:

JSON.stringify(meetup, ['title', 'participants']); // '{"title":"Conference","participants":[{},{}]}'

Также на месте replacer можно написать функцию, а не массив. Она будет вызываться для каждой пары (key, value) и должна возвращать замененное значение вместо исходного, либо undefined чтобы пропустить значение.

JSON.stringify(meetup, function replacer(key, value) { return (value == 'John') ? undefined : value; }); // '{"title":"Conference","participants":[{},{"name":"Alice"}]}'

Первый вызов – особенный. Ему передаётся специальный «объект-обёртка»: {"": meetup}. Другими словами, первая (key, value) пара имеет пустой ключ, а значением является целевой объект в общем.

JSON.stringify(meetup, function replacer(key, value) { if (value === meetup) console.log(key === '') }); // true

Пользовательский «toJSON»

Если объект имеет метод toJSON для преобразования в JSON, то он вызывается через JSON.stringify:

let room = { number: 23, toJSON() { return this.number; } }; let meetup = { title: "Conference", room, }; JSON.stringify(meetup); // {"title":"Conference","room":23}

JSON.parse

JSON.parse(str, [reviver]) - метод для преобразования JSON обратно в объект.

str - это сам JSON для преобразования в объект.

reviver - необязательная ф-ция, которая будет вызываться для каждой пары (ключ, значение) и преобразовывать значения.

Например, используется чтобы значения объектов date не оставались строками после преобразования:

let obj = {date: new Date()}; let json = JSON.stringify(obj); // "2023-07-02T09:37:28.557Z" JSON.parse(json, function(key, value) { if (key === 'date') return new Date(value); return value; }); // {date: Sun Jul 02 2023 12:40:39 ... }

Продвинутая работа с функциями

Остаточные параметры и spead оператор

Остаточные параметры (...)

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

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

function example(first, second, ...rest) {}

Переменная "arguments"

Все аргументы функции находятся в итерируемом объекте - псевдомассиве arguments под своими порядковыми номерами.

function showName() { for (let arg of arguments) { console.log(arg); } } showName("Юлий", "Цезарь"); // Юлий, затем Цезарь

У стрелочных функций переменной arguments нет.

Оператор расширения (spread)

Оператор расширения тоже использует ..., но делает совершенно противоположное.

Он «расширяет» перебираемый объект в список аргументов.

let arr = [3, 5, 1]; Math.max(...arr); // 5 let merged = [0, ...[3, 5, 1], 2, ...[8, 9, 15]]; merged; // [0, 3, 5, 1, 2, 8, 9, 15]

Оператор расширения работает с любым перебираемым объектом.

[..."Привет"]; // ['П', 'р', 'и', 'в', 'е', 'т']

Область видимости переменных, замыкание

Лексическое Окружение

В JavaScript у каждой выполняемой функции, блока кода и скрипта есть связанный с ними внутренний объект LexicalEnvironment, имеющий два свойства:

  1. Environment Record – объект, в котором как свойства хранятся все локальные переменные и значение this.
  2. Ссылка на внешнее лексическое окружение (то, которое соответствует коду снаружи от текущих фигурных скобок).

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

Переменная – это свойство специального внутреннего объекта, связанного с текущим выполняющимся блоком/функцией/скриптом. Работа с переменными – это на самом деле работа со свойствами этого объекта.

Когда код хочет получить доступ к переменной – сначала происходит поиск во внутреннем лексическом окружении, затем во внешнем, затем в следующем и так далее, до глобального.

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

Вложенные функции

Функция называется «вложенной», когда она создаётся внутри другой функции.

function MakeCounter() { let count = 0; this.up = function() { return ++count; }; this.down = function() { return --count; }; } let counter = new MakeCounter(); counter.up(); // 1 counter.up(); // 2 counter.down(); // 1

Обе вложенные функции были созданы с одним и тем же внешним лексическим окружением, так что они имеют доступ к одной и той же переменной count.

Окружение в деталях

Функции «при рождении» получают скрытое свойство [[Environment]], которое ссылается на лексическое окружение места, где они были созданы.

Замыкание – это функция, которая запоминает свои внешние переменные и может получить к ним доступ.

В JavaScript все функции изначально являются замыканиями, так как запоминают, где были созданы с помощью свойства [[Environment]] (кроме функций, созданных с помощью new Function, которые всегда ссылаются на глобальное лексическое окружение).

Блоки кода и циклы

У каждой итерации цикла своё собственное лексическое окружение.

В браузере все скрипты (кроме type="module") разделяют одну общую глобальную область. Если создать глобальную переменную в одном скрипте, она станет доступна и в других, что может стать источником конфликтов при использовании одной переменной.

Если необходимо этого избежать, можно использовать блок кода {...} для изоляции всего скрипта или какой-то его части в «локальной области видимости».

IIFE

В прошлом в JavaScript не было лексического окружения на уровне блоков кода. Придумали «immediately-invoked function expressions» (аббревиатура IIFE), что означает функцию, запускаемую сразу после объявления.

(function() { var message = "Hello"; console.log(message); // Hello })(); console.log(message); // ReferenceError: message is not defined

Существуют разные пути создания IIFE:

  • (function() {...})();
  • (function() {...}());
  • !function() {...}();
  • +function() {...}();
  • void function() {...}();

"var" и глобальный объект

Устаревшее ключевое слово "var"

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

Если блок кода находится внутри функции, то var становится локальной переменной в этой функции.

+function sayHi() { if (true) { var phrase = "Привет"; } console.log(phrase); // "Привет" }(); console.log(phrase); // ReferenceError: phrase is not defined

Объявления var обрабатываются в начале запуска функции.

+function() { phrase = "Привет"; if (false) { var phrase; } console.log(phrase); // Привет }();

Это поведение называется «hoisting» (всплытие, поднятие), потому что все объявления переменных var «всплывают» в самый верх функции.

Условие if (false) никогда не выполнится. Но это не препятствует созданию переменной var phrase, которая находится внутри него, поскольку объявления var «всплывают» в начало функции. В момент присвоения значения переменная уже существует.

Отличия var от let/const:

  1. Переменные var не имеют блочной области видимости, они ограничены телом функции, либо скриптом, если переменная глобальная.
  2. Объявления (инициализация) переменных var производится в начале исполнения функции (или скрипта для глобальных переменных).
  3. Глобальные переменные, объявленные с помощью var становятся свойствами глобального объекта.

Глобальный объект

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

В браузере он называется window, в Node.js — global, в другой среде исполнения может называться иначе.

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

Ко всем свойствам глобального объекта можно обращаться напрямую:

window.alert("Привет"); // то же, что и просто alert("Привет") window.innerHeight // высота окна браузера

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

var name = 'Sergey'; console.log( window.name ); // Sergey

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

window.currentUser = { name: "John" }; console.log(currentUser.name); // John (либо window.currenUser.name)

Объект функции, NFE

В JavaScript функции – это объекты. Их можно передавать по ссылке, добавлять/удалять свойства и тд.

Объект функции содержит несколько полезных свойств.

Свойство «name»

Имя функции доступно как свойство name.

function sayHi() {} console.log( sayHi.name ); // "sayHi"

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

let arr = [function() {}]; console.log(arr[0].name === ''); // true

Свойство «length»

Встроенное свойство length содержит количество параметров функции в её объявлении.

function f1(a) {} function many(a, b, ...more) {} console.log(f1.length); // 1 console.log(many.length); // 2

Named Function Expression

Named Function Expression или NFE – это термин для Function Expression, у которого есть имя.

let sayHi = function func() {};

Функция всё ещё задана как Function Expression. Добавление func после function не превращает объявление в Function Declaration, потому что оно все ещё является частью выражения присваивания. Функция все еще доступна как sayHi().

Есть две особенности имени func, ради которого оно даётся:

  1. Позволяет функции ссылаться на себя же.
  2. Не доступно за пределами функции.
let sayHi = function func(who) { if (who) { console.log(`Hello, ${who}`); } else { func("Guest"); } }; sayHi(); // Hello, Guest func(); // ReferenceError: func is not defined

Такая функция не сломается при присвоении другой переменной. Имя NFE может быть использовано для рекурсивных вызовов и т.п.

"new Function" и eval

Синтаксис "new Function"

Существует ещё один вариант объявлять функции.

let func = new Function([arg1, arg2, ...argN], functionBody); let sum = new Function('a', 'b', 'return a + b'); sum(1, 2); // 3

new Function позволяет превратить любую строку в функцию. Например, можно получить новую функцию с сервера и затем выполнить её.

Замыкание

Когда функция создаётся с использованием new Function, в её [[Environment]] записывается ссылка не на внешнее лексическое окружение, в котором она была создана, а на глобальное.

function getFunc() { let value = "test"; let func = new Function('alert(value)'); return func; } getFunc()(); // ReferenceError: value is not defined

Без этой особенности new Function были бы проблемы с минификаторами, так как они дают укороченные имена переменным. Например, объявление в функции переменной let userName минификатор изменяет на let a (или другую букву, если она не занята) и изменяет её везде. Обычно так делать безопасно, потому что переменная является локальной, и никто снаружи не имеет к ней доступ.

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

  • new Function('a', 'b', 'return a + b'); // стандартный синтаксис
  • new Function('a,b', 'return a + b'); // через запятую в одной строке
  • new Function('a , b', 'return a + b'); // через запятую с пробелами в одной строке

eval

Встроенная функция eval выполняет строку кода и возвращает результат последней инструкции.

eval('let i = 0; ++i'); // 1

Код в eval выполняется в текущем лексическом окружении, поэтому ему доступны внешние переменные. Значения внешних переменных можно изменять:

+function f() { let a = 2; eval('a = 10'); console.log(a); // 10 }();

В строгом режиме у eval имеется своё лексическое окружение. Поэтому функции и переменные, объявленные внутри eval, нельзя увидеть снаружи:

eval("let x = 5; function f() {}"); console.log(f); // ReferenceError: f is not defined

На данный момент нет причин использовать eval. Код в eval способен получать доступ к внешним переменным, и это может иметь побочные эффекты. Минификаторы не трогают имена переменных, которые могут быть доступны из eval, что ухудшает степень сжатия кода.

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

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

let x = 1; { let x = 5; window.eval('console.log(x)'); // 1 new Function('console.log(x)')(); // так тоже работает }

Если коду внутри eval нужны локальные переменные, стоит заменить eval на new Function и передать необходимые данные как аргументы:

let f = new Function('a', 'console.log(a)'); f(5); // 5

Планирование: setTimeout и setInterval

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

setTimeout - позволяет вызвать функцию один раз через определённый интервал времени.

setInterval - позволяет вызывать функцию регулярно, повторяя вызов через определённый интервал времени.

Эти методы не являются частью спецификации, но большинство сред выполнения JavaScript-кода имеют внутренний планировщик и предоставляют доступ к этим методам. В частности, они поддерживаются во всех браузерах и Node.js.

setTimeout

let timerId = setTimeout(func|code, [delay], [arg1], [arg2], ...)

func|code - функция для выполнения. По историческим причинам можно передать и строку кода.

delay - задержка перед запуском в миллисекундах. Значение по умолчанию – 0.

arg1, arg2, … - аргументы, передаваемые в функцию.

Данный код вызывает sayHi спустя одну секунду:

function sayHi(phrase, who) { console.log( phrase + ', ' + who ); } setTimeout(sayHi, 1000, "Привет", "Джон"); // Привет, Джон
Отмена через clearTimeout

Вызов setTimeout возвращает «идентификатор таймера» timerId, который можно использовать для отмены дальнейшего выполнения.

let timerId = setTimeout(...); clearTimeout(timerId);

setInterval

let timerId = setInterval(func|code, [delay], [arg1], [arg2], ...)

Функция запускается периодически через указанный интервал времени.

Чтобы остановить дальнейшее выполнение функции, необходимо вызвать clearInterval(timerId).

let timerId = setInterval(() => console.log('tick'), 2000);// повторить с интервалом 2 секунды setTimeout(() => { clearInterval(timerId); console.log('stop'); }, 5000); // остановить вывод через 5 секунд

Рекурсивный setTimeout

Рекурсивный setTimeout планирует следующий вызов после окончания текущего.

let timerId = setTimeout(function tick() { alert('tick'); timerId = setTimeout(tick, 2000); }, 2000);

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

Ограничение браузера для setInterval и вложенных setTimeout: после пяти и более вложенных вызовов интервал должен составлять минимум 4 миллисекунды.

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

setTimeout с нулевой задержкой

setTimeout(func, 0) или просто setTimeout(func) планирует вызов func настолько быстро, насколько это возможно.

Но планировщик будет вызывать функцию только после завершения выполнения текущего кода.

setTimeout('console.log(a)'); // 1000 let a = 0; for (let i = 0; i < 1000; i++) { a++; }

Декораторы и переадресация вызова, call/apply

Прозрачное кеширование

Допустим, есть функция slow(x), выполняющая ресурсоемкие вычисления.

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

Вместо того, чтобы ее усложнять, заключим ее в функцию-обертку, которая добавит кеширование:

function slow(x) { console.log(`Called with ${x}`); return x; } function cachingDecorator(func) { let cache = new Map(); return function(x) { if (cache.has(x)) { // если кеш содержит такой x, return cache.get(x); // читаем из него результат } let result = func(x); // иначе, вызываем функцию cache.set(x, result); // и кешируем результат return result; }; } slow = cachingDecorator(slow); console.log( slow(1) ); // Called with 1, затем 1 console.log( slow(1) ); // 1

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

Применение «func.call» для передачи контекста

func.call(context, arg1, arg2, ...) - позволяет вызывать функцию, явно устанавливая this.

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

В приведённом ниже коде worker.slow() перестаёт работать после применения декоратора:

let worker = { someMethod() { return 1; }, slow(x) { console.log("Called with " + x); return x * this.someMethod(); } }; console.log(worker.slow(5)); // оригинальный метод работает worker.slow = cachingDecorator(worker.slow); console.log(worker.slow(5)); // TypeError: Cannot read property 'someMethod' of undefined

Причина в том, что декоратор вызывает оригинальную функцию как func(x), и она в данном случае получает this = undefined. Т.е. декоратор передаёт вызов оригинальному методу, но без контекста.

Используя func.call исправим строку вызова оригинальной функции:

let result = func.call(this, x); // теперь this передаётся правильно

При выполнении worker.slow(5) обёртка получает 5 в качестве аргумента и this=worker (так как это объект перед точкой).

Переходим к нескольким аргументам с «func.apply»

Синтаксис: func.apply(context, args)

Разница в синтаксисе между call и apply состоит в том, что call ожидает список аргументов, в то время как apply принимает псевдомассив.

Чтобы кешировать метод с несколькими аргументами worker.sum, можно соединить два значения в одно. В данном случае используем строку "a,b" как ключ к Map.

Также понадобится заменить func.call(this, x) на func.call(this, ...arguments).

let worker = { sum(a, b) { console.log(`Called with ${a},${b}`); return a + b; } }; function cachingDecorator(func, hash) { let cache = new Map(); return function() { let key = hash(arguments); if (cache.has(key)) { return cache.get(key); } let result = func.call(this, ...arguments); cache.set(key, result); return result; }; } function hash(args) { return args[0] + ',' + args[1]; } worker.sum = cachingDecorator(worker.sum, hash); console.log( worker.sum(3, 5) ); // Called with 3,5, затем 8 console.log( worker.sum(3, 5) ); // 8

Вместо func.call(this, ...arguments) можно было написать func.apply(this, arguments).

Заимствование метода

function hash() { return arguments.join(); // TypeError: arguments.join is not a function } function hash() { return [].join.call(arguments); // работает }

Мы берём (заимствуем) метод join из обычного массива [].join. И используем [].join.call, чтобы выполнить его в контексте arguments. Алгоритм встроенного метода arr.join(glue) допускает любой псевдомассив this.

Привязка контекста к функции

Синтаксис:

let boundFunc = func.bind(context, [arg1], [arg2], ...); let user = { name: "Вася" }; function f() { console.log(this.name); } let func = f.bind(user); func(); // Вася

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

for (let key in user) { if (typeof user[key] == 'function') { user[key] = user[key].bind(user); } }

Некоторые JS-библиотеки предоставляют встроенные функции для удобной массовой привязки контекста, например _.bindAll(obj) в lodash.

Частичное применение

Частичное применение - это создание новой функции с фиксированием одного или нескольких из существующих параметров.

let mul = (a, b) => a * b; let double = mul.bind(null, 2); // фиксируем контекст и первый аргумент console.log(double(3)); // 6

Частичное применение без контекста

Экзотический объект bound function, возвращаемый при первом вызове f.bind(...), запоминает контекст (и аргументы, если они были переданы) только во время создания.

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

function partial(func, ...boundArgs) { return function(...args) { return func.call(this, ...boundArgs, ...args); } } let user = { firstName: "John", say(time, phrase) { console.log(`[${time}] ${this.firstName}: ${phrase}!`); } }; user.sayNow = partial(user.say, new Date().getHours() + ':' + new Date().getMinutes()); // частично применённый метод с фиксированным временем user.sayNow("Hello"); // [10:00] John: Hello!

Каррирование

Каррирование – это трансформация функций таким образом, чтобы они принимали аргументы не как f(a, b, c), а как f(a)(b)(c).

Для каррирования необходима функция с фиксированным количеством аргументов. Функцию, которая использует остаточные параметры, типа f(...args), каррировать не получится.

Создадим вспомогательную функцию curry(f), которая выполняет каррирование функции f с двумя аргументами. Другими словами, curry(f) для функции f(a, b) трансформирует её в f(a)(b).

function curry(f) { return function(b) { return function(a) { return f(a, b); }; }; } function sum(a, b) { return a + b; } let carriedSum = curry(sum); console.log( carriedSum(1)(2) ); // 3

Продвинутая реализация каррирования

Реализация каррирования для функций с множеством аргументов:

function curry(func) { return function wrapper(...args) { if (args.length < func.length) { return function(...args2) { return wrapper.call(this, ...args, ...args2); } } return func.apply(this, args); } }

Применение:

function sum(a, b, c) { return a + b + c; } let curriedSum = curry(sum); console.log( curriedSum(1, 2, 3) ); // можно вызывать нормально console.log( curriedSum(1)(2,3) ); // каррирование первого аргумента console.log( curriedSum(1)(2)(3) ); // каррирование всех аргументов

Каррирование на практике

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

function log(date, importance, message) { let formatDate = (date.getHours() + ':' + date.getMinutes()).replace(/\b\d\b/g, '0$&'); console.log(`${formatDate} ${importance} ${message}`); } log = curry(log); let logNow = log(new Date()); logNow("DEBUG", "some debug"); // 18:05 DEBUG some debug

Можно пойти дальше и сделать удобную функцию для отладочных логов с текущим временем:

let debugNow = logNow("DEBUG");

Стрелочные функции

У стрелочных функций нет this.

Отсутствие this ведёт к другому ограничению: стрелочные функции не могут быть использованы как конструкторы и вызваны с new.

let User = () => { return { name: "John" }; // даже если не использовать this } let user = new User(); // TypeError: User is not a constructor

Стрелочные функции не имеют переменной arguments.

У них также нет super.

Регулярные выражения

Шаблоны, флаги, якоря

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

  1. let regexp = new RegExp("шаблон", "флаги");
  2. let regexp = /шаблон/флаги;

Основная разница между этими двумя способами создания в том, что слеши /.../ не допускают вставок переменных (наподобие возможных в строках через ${...}).

Якоря

Символы ^ (начало строки) и $ (конец строки) - аналоги методов startsWith и endsWith.

Оба якоря вместе ^...$ используются для проверки на полное совпадение.

let regexp = /^\d\d:\d\d$/;
Граница слова: \b

Границей слова считается:

  1. Начало текста, если его первый символ \w.
  2. Позиция внутри текста, если слева находится \w, а справа – не \w, или наоборот.
  3. Конец текста, если его последний символ \w.
"13:4:1".replace(/\b\d\b/g, "0$&") // 13:04:01

Флаги

i - поиск не зависит от регистра символов.

g - «global», ищет все совпадения.

m - многострочный режим. Влияет только на поведение якорей ^ и $ - они означают не только начало/конец текста, но и начало/конец каждой строки в тексте.

s - «dotall», точка . может соответствовать символу перевода строки \n.

u - поддержка Юникода, корректная обработка суррогатных пар.

y - режим поиска на конкретной позиции в тексте.

Методы

Поиск: str.match(regexp)
  1. С флагом g возвращает массив всех совпадений.
  2. Без g возвращает первое совпадение в виде массива.
  3. Если совпадений нет, возвращается null.
Поиск: str.search(regexp)

str.search(regexp) возвращает позицию первого совпадения с regexp в строке str или -1, если совпадения нет.

Проверка: regexp.test(str)

regexp.test(str) возвращает true, есть ли есть хоть одно совпадение, иначе false.

Замена: str.replace(regexp, replacement)

Совпадения с regexp заменяются на replacement. В строке замены replacement можно использовать спецсимволы для вставки фрагментов совпадения:

  • $& - вставляет всё найденное совпадение
  • $` - вставляет часть строки до совпадения
  • $' - вставляет часть строки после совпадения
  • $n - если n это 1-2 значное число, вставляет содержимое n-й скобочной группы регулярного выражения
  • $<name> - вставляет содержимое скобочной группы с именем name
  • $$ - вставляет символ $

str.replace(str|regexp, str|func) - полный синтаксис.

Когда первый аргумент replace является строкой, он заменяет только первое совпадение.

Для ситуаций, которые требуют «умных» замен, вторым аргументом может быть функция, которая будет вызываться для каждого совпадения с аргументами func(match, p1, p2, ..., pn, offset, input, groups):

  • match – найденное совпадение
  • p1, p2, ..., pn – содержимое скобочных групп
  • offset – позиция, на которой найдено совпадение
  • input – исходная строка
  • groups – объект с содержимым именованных скобок
"html and css".replace(/html|css/gi, str => str.toUpperCase()); // HTML and CSS

Символьные классы

Наиболее используемые:

  • \d - «digit», символ от 0 до 9
  • \s - «space», пробел, \t, \n и другие (\v, \f и \r)
  • \w - «word», a-zA-Z0-9_ (символ латинского алфавита, цифра или подчёркивание)

Для каждого символьного класса существует «обратный класс», обозначаемый той же буквой, но в верхнем регистре:

  • \D - не цифра
  • \S - не пробельный символ
  • \W - любой символ, кроме \w (например, символы русского алфавита)

Точка . – это специальный символьный класс, который соответствует «любому символу, кроме новой строки», но если регулярное выражение имеет флаг s, то точка . соответствует буквально любому символу.

Юникод: флаг "u" и класс \p{...}

Флаг u включает поддержку Юникода в регулярных выражениях, то есть:

  1. Символы из 4 байт воспринимаются как единое целое, а не как два символа по 2 байта
  2. Работает поиск по Юникодным свойствам \p{…}
Юникодные свойства \p{…}

Каждому символу в кодировке Юникод соответствует множество свойств. В регулярном выражении можно искать символ с заданным свойством, указав его в \p{…}.

Некоторые категории символов:

  • L - буквы
  • N - числа
    • Hex_Digit - шестнадцатеричная цифра
  • P - знаки пунктуации
  • S - символы
    • Currency_Symbol - знаки валюты

Свойство Script (или, сокращенно, sc) - система написания. Может иметь значения Cyrillic, Greek, Arabic и другие.

\p{sc=Han} - поиск китайских иероглифов \P{sc=Cyrillic} - поиск символов, отличных от кириллических

Экранирование, специальные символы

Специальные символы, которые имеют особое значение в регулярном выражении: [ ] \ ^ $ . | ? * + ( )

Экранирование символов

Чтобы использовать специальный символ как обычный, необходимо добавить перед ним обратную косую черту \.

Символ косой черты /, так называемый «слэш», не является специальным символом, но он используется для открытия и закрытия регулярного выражения, поэтому его тоже нужно экранировать.

new RegExp

Строковые кавычки «съедают» символы обратной косой черты.

new RegExp("\d\.\d"); // d.d

Чтобы исправить это, необходимо удвоить обратную косую черту.

new RegExp("\\d\\.\\d"); // \d\.\d (теперь правильно)

Наборы и диапазоны [...]

[eao] - набор, ищет любой символ из заданных.

[a-z] - соответствует диапазону символов от a до z.

[^aeyo] - соответствуют любому символу за исключением заданных.

Символьные классы - сокращение для наборов символов:

  • \d – то же самое, что и [0-9]
  • \w – то же самое, что и [a-zA-Z0-9_]
  • \D - то же самое, что и [^0-9]

Внутри [...] экранировать необходимо только закрывающую квадратную скобку ], тире - не надо экранировать в начале или в конце (где оно не задаёт диапазон), символ каретки ^ нужно экранировать только в начале (где он означает исключение).

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

Квантификаторы +, *, ? и {n}

Квантификаторы указывают количество повторений символа.

Самый простой квантификатор — это число в фигурных скобках: {n}.

{5} - точное количество {3,5} - диапазон, от 3 до 5 {3,} - диапазон, 3 и более (верхнюю границу можно не указывать)

Для самых востребованных квантификаторов есть сокращённые формы записи:

  • + эквивалентен {1,}, то есть один или более
  • ? эквивалентен {0,1}, то есть ноль или один
  • * эквивалентен {0,}, то есть ноль или более
/<\/?[a-z][a-z0-9]*>/i // регулярное выражение для «открывающего или закрывающего HTML-тега без атрибутов»

Жадные и ленивые квантификаторы

Жадный поиск

В жадном режиме (по умолчанию) квантификатор повторяется столько раз, сколько это возможно.

'a "witch" and her "broom" is one'.match(/".+"/g) // "witch" and her "broom"

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

Ленивый режим

«Ленивый» режим противоположен «жадному». Он означает: «повторять квантификатор наименьшее количество раз».

Мы можем включить его, вставив знак вопроса ? после квантификатора, то есть будет *? или +? или даже ?? для ?.

'a "witch" and her "broom" is one'.match(/".+?"/g) // "witch", "broom" let regexp = /"[^"]+"/g; // альтернативный подход с тем же результатом

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

Например, если нужно найти ссылки вида <a href="..." class="doc">, с произвольным href:

let str = '...<a href="link1" class="wrong">... <p style="" class="doc">...'; let regexp = /<a href=".*?" class="doc">/g; // <a href="link1" class="wrong">... <p style="" class="doc"> let regexp = /<a href="[^"]*" class="doc">/g; // null (верный результат)

Скобочные группы

Часть шаблона можно заключить в скобки (...), или «скобочную группу»:

  1. Это позволяет поместить часть совпадения в отдельное свойство.
  2. Если установить квантификатор после скобок, то он будет применяться ко всему содержимому скобки, а не к одному символу.

Например, регулярное выражение для поиска доменов (mail.com, my-site.users.mail.com):

let regexp = /([\w-]+\.)+\w+/g;

Пример можно расширить, создав регулярное выражение для поиска email (формат: имя@домен):

let regexp = /[-.\w]+@([\w-]+\.)+[\w-]+/g;

Содержимое скобок в match

Метод str.match(regexp) (для regexp без флага g) ищет первое совпадение и возвращает его в виде массива: на позиции 0 будет всё совпадение целиком, на позиции 1 – содержимое первой скобочной группы, на позиции 2 – второй и тд.

Скобки могут быть и вложенными (их номера идут слева направо, по открывающей скобке).

Если скобочная группа необязательна (стоит квантификатор (...)?), соответствующий элемент массива существует и равен undefined.

let match = 'ab'.match(/a(z)?(b)/); [...match]; // ['ab', undefined, 'b']

Поиск всех совпадений с группами: matchAll

При поиске с флагом g метод match не вернет скобочные группы. Необходимо использовать matchAll:

  1. Он возвращает не массив, а перебираемый объект.
  2. Возвращает каждое совпадение в виде массива со скобочными группами.
  3. Если совпадений нет, он возвращает не null, а пустой перебираемый объект.
let results = '<h1> <h2>'.matchAll(/<(.*?)>/g);

Для оптимизации при вызове matchAll движок JavaScript возвращает перебираемый объект, в котором ещё нет результатов:

alert(results); // [object RegExp String Iterator]

Получить результаты можно так:

  1. results = Array.from(results); // превращаем в массив
  2. for(let result of results); // перебираем в цикле
  3. let [tag1, tag2] = results; // или используем деструктуризацию

Именованные группы

Для удобства скобкам можно присваивать имена добавлением ?<name> после открытия скобки.

Именованные группы располагаются в свойстве groups результата match.

let dateRegexp = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/; "2019-04-30".match(dateRegexp).groups; // {year: '2019', month: '04', day: '30'}

Скобочные группы при замене

Метод str.replace(regexp, replacement) позволяет использовать в строке замены содержимое скобок при помощи обозначений вида $n, где n – номер скобочной группы.

Для именованных скобок можно также использовать ссылку как $<имя>.

let regexp = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/g; "2019-10-30".replace(regexp, '$3.$<month>.$<year>'); // 30.10.2019

Исключение из запоминания через :?

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

let regexp = /(?:go)+ (\w+)/i; "Gogogo John!".match(regexp).length; // 2 (Gogogo John, John)

Также мы не можем ссылаться на такие скобки в строке замены.

Обратные ссылки в шаблоне: \N и \k<имя>

Необходимо найти строки в кавычках: либо одинарных '...', либо двойных "..." – оба варианта должны подходить.

let str = `He said: "She's the one!".`;

По номеру:

let regexp = /(['"])(.*?)\1/g; str.match(regexp); // "She's the one!"

Движок регулярных выражений находит первую кавычку из шаблона (['"]) и запоминает её содержимое. Далее в шаблоне \1 означает «найти то же самое, что в первой скобочной группе».

То же самое по имени:

let regexp = /(?<quote>['"])(.*?)\k<quote>/g;

Альтернация (или) |

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

Чтобы применить альтернацию только к части шаблона, нужно заключить её в скобочную группу.

Пример - поиск времени в правильном формате:

let regexp = /([01]\d|2[0-3]):[0-5]\d/g; "10:10 23:59 25:99 1:2".match(regexp); // 10:10,23:59

Опережающие и ретроспективные проверки

Синтаксис опережающей проверки: X(?=Y)

Он означает: найди X при условии, что за ним следует Y, где X и Y - любой шаблон.

let regexp = /\d+(?=€)/; "1 индейка стоит 30€".match(regexp); // 30

Возможны и более сложные проверки, например X(?=Y)(?=Z) (искать X при условии, что за ним идёт и Y и Z).

Синтаксис негативной опережающей проверки: X(?!Y)

Он означает: найди такой X, за которым НЕ следует Y.

Ретроспективная проверка

Синтаксис ретроспективной проверки: (?<=Y)X

Синтаксис негативной ретроспективной проверки: (?<!Y)X

let regexp = /(?<=\$)\d+/g; "1 индейка стоит $30".match(regexp); // 30

Скобочные группы

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

let regexp = /\d+(?=(€))/

Запрет катастрофического возврата

В некоторых случаях регулярные выражения могут выполняться очень долго:

let regexp = /^(\d+)*$/; regexp.test("012345678901234567890123456789!") // скрипт зависнет на какое-то время

Движок не доходит до конца и начинает "отступать", по-разному разбивая строку. Кол-во комбинаций в данном случае - 2^n - 1.

Исправить можно уменьшив кол-во комбинаций или запретив возврат.

Шаблон, захватывающий максимальное количество повторений \w без возврата, выглядит так: (?=(\w+))\1.

"JavaScript".match(/\w+Script/); // JavaScript (происходит возврат) "JavaScript".match(/(?=(\w+))\1Script/); // null (шаблон \1 забрал на себя все буквы)

Поиск на заданной позиции, флаг "y"

Для поиска, начиная с нужной позиции, можно использовать метод regexp.exec(str).

Без флагов g и y этот ищет первое совпадение в строке аналогично str.match(regexp).

С флагом g осуществляет поиск начиная с позиции, заданной свойством regexp.lastIndex.

Последовательные вызовы regexp.exec могут найти все совпадения (альтернатива методу str.matchAll).

Можно самостоятельно задать lastIndex, начав поиск с нужной позиции.

Флаг y заставляет regexp.exec искать ровно на позиции lastIndex. Использование флага y – ключ к хорошей производительности.

let str = 'let varName = "value"'; let regexp = /\w+/y; regexp.lastIndex = 3; regexp.exec(str); // null (на позиции 3 пробел, а не слово) regexp.lastIndex = 4; regexp.exec(str); // varName (слово на позиции 4)

regexp.test с флагом g ищет, начиная с regexp.lastIndex и обновляет это свойство как и regexp.exec.

Свойства объекта, их конфигурация

Флаги и дескрипторы свойств

Флаги свойств

Помимо значения value, свойства объекта имеют три специальных атрибута (так называемые «флаги»). Когда мы создаём свойство «обычным способом», все они имеют значение true.

writable – если truthy, свойство можно изменить, иначе оно только для чтения.

enumerable – если truthy, свойство перечисляется в циклах, в противном случае циклы его игнорируют.

configurable – если truthy, свойство можно удалить, а эти атрибуты можно изменять, иначе этого делать нельзя.

Метод Object.getOwnPropertyDescriptor позволяет получить полную информацию о свойстве.

let descriptor = Object.getOwnPropertyDescriptor(obj, propertyName);

obj - объект, из которого мы получаем информацию.

propertyName - имя свойства.

let user = { name: "John" }; let descriptor = Object.getOwnPropertyDescriptor(user, 'name'); console.log(descriptor); // {value: "John", writable: true, enumerable: true, configurable: true}

Чтобы изменять флаги, существует метод Object.defineProperty.

Object.defineProperty(obj, propertyName, descriptor);

obj, propertyName - объект и его свойство, для которого нужно применить дескриптор.

descriptor - применяемый дескриптор.

Если свойство существует, defineProperty обновит его флаги. Иначе метод создаёт новое свойство с указанным значением и флагами; если какой-либо флаг не указан явно, ему присваивается значение false.

let user = {}; Object.defineProperty(user, "name", { value: "John" }); let descriptor = Object.getOwnPropertyDescriptor(user, 'name'); console.log(descriptor); // {value: 'John', writable: false, enumerable: false, configurable: false}

Только для чтения

Сделаем свойство user.name доступным только для чтения. Для этого изменим флаг writable:

let user = { name: "John" }; Object.defineProperty(user, "name", { writable: false }); user.name = "Pete"; // TypeError: Cannot assign to read only property 'name' of object '#<Object>'

Ошибка появляется только в строгом режиме. Но без 'use strict' операция записи в свойство «только для чтения» всё равно не будет выполнена успешно.

Неперечислимое свойство

Встроенный метод toString в объектах – неперечислимый, его не видно в цикле for..in.

Мы можем написать свой toString, который будет неперечислимым.

let user = { name: "John", toString() { return this.name; } }; Object.defineProperty(user, "toString", { enumerable: false }); for (let key in user) console.log(key); // name

Неперечислимые свойства также не возвращаются Object.keys:

console.log(Object.keys(user)); // name

Неконфигурируемое свойство

Флаг неконфигурируемого свойства (configurable:false) иногда предустановлен для некоторых встроенных объектов и свойств.

Неконфигурируемое свойство не может быть удалено.

Например, свойство Math.PI – только для чтения, неперечислимое и неконфигурируемое.

let descriptor = Object.getOwnPropertyDescriptor(Math, 'PI'); console.log(descriptor); // {value: 3.141592653589793, writable: false, enumerable: false, configurable: false}

Определение свойства как неконфигурируемого невозможно отменить, потому что defineProperty не работает с неконфигурируемыми свойствами.

Object.defineProperty(Math, 'PI', {writable: true}); // TypeError: Cannot redefine property: PI

Ошибки отображаются только в строгом режиме. Операции всё равно не будут выполнены успешно.

Object.defineProperties и .getOwnPropertyDescriptors

Метод Object.defineProperties позволяет определять множество свойств сразу.

Object.defineProperties(obj, { prop1: descriptor1, prop2: descriptor2, ... }); Object.defineProperties(user, { name: { value: "John", writable: false }, surname: { value: "Smith", writable: false }, });

Метод Object.getOwnPropertyDescriptors возвращает все дескрипторы всех свойств, включая свойства-символы.

Вместе с Object.defineProperties этот метод можно использовать для неглубокого клонирования объекта вместе с его флагами:

let clone = Object.defineProperties({}, Object.getOwnPropertyDescriptors(obj));

Глобальное запечатывание объекта

Методы, которые ограничивают доступ ко всему объекту:

Object.preventExtensions(obj) - запрещает добавлять новые свойства в объект.

Object.seal(obj) - запрещает добавлять/удалять свойства. Устанавливает configurable: false для всех существующих свойств.

Object.freeze(obj) - Запрещает добавлять/удалять/изменять свойства. Устанавливает configurable: false, writable: false для всех существующих свойств.

А также есть методы для их проверки:

Object.isExtensible(obj) - возвращает false, если добавление свойств запрещено, иначе true.

Object.isSealed(obj) - возвращает true, если добавление/удаление свойств запрещено и для всех существующих свойств установлено configurable: false.

Object.isFrozen(obj) - Возвращает true, если добавление/удаление/изменение свойств запрещено, и для всех текущих свойств установлено configurable: false, writable: false.

Свойства - геттеры и сеттеры

Свойства объекта делятся на cвойства-данные (data properties) и свойства-аксессоры (accessor properties).

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

Геттеры и сеттеры

Свойства-аксессоры представлены методами: get (геттер) – для чтения и set (сеттер) – для записи.

Добавим в объект со свойствами name и surname свойство fullName для полного имени. Чтобы не дублировать уже имеющуюся информацию, реализуем его при помощи аксессора.

let user = { name: "John", surname: "Smith", get fullName() { return `${this.name} ${this.surname}`; } }; console.log(user.fullName); // John Smith user.fullName = "Тест"; // TypeError: Cannot set property fullName of #<Object> which has only a getter

Исправим ошибку, добавив сеттер для user.fullName:

let user = { name: "John", surname: "Smith", get fullName() { return `${this.name} ${this.surname}`; }, set fullName(value) { [this.name, this.surname] = value.split(" "); } }; user.fullName = "Alice Cooper"; console.log(user.name); // Alice console.log(user.surname); // Cooper

В итоге получено «виртуальное» свойство fullName. Его можно прочитать и изменить.

Дескрипторы свойств доступа

Свойство объекта может быть либо свойством-аксессором, либо свойством-данным.

Свойства-аксессоры не имеют value и writable, но взамен предлагают функции get и set. То есть, дескриптор аксессора имеет атрибуты get, set, enumerable, configurable.

let user = { name: "John", surname: "Smith" }; Object.defineProperty(user, 'fullName', { get() { return `${this.name} ${this.surname}` }, set(value) { [this.name, this.surname] = value.split(" ") }, }); console.log(user.fullName); // John Smith for (let key in user) console.log(key); // name, затем surname

Умные геттеры/сеттеры

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

let user = { get name() { return this._name; }, set name(value) { if (value.length < 4) { console.log("Имя слишком короткое"); return; } this._name = value; } }; user.name = "Pete"; console.log(user.name); // Pete user.name = ""; // Имя слишком короткое

Таким образом, само имя хранится в _name, доступ к которому производится через геттер и сеттер.

Технически, внешний код всё ещё может получить доступ к имени напрямую с помощью user._name, но существует соглашение о том, что свойства, начинающиеся с символа _, являются внутренними, и к ним не следует обращаться из-за пределов объекта.

Использование для совместимости

Акцессоры позволяют в любой момент взять «обычное» свойство и изменить его поведение, поменяв на геттер и сеттер.

Например, мы начали реализовывать объект user, используя свойства-данные name и age:

function User(name, age) { this.name = name; this.age = age; } let john = new User("John", 25); console.log( john.age ); // 25

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

function User(name, birthday) { this.name = name; this.birthday = birthday; } let john = new User("John", new Date(1992, 6, 1));

Что делать со старым кодом, который использует свойство age?

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

Добавление геттера для age решит проблему:

function User(name, birthday) { this.name = name; this.birthday = birthday; Object.defineProperty(this, "age", { get() { let now = new Date(); if (now.getMonth() < this.birthday.getMonth() || now.getMonth() === this.birthday.getMonth() && now.getDate() < this.birthday.getDate()) { return now.getFullYear() - this.birthday.getFullYear() - 1; } return now.getFullYear() - this.birthday.getFullYear(); } }); } let john = new User("John", new Date(1992, 7, 15)); console.log(john.birthday); // доступен и день рождения console.log(john.age); // и возраст

Прототипы, наследование

Прототипное наследование

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

[[Prototype]]

Объекты имеют свойство [[Prototype]], которое либо равно null, либо ссылается на другой объект («прототип»).

Если свойство в объекте отсутствует, то возвращается одноименное свойство из прототипа. Такое свойство называется унаследованным.

Свойство __proto__ изменяет [[Prototype]] объекта. Ограничения:

  1. Ссылки не могут идти по кругу.
  2. Значение __proto__ может быть объектом или null. Другие типы игнорируются.

Метод obj.hasOwnProperty(key) возвращает true, если у obj есть собственное, не унаследованное, свойство с именем key.

Операция записи не использует прототип

Операции записи/удаления работают напрямую с объектом.

Свойства-аксессоры – исключение, так как запись в него обрабатывается функцией-сеттером. То есть это фактически вызов функции.

let user = { name: "John", surname: "Smith", set fullName(value) { [this.name, this.surname] = value.split(" "); }, get fullName() { return `${this.name} ${this.surname}`; } }; let admin = { __proto__: user, isAdmin: true }; console.log(admin.fullName); // John Smith admin.fullName = "Alice Cooper"; console.log(admin.name); // Alice console.log(admin.surname); // Cooper

Значение «this»

Не важно, где находится метод: в объекте или его прототипе. При вызове метода this — всегда объект перед точкой.

Таким образом, вызов сеттера admin.fullName в качестве this использует admin, а не user.

let animal = { sleep() { this.isSleeping = true; } }; let rabbit = { __proto__: animal }; rabbit.sleep(); console.log(rabbit.isSleeping); // true console.log(animal.isSleeping); // undefined

F.prototype

Новые объекты могут быть созданы с помощью функции-конструктора new F().

Если в F.prototype содержится объект, оператор new устанавливает его в качестве [[Prototype]] для нового объекта.

F.prototype - это обычное свойство с именем "prototype". Оно устанавливает прототип объекта только в момент вызова new F().

let animal = { eats: true }; function Rabbit(name) { this.name = name; } Rabbit.prototype = animal; let rabbit = new Rabbit("White Rabbit"); console.log( rabbit.eats ); // true

F.prototype по умолчанию, свойство constructor

По умолчанию prototype – объект с единственным свойством constructor, которое ссылается на функцию-конструктор.

function Rabbit() {} console.log( Rabbit.prototype.constructor == Rabbit ); // true

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

let obj2 = new obj1.constructor(args);

Однако, если заменить прототип по умолчанию на другой объект, то свойства constructor в нём не будет.

Встроенные прототипы

Object.prototype

alert( {} ); // [object Object]

obj = {} – это то же самое, что и obj = new Object(), где Object – встроенная функция-конструктор для объектов с собственным свойством prototype, которое ссылается на объект с методом toString и другими.

Когда вызывается new Object() (или создаётся объект с помощью литерала {...}), свойство [[Prototype]] этого объекта устанавливается на Object.prototype.

Таким образом, когда alert вызывает преобразование в примитив, метод obj.toString() берётся из Object.prototype.

Другие встроенные прототипы

Другие встроенные объекты, такие как Array, Date, Function и другие, также хранят свои методы в прототипах.

Согласно спецификации, наверху иерархии встроенных прототипов находится Object.prototype.

Наследование встроенных объектов

При создании массива [1, 2, 3] используется конструктор массива Array. Поэтому прототипом массива становится Array.prototype.

let arr = [1, 2, 3]; console.log( arr.__proto__ === Array.prototype ); // true console.log( arr.__proto__.__proto__ === Object.prototype ); // true console.log( arr.__proto__.__proto__.__proto__ ); // null

В браузерных инструментах, таких как консоль разработчика, можно посмотреть цепочку наследования, используя console.dir.

Примитивы

Примитивы - не объекты. Но если мы попытаемся получить доступ к их свойствам, будет создан временный объект-обёртка с использованием встроенных конструкторов String, Number и Boolean, который предоставит методы и после этого исчезнет.

Методы этих объектов также находятся в прототипах, доступных как String.prototype, Number.prototype и Boolean.prototype.

Специальные значения null и undefined не имеют объектов-обёрток. Также у них нет соответствующих прототипов.

Изменение встроенных прототипов

Встроенные прототипы можно изменять. Но их не рекомендуется менять, кроме как для создания полифилов.

Методы прототипов, объекты без __proto__

__proto__ - устаревший геттер/сеттер для свойства [[Prototype]]. Современные же методы это:

Object.create(proto, [descriptors]) – создаёт пустой объект со свойством [[Prototype]], указанным как proto, и необязательными дескрипторами свойств descriptors.

Object.getPrototypeOf(obj) – возвращает свойство [[Prototype]] объекта obj.

Object.setPrototypeOf(obj, proto) – устанавливает свойство [[Prototype]] объекта obj как proto.

У Object.create есть необязательный второй аргумент: дескрипторы свойств. Можно добавить дополнительное свойство новому объекту таким образом:

let animal = { eats: true }; let rabbit = Object.create(animal, { jumps: { value: true, } }); console.log(rabbit.jumps); // true
Не нужно менять [[Prototype]] существующих объектов, если важна скорость

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

Изменение прототипа «на лету» с помощью Object.setPrototypeOf или obj.__proto__ – очень медленная операция, которая ломает внутренние оптимизации для операций доступа к свойствам объекта.

"Простейший" объект

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

let obj = {}; let key = prompt("What's the key?", "__proto__"); obj[key] = "some value"; console.log(obj[key] === Object.prototype); // true

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

Такие ошибки являются сложными для отлавливания или даже становятся уязвимостями.

Избежать проблемы можно, переключившись на использование коллекции Map или создав «простейший» (без прототипа) объект с помощью Object.create(null).

Кроме того, Object.create даёт лёгкий способ создать поверхностную копию объекта со всеми дескрипторами:

let clone = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));

Классы

Класс: базовый синтаксис

Иногда нужно создавать много объектов одного вида, например пользователей, товары или что-то ещё. Конструкция «class» предоставляет новые возможности, полезные для объектно-ориентированного программирования.

Синтаксис «class»

Методы в классе не разделяются запятой:

class MyClass { prop = value; constructor() {} method(...) {} get something(...) {} set something(...) {} [Symbol.iterator]() {} }

Вызов new MyClass() создает новый объект со всеми перечисленными методами и запускает конструктор класса.

Методы не записываются в объект, а находятся в свойстве prototype класса.

class User { constructor(name) { this.name = name; } sayHi() { console.log(this.name); } } console.log(typeof User); // function console.log(Object.getOwnPropertyNames(User.prototype)); // ['constructor', 'sayHi']
Отличия классов от конструкторов:
  1. Функция, созданная с помощью class, помечена специальным внутренним свойством [[IsClassConstructor]]: true. В отличие от обычных функций, конструктор класса не может быть вызван без new.
  2. Методы класса являются неперечислимыми. Определение класса устанавливает флаг enumerable в false для всех методов в "prototype".
  3. Классы всегда используют 'use strict'.

Class Expression

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

let User = class {};

Аналогично Named Function Expression, Class Expression может иметь имя, которое не видно за его пределами.

let User = class MyClass { sayHi() { console.log(MyClass); } }; new User().sayHi(); // class MyClass { ... console.log(MyClass); // ReferenceError: MyClass is not defined

Геттеры/сеттеры

Как и в литеральных объектах, в классах можно объявлять вычисляемые свойства, геттеры/сеттеры и т.д.

При объявлении класса геттеры/сеттеры создаются на User.prototype.

class User { constructor(name) { this.name = name; } get name() { return this._name; } set name(value) { if (value.length < 4) { console.log("Имя слишком короткое."); return; } this._name = value; } } let user = new User("Иван"); console.log(user.name); // Иван user = new User(''); // Имя слишком короткое.

Свойства классов

Свойство name создаётся оператором new перед запуском конструктора, это именно свойство объекта.

class User { name = "Аноним"; } console.log( Object.keys( new User() ) ); // ['name']

Свойства-функции класса, объявленные как method = function() {} также становятся свойствами объекта.

Наследование классов

Ключевое слово «extends»

class Child extends Parent - синтаксис для расширения другого класса.

Ключевое слово extends устанавливает Child.prototype.[[Prototype]] в Parent.prototype.

После extends разрешены любые выражения:

function f(phrase) { return class { sayHi() { console.log(phrase); } }; } class User extends f("Привет") {} new User().sayHi(); // Привет

Переопределение методов

Ключевое слово super позволяет создать метод класса, основываясь на родительском.

super.method(...) вызывает родительский метод.

super(...) для вызова родительского конструктора (работает только внутри нашего конструктора).

class Child extends Parent { someMethod() { super.someMethod(); this.anotherMethod(); } }

Переопределение конструктора

Если класс расширяет другой класс и не имеет конструктора, то автоматически создаётся такой конструктор:

constructor(...args) { super(...args); }

Иначе конструктор должен обязательно вызывать super(...) и делать это перед использованием this.

class Parent {} class Child extends Parent { constructor() {} } console.log( new Child() ); // ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor

Переопределение полей класса

Поле класса инициализируется:

  • Перед конструктором для базового класса (который ничего не расширяет)
  • Сразу после super() для производного класса
class Animal { name = 'animal'; constructor() { console.log(this.name); } } class Rabbit extends Animal { name = 'rabbit'; } new Animal(); // animal new Rabbit(); // animal

На момент вызова console.log(this.name) поле name родительского класса еще не было переопределено дочерним, поэтому вызов new Rabbit() возвращает 'animal'.

Устройство super, [[HomeObject]]

Когда функция объявляется как метод внутри класса или объекта, она получает свойство [[HomeObject]], равное этому объекту.

let animal = { name: "Животное", eat() { // animal.eat.[[HomeObject]] == animal console.log(`${this.name} ест.`); } }; let rabbit = { __proto__: animal, name: "Кролик", eat() { // rabbit.eat.[[HomeObject]] == rabbit super.eat(); } }; let longEar = { __proto__: rabbit, name: "Длинноух", eat() { // longEar.eat.[[HomeObject]] == longEar super.eat(); } }; longEar.eat(); // Длинноух ест.

[[HomeObject]] нельзя изменить, эта связь – навсегда. Поэтому копировать метод, использующий super, между разными объектами небезопасно.

Методы, а не свойства-функции

Методы объекта, объявленные как "method: function()" не получают свойство [[HomeObject]], поэтому им недоступно ключевое слово super.

let animal = { eat: function() {} }; let rabbit = { __proto__: animal, eat: function() { super.eat(); } }; rabbit.eat(); // SyntaxError: 'super' keyword unexpected here

Статические свойства и методы

Можно присвоить метод самому классу. Такие методы называются статическими.

class User { static staticMethod() { console.log(this === User); } } User.staticMethod(); // true

Это то же самое, что присвоить метод напрямую как свойство функции:

User.staticMethod = function() { ... }

Статические свойства также возможны. Они были добавлены в язык позже:

class Article { static publisher = "Илья Кантор"; } console.log( Article.publisher ); // Илья Кантор

Наследование статических свойств и методов

Статические свойства и методы наследуются.

По умолчанию [[Prototype]] класса устанавливается в Function.prototype:

console.log(Function.__proto__ === Function.prototype); // true

Ключевое слово extends дает дочернему классу ссылку [[Prototype]] на родительский.

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

Наследование классов

Приватные методы и свойства

Приватные свойства и методы должны начинаться с #. Они доступны только внутри класса. К ним нельзя получить доступ извне или из наследуемых классов.

Приватные поля не конфликтуют с публичными. Может быть два поля одновременно – приватное #waterAmount и публичное waterAmount.

class CoffeeMachine { #waterAmount = 0; get waterAmount() { return this.#waterAmount; } set waterAmount(value) { if (value < 0) throw new Error("Отрицательный уровень воды"); this.#waterAmount = value; } } let machine = new CoffeeMachine(); machine.waterAmount = 100; console.log(machine.waterAmount); // 100 console.log(machine.#waterAmount); // SyntaxError: Private field '#waterAmount' must be declared in an enclosing class

Расширение встроенных классов

Встроенные методы, такие как filter, map и другие возвращают объекты того же класса, к которому были применены.

class PowerArray extends Array { isEmpty() { return this.length === 0; } } let arr = new PowerArray(1, 2).map( item => item * 2); console.log(arr.isEmpty()); // false console.log(arr.constructor === PowerArray); // true

При помощи статического геттера Symbol.species это поведение можно настроить:

class PowerArray extends Array { static get [Symbol.species]() { return Array; } } let arr = new PowerArray(1, 2).map( item => item * 2); console.log(arr.constructor === Array); // true

Отсутствие статического наследования встроенных классов

Обычно, когда один класс наследует другой, то наследуются и статические методы. Но встроенные классы не наследуют статические методы друг друга.

В этом отличие наследования встроенных объектов от наследования посредством extends.

Наследование встроенных классов

Проверка класса: "instanceof"

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

Оператор instanceof

obj instanceof Class - вернёт true, если obj принадлежит классу Class или наследующему от него.

То есть, он сравнивает Class.prototype с obj.__proto__, obj.__proto__.__proto__ и тд.

Но если Class имеет статический метод Symbol.hasInstance, то вызывается он: instanceof вернет true, если Symbol.hasInstance вернет truthy, иначе false.

class Animal { static [Symbol.hasInstance](obj) { if (obj.canEat) return true; } } let obj = { canEat: true }; console.log(obj instanceof Animal); // true

objA.isPrototypeOf(objB) - возвращает true, если objA равен objB.__proto__, либо objB.__proto__.__proto__ и тд.

То есть, без учета Symbol.hasInstance, obj instanceof Class равнозначно Class.prototype.isPrototypeOf(obj).

Object.prototype.toString возвращает тип

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

console.log( {}.toString.call(alert) ); // [object Function] console.log( {}.toString.call(null) ); // [object Null]
Symbol.toStringTag

Поведение метода объектов toString можно настраивать, используя специальное свойство объекта Symbol.toStringTag.

let user = { [Symbol.toStringTag]: "User" }; console.log( {}.toString.call(user) ); // [object User]

Такое свойство есть у большей части объектов, специфичных для определённых окружений.

console.log( {}.toString.call(window) ); // [object Window]

Примеси

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

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

let sayHiMixin = { sayHi() { console.log(`Привет, ${this.name}`); }, }; class User { constructor(name) { this.name = name; } } Object.assign(User.prototype, sayHiMixin); new User("Вася").sayHi(); // Привет, Вася!

Примеси могут наследовать друг друга.

Обработка ошибок

Обработка ошибок, "try..catch"

Синтаксис «try…catch»

Конструкция try..catch состоит из двух основных блоков: try, и затем catch:

try { // код... } catch (err) { // обработка ошибки }

Сначала выполняется код внутри блока try {...}.

Если в нём нет ошибок, то блок catch(err) игнорируется.

Если же в нём возникает ошибка, то выполнение try прерывается, и поток управления переходит в начало catch(err). Переменная err содержит объект ошибки с подробной информацией о произошедшем.

try { console.log(1); // 1 lalala; console.log('Выполнение сюда не дойдет'); } catch(err) { console.log(err); // 'ReferenceError: lalala is not defined' }

try..catch работает синхронно, поэтому не поймает исключение, которое произойдет, например, в setTimeout. Для этого try..catch должен находиться внутри запланированной функции:

setTimeout(function() { try { noSuchVariable; } catch { console.log("Ошибка поймана"); } }, 1000);

Объект ошибки

Для всех встроенных ошибок объект ошибки имеет два основных свойства:

name - имя ошибки. Например, "ReferenceError".

message - текстовое сообщение о деталях ошибки.

В целях отладки может использоваться нестандартное свойство stack, содержащее информацию о последовательности вложенных вызовов, которые привели к ошибке.

Генерация собственных ошибок

Оператор throw генерирует ошибку. Синтаксис:

throw <объект ошибки> - в качестве объекта ошибки можно передать что угодно, даже примитив, но лучше, чтобы это был объект со свойствами name и message для совместимости со встроенными ошибками.

Есть множество встроенных конструкторов для стандартных ошибок: Error, SyntaxError и тд. Они генерируют объект ошибки, записывая в свойство name имя конструктора, а в message - содержимое аргумента:

let error = new Error("Ого, ошибка! o_O"); console.log(error.name); // Error console.log(error.message); // Ого, ошибка! o_O
Проброс исключения

Блок catch предназначен для обработки только тех ошибок, которые ему известны. Остальные ошибки следует «пробрасывать» через throw err.

try { blabla(); // генерируется ReferenceError } catch (e) { if (e.name !== 'SyntaxError') { throw e; } }
try…catch…finally

Конструкция try..catch может содержать ещё одну секцию: finally. Если секция есть, то она выполнится после try, если ошибок не было, в противном случае после catch. Блок finally срабатывает при любом выходе из try..catch, в том числе и return.

try..finally без catch

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

Глобальный catch

В случае, если произошла ошибка снаружи try..catch, и скрипт упал, мы хотим залогировать ошибку, показать что-то пользователю и тд.

Способа сделать это нет в спецификации, но обычно окружения предоставляют его. В Node.js для этого есть process.on("uncaughtException"). В браузере можно присвоить функцию специальному свойству window.onerror, которая будет вызвана в случае необработанной ошибки.

window.onerror = function(message, url, line, col, error) { // ... };

Пользовательские ошибки, расширение Error

Мы можем наследовать свои классы ошибок от Error и других встроенных классов ошибок, так как за счет наследования появляется возможность идентификации объектов ошибок посредством obj instanceof Error.

"Псевдокод" встроенного класса Error, определённого самим JavaScript:

class Error { constructor(message) { this.message = message; this.name = "Error"; this.stack = <стек вызовов>; } }

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

class ValidationError extends Error { constructor(message) { super(message); this.name = this.constructor.name; } } class PropertyRequiredError extends ValidationError { constructor(property) { super("Нет свойства: " + property); this.property = property; } }

Применение:

function readUser(json) { let user = JSON.parse(json); if (!user.name) { throw new PropertyRequiredError("name"); } return user; } try { let user = readUser('{ "age": 25 }'); } catch (err) { if (err instanceof ValidationError) { console.log("Неверные данные. " + err.message); // Неверные данные. Нет свойства: name console.log(err.name); // PropertyRequiredError console.log(err.property); // name } else if (err instanceof SyntaxError) { console.log("Ошибка синтаксиса JSON: " + err.message); } else { throw err; } }

Промисы, async/await

Промисы

Синтаксис создания промиса:

let promise = new Promise(function(resolve, reject) { // функция-исполнитель (executor) });

Функция-исполнитель выполняется синхронно и когда она получит результат (обычно не сразу), должна вызвать resolve или reject - колбеки, которые предоставляет сам JavaScript.

resolve(value) — если работа завершилась успешно, с результатом value.

reject(error) — если произошла ошибка, error – объект ошибки.

У объекта promise, возвращаемого конструктором new Promise, есть внутренние свойства:

state — состояние промиса, может иметь одно из трех значений:

  • "pending" («ожидание») - начальное состояние
  • "fulfilled" («выполнен») - состояние после вызова resolve
  • "rejected" («отклонен») - состояние после вызова reject

result — результат выполнения, undefined если промис в состоянии "pending", иначе значение, которое было передано в колбек resolve или reject.

При вызове колбека resolve/reject промис изменяет состояние. Состояние промиса может быть изменено только один раз.

Потребители: then, catch

Методы then и catch ждут выполнения промиса, если он находится в состоянии "pending", иначе запускаются сразу.

then
promise.then( function(result) { /* ... */ }, // вызывается, если промис перешел в состояние 'fulfilled' function(error) { /* ... */ } // вызывается, если промис перешел в состояние 'rejected' );

Если обрабатывать отклоненный промис не нужно, в then можно передать только один аргумент.

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

catch
promise.catch(f) - это сокращённый вариант promise.then(null, f).

Очистка: finally

Вызов .finally(f) похож на .then(f, f), в том смысле, что f выполнится в любом случае. Обычно его задача – выполнить «общие» завершающие процедуры.

Обработчик, вызываемый из finally, не получает результат предыдущего обработчика. Он «пропускает» результат или ошибку дальше, к последующим обработчикам:

new Promise((resolve, reject) => { setTimeout(() => resolve(2), 2000); }) .finally(() => console.log(1)) // 1, срабатывает первым .then(result => console.log(result)); // 2

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

Обработчики промисов .then / .catch / .finally всегда асинхронны.

Цепочка промисов

Обработчик handler, переданный в .then(handler), может вернуть промис. В этом случае дальнейшие обработчики ожидают, пока он выполнится, и затем получают его результат.

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

Thenable

Обработчик может возвращать любой объект, содержащий метод then, и этот объект будет обработан как промис. Такие объекты называют «thenable».

class Thenable { constructor(num) { this.num = num; } then(resolve, reject) { console.log(0); setTimeout(() => resolve(this.num * 2), 1000); } } Promise.resolve(1) .then(result => { return new Thenable(result); // 0, сразу }) .then(result => console.log(result)); // 2, через секунду

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

Промисы: обработка ошибок

Самый лёгкий путь перехватить ошибку – добавить catch в конец цепочки промисов.

Неявный try…catch

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

new Promise((resolve, reject) => { throw new Error("Ошибка!"); }).catch(err => console.log(err)); // Error: Ошибка! new Promise((resolve, reject) => { reject(new Error("Ошибка!")); }).catch(err => console.log(err)); // Error: Ошибка!

"Невидимый try..catch" вокруг промиса автоматически перехватывает ошибку и превращает её в отклонённый промис.

Пробрасывание ошибок

В обычном try..catch мы можем проанализировать ошибку и повторно пробросить дальше, если не можем её обработать.

То же самое возможно для промисов.

new Promise((resolve, reject) => { throw new Error("Ошибка!"); }) .catch(function(err) { if (err instanceof URIError) { // обрабатываем ошибку } else { throw err; } }) .then(function() { /* не выполнится */ }) .catch(error => { console.log(`Неизвестная ошибка: ${error}`); });

Необработанные ошибки

Если ошибка в промисе не была перехвачена (не обработана в конце очереди микрозадач), скрипт умирает с сообщением в консоли.

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

  • promise - промис, который сгенерировал ошибку
  • reason - объект ошибки, которая не была обработана
window.addEventListener('unhandledrejection', function(event) { console.log(event.promise); // Promise <rejected>: Error: Ошибка! console.log(event.reason); // Error: Ошибка! }); new Promise(function() { throw new Error("Ошибка!"); });

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

Promise API

В классе Promise есть 6 статических методов.

Promise.all

Promise.all(iterable) - принимает перебираемый объект с промисами (разрешено передавать не-промисы) и возвращает новый промис. Новый промис завершится, когда завершится весь переданный список промисов, и его результатом будет массив их результатов.

Promise.all([1, new Promise( r => setTimeout(() => r(2), 1000) ), 3]) .then(result => console.log(result)); // [1, 2, 3]

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

Если любой из промисов завершится с ошибкой, то промис, возвращённый Promise.all, немедленно завершается с этой ошибкой.

Promise.allSettled

Promise.allSettled(iterable) - всегда ждет завершения всех промисов. В массиве результатов будет:

{status: "fulfilled", value: any} - для успешно завершившихся промисов.

{status: "rejected", reason: err} - для промисов с ошибкой.

Если браузер не поддерживает Promise.allSettled, для него легко сделать полифил:

if (!Promise.allSettled) { Promise.allSettled = function(promises) { return Promise.all(promises.map(p => Promise.resolve(p).then(value => ({ status: 'fulfilled', value: value }), error => ({ status: 'rejected', reason: error })))); }; }

Promise.race

Promise.race(iterable) - ждёт только первый выполненный промис.

Promise.race([ new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)), new Promise((resolve, reject) => setTimeout(() => reject(new Error("Ошибка!")), 2000)), new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000)) ]).then(result => console.log(result)); // 1

Promise.any

Promise.any(iterable) - ждет только первый успешно выполенный промис. Если ни один из переданных промисов не завершится успешно, тогда возвращённый промис будет отклонён с помощью AggregateError – специального объекта ошибок, который хранит все ошибки промисов в своём свойстве errors.

Promise.any([ new Promise((resolve, reject) => setTimeout(() => reject(new Error("Ошибка!")), 1000)), new Promise((resolve, reject) => setTimeout(() => reject(new Error("Ошибка!")), 2000)), ]).catch(err => console.log(err)); // AggregateError: All promises were rejected

Promise.resolve/reject

Promise.resolve(value) создаёт успешно выполненный промис с результатом value.

Promise.reject(error) создаёт промис, завершённый с ошибкой error.

Async/await

Ключевое слово async перед объявлением функции (или метода) обязывает её всегда возвращать промис и позволяет использовать await в теле этой функции.

await может использоваться только в async функциях и на верхнем уровне модулей (глобально).

Ключевое слово await заставит интерпретатор JavaScript ждать до тех пор, пока промис справа от await не выполнится. После чего оно вернёт его результат, и выполнение кода продолжится.

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

Такие ошибки можно ловить, используя try..catch, как с обычным throw:

(async () => { try { await fetch('http://no-such-url'); } catch(err) { console.log(err); // TypeError: failed to fetch } })()

Если нет try..catch, асинхронная функция будет возвращать завершившийся с ошибкой промис. В этом случае можно будет использовать метод .catch, чтобы обработать ошибку: f().catch(/* ... */).

async/await отлично работает с Promise.all:

let results = await Promise.all([ fetch(url1), fetch(url2), ]);

Генераторы, продвинутая итерация

Генераторы

Функция-генератор

function* - конструкция, объявляющая функцию-генератор. Когда функция-генератор вызывается, она возвращает специальный объект - генератор.

Генератор имеет метод next(), при вызове которого запускается выполнение кода до ближайшей инструкции yield <значение> (при отсутствии значения, оно предполагается равным undefined). По достижении yield выполнение функции приостанавливается, а соответствующее значение – возвращается во внешний код.

Результатом метода next() всегда является объект с двумя свойствами:

value - значение из yield.

done - true/false. true если выполнение функции завершено, иначе false.

function* generateSequence() { yield 1; return 2; } let generator = generateSequence(); console.log(generator.toString()); // [object Generator] console.log(generator.next()); // {value: 1, done: false} console.log(generator.next()); // {value: 2, done: true} console.log(generator.next()); // {value: undefined, done: true}
Перебор генераторов

Генераторы являются перебираемыми объектами.

function* generateSequence() { yield 1; yield 2; return 3; } for (let value of generateSequence()) { console.log(value); // 1, затем 2 }
Использование генераторов для перебираемых объектов

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

let range = { from: 1, to: 5, *[Symbol.iterator]() { // краткая запись для [Symbol.iterator]: function*() for (let value = this.from; value <= this.to; value++) { yield value; } } }; console.log( [...range] ); //  [1, 2, 3, 4, 5]

Композиция генераторов

Для генераторов есть особый синтаксис yield*, который позволяет «вкладывать» генераторы один в другой (осуществлять их композицию).

function* generateSequence(start, end) { for (let i = start; i <= end; i++) yield i; } function* generatePasswordCodes() { yield* generateSequence(48, 57); yield* generateSequence(65, 90); yield* generateSequence(97, 122); } let str = ''; for(let code of generatePasswordCodes()) { str += String.fromCharCode(code); } console.log(str); // 0123456789A..Za..z

yield – дорога в обе стороны

yield – не только возвращает результат, но и может передавать значение извне в генератор.

function* gen() { let ask1 = yield "2 + 2 = ?"; console.log(ask1); // 4 let ask2 = yield "3 * 3 = ?" console.log(ask2); // 9 } let generator = gen(); console.log( generator.next().value ); // "2 + 2 = ?" console.log( generator.next(4).value ); // "3 * 3 = ?" console.log( generator.next(9).done ); // true

generator.throw

Можно передать не только результат, но и инициировать ошибку. Чтобы передать ошибку в yield, нужно вызвать generator.throw(err). В таком случае исключение err возникнет на строке с yield.

function* gen() { try { let result = yield "2 + 2 = ?"; console.log("Выполнение не дойдёт до этой строки"); } catch(e) { console.log(e); // Error: Ответ не найден в моей базе данных } } let generator = gen(); console.log( generator.next().value ); // 2 + 2 = ? generator.throw(new Error("Ответ не найден в моей базе данных"));

Асинхронные итераторы и генераторы

Асинхронные итераторы

Чтобы сделать объект итерируемым асинхронно:

  1. Используется Symbol.asyncIterator вместо Symbol.iterator.
  2. next() должен возвращать промис.
  3. Чтобы перебрать такой объект, используется цикл for await (let item of iterable).
let range = { from: 1, to: 5, [Symbol.asyncIterator]() { return { current: this.from, last: this.to, async next() { await new Promise(resolve => setTimeout(resolve, 1000)); if (this.current <= this.last) { return { done: false, value: this.current++ }; } else { return { done: true }; } } }; } }; (async () => { for await (let value of range) { console.log(value); // через секунду 1, затем через секунду 2, ... 5 } })();

Асинхронные генераторы

async function* создает асинхронную функцию-генератор, при вызове функция создает генератор, который можно перебирать с помощью for await .. of.

async function* generateSequence(start, end) { for (let i = start; i <= end; i++) { await new Promise(resolve => setTimeout(resolve, 1000)); yield i; } } (async () => { let generator = generateSequence(1, 5); for await (let value of generator) { console.log(value); // через секунду 1, затем через секунду 2, ... 5 } })();

Метод generator.next() теперь тоже асинхронный и возвращает промисы.

Асинхронно перебираемые объекты

Symbol.asyncIterator может возвращать генератор, а не простой объект с методом next.

let range = { from: 1, to: 5, async *[Symbol.asyncIterator]() { // то же, что [Symbol.asyncIterator]: async function*() for (let value = this.from; value <= this.to; value++) { await new Promise(resolve => setTimeout(resolve, 1000)); yield value; } } };

Модули

Модули, введение

Что такое модуль?

Модуль – это просто файл. Один скрипт – это один модуль.

Модули могут загружать друг друга и использовать директивы export и import, чтобы обмениваться функциональностью, вызывать функции одного модуля из другого:

  • export отмечает переменные и функции, которые должны быть доступны вне текущего модуля
  • import позволяет брать функциональность из других модулей

📁 sayHi.js:

export function sayHi() { console.log('hi') };

📁 main.js:

import {sayHi} from './sayHi.js'; sayHi(); // hi

Основные возможности модулей

В модулях всегда используется режим 'use strict'.

Каждый модуль имеет свою собственную область видимости. Переменные и функции, объявленные в модуле, не видны в других скриптах. При необходимости сделать глобальную переменную, можно явно присвоить её объекту window.

Код в модуле выполняется только один раз при импорте

📁 sayHi.js:

console.log('hi'); // hi

📁 main.js:

import './sayHi.js';

📁 another.js:

import './sayHi.js';
import.meta
console.log(import.meta.url); // ссылка на html страницу для встроенного скрипта, либо ссылка на файл со скриптом для внешнего

Особенности в браузерах

Модули являются отложенными (deferred), точно так же, как скрипты с атрибутом defer.

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

Внешние скрипты

Внешние скрипты с атрибутом type="module" имеют два отличия от обычных скриптов.

Внешние скрипты с одинаковым атрибутом src запускаются только один раз.

Если модульный скрипт загружается с другого домена, то удалённый сервер должен установить заголовок Access-Control-Allow-Origin означающий, что загрузка скрипта разрешена, иначе скрипт не выполнится.

Совместимость, «nomodule»

Старые браузеры не понимают атрибут type="module". Скрипты с неизвестным атрибутом type просто игнорируются. Можно сделать для них «резервный» скрипт при помощи атрибута nomodule.

<script nomodule> console.log('Современные браузеры не выполнят этот скрипт'); </script>

Экспорт и импорт

Экспорт до объявления
export const MODULES_BECAME_STANDARD_YEAR = 2015;
Экспорт отдельно от объявления

📁 say.js

let sayHi = user => console.log(`Hello, ${user}`); let sayBye = user => console.log(`Bye, ${user}`); export {sayHi, sayBye};
Импорт *

📁 main.js

import * as say from './say.js'; say.sayHi('John'); // Hello, John say.sayBye('John'); // Bye, John
Импорт «как»
import {sayHi as hi, sayBye as bye} from './say.js';
Экспортировать «как»

📁 say.js

export {sayHi as hi, sayBye as bye};

📁 main.js

import {hi, bye} from './say.js'; hi('John'); // Hello, John! bye('John'); // Bye, John!

Экспорт по умолчанию

На практике часто встречаются модули, которые объявляют что-то одно. Для этого подхода есть специальный синтаксис - export default («экспорт по умолчанию»).

📁 user.js

export default class User { constructor(name) { this.name = name; } }

📁 main.js

import User from './user.js'; console.log( new User('John') ); // {name: 'John'}

Так как в файле может быть максимум один export default, то экспортируемая сущность не обязана иметь имя.

export default [1, 2, 3];

Чтобы соблюсти единообразие кода, принято давать соответствующие имена импортируемым переменным:

import LoginForm from './loginForm.js'; import func from '/path/to/func.js';
Имя «default»

Экспорт по умолчанию отдельно от объявления экспортируемой сущности:

let sayHi = user => console.log(`Hello, ${user}!`); export {sayHi as default};

Экспорт по умолчанию вместе с именованным экспортом:

📁 user.js

export default class User { constructor(name) { this.name = name; } } export let sayHi = user => console.log(`Hello, ${user}!`);

Примеры импорта такого экспорта:

📁 main.js

import {default as User, sayHi} './user.js';

📁 main.js

import User, {sayHi} from './user.js';

📁 main.js

import * as user from './user.js'; let User = user.default; console.log(new User('John')); // {name: 'John'} user.sayHi('Pete'); // Hello, Pete!

Реэкспорт

Синтаксис «реэкспорта» export ... from ... позволяет импортировать что-то и тут же экспортировать, возможно под другим именем:

import {login, logout} from 'auth/index.js'; // без инициализации этих переменных в промежуточном файле
Реэкспорт экспорта по умолчанию

Чтобы реэкспортировать экспорт по умолчанию, нужно написать export {default as User} или export {default} для реэкспорта по умолчанию.

Реэкспорт именованных экспортов и экспортов по умолчанию одновременно:

export {default as User, sayHi} from './user.js';

Или используя две инструкции:

export * from './user.js'; // реэкспортирует только именованные экспорты export {default} from './user.js';

Динамические импорты

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

Использовать его можно динамически в любом месте кода:

let modulePath = prompt("Какой модуль загружать?"); import(modulePath) .then(obj => <объект модуля>) .catch(err => <ошибка загрузки, например если нет такого модуля>)

Используя async/await: let module = await import(modulePath).

let {sayHi, default: User} = await import('./first.js'); sayHi(); let user = new User("John");

Динамический импорт работает в обычных скриптах, он не требует указания type="module".

Разное

Proxy и Reflect

Особый, «экзотический» объект Proxy не имеет собственных свойств. Он «оборачивается» вокруг другого объекта и может перехватывать и обрабатывать разные действия с ним.

let proxy = new Proxy(target, handler);

target – это объект, для которого нужно сделать прокси, может быть чем угодно, включая функции.

handler – конфигурация прокси - объект с «ловушками» («traps»), то есть методами, которые перехватывают разные операции.

При операциях над proxy, если в handler имеется соответствующая «ловушка», она срабатывает, и прокси имеет возможность по-своему обработать её, иначе операция будет совершена над оригинальным объектом target.

let target = {}; let proxy = new Proxy(target, {}); // прокси без ловушек proxy.test = 5; console.log(target.test); // 5 console.log(proxy.test); // 5 for (let key in proxy) console.log(key); // test

Для большинства действий с объектами в спецификации JavaScript есть так называемый «внутренний метод», который на самом низком уровне описывает, как его выполнять.

Например, [[Get]] – внутренний метод для чтения свойства, [[Set]] – для записи и тд.

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

Ловушки Proxy

JavaScript налагает некоторые условия – инварианты на реализацию внутренних методов и ловушек.

[[Set]] должен возвращать truthy, если значение было успешно записано, иначе falsish.

[[Delete]] должен возвращать truthy, если значение было успешно удалено, иначе falsish.

[[GetPrototypeOf]] должен возвращать то же значение, что и [[GetPrototypeOf]], применённый к оригинальному объекту (чтение прототипа объекта прокси всегда должно возвращать прототип оригинального объекта).

Ловушки могут перехватывать вызовы этих методов, но должны выполнять указанные условия.

Инварианты гарантируют корректное и последовательное поведение конструкций и методов языка.

Значение по умолчанию с ловушкой «get»

get(target, property, receiver) - метод для перехвата операции чтения.

target – оригинальный объект, который передавался первым аргументом в конструктор new Proxy.

property – имя свойства.

receiver – обычно сам объект прокси (или наследующий от него объект). Аргумент имеет значение, только если свойство – геттер.

Например, есть объект-словарь с фразами на английском и их переводом на испанский.

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

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

let dictionary = { 'Hello': 'Hola', 'Bye': 'Adiós' }; dictionary = new Proxy(dictionary, { get(target, prop) { if (prop in target) return target[prop]; return prop; } }); console.log( dictionary['Hello'] ); // Hola console.log( dictionary['Welcome'] ); // Welcome

Валидация с ловушкой «set»

set(target, property, value, receiver) - ловушка, срабатывающая при записи свойства.

target – оригинальный объект, который передавался первым аргументом в конструктор new Proxy.

property – имя свойства.

value – значение свойства.

receiver – обычно сам объект прокси (или наследующий от него объект). Аргумент имеет значение, только если свойство – сеттер.

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

let numbers = []; numbers = new Proxy(numbers, { set(target, prop, value) { if (typeof value === 'number') { target[prop] = value; return true; } } }); numbers.push(50); // добавилось успешно numbers.push("тест"); // TypeError: 'set' on proxy: trap returned falsish for property '2'

Встроенная функциональность массива по-прежнему работает. Методы массива внутри себя используют операцию [[Set]], которая перехватывается прокси. Таким образом, код остаётся чистым и прозрачным.

Если забыть вернуть truthy в случае успешной записи свойства, это приведёт к ошибке TypeError.

Перебор при помощи «ownKeys» и «getOwnPropertyDescriptor»

Object.keys, цикл for..in и большинство других методов, работающих со списком свойств объекта, используют внутренний метод [[OwnPropertyKeys]] (перехватываемый ловушкой ownKeys) для их получения.

В примере ниже мы используем ловушку ownKeys, чтобы цикл for..in по объекту, равно как Object.keys и Object.values пропускали свойства, начинающиеся с подчёркивания _:

let user = { name: "Вася", age: 30, _password: "***" }; user = new Proxy(user, { ownKeys(target) { return Object.keys(target).filter(key => !key.startsWith('_')); } }); console.log( Object.keys(user) ); // ['name', 'age'] console.log( Object.values(user) ); // ['Вася', 30]

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

let user = {}; user = new Proxy(user, { ownKeys(target) { return ['a', 'b', 'c']; } }); console.log( Object.keys(user) ); // []

Причина такова: Object.keys возвращает только свойства с флагом enumerable. Для того, чтобы определить, есть ли этот флаг, он для каждого свойства вызывает внутренний метод [[GetOwnProperty]], который получает его дескриптор. В данном случае свойство отсутствует, его дескриптор пуст, флага enumerable нет, поэтому оно пропускается.

Чтобы Object.keys возвращал свойство, нужно либо чтобы свойство в объекте физически было (с флагом enumerable), либо перехватить вызовы [[GetOwnProperty]] (с помощью ловушки getOwnPropertyDescriptor), и там вернуть дескриптор с enumerable: true.

let user = {}; user = new Proxy(user, { ownKeys(target) { // вызывается 1 раз для получения списка свойств return ['a', 'b', 'c']; }, getOwnPropertyDescriptor(target, prop) { // вызывается для каждого свойства return { enumerable: true, configurable: true, }; } }); console.log( Object.keys(user) ); // ['a', 'b', 'c']

Защищённые свойства с ловушкой «deleteProperty» и другими

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

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

let user = { name: "Вася", _password: "***", }; user = new Proxy(user, { get(target, prop) { if (prop.startsWith('_')) throw new Error('Отказано из [[Get]]'); let value = target[prop]; return typeof value === 'function' ? value.bind(target) : value; }, set(target, prop, value) { if (prop.startsWith('_')) throw new Error('Отказано из [[Set]]'); target[prop] = value; return true; }, deleteProperty(target, prop) { if (prop.startsWith('_')) throw new Error('Отказано из [[Delete]]'); delete target[prop]; return true; }, ownKeys(target) { return Object.keys(target).filter(key => !key.startsWith('_')); } }); console.log(user._password); // Error: Отказано из [[Get]] user._password = "test"; // Error: Отказано из [[Set]] delete user._password; // Error: Отказано из [[Delete]] for (let key in user) console.log(key); // name

Метод самого объекта, например user.checkPassword(), должен иметь доступ к свойству _password:

checkPassword() { return this._password === '***' }

Поэтому он привязывается к оригинальному объекту target: value.bind(target). Дальнейшие вызовы будут происходить без всяких ловушек.

Такое решение обычно работает, но, поскольку, метод может передать оригинальный объект куда-то ещё, возможна путаница: где изначальный объект, а где – проксированный. К тому же, объект может проксироваться несколько раз для добавления новых возможностей, и если передавать методу исходный, то могут быть неожиданности.

«В диапазоне» с ловушкой «has»

Предположим, есть объект range, описывающий диапазон:

let range = { start: 1, end: 10 };

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

range = new Proxy(range, { has(target, prop) { return prop >= target.start && prop <= target.end; } }); console.log(2 in range); // true

Оборачиваем функции: «apply»

Можно оборачивать в прокси и функции.

Ловушка apply(target, thisArg, args) активируется при вызове прокси как функции:

target – оригинальный объект (функция).

thisArg – это контекст this.

args – список аргументов.

Вспомним декоратор delay(f, ms):

function delay(f, ms) { return function() { setTimeout(() => f.apply(this, arguments), ms); }; } function sayHi(user) { console.log(`Привет, ${user}!`); } sayHi = delay(sayHi, 2000); sayHi("Вася"); // Привет, Вася! (через 2 секунды)

Функция-обёртка вызывает нужную функцию с указанной задержкой. Но она не перенаправляет операции чтения/записи свойства и другие. После обёртывания доступ к свойствам оригинальной функции, таким как name, length, и другим, будет потерян.

console.log(sayHi.length); // 1 sayHi = delay(sayHi, 2000); console.log(sayHi.length); // 0

Используем прокси вместо функции-обёртки:

function delay(f, ms) { return new Proxy(f, { apply(target, thisArg, args) { setTimeout(() => target.apply(thisArg, args), ms); } }); } function sayHi(user) { console.log(`Привет, ${user}!`); } sayHi = delay(sayHi, 2000); console.log(sayHi.length); // 1 sayHi("Вася"); // Привет, Вася! (через 2 секунды)

Теперь не только вызовы, но и другие операции на прокси перенаправляются к оригинальной функции.

Reflect

Reflect – встроенный объект, упрощающий создание прокси.

Reflect делает возможным обращение к внутренним методам, таким как [[Get]], [[Set]] напрямую. Его методы – минимальные обёртки вокруг внутренних методов.

Методы Reflect
let user = {}; Reflect.set(user, 'name', 'Вася'); console.log(user.name); // Вася

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

Можно использовать Reflect, чтобы перенаправить операцию на исходный объект.

В этом примере обе ловушки get и set прозрачно (как будто их нет) перенаправляют операции чтения и записи на объект, при этом выводя сообщение:

let user = { name: "Вася" }; user = new Proxy(user, { get(target, prop, receiver) { console.log(`GET ${prop}`); return Reflect.get(target, prop, receiver); }, set(target, prop, val, receiver) { console.log(`SET ${prop}=${val}`); return Reflect.set(target, prop, val, receiver); } }); let name = user.name; // GET name user.name = "Петя"; // SET name=Петя

Reflect.get читает свойство объекта.

Reflect.set записывает свойство и возвращает true при успехе, иначе false.

То есть, если ловушка хочет перенаправить вызов на объект, достаточно вызвать Reflect.<метод> с теми же аргументами.

Прокси для геттера

Допустим, есть объект user со свойством _name и геттером для него. Сделаем вокруг user прокси. Ловушка get «прозрачная», она возвращает свойство исходного объекта и больше ничего не делает.

Если унаследовать от проксированного user объект admin, увидим, что тот ведёт себя некорректно:

let user = { _name: "Гость", get name() { return this._name; } }; let userProxy = new Proxy(user, { get(target, prop, receiver) { return target[prop]; } }); let admin = { __proto__: userProxy, _name: "Админ" }; console.log(admin.name); // Гость

Обращение к свойству admin.name должно возвращать строку "Админ", а выводит "Гость"!

Проблема в прокси. При чтении admin.name, так как в объекте admin нет свойства name, оно ищется в прототипе. Прототипом является прокси userProxy. При чтении из прокси свойства name срабатывает ловушка get и возвращает его из исходного объекта как target[prop]. Вызов target[prop], если prop – это геттер, запускает его код в контексте this=target.

Для исправления таких ситуаций и нужен receiver. В нём хранится ссылка на правильный контекст this, который нужно передать геттеру. Сделать это позволяет Reflect.get.

let user = { _name: "Гость", get name() { return this._name; } }; let userProxy = new Proxy(user, { get(target, prop, receiver) { return Reflect.get(...arguments); } }); let admin = { __proto__: userProxy, _name: "Админ" }; console.log(admin.name); // Админ

Сейчас receiver, содержащий ссылку на корректный this (то есть на admin), передаётся геттеру посредством Reflect.get.

return Reflect... даёт простую и безопасную возможность перенаправить операцию на оригинальный объект и при этом предохраняет от возможных ошибок, связанных с этим действием.

Ограничения прокси

Прокси – уникальное средство для настройки поведения объектов на самом низком уровне. Но есть некоторые ограничения.

Встроенные объекты: внутренние слоты

Многие встроенные объекты, например Map, Set, Date, Promise и другие, за исключением Array, используют так называемые «внутренние слоты».

Например, Map хранит элементы во внутреннем слоте [[MapData]]. Встроенные методы обращаются к слотам напрямую, не через [[Get]]/[[Set]]. Таким образом, прокси не может перехватить их.

Если встроенный объект проксируется, то в прокси не будет этих «внутренних слотов» и попытка вызвать на таком прокси встроенный метод приведёт к ошибке.

let map = new Map(); let proxy = new Proxy(map, {}); proxy.set('test', 1); // TypeError: Method Map.prototype.set called on incompatible receiver #<Map>

Встроенный метод Map.prototype.set пытается получить доступ к своему внутреннему свойству this.[[MapData]], но так как this=proxy, то не может его найти.

К счастью, есть способ исправить это:

let map = new Map(); let proxy = new Proxy(map, { get(target, prop, receiver) { let value = Reflect.get(...arguments); return typeof value == 'function' ? value.bind(target) : value; } }); proxy.set('test', 1); console.log(proxy.get('test')); // 1

Всё сработало, потому что get привязывает свойства-функции, такие как map.set, к оригинальному объекту map. Таким образом, когда реализация метода set попытается получить доступ к внутреннему слоту this.[[MapData]], то всё пройдёт благополучно.

Приватные поля

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

class User { #name = "Гость"; getName() { return this.#name; } } let user = new User(); user = new Proxy(user, {}); console.log(user.getName()); // TypeError: Cannot read private member #name from an object whose class did not declare it

В вызове getName() значением this является проксированный user, в котором нет внутреннего слота с приватными полями.

Решением, как и в предыдущем случае, является привязка контекста к методу.

Прокси != оригинальный объект

Прокси не перехватывают проверку на строгое равенство ===.

Так что все операции и встроенные классы, которые используют строгую проверку объектов на равенство, отличат прокси от изначального объекта.

Отключаемые прокси

Отключаемый (revocable) прокси – это прокси, который может быть отключён вызовом специальной функции.

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

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

let {proxy, revoke} = Proxy.revocable(target, handler)

Вызов возвращает объект с proxy и функцией revoke, которая отключает его.

let object = { data: "Важные данные" }; let {proxy, revoke} = Proxy.revocable(object, {}); console.log(proxy.data); // Важные данные revoke(); console.log(proxy.data); // TypeError: Cannot perform 'get' on a proxy that has been revoked

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

Мы можем хранить функцию revoke в WeakMap, чтобы легко найти её по объекту прокси:

let revokes = new WeakMap(); let object = { data: "Важные данные" }; let {proxy, revoke} = Proxy.revocable(object, {}); revokes.set(proxy, revoke); revoke = revokes.get(proxy); revoke(); console.log(proxy.data); // TypeError...

Если прокси объект становится недостижимым, то WeakMap позволяет сборщику мусора удалить его из памяти вместе с соответствующей функцией revoke, которая в этом случае больше не нужна.

Побитовые операторы

Побитовые операторы работают следующим образом:

  1. Операнды преобразуются в 32-битные целые числа, представленные последовательностью битов. Дробная часть отбрасывается.
  2. Каждый бит в первом операнде рассматривается вместе с соответствующим битом второго операнда. Оператор применяется к каждой паре бит.
  3. Получившаяся в результате последовательность бит интерпретируется как обычное число.

Все побитовые операторы:

  • a & b - побитовое И. Результат a & b равен единице только когда оба бита a и b равны единице.
  • a | b - побитовое ИЛИ. Результат a | b равен 1, если хотя бы один бит из a и b равен 1.
  • a ^ b - побитовое исключающее ИЛИ. a ^ b равно 1, если a и b противоположны.
  • ~a - побитовое НЕ. Заменяет каждый бит операнда на противоположный.
  • a << b - левый сдвиг. Сдвигает двоичное представление a на b битов влево, добавляя справа нули.
  • a >> b - правый сдвиг. Сдвигает двоичное представление a на b битов вправо, отбрасывая сдвигаемые биты. Знак числа сохраняется.
  • a >>> b - сдвиг вправо без знака. Это правый сдвиг с заполнением нулями слева.

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

Побитовые операторы ^, &, | имеют низкий приоритет и выполняются после сравнений ==.

Отрицательные числа в двоичном представлении

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

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

Применение побитовых операторов

Округление

Так как битовые операции отбрасывают десятичную часть, их можно использовать для округления вместо Math.floor:

console.log( ~~12.845 ); // 12

Подойдёт и исключающее ИЛИ (^) с нулём:

console.log( 12.845 ^ 0 ); // 12
Исключающее ИЛИ ^

Исключающее ИЛИ можно использовать для шифрования, так как эта операция полностью обратима. То есть, выполняется формула a ^ b ^ b === a.

console.log(4921 ^ 708 ^ 708); // 4921
Побитовое НЕ ~

Обращение (инвертирование) битов – это побитовое НЕ (~). То есть, при таком формате представления числа -n = ~n + 1. Или, если перенести единицу:

~n = -(n+1)

Побитовое НЕ ~ позволяет осуществлять проверку на -1, так как ~n === 0 только если n === -1.

Проверка на -1 пригождается, например, при поиске символа в строке:

let str = 'Проверка'; if (~str.indexOf('верка')) { // str.indexOf("верка") !== -1 console.log('найдено'); }
Битовые маски

Битовое представление числа иногда используется для упаковки нескольких значений («флагов») в одно. Это экономит память и позволяет проверять наличие комбинации флагов одним оператором &.

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

Как правило, доступы задаются в виде констант:

const ACCESS_ADMIN = 1; // 00001 const ACCESS_GOODS_EDIT = 2; // 00010 const ACCESS_GOODS_VIEW = 4; // 00100 const ACCESS_ARTICLE_EDIT = 8; // 01000 const ACCESS_ARTICLE_VIEW = 16; // 10000

Из этих констант получить нужную комбинацию доступов можно при помощи операции |.

const guest = ACCESS_ARTICLE_VIEW | ACCESS_GOODS_VIEW; // 10100 const editor = guest | ACCESS_ARTICLE_EDIT | ACCESS_GOODS_EDIT; // 11110 const admin = editor | ACCESS_ADMIN; // 11111

Теперь, чтобы понять, имеет ли guest нужный доступ, например управление правами – достаточно применить к нему побитовый оператор И (&) с соответствующей константой. Ненулевой результат будет означать, что доступ есть:

console.log(guest & ACCESS_ADMIN); // 0 console.log(editor & ACCESS_ARTICLE_EDIT); // 8

BigInt

BigInt – это специальный числовой тип, который предоставляет возможность работать с целыми числами произвольной длины.

Чтобы создать значение типа BigInt, необходимо добавить n в конец числового литерала или вызвать функцию BigInt, которая создаст число типа BigInt из переданного аргумента. Аргументом может быть число, строка и др.

console.log(BigInt('10') === 10n); // true

Математические операторы

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

console.log(8n / 3n); // 2n

Смешивать bigint и обычные числа нельзя.

1n + 2; // TypeError: Cannot mix BigInt and other types, use explicit conversions

Необходимо их явно конвертировать одним из способов:

console.log(1n + BigInt(2)); // 3n console.log(Number(1n) + 2); // 3

Операции сравнения

Сравнивать bigint и number числа можно:

console.log( 2n > 1 ); // true console.log( 1 == 1n ); // true

Логические операции

Логические операторы работают с bigint как с обычными числами:

console.log( 1n || 2 ); // 1n console.log( 0n || 2 ); // 2

Intl: интернационализация в JavaScript

Intl.Collator - умеет правильно сравнивать и сортировать строки.

Intl.DateTimeFormat - умеет форматировать дату и время в соответствии с нужным языком.

Intl.NumberFormat - умеет форматировать числа в соответствии с нужным языком.

Локаль

Локаль – первый аргумент всех методов, связанных с интернационализацией. Описывается строкой из трёх (обычно меньше) компонентов, которые разделяются дефисом:

  1. Код языка.
  2. Код способа записи.
  3. Код страны.

ru – русский язык, без уточнений.

en-GB – английский язык, используемый в Англии.

en-US – английский язык, используемый в США.

Если локаль не указана или undefined – берётся локаль по умолчанию, установленная в окружении (браузере).

Строки, Intl.Collator

let collator = new Intl.Collator([locales, [options]]) - создание

collator.compare(str1, str2) - метод для сравнения

  • 1 если str1 > str2
  • -1 если str1 < str2
  • 0 если str2 == str2

locales - локаль, одна или массив в порядке предпочтения.

options - объект с дополнительными настройками:

  • sensitivity – чувствительность к различию символов.
    • 'base'е == ё, а == А
    • 'accent'е != ё, а == А
    • 'case'е == ё, а != А
    • ['variant'] – е != ё, а != А
  • ignorePunctuation – игнорирование знаков пунктуации.
    • true – игнорировать знаки пунктуации и пробелы
    • [false] – знаки пунктуации имеют значение
  • numeric – применять численное сравнение.
    • true – применять (12 > 2)
    • [false] – игнорировать (12 < 2)
  • caseFirst – при сортировке определяет, прописные или строчные буквы идут первыми.
    • 'upper' – прописные
    • 'lower' – строчные
let collator = new Intl.Collator(undefined, { sensitivity: "accent" }); console.log( collator.compare("ЁжиК", "ёжик") ); // 0

Даты, Intl.DateTimeFormat

let formatter = new Intl.DateTimeFormat([locales, [options]]) - создание

let dateString = formatter.format(date) - форматирование

options - объект, который может иметь следующие свойства:

  • hour12 – двенадцатичасовой формат – true/false
  • weekday – обозначение дня недели – narrow/short/long
  • era – эра – narrow/short/long
  • year2-digit/numeric
  • month2-digit/numeric/narrow/short/long
  • day2-digit/numeric
  • hour2-digit/numeric
  • minute2-digit/numeric
  • second2-digit/numeric

Значения свойств:

'narrow' – одна заглавная буква.

'short' – две или три буквы.

'long' – целое слово.

'numeric' – число, может совпадать с 2-digit.

'2-digit' – две цифры.

let formatter = new Intl.DateTimeFormat('ru', { year: '2-digit', month: '2-digit', day: '2-digit', }); console.log( formatter.format( new Date() ) ); // 07.07.23

Числа, Intl.NumberFormat

let formatter = new Intl.NumberFormat([locales[, options]]) - создание

formatter.format(number) - форматирование

options - объект, который может иметь следующие свойства:

  • style – cтиль форматирования – [decimal], percent, currency
  • currency – алфавитный код валюты (RUB, USD и тд)
  • currencyDisplay – как отображать валюту – [symbol], code, name
  • useGrouping – разделение цифр по три – [true], false
  • minimumFractionDigits – минимальное число десятичных цифр – от 0 до 20
  • maximumFractionDigits – максимальное число десятичных цифр – от minimumFractionDigits до 20
  • minimumSignificantDigits – минимальное число значимых цифр – от 1 до 21
  • maximumSignificantDigits – максимальное число значимых цифр – от minimumSignificantDigits до 21
let formatter1 = new Intl.NumberFormat("ru", { maximumSignificantDigits: 3 }); console.log( formatter1.format(1234567890.123) ); // 1 230 000 000 let formatter2 = new Intl.NumberFormat("ru", { style: "currency", currency: "GBP" }); console.log( formatter2.format(1234.5) ); // 1 234,50 £

Методы в Date, String, Number

Все эти методы при запуске создают соответствующий объект Intl.* и передают ему опции, можно рассматривать их как укороченные варианты вызова.

str1.localeCompare(str2 [, locales [, options]]) - сравнивает строки с учетом локали.

date.toLocaleString([locales [, options]]) - форматирует дату с учетом локали.

date.toLocaleDateString([locales [, options]]) - имеет year, month, day по умолчанию.

date.toLocaleTimeString([locales [, options]]) - имеет hour, minute, second по умолчанию.

console.log( new Date().toLocaleTimeString() ); // 09:21:05

num.toLocaleString([locales [, options]]) - форматирует число с учетом локали.

Бинарные данные и файлы

ArrayBuffer, бинарные массивы

Базовый объект для работы с бинарными данными представляет собой ссылку на непрерывную область памяти фиксированной длины:

let buffer = new ArrayBuffer(16); // создаётся буфер длиной 16 байт console.log(buffer.byteLength); // 16

Для работы с ArrayBuffer нужен специальный объект, реализующий «представление» данных.

Uint8Array – представляет каждый байт в ArrayBuffer как отдельное число («8-битное целое без знака»). Возможные значения - от 0 до 255.

Uint16Array – представляет каждые 2 байта как целое число. Значения от 0 до 65535.

Uint32Array – представляет каждые 4 байта как целое число. Значения от 0 до 4294967295.

Float64Array – представляет каждые 8 байт как число с плавающей точкой. Значения от 5e-324 до 1.8e308.

Uint8ClampedArray - отличается от Uint8Array тем, что значения больше 255 представлены числом 255, отрицательные - нулем.

Есть и другие: BigUint64Array (числа от 0 до 2^64 - 1), BigInt64Array, Float32Array, Int8Array (числа от -128 до -127), Int16Array, Int32Array.

При выходе за пределы значений (если это не Uint8ClamedArray) лишние старшие биты будут отброшены:

let buffer = new ArrayBuffer(16); let uint8array = new Uint8Array(buffer); console.log((256).toString(2)); // 100000000 uint8array[0] = 256; console.log(uint8array[0]); // 0 console.log(Uint8Array.BYTES_PER_ELEMENT); // 1 console.log(uint8array.length); // 16 console.log(uint8array.byteLength); // 16
TypedArray

Это общий термин для всех таких представлений (Uint8Array, Uint32Array и т.д.).

Есть 5 вариантов создания типизированных массивов:

new TypedArray(buffer, [byteOffset], [length]) - типизированный массив длиной buffer.byteLength / TypedArray.BYTES_PER_ELEMENT.

new TypedArray(arrayLike) - пустой типизированный массив длиной arrayLike.length с содержимым псевдомассива arrayLike.

new TypedArray(typedArray) - типизированный массив той же длины с содержимым typedArray. При необходимости значения приводятся к новому типу.

new TypedArray(length) - типизированный массив длины length.

new TypedArray() - пустой типизированный массив.

ArrayBuffer создаётся автоматически во всех случаях, кроме первого. Для доступа к ArrayBuffer в TypedArray есть следующие свойства:

buffer – ссылка на объект ArrayBuffer.

byteLength – размер содержимого ArrayBuffer в байтах.

Методы TypedArray

Типизированные массивы TypedArray, имеют те же методы, что и массивы Array. Исключения: splice, concat.

Имеются 2 дополнительных метода:

arr.set(fromArr, [offset]) - копирует все элементы из fromArr в arr, начиная с позиции offset (0 по умолчанию).

arr.subarray([begin, end]) - создаёт новое представление того же типа для данных, начиная с позиции begin до (не включая) end.

DataView

DataView – это специальное нетипизированное представление данных из ArrayBuffer. Оно позволяет обращаться к данным на любой позиции и в любом формате. Синтаксис:

new DataView(buffer, [byteOffset], [byteLength])

buffer – ссылка на бинарные данные ArrayBuffer.

byteOffset – начальная позиция данных для представления (по умолчанию 0).

byteLength – длина данных (в байтах), используемых в представлении (по умолчанию – до конца buffer).

let uint8Array = new Uint8Array([255, 255, 255, 255]); let dataView = new DataView(uint8Array.buffer); console.log( dataView.getUint8(0) ); // 255 console.log( dataView.getUint16(0) ); // 65535 console.log( dataView.getUint32(0) ); // 4294967295 dataView.setUint32(0, 0); // во все 4 байта записаны нули console.log(uint8Array); // [0, 0, 0, 0];

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

TextDecoder и TextEncoder

let decoder = new TextDecoder([label], [options]) - объект, позволяющий декодировать данные из бинарного буфера в обычную строку.

label – кодировка (utf-8 по умолчанию).

options – объект с дополнительными настройками.

  • fatal – [false] / true. Если true, генерируется ошибка для невалидных (не декодируемых) символов, иначе (по умолчанию) они заменяются символом \uFFFD.
  • ignoreBOM – [false] / true. Если true, тогда игнорируется BOM (дополнительный признак, определяющий порядок следования байтов).
let str = decoder.decode([input], [options]);

input – бинарный буфер BufferSource (бинарные данные в любом виде) для декодирования.

options – объект с дополнительными настройками.

  • stream – [false] / true. Если true, TextDecoder запоминает символ, на котором остановился процесс, и декодирует его со следующим фрагментом.
let uint8Array = new Uint8Array([72, 101, 108, 108, 111]); console.log( new TextDecoder().decode(uint8Array) ); // Hello

let encoder = new TextEncoder() - объект, кодирующий строку в бинарный массив. Поддерживается только кодировка utf-8. Имеет два метода:

encode(str) – возвращает бинарный массив Uint8Array, содержащий закодированную строку.

encodeInto(str, destination) – кодирует строку str и помещает её в destination, который должен быть экземпляром Uint8Array.

let encoder = new TextEncoder(); console.log(encoder.encode("Hello")); // [72, 101, 108, 108, 111]

Blob

new Blob(blobParts, options) - создание объекта Blob.

blobParts – массив значений Blob/BufferSource/String.

options – необязательный объект с дополнительными настройками:

  • type – тип объекта, обычно MIME-тип, например, image/png
  • endings – если указан, изменяет окончания строк создаваемого Blob
let blob1 = new Blob(["…"], {type: 'text/html'}); let hello = new Uint8Array([72, 101, 108, 108, 111]); let blob2 = new Blob([hello, ' ', 'world'], {type: 'text/plain'});

Данные в Blob неизменяемы (immutable), но можно делать срезы и создавать новый Blob на их основе.

blob.slice([byteStart], [byteEnd], [contentType]) - делает срез Blob.

byteStart – стартовая позиция байта, по умолчанию 0.

byteEnd – последний байт, по умолчанию до конца.

contentType – тип type создаваемого Blob-объекта, по умолчанию такой же, как и исходный.

Blob как URL

URL.createObjectURL(blob) - создает уникальный URL в формате blob.

Для каждого URL, сгенерированного через URL.createObjectURL, браузер сохраняет внутреннее соответствие URL → Blob.

URL.revokeObjectURL(url) - удаляет внутреннюю ссылку на объект, что позволяет (если нет другой ссылки) удалить его сборщику мусора, и очистить память.

Пример: при клике загружается динамически генерируемый blob (.txt) с текстом "Hello, world!".

HTML: <a download="hello.txt" href='#' id="link">Загрузить</a>

let blob = new Blob(["Hello, world!"], {type: 'text/plain'}); link.href = URL.createObjectURL(blob);

Пример: автозагрузка ссылка с эмуляцией клика.

let link = document.createElement('a'); link.download = 'hello.txt'; let blob = new Blob(['Hello, world!'], {type: 'text/plain'}); link.href = URL.createObjectURL(blob); link.click(); URL.revokeObjectURL(link.href);
Изображение в Blob

canvas-метод .toBlob(callback, format, quality) создаёт Blob и вызывает функцию callback при завершении.

Можно создать Blob для изображения, части изображения или даже создать скриншот страницы. Операции с изображениями выполняются через элемент <canvas>. Для отрисовки изображения (или его части) используется canvas.drawImage.

Пример: изображение копируется и загружается.

let img = document.querySelector('img'); let canvas = document.createElement('canvas'); canvas.width = img.clientWidth; canvas.height = img.clientHeight; let context = canvas.getContext('2d'); context.drawImage(img, 0, 0); canvas.toBlob(function(blob) { let link = document.createElement('a'); link.download = 'example.png'; link.href = URL.createObjectURL(blob); link.click(); URL.revokeObjectURL(link.href); }, 'image/png');

Или async/await вместо колбэка:

let blob = await new Promise(resolve => canvasElem.toBlob(resolve, 'image/png'));
Из Blob в ArrayBuffer

Если нужна производительная низкоуровневая обработка, можно использовать ArrayBuffer из FileReader:

let fileReader = new FileReader(); fileReader.readAsArrayBuffer(blob); fileReader.onload = function(event) { let arrayBuffer = fileReader.result; };

Fetch

let promise = fetch(url, [options]) - современный метод сделать сетевой запрос.

url – URL для отправки запроса.

options – дополнительные параметры: метод, заголовки и так далее. Если не указаны, это простой GET-запрос, скачивающий содержимое по адресу url.

Результатом запроса становится объект встроенного класса Response.

response.status – код статуса HTTP-запроса, например 200.

response.ok – логическое значение: будет true, если код HTTP-статуса в диапазоне 200-299.

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

response.text() – читает ответ и возвращает как обычный текст.

response.json() – декодирует ответ в формате JSON.

response.formData() – возвращает ответ как объект FormData.

response.blob() – возвращает объект как Blob.

response.arrayBuffer() – возвращает ответ как ArrayBuffer.

fetch('https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits') .then(response => response.json()) .then(commits => console.log(commits[0].author.login)); // iliakan
Заголовки ответа

Заголовки ответа хранятся в похожем на Map объекте response.headers.

fetch('https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits') .then(response => { console.log( Object.fromEntries(response.headers) ); // {cache-control: 'public, max-age=60, s-maxage=60', content-type: ... console.log( response.headers.get('content-type') ); // application/json; charset=utf-8 })
Заголовки запроса

Для установки заголовка запроса в fetch можно использовать опцию headers. Она содержит объект с исходящими заголовками.

let response = fetch(protectedUrl, { headers: { Authentication: 'secret' } });

Некоторые HTTP-заголовки нельзя установить. Они обеспечивают достоверность данных и корректную работу протокола HTTP, поэтому контролируются исключительно браузером.

Для отправки POST-запроса или запроса с другим методом, необходимо использовать fetch параметры.

method – HTTP метод, например POST.

body – тело запроса, одно из списка:

  • строка (например, в формате JSON)
  • объект FormData для отправки данных как form/multipart
  • Blob/BufferSource для отправки бинарных данных
  • URLSearchParams для отправки данных в кодировке x-www-form-urlencoded, используется редко
let user = { name: 'John', surname: 'Smith' }; let response = await fetch('/article/fetch/post/user', { method: 'POST', headers: { 'Content-Type': 'application/json;charset=utf-8' }, body: JSON.stringify(user) }); let result = await response.json(); console.log(result.message);

Поскольку тело запроса body – строка, заголовок Content-Type по умолчанию будет text/plain;charset=UTF-8. Но, так как мы посылаем JSON, нужно поставить правильный Content-Type для JSON.