Hello, world!


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


В первой части мы поговорим о возможностях JS, во второй — о возможностях TS.


Это первая часть.


Обратите внимание: название почти каждой возможности — это также ссылка на соответствующий раздел MDN.


Руководства, шпаргалки, вопросы и другие материалы по JavaScript, TypeScript, React, Next.js, Node.js, Express, Prisma, GraphQL, Docker и другим технологиям, а также Блог по веб-разработке.


ECMAScript


До ES2020 (возможности, о которых многие не знают)


Теггированые шаблонные литералы / Tagged template literals: если после названия функции указать шаблонный литерал, то функция получит части шаблонных литералов и значения шаблона, например:


// Предположим, что мы хотим форматировать число, содержащееся в строке
function formatNumbers(strings: TemplateStringsArray, number: number): string {
  return strings[0] + number.toFixed(2) + strings[1];
}
console.log(formatNumbers`This is the value: ${0}, it's important.`);
// This is the value: 0.00, it's important.

// Или мы хотим "переводить" (в данном случае в нижний регистр) ключи переводов, содержащиеся в строке
function translateKey(key: string): string {
  return key.toLocaleLowerCase();
}
function translate(strings: TemplateStringsArray, ...expressions: string[]): string {
  return strings.reduce((accumulator, currentValue, index) => accumulator + currentValue + translateKey(expressions[index] ?? ''), '');
}
console.log(translate`Hello, this is ${'NAME'} to say ${'MESSAGE'}.`);
// Hello, this is name to say message.

Символы / Symbols: примитивы, представляющие собой гарантировано уникальные значения (Symbol("foo") === Symbol("foo"); // false), которые часто используются в качестве ключей объектов во избежание коллизий с другими ключами, например:


const obj: { [index: string]: string } = {};

const symbolA = Symbol('a');
const symbolB = Symbol.for('b');

console.log(symbolA.description); // "a"

obj[symbolA] = 'a';
obj[symbolB] = 'b';
obj['c'] = 'c';
obj.d = 'd';

console.log(obj[symbolA]); // "a"
console.log(obj[symbolB]); // "b"
// Ключ не может быть другим символов или быть не символом
console.log(obj[Symbol('a')]); // undefined
console.log(obj['a']); // undefined

// Ключи-символы не "перечисляются" (enumerated) при использовании `for/in`.
for (const i in obj) {
  console.log(i); // "c", "d"
}

ES2020


Оператор опциональной последовательности / Optional chaining (?.): обычно используется для безопасного доступа к свойству потенциально несуществующего/неопределенного (undefined) объекта, но также может использоваться для безопасного доступа по индексу к элементу потенциально несуществующего массива и вызова потенциально несуществующей функции, например:


// Раньше:
// Если у нас был потенциально несуществующий объект,
// мы не могли легко получить доступ к его свойству
const object: { name: string } | undefined = Math.random() > 0.5 ? undefined : { name: 'test' };
const value = object.name; // TypeError: 'object' is possibly 'undefined'

// Мы должны были проверять "определенность" объекта
// Это ухудшало читаемость кода и становилось сложным в случае вложенных объектов
const objectOld: { name: string } | undefined = Math.random() > 0.5 ? undefined : { name: 'test' };
const valueOld = objectOld ? objectOld.name : undefined;

// Сейчас:
// Мы можем использовать оператор опциональной последовательности
// для безопасного доступа к свойству потенциально несуществующего объекта
const objectNew: { name: string } | undefined = Math.random() > 0.5 ? undefined : { name: 'test' };
const valueNew = objectNew?.name;

// Его также можно использовать для безопасного доступа по индексу и вызова функции
const array: string[] | undefined = Math.random() > 0.5 ? undefined : ['test'];
const item = array?.[0];
const func: (() => string) | undefined = Math.random() > 0.5 ? undefined : () => 'test';
const result = func?.();

Оператор нулевого слияния / Nullish coalescing operator (??): является альтернативой оператора ||. Отличие между этими операторами состоит в том, что || применяется ко всем ложным значениям, а ?? — только к undefined и null, например:


const value: string | undefined = Math.random() > 0.5 ? undefined : 'test';

// Раньше:
// Для условного присвоения значения переменной мы использовали оператор `||`
const anotherValue = value || 'hello';
console.log(anotherValue); // "test" или "hello"

// Это не всегда работало хорошо
const incorrectValue = '' || 'incorrect';
console.log(incorrectValue); // всегда "incorrect"
const anotherIncorrectValue = 0 || 'incorrect';
console.log(anotherIncorrectValue); // всегда "incorrect"

// Сейчас:
// Оператор нулевого слияния применяется только в отношении `undefined` и `null`
const newValue = value ?? 'hello';
console.log(newValue) // "test" или "hello"

// Ложные значения не заменяются
const correctValue = '' ?? 'incorrect';
console.log(correctValue); // всегда ""
const anotherCorrectValue = 0 ?? 'incorrect';
console.log(anotherCorrectValue); // всегда 0

import(): функциональное выражение динамического импорта — как import ... from '...', но во время выполнения кода и с возможностью использования переменных:


let importModule;
if (shouldImport) {
  importModule = await import('./module.mjs');
}

String.matchAll(): возвращает несколько совпадений регулярного выражения, включая группы захвата (capture groups), без использования циклов:


const stringVar = 'testhello,testagain,';

// Раньше:
// Получаем совпадения, но без групп захвата
console.log(stringVar.match(/test([\w]+?),/g));
// ["testhello,", "testagain,"]

// Получаем одно совпадение с группой захвата
const singleMatch = stringVar.match(/test([\w]+?),/);
if (singleMatch) {
  console.log(singleMatch[0]); // "testhello,"
  console.log(singleMatch[1]); // "hello"
}

// Получаем все совпадения с группами захвата (метод `exec` запоминает индекс последнего совпадения)
// `execMatch` должен быть определен за пределами цикла (для сохранения состояния) и быть глобальным (флаг `g`),
// иначе цикл будет бесконечным
const regex = /test([\w]+?),/g;
let execMatch;
while ((execMatch = regex.exec(stringVar)) !== null) {
  console.log(execMatch[0]); // "testhello,", "testagain,"
  console.log(execMatch[1]); // "hello", "again"
}

// Сейчас:
// Регулярное выражение должно быть глобальным
const matchesIterator = stringVar.matchAll(/test([\w]+?),/g);
// Итерация или преобразование в массив (Array.from()), доступ по индексу запрещен
for (const match of matchesIterator) {
  console.log(match[0]); // "testhello,", "testagain,"
  console.log(match[1]); // "hello", "again"
}

Promise.allSettled(): похож на Promise.all(), но ожидает (любого) разрешения всех промисов, а не возвращает первую ошибку, что облегчает обработку ошибок:


async function success1() { return 'a' };
async function success2() { return 'b' };
async function fail1() { throw 'fail 1' };
async function fail2() { throw 'fail 2' };

// Раньше:
console.log(await Promise.all([success1(), success2()])); // ["a", "b"]
// но:
try {
  await Promise.all([success1(), success2(), fail1(), fail2()]);
} catch (e) {
  console.log(e); // "fail 1"
}
// Мы перехватываем одну ошибку и не имеем доступа к "успешным" значениям

// Фикс (плохой код):
console.log(await Promise.all([ // ["a", "b", undefined, undefined]
  success1().catch(e => { console.log(e); }),
  success2().catch(e => { console.log(e); }),
  fail1().catch(e => { console.log(e); }), // "fail 1"
  fail2().catch(e => { console.log(e); })])); // "fail 2"

// Сейчас:
const results = await Promise.allSettled([success1(), success2(), fail1(), fail2()]);
const successfulResults = results
  .filter(result => result.status === 'fulfilled')
  .map(result => (result as PromiseFulfilledResult<string>).value);
console.log(successfulResults); // ["a", "b"]
results.filter(result => result.status === 'rejected').forEach(error => {
  console.log((error as PromiseRejectedResult).reason); // "fail 1", "fail 2"
});
// или:
for (const result of results) {
  if (result.status === 'fulfilled') {
    console.log(result.value); // "a", "b"
  } else if (result.status === 'rejected') {
    console.log(result.reason); // "fail 1", "fail 2"
  }
}

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


// Раньше:
// JS хранит числа как числа с плавающей запятой, что всегда влечет небольшую потерю точности,
// которая существенно возрастает после определенного числа
const maxSafeInteger = 9007199254740991;
console.log(maxSafeInteger === Number.MAX_SAFE_INTEGER); // true

// БОльшие числа сравниваются некорректно
console.log(Number.MAX_SAFE_INTEGER + 1 === Number.MAX_SAFE_INTEGER + 2); // true

// Сейчас:
// Тип данных `BigInt` теоретически позволяет хранить и оперировать неопределенно большими (целыми) числами
const maxSafeIntegerPreviously = 9007199254740991n;
console.log(maxSafeIntegerPreviously); // 9007199254740991

const anotherWay = BigInt(9007199254740991);
console.log(anotherWay); // 9007199254740991

// Обратите внимание: в конструктор нельзя передавать числа, которые больше чем MAX_SAFE_INTEGER
const incorrect = BigInt(9007199254740992);
console.log(incorrect); // 9007199254740992
const incorrectAgain = BigInt(9007199254740993);
console.log(incorrectAgain); // 9007199254740992

// Но можно передавать строки или использовать другой синтаксис
const correct = BigInt('9007199254740993');
console.log(correct); // 9007199254740993
const correctAgain = 9007199254740993n;
console.log(correctAgain); // 9007199254740993

// Другие форматы также могут передаваться в виде строк
const hex = BigInt('0x1fffffffffffff');
console.log(hex); // 9007199254740991
const octal = BigInt('0o377777777777777777');
console.log(octal); // 9007199254740991
const binary = BigInt('0b11111111111111111111111111111111111111111111111111111');
console.log(binary); // 9007199254740991

// Большинство арифметических операций работает, как ожидается,
// если другой операнд также является `BigInt`
// Все операции возвращают `BigInt`
const addition = maxSafeIntegerPreviously + 2n;
console.log(addition); // 9007199254740993

const multiplication = maxSafeIntegerPreviously * 2n;
console.log(multiplication); // 18014398509481982

const subtraction = multiplication - 10n;
console.log(subtraction); // 18014398509481972

const modulo = multiplication % 10n;
console.log(modulo); // 2

const exponentiation = 2n ** 54n;
console.log(exponentiation); // 18014398509481984

const exponentiationAgain = 2n^54n;
console.log(exponentiationAgain); // 18014398509481984

const negative = exponentiation * -1n;
console.log(negative); // -18014398509481984

// Деление работает немного иначе, поскольку `BigInt` может хранить только целые числа
const division = multiplication / 2n;
console.log(division); // 9007199254740991
// Для целых чисел, которые делятся без остатка, это работает хорошо

// Иначе результат округляется до целого числа в меньшую сторону
const divisionAgain = 5n / 2n;
console.log(divisionAgain); // 2

// При проверке на равенство с помощью оператора `==`
// `BigInt` приводится к обычному числу
console.log(0n === 0); // false
console.log(0n == 0); // true

// Сравнение работает как ожидается
console.log(1n < 2); // true
console.log(2n > 1); // true
console.log(2 > 2); // false
console.log(2n > 2); // false
console.log(2n >= 2); // true

// Тип
console.log(typeof 1n); // "bigint"

globalThis: предоставляет доступ к глобальным переменным, независимо от среды выполнения кода (браузер, Node.js и др.):


console.log(globalThis.Math); // объект `Math`

import.meta: в числе прочего, при использовании модулей ES, предоставляет доступ к URL текущего модуля:


console.log(import.meta.url); // "file://..."

export * as… from '...': позволяет с легкостью повторно экспортировать (re-export) дефолтные экспорты в качестве субмодулей:


export * as am from 'another-module'

import { am } from 'module'

ES2021


String.replaceAll(): заменяет все вхождения подстроки в строке, является альтернативой регулярного выражения с флагом g:


const testString = 'hello/greetings everyone/everybody';
// Раньше:
// Заменяет только первое вхождение
console.log(testString.replace('/', '|'));
// 'hello|greetings everyone/everybody'

// Заменяет все вхождения
// Регулярное выражение + экранирование + глобальный флаг
console.log(testString.replace(/\//g, '|'));
// 'hello|greetings everyone|everybody'

// Сейчас:
// Заменяет все вхождения
// Чище и быстрее
console.log(testString.replaceAll('/', '|'));
// 'hello|greetings everyone|everybody'

Promise.any(): возвращается первое "успешное" значение. Отклоняется только при отклонении всех промисов (в этом случае возвращается AggregateError), в отличие от Promise.race(), который отклоняется при отклонении любого промиса:


async function success1() { return 'a' };
async function success2() { return 'b' };
async function fail1() { throw 'fail 1' };
async function fail2() { throw 'fail 2' };

// Раньше:
console.log(await Promise.race([success1(), success2()])); // "a"
// но:
try {
  await Promise.race([fail1(), fail2(), success1(), success2()]);
} catch (e) {
  console.log(e); // "fail 1"
}
// Перехватываем одну ошибку и не имеем доступа к "успешным" значениям

// Фикс (плохой код):
console.log(await Promise.race([ // "a"
  fail1().catch(e => { console.log(e); }), // "fail 1"
  fail2().catch(e => { console.log(e); }), // "fail 2"
  success1().catch(e => { console.log(e); }),
  success2().catch(e => { console.log(e); })]));

// Сейчас:
console.log(await Promise.any([fail1(), fail2(), success1(), success2()])); // "a"
try {
  await Promise.any([fail1(), fail2()]);
} catch (e) {
  console.log(e); // [AggregateError]
  console.log(e.errors); // ["fail 1", "fail 2"]
}

Оператор присваивания нулевого слияния / Nullish coalescing assignment (??=): присваивает новое значение переменной только в том случае, когда текущим значением переменной является null или undefined:


let x1 = undefined;
let x2 = 'a';
const getNewValue = () => 'b';

x1 ??= 'b';
console.log(x1) // "b"

// Обратите внимание: `getNewValue()` не выполняется
x2 ??= getNewValue();
console.log(x1) // "a"

Оператор присваивания логического И / Logical and assignment (&&=): присваивает новое значение переменной только в том случае, когда текущим значением переменной является истинное значение:


let x1 = undefined;
let x2 = 'a';
const getNewValue = () => 'b';

x1 &&= getNewValue();
console.log(x1) // undefined

x2 &&= 'b';
console.log(x1) // "b"

Оператор присваивания логического ИЛИ / Logical or assignment (||=): присваивает новое значение переменной только в том случае, когда текущим значением переменной является ложное значение:


let x1 = undefined;
let x2 = 'a';
const getNewValue = () => 'b';

x1 ||= 'b';
console.log(x1) // "b"

x2 ||= getNewValue();
console.log(x1) // "a"

WeakRef: содержит "слабую" ссылку на объект. Слабая ссылка не препятствует уничтожению объекта сборщиком мусора:


const ref = new WeakRef(element);

// Получаем значение, если объект/элемент существует и не был уничтожен сборщиком мусора
const value = ref.deref;
console.log(value); // undefined
// Похоже, объекта больше нет

Разделители числовых литералов / Numeric literal separators: позволяет разделять числа для повышения читаемости, не влияет на функционал:


const int = 1_000_000_000;
const float = 1_000_000_000.999_999_999;
const max = 9_223_372_036_854_775_807n;
const binary = 0b1011_0101_0101;
const octal = 0o1234_5670;
const hex = 0xD0_E0_F0;

ES2022


await верхнего уровня / Top level await: позволяет использовать ключевое слово await на верхнем уровне модулей, что избавляет от необходимости оборачивать асинхронный код в асинхронную функцию и улучшает обработку ошибок:


async function asyncFuncSuccess() {
  return 'test';
}
async function asyncFuncFail() {
  throw new Error('Test');
}

// Раньше:
// Ждать разрешения промиса можно было только внутри асинхронной функции
// await asyncFuncSuccess(); // SyntaxError: await is only valid in async functions
// Обертка приводит к усложнению обработки ошибок и потере контроля за порядком выполнения кода
try {
  (async () => {
    console.log(await asyncFuncSuccess()); // "test"
    try {
      await asyncFuncFail();
    } catch (e) {
      // Иначе ошибки не будут перехвачены (или будут перехвачены слишком поздно с усложненной трассировкой стека)
      console.error(e); // Error: "Test"
      throw e;
    }
  })();
} catch (e) {
  // Не выполняется или выполняется слишком поздно
  console.error(e);
}
// Выводится до разрешения промиса
console.log('Hey'); // "Hey"

// Сейчас:
// Файл должен быть модулем (`"type"" "module"` в `package.json` или расширение ".mjs")
console.log(await asyncFuncSuccess()); // "test"
try {
  await asyncFuncFail();
} catch (e) {
  console.error(e); // Error: "Test"
}
// Выводится после разрешения промиса
console.log('Hello'); // "Hello"

#private: делает члены класса (свойства и методы) приватными (закрытыми). Такие члены доступны только внутри класса, в котором они определены. Они не могут удаляться или определяться динамически. Любое некорректное поведение завершается синтаксической ошибкой JS. В TS-проектах для обозначения приватных членов класса используется ключевое слово private.


class ClassWithPrivateField {
  #privateField;
  #anotherPrivateField = 4;

  constructor() {
    this.#privateField = 42; // Ok
    this.#privateField; // SyntaxError
    this.#undeclaredField = 444; // SyntaxError
    console.log(this.#anotherPrivateField); // 4
  }
}

const instance = new ClassWithPrivateField();
instance.#privateField === 42; // SyntaxError

Статические члены класса / Static class members: делает поле класса (свойство или метод) статическим:


class Logger {
  static id = 'Logger1';
  static type = 'GenericLogger';
  static log(message: string | Error) {
    console.log(message);
  }
}

class ErrorLogger extends Logger {
  static type = 'ErrorLogger';
  static qualifiedType;
  static log(e: Error) {
    return super.log(e.toString());
  }
}

console.log(Logger.type); // "GenericLogger"
Logger.log('Test'); // "Test"

// Инстанцирование класса, содержащего только статические поля, бесполезно и
// выполняется здесь только в целях демонстрации
const log = new Logger();

ErrorLogger.log(new Error('Test')); // Error: "Test" (инстанцирование суперкласса не меняет поведение подклассов)
console.log(ErrorLogger.type); // "ErrorLogger"
console.log(ErrorLogger.qualifiedType); // undefined
console.log(ErrorLogger.id); // "Logger1"

// Выбрасывается исключение, поскольку `log` - статический метод, а не метод экземпляра
console.log(log.log()); // log.log is not a function

Статические блоки инициализации / Static initialization blocks: блок кода, который выполняется при инициализации класса. Как правило, такие блоки используются в качестве "конструкторов" статических членов классов:


class Test {
  static staticProperty1 = 'Property 1';
  static staticProperty2;
  static {
    this.staticProperty2 = 'Property 2';
  }
}

console.log(Test.staticProperty1); // "Property 1"
console.log(Test.staticProperty2); // "Property 2"

Утверждение импорта / Import assertion (пока доступно только в V8): определяет тип импортируемого ресурса. Может использоваться, например, для импорта JSON без необходимости его разбора:


import json from './foo.json' assert { type: 'json' };
console.log(json.answer); // 42

Индексы совпадений регулярного выражения / RegExp match indices: начальный и конечный индексы совпадения регулярного выражения с группами захвата. Это работает с RegExp.exec(), RegExp.match() и String.matchAll():


const matchObj = /(test+)(hello+)/d.exec('start-testesthello-stop');

// Раньше:
console.log(matchObj?.index); // 9 - только начальный индекс совпадения

// Сейчас:
if (matchObj) {
  // Начальный и конечный индексы совпадения
  console.log(matchObj.indices[0]); // [9, 18]

  // Начальный и конечный индексы групп захвата
  console.log(matchObj.indices[1]); // [9, 13]
  console.log(matchObj.indices[2]); // [13, 18]
}

Негативная индексация / Negative indexing: метод Array.at возвращает элементы массива с конца (с помощью отрицательных индексов). at(-1) является эквивалентом arr[arr.length - 1] для получения последнего элемента, но не для его установки:


console.log([4, 5].at(-1)) // 5

const array = [4, 5];
array.at(-1) = 3; // SyntaxError

Object.hasOwn(): альтернатива метода Object.hasOwnProperty(), позволяющая определять наличие в объекте указанного свойства. Работает лучше в некоторых крайних случаях:


const obj = { name: 'test' };

console.log(Object.hasOwn(obj, 'name')); // true
console.log(Object.hasOwn(obj, 'gender')); // false

Причина ошибки / Error cause: при повторном выбросе исключения (re-throwing) в качестве второго аргумента в конструктор Error можно передать объект со свойством cause, значением которого является оригинальное исключение:


try {
  try {
    connectToDatabase();
  } catch (err) {
    throw new Error('Не удалось подключиться к базе данных.', { cause: err });
  }
} catch (err) {
  console.log(err.cause); // ReferenceError: connectToDatabase is not defined
}

На этом перевод первой части, посвященной возможностям JS, завершен. В следующей части мы поговорим о возможностях TS.


Надеюсь, вы узнали что-то новое и не зря потратили время.


Happy coding!




Комментарии (11)


  1. savostin
    00.00.0000 00:00
    -1

    Интересно, для чего может понадобиться WeakRef?

    at(-1) является эквивалентом arr[arr.length - 1] для получения последнего элемента, но не для его установки

    Странно, почему нет - это ж удобно.


    1. HeyAleksey
      00.00.0000 00:00
      +1

      WeakRef нужен для передачи объектов в сторонние классы/модули без увеличения счетчика ссылок. Необходимо для корректной работы GC если мы не уверены, что 3-я сторона будет корректно чистить ссылки на объект.


      1. savostin
        00.00.0000 00:00

        Эм, так тогда GC будет точно некорректно чистить. Вернее совсем не будет. И как вообще эта третья сторона может контролировать GC?


        1. GCU
          00.00.0000 00:00
          +1

          Варианта было два - хранить/держать ссылку и не давать собрать GC или же не хранить ссылку - не мешать GC. WeakRef же третий вариант - ссылка хранится, но ненадежно - может пропасть. Удобно для всякого рода кэширования - само очистится GC.


      1. RAX7
        00.00.0000 00:00

        3-я сторона может спокойно вызвать const value = ref.deref(); и держать ссылку сколько её угодно. Так что для подобных вещей WeakRef бесполезен.


    1. vanxant
      00.00.0000 00:00
      +1

      at это функция, в js она не может вернуть rvalue, в смысле указатель на внутренности массива. Максимум что могли сделать это at(-1, newvalue)


    1. demimurych
      00.00.0000 00:00
      +1

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

      В результате чего, если потребуется дополнительная память, то подобный кеш будет уничтожен GC. И наоборот, если памяти достаточно, мы ее излишок єффективно используем под разного рода кеширующие структуры.


  1. demimurych
    00.00.0000 00:00
    +1

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

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

    Под спойлером перечисление ошибок найденных как в переводе так и в оригинале.

    Символы / Symbols: примитивы

     В оригинальном тексте никаких заявлений о том что Symbol является примитивом - нет. Что соответствует спецификации, в которой нет описания "примитивов" или "примитивных типов" или "простых типов".

    Тип Symbol - это тип, который возвращает Primitive Value, чего совершенно недостаточно, чтобы записывать его (как и любой другой подобный тип) в группу примитивов или примитивных типов.

    Так как спецификация ECMA не только не содержит определения примитивных типов (Primitive value - это не примитивный тип), но и не использует подобного словосочетания ни с одним из связанных с типами контекстом, то мы можем опираться только на общепринятые критерии определения того, какими свойствами обладают простые(примитивные) и сложные(составные) типы. Характерными чертами составных типов являются:

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

    2. В арсенале языка существуют штатные средства изменения поведения типа для разных ситуаций в которых он может использоваться.

    Как первый пункт, так и второй характерен для любого из JavaScript типов, а именно:

    1. Любой из типов, которые пытаются отнести в JS к примитивным, имеет сложную обьектную структуру как на уровне спецификации, так и на уровне ее реализаций в конкретных runTime.
      Доступ к которой, в рамках спецификации, осуществляется путем использования конструктора типа, в момент доступа к любому из его свойств.
      И форсируя использование конструктора - напрямую в случае конкреткных реализаций например V8.

    2. Поведение подобных типов, может быть легко изменено путем использования штатных средств предоставляемых языком. Вне зависимости от того, определено ли значение при помощи конструктора типа или его литерала.
      То есть программист может перепрограммировать поведение типа (например String) таким образом, что доступ к его Primitive Value, в разных ситуациях, будет приводить к разным результатам в зависимости от желания программиста.
      Например доступ к символу строки по индексу, может возвращать то, что угодно программисту, но не то, что было задано при инициализации связи со строкой

    Более того, если подойти к этой проблеме с другой стороны, т.е. попытаться нормализовать подобное поведение языка JS как его особенности определения примитивных типов (даже не смотря на спецификацию) то окажется, что язык обладает всеми необходимыми средствами для того, чтобы любой Object привести ровно к такому же поведению, что и любой из типов, ошибочно записываемых в примитивные.

    Optional chaining

    Оператор опциональной последовательности / Optional chaining (?.): обычно используется для безопасного доступа к свойству потенциально несуществующего/неопределенного (undefined) объекта,

    Неудачный перевод выражения undefined object допускающий интерпретацию его как существование некоторого обьекта с типом undefined чего в JS быть не может.

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

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

    Optional chaining (?.) не может предоставить доступа к несуществующему идентификатору с каким бы типом данных он не был связан.

    Заявленные примеры создают ложное впечатление будто бы это так, а на самом деле optional chaining, в заявленных примерах применяется либо к undefined либо к null, что совершенно не отвечает формулировке - потенциально не существующему. Потому как, что undefined что null - это реально существующие данные.

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

    anyNonExistIdentifier ?. (); 
    Uncaught ReferenceError: anyNonExistIdentifier is not defined
    undefined .? ();  // undefined
    null?.(); // undeifned

    Иными словами правильно было бы заявить, что Optional chaining это возможность языка, применяемая исключительно к CallExpression или MemberExpression и позволяющая произвести окончательное вычисление выражения в том случае, если как первое так и второе определено и не возвращает тип Undefined или Null

    Например:

    (()=>{
    	doThing?.(); // undefined
    	var doThing;
    })();
    
    (()=>{
    	doLetThing?.(); 
        Uncaught ReferenceError: Cannot access 'doLetThing' before initialization
    	let doLetThing;
    })();

    Оператор нулевого слияния / Nullish coalescing operator (??):

    Оператор нулевого слияния / Nullish coalescing operator (??): является альтернативой оператора ||. Отличие между этими операторами состоит в том, что || применяется ко всем ложным значениям, а ?? — только к undefined и null

    Спецификация ничего не знает про термин Nullish - это жаргон.
    Спецификация определяет еще один Binary Logical Operator со своей логикой поведения, которая заключается в том, что если вычисление левой части выражения приводит к результату null или undefined то следует вычислить правую часть выражения.

    Например:

    var theThing = globalThis.someNonExistProperty ?? "Yo";
    console.log(theThing); // Yo
    
    var theThing = null ?? "Yo";
    console.log(theThing); // Yo
    
    var theThing = undefined ?? "Yo";
    console.log(theThing); // Yo
    
    var theThing = nonExistIdentifier ?? "Yo";
    Uncaught ReferenceError: someNonExistIdentifier is not defined

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

    1. ?? - приведет к выполнении правой части, если левая либо:
      undefined или null

    2. ?. - приведет к применению правой части к левой, если левая не undefined или null

    Например:

    var theObj = {
      name: "DemiMurych",
    };
    
    console.log( theObj.name ?? "The Name is not defined" );
    DemiMurych
    console.log( theObj.age ?? "The Age is unknown" );
    The Age is unknown
    
    console.log( theObj ?. name  );
    DemiMurych
    console.log( theObj ?. age  );
    undefined

    Отличие между этими операторами состоит в том, что || применяется ко всем ложным значениям

    Некорректно заявлять подобное. Правильно говорить, что Binary Logick Operator || приводит к выполнению правой части выражения в том случае, когда результат левой части выражения, переданный в ToBoolean вернет результат False.

    Что произойдет в случае: undefined, null, +0????, -0????, NaN, 0ℤ и особом случае передачи обьекта имеющего установленное внутреннее свойство [[IsHTMLDDA]]

    import()

    import(): функциональное выражение динамического импорта — как import ... from '...', но во время выполнения кода и с возможностью использования переменных

    import() - не является функциональным выражением. ImportCall это особый, самостоятельный тип выражения, на который не распространяются правила типичного CallExpression (вызова функции).

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

    const theLib = import("/somePath/someLib");
    Uncaught ReferenceError: improt is not defined

    Promise.allSettled()

    Promise.allSettled(): похож на Promise.all(), но ожидает (любого) разрешения всех промисов, а не возвращает первую ошибку, что облегчает обработку ошибок

    Promise.allSettled - возвращает Promise который примет состояние fulfilled в случае если все Promise из списка переданного в Promise.allSettled будут разрешены либо вfulfilled либо в rejected. Например:

    Promise.allSettled([ 
      Promise.resolve(1), 
      Promise.reject(2)
    ]).then(
      ( theRes)=>( 
        theRes.forEach( 
          ( {status: theStatus} )=>( 
            console.log(theStatus)
          ) 
        )  
      )
    );
    fulfilled
    rejected

    И никогда не разрешиться, если хотя бы один из промисов будет в состоянии Pending:

    Promise.allSettled([ 
      Promise.resolve(1), 
      Promise.reject(2),
      new Promise( ()=>{} )
    
    ]).then(
      ( theRes)=>( 
        theRes.forEach( 
          ( {status: theStatus} )=>( 
            console.log(theStatus)
          ) 
        )  
      )
    );
    Promise {<pending>}

    В следствии чего, совершенно неверно утверждать, что это поведение похоже на Promise.All так как последний не будет ожидать разрешения всех промисов из переданного списка, если хотя бы один уже завершился в состоянии rejected

    globalThis

    globalThis: предоставляет доступ к глобальным переменным, независимо от среды выполнения кода (браузер, Node.js и др.)

    Ошибка и в источнике и в переводе.
    globalThis - по умолчанию, содержит в себе ссылку на глобальный обьект.

    Однако, поскольку globalThis допускает возможность связать себя с чем угодно, нет никаких гарантий что globalThis всегда будет ссылаться именно на него (Global Object).

    В глобальном объекте действительно будут созданы новые Property НО только для HoistableDeclaration и только для VariableStatement,
    и не будут созданы для LexicalyScopedDeclaration.
    В чем легко убедиться на простом примере:

    var theVarIdet = 10;
    console.log('variable statement: ', globalThis.theVarIdet);
    variable statement:  10
    
    let theLetIdent = 10;
    console.log('lexicaly declaration: ', globalThis.theLetIdent);
    lexicaly declaration:  undefined

    JavaScript и глобальные переменные

    Никаких глобальных переменных в Js не существует. Современный JS вообще не описывается в терминах областей видимости. Современный Js описывается в рамках:
    HOST => Realm => Eviroment
    Что видно не только из официальной спецификации но и из смежных спецификаций, например HTML5.

    Ближе всего к концепции глобальной области видимости лежит Global Enviroment Record. Поскольку в GER так же как и в глобальной области видимости не существует родителя.

    Проблемы аналогии начинаются тогда, когда оказывается что в JS Global Enviroment Record не обязательно существует в единственном экземпляре. Как и тогда, когда оказывается, что не существует способа получить доступ ко всем идентификаторам из GER любым иным способом кроме как пройдя всю цепочку окружений.

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

    Разное

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

    await верхнего уровня / Top level await: позволяет использовать ключевое слово await на верхнем уровне модулей, что избавляет от необходимости оборачивать асинхронный код в асинхронную функцию и улучшает обработку ошибок:

    И при этом снижает производительность инициализации модулей на значимый процент времени, поскольку возникают издержки на реализацию генератор подобной функции с сохранением конектстов на каждый awaite. В среднем потери колеблется от 10 до 30%.

    По этой причине, в случае если поставленная задача требует оптимизации потребляемых ресурсов, использование в модулях top level await противопоказано.

    Причина ошибки / Error cause: при повторном выбросе исключения (re-throwing) в качестве

    Словосочетание - выброс исключений ошибочно добавлено переводчиком и отсутствует в оригинальном тексте.

    В JavaScript нет выброса исключений в том смысле в котором оно существует в других языках. В JavaScript throw это обычный return из текущего Job но с установленным флагом, который позволяет обработать этот return при помощи try statement.

    Вероятно после этого открытия, многим станет понятным "изначально странное поведение" внутри Executor в конструкторе Promise когда вложенные throw не отлавливаются вышестоящим Catch.

    Вместо ИГОГО

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

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


    1. RAX7
      00.00.0000 00:00

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

      Не могли бы вы привести пример кода, где такая магия будет работать.


  1. kirill-pavlovskii15
    00.00.0000 00:00

    А какие use case есть у top level await ? То есть такой же fecth() только не завернутый в async функцию ?


    1. yarkov
      00.00.0000 00:00

      Коннект к БД при инициализации приложения. Это первое что пришло в голову.

      Ну или вот ещё: https://habr.com/ru/post/524068/