前端开发面试高频考点 (JS & CSS)
JavaScript 核心
Q: 什么是闭包 (Closure)?它有什么应用场景和潜在问题?
A:
- 定义: 闭包是指一个函数能够访问并操作其自身作用域之外的变量(通常是其父函数作用域中的变量),即使在父函数执行完毕后。简单来说,就是函数和其词法环境(Lexical Environment)的组合。当一个内部函数被返回或传递到外部时,它会“记住”其创建时的词法环境。
- 应用场景:
- 数据封装与私有变量: 通过闭包创建私有变量和方法,外部无法直接访问,只能通过暴露的接口(返回的函数)来操作。javascript
function createCounter() { let count = 0; // 私有变量 return { increment: function() { count++; }, getCount: function() { return count; } }; } const counter = createCounter(); counter.increment(); console.log(counter.getCount()); // 1 console.log(counter.count); // undefined (无法直接访问) - 模块化: 在 ES6 模块之前,常用闭包来模拟模块作用域,防止全局变量污染。
- 回调函数中保存状态: 在循环中创建事件监听器或异步回调时,闭包可以帮助每个回调函数记住其对应的特定值。javascript
// 错误示例 (var 导致共享 i) for (var i = 1; i <= 3; i++) { setTimeout(function timer() { console.log(i); // 会输出 4, 4, 4 (循环结束时的 i 值) }, i * 100); } // 正确示例 (使用闭包或 let) for (var i = 1; i <= 3; i++) { (function(j) { // 使用 IIFE 创建闭包 setTimeout(function timer() { console.log(j); // 输出 1, 2, 3 }, j * 100); })(i); } // 或者使用 let (块级作用域天然形成闭包效果) for (let i = 1; i <= 3; i++) { setTimeout(function timer() { console.log(i); // 输出 1, 2, 3 }, i * 100); } - 函数柯里化 (Currying) 和高阶函数: 闭包是实现这些技术的基础。
- 数据封装与私有变量: 通过闭包创建私有变量和方法,外部无法直接访问,只能通过暴露的接口(返回的函数)来操作。
- 潜在问题:
- 内存泄漏: 如果闭包引用了不再需要的外部变量(尤其是 DOM 元素),并且这个闭包本身持续存在(例如作为全局变量、定时器回调、事件监听器),那么这些外部变量占用的内存就无法被垃圾回收器回收,导致内存泄漏。需要注意及时解除不再需要的引用。
Q: 解释一下 Promise 是什么?它的状态有哪些?如何解决回调地狱 (Callback Hell)?
A:
- 定义: Promise 是 JavaScript 中处理异步操作的一种解决方案。它是一个代表了异步操作最终完成(或失败)及其结果值的对象。相比传统的回调函数,Promise 提供了更优雅、更易于管理异步代码的方式。
- 状态: Promise 对象有三种状态:
- Pending (进行中): 初始状态,既不是成功,也不是失败。
- Fulfilled (已成功): 表示异步操作成功完成。
- Rejected (已失败): 表示异步操作失败。 状态一旦从 Pending 变为 Fulfilled 或 Rejected,就不会再改变(状态凝固)。
- 解决回调地狱:
- 回调地狱是指多层嵌套的回调函数,使得代码难以阅读、理解和维护。
- Promise 通过
.then()方法的链式调用来解决这个问题。每个.then()方法返回一个新的 Promise,可以继续链接下一个.then()处理后续操作或错误。.catch()方法用于统一捕获链中任何地方发生的错误。
javascript// 回调地狱示例 asyncOp1(function(result1) { asyncOp2(result1, function(result2) { asyncOp3(result2, function(result3) { // ...层层嵌套 }, failureCallback); }, failureCallback); }, failureCallback); // Promise 解决 asyncOp1() .then(result1 => asyncOp2(result1)) // .then 返回新的 Promise .then(result2 => asyncOp3(result2)) .then(result3 => { // 处理最终结果 }) .catch(error => { // 统一错误处理 console.error("发生错误:", error); });
Q: async/await 是什么?它和 Promise 的关系?
A:
定义:
async/await是 ES2017 (ES8) 引入的异步编程语法糖,旨在让异步代码的书写方式更接近同步代码,提高可读性。关系:
async/await基于 Promise 实现。它并没有创造新的底层机制,而是对 Promise 的一种更易用的封装。async函数:在函数声明前加上async关键字,表示该函数内部可能包含异步操作。async函数总是隐式地返回一个 Promise。如果函数内部返回一个非 Promise 值,它会被自动包装成一个 resolved 的 Promise;如果函数内部抛出错误,它会返回一个 rejected 的 Promise。await操作符:只能在async函数内部使用。它用于等待一个 Promise 对象的状态变为 Fulfilled 或 Rejected。- 如果 Promise 变为 Fulfilled,
await会暂停当前async函数的执行,等待 Promise 完成,然后返回 Promise 的解决值,并恢复async函数的执行。 - 如果 Promise 变为 Rejected,
await会抛出 Promise 的拒绝原因(通常是一个 Error 对象),这个错误可以被try...catch语句捕获。
- 如果 Promise 变为 Fulfilled,
优势:
- 代码结构更清晰,像同步代码一样直观。
- 错误处理更方便,可以使用标准的
try...catch结构。 - 调试更友好。
javascript// 使用 Promise.then function fetchData() { return fetch('/api/data') .then(response => { if (!response.ok) { throw new Error('Network response was not ok'); } return response.json(); }) .then(data => { console.log(data); return data; // 传递数据 }) .catch(error => { console.error('Fetch error:', error); throw error; // 重新抛出以便上层捕获 }); } // 使用 async/await async function fetchDataAsync() { try { const response = await fetch('/api/data'); // 等待 fetch 完成 if (!response.ok) { throw new Error('Network response was not ok'); } const data = await response.json(); // 等待 json 解析完成 console.log(data); return data; // 返回数据 } catch (error) { console.error('Fetch error:', error); throw error; // 向上抛出错误 } }
Q: 箭头函数 (Arrow Function) 和普通函数 (Regular Function) 有什么主要区别?
A:
箭头函数和普通函数的主要区别在于:
this绑定:- 普通函数:
this的值在函数被调用时确定,取决于调用方式(全局调用、方法调用、构造函数调用、apply/call/bind调用)。 - 箭头函数:
this的值在函数被定义时确定,它捕获其词法作用域(即外层非箭头函数作用域)的this值。箭头函数的this一旦绑定就不会改变,call/apply/bind无法改变箭头函数的this指向。
- 普通函数:
- 不能作为构造函数:
- 箭头函数不能使用
new关键字调用,因为它没有自己的this绑定,也没有prototype属性。尝试用new调用箭头函数会抛出错误。
- 箭头函数不能使用
- 没有
arguments对象:- 箭头函数内部没有自己的
arguments对象。如果需要访问所有传入的参数,可以使用剩余参数(Rest Parameters...args)。
- 箭头函数内部没有自己的
- 没有
prototype属性:- 箭头函数没有
prototype属性,因此不能用作构造器。
- 箭头函数没有
- 不能用作 Generator 函数:
- 箭头函数不能使用
yield关键字,不能定义为 Generator 函数。
- 箭头函数不能使用
- 语法简洁:
- 当只有一个参数时,可以省略括号
()。 - 当函数体只有一条返回语句时,可以省略花括号
{}和return关键字(隐式返回)。
- 当只有一个参数时,可以省略括号
// this 绑定示例
const obj = {
value: 10,
regularFunc: function() {
console.log('Regular this:', this.value); // this 指向 obj
setTimeout(function() {
console.log('Regular setTimeout this:', this.value); // this 指向 window/undefined (非严格/严格模式)
}, 100);
},
arrowFunc: function() {
console.log('Outer this:', this.value); // this 指向 obj
setTimeout(() => {
console.log('Arrow setTimeout this:', this.value); // this 捕获外层 this (obj)
}, 100);
}
};
obj.regularFunc();
obj.arrowFunc();
// 语法简洁
const add = (a, b) => a + b;
const square = x => x * x;Q: var, let, const 之间有什么区别?
A:
| 特性 | var | let | const |
|---|---|---|---|
| 作用域 | 函数作用域 或 全局作用域 | 块级作用域 {} | 块级作用域 {} |
| 变量提升 | 存在 (声明提升,初始化为 undefined) | 存在 (声明提升,但存在暂时性死区 TDZ) | 存在 (声明提升,但存在暂时性死区 TDZ) |
| 重复声明 | 允许 在同一作用域内 | 不允许 在同一作用域内 | 不允许 在同一作用域内 |
| 赋值 | 允许重新赋值 | 允许重新赋值 | 不允许 重新赋值 (常量) |
| 初始值 | 可以不初始化 (默认为 undefined) | 可以不初始化 (默认为 undefined) | 必须 在声明时初始化 |
| 全局对象属性 | 在全局作用域声明时,会成为 window (浏览器) 或 global (Node) 的属性 | 在全局作用域声明时,不会成为全局对象的属性 | 在全局作用域声明时,不会成为全局对象的属性 |
暂时性死区 (Temporal Dead Zone - TDZ): let 和 const 声明的变量,在代码块内,从块的开始到声明语句之间,访问该变量会抛出 ReferenceError。即使变量声明提升了,但在初始化之前是不能访问的。
const 注意点: const 声明的常量指的是变量标识符指向的内存地址不能改变。对于基本类型(如数字、字符串、布尔值),这意味着值本身不能改变。对于引用类型(如对象、数组),这意味着不能将变量重新指向另一个对象或数组,但可以修改该对象或数组内部的属性或元素。
const myObj = { key: 'value' };
myObj.key = 'new value'; // 这是允许的
// myObj = { otherKey: 'other value' }; // 这会抛出 TypeError
const myArr = [1, 2];
myArr.push(3); // 这是允许的
// myArr = [4, 5]; // 这会抛出 TypeError建议: 优先使用 const,如果变量需要被重新赋值,则使用 let。尽量避免使用 var 以减少作用域和变量提升带来的潜在问题。
Q: JavaScript 中的 this 指向在不同场景下是如何确定的?
A:
this 的指向不是在函数定义时确定的,而是在函数执行时确定的,主要取决于函数的调用方式:
全局上下文:
- 在任何函数体之外,
this指向全局对象 (window在浏览器中,global在 Node.js 中)。 - 在严格模式 (
'use strict') 下,全局上下文中的this是undefined。
- 在任何函数体之外,
普通函数调用 (直接调用):
- 非严格模式下,
this指向全局对象 (window或global)。 - 严格模式下,
this是undefined。
javascriptfunction showThis() { console.log(this); } showThis(); // 非严格: window/global, 严格: undefined- 非严格模式下,
方法调用 (作为对象的方法调用):
- 当函数作为对象的一个属性被调用时 (
obj.method()),this指向调用该方法的对象 (obj)。
javascriptconst myObj = { name: 'My Object', greet: function() { console.log(`Hello from ${this.name}`); } }; myObj.greet(); // 输出 "Hello from My Object"- 当函数作为对象的一个属性被调用时 (
构造函数调用:
- 当使用
new关键字调用函数时,该函数作为构造函数。此时,this指向新创建的实例对象。
javascriptfunction Person(name) { this.name = name; } const person1 = new Person('Alice'); console.log(person1.name); // 输出 "Alice"- 当使用
显式绑定 (
call,apply,bind):function.call(thisArg, arg1, arg2, ...): 调用函数,并将this绑定到thisArg,参数逐个传递。function.apply(thisArg, [argsArray]): 调用函数,并将this绑定到thisArg,参数以数组形式传递。function.bind(thisArg, arg1, ...): 创建一个新函数,其this永久绑定到thisArg,可以预设部分参数。新函数被调用时this不会再改变。
javascriptfunction sayHello(greeting) { console.log(`${greeting}, ${this.name}`); } const user = { name: 'Bob' }; sayHello.call(user, 'Hi'); // 输出 "Hi, Bob" sayHello.apply(user, ['Hey']); // 输出 "Hey, Bob" const greetBob = sayHello.bind(user, 'Hello'); greetBob(); // 输出 "Hello, Bob"箭头函数:
- 箭头函数没有自己的
this绑定。它会捕获其定义时所在的词法作用域的this值。this的值在箭头函数定义时就已经确定,并且不能通过call,apply,bind来改变。
- 箭头函数没有自己的
Q: 解释一下 JavaScript 的事件循环 (Event Loop) 机制。
A:
JavaScript 是单线程的,意味着它一次只能执行一个任务。为了处理耗时的操作(如 I/O、网络请求、定时器)而不会阻塞主线程,JavaScript 引入了异步编程模型和事件循环机制。
事件循环的核心组成部分:
- 调用栈 (Call Stack): 一个后进先出 (LIFO) 的数据结构,用于追踪函数调用。当一个函数被调用时,它的执行上下文(包含参数、局部变量等)被推入栈顶;当函数执行完毕返回时,其上下文从栈顶弹出。同步代码按顺序在调用栈中执行。
- Web APIs (浏览器环境) / C++ APIs (Node.js 环境): 浏览器或 Node.js 提供的异步 API(如
setTimeout,setInterval,fetch, DOM 事件监听器,Promise等)。当主线程遇到这些异步操作时,会将它们交给相应的 API 处理,并注册一个回调函数。主线程继续执行后续的同步代码。 - 任务队列 (Task Queue / Callback Queue / Macrotask Queue): 一个先进先出 (FIFO) 的队列,用于存放已完成的异步操作的回调函数(也称为宏任务 Macrotask)。当 Web API 完成一个异步任务时(例如
setTimeout时间到了,fetch请求有响应了),它会将对应的回调函数放入任务队列中等待执行。 - 微任务队列 (Microtask Queue): 另一个 FIFO 队列,用于存放微任务 (Microtask)。微任务通常由 Promise (如
.then,.catch,.finally的回调)、MutationObserver、queueMicrotask()等产生。微任务的优先级高于宏任务。
事件循环过程:
- 执行调用栈中的所有同步代码。
- 当调用栈为空时,事件循环开始检查微任务队列。
- 如果微任务队列不为空,则依次执行队列中的所有微任务,直到微任务队列变空。如果在执行微任务的过程中又产生了新的微任务,这些新的微任务也会被添加到队列末尾并在当前轮次执行完毕。
- 当微任务队列为空后,事件循环检查任务队列(宏任务队列)。
- 如果任务队列不为空,则取出一个宏任务(队列中的第一个),将其回调函数推入调用栈执行。
- 该宏任务执行完毕后,调用栈再次变空。事件循环返回第 2 步,再次检查微任务队列。
- 重复这个过程(检查微任务 -> 执行所有微任务 -> 取一个宏任务执行 -> 检查微任务...),这就是事件循环。
关键点:
- 一次事件循环迭代只会执行一个宏任务。
- 在每次宏任务执行完毕后,会清空整个微任务队列。
console.log('1. Script start'); // 同步
setTimeout(function() { // 宏任务 1
console.log('4. setTimeout');
}, 0);
Promise.resolve().then(function() { // 微任务 1
console.log('3. Promise then 1');
}).then(function() { // 微任务 2 (由微任务1产生)
console.log('5. Promise then 2'); // 注意:这个会在 setTimeout 之前执行完所有微任务后执行
});
console.log('2. Script end'); // 同步
// 输出顺序:
// 1. Script start
// 2. Script end
// 3. Promise then 1
// 4. setTimeout (错!应该是 5. Promise then 2 在 4 前面)
// 修正解释:微任务1执行后产生微任务2,微任务2加入微任务队列。当前微任务队列处理完后,才去处理宏任务队列。
// 正确输出顺序:
// 1. Script start
// 2. Script end
// 3. Promise then 1
// 5. Promise then 2 <-- 所有微任务先执行完
// 4. setTimeout <-- 然后执行宏任务(修正上方注释中的错误解释和顺序)
正确输出顺序:
1. Script start(同步)2. Script end(同步)3. Promise then 1(第一个微任务)5. Promise then 2(由第一个微任务产生的第二个微任务,在下一轮宏任务前执行)4. setTimeout(第一个宏任务)
Q: 解释一下原型 (Prototype) 和原型链 (Prototype Chain)。
A:
- 原型 (Prototype):
- 在 JavaScript 中,对象可以从其他对象继承属性和方法。这种继承机制就是通过原型实现的。
- 每个函数(除了箭头函数)在创建时都会自动获得一个
prototype属性,这个属性是一个对象,被称为原型对象。这个原型对象包含可以被该函数创建的实例共享的属性和方法。 - 每个对象(除了
Object.create(null)创建的对象)都有一个内部属性[[Prototype]](在现代浏览器中可以通过__proto__访问,或者通过Object.getPrototypeOf()获取),这个属性指向创建该对象的构造函数的原型对象 (Constructor.prototype)。
- 原型链 (Prototype Chain):
- 当试图访问一个对象的某个属性或方法时,JavaScript 引擎会首先在该对象自身查找。
- 如果在对象自身找不到,引擎会沿着
[[Prototype]]链(即__proto__指向的对象)向上查找,去其构造函数的原型对象 (Constructor.prototype) 中查找。 - 如果还没找到,就继续沿着原型对象的
[[Prototype]]链向上查找,直到找到该属性/方法,或者到达原型链的顶端 (Object.prototype)。 - 如果在
Object.prototype中仍然找不到,并且Object.prototype的[[Prototype]]是null(原型链的终点),则访问会返回undefined(对于属性)或抛出TypeError(如果试图调用一个不存在的方法)。 - 这个由对象的
[[Prototype]]连接起来的查找路径就构成了原型链。
总结:
- 函数有
prototype属性 (指向原型对象)。 - 实例对象有
__proto__属性 (指向构造函数的prototype)。 Constructor.prototype对象的constructor属性指回构造函数Constructor本身。- 原型链通过
__proto__连接,用于属性和方法的查找。
CSS 核心
Q: 什么是 CSS 样式污染 (或样式冲突)?如何避免?
A:
- 定义: CSS 样式污染(或称样式冲突、全局作用域问题)是指在一个大型项目或使用了多个库/组件的项目中,由于 CSS 规则默认是全局作用域的,不同模块或组件定义的 CSS 规则可能会无意中相互覆盖或影响,导致样式混乱或不符合预期。尤其是当使用通用或简单的类名(如
.title,.button)时,冲突的风险更大。 - 避免方法:
- 命名规范 (Methodologies):
- BEM (Block, Element, Modifier): 一种流行的 CSS 命名约定,通过结构化的命名(如
block__element--modifier)来创建唯一的、描述性的类名,降低冲突概率。例如:.card__title--large。
- BEM (Block, Element, Modifier): 一种流行的 CSS 命名约定,通过结构化的命名(如
- CSS Modules:
- 一种 CSS 文件处理方式(通常需要构建工具如 Webpack, Vite 支持)。每个 CSS 文件被视为一个独立的模块,其中的类名默认是局部作用域的。构建工具会为每个类名生成一个唯一的 hash 值(如
.styles_title_a3f7p),在 JS 中导入并使用这些生成的类名。彻底解决了全局污染问题。
css/*component.module.css*/ .title { color: blue; }javascript// component.js import styles from './component.module.css'; element.className = styles.title; // 应用的是生成的唯一类名 - 一种 CSS 文件处理方式(通常需要构建工具如 Webpack, Vite 支持)。每个 CSS 文件被视为一个独立的模块,其中的类名默认是局部作用域的。构建工具会为每个类名生成一个唯一的 hash 值(如
- CSS-in-JS:
- 将 CSS 样式直接写在 JavaScript 文件中的库或模式(如 Styled Components, Emotion)。样式与组件紧密耦合,通常会生成带 hash 的唯一类名或使用行内样式,天然具有作用域隔离。
javascript// 使用 styled-components import styled from 'styled-components'; const Title = styled.h1` color: red; font-size: 2em; `; // <Title>My Component Title</Title> - Shadow DOM:
- Web Components 标准的一部分。允许将一部分 DOM 结构和其关联的 CSS 封装起来,形成一个独立的、与主文档隔离的 "影子树"。Shadow DOM 内部的样式不会影响外部,外部样式也不会轻易影响内部(除非使用特定选择器如
::part,::slotted)。
- Web Components 标准的一部分。允许将一部分 DOM 结构和其关联的 CSS 封装起来,形成一个独立的、与主文档隔离的 "影子树"。Shadow DOM 内部的样式不会影响外部,外部样式也不会轻易影响内部(除非使用特定选择器如
- Scoped CSS (框架特性):
- 一些前端框架(如 Vue.js)提供了
<style scoped>语法。框架的构建工具会处理这些样式,为选择器添加一个唯一的属性选择器(如[data-v-f3f3eg9]),使得样式只作用于当前组件的元素。
vue<template> <div class="message">Hello</div> </template> <style scoped> .message { /* 最终会变成类似 .message[data-v-f3f3eg9] */ color: green; } </style> - 一些前端框架(如 Vue.js)提供了
- 提高 CSS 选择器特异性 (Specificity):
- 虽然不是根本解决办法,但合理使用更高特异性的选择器(如 ID 选择器、组合选择器、属性选择器)可以减少被低特异性全局规则覆盖的风险。但这会增加样式的复杂性和维护难度,不推荐作为主要策略。
- 命名空间 (Namespace):
- 为项目或模块的类名添加统一的前缀,手动创建命名空间,如
.my-project-button,.my-module-card。比 BEM 简单,但不如 BEM 结构化。
- 为项目或模块的类名添加统一的前缀,手动创建命名空间,如
- 命名规范 (Methodologies):
最佳实践: 对于现代前端开发,推荐使用 CSS Modules、CSS-in-JS 或框架提供的 Scoped CSS 功能来系统性地解决样式污染问题。BEM 也是一种非常有效且不依赖特定工具的纯 CSS 解决方案。
Q: 解释一下 CSS 盒模型 (Box Model),以及标准盒模型和 IE 盒模型的区别?如何切换盒模型?
A:
- CSS 盒模型:
- 在 CSS 中,每个 HTML 元素都被视为一个矩形的盒子。这个盒子由四个部分组成,从内到外依次是:
- Content: 盒子的内容区域,显示文本、图片等实际内容。其尺寸由
width和height属性定义。 - Padding (内边距): 包裹在内容区域外部的透明区域。其大小由
padding属性(padding-top,padding-right,padding-bottom,padding-left)定义。 - Border (边框): 包裹在内边距外部的线条。其样式、宽度和颜色由
border属性(border-width,border-style,border-color)定义。 - Margin (外边距): 包裹在边框外部的透明区域,用于控制元素与其他元素之间的间距。其大小由
margin属性(margin-top,margin-right,margin-bottom,margin-left)定义。
- Content: 盒子的内容区域,显示文本、图片等实际内容。其尺寸由
- 在 CSS 中,每个 HTML 元素都被视为一个矩形的盒子。这个盒子由四个部分组成,从内到外依次是:
- 标准盒模型 (W3C Box Model /
content-box):- 默认的盒模型。
- 元素的
width和height属性只包含 Content 区域的尺寸。 - 盒子的实际总宽度 =
width+padding-left+padding-right+border-left-width+border-right-width。 - 盒子的实际总高度 =
height+padding-top+padding-bottom+border-top-width+border-bottom-width。
- IE 盒模型 (Quirks Mode Box Model /
border-box):- 在旧版 IE 浏览器的怪异模式 (Quirks Mode) 下表现的盒模型,现在可以通过 CSS 属性设置为标准行为。
- 元素的
width和height属性包含了 Content、Padding 和 Border 的尺寸。 - 盒子的实际总宽度 =
width。 - 盒子的实际总高度 =
height。 - 内容区域的宽度会因
padding和border的增加而收缩。
- 如何切换盒模型:
使用 CSS 的
box-sizing属性可以控制元素采用哪种盒模型。box-sizing: content-box;(默认值): 使用标准盒模型。box-sizing: border-box;: 使用 IE 盒模型(通常更推荐)。设置border-box后,给元素设置的width和height就是最终渲染的尺寸(包括内边距和边框),布局计算更直观,不易因添加padding或border而撑破布局。
最佳实践: 通常在项目的全局 CSS 或 CSS Reset 中设置以下规则,将所有元素(或特定元素)的盒模型统一为
border-box:csshtml { box-sizing: border-box; } *, *:before, *:after { box-sizing: inherit; /* 让所有元素继承 html 的 box-sizing 设置 */ }
Q: CSS 选择器有哪些?它们的优先级 (Specificity) 是如何计算的?
A:
- 常见的 CSS 选择器:
- 类型选择器 (Type Selectors): 选择 HTML 元素类型,如
h1,p,div。 - 类选择器 (Class Selectors): 选择具有特定
class属性的元素,如.classname。 - ID 选择器 (ID Selectors): 选择具有特定
id属性的元素,如#idname。ID 在一个 HTML 文档中应该是唯一的。 - 属性选择器 (Attribute Selectors): 基于元素的属性及其值进行选择,如
[type="text"],[href^="https://"],[data-active]。 - 伪类选择器 (Pseudo-class Selectors): 选择处于特定状态的元素,如
:hover(鼠标悬停),:active(被激活),:focus(获得焦点),:nth-child(n)(选择特定顺序的子元素),:first-child,:last-child,:not(selector)等。 - 伪元素选择器 (Pseudo-element Selectors): 选择元素的某个部分(而不是元素本身),并可以为其添加样式,如
::before(在元素内容前插入生成的内容),::after(在元素内容后插入生成的内容),::first-line(选择第一行文本),::first-letter(选择第一个字母),::selection(选中的文本)。注意:在 CSS3 中,伪元素推荐使用双冒号::,但单冒号:为了兼容旧浏览器也通常有效。 - 后代选择器 (Descendant Combinator): 用空格分隔,选择某个元素内部的后代元素,如
div p(选择div内的所有p元素,无论层级多深)。 - 子选择器 (Child Combinator): 用
>分隔,选择某个元素的直接子元素,如ul > li(只选择ul的直接子元素li)。 - 相邻兄弟选择器 (Adjacent Sibling Combinator): 用
+分隔,选择紧跟在某个元素之后的同级元素,如h2 + p(选择紧跟在h2后面的那个p元素)。 - 通用兄弟选择器 (General Sibling Combinator): 用
~分隔,选择某个元素之后的所有同级元素,如h2 ~ p(选择h2之后的所有同级p元素)。 - 通用选择器 (Universal Selector):
*,选择所有元素。通常用于重置样式,但优先级最低。 - 分组选择器 (Grouping Selector): 用
,分隔,将相同的样式应用于多个选择器,如h1, h2, .highlight { color: red; }。
- 类型选择器 (Type Selectors): 选择 HTML 元素类型,如
- 优先级 (Specificity) 计算:
- 当多个 CSS 规则应用到同一个元素上时,浏览器需要根据选择器的**特异性(优先级)**来决定哪个规则生效。特异性通常用一个四元组
(a, b, c, d)或三元组(b, c, d)来表示(忽略 a,即行内样式)。 - 计算规则:
a: 行内样式 (Inline Styles) - 在元素的style属性中直接定义的样式。如果存在,a=1,否则a=0。行内样式优先级最高。b: ID 选择器的数量。c: 类选择器、属性选择器、伪类选择器的数量。d: 类型选择器、伪元素选择器的数量。
- 比较规则: 从左到右逐位比较
(a, b, c, d)。第一个不相等的位决定了哪个选择器优先级更高。例如(0, 1, 0, 0)(一个 ID 选择器) 比(0, 0, 10, 0)(十个类选择器) 优先级更高。 - 特殊情况:
!important: 添加在样式属性值后面的!important声明具有最高的优先级,它会覆盖任何其他来源的样式,包括行内样式。应谨慎使用!important,因为它会破坏正常的级联规则,使样式难以维护和调试。- 通用选择器 (
*) 和 继承的样式 (Inherited Styles) 的特异性为(0, 0, 0, 0),优先级最低。 - 如果多个规则具有完全相同的特异性,则后定义的规则(在 CSS 文件中或
<style>标签中位置更靠后的规则)会覆盖先定义的规则。
- 当多个 CSS 规则应用到同一个元素上时,浏览器需要根据选择器的**特异性(优先级)**来决定哪个规则生效。特异性通常用一个四元组
Q: 解释一下 Flexbox (弹性布局) 和 Grid (网格布局) 的主要区别和适用场景。
A:
Flexbox 和 Grid 都是 CSS3 中强大的布局模块,用于创建复杂、响应式的页面布局,但它们的设计目标和工作方式不同。
Flexbox (Flexible Box Layout):
- 设计思想: 主要用于一维布局。它让容器能够改变其子项(flex items)的宽度、高度和顺序,以最好地填充可用空间,即使子项的大小未知或动态变化。
- 轴线: 核心概念是主轴 (Main Axis) 和交叉轴 (Cross Axis)。你可以设置主轴是水平方向还是垂直方向。Flexbox 主要关注沿着单根轴线对齐和分布空间。
- 控制能力: 提供了对子项在主轴上的对齐(
justify-content)、分布、排序(order)以及在交叉轴上的对齐(align-items、align-self)、换行(flex-wrap)和多行对齐(align-content)的精细控制。子项本身也可以设置伸缩因子(flex-grow,flex-shrink,flex-basis)来控制如何分配剩余空间或收缩。 - 适用场景:
- 组件内部布局: 非常适合对齐一组项目,如导航栏、按钮组、卡片内的元素排列。
- 小规模布局: 处理简单的行或列布局。
- 内容驱动的布局: 当项目数量不定或尺寸需要灵活适应内容时。
- 一维对齐: 需要在一条线上(水平或垂直)精确控制元素的对齐和分布。
Grid Layout (CSS Grid Layout):
- 设计思想: 主要用于二维布局。它允许你将页面分割成行和列组成的网格,然后将元素精确地放置到这些网格单元(Grid Cells)或跨越多个单元的区域(Grid Areas)中。
- 轴线: 同时处理行轴和列轴。布局是基于网格线的。
- 控制能力: 提供了对网格结构(定义行和列的大小、间距
grid-template-rows,grid-template-columns,gap)、项目放置(grid-row,grid-column,grid-area)、对齐(justify-items,align-items,justify-content,align-content)和层叠(z-index)的全面控制。可以创建非常复杂的、非线性的布局结构。 - 适用场景:
- 整体页面布局: 非常适合构建整个页面的宏观布局框架,如页眉、页脚、侧边栏、主内容区。
- 复杂的二维结构: 需要精确控制元素在行和列上的位置和跨度,如仪表盘、画廊、复杂的表单布局。
- 内容与布局分离: Grid 允许布局结构与 HTML 源码顺序在一定程度上分离。
主要区别总结:
- 维度: Flexbox 主要是一维的,Grid 是二维的。
- 控制焦点: Flexbox 侧重于内容流和空间分布(“内容优先”),Grid 侧重于精确的网格位置(“布局优先”)。
- 方向: Flexbox 需要明确指定主轴方向(
flex-direction),Grid 天然就是行和列。 - 项目放置: Flexbox 项目通常按顺序排列,Grid 项目可以放置在网格的任意指定位置。
协同使用: Flexbox 和 Grid 并不互斥,它们可以很好地协同工作。通常使用 Grid 进行页面的整体宏观布局,然后在 Grid 的单元格内部使用 Flexbox 来处理该单元格内元素的微观对齐和分布。
Q: 如何实现一个元素的水平垂直居中?列举几种方法。
A:
实现元素的水平垂直居中是 CSS 布局中非常常见的需求,有多种方法可以实现,适用于不同的场景和元素类型。
方法一: Flexbox (最常用和推荐)
- 原理: 将父容器设置为 Flex 容器,然后使用
justify-content: center;实现主轴(通常是水平)居中,使用align-items: center;实现交叉轴(通常是垂直)居中。 - 代码:css
.parent { display: flex; justify-content: center; /* 水平居中 */ align-items: center; /* 垂直居中 */ height: 300px; /* 父容器需要有高度才能垂直居中 */ border: 1px solid black; } .child { width: 100px; height: 50px; background-color: lightblue; }html<div class="parent"> <div class="child">Centered Content</div> </div> - 优点: 代码简洁、灵活,适用于子元素尺寸未知的情况。
- 缺点: 需要浏览器支持 Flexbox (现代浏览器都支持)。
方法二: Grid (同样非常推荐)
- 原理: 将父容器设置为 Grid 容器,然后使用
place-items: center;(这是align-items: center;和justify-items: center;的简写) 让子项在其网格单元格内居中。如果父容器只有一个单元格(默认情况),子项就会在父容器中居中。或者使用place-content: center;(这是align-content: center;和justify-content: center;的简写) 对齐整个网格内容(如果只有一个项目效果类似)。 - 代码 (使用
place-items):css.parent { display: grid; place-items: center; /* 水平垂直居中网格项 */ height: 300px; border: 1px solid black; } .child { width: 100px; height: 50px; background-color: lightcoral; } - 代码 (使用
place-content- 更适合只有一个子元素的情况):css.parent { display: grid; /* 或 display: flex; 也能用 place-content */ place-content: center; /* 水平垂直居中网格(或Flex)内容 */ height: 300px; border: 1px solid black; } /* child 样式同上 */ - 优点: 代码同样简洁,功能强大。
- 缺点: 需要浏览器支持 Grid (现代浏览器都支持)。
方法三: 绝对定位 + Transform (常用,子元素尺寸可未知)
- 原理: 将子元素设置为绝对定位 (
position: absolute;),将其左上角移动到父容器的中心 (top: 50%; left: 50%;),然后使用transform: translate(-50%, -50%);将子元素自身向左上方回移其自身宽高的一半,从而实现精确居中。 - 代码:css
.parent { position: relative; /* 父容器需要定位上下文 */ height: 300px; border: 1px solid black; } .child { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 100px; /* 宽度和高度可以不固定 */ background-color: lightgreen; } - 优点: 子元素的尺寸可以未知或动态改变。兼容性好(
transform需要考虑前缀,但现代浏览器基本无需)。 - 缺点: 子元素脱离文档流。
transform可能影响某些元素的渲染(如fixed定位的子元素)。
方法四: 绝对定位 + Margin Auto (子元素尺寸固定)
- 原理: 将子元素设置为绝对定位 (
position: absolute;),设置top: 0; right: 0; bottom: 0; left: 0;使其边界紧贴父容器的四个边缘,然后设置margin: auto;。当一个绝对定位元素的对立方向(如top和bottom,或left和right)都被设置为非auto值(如0),并且该元素具有固定的尺寸时,margin: auto;会自动计算并分配相等的边距,从而实现居中。 - 代码:css
.parent { position: relative; height: 300px; border: 1px solid black; } .child { position: absolute; top: 0; right: 0; bottom: 0; left: 0; margin: auto; /* 关键 */ width: 100px; /* 必须有固定尺寸 */ height: 50px; /* 必须有固定尺寸 */ background-color: gold; } - 优点: 兼容性好。
- 缺点: 子元素必须具有固定的
width和height。子元素脱离文档流。
方法五: Table (旧方法,不推荐用于布局)
- 原理: 利用
display: table,display: table-cell,vertical-align: middle,text-align: center模拟表格布局的居中特性。 - 代码:css
.parent { display: table; width: 100%; /* 或固定宽度 */ height: 300px; border: 1px solid black; } .pseudo-cell { /* 需要一个中间层模拟 table-cell */ display: table-cell; vertical-align: middle; /* 垂直居中 */ text-align: center; /* 水平居中 (对 inline 或 inline-block 子元素) */ } .child { display: inline-block; /* 子元素设为 inline-block 以响应 text-align */ width: 100px; height: 50px; background-color: lightpink; /* 如果子元素是块级,水平居中需用 margin: 0 auto; */ }html<div class="parent"> <div class="pseudo-cell"> <div class="child">Centered Content</div> </div> </div> - 优点: 兼容性极好。
- 缺点: 结构复杂(通常需要额外包裹层),语义不佳(滥用表格显示属性),不灵活。现代布局应避免使用此方法。
选择哪种方法?
- 对于现代浏览器,Flexbox 和 Grid 是首选,代码简洁且功能强大。
- 如果需要兼容旧浏览器或子元素尺寸未知且不能使用 Flex/Grid,绝对定位 + Transform 是很好的选择。
- 如果子元素尺寸固定,绝对定位 + Margin Auto 也可以。
- 避免使用 Table 方法进行布局。
其他相关知识点
Q: 什么是跨域 (Cross-Origin)?有哪些常见的解决方案?
A:
- 定义: 跨域是指浏览器出于安全考虑,限制从一个源 (Origin) 加载的文档或脚本去与另一个源的资源进行交互。这被称为同源策略 (Same-Origin Policy)。
- 同源: 如果两个 URL 的 协议 (Protocol)、域名 (Host) 和 端口 (Port) 完全相同,则它们属于同源。只要有一个部分不同,就视为跨域。
http://example.com:80和http://example.com(默认端口80) 是同源。http://example.com和https://example.com(协议不同) 是跨域。http://example.com和http://www.example.com(域名不同,即使指向同一IP) 是跨域。http://example.com和http://example.com:8080(端口不同) 是跨域。
- 同源策略的限制: 主要限制以下几种行为:
- DOM 访问: 不同源的页面不能相互读取或修改对方的 DOM。
- Cookie、LocalStorage、IndexedDB 读取: 通常无法读取非同源存储的数据。
- AJAX 请求:
XMLHttpRequest或Fetch API发起的跨域请求默认会被浏览器阻止,除非目标服务器明确允许。这是最常见的跨域问题场景。
- 常见的跨域解决方案 (主要针对 AJAX 请求):
- CORS (Cross-Origin Resource Sharing - 跨源资源共享):
- 目前最主流、最规范的解决方案。
- 原理: 服务器端通过设置特定的 HTTP 响应头(如
Access-Control-Allow-Origin,Access-Control-Allow-Methods,Access-Control-Allow-Headers等)来告知浏览器,允许来自指定源(或所有源*)的请求访问其资源。 - 浏览器在发送跨域请求时(特别是“非简单请求”,如 PUT/DELETE 或带有自定义头部的请求),会先发送一个 OPTIONS 预检请求 (Preflight Request) 到服务器,询问服务器是否允许实际的请求。服务器响应允许后,浏览器才会发送真正的请求。
- 实现: 主要在后端服务器进行配置。前端代码基本无需特殊处理(除了可能需要设置
withCredentials为true来发送 Cookie)。
- JSONP (JSON with Padding):
- 原理: 利用
<script>标签的src属性不受同源策略限制的特点。客户端动态创建一个<script>标签,其src指向服务器端的一个接口,并带上一个回调函数名作为参数(如?callback=handleResponse)。服务器端接收到请求后,不再返回纯 JSON 数据,而是返回一段调用该回调函数的 JavaScript 代码,并将 JSON 数据作为参数传入(如handleResponse({"data": "value"}))。浏览器加载并执行这段脚本,从而调用了前端定义好的回调函数。 - 优点: 兼容性好,支持老旧浏览器。
- 缺点:
- 只支持 GET 请求。
- 安全性较低(容易遭受 XSS 攻击,需要信任提供 JSONP 服务的源)。
- 错误处理不方便(无法捕获网络错误等)。
- 目前已较少使用,优先考虑 CORS。
- 原理: 利用
- 代理服务器 (Proxy):
- 原理: 在同源的 Web 服务器(如 Node.js, Nginx, Apache)上设置一个代理接口。前端的 AJAX 请求发送给这个同源的代理接口,然后由代理服务器代替前端去请求目标跨域服务器的资源,并将获取到的响应返回给前端。因为服务器之间的数据请求不受浏览器同源策略的限制,所以可以成功获取数据。
- 实现:
- 开发环境: 现代前端构建工具(如 Webpack 的
devServer.proxy, Vite 的server.proxy)通常内置了方便配置的开发代理。 - 生产环境: 需要配置 Nginx、Apache 或使用 Node.js (如
http-proxy-middleware) 等搭建反向代理。
- 开发环境: 现代前端构建工具(如 Webpack 的
- 优点: 前端代码无需修改,可以解决各种跨域问题,相对安全(控制权在自己服务器)。
- 缺点: 需要额外部署和维护一个代理服务。
- WebSocket:
- WebSocket 协议本身不受同源策略限制,可以建立跨域连接。如果前后端通信需要实时、双向,WebSocket 是一个很好的选择,天然解决了跨域问题。
postMessageAPI:- 用于窗口之间(如
iframe与父窗口、不同标签页之间)的安全跨源通信。发送方使用otherWindow.postMessage(message, targetOrigin),接收方监听message事件。适用于特定场景的跨域通信,而非通用的 AJAX 请求。
- 用于窗口之间(如
- CORS (Cross-Origin Resource Sharing - 跨源资源共享):
Q: == 和 === 有什么区别?
A:
== 和 === 都是 JavaScript 中的比较运算符,主要区别在于比较过程是否进行类型转换。
==(相等运算符 / Loose Equality):- 在比较不同类型的操作数时,会尝试进行类型转换 (Type Coercion),将它们转换成相同的类型(通常是数字或字符串),然后再比较它们的值是否相等。
- 类型转换规则比较复杂:
null == undefined// true- 字符串和数字比较:字符串会尝试转换为数字。
'5' == 5// true - 布尔值和非布尔值比较:布尔值会转换为数字(
true-> 1,false-> 0)。true == 1// true - 对象和原始类型比较:对象会尝试调用
valueOf()或toString()方法转换为原始类型再比较。[10] == 10// true ([10].toString()->'10','10' == 10->10 == 10)
- 缺点: 由于隐式类型转换的存在,
==的行为有时可能不符合直觉,容易引入难以发现的 Bug。
===(严格相等运算符 / Strict Equality):- 不会进行类型转换。
- 它首先比较两个操作数的类型是否相同。如果类型不同,直接返回
false。 - 如果类型相同,再比较它们的值是否相等。
- 对于原始类型(数字、字符串、布尔、null、undefined、Symbol、BigInt),值相等则返回
true。 - 对于引用类型(对象、数组、函数),只有当它们引用同一个内存地址(即是同一个对象)时才返回
true。
- 对于原始类型(数字、字符串、布尔、null、undefined、Symbol、BigInt),值相等则返回
- 特例:
NaN === NaN// false (NaN 不等于任何值,包括它自己)+0 === -0// true
总结与建议:
==: 值相等(会进行类型转换)。===: 类型和值都相等(不进行类型转换)。- 推荐: 在开发中,为了代码的清晰性和可预测性,强烈建议始终使用
===进行相等比较,除非你有明确的理由需要利用==的类型转换特性(这种情况很少见,且通常可以用更明确的方式实现)。
console.log(5 == '5'); // true (类型转换后比较值)
console.log(5 === '5'); // false (类型不同)
console.log(true == 1); // true (true 转为 1)
console.log(true === 1); // false (类型不同)
console.log(null == undefined); // true (特殊规则)
console.log(null === undefined);// false (类型不同)
const obj1 = { a: 1 };
const obj2 = { a: 1 };
const obj3 = obj1;
console.log(obj1 == obj2); // false (引用不同)
console.log(obj1 === obj2); // false (引用不同)
console.log(obj1 == obj3); // true (引用相同)
console.log(obj1 === obj3); // true (引用相同)
console.log(NaN == NaN); // false
console.log(NaN === NaN); // false