Skip to content

前端开发面试高频考点 (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 对象有三种状态:
    1. Pending (进行中): 初始状态,既不是成功,也不是失败。
    2. Fulfilled (已成功): 表示异步操作成功完成。
    3. 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 语句捕获。
  • 优势:

    • 代码结构更清晰,像同步代码一样直观。
    • 错误处理更方便,可以使用标准的 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:

箭头函数和普通函数的主要区别在于:

  1. this 绑定:
    • 普通函数: this 的值在函数被调用时确定,取决于调用方式(全局调用、方法调用、构造函数调用、apply/call/bind调用)。
    • 箭头函数: this 的值在函数被定义时确定,它捕获其词法作用域(即外层非箭头函数作用域)的 this 值。箭头函数的 this 一旦绑定就不会改变call/apply/bind 无法改变箭头函数的 this 指向。
  2. 不能作为构造函数:
    • 箭头函数不能使用 new 关键字调用,因为它没有自己的 this 绑定,也没有 prototype 属性。尝试用 new 调用箭头函数会抛出错误。
  3. 没有 arguments 对象:
    • 箭头函数内部没有自己的 arguments 对象。如果需要访问所有传入的参数,可以使用剩余参数(Rest Parameters ...args)。
  4. 没有 prototype 属性:
    • 箭头函数没有 prototype 属性,因此不能用作构造器。
  5. 不能用作 Generator 函数:
    • 箭头函数不能使用 yield 关键字,不能定义为 Generator 函数。
  6. 语法简洁:
    • 当只有一个参数时,可以省略括号 ()
    • 当函数体只有一条返回语句时,可以省略花括号 {}return 关键字(隐式返回)。
javascript
// 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:

特性varletconst
作用域函数作用域 或 全局作用域块级作用域 {}块级作用域 {}
变量提升存在 (声明提升,初始化为 undefined)存在 (声明提升,但存在暂时性死区 TDZ)存在 (声明提升,但存在暂时性死区 TDZ)
重复声明允许 在同一作用域内不允许 在同一作用域内不允许 在同一作用域内
赋值允许重新赋值允许重新赋值不允许 重新赋值 (常量)
初始值可以不初始化 (默认为 undefined)可以不初始化 (默认为 undefined)必须 在声明时初始化
全局对象属性在全局作用域声明时,会成为 window (浏览器) 或 global (Node) 的属性在全局作用域声明时,不会成为全局对象的属性在全局作用域声明时,不会成为全局对象的属性

暂时性死区 (Temporal Dead Zone - TDZ): letconst 声明的变量,在代码块内,从块的开始到声明语句之间,访问该变量会抛出 ReferenceError。即使变量声明提升了,但在初始化之前是不能访问的。

const 注意点: const 声明的常量指的是变量标识符指向的内存地址不能改变。对于基本类型(如数字、字符串、布尔值),这意味着值本身不能改变。对于引用类型(如对象、数组),这意味着不能将变量重新指向另一个对象或数组,但可以修改该对象或数组内部的属性或元素

javascript
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 的指向不是在函数定义时确定的,而是在函数执行时确定的,主要取决于函数的调用方式:

  1. 全局上下文:

    • 在任何函数体之外,this 指向全局对象 (window 在浏览器中,global 在 Node.js 中)。
    • 在严格模式 ('use strict') 下,全局上下文中的 thisundefined
  2. 普通函数调用 (直接调用):

    • 非严格模式下,this 指向全局对象 (windowglobal)。
    • 严格模式下,thisundefined
    javascript
    function showThis() {
      console.log(this);
    }
    showThis(); // 非严格: window/global, 严格: undefined
  3. 方法调用 (作为对象的方法调用):

    • 当函数作为对象的一个属性被调用时 (obj.method()),this 指向调用该方法的对象 (obj)。
    javascript
    const myObj = {
      name: 'My Object',
      greet: function() {
        console.log(`Hello from ${this.name}`);
      }
    };
    myObj.greet(); // 输出 "Hello from My Object"
  4. 构造函数调用:

    • 当使用 new 关键字调用函数时,该函数作为构造函数。此时,this 指向新创建的实例对象
    javascript
    function Person(name) {
      this.name = name;
    }
    const person1 = new Person('Alice');
    console.log(person1.name); // 输出 "Alice"
  5. 显式绑定 (call, apply, bind):

    • function.call(thisArg, arg1, arg2, ...): 调用函数,并将 this 绑定到 thisArg,参数逐个传递。
    • function.apply(thisArg, [argsArray]): 调用函数,并将 this 绑定到 thisArg,参数以数组形式传递。
    • function.bind(thisArg, arg1, ...): 创建一个新函数,其 this 永久绑定到 thisArg,可以预设部分参数。新函数被调用时 this 不会再改变。
    javascript
    function 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"
  6. 箭头函数:

    • 箭头函数没有自己的 this 绑定。它会捕获其定义时所在的词法作用域this 值。this 的值在箭头函数定义时就已经确定,并且不能通过 call, apply, bind 来改变。

Q: 解释一下 JavaScript 的事件循环 (Event Loop) 机制。

A:

JavaScript 是单线程的,意味着它一次只能执行一个任务。为了处理耗时的操作(如 I/O、网络请求、定时器)而不会阻塞主线程,JavaScript 引入了异步编程模型和事件循环机制。

事件循环的核心组成部分:

  1. 调用栈 (Call Stack): 一个后进先出 (LIFO) 的数据结构,用于追踪函数调用。当一个函数被调用时,它的执行上下文(包含参数、局部变量等)被推入栈顶;当函数执行完毕返回时,其上下文从栈顶弹出。同步代码按顺序在调用栈中执行。
  2. Web APIs (浏览器环境) / C++ APIs (Node.js 环境): 浏览器或 Node.js 提供的异步 API(如 setTimeout, setInterval, fetch, DOM 事件监听器, Promise 等)。当主线程遇到这些异步操作时,会将它们交给相应的 API 处理,并注册一个回调函数。主线程继续执行后续的同步代码。
  3. 任务队列 (Task Queue / Callback Queue / Macrotask Queue): 一个先进先出 (FIFO) 的队列,用于存放已完成的异步操作的回调函数(也称为宏任务 Macrotask)。当 Web API 完成一个异步任务时(例如 setTimeout 时间到了,fetch 请求有响应了),它会将对应的回调函数放入任务队列中等待执行。
  4. 微任务队列 (Microtask Queue): 另一个 FIFO 队列,用于存放微任务 (Microtask)。微任务通常由 Promise (如 .then, .catch, .finally 的回调)、MutationObserverqueueMicrotask() 等产生。微任务的优先级高于宏任务

事件循环过程:

  1. 执行调用栈中的所有同步代码。
  2. 当调用栈为空时,事件循环开始检查微任务队列
  3. 如果微任务队列不为空,则依次执行队列中的所有微任务,直到微任务队列变空。如果在执行微任务的过程中又产生了新的微任务,这些新的微任务也会被添加到队列末尾并在当前轮次执行完毕。
  4. 当微任务队列为空后,事件循环检查任务队列(宏任务队列)
  5. 如果任务队列不为空,则取出一个宏任务(队列中的第一个),将其回调函数推入调用栈执行。
  6. 该宏任务执行完毕后,调用栈再次变空。事件循环返回第 2 步,再次检查微任务队列。
  7. 重复这个过程(检查微任务 -> 执行所有微任务 -> 取一个宏任务执行 -> 检查微任务...),这就是事件循环。

关键点:

  • 一次事件循环迭代只会执行一个宏任务。
  • 在每次宏任务执行完毕后,会清空整个微任务队列。
javascript
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. 1. Script start (同步)
  2. 2. Script end (同步)
  3. 3. Promise then 1 (第一个微任务)
  4. 5. Promise then 2 (由第一个微任务产生的第二个微任务,在下一轮宏任务前执行)
  5. 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)时,冲突的风险更大。
  • 避免方法:
    1. 命名规范 (Methodologies):
      • BEM (Block, Element, Modifier): 一种流行的 CSS 命名约定,通过结构化的命名(如 block__element--modifier)来创建唯一的、描述性的类名,降低冲突概率。例如:.card__title--large
    2. 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; // 应用的是生成的唯一类名
    3. 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>
    4. Shadow DOM:
      • Web Components 标准的一部分。允许将一部分 DOM 结构和其关联的 CSS 封装起来,形成一个独立的、与主文档隔离的 "影子树"。Shadow DOM 内部的样式不会影响外部,外部样式也不会轻易影响内部(除非使用特定选择器如 ::part, ::slotted)。
    5. 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>
    6. 提高 CSS 选择器特异性 (Specificity):
      • 虽然不是根本解决办法,但合理使用更高特异性的选择器(如 ID 选择器、组合选择器、属性选择器)可以减少被低特异性全局规则覆盖的风险。但这会增加样式的复杂性和维护难度,不推荐作为主要策略。
    7. 命名空间 (Namespace):
      • 为项目或模块的类名添加统一的前缀,手动创建命名空间,如 .my-project-button, .my-module-card。比 BEM 简单,但不如 BEM 结构化。

最佳实践: 对于现代前端开发,推荐使用 CSS Modules、CSS-in-JS 或框架提供的 Scoped CSS 功能来系统性地解决样式污染问题。BEM 也是一种非常有效且不依赖特定工具的纯 CSS 解决方案。

Q: 解释一下 CSS 盒模型 (Box Model),以及标准盒模型和 IE 盒模型的区别?如何切换盒模型?

A:

  • CSS 盒模型:
    • 在 CSS 中,每个 HTML 元素都被视为一个矩形的盒子。这个盒子由四个部分组成,从内到外依次是:
      1. Content: 盒子的内容区域,显示文本、图片等实际内容。其尺寸由 widthheight 属性定义。
      2. Padding (内边距): 包裹在内容区域外部的透明区域。其大小由 padding 属性(padding-top, padding-right, padding-bottom, padding-left)定义。
      3. Border (边框): 包裹在内边距外部的线条。其样式、宽度和颜色由 border 属性(border-width, border-style, border-color)定义。
      4. Margin (外边距): 包裹在边框外部的透明区域,用于控制元素与其他元素之间的间距。其大小由 margin 属性(margin-top, margin-right, margin-bottom, margin-left)定义。
  • 标准盒模型 (W3C Box Model / content-box):
    • 默认的盒模型。
    • 元素的 widthheight 属性只包含 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 属性设置为标准行为。
    • 元素的 widthheight 属性包含了 Content、Padding 和 Border 的尺寸。
    • 盒子的实际总宽度 = width
    • 盒子的实际总高度 = height
    • 内容区域的宽度会因 paddingborder 的增加而收缩。
  • 如何切换盒模型:
    • 使用 CSS 的 box-sizing 属性可以控制元素采用哪种盒模型。

      • box-sizing: content-box; (默认值): 使用标准盒模型。
      • box-sizing: border-box; : 使用 IE 盒模型(通常更推荐)。设置 border-box 后,给元素设置的 widthheight 就是最终渲染的尺寸(包括内边距和边框),布局计算更直观,不易因添加 paddingborder 而撑破布局。
    • 最佳实践: 通常在项目的全局 CSS 或 CSS Reset 中设置以下规则,将所有元素(或特定元素)的盒模型统一为 border-box

      css
      html {
        box-sizing: border-box;
      }
      *, *:before, *:after {
        box-sizing: inherit; /* 让所有元素继承 html 的 box-sizing 设置 */
      }

Q: CSS 选择器有哪些?它们的优先级 (Specificity) 是如何计算的?

A:

  • 常见的 CSS 选择器:
    1. 类型选择器 (Type Selectors): 选择 HTML 元素类型,如 h1, p, div
    2. 类选择器 (Class Selectors): 选择具有特定 class 属性的元素,如 .classname
    3. ID 选择器 (ID Selectors): 选择具有特定 id 属性的元素,如 #idname。ID 在一个 HTML 文档中应该是唯一的。
    4. 属性选择器 (Attribute Selectors): 基于元素的属性及其值进行选择,如 [type="text"], [href^="https://"], [data-active]
    5. 伪类选择器 (Pseudo-class Selectors): 选择处于特定状态的元素,如 :hover (鼠标悬停), :active (被激活), :focus (获得焦点), :nth-child(n) (选择特定顺序的子元素), :first-child, :last-child, :not(selector) 等。
    6. 伪元素选择器 (Pseudo-element Selectors): 选择元素的某个部分(而不是元素本身),并可以为其添加样式,如 ::before (在元素内容前插入生成的内容), ::after (在元素内容后插入生成的内容), ::first-line (选择第一行文本), ::first-letter (选择第一个字母), ::selection (选中的文本)。注意:在 CSS3 中,伪元素推荐使用双冒号 ::,但单冒号 : 为了兼容旧浏览器也通常有效。
    7. 后代选择器 (Descendant Combinator): 用空格分隔,选择某个元素内部的后代元素,如 div p (选择 div 内的所有 p 元素,无论层级多深)。
    8. 子选择器 (Child Combinator):> 分隔,选择某个元素的直接子元素,如 ul > li (只选择 ul 的直接子元素 li)。
    9. 相邻兄弟选择器 (Adjacent Sibling Combinator):+ 分隔,选择紧跟在某个元素之后的同级元素,如 h2 + p (选择紧跟在 h2 后面的那个 p 元素)。
    10. 通用兄弟选择器 (General Sibling Combinator):~ 分隔,选择某个元素之后的所有同级元素,如 h2 ~ p (选择 h2 之后的所有同级 p 元素)。
    11. 通用选择器 (Universal Selector): *,选择所有元素。通常用于重置样式,但优先级最低。
    12. 分组选择器 (Grouping Selector):, 分隔,将相同的样式应用于多个选择器,如 h1, h2, .highlight { color: red; }
  • 优先级 (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> 标签中位置更靠后的规则)会覆盖先定义的规则。

Q: 解释一下 Flexbox (弹性布局) 和 Grid (网格布局) 的主要区别和适用场景。

A:

Flexbox 和 Grid 都是 CSS3 中强大的布局模块,用于创建复杂、响应式的页面布局,但它们的设计目标和工作方式不同。

  • Flexbox (Flexible Box Layout):

    • 设计思想: 主要用于一维布局。它让容器能够改变其子项(flex items)的宽度、高度和顺序,以最好地填充可用空间,即使子项的大小未知或动态变化。
    • 轴线: 核心概念是主轴 (Main Axis)交叉轴 (Cross Axis)。你可以设置主轴是水平方向还是垂直方向。Flexbox 主要关注沿着单根轴线对齐和分布空间。
    • 控制能力: 提供了对子项在主轴上的对齐(justify-content)、分布、排序(order)以及在交叉轴上的对齐(align-itemsalign-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;。当一个绝对定位元素的对立方向(如 topbottom,或 leftright)都被设置为非 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;
    }
  • 优点: 兼容性好。
  • 缺点: 子元素必须具有固定的 widthheight。子元素脱离文档流。

方法五: 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>
  • 优点: 兼容性极好。
  • 缺点: 结构复杂(通常需要额外包裹层),语义不佳(滥用表格显示属性),不灵活。现代布局应避免使用此方法。

选择哪种方法?

  • 对于现代浏览器,FlexboxGrid 是首选,代码简洁且功能强大。
  • 如果需要兼容旧浏览器或子元素尺寸未知且不能使用 Flex/Grid,绝对定位 + Transform 是很好的选择。
  • 如果子元素尺寸固定,绝对定位 + Margin Auto 也可以。
  • 避免使用 Table 方法进行布局。

其他相关知识点

Q: 什么是跨域 (Cross-Origin)?有哪些常见的解决方案?

A:

  • 定义: 跨域是指浏览器出于安全考虑,限制从一个源 (Origin) 加载的文档或脚本去与另一个源的资源进行交互。这被称为同源策略 (Same-Origin Policy)
  • 同源: 如果两个 URL 的 协议 (Protocol)域名 (Host)端口 (Port) 完全相同,则它们属于同源。只要有一个部分不同,就视为跨域。
    • http://example.com:80http://example.com (默认端口80) 是同源。
    • http://example.comhttps://example.com (协议不同) 是跨域。
    • http://example.comhttp://www.example.com (域名不同,即使指向同一IP) 是跨域。
    • http://example.comhttp://example.com:8080 (端口不同) 是跨域。
  • 同源策略的限制: 主要限制以下几种行为:
    • DOM 访问: 不同源的页面不能相互读取或修改对方的 DOM。
    • Cookie、LocalStorage、IndexedDB 读取: 通常无法读取非同源存储的数据。
    • AJAX 请求: XMLHttpRequestFetch API 发起的跨域请求默认会被浏览器阻止,除非目标服务器明确允许。这是最常见的跨域问题场景。
  • 常见的跨域解决方案 (主要针对 AJAX 请求):
    1. CORS (Cross-Origin Resource Sharing - 跨源资源共享):
      • 目前最主流、最规范的解决方案。
      • 原理: 服务器端通过设置特定的 HTTP 响应头(如 Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers 等)来告知浏览器,允许来自指定源(或所有源 *)的请求访问其资源。
      • 浏览器在发送跨域请求时(特别是“非简单请求”,如 PUT/DELETE 或带有自定义头部的请求),会先发送一个 OPTIONS 预检请求 (Preflight Request) 到服务器,询问服务器是否允许实际的请求。服务器响应允许后,浏览器才会发送真正的请求。
      • 实现: 主要在后端服务器进行配置。前端代码基本无需特殊处理(除了可能需要设置 withCredentialstrue 来发送 Cookie)。
    2. JSONP (JSON with Padding):
      • 原理: 利用 <script> 标签的 src 属性不受同源策略限制的特点。客户端动态创建一个 <script> 标签,其 src 指向服务器端的一个接口,并带上一个回调函数名作为参数(如 ?callback=handleResponse)。服务器端接收到请求后,不再返回纯 JSON 数据,而是返回一段调用该回调函数的 JavaScript 代码,并将 JSON 数据作为参数传入(如 handleResponse({"data": "value"}))。浏览器加载并执行这段脚本,从而调用了前端定义好的回调函数。
      • 优点: 兼容性好,支持老旧浏览器。
      • 缺点:
        • 只支持 GET 请求。
        • 安全性较低(容易遭受 XSS 攻击,需要信任提供 JSONP 服务的源)。
        • 错误处理不方便(无法捕获网络错误等)。
        • 目前已较少使用,优先考虑 CORS。
    3. 代理服务器 (Proxy):
      • 原理:同源的 Web 服务器(如 Node.js, Nginx, Apache)上设置一个代理接口。前端的 AJAX 请求发送给这个同源的代理接口,然后由代理服务器代替前端去请求目标跨域服务器的资源,并将获取到的响应返回给前端。因为服务器之间的数据请求不受浏览器同源策略的限制,所以可以成功获取数据。
      • 实现:
        • 开发环境: 现代前端构建工具(如 Webpack 的 devServer.proxy, Vite 的 server.proxy)通常内置了方便配置的开发代理。
        • 生产环境: 需要配置 Nginx、Apache 或使用 Node.js (如 http-proxy-middleware) 等搭建反向代理。
      • 优点: 前端代码无需修改,可以解决各种跨域问题,相对安全(控制权在自己服务器)。
      • 缺点: 需要额外部署和维护一个代理服务。
    4. WebSocket:
      • WebSocket 协议本身不受同源策略限制,可以建立跨域连接。如果前后端通信需要实时、双向,WebSocket 是一个很好的选择,天然解决了跨域问题。
    5. postMessage API:
      • 用于窗口之间(如 iframe 与父窗口、不同标签页之间)的安全跨源通信。发送方使用 otherWindow.postMessage(message, targetOrigin),接收方监听 message 事件。适用于特定场景的跨域通信,而非通用的 AJAX 请求。

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
    • 特例:
      • NaN === NaN // false (NaN 不等于任何值,包括它自己)
      • +0 === -0 // true
  • 总结与建议:

    • ==: 值相等(会进行类型转换)。
    • ===: 类型和值都相等(不进行类型转换)。
    • 推荐: 在开发中,为了代码的清晰性和可预测性,强烈建议始终使用 === 进行相等比较,除非你有明确的理由需要利用 == 的类型转换特性(这种情况很少见,且通常可以用更明确的方式实现)。
javascript
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