Пользователь вводит задачу и нажимает Enter — новый <li> должен появиться в списке мгновенно, без перезагрузки страницы. По клику на крестик — элемент должен исчезнуть. Это стандартная задача динамического обновления DOM: создание элементов, вставка и удаление. Именно здесь кроется классическая XSS-уязвимость при неаккуратном использовании innerHTML.
addEventListener('click', ...) на динамически созданных элементахconst btn = document.createElement('button')
btn.textContent = 'Добавить в корзину'
btn.className = 'btn btn-primary'
btn.dataset.productId = '42'appendChild — добавляет в конец:
const list = document.querySelector('.todo-list')
const item = document.createElement('li')
item.textContent = 'Новая задача'
list.appendChild(item) // вставляет в конец ulprepend / append — современный способ (принимают строки и узлы):
list.prepend(item) // в начало
list.append(item) // в конец
list.append('текст', item) // несколько аргументовbefore / after — вставка рядом с элементом (не внутрь):
const divider = document.createElement('hr')
item.before(divider) // перед item
item.after(divider) // после itemМетод парсит HTML-строку и вставляет без перезаписи существующего содержимого:
element.insertAdjacentHTML('beforebegin', '<hr>') // перед element
element.insertAdjacentHTML('afterbegin', '<b>Начало</b>') // первый ребёнок
element.insertAdjacentHTML('beforeend', '<b>Конец</b>') // последний ребёнок
element.insertAdjacentHTML('afterend', '<hr>') // после elementitem.remove() // удалить элемент из DOM
item.replaceWith(newItem) // заменить на другой элемент или строку// innerHTML — парсит HTML, ОПАСНО с пользовательскими данными!
div.innerHTML = '<b>Жирный текст</b>' // отобразит жирный текст
div.innerHTML = userInput // XSS-уязвимость!
// textContent — только текст, безопасно
div.textContent = '<b>Экранировано</b>' // покажет как есть, без разметки
div.textContent = userInput // всегда безопасноНикогда не вставляйте пользовательский ввод через innerHTML — это открывает путь для XSS-атак. Используйте textContent или экранирование.
const card = document.querySelector('.product-card')
const shallowCopy = card.cloneNode(false) // только сам элемент, без детей
const deepCopy = card.cloneNode(true) // со всем содержимым
document.querySelector('.grid').appendChild(deepCopy)function addTodo(text) {
const li = document.createElement('li')
li.className = 'todo-item'
const span = document.createElement('span')
span.textContent = text // textContent — безопасно
const btn = document.createElement('button')
btn.textContent = '✕'
btn.addEventListener('click', () => li.remove())
li.append(span, btn)
document.querySelector('.todo-list').appendChild(li)
}1. innerHTML с пользовательским вводом — XSS-уязвимость:
// Плохо: пользователь может ввести <script>alert('xss')</script>
const userComment = '<img src=x onerror="stealCookies()">'
commentDiv.innerHTML = userComment // ВЫПОЛНЯЕТ произвольный JS!
// Хорошо: textContent экранирует всё
commentDiv.textContent = userComment // показывает как текст, безопасно2. Вставка элемента в несколько мест — он перемещается, не копируется:
const item = document.createElement('li')
item.textContent = 'Задача'
list1.appendChild(item) // вставили в первый список
list2.appendChild(item) // элемент ПЕРЕМЕСТИЛСЯ из list1 в list2!
// Правильно: клонировать для вставки в несколько мест
list1.appendChild(item)
list2.appendChild(item.cloneNode(true))3. Частые вставки в цикле — лучше использовать DocumentFragment:
// Плохо: каждая вставка вызывает reflow
items.forEach(text => {
const li = document.createElement('li')
li.textContent = text
ul.appendChild(li) // reflow на каждой итерации!
})
// Хорошо: собери в fragment, вставь один раз
const fragment = document.createDocumentFragment()
items.forEach(text => {
const li = document.createElement('li')
li.textContent = text
fragment.appendChild(li)
})
ul.appendChild(fragment) // один reflow<template> элемент + cloneNode(true) для повторяющихся блоковПоскольку в sandbox нет настоящего DOM, примеры ниже реализуют мини-виртуальный DOM на JavaScript-объектах с методом render(), возвращающим HTML-строку.
Виртуальный DOM-строитель: createEl, append, render в строку
// Мини-виртуальный DOM
function createEl(tag, attrs = {}, ...children) {
return { tag, attrs, children: children.flat() }
}
function append(parent, ...nodes) {
parent.children.push(...nodes)
return parent
}
function prepend(parent, ...nodes) {
parent.children.unshift(...nodes)
return parent
}
function remove(parent, node) {
parent.children = parent.children.filter(c => c !== node)
}
function cloneNode(node) {
return JSON.parse(JSON.stringify(node))
}
function render(node) {
if (typeof node === 'string') return node
const attrStr = Object.entries(node.attrs)
.map(([k, v]) => `${k}="${v}"`)
.join(' ')
const opening = attrStr ? `<${node.tag} ${attrStr}>` : `<${node.tag}>`
const inner = node.children.map(render).join('')
return `${opening}${inner}</${node.tag}>`
}
// Динамически строим список задач
const ul = createEl('ul', { class: 'todo-list' })
const tasks = ['Проверить почту', 'Написать тест', 'Сделать code review']
tasks.forEach((text, i) => {
const li = createEl('li', { class: 'todo-item', 'data-index': String(i) },
createEl('span', {}, text),
createEl('button', { class: 'remove-btn' }, '✕'),
)
append(ul, li)
})
console.log('До удаления:')
console.log(render(ul))
// Удаляем второй элемент (индекс 1)
remove(ul, ul.children[1])
console.log('\nПосле удаления второго элемента:')
console.log(render(ul))
// Клонируем первый и добавляем в конец
const cloned = cloneNode(ul.children[0])
cloned.attrs['data-index'] = '99'
cloned.children[0].children[0] = 'Клонированная задача'
append(ul, cloned)
console.log('\nПосле клонирования:')
console.log(render(ul))Пользователь вводит задачу и нажимает Enter — новый <li> должен появиться в списке мгновенно, без перезагрузки страницы. По клику на крестик — элемент должен исчезнуть. Это стандартная задача динамического обновления DOM: создание элементов, вставка и удаление. Именно здесь кроется классическая XSS-уязвимость при неаккуратном использовании innerHTML.
addEventListener('click', ...) на динамически созданных элементахconst btn = document.createElement('button')
btn.textContent = 'Добавить в корзину'
btn.className = 'btn btn-primary'
btn.dataset.productId = '42'appendChild — добавляет в конец:
const list = document.querySelector('.todo-list')
const item = document.createElement('li')
item.textContent = 'Новая задача'
list.appendChild(item) // вставляет в конец ulprepend / append — современный способ (принимают строки и узлы):
list.prepend(item) // в начало
list.append(item) // в конец
list.append('текст', item) // несколько аргументовbefore / after — вставка рядом с элементом (не внутрь):
const divider = document.createElement('hr')
item.before(divider) // перед item
item.after(divider) // после itemМетод парсит HTML-строку и вставляет без перезаписи существующего содержимого:
element.insertAdjacentHTML('beforebegin', '<hr>') // перед element
element.insertAdjacentHTML('afterbegin', '<b>Начало</b>') // первый ребёнок
element.insertAdjacentHTML('beforeend', '<b>Конец</b>') // последний ребёнок
element.insertAdjacentHTML('afterend', '<hr>') // после elementitem.remove() // удалить элемент из DOM
item.replaceWith(newItem) // заменить на другой элемент или строку// innerHTML — парсит HTML, ОПАСНО с пользовательскими данными!
div.innerHTML = '<b>Жирный текст</b>' // отобразит жирный текст
div.innerHTML = userInput // XSS-уязвимость!
// textContent — только текст, безопасно
div.textContent = '<b>Экранировано</b>' // покажет как есть, без разметки
div.textContent = userInput // всегда безопасноНикогда не вставляйте пользовательский ввод через innerHTML — это открывает путь для XSS-атак. Используйте textContent или экранирование.
const card = document.querySelector('.product-card')
const shallowCopy = card.cloneNode(false) // только сам элемент, без детей
const deepCopy = card.cloneNode(true) // со всем содержимым
document.querySelector('.grid').appendChild(deepCopy)function addTodo(text) {
const li = document.createElement('li')
li.className = 'todo-item'
const span = document.createElement('span')
span.textContent = text // textContent — безопасно
const btn = document.createElement('button')
btn.textContent = '✕'
btn.addEventListener('click', () => li.remove())
li.append(span, btn)
document.querySelector('.todo-list').appendChild(li)
}1. innerHTML с пользовательским вводом — XSS-уязвимость:
// Плохо: пользователь может ввести <script>alert('xss')</script>
const userComment = '<img src=x onerror="stealCookies()">'
commentDiv.innerHTML = userComment // ВЫПОЛНЯЕТ произвольный JS!
// Хорошо: textContent экранирует всё
commentDiv.textContent = userComment // показывает как текст, безопасно2. Вставка элемента в несколько мест — он перемещается, не копируется:
const item = document.createElement('li')
item.textContent = 'Задача'
list1.appendChild(item) // вставили в первый список
list2.appendChild(item) // элемент ПЕРЕМЕСТИЛСЯ из list1 в list2!
// Правильно: клонировать для вставки в несколько мест
list1.appendChild(item)
list2.appendChild(item.cloneNode(true))3. Частые вставки в цикле — лучше использовать DocumentFragment:
// Плохо: каждая вставка вызывает reflow
items.forEach(text => {
const li = document.createElement('li')
li.textContent = text
ul.appendChild(li) // reflow на каждой итерации!
})
// Хорошо: собери в fragment, вставь один раз
const fragment = document.createDocumentFragment()
items.forEach(text => {
const li = document.createElement('li')
li.textContent = text
fragment.appendChild(li)
})
ul.appendChild(fragment) // один reflow<template> элемент + cloneNode(true) для повторяющихся блоковПоскольку в sandbox нет настоящего DOM, примеры ниже реализуют мини-виртуальный DOM на JavaScript-объектах с методом render(), возвращающим HTML-строку.
Виртуальный DOM-строитель: createEl, append, render в строку
// Мини-виртуальный DOM
function createEl(tag, attrs = {}, ...children) {
return { tag, attrs, children: children.flat() }
}
function append(parent, ...nodes) {
parent.children.push(...nodes)
return parent
}
function prepend(parent, ...nodes) {
parent.children.unshift(...nodes)
return parent
}
function remove(parent, node) {
parent.children = parent.children.filter(c => c !== node)
}
function cloneNode(node) {
return JSON.parse(JSON.stringify(node))
}
function render(node) {
if (typeof node === 'string') return node
const attrStr = Object.entries(node.attrs)
.map(([k, v]) => `${k}="${v}"`)
.join(' ')
const opening = attrStr ? `<${node.tag} ${attrStr}>` : `<${node.tag}>`
const inner = node.children.map(render).join('')
return `${opening}${inner}</${node.tag}>`
}
// Динамически строим список задач
const ul = createEl('ul', { class: 'todo-list' })
const tasks = ['Проверить почту', 'Написать тест', 'Сделать code review']
tasks.forEach((text, i) => {
const li = createEl('li', { class: 'todo-item', 'data-index': String(i) },
createEl('span', {}, text),
createEl('button', { class: 'remove-btn' }, '✕'),
)
append(ul, li)
})
console.log('До удаления:')
console.log(render(ul))
// Удаляем второй элемент (индекс 1)
remove(ul, ul.children[1])
console.log('\nПосле удаления второго элемента:')
console.log(render(ul))
// Клонируем первый и добавляем в конец
const cloned = cloneNode(ul.children[0])
cloned.attrs['data-index'] = '99'
cloned.children[0].children[0] = 'Клонированная задача'
append(ul, cloned)
console.log('\nПосле клонирования:')
console.log(render(ul))Напиши функцию buildTodoList(items), которая принимает массив строк и возвращает HTML-строку, представляющую список <ul class="todo-list"> с элементами <li class="todo-item"> для каждого элемента массива. Каждый <li> должен содержать <span> с текстом задачи и <button class="delete-btn"> с текстом "✕".
items.map(item => `<li class="todo-item"><span>${item}</span><button class="delete-btn">✕</button></li>`).join("") — оберни результат в <ul class="todo-list">...</ul>.