JS — мне сегодня 30 лет
- четверг, 19 марта 2026 г. в 00:00:06
В 2025 году JavaScript исполнилось 30 лет — хороший повод попытаться объять необъятное разобраться, как он меняется и в каком направлении развивается. За три десятилетия язык переживал периоды скепсиса и бурного роста, обрастал экосистемой и стандартами, и в итоге вышел далеко за пределы браузера, охватив серверную и кросс‑платформенную разработку.
Всем привет! Меня зовут Владимир, я разработчик СберБанк Онлайн в канале «веб», и в этой статье я хотел бы затронуть этапы эволюции JavaScript, зафиксировать нововведения спецификации ECMAScript 2025, разобрать наиболее значимые предложения для будущих версий стандарта и попытаться понять его место в эпоху стремительного развития ИИ.

JavaScript появился в1995 году благодаря Брендану Эйху. По известной истории, первую версию языка создали всего за 10 дней. Сначала он назывался Mocha, затем — LiveScript, и лишь позже получил имя JavaScript, под которым мы знаем его сегодня. Иногда можно встретить утверждение, что он также назывался CoffeeScript, однако это неверно: CoffeeScript — отдельный язык, появившийся значительно позже и компилирующийся в JavaScript. Изначально JavaScript был встроен в Netscape Navigator 2.0 и предназначался для одной задачи — добавить веб‑страницам интерактивность. Ранние версии (1.0–1.5) служили инструментом придания интерфейсу динамики в эпоху бума доткомов: для проверки форм, всплывающих окон, реакции на действия пользователя. К концу 1990-х язык стандартизовали под именем ECMAScript: в 1997 году вышла спецификация ECMAScript 1, а в 1999 — ECMAScript 3, получившая регулярные выражения, switch, try/catch и ряд других ключевых возможностей. Именно ES3 во многом сформировал тот JavaScript, который мы знаем.
В начале 2000-х развитие языка несколько замедлилось на фоне браузерных войн и различий в реализациях движков. Распространение AJAX изменило характер веб‑приложений: возможность загружать данные без перезагрузки страницы сделала интерфейсы более динамичными и изменила подход к их построению. В этот период широкое распространение получили JavaScript‑библиотеки как слой абстракции над нестабильной браузерной средой. Самой популярной стала библиотека jQuery, которая упростила работу с DOM, частично сгладила различия между браузерами и предложила более удобный API поверх нативных механизмов. Многие из этих идей впоследствии получили отражение в стандартах веб‑платформы: использование CSS‑селекторов через querySelector и querySelectorAll, работа с классами через classList, унификация обработки событий через addEventListener. Этот этап сформировал культуру разработки поверх абстракций и подготовил почву для следующего поколения инструментов — полноценных клиентских фреймворков.
Попытка выпуска ECMAScript 4 в итоге не была завершена. Проект подразумевал существенные изменения языка, в том числе элементы статической типизации, концепцию namespace и расширенную систему модулей. В процессе стандартизации стало ясно, что объём предлагаемых изменений требует иного подхода к развитию спецификации, и работу над ES4 прекратили. Крупным обновлением стал ECMAScript 5 (2009), который расширил стандартный API и уточнил поведение языка: появился режим «use strict», методы работы с массивами (map, filter, reduce), метод Object.defineProperty с поддержкой дескрипторов свойств, встроенная поддержка JSON, методы управления расширяемостью объектов (Object.freeze, Object.seal, Object.preventExtensions), а также ряд изменений и уточнений, связанных с моделью объектов и их прототипами. При этом «use strict» оказался базовым режимом для последующих конструкций, поскольку именно в строгом режиме по умолчанию работают модули и классы. С выходом ECMAScript 2015 (ES6) язык получил масштабное обновление: добавили модули (import/export), классы, стрелочные функции, блочную область видимости через let и const, шаблонные строки, деструктуризацию, параметры по умолчанию, промисы, структуры данных Map и Set, примитив Symbol, механизм перехвата операций через Proxy, новый цикл for…of для перебора итерируемых объектов и другие изменения спецификации, что расширило выразительные возможности языка и систематизировало подход к модульности, асинхронности и работе со структурами данных.
Однако, практическое внедрение новых возможностей языка долгое время сдерживалось поддержкой браузеров, прежде всего Internet Explorer. Необходимость использовать транспиляцию и полифилы для обеспечения совместимости усложняла использование современных возможностей стандарта и сказалась на популярности браузера среди пользователей и разработчиков. В последующие годы TC39 — технический комитет Ecma International, отвечающий за развитие ECMAScript, — перешёл к ежегодной модели публикации спецификации. Это придало регулярности процессу развития и сформировало современный механизм эволюции языка через систему Proposal — предложений, описывающих новую функцию или изменение, которое планируется добавить в язык. Сегодня, в 2026 году, актуальной версией стандарта является ECMAScript 2025. Если хочется, чтобы вам окончательно свело олдскулы, то вот довольно залипательный лэндинг с ключевыми событиями JS‑экосистемы, а также его русский перевод.
Небольшое примечание: примеры приведены исключительно для демонстрации новых возможностей языка. Во многих случаях подобные задачи уже решаются существующими библиотеками или иными инструментами. В статье акцент сделан на нативных возможностях JS, а не на выборе оптимального production-подхода.
ECMAScript 2025
Import Attribute. Теперь можно импортировать не-JS ресурсы (в первую очередь JSON) с указанием типа. Введён явный способ передать хосту (браузеру или рантайму) метаданные о том, как именно нужно интерпретировать импортируемый модуль. Дополнительно стандарт консолидирует синтаксис вокруг with { type: "json" } (вместо раннего assert { … }).
Было:
import fs from 'fs/promises'; const raw = await fs.readFile('./config.json', 'utf-8'); const config = JSON.parse(raw);
Стало:
import config from './config.json' assert { type: 'json' }; console.log(config.apiUrl); // уже готовы распаршенный JSON
То есть это предложение формализует то, что ранее решалось на уровне инструментов (webpack, vite и т. п.) или нестандартизированных механизмов, и задаёт единый протокол передачи «атрибутов» импорта хост‑среде.
Iterator Helpers (итерационные методы). До появления Iterator Helpers итераторы в JavaScript оставались низкоуровневым механизмом: у них есть метод next(), но отсутствуют стандартные операции уровня map и filter. Поэтому для построения цепочек преобразований приходилось либо преобразовывать данные в массив и тем самым терять ленивость вычислений, либо писать генераторы и ручные циклы. Iterator Helpers добавляют к итераторам функциональные методы (map, filter, flatMap, reduce, toArray и др.), позволяя собирать последовательности трансформаций без промежуточных коллекций. Это упрощает работу с потоками данных и последовательностями, особенно когда исходные данные потенциально большие или поступают по мере необходимости.
До ES2025 преобразование выполнялось через генератор, цикл for...of и промежуточный массив:
function* numbers() { for (let i = 1; i <= 10; i++) yield i; } const result = []; for (const n of numbers()) { if (n % 2 === 0) result.push(n * 2); } console.log(result); // [4, 8, 12, 16, 20]
В актуальной спецификации можно работать с цепочкой операций непосредственно над итераторами, без промежуточных шагов и временных коллекций.
function* numbers() { for (let i = 1; i <= 10; i++) yield i; } const result = numbers() .filter(n => n % 2 === 0) .map(n => n * 2) .toArray(); console.log(result); // [4, 8, 12, 16, 20]
Также в стандарт включён метод Array.fromAsync(), позволяющий создавать массив из асинхронного источника (например, асинхронного генератора или потока данных) без ручного цикла for await.
Новые методы для Set. Встроенный объект Set (множество) обзавёлся удобными методами для операций над множествами: пересечение intersection(), объединение union(), разность difference(), симметричная разность symmetricDifference(), а также методами проверки отношений: isSubsetOf(), isSupersetOf(), isDisjointFrom(). Эти операции ранее приходилось реализовывать вручную, теперь же они доступны «из коробки».
Было:
const a = new Set([1, 2, 3]); const b = new Set([2, 3, 4]); const intersection = new Set([...a].filter(x => b.has(x))); console.log(intersection); // Set { 2, 3 }
Стало:
const a = new Set([1, 2, 3]); const b = new Set([2, 3, 4]); const intersection = a.intersection(b); console.log(intersection); // Set { 2, 3 }
В результате мы получили удобные методы на уровне языка, как в Python, без использования «добавочных» методов.
Метод Error.isError() Проверка «это ошибка или нет» в JavaScript исторически выполнялась через instanceof Error. Такой подход работает в пределах одного execution context, однако даёт ложные отрицания, если объект ошибки создан в другом realm — например, внутри iframe. Альтернативы вроде Object.prototype.toString.call(x) также не являются надёжными из‑за возможности подмены Symbol.toStringTag. Error.isError(value) вводит стандартный и устойчивый механизм проверки: метод возвращает true только для реальных экземпляров Error и его подклассов независимо от их происхождения. Это закрывает проблему кросс‑realm проверок и упрощает обработку ошибок из внешних источников.
export const isRealError = (e) => e instanceof Error; export const demo = () => { const iframe = document.createElement("iframe"); document.body.appendChild(iframe); const foreignError = iframe.contentWindow.eval("new Error('boom')"); return isRealError(foreignError); // false };
Проверка через instanceof зависит от конкретного realm, поэтому объект Error, созданный в iframe, не распознаётся как ошибка.
С новым методом:
export const isRealError = (e) => Error.isError(e); export const demo = () => { const iframe = document.createElement("iframe"); document.body.appendChild(iframe); const foreignError = iframe.contentWindow.eval("new Error('boom')"); return isRealError(foreignError); // true };
Error.isError() корректно определяет ошибку независимо от контекста её создания.
Promise.try(). В спецификацию добавлен новый статический метод Promise.try, предназначенный для унификации начала Promise-цепочек. Он выполняет переданную функцию и гарантирует, что её результат будет приведён к Promise, а любое синхронное исключение автоматически превратится в rejected-состояние. Ранее для обеспечения корректной маршрутизации синхронных ошибок внутри Promise-цепочки использовали конструкцию Promise.resolve().then(fn), позволяющую выполнить функцию в рамках асинхронного контекста:
export const loadUser = () => Promise.resolve() .then(parseUserId) .then((id) => fetch(`/api/users/${id}`)) .then((res) => res.json()) .catch((e) => console.error("Caught:", e)); const parseUserId = () => { const id = localStorage.getItem("userId"); if (!id) throw new Error("Missing userId"); return id; };
С появлением Promise.try эта конструкция получила встроенное выражение в языке:
export const loadUser = () => Promise.try(() => parseUserId()) .then((id) => fetch(`/api/users/${id}`)) .catch((e) => console.error(e)); const parseUserId = () => { const id = localStorage.getItem("userId"); if (!id) throw new Error("Missing userId"); return id; };
Помимо вышеперечисленных, стандарт ввёл несколько дополнений: статический метод RegExp.escape() для экранирования строк в шаблонах регулярных выражений, поддержку локальных модификаторов флагов (inline-flags, позволяющих применять флаг, например i, только к части шаблона), а также возможность повторно использовать одно и то же имя группы захвата в разных альтернативах.
Также появилась поддержка 16-битных чисел с плавающей запятой: функция округления Math.f16round(), типизированный массив Float16Array, а также методы для чтения и записи 16-битных значений в DataView: getFloat16() и setFloat16(). С практической точки зрения это может быть полезно при обработке графики, в WebGL/WebGPU или при работе с потоками данных из ML-моделей, позволяя оптимизировать потребление памяти без перехода к ручной бинарной упаковке или сторонним решениям.
Конечно, это не полныйсписок, в недрах TC39 (технического комитета по стандартизации JS) обсуждается множество идей — от встроенной поддержки signals и реактивности до улучшений для WebAssembly и даже экспериментальных аннотаций типов. Однако направление развития ясно: JavaScript становится более мощным и удобным, не жертвуя простотой. Комитет соблюдает принцип обратной совместимости, поэтому новые возможности добавляют постепенно.
Стоит отметить, что темпы развития JS отчасти ускоряются из‑за конкуренции и запроса сообщества. Например, всплеск интереса к Python (во многом благодаря успехам в ИИ) мотивирует JavaScript адаптироваться: проще интегрироваться с ИИ‑библиотеками, перенимать удачные функции (как поддержку 16-битных чисел) и так далее. В следующие несколько лет мы, вероятно, увидим дальнейшее сближение возможностей разных языков: JavaScript будет заимствовать лучшие идеи, оставаясь при этом основным языком веба.
Date в JavaScript совмещает несколько разных концепций: абсолютную точку во времени, локальную дату и работу с часовыми поясами, при этом остаётся мутабельным и зависит от настроек среды выполнения (локального часового пояса ОС, браузера или серверного окружения Node.js). В результате даже базовые операции могут вести себя по-разному в разных окружениях, а преобразования между локальным временем и UTC происходят неявно.
Например, прибавление одного дня к календарной дате:
export const addDay = (isoDate) => { const d = new Date(isoDate); d.setDate(d.getDate() + 1); return d.toISOString().slice(0, 10); }; addDay("2026-03-29"); // "2026-03-30" или "2026-03-29"
Результат зависит от локального часового пояса и перехода на летнее время, а сам объект изменяется на месте. Кроме того, Date не разделяет календарную дату и момент времени: строка «2026-03-29» интерпретируется через локальный часовой пояс, затем преобразуется в UTC-формат при вызове toISOString(), что может приводить к сдвигу дня.
Temporal предлагает иную модель работы со временем: вместо одного универсального типа ввели набор специализированных сущностей с чётко разделённой семантикой.Temporal.Instant представляет абсолютную точку во времени (UTC), Temporal.PlainDate — календарную дату без времени и часового пояса, Temporal.PlainDateTime — дату и время без привязки к часовому поясу, а Temporal.ZonedDateTime — дату и время в конкретном поясе с учётом его правил. Такое разделение устраняет смешение понятий «календарная дата» и «момент времени», характерное для Date. Дополнительно все объекты Temporal неизменяемы: любая операция возвращает новое значение вместо изменения исходного.
В случае работы именно с календарной датой используют Temporal.PlainDate:
export const addDay = (isoDate) => { return Temporal.PlainDate .from(isoDate) .add({ days: 1 }) .toString(); }; addDay("2026-03-29"); // "2026-03-30"
Здесь результат не зависит от локальных настроек среды, поскольку тип изначально не содержит информации о часовом поясе и не выполняет скрытых преобразований в UTC-формат. Исходное значение остаётся неизменным, а работа с часовым поясом становится явной только при использовании соответствующего типа, например, Temporal.ZonedDateTime, где пояс является частью модели данных и учитывается при вычислениях.
Типичная задача — добавить к методам инфраструктурное поведение: журналирование, обработку ошибок, отправку аналитических событий, проверку аргументов и другие повторяющиеся аспекты. Без поддержки на уровне языка это обычно реализуют через ручные обёртки, что требует дополнительного кода и явного применения расширяющей логики к каждому методу. Например, журналирование ошибок сетевых запросов можно реализовать через функцию‑обёртку:
const withErrorLogging = (fn) => async (...args) => { try { return await fn(...args); } catch (e) { console.error("[api error]", e); throw e; } }; class Api { fetchUser = async (id) => { const res = await fetch(`/api/users/${id}`); if (!res.ok) throw new Error(res.statusText); return res.json(); }; fetchPosts = async () => { const res = await fetch("/api/posts"); if (!res.ok) throw new Error(res.statusText); return res.json(); }; } const api = new Api(); api.fetchUser = withErrorLogging(api.fetchUser); api.fetchPosts = withErrorLogging(api.fetchPosts);
Такой подход работает и, по сути, является ручной реализацией декорирования. Однако расширяющее поведение применяется уже после определения класса и создания экземпляра, что требует явной модификации методов и дополнительной дисциплины при масштабировании. С введением декораторов механизм переносится на уровень определения класса:
const logErrors = (value, context) => { if (context.kind !== "method") return; return async function (...args) { try { return await value.apply(this, args); } catch (e) { console.error("[fetch]", e); throw e; } }; }; class Api { @logErrors fetchUser = async (id) => { const res = await fetch(`/api/users/${id}`); if (!res.ok) throw new Error(res.statusText); return res.json(); }; } export const fetchUser = (id) => new Api().fetchUser(id);
Здесь правило расширения фиксируется непосредственно в месте объявления метода. Поведение становится декларативным: инфраструктурная логика описывается рядом с методом и применяется автоматически при определении класса, без дополнительной инициализации. Важно учитывать, что декораторы применяются к элементам класса (методам, полям, аксессорам) и самим классам на этапе их определения, обычные объектные литералы декорировать напрямую нельзя.
Во многих сценариях требуется гарантированное освобождение созданных ресурсов: отмена подписок, остановка observer-ов, закрытие соединений, очистка таймеров. В простых случаях это реализуется через useEffect с функцией очистки, однако при усложнении влияния — нескольких ресурсов, ветвлений и ранних выходов — логика освобождения начинает распределяться по разным участкам кода.
Proposal Explicit Resource Management вводит конструкции using и await using, позволяющие привязать освобождение ресурса к области видимости и гарантировать вызов очистки при выходе из неё, включая ситуации со throw или ранним return. Механизм основан на специальном встроенном символе Symbol.dispose: если значение, объявленное через using, содержит метод по этому символу, то он будет автоматически вызван при завершении области видимости. Это не соглашение по имени (dispose, close, destroy), а стандартизированный протокол языка. Если объект реализует обычный метод dispose(), но не определяет [Symbol.dispose], то механизм работать не будет — спецификация ищет именно встроенный символ, и при его отсутствии выбрасывает TypeError.
Рассмотрим пример с IntersectionObserver:
import { useEffect, useRef } from "react"; export const Card = ({ onVisible }) => { const ref = useRef(null); useEffect(() => { if (!ref.current) return; const observer = new IntersectionObserver(([entry]) => { if (entry.isIntersecting) onVisible(); }); observer.observe(ref.current); return () => { observer.disconnect(); }; }, [onVisible]); return <div ref={ref}>...</div>; };
В текущем варианте очистка явно прописана в return. При добавлении второго Observer, подписки или таймера необходимо не забыть добавить соответствующее освобождение ресурса.
С использованием using освобождение становится частью объявления:
import { useEffect, useRef } from "react"; const createIntersectionObserver = (onEntry) => { const observer = new IntersectionObserver(onEntry); return { observe: (el) => observer.observe(el), [Symbol.dispose]: () => observer.disconnect(), }; }; export const Card = ({ onVisible }) => { const ref = useRef(null); useEffect(() => { if (!ref.current) return; using io = createIntersectionObserver(([entry]) => { if (entry.isIntersecting) onVisible(); }); io.observe(ref.current); }, [onVisible]); return <div ref={ref}>...</div>; };
using является лексическим объявлением с блочной областью видимости аналогично let и const, повторное присваивание переменной не допускается, а при выходе из области видимости автоматически вызывается io[Symbol.dispose](). Освобождение ресурса становится симметричным его объявлению и не требует отдельного блока очистки. Для случаев, когда освобождение ресурса является асинхронным (например, требуется дождаться корректного закрытия соединения), предусмотрен await using, в этом случае используется Symbol.asyncDispose, а завершение области видимости сопровождается ожиданием выполнения очистки.
Часто контекст выполнения — идентификатор запроса, traceId, текущий tenant, фича‑флаги — необходимо «пронести» через цепочку await‑вызовов. В браузерной среде это обычно решается либо явной передачей параметров через каждый слой, либо использованием глобального состояния, что ухудшает читаемость и тестируемость кода. В React‑приложениях подобная проблема проявляется при журналировании, аналитике и трассировке пользовательских действий, когда идентификатор взаимодействия должен быть доступен в глубоко вложенных асинхронных вызовах. Proposal Async Context вводит стандартный механизм хранения данных, привязанных к текущему асинхронному выполнению, с автоматическим сохранением контекста через await. Это позволяет отделить инфраструктурный контекст от бизнес‑логики без загрязнения сигнатур функций.
Предположим, что при клике на кнопку мы хотим сгенерировать traceId, выполнить несколько асинхронных операций, ну и включать этот traceId во все аналитические события.
function generateTraceId() { return crypto.randomUUID(); } async function trackEvent(name, traceId) { await fetch("/analytics", { method: "POST", body: JSON.stringify({ name, traceId }), }); } async function fetchUser(traceId) { await trackEvent("fetch_user_start", traceId); const res = await fetch("/api/user"); await trackEvent("fetch_user_success", traceId); return res.json(); } const ProfileButton = () => { const handleClick = async () => { const traceId = generateTraceId(); await trackEvent("click_profile", traceId); const user = await fetchUser(traceId); }; return <button onClick={handleClick}>Load profile</button>; }
Здесь нам приходится передавать в каждую функцию traceId, эта инфраструктурная деталь «засоряет» бизнес‑сигнатуры, и получается, что добавление нового слоя требует не забыть прокинуть параметр дальше.
Как это выглядело бы с реализацией Async Context:
const traceIdVar = new AsyncContext.Variable({ name: "traceId" }); function generateTraceId() { return crypto.randomUUID(); } async function trackEvent(name) { const traceId = traceIdVar.get(); await fetch("/analytics", { method: "POST", body: JSON.stringify({ name, traceId }), }); } async function fetchUser() { await trackEvent("fetch_user_start"); const res = await fetch("/api/user"); await trackEvent("fetch_user_success"); return res.json(); } const ProfileButton = () => { const handleClick = async () => { const traceId = generateTraceId(); await traceIdVar.run(traceId, async () => { await trackEvent("click_profile"); const user = await fetchUser(); }); }; return <button onClick={handleClick}>Load profile</button>; }
При таком подходе traceId больше не передаётся через параметры, бизнес-функции (fetchUser, trackEvent) не зависят от способа хранения контекста, а сам контекст автоматически сохраняется через await и асинхронные границы, инфраструктурная логика (трассировка, аналитика) становится ортогональной бизнес-логике. Таким образом, механизм Async Context может существенно упростить архитектуру асинхронных сценариев без привязки к конкретному runtime-решению.
Глубокие цепочки преобразований данных в JS быстро превращаются в «матрёшку» вызовов f(g(h(x))), которую неудобно читать и править. Обычно это лечат временными переменными, но тогда код распадается на шумные шаги. |> предлагает записывать такие цепочки линейно: читаем слева направо, как реально течёт значение. Это про читаемость и поддержку, а не про новую модель вычислений.
Было:
const result = format( normalize( validate( parse(input) ) ) );
Стало, линейная запись пайплайна:
const result = input |> parse(%) |> validate(%) |> normalize(%) |> format(%);
В современной архитектуре, основанной на модулях, нередко используется подключаемый код: плагины, внешние библиотеки, расширения. ES‑модули структурируют код и изолируют области видимости на уровне файлов, однако среда выполнения остаётся общей: импортированный модуль исполняется в том же realm (отдельное глобальное JS‑окружение с собственным globalThis, набором встроенных объектов и их прототипами). Это означает, что любой подключённый модуль фактически становится частью глобального окружения и может повлиять на поведение стандартных объектов.
Допустим, у нас есть подключаемая библиотека, и мы используем её для трансформации данных перед отображением.
// plugin.js export const customTransform = (items) => { // по ошибке переопределён стандартный метод Array.prototype.map = function () { return ["intercepted"]; }; return items.map((x) => x * 2); };
Подключаем библиотеку и используем её:
import { customTransform } from "./plugin.js"; console.log(customTransform([1, 2, 3])); // ["intercepted"] // теперь map изменён во всём приложении console.log([10, 20].map((x) => x + 1)); // ["intercepted"]
Даже если цель библиотеки — просто обработка массива, то она изменила поведение встроенного метода, поскольку код исполняется в общем глобальном окружении. Такие изменения начинают влиять на всё приложение, а источник проблемы может находиться далеко от места проявления ошибки.
Как это будет выглядеть с ShadowRealm:
const realm = new ShadowRealm(); const customTransform = await realm.importValue( "./plugin.js", "customTransform" ); console.log(customTransform([1, 2, 3])); // ["intercepted"] // поведение map в основном приложении не изменилось console.log([10, 20].map((x) => x + 1)); // [11, 21]
В этом случае библиотека исполняется в отдельном realm — с собственным globalThis и собственными встроенными объектами. У realm свои версии Array, Object, Error и их прототипов, поэтому переопределение Array.prototype.map затрагивает только внутреннее окружение ShadowRealm и не влияет на основной код. Важно отметить, что ShadowRealm не создаёт новый поток и не переводит взаимодействие в модель сообщений, как Worker, а изолирует именно глобальное JS‑окружение, сохраняя синхронное выполнение и привычную модель вызовов.
Мы регулярно работаем с дискриминированными объединениями: состояния загрузки (loading, success, error), типы событий, варианты ответов API. Сегодня это выражается через цепочки if и switch с ручной проверкой структуры и последующей деструктуризацией. Такой код избыточен, склонен к ошибкам и плохо масштабируется при добавлении новых вариантов. Proposal Pattern Matching предлагает декларативный механизм структурного сопоставления, в котором проверка формы значения и извлечение данных объединены в единую конструкцию.
Предположим, компонент рендерит состояние запроса к API, без каких-либо библиотек:
function renderUserState(state) { if (state.status === "loading") { return "Loading..."; } if (state.status === "error" && state.error) { return `Error: ${state.error.message}`; } if (state.status === "success" && state.data) { return `User: ${state.data.name}`; } return "Unknown state"; }
Проверка дискриминатора (status) размазана по условиям, структура объекта проверяется вручную, добавление нового статуса требует обновления всех веток условий.
Как этот код выглядел бы со структурным сопоставлением:
function renderUserState(state) { return match (state) { { status: "loading" } => "Loading...", { status: "error", error: { message } } => `Error: ${message}`, { status: "success", data: { name } } => `User: ${name}`, default => "Unknown state", }; }
Что у нас меняется концептуально: ветка явно описывает ожидаемую структуру объекта, деструктуризация встроена в шаблон, код ближе к декларативному описанию вариантов состояния.
Объекты и массивы в JavaScript обладают ссылочной семантикой, что означает, что два значения с одинаковым содержимым считаются неравными, если они представляют разные ссылки в памяти. Поэтому объекты с идентичными ключами и значениями, созданные независимо друг от друга, не равны.
const a = { x: 1, y: 2 }; const b = { x: 1, y: 2 }; console.log(a === b); // false
Выражение возвращает false, и такое поведение является базовым свойством текущей модели данных языка. Подобная семантика усложняет сценарии, в которых требуется сравнение структур «по значению», а также создание стабильных ключей для кешей или механизмов мемоизации, поэтому приходится прибегать к сериализации, реализовывать собственные функции глубокого сравнения или использовать сторонние библиотеки, например lodashc с его методом isEqual.
На практике равенство структур часто пытаются определить через сериализацию:
const a = { x: 1, y: 2 }; const b = { x: 1, y: 2 }; console.log(a === b); // false function deepEqual(obj1, obj2) { return JSON.stringify(obj1) === JSON.stringify(obj2); } console.log(deepEqual(a, b)); // true
Однако такой подход имеет ограничения, поскольку зависит от порядка свойств. Метод JSON.stringify формирует строковое представление объекта в соответствии с правилами перечисления свойств, когда сначала идут индексоподобные ключи, затем строковые, затем символы. При этом порядок строковых ключей определяется моментом их добавления в объект, а не логической эквивалентностью структур, например:
const a = { x: 1, y: 2 }; const b = { y: 2, x: 1 }; console.log(deepEqual(a, b)); // false
Несмотря на структурную идентичность объектов, различие порядка добавления свойств приводит к разным строковым представлениям, вследствие чего сравнение через сериализацию даёт некорректный результат. Proposal Records & Tuples предлагал ввести в язык неизменяемые value-типы со структурным равенством, при котором сравнение выполняется по содержимому, а не по ссылке. Предполагаемый синтаксис выглядел следующим образом:
const a = #{ x: 1, y: 2 }; const b = #{ x: 1, y: 2 }; console.log(a === b); // true
Такие структуры задумывались как глубоко неизменяемые, не имеющие прототипа, сравниваемые по значению и пригодные для использования в качестве стабильных ключей, что потенциально упрощало бы построение кешей, механизмов мемоизации и других сценариев, где важна предсказуемость сравнения.
Несмотря на концептуальную привлекательность, proposal отозвали, поскольку интеграция value-типов в существующую модель JavaScript оказалась сложной с точки зрения совместимости, взаимодействия с прототипной системой и общей экосистемой языка. Тем не менее, предложение наглядно демонстрирует направление, в котором периодически предпринимаются попытки переосмысления фундаментальных свойств модели данных JavaScript.

В последние годы индустрия разработки переживает быстрый рост решений на основе ИИ и машинного обучения. На этом фоне закономерно возникает вопрос: как меняется роль JavaScript, и может ли он уступить место языкам, сильным в ML‑стеке, прежде всего Python?
С одной стороны, рост интереса к ИИ действительно усилил позиции Python: он является базовым языком для значительной части ML‑инструментария и экосистемы данных. Тенденция видна и в массовых опросах разработчиков: в Stack Overflow Developer Survey отдельно отмечено ускорение роста Python, а также указано, что его доля увеличилась на 7 процентных пунктов по сравнению с 2024 годом. Однако это не означает вытеснение JavaScript. Напротив, JavaScript сохраняет устойчивую роль универсального технологического слоя для прикладных продуктов и интеграций. Показательно, что в том же опросе Stack Overflow среди «профессионалов, использующих ИИ‑инструменты», JavaScript остаётся наиболее распространённым языком: 70,5 % против 56,1 % у Python, что довольно занимательно: даже когда ИИ становится центральной частью продукта, значительная доля прикладной разработки и интеграционной логики остаётся в зоне JS/TS.
Отдельного внимания заслуживает то, как именно пользователи «встречаются» с ИИ. Наиболее массовой формой прямого взаимодействия остаётся чат‑интерфейс: в потребительском исследовании Capgemini Research Institute чат‑боты названы самым часто используемым сценарием среди регулярных пользователей GenAI. Масштаб этой UX‑формы также иллюстрирует роль диалогового интерфейса, к июлю 2025 года ChatGPT достиг порядка 700 млн пользователей и генерировал 18 млрд сообщений в неделю. Вывод относительно JS напрашивается довольно простой: если доминирующая оболочка, с которой массовый пользователь взаимодействует с ИИ, — диалог, то наиболее важными становятся интерфейсы, состояние чата, стриминг ответов, интеграции с инструментами и источниками данных — то есть прикладной слой, в котором JS/TS является core‑технологией.
Эта тенденция отражается и в инструментах. Помимо UI‑библиотек для чат‑ассистентов — от Microsoft Bot Framework Web Chat до специализированных React‑решений со стримингом ответов — формируется инфраструктурный слой для LLM‑интеграций. Vercel AI SD предоставляет унифицированный API, поддержку стриминга и tool‑calling, показывая, что JS‑экосистема системно выстраивает прикладной уровень вокруг моделей. Параллельно развивается агентная архитектура: LangChain.js позволяет строить цепочки вызовов, RAG‑сценарии и работу с инструментами в JavaScript, а LangGraph.js добавляет управляемые графы взаимодействия и явные state‑машины для многоагентных систем. Таким образом формируется орекстрационный уровень ИИ‑приложений, на котором JavaScript отвечает за координацию моделей, внешних сервисов и бизнес‑логики.
Одновременно растёт сегмент zero-code и ИИ‑конструкторов, которые преимущественно существуют в десктоп‑вебе. Lovable позиционируется как AI‑first веб‑платформа для генерации и развёртывания приложений через чат‑интерфейс; при этом итоговый результат — это полноценное веб‑приложение на основе JS‑экосистемы (React,TS, Vite, Tailwind CSS, shadcn/ui). n8n, open‑source платформа автоматизации, активно используется для построения AI‑workflow — от интеграции LLM до RAG‑ и многоагентных сценариев — и предоставляет Code node для выполнения пользовательского JavaScript внутри процесса. Даже в low‑code среде JavaScript остаётся языком интеграции: обработка API, трансформация данных, маршрутизация логики и постобработка результатов моделей часто реализуются именно через вставку JS‑кода.
Параллельно JavaScript расширяет присутствие и в ML‑слое: библиотека TensorFlow.js позволяет запускать модели непосредственно в браузере и в Node.js,а экосистема дополняется прикладными инструментами вроде Transformers.js и ONNX Runtime Web, ориентированными на запуск современных моделей в клиентской среде. Для вычислений на стороне клиента требуется доступ к аппаратному ускорению, и такую возможность предоставляет WebGPU — API для использования графического процессора в браузере. Это не делает JavaScript альтернативой Python для обучения крупных моделей, но позволяет запускать модели прямо на клиенте: автодополнение текста, обработку изображений и видео, background removal, AR‑маски, локальный OCR и гибридные RAG‑подходы с частью логики у пользователя.
В совокупности эти инструменты демонстрируют важный сдвиг: ИИ-продукты всё чаще собираются как веб-системы с оркестрационным слоем, где JavaScript выполняет роль связующего уровня между моделями, API, интерфейсами и бизнес-логикой. Даже если обучение моделей происходит вне JS-экосистемы, прикладной уровень и продуктовая интеграция остаются тесно связаны с ней.

JavaScript остаётся основным языком веба и продолжает развиваться вместе с платформой; одновременно расширяется сфера его применения: от пользовательских интерфейсов до оркестрации ИИ-сервисов и интеграции моделей в прикладные системы. ECMAScript 2025 и текущие proposal-ы показывают, что язык аккумулирует удачные практики из других экосистем, не ломая обратную совместимость и не пересматривая свои базовые концепции, такие как объектная модель, прототипное наследование или ссылочная семантика значений. При этом JavaScript остаётся ключевым звеном между передовыми технологиями, такими как ИИ, и их массовым внедрением, которое возможно только в понятных и привычных пользовательских сценариях.