Галоўная / Артыкулы / Скрытыя інструменты JavaScript: сімвалі, WeakMaps, проксіяў і генератары

Скрытыя інструменты JavaScript: сімвалі, WeakMaps, проксіяў і генератары

Дазвольце даклэ расказаць, як Сімвалы, WeakMap/WeakSet, Proxy/Reflect, FinalizationRegistry і генератары працуюць унутрана, каб запобегчы цэрпліванню памяці і адкрыць можлівасці метапрограмавання на рэўерснай ступені.

2046 слоў

Адаптаванне закрытага механізма, які стоіць за вашымі улюбленымі фрэймворкамі, і абоўсюдна паляганне на проблемы вытэкання памяці

Большая частка часу разработка на JavaScript апоўнюецца вядомым наборам інструментаў: const, let, функцыямі-стрэлкамі, map/filter, деструктурыраваннем масэў і async/await. Гэты повсякдзенны набор падмагае рашэнне пераважнаўсіх задач, якія вы ствараеце.

Але спэкыфікацыя ECMAScript таксама містіць набор менш вядомых інструментаў: прымітывныя типы, спецыялізаваныя контэйнеры дадзеных і метапрограмавальныя элементы ніжнага рэвэля, якія практычна ніколі не паводзяцца ў вводных туторыялах. Авары фрэймворкаў і бібліятэкаў постацей апыльваюцца імі, каб ухіліцца ад вытэкання памяці, узбегнуць калізій з іменамі, скорыстацца внутранім працэю механізма і падтрымаць стабільнасць вялікіх кодавых баз. Калі вы ўсё гэта зрозумеце, вы пачнете бачыць іх усюды ў залежнасцях, якія вжываеце.

Звыканне да гэтым функцыям зменяе спосаб працы з трымстанам жыццёвага об’ектаў, межамі інкапсуляцыі і выкаанням кода пад час роботы. Ёсць огляд можлівасцяў, якія, што ў 99% случаёў, дапамаглі вам у прымэтным коде, не ведаючы пра іх наявнасць.

1. Сімвалы: ключы, якія ніколі не суперасягаюцься

Symbol, дадзеный у ES6, — это прымітны тип, які знаходзится разам з string, number, boolean, null, undefined і bigint.

Każны вызов Symbol() стварае абсалютна новую, унікальную значэнне. Нават два сімвалы, створаныя з абоўсцю той самай характарызаціяй, ніколі не будуць роўныя межы сабой:

const id1 = Symbol('id');
const id2 = Symbol('id');

console.log(id1 === id2); // false

Тэкст, які вы пасылайце ў Symbol('id'), — это проста неабавязковая абазовка для выводу пад час дыбагавання; ён не вплывае на ідэнтычнасць сімвалу.

Чаму взагалі існуюць сімволы?

Да тым, як існавалі сімволы, ключы об’ектаў павинны былі быть стрэнгамі. Жыць це стварала сяржызны аблік: якщо вы напісалі бібліятэку, якая прыкладзеўа внутранія метаданы да об’екта, які належыць іншаму чалавеку, вы моглі лёгка знишчыць атрыбут, які вже там існаваў:

// Risky: Another script might already use `isProcessed`
user.isProcessed = true;

Сімволы цэлкам ухіляюцца ад гэтага, таму што не існуе можлівасці, каб іншы фрагмент коду випадкова створыў той самы сімвол:

const TRACKING_ID = Symbol('trackingId');

const user = {
  name: 'Sarah',
  role: 'Admin'
};

// Safe: Nothing can clash with this exact key
user[TRACKING_ID] = 'txn_89412';

«Мантія невызначальнасці»

Атрыбуты, якія маюць ключ у вобразе сімволу, ігнаруюцца звычнымі інструментамі рэфлексіі і серыялізацыі:

  • Циклы for...in іх ігнаруюць.
  • Object.keys(user) іх не выключае.
  • JSON.stringify(user) ўсё цэла адключае іх.
console.log(Object.keys(user)); // ['name', 'role']
console.log(JSON.stringify(user)); // '{"name":"Sarah","role":"Admin"}'

Як бы там не было, клавішы симвалоў насправды не ўтратаць у таёмніце — вызначэнне Object.getOwnPropertySymbols(user) все равно ўможлівае ўвидзеть іх. Але для звычайных цэляў ітераціі та серыялізацыі яны застаюцца схованымі, што робіць іх падходячымі для внутрашняых флагоў, запам’ятаваных значэнняў і метадаў на рэвэлі гэтавыя, якія не павінны прасачвацца ў публічную API.

Вядомыя симвалы: доступ да внутраніх элементаў мовы

Мова таксама прыносіць з сабой набор вбудованых симвалоў, якія называюцца вядомымі симваламі, і якія выклікаюцца як статычныя атрыбуты об’екта Symbol. Яны даюць вам прымітны доступ да протаколаў, ад якіх сама енжынерыя залежыць.

Адскарычаная ітерацыя через Symbol.iterator

Вы можете змусіць будзь-яны звычайны об’ект працаваць з for...of, реалізававшы самі такі симвал:

const inventory = {
  items: ['Keyboard', 'Monitor', 'Desk Pad'],
  [Symbol.iterator]() {
    let index = 0;
    return {
      next: () => {
        if (index < this.items.length) {
          return { value: this.items[index++], done: false };
        }
        return { done: true };
      }
    };
  }
};

for (const item of inventory) {
  console.log(item); // Prints: Keyboard, Monitor, Desk Pad
}

Спецыяльная поведзьба пры ператворэнні з Symbol.toPrimitive

Гэта дазволяе вам точна выбраць, што будзе выканана, калі ваш об’ект будзе ператворены ў строку або число:

const money = {
  amount: 250,
  currency: 'USD',
  [Symbol.toPrimitive](hint) {
    if (hint === 'number') return this.amount;
    if (hint === 'string') return `${this.amount} ${this.currency}`;
    return this.amount; // default
  }
};

console.log(+money + 50); // 300
console.log(`${money}`);  // "250 USD"

2. WeakMap і WeakSet: чыстка памяці без ручнага ведэння

Ёсць сэнс у WeakMap, якщо спачатку паглядзець, як звычайны Map адносіцца да памяці.

Звычайны Map зберагае сильную ссылку на кожны ключ і значэнне, якія знаходзяцца ў ям. Это значыць, што пакуль сам Map застаецца жывым, ніякі з яго ключоў не можа быць выкарыстаны для чысткі памяці — нават пасля таго, як усе іншыя часткі вашаеў прыемлена пересталі на яныс ссылавацца.

let cache = new Map();
let element = document.querySelector('#heavy-widget');

cache.set(element, { clicks: 0 });

// Later, the DOM node is removed:
element.remove();
element = null;

// Problem: The DOM node is still stuck in memory because `cache` holds it.

Это ўзніклы канфлікт, які часта стварае выцэрпанне памяці на стороне кліента, бальшаваць у додатках з адной сторанай, дзе корыстувальнікі пераходзяць между відобразамі без повнага перзавантажэння стораны для скасавання выцэрпання памяці.

Сюды і вступае WeakMap. Ён зберагае толькі слабыя канекты да своіх ключоў, што мае два важлівыя наследкі:

  1. Ключы павінны быть об’ектамі (або, у сучасных двыжках, незарэўнаванымі сімволамі) — прымітывы, такія як строкі чыста цифры, не можу быць ключамі.
  2. Калі ў вашым праекте больш няма нічога, што канектвацца да об’екта-клуча, сборчык сметніка можа яго звярнуць, і адпаведны элемент WeakMap знікне разам з ям.
const metadataStore = new WeakMap();

function setupWidget(domNode) {
  metadataStore.set(domNode, { initializedAt: Date.now() });
}

// When domNode is removed from the DOM and its variable goes out of scope,
// the WeakMap entry is garbage-collected automatically.

Компрэсіі, якія ўжо ўключаныя ў дзеянне

Паколькі збір сметлі выканае ў неперакладзенныя часы, якія выбірае браузер, а не ваш код, WeakMap спецыяльна обмежвае тое, што можна з яго зробіць:

  • Ён не ўможлівы для перэходу па элементах: няма .forEach(), няма for...of, няма .keys().
  • Няма атрыбута .size, таму нельга выявіць, сколькі элементаў зараз існуе.
  • Доступныя толькі чатыры методы: .get(), .set(), .has() і .delete().

Якбы было можна перэйсці па ключоўых атрыбутах чытаць значэння розмеру, результаты залежалі бы ад таго, чы не выканана ўжо збір сметлі — чыя бы це прывела да таго, што поведанне вашай программы стала б фактычна неперакладзенным. Усуненне можлівасці перэходу па элементах дапамагае заставіць API прыемлівым незалежна ад часу выканання збору сметлі.

Практычны патэрн: справжняй прыватны стан экземпляра

Да чынныя прыватныя поля класа (#field) не сталі шырока падтрымлваныя, WeakMap быў стандартным спосабам для стварэння сапраўдзіва прыватнага внутранняга стану экзаментаў класа:

const privateData = new WeakMap();

class BankAccount {
  constructor(initialBalance) {
    privateData.set(this, { balance: initialBalance });
  }

  deposit(amount) {
    const data = privateData.get(this);
    data.balance += amount;
  }

  getBalance() {
    return privateData.get(this).balance;
  }
}

const account = new BankAccount(100);
account.deposit(50);
console.log(account.getBalance()); // 150
console.log(account.balance);       // undefined (completely inaccessible)

Пакалі сам экзамент (this) выкорыстоўваецца як ключ, калі экзамент больш нігде не выкарыстоўваецца, яго прыватныя даныя автаматычна адчышваюцца разам з ям.

3. Proxy і Reflect: метапрограмаванне, якое можна выкарыстоўваць сёння

Proxy абгортае об’ект і дазваляе перехопляць яго найбазавейшыя операцыі — чытанне атрыбута, прызначэнне яго, вызов функцыі або выдаленне ключа.

Якща вы калі-небудзь адкладвалі змены ў рэактыўным стане ў Vue 3 або спазіравалі, як сучасная бібліятэка рэактыўнасці автаматычна фіксуе змены, вы ведалі справу з Proxy у тылу кадроў.

const targetUser = { name: 'Alex', age: 28 };

const handler = {
  get(target, prop, receiver) {
    console.log(`Reading property "${prop}"`);
    return Reflect.get(target, prop, receiver);
  },
  set(target, prop, value, receiver) {
    if (prop === 'age' && typeof value !== 'number') {
      throw new TypeError('Age must be a valid number.');
    }
    console.log(`Setting "${prop}" to ${value}`);
    return Reflect.set(target, prop, value, receiver);
  }
};

const monitoredUser = new Proxy(targetUser, handler);

monitoredUser.age = 29; // Logs: Setting "age" to 29
console.log(monitoredUser.name); // Logs: Reading property "name" -> "Alex"
// monitoredUser.age = 'thirty'; // Throws TypeError

Вы можаце пазіць, як у адпрацоўвальніку Proxy выкарыстоўваюцца Reflect.get() і Reflect.set(). Reflect — это вбудованы глобальны об’ект, які мае методы, якія відтворяюць операцыі, якія Proxy можа перехопіць. Кожная «пастка», якую можна запрацаваць у адпрацоўвальніку Proxy, мае вялікі адпаведны метод у Reflect, який прыме тыя ж аргументы. У замест на таго, каб ручна пісаць ў методе set ўзор на кшталт target[prop] = value — што можа спрацаваць некоректна, калі ў гэтым участку є протатыпы чы атрыбуты-дзержальнікі — вы вызываеце вялікі метод Reflect, який безпечна выканае стандартную дзейнасць двайчы і паведаміце вам булева значэнне, якое паказвае, чы рэальна аперацыя завершылася успехам.

4. FinalizationRegistry і WeakRef: стежыць за процесам збіркі сметлів

Дзеўнаць часоў у JavaScript не было спосабу выявіць, калі об’ект насправды быў перэйманы працою двайчыка. ES2021 заполніў гэты прыём двумя скарыстаемымі API низкага рэвэля, прызначанымі для програмавання у стыле распрацоўкі: WeakRef і FinalizationRegistry.

WeakRef зберагае аднос да об’екта, не спяшаючы процес перэймання аб’екта гарбажным колектаром, пры тым даўаючы можлівасць атрымаць об’ект, калі ён яшчэ жывы:

let heavyAsset = { buffer: new ArrayBuffer(1024 * 1024 * 16) }; // 16MB buffer
const assetRef = new WeakRef(heavyAsset);

// Dereference to access
const asset = assetRef.deref();
if (asset) {
  // Asset still exists in memory
  console.log("Using cached asset");
} else {
  // Asset was collected by GC; reload it
  console.log("Asset was garbage collected");
}

FinalizationRegistry дапамагае ў гэтым, дазволяючы зарэўнаваць калбек, які запускаецца пасля таго, как об’ект быў перэйманы. Яго зазвычай выкарыстоўваюць для ачышчэння зовнішніх рэсурсоў, прычастных да гэтага об’екта, або для логавання дыягностычных даных:

const registry = new FinalizationRegistry((heldValue) => {
  console.log(`Cleanup hook: Resource "${heldValue}" was garbage collected.`);
});

(() => {
  const temporaryWorker = { id: 'worker_404' };
  registry.register(temporaryWorker, temporaryWorker.id);
  // temporaryWorker leaves scope here
})();

Будзьце абярожны ў выкарыстоўванні гэтых двух API. Час выконання процедуры збору сметлівыя кашы разніцяюцца ў розных прыгледачах і дварчыках JavaScript і не можа быць прыгаданы чы абмусіў. Не трэба ніколі прыяёднваць важлівую логіку прыкладнення — зберагчэнне стану, параболка транзакцыі, ўсё, чаму павярнута ваша прыкладнае програма — да функціі-фіналізатора, адтак як няма гаранцый ў тым, калі, чы нават чы будзе яна выканана без зволікання.

5. Функцыі-генераторы: паўза і продаж чынення за запатранэннем

Звычныя функцыі JavaScript выконваюцца да ўсунення канца: як толькі яны вызваны, яны продолжаюць выконвацца пакуль не будзе вызначана оператыва return чы не будзе выклікана асывяцыя, без жадных паўз.

Функцыі-генераторы, заданыя сынтаксам function*, нарадзіваюць гэтую правілу. Яны можаць паўзіць выкананне і продаж чынення пазней, выкарыстоўваючы ключавае слова yield для пазначэння точак паўзы.

function* idGenerator() {
  let id = 1;
  while (true) {
    yield `UID_${id++}`;
  }
}

const gen = idGenerator();
console.log(gen.next().value); // UID_1
console.log(gen.next().value); // UID_2
console.log(gen.next().value); // UID_3

Атрыміце дзейсныя спэцыялізаваныя пагляд на цикл while (true) усередзінны ў тым генераторы. У звычной функцыі такі бесканечны цикл негайна заблокаваў бы вядро. Аднак у генераторы выконанне зупінаецца як толькі доходзіць да yield, вяртаючы кантроль таму, хто яго вызваў — нічога больш не выкананая пакуль знову не будзе вызваны .next().

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

function* processLargeLogFile(lines) {
  for (const line of lines) {
    if (line.includes('ERROR')) {
      yield line.trim();
    }
  }
}

// Memory remains flat because lines are yielded one by one as consumed
for (const errorLine of processLargeLogFile(rawLines)) {
  sendToAlertService(errorLine);
}

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

Калі наступны раз вы прыўязуеце метаданы да вузлаў DOM, якія дынамічна з’являюцца і зникаюць, выкарыстоўваеце WeakMap. Калі проектуеце архітэктуру плагінаў, дзе зовнішні код павінен визначаць сабе унікальныя ключы, выкарыстоўваеце Symbol. А калі вам патрэбны реакtyўны слой дадзеных, які автаматычна фіксуе змяны, ствараеце яго на адной з базоў Proxy.

Звыканне да гэтых інструментаў — гэта тое, што адзначае разліку межы напісання коду для прыкладных програм і стварэння программнага забезпечэння, прызначанага для масштабавання.

Спаднія матэрыялы

  • Дзесяць схованых проблем компонентаў React, які спамляюць сучасныя дапытлі — Дазвольце вам дакладна пазнакоміцца з дзесяцьма распашчастымііямі, якія часта з’яўляюцца ў компонентах React — ад недастатку семантычнага HTML да відсутнасці механізмаў мемаізацыі — і з спосабамі ўсунення гэтых проблем, каб дапытлі застаўаліся быстрымі, доступнымі і без бягаў у 2026 годзе.
  • Кэш карт сыроўкі Node — тыхае вытэканне памяці ў режыме разработкі — Дазвольце вам дакладна пазнакоміцца з прычынамі, чырэз якія актываўванне параметраў --enable-source-maps аб NODE_V8_COVERAGE можа спрычыніць несканавальнае зростанне памяці чырэз паўтаральныя вызывы функцый eval, а таксама з спосабамі дыагнастыкі і усунення гэтай проблемы ў дзесяньдзеў.
  • Адказ пра скрытыя цены праз адвёрсныя інжынерыя фонавых вызоў API — Навучыцеся выкарыстоваць інструменты DevTools прыборкі, каб знаходзіць і вярнаваць запыты XHR, REST і GraphQL, якія заваносяць цены, не патрэбнае парсаванне HTML.