IlmHamroh
JavaScript Full-stack/12-qism. JavaScript: ilg'or mavzular va kod sifati19/44-dars23 daqiqa
Mundarija (34)

JavaScript tagged template literals: teg funksiyasi, String.raw va xavfsiz HTML

Qisqacha: Tagged template — backtick oldiga funksiya nomi yozilgan template literal: html`<li>${izoh}</li>`. JavaScript bunday satrni o'zi yig'maydi. U matn bo'laklarini massiv qilib, ${} qiymatlarini esa alohida argumentlar qilib funksiyaga beradi: html(qismlar, ...qiymatlar). Funksiya ulardan nima yasashni o'zi hal qiladi — masalan, har qiymatni ekranlab, XSS'dan himoyalangan HTML yasaydi.

Bu darsda

  • Teg funksiyasi nima olishini (qismlar, ...qiymatlar) va ular qanday bo'linishini tushuntira olasiz.
  • Qismlar va qiymatlarni qayta yig'adigan o'z tegingizni yozasiz.
  • strings.raw va String.raw bilan teskari chiziqni "boricha" olishni bilasiz.
  • Foydalanuvchi matnini avtomatik ekranlaydigan html tegini yozasiz va uning chegaralarini aytib bera olasiz.
  • sql, css, gql teglari real loyihalarda nima uchun ishlatilishini tushunasiz.

Oldin bilishingiz kerak: Template literals, Spread va rest operatori, Kontentni o'zgartirish va XSS xavfi, reduce.

1. Nega bu kerak?

Oldingi darsda Intl.Collator va Intl.Segmenter bilan matnni saraladik va bo'laklarga ajratdik. Bugun matnni yig'ish jarayonini qo'lga olamiz.

«Bahor» saytida "Mehmonlar fikri" bo'limi bor. Stajyor Sardor uni template literal bilan chizdi:

js
royxat.innerHTML = `<li><b>${ism}:</b> ${izoh}</li>`;

Qisqa va o'qilishi oson. Lekin Kontentni o'zgartirish va XSS xavfi darsini eslang. izoh mehmon yozgan matn. Unda <img src="x" onerror="..."> bo'lsa, brauzer uni HTML deb o'qiydi va hujumchining kodi ishga tushadi. O'sha darsda ikki yo'l ko'rgan edik: textContent yoki har qiymatni ekranla funksiyasidan o'tkazish.

Ikkinchi yo'lning muammosi — unutish. Sardor o'nta joyda ${ekranla(izoh)} yozadi, o'n birinchisida esa shoshilib ${izoh} deb qo'yadi. Bitta unutilgan chaqiruv saytni ochib beradi.

Yaxshiroq bo'lardi, agar template literal o'zi har bir ${} qiymatini ekranlasa. Bu mumkin. Uning nomi — tagged template. Template literals darsida "backtick oldiga funksiya nomi yoziladi va u qiymatlarni xavfsiz qilishi mumkin" degan edik. 06-qismda esa styled.p`...` yozuvini ko'rgan edik (CSS-in-JS). Bugun o'sha va'dalarni bajaramiz.

Hayotdan o'xshatish: oshxonadagi kassir. Ofitsiant buyurtmani to'g'ridan-to'g'ri oshxonaga aytmaydi — avval kassaga beradi. Kassir har bir taomni tekshiradi, narxini qo'yadi, keyin oshxonaga uzatadi. Teg funksiyasi — ana shu kassir: template literalning har bir bo'lagi undan o'tadi.

2. Teg funksiyasi nima oladi

2.1 Birinchi teg

Tagged template (teg qo'yilgan shablon) — oldiga funksiya nomi yozilgan template literal. Bu funksiya teg (tag) deb ataladi. Qavs yozilmaydi: funksiya nomi va backtick yonma-yon turadi.

Teg nima olishini ko'rish uchun eng sodda funksiyani yozamiz. U hech narsa yig'maydi, faqat argumentlarini konsolga chiqaradi:

js
function korsat(qismlar, ...qiymatlar) {
  console.log(qismlar);
  console.log(qiymatlar);
}

const taom = "Osh";
const narx = 35000;

korsat`${taom} narxi: ${narx} so'm`;

Konsolda:

text
[ '', ' narxi: ', " so'm" ]
[ 'Osh', 35000 ]

korsat`...` — bu funksiya chaqiruvi. JavaScript template literalni ikkiga ajratdi:

  • qismlar — ${} lar orasidagi oddiy matn bo'laklari. Ular har doim satr.
  • qiymatlar — ${} ichidagi ifodalarning natijalari. Ular satrga aylantirilmaydi: 35000 son bo'lib qoldi.

Birinchi argument — qismlar massivi. Qolgan argumentlar — qiymatlar, har biri alohida. Ularni bitta massivga yig'ish uchun rest parametri ...qiymatlar ishlatdik.

Pastdagi Keyingi tugmasini bosib, JavaScript literalni qanday "kesishini" qadamma-qadam kuzating:

2.2 Bo'linish qoidasi

Vizualdagi oxirgi gapga qayting: qismlar doim qiymatlardan bittaga ko'p. Sababi oddiy. Har bir ${} ning chap va o'ng tomonida matn bo'lagi bor — hatto u bo'sh bo'lsa ham. Ikkita ${} matnni uch bo'lakka ajratadi, xuddi ikkita kesish nonni uch bo'lakka bo'lgandek.

Shuning uchun literal ${} bilan boshlansa, birinchi qism — bo'sh satr ''. ${} bilan tugasa, oxirgi qism ham bo'sh. Hatto faqat ikkita qiymatdan iborat literal ham uchta qism beradi:

js
function korsat(qismlar, ...qiymatlar) {
  console.log(qismlar.length, qiymatlar.length);
}

korsat`${1}${2}`;
korsat`Qiymatsiz matn`;

Konsolda:

text
3 2
1 0

Ikkinchi chaqiruvda ${} yo'q — bitta qism, nol qiymat. Qoida bu yerda ham ishlaydi: 0 + 1 = 1.

Bu qoida keyingi bo'limdagi yig'ish uchun kalit: qismlar[0], keyin qiymatlar[0], keyin qismlar[1], keyin qiymatlar[1] va oxirida yana bitta qism. Qism, qiymat, qism, qiymat, qism — doim qism bilan boshlanib, qism bilan tugaydi.

Tekshirib ko'ring: korsat`Stol ${4}, soat ${"19:00"}.` chaqiruvida qismlar va qiymatlar nimaga teng?

Javob

qismlar — [ 'Stol ', ', soat ', '.' ], qiymatlar — [ 4, '19:00' ]. Ikkita ${} matnni uch bo'lakka ajratdi. Oxirgi qism — nuqta, chunki literal ${} bilan emas, . bilan tugadi.

2.3 Qayta yig'ish: o'z tegingiz

Teg qismlar va qiymatlarni oldi. Endi ulardan satr yasaymiz. Har qiymatni yig'ishdan oldin o'zgartirish mumkin — tegning butun kuchi shunda.

«Bahor» kassasi uchun chek tegini yozamiz. U har bir sonni uch xonadan ajratib, oxiriga "so'm" qo'shadi. Matn qismlariga tegmaydi:

js
function summaniYoz(qiymat) {
  if (typeof qiymat !== "number") return qiymat;
  const ajratilgan = qiymat
    .toLocaleString("en-US")
    .replaceAll(",", " ");
  return `${ajratilgan} so'm`;
}

function chek(qismlar, ...qiymatlar) {
  let natija = qismlar[0];
  for (let i = 0; i < qiymatlar.length; i++) {
    natija += summaniYoz(qiymatlar[i]) + qismlar[i + 1];
  }
  return natija;
}

const taom = "Osh";
console.log(chek`${taom}: ${35000}, jami: ${105000}`);

Konsolda:

text
Osh: 35 000 so'm, jami: 105 000 so'm

Kodni qatorma-qator o'qiymiz:

  1. natija birinchi qism bilan boshlanadi — u har doim bor (bo'sh bo'lsa ham).
  2. Sikl har bir qiymat uchun ikki narsa qo'shadi: o'zgartirilgan qiymat va undan keyingi qism. Shuning uchun qismlar[i + 1].
  3. taom son emas — summaniYoz uni o'zgarishsiz qaytaradi. Sonlar esa formatlanadi.

toLocaleString("en-US") sonni 105,000 ko'rinishiga keltiradi, replaceAll esa vergulni bo'sh joyga almashtiradi. Formatlashning to'liq yo'li — Intl.NumberFormat darsida; bu yerda natija har kompyuterda bir xil chiqishi muhim edi.

2.4 Xuddi shu yig'ish — reduce bilan

Yuqoridagi sikl — har bir tegning qolipi. Uni reduce bilan qisqaroq yozish mumkin. Boshlang'ich qiymat berilmasa, reduce akkumulyatorni massivning birinchi elementi bilan boshlaydi va i = 1 dan aylanadi:

js
function birlashtir(qismlar, ...qiymatlar) {
  return qismlar.reduce(
    (natija, qism, i) => natija + qiymatlar[i - 1] + qism,
  );
}

console.log(birlashtir`Stol ${4}, soat ${"19:00"}.`);

Konsolda:

text
Stol 4, soat 19:00.

i = 1 da qiymatlar[0] va qismlar[1] qo'shiladi, i = 2 da — qiymatlar[1] va qismlar[2]. Natija tegsiz template literal bilan bir xil. Bu birlashtir — "hech narsa qilmaydigan" teg. Uni asos qilib, qiymatlar[i - 1] o'rniga ekranla(qiymatlar[i - 1]) kabi istalgan ishlovni qo'yasiz.

Sikl yangi o'quvchiga tushunarliroq, reduce esa kutubxonalarda ko'proq uchraydi — biz ikkalasini ham ishlatamiz.

2.5 Teg satr qaytarishi shart emas

Teg — oddiy funksiya. U istalgan narsani qaytarishi mumkin: son, obyekt, hatto boshqa funksiya. Masalan, bu teg matnni emas, uning "tuzilmasini" qaytaradi:

js
function tuzilma(qismlar, ...qiymatlar) {
  return { matnlar: [...qismlar], sonlar: qiymatlar.length };
}

console.log(tuzilma`Bron: ${"Dilshod aka"}, ${4} kishi`);

Konsolda:

text
{ matnlar: [ 'Bron: ', ', ', ' kishi' ], sonlar: 2 }

Bu imkoniyat ahamiyatli. «Hujumchi nigohi» bo'limidagi sql tegi aynan obyekt qaytaradi — satr emas. Shu bilan u xavfli birlashtirishning o'zini imkonsiz qiladi.

Tekshirib ko'ring: Teg funksiyasi return yozmasa, const x = korsat`salom`; dan keyin x nimaga teng?

Javob

undefined. Teg chaqiruvi — oddiy funksiya chaqiruvi. Funksiya return qilmasa, natija undefined (return va erta qaytish). Template literal o'zi hech qanday satr yasamaydi — buni teg qilishi kerak.

3. raw: teskari chiziqni boricha olish

3.1 Ikki xil qism: tayyor va xom

String asoslari darsidan bilasiz: satrda \n — yangi qator, \t — tabulyatsiya. JavaScript satrni o'qiyotganda bunday escape ketma-ketliklarini darhol haqiqiy belgiga aylantiradi.

Teg qismlarni ikki xil ko'rinishda oladi:

  • qismlar[i] — tayyor (cooked): escape'lar allaqachon belgiga aylangan;
  • qismlar.raw[i] — xom (raw): kodda qanday yozilgan bo'lsa, shunday. \n — ikkita belgi: \ va n.
js
function ikkalasi(qismlar) {
  console.log(qismlar[0].length, qismlar.raw[0].length);
  console.log(qismlar.raw[0]);
}

ikkalasi`Osh\nManti`;

Konsolda:

text
9 10
Osh\nManti

Tayyor qismda 9 belgi: \n bitta yangi qator belgisiga aylandi. Xom qismda 10 belgi: \ va n alohida turibdi. raw — qismlar massivining xususiyati. U sanalmaydigan (Xususiyat deskriptorlari), shuning uchun console.log(qismlar) uni ko'rsatmaydi.

3.2 String.raw: tayyor teg

Xom matn shunchalik kerakki, JavaScript'da u uchun tayyor teg bor — String.raw. U qismlarning xom ko'rinishini qiymatlar bilan birlashtiradi:

js
const yil = 2026;
const yol = String.raw`C:\Users\Sardor\new\menyu-${yil}.txt`;

console.log(yol); // C:\Users\Sardor\new\menyu-2026.txt

Windows yo'lidagi \U, \S, \n, \m — hech biri escape deb o'qilmadi. \n ham yangi qatorga aylanmadi. ${yil} esa odatdagidek qiymatga almashdi.

Bitta nozik joy bor. \ dan keyin darhol ${ kelsa, teskari chiziq dollarni ekranlaydi va ${...} oddiy matn bo'lib qoladi. Shuning uchun qiymatni papka nomining o'rtasiga qo'ydik, \${yil} deb emas.

Eng foydali joyi — regex. RegExp asoslari darsida new RegExp("\\d+") uchun teskari chiziqni ikki marta yozishga to'g'ri kelgan edi. String.raw bilan bitta yetarli:

js
const naqsh = new RegExp(String.raw`\+998 \d{2}`, "g");
const matn = "Tel: +998 90 000 00 00";

console.log(naqsh.source); // \+998 \d{2}
console.log(matn.match(naqsh)); // [ '+998 90' ]

Naqsh literal /.../ dagidek o'qiladi, lekin satr sifatida yig'ilyapti. Demak, uning ichiga ${} bilan qiymat qo'shsa ham bo'ladi.

Diqqat: String.raw ichiga foydalanuvchi matnini ${} bilan qo'yib regex yasash xavfli: matndagi . yoki + regex buyrug'iga aylanadi. Xavfsiz yo'li — Almashtirish chuqur va xavfsiz dinamik regex darsidagi RegExp.escape.

3.3 Noto'g'ri escape: tegsiz — xato, teg bilan — undefined

\u — Unicode escape boshlanishi. Undan keyin to'rtta o'n oltilik raqam kutiladi: \u0041 — A. Papka nomi kichik u bilan boshlansa, Windows yo'lida aynan \u paydo bo'ladi: C:\users. Undan keyin sers keladi — bu o'n oltilik raqamlar emas. Tegsiz template literal buni xato deb hisoblaydi:

js
const yol = `C:\users\sardor`;
text
SyntaxError: Invalid Unicode escape sequence

Tarjimasi: "Noto'g'ri Unicode escape ketma-ketligi." Kod ishga tushmaydi ham — xato o'qish bosqichida chiqadi. Katta \U esa xato bermaydi: u escape emas va oddiy U harfiga aylanadi.

Teg qo'yilgan literalda esa bunday escape xato emas (ES2018 dan beri). Tayyor qism undefined bo'ladi, xom qism esa saqlanadi:

js
function korsat(qismlar) {
  console.log(qismlar[0], qismlar.raw[0]);
}

korsat`C:\users\sardor`;
console.log(String.raw`C:\users\sardor`); // C:\users\sardor

Konsolda:

text
undefined C:\users\sardor
C:\users\sardor

Mantiqi shunday: tegsiz literalda JavaScript satrni o'zi yasashi kerak va noto'g'ri escape'ni hech narsaga aylantira olmaydi. Teg qo'yilganda esa satrni teg yasaydi. Unga xom matn kerak bo'lishi mumkin — shuning uchun JavaScript xato bermaydi, faqat tayyor ko'rinishni undefined qiladi.

Tekshirib ko'ring: String.raw`a\tb`.length nimaga teng? `a\tb`.length -chi?

Javob

Birinchisi — 4: a, \, t, b. String.raw escape'ni aylantirmaydi. Ikkinchisi — 3: \t bitta tabulyatsiya belgisiga aylandi.

4. Qismlar massivi haqida ikki nozik fakt

4.1 Massiv muzlatilgan

Teg olgan qismlar massivi — muzlatilgan. Unga yozib bo'lmaydi:

js
function tekshir(qismlar) {
  console.log(Object.isFrozen(qismlar), Object.isFrozen(qismlar.raw));
}

tekshir`Osh ${1} so'm`;

Konsolda:

text
true true

Bu — himoya. Qismlar kodning o'zidan olingan, ya'ni dasturchi yozgan matn. Teg ularni o'zgartira olmasligi kerak.

4.2 Har chaqiruv joyi uchun bitta massiv

Ikkinchi fakt kutilmaganroq. Kodning bitta joyidagi teg har safar aynan o'sha qismlar massivini oladi. Qiymatlar har safar yangi, qismlar esa — bir xil obyekt:

js
function qismlarniOl(qismlar) {
  return qismlar;
}

function narxQatori(narx) {
  return qismlarniOl`Narx: ${narx}`;
}

console.log(narxQatori(35000) === narxQatori(28000)); // true
const birinchi = qismlarniOl`Narx: ${1}`;
const ikkinchi = qismlarniOl`Narx: ${1}`;
console.log(birinchi === ikkinchi); // false

Birinchi console.log da narxQatori ikki marta chaqirildi, lekin ichidagi teg kodning bitta joyida turibdi. Shuning uchun ikkala chaqiruv === bilan teng massiv oldi (Havola semantikasi). birinchi va ikkinchi da esa ikkita alohida literal — kodning ikki joyi. Matni bir xil bo'lsa ham, massivlari har xil.

Bu nimaga kerak? Shablonni keshlash (bir marta hisoblangan natijani eslab qolish) uchun. Lit kutubxonasi (3.3 versiya) bitta html`...` shablonini birinchi marta tahlil qiladi va natijani qismlar massivi kaliti ostida eslab qoladi. Keyingi chaqiruvlarda faqat o'zgargan qiymatlarni yangilaydi. Lit'ni hozir bilish shart emas.

5. Xavfsiz HTML: html tegi

5.1 Birinchi variant

Endi «Nega bu kerak?» bo'limidagi masalaga qaytamiz. XSS darsidagi ekranla funksiyasini olamiz. U beshta maxsus belgini entity'ga almashtiradi — & birinchi, aks holda keyingi entity'lar buziladi. Keyin uni teg ichida har bir qiymatga qo'llaymiz:

js
function ekranla(matn) {
  return String(matn)
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll('"', "&quot;")
    .replaceAll("'", "&#39;");
}

function html(qismlar, ...qiymatlar) {
  return qismlar.reduce(
    (natija, qism, i) => natija + ekranla(qiymatlar[i - 1]) + qism,
  );
}

const izoh = '<img src="x" onerror="alert(1)">';
console.log(html`<li><b>Mehmon:</b> ${izoh}</li>`);

Konsolda:

text
<li><b>Mehmon:</b> &lt;img src=&quot;x&quot; onerror=&quot;alert(1)&quot;&gt;</li>

Natijaga qarang. <li> va <b> teglari tegilmadi — ular qismlarda, ya'ni dasturchi yozgan matnda. Hujumchining <img> i esa qiymatda edi va ekranlandi. Teg ikkalasini avtomatik farqlaydi: qism — ishonchli, qiymat — begona.

String(matn) nega kerak? Qiymat son bo'lishi mumkin: ${35000}. Sonda replaceAll metodi yo'q. String uni avval satrga aylantiradi.

Endi Sardor ${ekranla(izoh)} yozishni unuta olmaydi — u umuman yozmaydi. html tegini qo'ygan har bir joyda ekranlash avtomatik.

5.2 Brauzerda: hujum ishlamaydi

Xuddi shu tegni haqiqiy sahifada sinab ko'ramiz. Ikki izohdan biri — hujum. Agar u ishlasa, konsolda XSS yozuvi chiqadi:

html
<ul id="izohlar"></ul>
<script>
  function ekranla(matn) {
    return String(matn)
      .replaceAll("&", "&amp;")
      .replaceAll("<", "&lt;")
      .replaceAll(">", "&gt;")
      .replaceAll('"', "&quot;")
      .replaceAll("'", "&#39;");
  }
  function html(qismlar, ...qiymatlar) {
    return qismlar.reduce(
      (natija, qism, i) => natija + ekranla(qiymatlar[i - 1]) + qism,
    );
  }

  const izohlar = [
    "Osh zo'r, xizmat a'lo!",
    '<img src="x" onerror="console.log(\'XSS\')">',
  ];
  const royxat = document.querySelector("#izohlar");
  royxat.innerHTML = izohlar
    .map((izoh) => html`<li><b>Mehmon:</b> ${izoh}</li>`)
    .join("");
  console.log(royxat.children.length);
  console.log(royxat.querySelectorAll("img").length);
</script>

Konsolda:

text
2
0

Ikkita li chizildi, img esa bitta ham yo'q — hujumchining yozuvi ekranda oddiy harflar bo'lib turibdi. <b>Mehmon:</b> esa qalin. "Tahrirlab ko'rish" ni bosing va html so'zini o'chirib, oddiy template literal qiling. Konsolda XSS chiqadi va img soni 1 bo'ladi.

5.3 Ichma-ich shablonlar

Birinchi variantda bitta muammo bor. Ro'yxatni tegning ichida yasashga urinib ko'raylik:

js
function ekranla(matn) {
  return String(matn)
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;");
}

function html(qismlar, ...qiymatlar) {
  return qismlar.reduce(
    (natija, qism, i) => natija + ekranla(qiymatlar[i - 1]) + qism,
  );
}

const taomlar = ["Osh", "Manti"];
const ichki = taomlar.map((taom) => html`<li>${taom}</li>`);

console.log(html`<ul>${ichki}</ul>`);

Konsolda:

text
<ul>&lt;li&gt;Osh&lt;/li&gt;,&lt;li&gt;Manti&lt;/li&gt;</ul>

Ikki xato birdaniga. Birinchidan, ichki html allaqachon xavfsiz qilgan <li> teglar ikkinchi marta ekranlandi va endi harflar bo'lib ko'rinadi. Ikkinchidan, massiv satrga aylanganda elementlari vergul bilan ulandi (massivni matnga aylantirish).

Teg "bu satr allaqachon xavfsiz" ekanini bilishi kerak. Oddiy satrda bunday belgi yo'q. Shuning uchun html endi oddiy satr emas, maxsus klass nusxasini qaytaradi. Teg qiymatni ekranlashdan oldin tekshiradi: bu nusxa bo'lsa — ekranlamaydi, massiv bo'lsa — har elementini tayyorlab, bo'sh satr bilan ulaydi:

js
class XavfsizHtml {
  constructor(matn) {
    this.matn = matn;
  }
  toString() {
    return this.matn;
  }
}

function ekranla(matn) {
  return String(matn)
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll('"', "&quot;")
    .replaceAll("'", "&#39;");
}

function tayyorla(qiymat) {
  if (qiymat instanceof XavfsizHtml) return qiymat.matn;
  if (Array.isArray(qiymat)) return qiymat.map(tayyorla).join("");
  return ekranla(qiymat);
}

function html(qismlar, ...qiymatlar) {
  const matn = qismlar.reduce(
    (natija, qism, i) => natija + tayyorla(qiymatlar[i - 1]) + qism,
  );
  return new XavfsizHtml(matn);
}

const taomlar = ["Osh", "Manti <yangi>"];
const ichki = taomlar.map((taom) => html`<li>${taom}</li>`);

console.log(String(html`<ul>${ichki}</ul>`));

Konsolda:

text
<ul><li>Osh</li><li>Manti &lt;yangi&gt;</li></ul>

Ichki <li> lar tirik, vergul yo'q, <yangi> esa baribir ekranlandi — chunki u ichki shablonning qiymati edi. toString metodi nusxani kerak joyda satrga aylantiradi: String(...), innerHTML = ... yoki boshqa template literal ichida (class sintaksisi).

XavfsizHtml — "tekshirilgan" degan muhr. Muhrni faqat html tegi qo'yadi. Shuning uchun begona satr muhrsiz keladi va doim ekranlanadi.

Tekshirib ko'ring: Yuqoridagi tegda html`<p>${new XavfsizHtml(izoh)}</p>` deb yozilsa va izoh — mehmon matni bo'lsa, nima bo'ladi?

Javob

XSS. tayyorla XavfsizHtml nusxasini ekranlamaydi — u muhrga ishonadi. Muhrni qo'lda, begona matnga bossangiz, himoya o'chadi. Shuning uchun real kutubxonalarda bunday "ishonchli HTML" yasash funksiyasining nomi qo'rqinchli qilib qo'yiladi (React'da dangerouslySetInnerHTML — "xavfli"), toki ishlatgan odam o'ylab ko'rsin.

5.4 Ekranlash yetmaydigan joylar

html tegi juda ko'p holatni yopadi, lekin hammasini emas. Ekranlash qiymat matn yoki qo'shtirnoqli atribut ichiga tushganda ishlaydi. Uchta xavfli joy qoladi:

  • href va src: html`<a href="${havola}">` da havola javascript:alert(1) bo'lsa, bu satrda ekranlanadigan belgi yo'q. Bosilganda kod ishlaydi. Havolani alohida tekshirish kerak (https: bilan boshlanadimi).
  • Qo'shtirnoqsiz atribut: html`<td class=${sinf}>` da sinf ichidagi bo'sh joy yangi atribut ochadi: x onclick=.... Atribut qiymati doim qo'shtirnoq ichida bo'lsin.
  • <script> va <style> ichi: u yerda HTML qoidalari emas, JavaScript va CSS qoidalari ishlaydi. Begona qiymatni u yerga qo'ymang.

Bu holatlarni to'liq XSS va DOM xavfsizligi darsida ko'ramiz. Real loyihalarda o'z tegingiz o'rniga tekshirilgan vositalar ishlatiladi: Lit 3.3-bob — html tegi bilan, DOMPurify 3.4-bob — tayyor HTML'ni tozalash uchun. Bugungi teg — ularning ichida nima borligini tushunish uchun.

Hujumchi nigohi

«Bahor»ning bron tizimi serverda ma'lumotlar bazasidan mehmonni ism bo'yicha qidiradi. Bazaga savol SQL tilida yoziladi — ma'lumotlar bazasining so'rov tili (26-qismda o'rganamiz, hozir bilish shart emas). Sardorning birinchi variantida so'rov template literal bilan yasalgan:

js
const ism = "x' OR '1'='1";
const sorov = `SELECT * FROM bronlar WHERE ism = '${ism}'`;

console.log(sorov);

Konsolda:

text
SELECT * FROM bronlar WHERE ism = 'x' OR '1'='1'

Hujumchi formaga ism o'rniga x' OR '1'='1 yozdi. Uning qo'shtirnog'i Sardorning qo'shtirnog'ini yopdi. Keyin OR '1'='1' qo'shildi — bu shart har doim rost. Natijada baza barcha mehmonlarning bronlarini qaytaradi: ismlar, telefonlar. Bu hujumning nomi — SQL injection (SQL ga kod "ukol qilish"). U OWASP Top 10 ro'yxatida — veb-ilovalarning eng xavfli zaifliklari orasida — ko'p yillardan beri turibdi.

Himoya — qiymatni so'rov matniga umuman qo'shmaslik. Parametrlangan so'rov (parameterized query): so'rov matnida qiymat o'rniga $1, $2 belgilari turadi, qiymatlar esa bazaga alohida ro'yxat bo'lib boradi. Baza ularni hech qachon kod deb o'qimaydi. sql tegi aynan shuni yasaydi:

js
function sql(qismlar, ...qiymatlar) {
  const matn = qismlar.reduce(
    (natija, qism, i) => `${natija}$${i}${qism}`,
  );
  return { matn, qiymatlar };
}

const ism = "x' OR '1'='1";
const sorov = sql`SELECT * FROM bronlar WHERE ism = ${ism}`;

console.log(sorov.matn);
console.log(sorov.qiymatlar);

Konsolda:

text
SELECT * FROM bronlar WHERE ism = $1
[ "x' OR '1'='1" ]

`${natija}$${i}${qism}` — birinchi $ oddiy dollar belgisi, undan keyingi ${i} — qiymat. Shunday qilib $1, $2 hosil bo'ladi.

Hujumchining matni so'rovga kirmadi. U alohida ro'yxatda turibdi va baza uni "ism shu satrga teng bo'lgan qator" deb qidiradi — bunday ism yo'q, natija bo'sh. Teg satr emas, obyekt qaytargani uchun dasturchi uni xato bilan oddiy satrdek ishlatib ham yubora olmaydi.

postgres kutubxonasining sql`...` tegi xuddi shu tamoyilda ishlaydi. Server va bazani 25–27-qismlarda o'rganamiz. Bugungi saboq: qiymatni kodga yopishtirmang — teg yoki parametr orqali alohida bering. Bu qoida HTML'da ham (html tegi), SQL'da ham bir xil.

6. Ko'p uchraydigan xatolar

6.1 Tegni qavs bilan chaqirish

js
function html(qismlar, ...qiymatlar) {
  return qismlar.reduce(
    (natija, qism, i) => natija + qiymatlar[i - 1] + qism,
  );
}

const taom = "Osh";
html(`<b>${taom}</b>`);
text
TypeError: qismlar.reduce is not a function

Tarjimasi: "qismlar.reduce funksiya emas." html(`...`) — oddiy chaqiruv. Template literal avval o'zi yig'ildi va html ga bitta tayyor satr keldi. Satrda reduce metodi yo'q. Xato reduce so'zini ko'rsatyapti, lekin aybdor qavs. Tuzatish: qavssiz yozing — html`<b>${taom}</b>`. Eng yomoni: tegingiz reduce emas, boshqa narsa ishlatsa, xato chiqmasligi va ekranlash jimgina o'tkazib yuborilishi mumkin.

6.2 Indeksni adashtirish

Siklda natija += qiymatlar[i] + qismlar[i] deb yozilsa, birinchi qism ikki marta qo'shiladi, oxirgisi esa yo'qoladi. Qoida: natija qismlar[0] bilan boshlanadi, siklda — qiymatlar[i] va qismlar[i + 1]. reduce variantida — qiymatlar[i - 1] va qism.

6.3 Windows yo'li oddiy template literalda

`C:\users\sardor` — SyntaxError, `C:\new` esa jimgina yangi qatorga aylanadi. Tuzatish: String.raw yoki teskari chiziqni ikki marta yozing.

6.4 Ikki marta ekranlash va "hamma joyga yetarli" deb o'ylash

Ichma-ich tegda muhr bo'lmasa, ekranda &lt;li&gt; chiqadi — ekranlash bir marta, eng oxirida bo'lsin. Teg esa href dagi javascript: ni, qo'shtirnoqsiz atributni va <script> ichini himoya qilmaydi — «Ekranlash yetmaydigan joylar» bo'limiga qayting.

7. Mashqlar

1-mashq (oson): Katta harf tegi

katta tegini yozing: u qismlarga tegmasin, har bir qiymatni esa katta harfga aylantirsin. Ishora: String(qiymat).toUpperCase() (Matnni o'zgartirish) va «Qayta yig'ish: o'z tegingiz» bo'limidagi sikl qolipi — natija birinchi qismdan boshlanadi.

js
const ism = "Dilshod aka";
console.log(katta`Xush kelibsiz, ${ism}! Stol ${4}.`);
// Xush kelibsiz, DILSHOD AKA! Stol 4.
Yechim
js
function katta(qismlar, ...qiymatlar) {
  let natija = qismlar[0];
  for (let i = 0; i < qiymatlar.length; i++) {
    natija += String(qiymatlar[i]).toUpperCase() + qismlar[i + 1];
  }
  return natija;
}

const ism = "Dilshod aka";
console.log(katta`Xush kelibsiz, ${ism}! Stol ${4}.`);

Konsolda:

text
Xush kelibsiz, DILSHOD AKA! Stol 4.

4 son edi — String uni satrga aylantirdi, toUpperCase raqamga ta'sir qilmadi. "Xush kelibsiz" esa qism, u o'zgarmadi.

2-mashq (o'rta): Bo'sh qiymatlarni yashirish

Shablonda ba'zan qiymat bo'lmaydi: ${izoh} — undefined, ${chegirma} — null. Oddiy literal ularni "undefined" va "null" deb yozib qo'yadi. toza tegini yozing: undefined, null va false qiymatlar o'rniga bo'sh satr qo'ysin, qolganlari odatdagidek. Ishora: «Xuddi shu yig'ish — reduce bilan» bo'limidagi qolip va ?? operatori — lekin false uchun alohida shart kerak.

js
const izoh = undefined;
console.log(toza`Bron: ${"Dilshod aka"}${izoh}, VIP: ${false}`);
// Bron: Dilshod aka, VIP: 
Yechim
js
function bosmiYoqmi(qiymat) {
  return qiymat === undefined || qiymat === null || qiymat === false;
}

function toza(qismlar, ...qiymatlar) {
  return qismlar.reduce((natija, qism, i) => {
    const qiymat = qiymatlar[i - 1];
    return natija + (bosmiYoqmi(qiymat) ? "" : qiymat) + qism;
  });
}

const izoh = undefined;
const natija = toza`Bron: ${"Dilshod aka"}${izoh}, VIP: ${false}`;
console.log(JSON.stringify(natija)); // "Bron: Dilshod aka, VIP: "

JSON.stringify natijani qo'shtirnoq ichida ko'rsatadi — oxiridagi bo'sh joy ko'rinib turishi uchun. 0 va "" esa yashirilmadi: 0 — haqiqiy son (masalan, "0 ta bo'sh stol"). ?? faqat null va undefined ni ushlagani uchun false ni alohida tekshirdik. Lit kutubxonasi ham null, undefined va false ni xuddi shunday hech narsa chizmaydi.

3-mashq (qiyin): Chiroyli ko'p qatorli matn

Template literalda ko'p qatorli matn kod bilan birga suriladi (Template literals darsidagi bo'sh joy tuzog'i). Shu muammo uchun tekisla tegini yozing. U avval qiymatlarni joyiga qo'yib matnni yig'sin, keyin birinchi va oxirgi bo'sh qatorlarni olib tashlasin va har qatordagi umumiy chap surilishni kessin. Ishora: split("\n"), filter va Math.min bilan eng kichik surilishni toping; surilishni regex /^ */ bilan o'lchang (RegExp asoslari).

js
function xabarYasa(ism) {
  return tekisla`
    Hurmatli ${ism}!
      Broningiz tasdiqlandi.
    «Bahor»
  `;
}
console.log(xabarYasa("Dilshod aka"));
Yechim
js
function tekisla(qismlar, ...qiymatlar) {
  const matn = qismlar.reduce(
    (natija, qism, i) => natija + qiymatlar[i - 1] + qism,
  );
  const qatorlar = matn.split("\n");
  if (qatorlar[0].trim() === "") qatorlar.shift();
  if (qatorlar.at(-1).trim() === "") qatorlar.pop();
  const surilishlar = qatorlar
    .filter((qator) => qator.trim() !== "")
    .map((qator) => qator.match(/^ */)[0].length);
  const eng = Math.min(...surilishlar);
  return qatorlar.map((qator) => qator.slice(eng)).join("\n");
}

function xabarYasa(ism) {
  return tekisla`
    Hurmatli ${ism}!
      Broningiz tasdiqlandi.
    «Bahor»
  `;
}

console.log(xabarYasa("Dilshod aka"));

Konsolda:

text
Hurmatli Dilshod aka!
  Broningiz tasdiqlandi.
«Bahor»

Eng kichik surilish — 4 ta bo'sh joy. Har qatordan 4 ta kesildi, ikkinchi qatorning qo'shimcha ikkitasi esa saqlandi. Bo'sh qatorlar surilish hisobiga kirmadi: aks holda Math.min 0 ni topib, hech narsani kesmasdi. npm'da dedent nomli paket aynan shu ishni qiladi.

4-mashq: Vazifalar qadami — html tegi alohida modulda

vazifalar ning kanonik kodi DOM'ni createElement va textContent bilan quradi — bu eng xavfsiz yo'l, uni o'zgartirmaymiz. Bugungi tegni esa mashqlar repoda modul qilib saqlaymiz: kurs/mashqlar/12/19-teglar/html.mjs. Undan XavfsizHtml, ekranla va html ni eksport qiling («Ichma-ich shablonlar» bo'limidagi variant). Yonida sinash.mjs yasang: u html ni import qilib, vazifalar ro'yxatidan <ul> yasasin. Bitta vazifa matni — "<b>Non</b> olish". U ekranlanganini tekshiring.

Yechim
js
// 12/19-teglar/html.mjs
export class XavfsizHtml {
  constructor(matn) {
    this.matn = matn;
  }
  toString() {
    return this.matn;
  }
}

export function ekranla(matn) {
  return String(matn)
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll('"', "&quot;")
    .replaceAll("'", "&#39;");
}

function tayyorla(qiymat) {
  if (qiymat instanceof XavfsizHtml) return qiymat.matn;
  if (Array.isArray(qiymat)) return qiymat.map(tayyorla).join("");
  return ekranla(qiymat);
}

export function html(qismlar, ...qiymatlar) {
  const matn = qismlar.reduce(
    (natija, qism, i) => natija + tayyorla(qiymatlar[i - 1]) + qism,
  );
  return new XavfsizHtml(matn);
}
js
// 12/19-teglar/sinash.mjs
import { html } from "./html.mjs";

const vazifalar = [
  { id: 1, matn: "<b>Non</b> olish", bajarildi: false },
  { id: 2, matn: "Onamga qo'ng'iroq qilish", bajarildi: true },
];

const royxat = html`<ul>${vazifalar.map(
  (v) => html`<li data-id="${v.id}">${v.matn}</li>`,
)}</ul>`;

console.log(String(royxat));

Terminalda node 12/19-teglar/sinash.mjs:

text
<ul><li data-id="1">&lt;b&gt;Non&lt;/b&gt; olish</li><li data-id="2">Onamga qo&#39;ng&#39;iroq qilish</li></ul>

<b> ekranlandi, apostrof esa &#39; ga aylandi — brauzer uni baribir ' deb ko'rsatadi. data-id qiymati qo'shtirnoq ichida — «Ekranlash yetmaydigan joylar» bo'limidagi qoida.

bash
git add 12/19-teglar
git commit -m "12/19: html tegi va uni sinash"

8. Real ishda

Tagged template'ni o'zingiz kam yozasiz, lekin tayyorlarini tez-tez uchratasiz. Kutubxona nomlarini eslab qolish shart emas — tanish shaklni ko'rib, "bu teg" deyishingiz yetarli:

Teg Qayerda Nima qiladi
html`...` Lit 3.3 xavfsiz va keshlangan HTML
css`...`, styled.p`...` styled-components 6.5 CSS'dan klass yasaydi
gql`...` graphql-tag 2.12 GraphQL so'rovini tahlil qiladi
sql`...` postgres 3.4 parametrlangan SQL so'rovi
  • Web Components va Lit. Lit butunlay html va css teglariga qurilgan. Web Components'ni 13-qismda o'rganamiz. gql — GraphQL degan so'rov tilining tegi; uni ham keyinroq ko'rasiz.
  • Ma'lumotlar bazasi. postgres, Prisma'ning $queryRaw va Drizzle'ning sql teglari so'rovni parametrlangan holda yuboradi. Code review'da tegsiz `SELECT ... ${id}` ni ko'rsangiz — darhol savol bering.
  • Intervyu. "Tagged template nima qaytaradi?", "String.raw nima uchun kerak?", "Qismlar va qiymatlar soni qanday bog'liq?" — middle darajadagi savollar.

Xulosa

  • Tagged template — teg`...`: JavaScript literalni o'zi yig'maydi, uni teg(qismlar, ...qiymatlar) ga beradi.
  • Qismlar doim qiymatlardan bittaga ko'p; yig'ish — qismlar[0], keyin har qiymat va undan keyingi qism.
  • qismlar.raw — xom matn; String.raw — escape'larni aylantirmaydigan tayyor teg (Windows yo'li, regex).
  • Qismlar massivi muzlatilgan va bitta chaqiruv joyi uchun doim bir xil — kutubxonalar shu bilan keshlaydi.
  • html tegi har qiymatni avtomatik ekranlaydi; ichma-ich shablonlar uchun muhr kerak; href, qo'shtirnoqsiz atribut va <script> ichi uchun ekranlash yetmaydi.
  • Qiymatni kodga yopishtirmang: sql tegi so'rov matni va qiymatlarni alohida beradi — SQL injection'ga yo'l yo'q.

Keyingi dars: Proxy va Reflect — bugun template literalni "ushlab qolgan" bo'lsak, keyingi darsda obyekt bilan bo'ladigan har bir amalni — o'qish, yozish, o'chirishni — ushlab qolishni o'rganamiz.

Manbalar

  • MDN: "Template literals (Template strings)" — "Tagged templates" va "Raw strings" bo'limlari, "String.raw()" — developer.mozilla.org
  • ECMAScript 2025 Language Specification, 13.2.8 "Template Literals" va 13.3.11 "Tagged Templates" — tc39.es/ecma262
  • TC39: "Template Literal Revision" (ES2018, noto'g'ri escape'lar teg ichida) — github.com/tc39/proposal-template-literal-revision
  • OWASP: "SQL Injection Prevention Cheat Sheet" — cheatsheetseries.owasp.org
  • Lit: "Expressions" (Templates) — lit.dev/docs
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
JavaScript tagged template literals: teg funksiyasi, String.raw va xavfsiz HTML — IlmHamroh