Ты пишешь стиль .btn { color: red }, но кнопка по-прежнему синяя — потому что где-то есть #header .btn { color: blue } с большей специфичностью. Или в JavaScript: document.querySelector('form input:not([disabled]):first-child') — ты должен понимать что это вернёт. Специфичность и селекторы — это «язык приоритетов» в CSS.
В больших проектах CSS-стили конфликтуют. Специфичность — правило, по которому браузер решает какой стиль применить. JavaScript использует CSS-селекторы для поиска элементов — чем лучше ты знаешь синтаксис, тем точнее запросы.
document.querySelector, querySelectorAll — принимают CSS-селекторы// Базовые:
// div — по тегу
// .foo — по классу
// #foo — по ID
// * — все элементы
// Атрибуты:
// [type="text"] — точное значение
// [href^="https"] — начинается с "https"
// [src$=".png"] — заканчивается на ".png"
// [class*="btn"] — содержит "btn"
// [data-active] — атрибут существует
// Псевдоклассы:
// :hover :focus :active :checked :disabled
// :first-child :last-child :nth-child(2n+1)
// :not(.hidden) :is(h1, h2, h3) :has(img)
// Псевдоэлементы:
// ::before ::after ::placeholder ::selection
// Комбинаторы:
// div p — потомок (любой уровень)
// div > p — прямой дочерний
// div + p — следующий сосед
// div ~ p — все следующие соседиСпецифичность записывается как тройка (id, class, element):
// #header → (1, 0, 0) — выигрывает почти всегда
// .btn.active → (0, 2, 0) — два класса
// div.menu > li → (0, 1, 2) — класс + два тега
// a:hover → (0, 1, 1) — псевдокласс = класс
// [type="text"] → (0, 1, 0) — атрибут = класс
// ::before → (0, 0, 1) — псевдоэлемент = тег
// style="..." inline → (1, 0, 0, 0) — ещё выше
// !important → перебивает всё
// При равной специфичности — побеждает тот, кто объявлен ПОЗЖЕ// :is() — сокращение для групп (специфичность = максимум из списка)
// :is(h1, h2, h3) { margin: 0 } = h1, h2, h3 { margin: 0 }
// :has() — "родительский" селектор (CSS 2023+)
// .card:has(img) — карточки с изображением
// form:has(:invalid) — форма с невалидным полем
// li:has(+ li) — все элементы кроме последнего
// :not() принимает список:
// li:not(:first-child, :last-child) — не первый и не последний// Принимает ЛЮБОЙ валидный CSS-селектор:
document.querySelector('#header')
document.querySelector('.btn.active[data-type="primary"]')
document.querySelector('form input[type="email"]:not([disabled])')
document.querySelector('li:nth-child(3)')
// querySelectorAll — NodeList (не Array, но итерируемый):
const items = document.querySelectorAll('.card')
[...items].map(el => el.textContent)
Array.from(items).forEach(el => { ... })
// Браузер разбирает селектор СПРАВА НАЛЕВО:
// 'div .btn' — сначала все .btn, потом фильтрует по div-предкуОшибка 1: Чрезмерная специфичность
/* ПЛОХО — теперь нельзя переопределить без #app */
#app .header .nav .btn { color: red; }
/* ХОРОШО — низкая специфичность, легко переопределить */
.nav-btn { color: red; }Ошибка 2: Использование !important вместо правильной специфичности
/* ПЛОХО — создаёт каскад !important, сложно поддерживать */
.btn { color: blue !important; }
.btn.active { color: red !important; }
/* ХОРОШО — увеличиваем специфичность естественно */
.btn { color: blue; }
.btn.active { color: red; } /* (0,2,0) > (0,1,0) */Ошибка 3: Неэффективные querySelector
// МЕДЛЕННО — * в начале, поиск по всему документу
document.querySelectorAll('* .btn')
// БЫСТРЕЕ — ищем от конкретного контейнера
const container = document.getElementById('app')
container.querySelectorAll('.btn')element.matches('.btn.active') — проверка селектора программноscreen.getBy... в Testing Library использует семантические селекторыКалькулятор специфичности CSS: разбор селектора, подсчёт очков, сортировка каскада
// Калькулятор специфичности CSS-селекторов
function calculateSpecificity(selector) {
let ids = 0, classes = 0, elements = 0
let s = selector
// Шаг 1: псевдоэлементы ::before, ::after (считаются как элемент)
s = s.replace(/::[-\w]+/g, () => { elements++; return '' })
// Шаг 2: псевдоклассы :hover, :nth-child() (как класс)
s = s.replace(/:[-\w]+(?:\([^)]*\))?/g, () => { classes++; return '' })
// Шаг 3: ID-селекторы #foo
s = s.replace(/#[-\w]+/g, () => { ids++; return '' })
// Шаг 4: классы .foo
s = s.replace(/\.[-\w]+/g, () => { classes++; return '' })
// Шаг 5: атрибуты [type="text"] (как класс)
s = s.replace(/\[[^\]]*\]/g, () => { classes++; return '' })
// Шаг 6: теги (оставшееся после уборки спецсимволов)
const tags = s.replace(/[\s>+~*]/g, '').match(/[a-zA-Z][-\w]*/g) ?? []
elements += tags.length
return [ids, classes, elements]
}
function compareSpecificity(a, b) {
for (let i = 0; i < 3; i++) {
if (a[i] !== b[i]) return a[i] - b[i]
}
return 0
}
function formatSpec([ids, classes, elements]) {
return `(${ids},${classes},${elements})`
}
function score([ids, classes, elements]) {
return ids * 10000 + classes * 100 + elements
}
// ===== Тест набора селекторов =====
console.log('=== Специфичность CSS-селекторов ===')
console.log()
const selectors = [
'#header',
'#nav .item',
'.btn.active',
'.btn:hover',
'div.menu > li',
'a',
'div > p',
'ul li',
'[type="text"]',
'input[type="email"]',
'::before',
'p::first-line',
':nth-child(2)',
'div:not(.hidden)',
]
for (const sel of selectors) {
const spec = calculateSpecificity(sel)
const s = score(spec)
console.log(`${sel.padEnd(26)} → ${formatSpec(spec)} (score: ${s})`)
}
// ===== Каскад: сортировка по специфичности =====
console.log('\n=== CSS Каскад: от наименее к наиболее специфичному ===')
const cssRules = [
{ selector: 'a', value: 'color: blue' },
{ selector: '.link', value: 'color: green' },
{ selector: 'nav .link', value: 'color: teal' },
{ selector: '#header a', value: 'color: red' },
{ selector: '.nav #main-link', value: 'color: orange' },
]
const sorted = cssRules
.map(r => ({ ...r, spec: calculateSpecificity(r.selector) }))
.sort((a, b) => compareSpecificity(a.spec, b.spec))
for (const rule of sorted) {
console.log(` ${formatSpec(rule.spec).padEnd(12)} ${rule.selector.padEnd(20)} → ${rule.value}`)
}
const winner = sorted[sorted.length - 1]
console.log(`\nПобеждает: "${winner.value}" (наибольшая специфичность: ${formatSpec(winner.spec)})`)
// ===== Сравнение пар =====
console.log('\n=== Сравнение пар (какой стиль победит?) ===')
const pairs = [
['#nav a', '.nav-link:hover'],
['.btn.primary', 'div .btn'],
['li:nth-child(2)', '.list-item'],
['p::before', '.pseudo'],
]
for (const [a, b] of pairs) {
const specA = calculateSpecificity(a)
const specB = calculateSpecificity(b)
const cmp = compareSpecificity(specA, specB)
const winner = cmp > 0 ? a : cmp < 0 ? b : 'ничья (порядок решает)'
console.log(` "${a}" vs "${b}"`)
console.log(` ${formatSpec(specA)} vs ${formatSpec(specB)} → побеждает: ${winner}`)
}Ты пишешь стиль .btn { color: red }, но кнопка по-прежнему синяя — потому что где-то есть #header .btn { color: blue } с большей специфичностью. Или в JavaScript: document.querySelector('form input:not([disabled]):first-child') — ты должен понимать что это вернёт. Специфичность и селекторы — это «язык приоритетов» в CSS.
В больших проектах CSS-стили конфликтуют. Специфичность — правило, по которому браузер решает какой стиль применить. JavaScript использует CSS-селекторы для поиска элементов — чем лучше ты знаешь синтаксис, тем точнее запросы.
document.querySelector, querySelectorAll — принимают CSS-селекторы// Базовые:
// div — по тегу
// .foo — по классу
// #foo — по ID
// * — все элементы
// Атрибуты:
// [type="text"] — точное значение
// [href^="https"] — начинается с "https"
// [src$=".png"] — заканчивается на ".png"
// [class*="btn"] — содержит "btn"
// [data-active] — атрибут существует
// Псевдоклассы:
// :hover :focus :active :checked :disabled
// :first-child :last-child :nth-child(2n+1)
// :not(.hidden) :is(h1, h2, h3) :has(img)
// Псевдоэлементы:
// ::before ::after ::placeholder ::selection
// Комбинаторы:
// div p — потомок (любой уровень)
// div > p — прямой дочерний
// div + p — следующий сосед
// div ~ p — все следующие соседиСпецифичность записывается как тройка (id, class, element):
// #header → (1, 0, 0) — выигрывает почти всегда
// .btn.active → (0, 2, 0) — два класса
// div.menu > li → (0, 1, 2) — класс + два тега
// a:hover → (0, 1, 1) — псевдокласс = класс
// [type="text"] → (0, 1, 0) — атрибут = класс
// ::before → (0, 0, 1) — псевдоэлемент = тег
// style="..." inline → (1, 0, 0, 0) — ещё выше
// !important → перебивает всё
// При равной специфичности — побеждает тот, кто объявлен ПОЗЖЕ// :is() — сокращение для групп (специфичность = максимум из списка)
// :is(h1, h2, h3) { margin: 0 } = h1, h2, h3 { margin: 0 }
// :has() — "родительский" селектор (CSS 2023+)
// .card:has(img) — карточки с изображением
// form:has(:invalid) — форма с невалидным полем
// li:has(+ li) — все элементы кроме последнего
// :not() принимает список:
// li:not(:first-child, :last-child) — не первый и не последний// Принимает ЛЮБОЙ валидный CSS-селектор:
document.querySelector('#header')
document.querySelector('.btn.active[data-type="primary"]')
document.querySelector('form input[type="email"]:not([disabled])')
document.querySelector('li:nth-child(3)')
// querySelectorAll — NodeList (не Array, но итерируемый):
const items = document.querySelectorAll('.card')
[...items].map(el => el.textContent)
Array.from(items).forEach(el => { ... })
// Браузер разбирает селектор СПРАВА НАЛЕВО:
// 'div .btn' — сначала все .btn, потом фильтрует по div-предкуОшибка 1: Чрезмерная специфичность
/* ПЛОХО — теперь нельзя переопределить без #app */
#app .header .nav .btn { color: red; }
/* ХОРОШО — низкая специфичность, легко переопределить */
.nav-btn { color: red; }Ошибка 2: Использование !important вместо правильной специфичности
/* ПЛОХО — создаёт каскад !important, сложно поддерживать */
.btn { color: blue !important; }
.btn.active { color: red !important; }
/* ХОРОШО — увеличиваем специфичность естественно */
.btn { color: blue; }
.btn.active { color: red; } /* (0,2,0) > (0,1,0) */Ошибка 3: Неэффективные querySelector
// МЕДЛЕННО — * в начале, поиск по всему документу
document.querySelectorAll('* .btn')
// БЫСТРЕЕ — ищем от конкретного контейнера
const container = document.getElementById('app')
container.querySelectorAll('.btn')element.matches('.btn.active') — проверка селектора программноscreen.getBy... в Testing Library использует семантические селекторыКалькулятор специфичности CSS: разбор селектора, подсчёт очков, сортировка каскада
// Калькулятор специфичности CSS-селекторов
function calculateSpecificity(selector) {
let ids = 0, classes = 0, elements = 0
let s = selector
// Шаг 1: псевдоэлементы ::before, ::after (считаются как элемент)
s = s.replace(/::[-\w]+/g, () => { elements++; return '' })
// Шаг 2: псевдоклассы :hover, :nth-child() (как класс)
s = s.replace(/:[-\w]+(?:\([^)]*\))?/g, () => { classes++; return '' })
// Шаг 3: ID-селекторы #foo
s = s.replace(/#[-\w]+/g, () => { ids++; return '' })
// Шаг 4: классы .foo
s = s.replace(/\.[-\w]+/g, () => { classes++; return '' })
// Шаг 5: атрибуты [type="text"] (как класс)
s = s.replace(/\[[^\]]*\]/g, () => { classes++; return '' })
// Шаг 6: теги (оставшееся после уборки спецсимволов)
const tags = s.replace(/[\s>+~*]/g, '').match(/[a-zA-Z][-\w]*/g) ?? []
elements += tags.length
return [ids, classes, elements]
}
function compareSpecificity(a, b) {
for (let i = 0; i < 3; i++) {
if (a[i] !== b[i]) return a[i] - b[i]
}
return 0
}
function formatSpec([ids, classes, elements]) {
return `(${ids},${classes},${elements})`
}
function score([ids, classes, elements]) {
return ids * 10000 + classes * 100 + elements
}
// ===== Тест набора селекторов =====
console.log('=== Специфичность CSS-селекторов ===')
console.log()
const selectors = [
'#header',
'#nav .item',
'.btn.active',
'.btn:hover',
'div.menu > li',
'a',
'div > p',
'ul li',
'[type="text"]',
'input[type="email"]',
'::before',
'p::first-line',
':nth-child(2)',
'div:not(.hidden)',
]
for (const sel of selectors) {
const spec = calculateSpecificity(sel)
const s = score(spec)
console.log(`${sel.padEnd(26)} → ${formatSpec(spec)} (score: ${s})`)
}
// ===== Каскад: сортировка по специфичности =====
console.log('\n=== CSS Каскад: от наименее к наиболее специфичному ===')
const cssRules = [
{ selector: 'a', value: 'color: blue' },
{ selector: '.link', value: 'color: green' },
{ selector: 'nav .link', value: 'color: teal' },
{ selector: '#header a', value: 'color: red' },
{ selector: '.nav #main-link', value: 'color: orange' },
]
const sorted = cssRules
.map(r => ({ ...r, spec: calculateSpecificity(r.selector) }))
.sort((a, b) => compareSpecificity(a.spec, b.spec))
for (const rule of sorted) {
console.log(` ${formatSpec(rule.spec).padEnd(12)} ${rule.selector.padEnd(20)} → ${rule.value}`)
}
const winner = sorted[sorted.length - 1]
console.log(`\nПобеждает: "${winner.value}" (наибольшая специфичность: ${formatSpec(winner.spec)})`)
// ===== Сравнение пар =====
console.log('\n=== Сравнение пар (какой стиль победит?) ===')
const pairs = [
['#nav a', '.nav-link:hover'],
['.btn.primary', 'div .btn'],
['li:nth-child(2)', '.list-item'],
['p::before', '.pseudo'],
]
for (const [a, b] of pairs) {
const specA = calculateSpecificity(a)
const specB = calculateSpecificity(b)
const cmp = compareSpecificity(specA, specB)
const winner = cmp > 0 ? a : cmp < 0 ? b : 'ничья (порядок решает)'
console.log(` "${a}" vs "${b}"`)
console.log(` ${formatSpec(specA)} vs ${formatSpec(specB)} → побеждает: ${winner}`)
}Реализуй функцию `calculateSpecificity(selector)`, которая принимает CSS-селектор и возвращает его специфичность в виде массива `[ids, classes, elements]`. Алгоритм — последовательная замена компонентов с инкрементом счётчиков: псевдоэлементы (elements++), псевдоклассы (classes++), ID (ids++), классы (classes++), атрибуты (classes++), теги (elements++).
Каждый шаг: s = s.replace(regex, () => { counter++; return "" }). В конце elements += tags.length. Возвращай [ids, classes, elements]. Порядок шагов важен: псевдоэлементы (::) до псевдоклассов (:)