阅读视图

发现新文章,点击刷新页面。

2025前端面试题

前端解决大规模并发问题的策略

前端处理大规模并发问题需要结合多种技术手段和架构优化,以下是一些关键解决方案:

1. 请求优化策略

  • 请求合并:将多个小请求合并为一个大请求
  • 节流(Throttle)与防抖(Debounce):控制高频事件的触发频率
  • 延迟加载:非关键资源延后加载
  • 分页/分批加载:大数据集分段获取

2. 缓存机制

// 使用内存缓存示例
const cache = new Map();

async function fetchWithCache(url) {
  if (cache.has(url)) {
    return cache.get(url);
  }
  const response = await fetch(url);
  const data = await response.json();
  cache.set(url, data);
  return data;
}

3. Web Workers处理密集型任务

// 主线程
const worker = new Worker('worker.js');
worker.postMessage({data: largeData});
worker.onmessage = (e) => {
  console.log('Result:', e.data);
};

// worker.js
self.onmessage = (e) => {
  const result = processLargeData(e.data); // CPU密集型操作
  self.postMessage(result);
};

4. WebSocket长连接

const socket = new WebSocket('wss://example.com');

socket.onopen = () => {
  socket.send(JSON.stringify({type: 'subscribe'}));
};

socket.onmessage = (event) => {
  const data = JSON.parse(event.data);
  // 处理实时数据更新...
};

5. Service Worker离线缓存

// service-worker.js
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open('v1').then((cache) => {
      return cache.addAll([
        '/styles/main.css',
        '/scripts/main.js',
        // ...其他关键资源
      ]);
    })
  );
});

self.addEventListener('fetch', (event) => {
 event.respondWith(
   caches.match(event.request).then((response) => {
     return response || fetch(event.request);
   })
 );
});

6. CDN与资源分发

  • 静态资源CDN分发
  • 按地域部署边缘节点
  • HTTP/2或HTTP/3多路复用

7. UI优化策略

  • 虚拟滚动(Virtual Scrolling):只渲染可视区域内容
  • 骨架屏(Skeleton Screen):提升用户感知体验
  • 渐进式渲染:优先显示关键内容

8. API设计配合

  • GraphQL:按需获取数据,减少冗余传输
  • BFF层(Backend For Frontend):为前端定制API聚合层

实际应用中,通常需要根据具体场景组合多种策略来应对高并发挑战。

Vue 开发者的外挂工具:配置一个 JSON,自动造出一整套页面!

🧰 引言:你是不是也这样?

作为一名前端开发者,你有没有经历过下面这些场景?

“又来一个新模块?好,先去 copy 个模板,改改名字……再写点基本结构……”

“这个组件结构每次都差不多,能不能别让我手敲了?”

“团队里每个人写的 Vue 文件格式都不一样,review 起来头大……”

如果你点头了,那这篇博客就是为你准备的!

今天我要分享的是我自己开发的一个 CLI 工具 —— catalog_file_generator,它能让你:

  • ✅ 通过一个配置文件,一键生成 Vue 页面和组件;
  • ✅ 支持多种模板类型(Vue2、Vue3、script setup);
  • ✅ 自动创建多层级目录结构;
  • ✅ 统一项目风格,提升团队协作效率;

一句话总结:它是一个“造房子”的工具,你只管画设计图,它帮你盖楼。


🔨 它到底能干啥?

场景一:批量创建模块?一行命令搞定!

你想创建 abcd 这几个模块,每个模块下都有 index.vueedit.vue,甚至还有子目录 components

传统做法是:新建目录 ➜ 拷贝模板 ➜ 改名 ➜ 修改内容 ➜ 循环重复……

catalog_file_generator 的话,只需要一个配置文件:

json
深色版本
{
  "a": {
    "index.vue": { "content": "a列表", "template": "v2" },
    "edit.vue": { "content": "a编辑", "template": "v2" }
  },
  "b": {
    "index.vue": { "content": "b列表", "template": "v2" },
    "edit.vue": { "content": "b编辑", "template": "v2" }
  },
  "c": {
    "index.vue": { "content": "c列表", "template": "v2" },
    "edit.vue": { "content": "c编辑", "template": "v2" },
    "info.vue": { "content": "c详情", "template": "v2" }
  },
  "d": {
    "components": {
      "tool.vue": { "content": "d工具组件", "template": "v2" }
    },
    "index.vue": { "content": "d列表", "template": "v2" }
  }
}

然后执行命令:

cf-cli generate -c config.json -o src/views

Boom!目录结构瞬间就建好了!


场景二:统一代码风格?模板说了算!

团队协作中,最怕的就是风格不统一。有人喜欢用 <script setup>,有人偏爱 Vue2 的 Options API。

怎么办?用 catalog_file_generator,直接在配置里指定模板路径:

json
深色版本
{
  "user": {
    "index.vue": {
      "content": "用户列表",
      "template": "/templates/vue3sTemp.vue"
    }
  }
}

所有生成的文件都使用同一个模板,风格一致,review 不头疼。


场景三:快速搭建 MVP?几分钟搞定几十个页面!

创业、内部孵化、临时需求……时间紧任务重?

用这个工具,几分钟就能搭出几十个页面结构,把精力留给真正重要的功能逻辑。


🚀 怎么安装和使用?

安装方式:

npm install -g catalog_file_generator

安装完成后,输入:

cf-cli --help

你会看到如下命令:

Usage: cf-cli [options] [command]

Commands:
  module     交互式生成模块(支持不同文件选择不同模板 + 输入中文内容)
  generate   根据配置文件生成模块结构(支持 .js/.json)

Options:
  -V, --version  输出版本号
  -h, --help     显示帮助信息

🛠️ 命令详解

1. 交互式创建模块:cf-cli module

适用于临时新增模块,比如用户管理、订单页等。

示例命令:

cf-cli module -n user,order --files index,edit,detail -o src/views

参数说明:

参数 含义
-n--name 模块名,多个用逗号分隔
--files 要生成的文件名列表,默认是 index
-o--output 输出目录,默认是 ./dist

执行后会进入交互流程:

  1. 选择模板类型:Vue2 / Vue3 / script setup / 自定义路径;
  2. 输入中文描述:自动替换模板中的占位符(如 #name#content);

2. 配置文件生成结构:cf-cli generate

适合一次性批量生成多个模块。

示例命令:

cf-cli generate -c config.json -o src/modules

参数说明:

参数 含义
-c--config 配置文件路径(支持 .json 或 .js
-o--output 输出目录,默认是 ./dist

📦 支持的模板类型一览

类型 示例 说明
内置模板 "v2" 使用工具自带的 Vue2 模板
绝对路径 "/templates/vue3sTemp.vue" 相对于项目根目录查找
相对路径 "../custom-templates/form.vue" 相对于当前模块目录查找

🧪 小试牛刀:试试看!

示例输出结构:

运行完命令后,会在 src/modules/ 下生成如下结构:

深色版本
src/modules/
├── user/
│   ├── index.vue
│   ├── edit.vue
│   └── detail.vue
└── order/
    ├── index.vue
    ├── edit.vue
    └── detail.vue

每个 .vue 文件都会根据你选择的模板和内容自动填充内容,无需手动编写。


⚙️ 如何封装到自己的项目脚本中?

你可以封装一个 Node.js 脚本来调用这个 CLI,方便集成到你的项目中。

创建 scripts/cf-page.js

#!/usr/bin/env node

const { spawn } = require('child_process');
const chalk = require('chalk');
const path = require('path');
const [name] = process.argv.slice(2);

if (!name) {
  console.error(chalk.red('❌ 请提供模块名称,例如:npm run cf:page UserPage'));
  process.exit(1);
}

const cliEntry = path.resolve(__dirname, '../node_modules/catalog_file_generator/cli.js');

const args = [
  'module',
  '-n', name,
  '--files', 'index',
  '-o', 'src/components'
];

const child = spawn('node', [cliEntry, ...args], { stdio: 'inherit' });

child.on('error', (err) => {
  console.error(chalk.red(`❌ 子进程启动失败:${err.message}`));
  process.exit(1);
});

child.on('close', (code) => {
  if (code === 0) {
    console.log(chalk.green(`✅ 模块【${name}】创建成功!`));
  } else {
    console.error(chalk.red(`❌ 创建失败,退出码:${code}`));
  }
});

在 package.json 中添加脚本:

"scripts": {
  "cf:page": "node scripts/cf-page.js"
}

使用方式:

npm run cf:page UserPage

🌟 总结:为什么你应该试试它?

功能 亮点
🧩 支持交互式创建模块 每个文件都能选模板、填内容
📄 支持配置文件驱动 一次生成多个模块,结构清晰可复用
🎨 多种模板类型可选 支持 Vue2/Vue3,也可自定义路径
📂 支持嵌套结构生成 灵活控制目录层级
🌈 带颜色的日志提示 提升用户体验,便于排查问题

🎉 结语:别让工具牵着你走,要让它为你服务!

catalog_file_generator 不只是一个脚手架工具,它更像是一位“代码建筑师”,你只需要告诉它你要什么结构,剩下的交给它就行。

下次当你又要手写第 10 个 Vue 文件的时候,不妨试试这个工具,让你从重复劳动中解放出来,去做更有价值的事!


📌 GitHub 仓库链接?

👉 我已经打包发布到了 npm,你可以直接使用 npm install -g catalog_file_generator 安装。

📌 想扩展功能?

👉 欢迎 fork、PR、提 issue,我们一起打造更强大的前端代码生成器!


🎯 最后送大家一句话:

“程序员的价值不是写多少行代码,而是让代码尽可能少地写。”

用工具解放双手,才是真正的“高效编程”。


觉得有帮助的话,记得点个赞、收藏、转发哦~
💬 欢迎留言交流你日常开发中遇到的重复性工作,我们可以一起想办法自动化解决!

一行代码生成绝对唯一 ID:告别 Date.now() 的不可靠方案

在现代 Web 开发中,生成唯一标识符(ID)是一个常见需求。无论是用户会话、临时文件还是数据库记录,我们都需要确保每个 ID 的绝对唯一性。然而,许多开发者仍在使用的传统方法其实存在严重缺陷。

常见误区与问题

误区一:时间戳 + 随机数组合

function generateNaiveId() {
    return Date.now().toString(36) + Math.random().toString(36).substr(2);
}
// 输出示例: "l6n7f4v2am50k9m7o4"

这种方法看似合理,实则存在两大致命缺陷:

  1. 时间戳精度问题Date.now() 仅精确到毫秒,同一毫秒内的多次调用会导致 ID 前缀相同

  2. 伪随机性问题Math.random() 不是加密级随机数,存在极小概率的重复风险

误区二:简单的自增计数器

let counter = 0;
function generateIncrementalId() {
    return counter++;
}

这种方案的问题更加明显:

  • 浏览器刷新后计数器重置

  • 多标签页环境下计数器独立运行,导致 ID 冲突

  • 完全不适合分布式环境

现代解决方案:crypto.randomUUID()

现代浏览器和 Node.js 提供了内置的加密解决方案:

const uniqueId = crypto.randomUUID();
// 示例输出: "3a6c4b2a-4c26-4d0f-a4b7-3b1a2b3c4d5e"

为什么这是最佳选择?

  1. 极低碰撞概率:基于 122 位随机数生成,组合数量达到天文数字级别

  2. 加密级安全性:使用密码学安全伪随机数生成器(CSPRNG)

  3. 标准化格式:符合 RFC 4122 v4 规范,全栈兼容

  4. 原生高效:无需第三方库,性能优异

兼容性与使用建议

crypto.randomUUID() 已在所有现代浏览器中得到支持:

  • Chrome 92+

  • Firefox 90+

  • Safari 15.4+

  • Node.js 14+

对于新项目,这是生成唯一 ID 的推荐方案。对于需要支持旧版浏览器的项目,可以考虑使用 polyfill 或第三方库(如 uuid 库)。

结论

告别不可靠的 Date.now()Math.random() 组合,拥抱现代浏览器提供的标准解决方案。crypto.randomUUID() 以一行代码的形式,提供了真正安全、可靠、标准的唯一 ID 生成能力,是 Web 开发中的最佳实践。

前端网络性能优化

在现代 Web 开发中,网络性能优化是提升用户体验的关键环节。加载缓慢的网站可能导致用户流失,因此,掌握网络性能优化的方法对于前端开发者来说至关重要。本文将详细介绍多种优化网络性能的策略,帮助你打造更快速、更流畅的 Web 应用。

一、优化打包体积

压缩与混淆代码

利用 Webpack、Rollup 等打包工具,可以对最终打包的代码进行压缩和混淆。通过移除代码中的注释、空格、换行符,以及缩短变量名,可以显著减少文件体积。例如,使用 Webpack 的 TerserPlugin 插件,可以自动压缩 JavaScript 代码。

// webpack.config.js
const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [new TerserPlugin()],
  },
};

多目标打包

针对不同浏览器打包出不同的兼容性版本,可以减少每个版本中的兼容性代码。例如,使用 @babel/preset-env 插件,可以根据目标浏览器自动添加所需的 polyfill。

// babel.config.js
module.exports = {
  presets: [
    [
      '@babel/preset-env',
      {
        targets: {
          browsers: ['> 1%', 'last 2 versions'],
        },
      },
    ],
  ],
};

二、利用压缩技术

现代浏览器普遍支持压缩格式,如 Gzip 和 Brotli。服务器可以在响应文件时进行压缩,只要解压时间小于优化的传输时间,压缩就是可行的。

启用 Gzip 压缩

在 Nginx 服务器中,可以通过以下配置启用 Gzip 压缩:

gzip on;
gzip_types text/plain text/css application/json application/javascript;

三、使用 CDN

内容分发网络(CDN)可以大幅缩减静态资源的访问时间。特别是对于公共库,可以使用知名的 CDN 资源,这样可以实现跨越站点的缓存。

引入 CDN 资源

<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>

四、合理设置缓存

对于除 HTML 外的所有静态资源,可以开启协商缓存。利用构建工具打包产生的文件 hash 值来置换缓存。

协商缓存

服务器在响应头中添加 ETagLast-Modified 字段,浏览器在后续请求中通过 If-None-MatchIf-Modified-Since 字段进行验证。

ETag: "5d8c72a5edcf8"
Last-Modified: Mon, 23 May 2021 09:00:00 GMT

五、启用 HTTP/2

HTTP/2 具有多路复用、头部压缩等特点,可以充分利用带宽传递大量的文件数据。

多路复用

HTTP/2 允许在单个连接上并行传输多个请求和响应,避免了 HTTP/1.1 中的队头阻塞问题。

六、雪碧图与图片优化

对于不使用 HTTP/2 的场景,可以将多个图片合并为雪碧图,以减少文件数量。

雪碧图

.icon {
  background-image: url('sprite.png');
  background-position: -10px -10px;
}

七、异步加载 JavaScript

通过 deferasync 属性,可以让页面尽早加载 JavaScript 文件,而不会阻塞 HTML 解析。

defer 与 async

<script src="app.js" defer></script>
<script src="analytics.js" async></script>
  • defer:脚本在 HTML 解析完成后执行。
  • async:脚本在下载完成后立即执行。

八、资源预加载

通过 prefetchpreload 属性,可以让页面预先下载可能用到的资源。

prefetch

<link rel="prefetch" href="next-page.js">

preload

<link rel="preload" href="critical-resource.js" as="script">

九、多个静态资源域

对于不使用 HTTP/2 的场景,将相对独立的静态资源分到多个域中保存,可以让浏览器同时开启多个 TCP 连接,并行下载。

多域名策略

<script src="https://static1.example.com/js/app.js"></script>
<link rel="stylesheet" href="https://static2.example.com/css/style.css">

总结

优化网络性能是一个持续的过程,需要开发者不断探索和实践。通过上述方法,可以显著提升 Web 应用的加载速度和用户体验。在实际开发中,应根据具体情况选择合适的优化策略,以达到最佳效果。

React中的路由艺术:用react-router-dom实现无缝页面切换

在现代前端开发中,单页应用(SPA)已成为主流,而路由则是SPA的核心功能之一。本文将介绍如何在React中使用react-router-dom库实现页面导航和路由控制。

什么是单页应用?

单页应用(SPA)是指整个应用只有一个HTML文件,通过动态加载不同组件来模拟多页面体验。如README中所说:

"只有一个html文件,将每一个页面都开发成一个组件,通过某一种手段来控制当前加载哪一个组件,来实现页面的切换效果"

React Router 基础

React Router是React生态中最流行的路由解决方案。首先需要安装:

npm i react-router-dom

核心组件

  1. BrowserRouter:使用HTML5 history API实现的路由模式
  2. Routes:路由出口,包含所有路由规则
  3. Route:定义路径与组件的映射关系

路由配置示例

让我们看看App.jsx中的基本路由配置:

import { BrowserRouter, Routes, Route, Link } from 'react-router-dom'

function App() {
  return (
    <div>
      <BrowserRouter>
        <Link to="/home">首页</Link>
        <Link to="/about">关于</Link>

        <Routes>
          <Route path='/home' element={<Home/>} />
          <Route path='/about' element={<About/>} />
          <Route path='/user' element={<User/>} />
        </Routes>
      </BrowserRouter>
    </div>
  )
}

这里我们:

  1. 使用BrowserRouter包裹整个路由系统
  2. Link组件创建导航链接
  3. Routes中定义路由规则,指定每个路径对应的组件

页面导航

在React组件中,我们有两种方式进行导航:

1. 使用Link组件(声明式导航)

<Link to="/about">关于</Link>

2. 使用useNavigate钩子(编程式导航)

Home.jsx中展示了如何使用编程式导航:

import { useNavigate } from "react-router-dom"

function Home() {
  const navigate = useNavigate()

  return (
    <div>
      <h2>Home Page</h2>
      <button onClick={() => {navigate('/user?id=1')}}>去用户页面</button>
    </div>
  )
}

获取URL参数

User.jsx展示了如何获取URL查询参数:

import { useSearchParams } from "react-router-dom" 

function User() {
  const [params] = useSearchParams()

  return (
    <h4>用户页面 -- {params.get('id')}</h4>
  )
}

当访问/user?id=1时,页面会显示"用户页面 -- 1"。

总结

React Router提供了强大而灵活的路由功能,让我们能够:

  • 通过BrowserRouter设置路由模式
  • 通过LinkuseNavigate实现导航
  • 使用useSearchParams获取URL参数

希望本文能帮助你快速上手React Router!在实际项目中,你还可以探索更多高级功能,如嵌套路由、路由守卫、懒加载等。

🧙‍♂️ CSS中的结界术:BFC如何拯救你的布局混乱?

🔮 场景引入:消失的绿色背景

想象你写了这样一段HTML————代码1

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>BFC</title>
  <style>
  .container {
    background-color:green;
  }
  .box {
    margin: 100px;
    width: 100px;
    height: 100px;
    background-color: red;
    float: left;
  }
  </style>
</head>
<body>
  <div class="container">
    <div class="box"></div>
  </div>
 
</body>
</html>

运行后却发现——绿色背景神秘消失了!🤔 这正是 图片 演示的经典问题:

Snipaste_2025-07-12_16-20-25.png

别急,这时候就需要召唤CSS世界的"结界大师"—— BFC 登场了!

🧩 什么是BFC?

BFC(Block Formatting Context) 即 块级格式化上下文 ,就像给元素罩上一个看不见的结界: 我们将它形象地描述为:

全新的渲染区域,不受外界支配

可以把BFC理解为:

  • 🏠 一个独立的房间,内部元素再怎么折腾也不会影响外面
  • 🚧 一堵无形的墙,阻止margin合并、清除浮动等布局问题
  • 🔍 一个严格的管家,计算高度时连浮动元素也不会放过

✨ BFC的四大超能力

1️⃣ 清除浮动:让父元素拥抱浮动的孩子

代码1 中容器高度坍塌的原因是:浮动元素脱离了文档流。只需给容器添加结界咒语

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>BFC清除浮动示例</title>
  <style>
    .container {
      background: #4CAF50; /* 绿色背景 */
      padding: 20px;
      /* 未触发BFC时:父元素高度为0,背景色不显示 */
      /* 触发BFC后:父元素高度包含浮动子元素 */
      overflow: hidden; /* 👈 触发BFC的关键 */
    }
    .box {
      width: 100px;
      height: 100px;
      background: #ff4444; /* 红色方块 */
      float: left; /* 子元素浮动 */
      margin: 10px;
    }
  </style>
</head>
<body>
  <h3>触发BFC后:父元素高度正常显示</h3>
  <div class="container">
    <div class="box"></div>
    <div class="box"></div>
    <div class="box"></div>
  </div>

  <h3 style="margin-top: 50px;">未触发BFC:父元素高度坍塌(背景色消失)</h3>
  <div class="container" style="overflow: visible;"> <!-- 移除BFC触发 -->
    <div class="box"></div>
    <div class="box"></div>
    <div class="box"></div>
  </div>
</body>
</html>

Snipaste_2025-07-12_16-39-59.png

效果说明 :上方绿色容器因 overflow:hidden 触发BFC,能正确包裹浮动的红色方块;下方容器未触发BFC,背景色消失(高度坍塌)。


2️⃣ 阻止margin合并:让边距不再"打架"

两个相邻divmargin会神秘重叠?给它们各自套上BFC结界:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>BFC阻止margin合并示例</title>
  <style>
    .box {
      height: 100px;
      margin: 20px 0; /* 上下margin各20px */
      background: #2196F3; /* 蓝色方块 */
    }
    .bfc-container {
      /* 触发BFC,阻止内部元素margin与外部合并 */
      display: flow-root; /* 👈 CSS3专门触发BFC的属性 */
    }
  </style>
</head>
<body>
  <h3>未阻止margin合并:两个元素间距只有20px(而非40px)</h3>
  <div class="box"></div>
  <div class="box"></div>

  <h3 style="margin-top: 50px;">触发BFC阻止合并:两个元素间距40px(20px+20px)</h3>
  <div class="bfc-container">
    <div class="box"></div>
  </div>
  <div class="bfc-container">
    <div class="box"></div>
  </div>
</body>
</html>

Snipaste_2025-07-12_16-42-19.png

效果说明 :上方两个蓝色方块margin重叠(间距20px);下方每个方块被BFC容器包裹,margin不重叠(间距40px)。


3️⃣ 防止文字环绕:给浮动元素划清界限

当文字遇到浮动元素会自动环绕,但有时我们需要"井水不犯河水":

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>BFC防止文字环绕对比</title>
  <style>
    .container {
      width: 400px;
      margin: 20px 0;
      padding: 15px;
      border: 2px dashed #9c27b0; /* 紫色虚线边框区分容器 */
    }
    .float-box {
      width: 120px;
      height: 120px;
      background: #ff5722; /* 橙色浮动元素(更醒目) */
      float: left;
      margin-right: 15px;
      display: flex;
      align-items: center;
      justify-content: center;
      color: white;
      font-weight: bold;
    }
    .text-content {
      background: #e3f2fd; /* 浅蓝色文本区域 */
      padding: 10px;
      /* 未触发BFC时文字会环绕浮动元素 */
    }
    /* 触发BFC的容器 */
    .bfc-container {
      background: #e8f5e9; /* 浅绿色BFC容器 */
      padding: 10px;
      /* 多种BFC触发方式对比(任选其一即可) */
      overflow: auto; /* 方式1:滚动条可能出现 */
      /* display: flow-root; */ /* 方式2:现代浏览器推荐,无副作用 */
      /* border: 1px solid transparent; */ /* 方式3:利用边框触发 */
    }
    .title {
      color: #333;
      border-bottom: 2px solid #eee;
      padding-bottom: 8px;
    }
  </style>
</head>
<body>
  <h2>BFC防止文字环绕效果对比</h2>

  <!-- 未触发BFC:文字环绕 -->
  <div class="container">
    <h3 class="title">未触发BFC(文字环绕浮动元素)</h3>
    <div class="float-box">浮动元素</div>
    <div class="text-content">
      <p>这是未触发BFC的文本容器,文字会从右侧环绕浮动元素。当文本足够长时,会在浮动元素下方继续排列:</p>
      <p>示例文本示例文本示例文本示例文本示例文本示例文本示例文本示例文本示例文本示例文本示例文本示例文本示例文本示例文本示例文本示例文本示例文本示例文本示例文本示例文本示例文本。</p>
    </div>
  </div>

  <!-- 触发BFC:文字不环绕 -->
  <div class="container">
    <h3 class="title">触发BFC(文本容器与浮动元素并列)</h3>
    <div class="float-box">浮动元素</div>
    <div class="bfc-container">
      <p>这是触发BFC的文本容器,整个容器会被浮动元素"推开",文字不会环绕:</p>
      <p>示例文本示例文本示例文本示例文本示例文本示例文本示例文本示例文本示例文本示例文本示例文本示例文本示例文本示例文本示例文本示例文本示例文本示例文本示例文本示例文本示例文本。</p>
    </div>
  </div>

  <!-- 新增:BFC解决margin合并问题 -->
  <div class="container">
    <h3 class="title">BFC额外能力:解决margin合并</h3>
    <div style="background: #ffebee; padding: 10px;">
      <p>未触发BFC:相邻元素margin会合并(两个p标签间距只有10px而非20px)</p>
      <p style="margin: 10px 0; background: #ef9a9a;">段落1(margin:10px)</p>
      <p style="margin: 10px 0; background: #ef9a9a;">段落2(margin:10px)</p>
    </div>
    <div class="bfc-container" style="margin-top: 20px;">
      <p>触发BFC:内部元素margin不会与外部合并</p>
      <p style="margin: 10px 0; background: #81c784;">段落A(margin:10px)</p>
      <p style="margin: 10px 0; background: #81c784;">段落B(margin:10px)</p>
    </div>
  </div>
</body>
</html>

屏幕截图 2025-07-12 174017.png

可以看到触发BFC明显可以防止文字环绕的效果。我们成功地用结界困住了它们。


4️⃣ 多列布局:防止列宽坍塌

在弹性布局普及前,BFC是实现多列布局的利器:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>BFC多列布局对比示例</title>
  <style>
    .demo-container {
      margin: 30px 0;
      padding: 15px;
      background: #f5f5f5;
      border-radius: 8px;
    }
    .title {
      color: #333;
      border-left: 4px solid #9c27b0;
      padding-left: 10px;
      margin-bottom: 20px;
    }
    /* 基础样式 */
    .layout-container {
      width: 100%;
      border: 2px solid #9c27b0;
      margin-bottom: 20px;
    }
    .column {
      width: 33.33%;
      float: left;
      color: white;
      padding: 15px;
      box-sizing: border-box; /* 确保padding不影响宽度计算 */
    }
    .col1 { background: #ff9800; }
    .col2 { background: #8bc34a; }
    .col3 { background: #03a9f4; }
    /* 未触发BFC的容器 */
    .without-bfc {
      /* 未触发BFC:容器高度塌陷,边框无法包裹浮动元素 */
    }
    /* 触发BFC的容器 */
    .with-bfc {
      /* 触发BFC:容器能正确包裹浮动元素 */
      overflow: hidden; 
    }
    /* 内容溢出测试 */
    .tall-content {
      height: 150px; /* 其中一列内容高度增加 */
    }
    .overflow-content {
      width: 120%; /* 内容宽度溢出列容器 */
    }
    /* 修复说明标签 */
    .fix-note {
      background: #ffe0b2;
      padding: 8px 12px;
      border-radius: 4px;
      font-size: 14px;
      color: #e65100;
      margin-top: 10px;
    }
  </style>
</head>
<body>
  <h2>BFC多列布局核心作用演示</h2>

  <!-- 问题1:未触发BFC导致容器高度塌陷 -->
  <div class="demo-container">
    <h3 class="title">问题:未触发BFC的容器无法包裹浮动元素</h3>
    <div class="layout-container without-bfc">
      <div class="column col1">第一列</div>
      <div class="column col2">第二列</div>
      <div class="column col3">第三列</div>
    </div>
    <p class="fix-note">⚠️ 现象:紫色边框容器高度塌陷(看不到高度),因为浮动元素脱离文档流</p>
  </div>

  <!-- 解决方案1:容器触发BFC解决高度塌陷 -->
  <div class="demo-container">
    <h3 class="title">解决方案:容器触发BFC包裹浮动元素</h3>
    <div class="layout-container with-bfc">
      <div class="column col1">第一列</div>
      <div class="column col2">第二列</div>
      <div class="column col3">第三列</div>
    </div>
    <p class="fix-note">✅ 修复:容器添加 <code>overflow: hidden</code> 触发BFC, now能正确包裹浮动列</p>
  </div>

  <!-- 问题2:内容溢出影响其他列 -->
  <div class="demo-container">
    <h3 class="title">问题:列内容溢出影响相邻列(未触发BFC)</h3>
    <div class="layout-container with-bfc">
      <div class="column col1">第一列</div>
      <div class="column col2">
        <div class="overflow-content">第二列内容溢出(宽度120%)</div>
      </div>
      <div class="column col3">第三列(被溢出内容覆盖)</div>
    </div>
    <p class="fix-note">⚠️ 现象:第二列内容溢出后覆盖了第三列,因为列本身未触发BFC</p>
  </div>

  <!-- 解决方案2:列触发BFC防止内容溢出 -->
  <div class="demo-container">
    <h3 class="title">解决方案:列元素触发BFC隔离内容</h3>
    <div class="layout-container with-bfc">
      <div class="column col1">第一列</div>
      <div class="column col2" style="overflow: hidden;"> <!-- 列触发BFC -->
        <div class="overflow-content">第二列内容溢出(但被BFC隔离)</div>
      </div>
      <div class="column col3">第三列(不受溢出影响)</div>
    </div>
    <p class="fix-note">✅ 修复:给第二列添加 <code>overflow: hidden</code> 触发BFC,内容溢出被限制在列内部</p>
  </div>

  <!-- 扩展:BFC实现等高列布局 -->
  <div class="demo-container">
    <h3 class="title">扩展:BFC实现等高列布局(传统方案)</h3>
    <div class="layout-container with-bfc">
      <div class="column col1">第一列(普通高度)</div>
      <div class="column col2" style="overflow: hidden;">
        <div class="tall-content">第二列(高度增加)</div>
      </div>
      <div class="column col3" style="overflow: hidden;">第三列(自动与最高列等高)</div>
    </div>
    <p class="fix-note">💡 原理:BFC容器内的浮动元素会根据内容最高的列自动调整高度,实现伪等高效果</p>
  </div>
</body>
</html>

屏幕截图 2025-07-12 174440.png屏幕截图 2025-07-12 174458.png

📜 召唤BFC的咒语(触发条件)

只需给元素添加以下任意一个属性,就能召唤BFC结界:

  • overflow: hidden (最常用)
  • float: left/right (但会让元素浮动)
  • position: absolute/fixed (脱离文档流)
  • display: inline-block/flex/grid (现代布局方案)
  • display: flow-root (CSS3新增,专门触发BFC且无副作用)

💡 结界大师的忠告

  1. 不要滥用overflow:hidden :可能会隐藏需要显示的内容(如下拉菜单)
  2. 现代布局优先用Flex/Grid :BFC更多是解决兼容性问题的"上古神器"
  3. 理解原理比死记规则重要 :BFC本质是浏览器的渲染隔离机制

🎬 总结

BFC就像CSS世界的结界术,看似神秘,实则是浏览器为我们提供的布局保护机制。下次遇到布局坍塌margin重叠等问题时,不妨默念咒语:

"overflow: hidden!" 🔮

深入解析CSRF攻击

在网络安全领域,CSRF(Cross-site Request Forgery,跨站请求伪造)是一种常见的攻击手段,它通过挟持用户在当前已登录的Web应用上执行非本意的操作,从而对用户和网站造成严重威胁。本文将详细介绍CSRF攻击的原理、危害以及有效的防御策略。

一、CSRF攻击的原理

image-20211101145156371

CSRF攻击的核心在于利用用户的身份信息,执行用户非本意的操作。攻击者通常会通过以下步骤实施CSRF攻击:

  1. 诱导用户访问恶意网站:攻击者首先需要诱导用户访问一个恶意网站。这可以通过电子邮件、即时消息或社交媒体链接等方式实现。
  2. 发送伪造请求:当用户访问恶意网站时,该网站会自动发送一个请求到目标网站。由于用户的浏览器会自动附带用户的Cookie信息,因此这个请求会以用户的身份发送。
  3. 执行非本意操作:目标网站收到请求后,会认为该请求是用户的真实意图,从而执行相应的操作。例如,攻击者可以利用CSRF攻击来修改用户的账户信息、转账或删除数据。

示例代码分析

以下是一个简单的CSRF攻击示例代码,展示了如何通过HTML表单发送伪造请求:

<!-- 1.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>CSRF Attack</title>
</head>
<body>
    <iframe
        src="data:text/html;base64,"
        frameborder="0"
        style="display: none"
    ></iframe>
</body>
</html>
<!-- 2.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>CSRF Attack</title>
</head>
<body>
    <form action="https://www.baidu.com" method="post">
        <input type="text" name="to" value="11111" />
        <input type="text" name="money" value="1000000" />
    </form>

    <script>
        document.querySelector('form').submit();
    </script>
</body>
</html>

在上述代码中,攻击者通过一个隐藏的<iframe>或自动提交的表单,向目标网站发送伪造的POST请求。由于用户的浏览器会自动附带用户的Cookie信息,因此目标网站会认为该请求是用户的真实意图。

二、CSRF攻击的危害

CSRF攻击可能会对用户和网站造成严重的危害,包括但不限于:

  1. 账户信息泄露:攻击者可以利用CSRF攻击修改用户的账户信息,如密码、邮箱地址等。
  2. 资金损失:攻击者可以利用CSRF攻击进行非法转账,导致用户资金损失。
  3. 数据泄露:攻击者可以利用CSRF攻击删除或修改用户数据,导致数据泄露或丢失。
  4. 声誉受损:CSRF攻击可能会导致网站的声誉受损,用户对网站的信任度降低。

三、防御CSRF攻击的策略

为了有效防御CSRF攻击,可以采取以下几种策略:

1. 不使用Cookie

不使用Cookie进行身份验证,改用其他身份验证机制,如Token。这种方法可以有效防止CSRF攻击,但可能会对某些功能(如SSR)造成影响。

2. 使用SameSite属性

在Cookie中设置SameSite属性,可以限制Cookie的发送范围。SameSite属性有以下几种值:

  • Strict:Cookie仅在第一方上下文中发送,不会在跨站请求中发送。
  • Lax:Cookie在第一方上下文中发送,某些跨站请求中也会发送。
  • None:Cookie在所有上下文中发送,但需要设置Secure属性。

SameSite属性可以有效防止CSRF攻击,但其兼容性较差,可能会挡住一些合法请求。

3. 使用CSRF Token

在表单中添加一个隐藏的CSRF Token字段,并在服务器端进行验证。每次用户提交表单时,服务器都会验证CSRF Token是否有效。如果Token无效,服务器将拒绝该请求。

<form action="https://www.example.com" method="post">
    <input type="hidden" name="csrf_token" value="随机生成的Token" />
    <input type="text" name="to" value="11111" />
    <input type="text" name="money" value="1000000" />
</form>

4. 检查Referer字段

在服务器端检查HTTP请求的Referer字段,确保请求来自合法的来源。如果Referer字段为空或来自非法来源,服务器将拒绝该请求。

if (request.headers.referer !== 'https://www.example.com') {
    return response.status(403).send('CSRF Attack Detected');
}

🧙‍♂️闭包应用场景之--防抖和节流

前言

前面写过关于JavaScriptthis指向的基础文章,链接:juejin.cn/post/751796…

今天我们继续来讲讲闭包的应用场景之一:防抖和节流

防抖 debounce

 场景举例:

有一个搜索框,用户每打一个字就发请求去服务器查结果。这样效率太低了,而且对服务器压力大。

你希望的是:等用户输入完停顿一下再发请求,比如停顿500毫秒后再发。

那要如何实现?

我们可以写个函数,在每次按键的时候先取消上一次还没执行的请求,设定一个新的定时器。

这时候就需要一个变量来保存“上一次的定时器”,而这个变量不能轻易被外部干扰或重置 —— 闭包正好可以做到这一点

function debounce(func, delay) {
  let timer; // 这个变量会被闭包保留住
  return function(...args) {
    clearTimeout(timer); // 清除之前的定时器
    timer = setTimeout(() => {
      func.apply(this, args); // 执行目标函数
    }, delay);
  };
}

最后一次按键之后delay时间执行,前面的都会清除定时器。

闭包的作用是:让 timer 变量一直存在,不会被销毁,这样每次都能判断是否需要清除旧的定时器。

节流 throttle

场景举例:

想监听用户的滚动事件,当页面滚动到底部时加载更多内容。但滚动事件非常频繁,可能一秒触发几十次,没必要每次都处理。

我们希望的是:每隔一定时间只执行一次处理逻辑,比如每1000毫秒最多执行一次。

怎么实现?

可以用一个变量记录上次执行的时间,或者记录是否正在“冷却中”。如果还在冷却期,就不执行新的操作。

同样地,这个变量也需要一直保留下来,不能每次调用都重新初始化 —— 闭包再次派上用场

function throttle(func, delay) {
  let lastTime = 0; // 记录上一次执行的时间戳
  return function(...args) {
    const now = Date.now();
    if (now - lastTime >= delay) {
      func.apply(this, args); // 执行函数
      lastTime = now; // 更新时间
    }
  };
}

闭包的作用是:让 lastTime 一直保留在内存里,这样每次调用函数都能知道上次执行是什么时候。

总结

  • 闭包就像一个小房间,里面的东西不会随便被别人动。
  • 在防抖和节流中,我们用闭包来保存一些“状态”(如定时器、时间戳),这些状态不会被外部干扰,也不会随着函数调用结束就被清空
  • 所以闭包成了实现防抖和节流的完美工具!

如果还觉得有点抽象,可以想象成:

防抖就是:等你做完一件事之后,过一会儿才执行操作。如果你一直做,那就一直不执行。

就像你打字的时候,系统不是你每按一个键就搜索,而是等你停下来之后才开始搜索。

用户输入“hello”,每敲一个字母就去服务器查结果,效率很低。加上防抖后,用户输入完 hello 后停顿500毫秒,才去搜索一次。

防抖的核心思想是: “别急着执行,等你停了再说。”

节流就是:控制某个动作的执行频率。

比如你有一个按钮,每次点击都会播放音乐,但如果用户疯狂点100次,你不希望真的播放100次音乐。
你可以加个“节流器”,规定:每5秒钟只能播放一次音乐。

【基础篇】Promise初体验+案例分析(上)

一、前言:为什么会出现Promise?

Promise的重要性我认为没有必要多讲,概括起来说就是五个字:必!须!得!掌!握!

而且还要掌握透彻,在实际的使用中,有非常多的应用场景我们不能立即知道应该如何继续往下执行。

最常见的一个场景就是ajax请求,通俗来说,由于网速的不同,可能你得到返回值的时间也是不同的,这个时候我们就需要等待,结果出来了之后才知道怎么样继续下去。

let xhr = new XMLHttpRequest();
xhr.open('get', 'https://v0.yiketianqi.com/api?unescape=1&version=v61&appid=82294778&appsecret=4PKVFula&city=%E5%8C%97%E4%BA%AC');
xhr.send();
xhr.onreadystatechange = function () {
    if (xhr.readyState === 4) {
        if (xhr.status >= 200 && xhr.status < 300) {
            console.log(xhr.responseText)
        }
    }
}

在ajax的原生实现中,利用了onreadystatechange事件,当该事件触发并且符合一定条件时,才能拿到想要的数据,之后才能开始处理数据,这样做看上去并没有什么麻烦,但如果这个时候,我们还需要另外一个ajax请求,这个新ajax请求的其中一个参数,得从上一个ajax请求中获取,这个时候我们就不得不等待上一个接口请求完成之后,再请求后一个接口。

let xhr = new XMLHttpRequest();
xhr.open('get', 'https://v0.yiketianqi.com/api?unescape=1&version=v61&appid=82294778&appsecret=4PKVFula&city=%E5%8C%97%E4%BA%AC');
xhr.send();
xhr.onreadystatechange = function () {
    if (xhr.readyState === 4) {
        if (xhr.status >= 200 && xhr.status < 300) {
            console.log(xhr.responseText)
            
            //伪代码....
            let xhr = new XMLHttpRequest();
            xhr.open('get','http://www.xx.com?a'+xhr.responseText);
            xhr.send();
            xhr.onreadystatechange = function(){
                if(xhr.readyState === 4){
                    if(xhr.status>=200 && xhr.status<300){
                        console.log(xhr.responseText)
                        
                    }
                }
            }
        }
    }
}

当出现第三个ajax(甚至更多)仍然依赖上一个请求时,我们的代码就变成了一场灾难。

这场灾难,往往也被称为回调地狱。因此我们需要一个叫做Promise的东西,来解决这个问题,当然,除了回调地狱之外,还有个非常重要的需求就是

为了代码更加具有可读性和可维护性,我们需要将数据请求与数据处理明确的区分开来

上面的写法,是完全没有区分开,当数据变得复杂时,也许我们自己都无法轻松维护自己的代码了。这也是模块化过程中,必须要掌握的一个重要技能,请一定重视。

二、Promise是什么?

Promise是异步编程的一种解决方案,比传统的解决方案回调函数更合理、更强大。

ES6将其写进了语言标准,统一了用法,原生提供了Promise对象。

指定回调函数的方式也变得更加灵活易懂,也解决了异步回调地狱的问题

旧方案是单纯使用回调函数,常见的异步操作有:定时器、fs模块、ajax、数据库操作

从语法上说,Promise是一个构造函数;

从功能上说,Promise对象用来封装一个异步操作并可以获取其成功/失败的结果值。

2.1 Promise的初体验

创建promise对象(pending状态)

const p = new Promise(executor);

其中:

executor函数: 执行器 (resolve, reject) => {}

resolve函数: 内部定义成功时我们调用的函数 value => {}

reject函数: 内部定义失败时我们调用的函数 reason => {}

executor会在Promise内部立即同步调用,异步操作在执行器中执行

实例对象调用Promise原型中的then方法来完成对结果的处理

<script>
    const p = new Promise((resolve, reject) => {
        //如果咱们公司今年挣钱了,年底就发奖金,否则不发
        resolve('ok');
    })
    console.log(p)
    p.then(() => {
        console.log('发奖金')
    }, () => {
        console.log('不发奖金')
    })
</script>

三、使用Promise的好处?

3.1 指定回调函数的方式更加灵活

  1. 旧的:必须在启动异步任务前指定
  1. promise:启动异步任务->返回promise对象->给promise对象绑定回调函数(甚至可以在异步任务结束后指定/多个)

3.2 可以解决回调地狱问题,支持链式调用

  1. 什么是回调地狱?回调函数嵌套调用,外部回调函数异步执行的结果是嵌套的回调执行的条件
  1. 回调地狱的缺点?不便于阅读不便于异常处理
  1. 解决方案?promise链式调用
  1. 终极解决方案?async/await

四、Promise实例对象的两个属性

  • PromiseState此属性为promise对象的状态属性。

【注】状态只能由pending->fulfilled 或者是 pending->rejected

    • fulfilled:成功的状态
    • rejected:失败的状态
    • pending:初始化的状态
  • PromiseResult此属性为promise对象的结果值(resolve以及reject函数的形参值)

五、resolve函数以及reject函数

  • resolve:修改promise对象的状态,由pending修改到fulfilled;将实参设置到这个属性PromiseResult中。
  • reject:修改promise对象的状态,由pending修改到rejected;将实参设置到这个属性PromiseResult中。

案例1:利用promise来进行读取文件操作

//1.普通文件读取方式
const fs = require('fs');

//2.直接利用readfile来进行读取
/* fs.readFile(__dirname + '/data.txt',(err,data)=>{
    if(err) throw err;
    console.log(data.toString());
}) */

//3.利用promise来实现文件的读取
const p = new Promise((resolve, reject) => {
    fs.readFile(__dirname + '/data.txt', (err, data) => {
        if (err) {
            reject(err);
        }else{
            resolve(data);
        }
    })
}); 

p.then(value=>{
    console.log(value.toString());
},reason=>{
    console.log(reason);
})

案例2:利用promise进行ajax请求

<body>
    <button>发送ajax请求</button>

    <script>
        //1.获取DOM元素对象
        let btn = document.querySelector('button');
        //2.绑定事件
        btn.onclick = function(){
            //3.创建promise实例对象
            const p = new Promise((resolve,reject)=>{
                //4.创建ajax实例对象
                const xhr = new XMLHttpRequest();
                //5.打开请求
                xhr.open('get','https://www.yiketianqi.com/free/day?appid=82294778&appsecret=4PKVFula&unescape=1');
                //6.发送请求
                xhr.send();
                //7.利用onreadystatechange事件
                xhr.onreadystatechange = function(){
                    //8.判断
                    if(xhr.readyState == 4){
                        if(xhr.status == 200){
                            resolve(xhr.responseText);
                        }else{
                            reject(xhr.response);
                        }
                    }
                }
            });
            p.then(value=>{
                console.log(JSON.parse(value));
            },reason=>{
                console.log('获取信息失败');
            })
        }
    </script>

</body>

案例3:利用promise进行数据库操作

const mongoose = require('mongoose');

new Promise((resolve, reject) => {
    mongoose.connect('mongodb://127.0.0.1/project');
    mongoose.connection.on('open', ()=>{
        //连接成功的情况
        resolve();
    });

    mongoose.connection.on('error', () => {
        //连接失败的情况
        reject();
    })
}).then(value => {
    //创建结构
    const NoteSchema = new mongoose.Schema({
        title: String,
        content: String
    })
    //创建模型
    const NoteModel = mongoose.model('notes', NoteSchema);

    //读取操作
    NoteModel.find().then(value => {
        console.log(value);
    }, reason => {
        console.log(reason);
    })
}, reason => {
    console.log('连接失败');
})

案例4:封装一个函数,作用是读取文件

const fs = require('fs');

function ReadFileFun(path){
    return new Promise((resolve,reject)=>{
         fs.readFile(path,(err,data)=>{
              //判断
              if(err){
                    reject(err)
              }else{
                    resolve(data);
              }
         })
    });
}

ReadFileFun('./data.txt').then(value=>{
    console.log(value.toString());
},reason=>{
    console.log(reason);
})

node中的promisify

  • promisify (只能在 NodeJS 环境中使用)
  • promisify 是 util 模块中的一个方法 util 是 nodeJS 的内置模块
  • 作用: 返回一个新的函数, 函数的是 promise 风格的.
const util = require('util');
const fs = require('fs');
//通过 fs.readFile 创建一个新的函数
const mineReadFile = util.promisify(fs.readFile);

mineReadFile('./resource/2.html')
.then(value => {
    console.log(value.toString());
}, reason => {
    console.log(reason);
})

六、Promise对象的状态

Promise对象通过自身的状态来控制异步操作,Promise实例具有三种状态.

  • 异步操作未完成:pending
  • 异步操作成功:fulfilled
  • 异步操作失败:rejected

这三种的状态的变化途径只有两种

  • 从pending(未完成)到fulfilled(成功)
  • 从pending(未成功)到rejected(失败)

一旦状态发生变化,就凝固了,不会再有新的状态变化,这也是Promise这个名字的由来,它的英语意思"承诺",

一旦承诺生效,就不得再改变了,这也意味着Promise实例的状态变化只可能发生一次。

在Promise对象的构造函数中,将一个函数作为第一个参数。而这个函数,就是用来处理Promise的状态变化。

上面的resolve和reject都为一个函数,他们的作用分别是将状态修改为resolved和rejected。

因此,Promise的最终结果只有两种。

异步操作成功,Promise实例传回一个值(value),状态变为fulfilled.
异步操作失败,Promise实例抛出一个错误(error),状态变为rejected

七、Promise的then方法

then:指定用于得到成功value的成功回调和用于得到失败reason的失败回调,返回一个新的promise对象

  • 成功的状态:执行第一个回调函数
  • 失败的状态:执行第二个回调函数

promise.then()返回的新promise的结果状态由什么决定?

(1) 简单表达: 由then()指定的回调函数执行的结果决定

(2) 详细表达:

① 如果抛出异常, 新promise变为rejected, reason为抛出的异常

const p = new Promise((resolve,reject)=>{
     resolve('ok');
});

let result = p.then(value=>{
    throw '错误';
},reason=>{
    console.log(reason);
});

console.log(result);

② 如果返回的是非promise的任意值, 新promise变为fulfilled, PromiseResult为返回的值

const p = new Promise((resolve,reject)=>{
                resolve('ok');
});

let result = p.then(value=>{
    return 100;
},reason=>{
    console.log(reason);
});

console.log(result);

③ 如果返回的是另一个新promise, 此promise的结果就会成为新promise的结果

const p = new Promise((resolve,reject)=>{
                resolve('ok');
});

let result = p.then(value=>{
    return new Promise((resolve,reject)=>{
        //resolve('111');
        reject('error');
    })
},reason=>{
    console.log(reason);
});

console.log(result);

八、Promise的链式调用

const p = new Promise((resolve,reject)=>{
    //resolve('ok');
    reject('error');
});

p.then(value=>{
    console.log(value);
},reason=>{
    console.log(reason);
}).then(value=>{
    console.log(value);
},reason=>{
    console.log(reason);
})

案例:通过promise的链式调用来读取文件

回调地狱的方式:

const fs = require('fs');
fs.readFile('./resource/1.html',(err,data1)=>{
    if(err) throw err;
    fs.readFile('./resource/1.html',(err,data2)=>{
        if(err) throw err;
        fs.readFile('./resource/1.html',(err,data3)=>{
            if(err) throw err;
            console.log(data1 + data2 + data3);
        })
    })
})

Promise的形式:

需求:读取resource下三个文件内容,并在控制台合并输出

new Promise((resolve,reject)=>{
    fs.readFile('./resource/1.html',(err,data)=>{
         //如果失败 则修改promise对象状态为失败
        if(err) reject(err);
        //如果成功 则修改promise对象状态为成功
        resolve(data);
    })
}).then(value=>{
    return new Promise((resolve,reject)=>{
        fs.readFile('./resource/2.html',(err,data)=>{
             //失败
            if(err) reject(err);
            //成功
            resolve([value,data]);
        })
    })
}).then(value=>{
    return new Promise((resolve,reject)=>{
        fs.readFile('./resource/3.html',(err,data)=>{
             //失败
            if(err) reject(err);
            value.push(data);
            //成功
            resolve(value);
        })
    })
}).then(value=>{
    console.log(value.join(""));
})

来源:b站尚硅谷Promise

下期讲解promise下的的八种方法

Vue 3 响应式黑魔法:ITERATE_KEY 如何解决新增属性的响应性难题

Vue 3 响应式黑魔法:ITERATE_KEY 如何解决新增属性的响应性难题

一位失业开发者的Vue响应式探索之旅:从面试困境到源码顿悟

前言:失业后的Vue面试困境

2025年6月,我因项目裁撤不幸失业。在随后的求职过程中,我遇到了一家996且不交公积金的公司,做了一周后果断跑路。作为Vue和uniapp技术栈的开发者,我惊讶地发现80%的面试官都会问同一个问题:"Vue 2.0 和 3.0 有什么区别?做了哪些改进?"

最初我的回答很浅显:

1. Vue 3 用了 Proxy 替代 Object.defineProperty
2. 新增了 Composition API

这个回答在大多数面试中已经足够,很多面试官听到"Proxy"就会点头认可。直到我翻开霍春阳的《Vue.js设计与实现》,才真正踏入响应式系统的神秘世界。我了解到:

  • Proxy 如何代理 target 对象
  • 用 Set 集合存放单个副作用函数
  • 用 Map 构建函数桶管理副作用
  • WeakMap 存储依赖关系
  • ref 如何解决基本类型的响应式问题

我以为这就是全部,直到今天读到关于 ITERATE_KEY 的章节,才震惊地发现:原来不是 Proxy 本身解决了 Vue 2 的新增属性问题,而是这个小小的 ITERATE_KEY 在幕后发挥着关键作用!

一、Proxy 的响应式基础与局限

Vue 3 使用 Proxy 实现响应式系统的核心拦截:

function reactive(obj) {
  return new Proxy(obj, {
    get(target, key, receiver) {
      track(target, key) // 依赖收集
      return Reflect.get(target, key, receiver)
    },
    set(target, key, value, receiver) {
      const result = Reflect.set(target, key, value, receiver)
      trigger(target, key) // 触发更新
      return result
    },
    // ... 其他陷阱
  })
}

在大多数面试中,回答"Vue 3 使用 Proxy 替代 defineProperty"已经足够展示基础理解。但当我深入源码后,发现了一个关键认知偏差:

面试中的常见误区

很多开发者(包括面试官)认为:

graph LR
    A[Proxy] --> B[自动解决新增属性响应性]

但实际机制是:

flowchart LR
    Proxy拦截操作 --> Input[识别操作类型]
    Input --> ConditionA{是ADD/DELETE?}
    ConditionA -- 是 --> Action1[触发ITERATE_KEY依赖]
    ConditionA -- 不是 --> Action2[触发常规依赖]

面试官可能深挖的陷阱

const state = reactive({ count: 0 })

// 问题1:新增属性
state.newProp = "test" // 为什么能触发更新?

// 问题2:数组操作
const arr = reactive([1, 2])
arr[2] = 3 // 为什么能触发更新?

表面答案:因为用了 Proxy
深度答案:Proxy 提供了拦截能力,但真正实现响应性的是 ITERATE_KEY 的依赖追踪机制

二、ITERATE_KEY:响应式系统的无名英雄

1. ITERATE_KEY 的诞生背景

在阅读《Vue.js设计与实现》的过程中,我发现了这个关键代码:

// 这只是书的一些代码思路
const ITERATE_KEY = Symbol()

const p = new Proxy(obj, {
    ownKeys(target) {
        // 将副作用函数与 ITERATE_KEY 关联
        track(target, ITERATE_KEY)
        return Reflect.ownKeys(target)
    }
})

// vue源码
// vue-next/packages/reactivity/src/effect.ts
const ITERATE_KEY = Symbol(__DEV__ ? 'iterate' : '')

这个看似简单的 Symbol,正是解决新增属性响应性的核心所在。

2. 数据结构关系图(面试加分项)

flowchart TD
    bucket[WeakMap] -->|target| depsMap[Map]
    depsMap -->|常规key| dep[Set]
    depsMap -->|ITERATE_KEY| iterateDep[Set]
    dep --> effect1[属性副作用]
    iterateDep --> effect2[结构副作用]
    effect2 --> forin[for...in循环]
    effect2 --> objectKeys[Object.keys]
    effect2 --> arrayLength[数组length]

3. 核心实现原理

依赖触发阶段的关键逻辑(trigger 函数)

function trigger(target, key, type) {
  // ...
  
  // 关键点:结构变化触发ITERATE_KEY依赖
  if (
    type === TriggerOpTypes.ADD ||
    type === TriggerOpTypes.DELETE ||
    (type === TriggerOpTypes.SET && isArray(target))
  ) {
    const iterateEffects = depsMap.get(ITERATE_KEY)
    if (iterateEffects) {
      effectsToRun.push(...iterateEffects)
    }
  }
}

三、为什么 ITERATE_KEY 是面试的加分项

1. 超越表面的理解

当其他候选人还在说"因为用了Proxy"时,你可以展示

// 简化的响应式核心
const ITERATE_KEY = Symbol('iterate')

function ownKeys(target) {
  track(target, ITERATE_KEY) // 关键行!
  return Reflect.ownKeys(target)
}

2. 面试实战案例

当被问到"Vue 3 如何解决新增属性响应性"时:

基础回答:

"Vue 3 使用 Proxy 替代 Object.defineProperty,Proxy 可以拦截属性添加操作"

进阶回答:

"除了 Proxy 的基础拦截,Vue 通过 ITERATE_KEY 机制追踪对象结构变化:

  1. 在 for...in/Object.keys 等操作时收集 ITERATE_KEY 依赖
  2. 添加/删除属性时触发这些依赖
  3. 这样无需特殊 API 就能保持响应性"

四、从失业到源码理解的成长

这段失业经历虽然艰难,却迫使我深入 Vue 3 的源码世界。现在面对"Vue 2 和 3 区别"这个问题,我能提供三层回答:

理解层次 回答内容 适合场景
基础层 Proxy 替代 defineProperty Composition API 普通面试
进阶层 响应式系统架构 WeakMap→Map→Set 依赖存储 技术Leader面试
深度层 ITERATE_KEY 解决结构变化 ref 处理基本类型 框架开发岗位

特别提醒:当面试官深入追问时,可以提到:

  • 数组 length 修改的特殊处理
  • Map/Set 的结构变化追踪
  • Symbol 作为 ITERATE_KEY 的优势

五、简易实现(面试手写参考)

// 面试可手写的核心代码
const ITERATE_KEY = Symbol()

function reactive(obj) {
  return new Proxy(obj, {
    ownKeys(target) {
      track(target, ITERATE_KEY)
      return Reflect.ownKeys(target)
    },
    set(target, key, value, receiver) {
      const type = key in target ? 'SET' : 'ADD'
      const result = Reflect.set(target, key, value, receiver)
      trigger(target, key, type)
      return result
    },
    deleteProperty(target, key) {
      const hadKey = key in target
      const result = Reflect.deleteProperty(target, key)
      if (hadKey) trigger(target, key, 'DELETE')
      return result
    }
  })
}

结语:技术深度的价值

在失业后的面试中,我发现:

  1. 普通公司问:"知道 Proxy 吗?" → 答出基础即可
  2. 优秀团队问:"为什么 Proxy 能解决新增属性?" → 需要理解 ITERATE_KEY
  3. 顶尖团队问:"请手写简易响应式系统" → 能实现核心逻辑

真正的技术竞争力不在于背诵 API,而在于理解设计思想。当我开始讲解 ITERATE_KEY 时,看到面试官眼睛亮了起来 - 这比任何996 offer都更有价值。

附:学习资源助力面试突围

  1. 《Vue.js设计与实现》 - 霍春阳
  2. Vue 3 响应式源码分析
  3. MDN Proxy 文档

🧠【彻底读懂 reduce】acc 是谁?我是谁?我们要干嘛?

🚧 开场:把你困住的误解先拆了

你是不是也以为:

“acc 是数组的每一项吗?是不是就相当于数组里的那个元素?”

答案是——不是!不是!真的不是!

让我们用这篇文章,从「误解」出发,到「掌握精髓」收官,完成一次技术脑内革命!


🏷️ 一、reduce 是什么?——数组界的「炼丹炉」

你可以把 reduce 理解为一口循环不断的炼丹炉

🌰 经典语法结构:

arr.reduce((acc, cur, index, array) => {
  return 新的 acc;
}, 初始值);
  • acc(累计器):每一次处理的中间产物,也就是“丹药”
  • cur:数组当前项,是“投进去的新材料”
  • return 的值会变成下一次的 acc
  • 初始值:就是最开始丢进去的那一勺清水/引子

☕ 巧记口诀:

初始入炉为丹引,
每步炼化靠 acc,
一路处理到尾项,
千形万象终归一!


🤯 二、那 acc 到底是什么?

很多人以为 acc 是数组每个值,但 它其实是你自己 return 出来的中间结果

看个例子就明白了:

[1, 2, 3].reduce((acc, cur) => {
  console.log('acc:', acc, 'cur:', cur);
  return acc + cur;
}, 0);

👉 输出如下:

acc: 0 cur: 1
acc: 1 cur: 2
acc: 3 cur: 3

最后返回 6!

你看见了吗?

  • acc 初始是 0
  • 第一轮:0 + 1 = 1,传给下一轮
  • 第二轮:1 + 2 = 3
  • 第三轮:3 + 3 = 6

acc 是你一手打造、一手维护、一手传承的“结果容器”。


📌 终极记忆法:

acc 是你“自己做出来”的,cur 是数组“送过来的”。


🧪 三、reduce 常见实战用法(带巧计)

1️⃣ 数组求和(基础)

[1, 2, 3, 4].reduce((acc, cur) => acc + cur, 0);

💡 巧计:acc 是你口袋的余额,cur 是你捡到的零钱。


2️⃣ 统计频次(中级)

['🍎', '🍌', '🍎'].reduce((acc, cur) => {
  acc[cur] = (acc[cur] || 0) + 1;
  return acc;
}, {});

💡 巧计:acc 是记账本,cur 是进店顾客,来一位就打正字。


3️⃣ 按类型分组(高级)

const list = [
  { name: '小明', group: 'A' },
  { name: '小红', group: 'B' },
  { name: '小刚', group: 'A' },
];
const grouped = list.reduce((acc, cur) => {
  (acc[cur.group] ||= []).push(cur);
  return acc;
}, {});

💡 巧计:acc 是教室,cur 是学生,按班级安排座位。


4️⃣ 扁平化数组(进阶)

[[1, 2], [3, 4], [5]].reduce((acc, cur) => acc.concat(cur), []);

💡 巧计:acc 是铺平的地面,cur 是被压扁的山丘。


5️⃣ 实现 map 和 filter(黑魔法)

// map
arr.reduce((acc, cur) => {
  acc.push(cur * 2);
  return acc;
}, []);

// filter
arr.reduce((acc, cur) => {
  if (cur % 2 === 0) acc.push(cur);
  return acc;
}, []);

💡 巧计:

map 是换头术,filter 是拣人术,reduce 是百变术!


⚔️ 四、和其他数组方法比个高低

函数 功能 返回值类型 可中断 初始值 灵活性
forEach 只执行遍历 无(undefined) 🚫
map 一对一映射 数组
filter 条件筛选 数组
some/every 条件判断 Boolean 🚫
reduce 多合一转化 任意类型 ✅✅✅

🎯 结论:reduce 是 JS 数组操作界的「全能选手」,其他方法做不到的,它都能!


🧬 五、底层实现你也能写!

模拟原生 reduce 的核心逻辑 👇

Array.prototype.myReduce = function (cb, initialValue) {
  let acc = initialValue;
  let i = 0;
  const arr = this;

  if (acc === undefined) {
    if (!arr.length) throw new TypeError('空数组没初始值');
    acc = arr[0];
    i = 1;
  }

  for (; i < arr.length; i++) {
    acc = cb(acc, arr[i], i, arr);
  }

  return acc;
};

💣 小心炸点:

[].reduce((a, b) => a + b); // ❌ 报错
[].reduce((a, b) => a + b, 0); // ✅ 返回 0

🔮 六、哲学升华 & 函数式世界里的 reduce

reduce 就是一条“函数管道”,把每一步处理的结果传给下一步,直到最终收束。

const compose = (...fns) => x => fns.reduceRight((acc, fn) => fn(acc), x);

在 redux 中间件、koa 洋葱模型、Vue 插件链……都能看到 reduce 的影子。

🌌 它是一种思维方式:组合、累积、转化、递进。


🧨 七、终极巧记总结

🥋 再念一遍口诀:

reduce 是个炼丹炉,
入料为你定起初;
acc 是丹,cur 是火,
一路熬来百变出。


🎁 彩蛋题:flatten 多维数组

const flatten = arr => arr.reduce((acc, cur) => {
  return acc.concat(Array.isArray(cur) ? flatten(cur) : cur);
}, []);

✅ 你现在应该已经掌握了:

  • ✅ acc 的本质(不是数组项,是 return 出来的中间结果)
  • ✅ reduce 的运行机制
  • ✅ 各种实战场景及巧妙类比
  • ✅ 和其他方法的核心区别
  • ✅ 自己实现 reduce
  • ✅ reduce 在函数式中的地位

👇 最后,如果你还记得最初的问题:

“acc 是数组里的每一项吗?”

你应该可以坚定地回答:

不是!acc 是我 return 出来的结果,是我的魂,是我的锤,是我整个计算的载体!

深入理解 useLayoutEffect:解决 UI "闪烁"问题的利器

前言

在 React 开发中,我们经常需要处理副作用(side effects)。React 提供了两个主要的 Hook 来处理副作用:useEffect 和 useLayoutEffect。虽然它们看起来很相似,但在使用时机和使用场景上有着重要区别。本文将重点介绍 useLayoutEffect,探讨它的工作原理、使用场景以及如何利用它解决常见的 UI 问题。

image.png

useEffect 回顾

在深入 useLayoutEffect 之前,让我们先快速回顾一下 useEffect

useEffect(() => {
  // 副作用代码
  return () => {
    // 清理函数
  };
}, [dependencies]);

可以看到,useEffect 的特点:

  • 在组件渲染完成后异步执行
  • 不会阻塞浏览器的绘制(paint)
  • 适用于大多数副作用场景(数据获取、订阅等)

useLayoutEffect 详解

API的用法

useLayoutEffect 的 API的用法如下:

useLayoutEffect(() => {
  // 副作用代码
  return () => {
    // 清理函数
  };
}, [dependencies]);

你会发现,其API的使用几乎和useEffect是完全相同的

执行阶段与特性

对于useLayoutEffect理解其执行阶段和特性是最重要的

  1. 执行阶段

    • 在 DOM 更新之后
    • 在浏览器计算布局(Layout)之后
    • 在浏览器实际绘制(Paint)屏幕之前
  2. 特性

    • 同步执行,会阻塞浏览器的绘制
    • 适合需要同步读取或修改 DOM 的场景
    • 可以避免视觉上的"闪烁"或布局跳动

React的生命周期

理解 React 组件的生命周期有助于我们更好地把握这两个 Hook 的区别:

React的生命周期:

  1. 组件渲染(Render) - React 计算虚拟 DOM 变化
  2. DOM 更新 - React 将变化应用到真实 DOM
  3. 浏览器计算布局(Layout) - 浏览器计算元素的新位置和尺寸
  4. useLayoutEffect 执行 ← 在这里!
  5. 浏览器绘制(Paint) - 实际将像素绘制到屏幕
  6. useEffect 执行

典型使用场景

1. 避免布局"闪烁"

什么是布局闪烁?

布局闪烁是指当用户界面的元素在渲染后突然改变位置或尺寸,导致用户看到明显的跳动或闪烁效果。这种现象通常发生在 React 组件需要根据 DOM 元素的尺寸或位置进行动态调整时。

发生布局闪烁的原因是当我们使用传统的useEffect时,元素变化是在渲染之后进行的

看这个例子:

function App(){
  const [content,setContent] = useState("11111111111111111111111111111111111111")
  
  //使用useEffect时:
  useEffect(() => {
    setContent('2222222222222222222222222222222222')
  },[])
  
  return (
    <div>{content}</div>
  )
}

当使用useEffect时:

  1. 组件首次渲染,content 初始值为 "1111..."(长字符串1)
  2. React 将 <div>1111...</div> 提交给浏览器绘制
  3. 用户短暂看到 "1111..." 的内容
  4. useEffect 回调执行,触发 setContent('2222...')
  5. 组件重新渲染,显示 <div>2222...</div>
  6. 用户看到内容从 "1111..." 变为 "2222..."(闪动过程)

现代浏览器和设备很高级,导致闪动的速度很快,你可能观察不到这个效果,但是在一些早期性能较低的设备中,对于页面中的一大堆内容,闪烁会给用户带来不好的用户体验

所以我们需要使用 useLayoutEffect ,这样做可以确保这些调整在用户看到屏幕之前完成,因此优化闪烁效果

function App(){
  const [content,setContent] = useState("11111111111111111111111111111111111111")
  
  //使用useLayoutEffect时:
  useLayoutEffect(() => {
    setContent('2222222222222222222222222222222222')
  },[])
  
  return (
    <div>{content}</div>
  )
}

这里的过程是这样的:

  1. 组件首次渲染,content 初始值为 "1111..."
  2. 在浏览器绘制前,useLayoutEffect 回调执行,设置 content 为 "2222..."
  3. 组件立即重新渲染,准备显示 <div>2222...</div>
  4. 浏览器绘制最终结果
  5. 用户直接看到 "2222...",没有看到中间状态

这种过程你可以理解为useLayoutEffect是和渲染同步进行的,因此就不会出现闪烁效果!


2. 同步获取 DOM 属性

useLayoutEffect还有一种常见的应用场景就是,当需要同步读取 DOM 并立即基于读取的值进行更新时:

function AutoHeightTextarea() {
  const textareaRef = useRef(null);
  const [height, setHeight] = useState('auto');
  
  useLayoutEffect(() => {
    // 同步获取滚动高度并设置
    setHeight(`${textareaRef.current.scrollHeight}px`);
  }, [value]);

  return (
    <textarea
      ref={textareaRef}
      style={{ height }}
      value={value}
      onChange={handleChange}
    />
  );
}

在这个例子中,为什么必须使用 useLayoutEffect?

  1. 同步性需求

    • 我们需要在浏览器绘制前确保文本框高度已经调整
    • 任何延迟都会导致用户看到文本框高度变化的"跳跃"效果
  2. DOM 测量与更新的原子性

image.png


性能考虑

由于 useLayoutEffect 会阻塞浏览器绘制,不当使用可能导致性能问题。遵循以下准则:

  1. 仅在必要时使用 - 大多数情况下,useEffect 是更好的选择
  2. 保持轻量 - 在 useLayoutEffect 中执行的操作应该尽可能快速
  3. 避免频繁触发 - 谨慎设置依赖项,避免不必要的执行


两者区别总结

特性 useEffect useLayoutEffect
执行时机 异步,不阻塞绘制 同步,阻塞绘制
适用场景 大多数副作用 DOM 测量和同步更新
服务端渲染 正常工作 会触发警告
性能影响 较小 可能较大(如果滥用)

何时使用 useLayoutEffect

  • 需要同步读取或修改 DOM 布局时
  • 需要避免视觉上的"闪烁"或布局跳动时
  • 在动画初始化或复杂交互场景中

总结

现在我相信你已经能够入门useLayoutEffect的基本概念、使用方法与应用场景了,如果想要了解更多的关于useLayoutEffect的详细知识,推荐可以去看React的官方文档

很全面的前端面试——CSS篇(上)

前言

CSS的面试题可谓是鱼龙混杂,在整理这一类的面试题时,也是觉得写之不尽,越写越多,原本是打算用一篇文章去写尽CSS的面试题,现在看来是我想多了,在文章字符数达到10w的时候被提示超出最大字符限制了,那么就将CSS篇暂时分为上下篇吧

文章里的一些实例也因为篇幅原因无法展示,也许以后会补上?不知道

算了,多说无益,让我们开始吧

鹦鹉兄弟摇饮料.gif

一、介绍一下CSS选择器及其优先级

CSS(层叠样式表)是网页设计的核心语言之一,而选择器则是CSS的基石CSS 选择器用于定位 HTML 元素,从而为其应用样式规则。

CSS选择器基础:认识选择器

CSS选择器的作用是"选择"HTML元素并为其应用样式。就像在人群中找人一样,我们需要不同的"识别方式"来找到特定的元素。

  • CSS 定义:层叠样式表,用于选择 HTML DOM 元素并应用样式规则。

  • 引入方式

    • 内联标签(<style> :在 HTML 文件的 <head> 标签内使用 <style> 标签编写 CSS 代码。
    • 外联样式(<link> :通过 <link> 标签引入外部 CSS 文件。
    • 行内样式:直接在 HTML 元素的 style 属性中编写 CSS 代码。
  • 渲染流程:先下载样式,再解析 DOM 并应用样式,DOM 与 CSS 结合形成渲染树(render tree),最后通过浏览器渲染引擎渲染得到页面。

渲染树示意图

image.png

选择器类型一览

选择器类型 格式示例 优先级权重
id 选择器 #id 100
类选择器 .classname 10
属性选择器 a[ref="eee"] 10
伪类选择器 li:last-child 10
标签选择器 div 1
伪元素选择器 li:after 1
相邻兄弟选择器 h1+p 0
子选择器 ul>li 0
后代选择器 li a 0
通配符选择器 * 0

1. 基本选择器类型

标签选择器 - 通过HTML标签名选择元素:

p {
  color: blue;
}

这会选择页面中所有的<p>段落元素,并将文字颜色设为蓝色。

类选择器 - 通过class属性选择元素:

.highlight {
  background-color: yellow;
}

在HTML中使用:<p class="highlight">这段文字会高亮</p>

ID选择器 - 通过id属性选择唯一元素:

#header {
  font-size: 24px;
}

在HTML中使用:<div id="header">网站标题</div>

二、CSS选择器优先级:谁说了算?

当多个样式规则作用于同一个元素时,浏览器如何决定应用哪个样式呢?这就涉及到优先级的概念。

优先级权重计算规则:

  • 标签选择器:1
  • 类选择器:10
  • ID选择器:100
  • 行内样式:1000
  • !important:最高优先级

让我们看一个实际例子:

<div class="container" id="main">
  <P style="color: pink;">我看看怎么个事!</P>
</div>
p {
  color: blue !important;
}
.container p {
  color: red;
}
#main p {
  color: green;
}

虽然行内样式权重最高(1000),但!important具有最高优先级,所以最终文字显示蓝色。如果没有!important,则行内样式的粉色会生效。

实例与展示

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>CSS</title>
    <style>
        /* 1(样式) */
        /* 如果一定要显示蓝色 */
        /* !important 很重要 */
        p {
            color: blue !important;
        }
        /* 10(类) + 1(样式) */
        .container p {
            color: red;
        }
        /* 100(id) + 1(样式) */
        #main p {
            color: green;
        }

    </style>
</head>
<body>
    <div class="container" id="main">
        <P style="color: pink;/* 1000 */">我看看怎么个事!</P>
    </div>
</body>
</html>

image.png

三、组合选择器:精准定位元素

1. 后代选择器(空格)

选择某个元素内部的所有特定后代元素:

.container p {
  text-decoration: underline;
}

这会选择.container内部的所有<p>元素,无论嵌套多深。

2. 子元素选择器(>)

只选择直接子元素:

.container > p {
  font-weight: bold;
}

这只会选择.container直接子元素中的<p>,不会选择嵌套在其他元素中的<p>

3. 相邻兄弟选择器(+)

选择紧接在某个元素后的第一个兄弟元素:

h1 + p {
  color: red;
}

这会让紧跟在<h1>后的第一个<p>变为红色。

4. 通用兄弟选择器(~)

选择某个元素后面的所有同级元素:

h1 ~ p {
  color: blue;
}

这会让<h1>后面的所有<p>兄弟元素变为蓝色。

实例与展示

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>选择器</title>
    <style>
        /* 相邻兄弟选择器 */
        /* 得分为2 */
        h1+p{
            color: red;
        }
        /* 通用兄弟选择器 */
        /* 得分为2 */
        h1~p{
            color: blue; 
        }
        /* 子元素选择器 直接子元素*/
        /* 用于选择.container元素内的段落文本 */
        .container >p{
                font-weight: bold;
        }
        .container p{       
            text-decoration: underline;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>标题</h1>
        <p>这是第一段文字</p>
        <p>这是第二段文字</p>
        <a href="">链接</a>
        <span>这是一个span元素</span>
        <div class="inner">
            <p>这是一个内部段落</p>
        </div>
    </div>
</body>
</html>

image.png

四、伪类选择器:元素的状态选择

伪类选择器允许我们根据元素的状态或位置来应用样式。

1. 交互状态伪类

/* 鼠标悬停时 */
p:hover {
  background-color: yellow;
}

/* 按钮被点击时 */
button:active {
  background-color: red;
  color: white;
}

/* 输入框获得焦点时 */
input:focus {
  border: 2px solid blue;
}

2. 结构伪类

/* 选择奇数位置的列表项 */
li:nth-child(odd) {
  background-color: lightgray;
}

/* 选择除最后一个外的所有子元素 */
li:not(:last-child) {
  margin-bottom: 10px;
}

3. 表单相关伪类

/* 复选框被选中时改变相邻标签颜色 */
input:checked + label {
  color: blue;
}

实例与展示

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        /* 伪类选择器 */
        button:active{
            background-color: red; 
            color: white;
        }
        p:hover{
            background-color: yellow;
        }
        /* 鼠标选中文本的效果 */
        ::selection{
            background-color: blue;
            color: white;
        }
        /* 输入框的效果 */
        input:focus{
            border: 2px solid blue;
            outline: none; /* 移除浏览器默认outline */
            accent-color: blue;  /* 设置复选框选中标记颜色 */
        }
         /* 复选框的效果 */
        input:checked + label{
            color: blue;
        } 
        li:nth-child(odd){
            background-color: lightgray;
        }
        li:not(:last-child){
            margin-bottom: 10px;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>伪类选择器示例</h1>
        <button>点击我</button>
        <p>鼠标悬浮在这里</p>
        <input type="text" placeholder="输入框">
        <input type="checkbox" id="option1">
        <label for="option1">选项1</label>
        <input type="checkbox" id="option2" checked>
        <label for="option2">选项2</label>
        <ul>
            <li>列表项1</li>
            <li>列表项2</li>
            <li>列表项3</li>
            <li>列表项4</li>
        </ul>
    </div>
</body>
</html>

video (4).gif

五、易错点一——组合选择器的优先级计算

下面我们来看一个题目

.container ul li:nth-child(odd)

这个选择器的优先级是多少?

答案为10+1+1+10=22

因为:nth-child(odd)是伪类选择器,优先级是10

六、易错点二——nth-child vs nth-of-type:关键区别

这两个选择器经常让人困惑,让我们通过一个例子来理解它们的区别:

<div class="container">
  <h1>nth-child vs nth-of-type 例子</h1>
  <p>这是第一个段落</p>
  <div>这是一个div</div>
  <p>这是第二个段落</p>
  <p>这是第三个段落</p>
  <div>这是第二个div</div>
</div>
/* 选择.container下第3个子元素,且这个元素必须是<p>标签 */
.container p:nth-child(3) {
  background-color: yellow;
}
/* 实际不会生效,因为第3个子元素是div不是p */

/* 选择.container下第3个<p>类型的元素 */
.container p:nth-of-type(3) {
  background-color: lightblue;
}
/* 这会选择"这是第三个段落" */

关键区别

  • nth-child(n):选择父元素的第n个子元素,且必须是指定类型
  • nth-of-type(n):选择父元素下第n个指定类型的子元素(忽略其他类型元素)

image.png

二、谈谈display的属性值

我们的详解会基于下面的表进行

display 属性值速查表

属性值 作用描述 示例代码 典型场景
none 完全隐藏元素,不占空间 .hidden { display: none; } 动态显示 / 隐藏元素
block 块级元素,独占一行,支持宽高 .block { display: block; width: 200px; } 容器、段落、标题
inline 行内元素,不换行,宽高由内容决定 .inline { display: inline; } 文本、链接、图片
inline-block 不换行,但支持宽高 .btn { display: inline-block; width: 100px; } 水平按钮组、图标
table 模拟表格布局 .table { display: table; } 数据表格、等高布局
table-cell 模拟表格单元格,支持垂直居中 .cell { display: table-cell; vertical-align: middle; } 表单对齐、垂直居中
flex 弹性布局,一维排列 .flex { display: flex; justify-content: center; } 导航栏、自适应卡片
grid 网格布局,二维排列 .grid { display: grid; grid-template-columns: repeat(3, 1fr); } 图片画廊、响应式网格

一、基础布局属性值

1. display: none

  • 作用:完全隐藏元素,不占用文档空间,脱离文档流。

  • 示例

    <style>
      .hidden { display: none; }
    </style>
    <div>可见内容</div>
    <div class="hidden">隐藏内容</div> <!-- 完全消失 -->
    <div>可见内容</div>
    

image.png

2. display: block

  • 作用:块级元素,独占一行,宽度默认为父元素的 100%,支持设置宽高。

  • 示例

    <style>
      .block {
        display: block;
        width: 200px;
        height: 50px;
        background: lightblue;
      }
    </style>
    <div class="block">块级元素1</div>
    <div class="block">块级元素2</div> <!-- 自动换行 -->
    
  • 常见元素<div><p><h1><ul>等。

image.png

3. display: inline

  • 作用:行内元素,不换行,宽度由内容决定,无法设置宽高。

  • 示例

    <style>
      .inline {
        display: inline;
        background: lightgreen;
        width: 200px; /* 无效 */
      }
    </style>
    <span class="inline">行内内容1</span>
    <span class="inline">行内内容2</span> <!-- 不换行 -->
    
  • 限制:垂直方向的marginpadding无效,水平方向有效。

  • 常见元素<span><a><img>等。

image.png

4. display: inline-block

  • 作用:行内块元素,不换行,但支持设置宽高,兼具inlineblock的特性。

  • 示例

    <style>
      .inline-block {
        display: inline-block;
        width: 100px;
        height: 50px;
        background: lightcoral;
      }
    </style>
    <div class="inline-block">元素1</div>
    <div class="inline-block">元素2</div> <!-- 不换行 -->
    
  • 应用场景:水平排列的按钮、图片画廊等。

  • image.png

二、表格布局属性值

5. display: table

  • 作用:将元素渲染为块级表格,需配合子元素的table-rowtable-cell

  • 示例

    <style>
      .table { display: table; }
      .row { display: table-row; }
      .cell { display: table-cell; border: 1px solid #ccc; }
    </style>
    <div class="table">
      <div class="row">
        <div class="cell">单元格1</div>
        <div class="cell">单元格2</div>
      </div>
    </div>
    
  • 特性:自动应用表格的布局规则(如单元格等高)。

image.png

6. display: table-cell

  • 作用:模拟表格单元格,常用于垂直居中。

  • 示例

    <style>
      .container {
        display: table-cell;
        width: 200px;
        height: 100px;
        background: lightblue;
        vertical-align: middle; /* 垂直居中 */
        text-align: center; /* 水平居中 */
      }
    </style>
    <div class="container">居中内容</div>
    

image.png

三、弹性布局(Flexbox)

7. display: flex

  • 作用:将元素转换为弹性容器,子元素成为弹性项目,支持灵活的对齐和分布。

  • 示例:水平均匀分布三个按钮

    <style>
      .flex-container {
        display: flex;
        justify-content: space-between; /* 均匀分布 */
      }
      .btn { padding: 10px; background: lightgreen; }
    </style>
    <div class="flex-container">
      <div class="btn">按钮1</div>
      <div class="btn">按钮2</div>
      <div class="btn">按钮3</div>
    </div>
    
  • 常用属性flex-directionjustify-contentalign-itemsflex-wrap等。

image.png

8. display: inline-flex

  • 作用:弹性容器,但表现为内联元素,不独占一行。

  • 示例

    <style>
      .inline-flex {
        display: inline-flex;
        background: lightyellow;
      }
      .item { width: 50px; height: 50px; background: lightcoral; }
    </style>
    <div class="inline-flex">
      <div class="item">1</div>
      <div class="item">2</div>
    </div>
    

image.png

四、网格布局(Grid)

9. display: grid

  • 作用:将元素转换为网格容器,子元素成为网格项目,支持二维布局。

  • 示例:创建 3×3 网格

    <style>
      .grid-container {
        display: grid;
        grid-template-columns: repeat(3, 1fr); /* 3列,等宽 */
        gap: 10px;
      }
      .grid-item { background: lightblue; padding: 20px; }
    </style>
    <div class="grid-container">
      <div class="grid-item">1</div>
      <div class="grid-item">2</div>
      <div class="grid-item">3</div>
      <!-- 自动填充剩余网格 -->
    </div>
    

image.png

  • 常用属性grid-template-columnsgrid-template-rowsgapplace-items等。

10. display: inline-grid

  • 作用:网格容器,但表现为内联元素。

  • 示例

    <style>
      .inline-grid {
        display: inline-grid;
        grid-template-columns: 50px 50px;
        gap: 5px;
        background: lightyellow;
      }
    </style>
    <div class="inline-grid">
      <div>1</div>
      <div>2</div>
    </div>
    

image.png

三、block、inline和inline-block的区别

block(块级元素)

  • 独占一行,前后会换行
  • 可以设置宽度(width)、高度(height)、内外边距(margin/padding)
  • 默认宽度为父元素的100%
  • 常见块级元素:<div><p><h1>-<h6><ul><ol><li>
<div style="border: 2px solid blue; padding: 10px;">
  这是一个块级元素
  <p style="background: lightyellow;">块级元素内的段落</p>
</div>

image.png

inline(行内元素)

  • 不会独占一行,与其他行内元素排列在同一行
  • 设置宽度和高度无效
  • 水平方向的内外边距有效,垂直方向的内外边距不会影响其他元素
  • 默认宽度为内容宽度
  • 常见行内元素:<span><a><strong><em><img>
<p>
  这是一段包含<span style="background: pink; padding: 0 10px;">行内元素</span>的文本,
  <a href="#" style="color: red;">链接</a>也是行内元素。
</p>

image.png

inline-block(行内块元素)

  • 结合了inline和block的特性
  • 不会独占一行,与其他行内元素排列在同一行
  • 可以设置宽度、高度、内外边距
  • 默认宽度为内容宽度
<div style="background: #f0f0f0; padding: 10px;">
  <span style="display: inline-block; width: 80px; height: 40px; background: lightgreen;">项目1</span>
  <span style="display: inline-block; width: 80px; height: 60px; background: lightblue;">项目2</span>
  <span style="display: inline-block; width: 80px; height: 50px; background: lightcoral;">项目3</span>
</div>

image.png

主要区别对比

特性 block inline inline-block
是否换行
设置width/height 有效 无效 有效
margin/padding 全部有效 水平有效,垂直特殊 全部有效
默认宽度 父元素100% 内容宽度 内容宽度
元素排列 垂直排列 水平排列 水平排列
包含关系 可包含其他块/行内 通常只含文本/行内 可包含其他块/行内

使用场景

  • block:用于构建页面主要结构,如头部、导航、内容区域、页脚等
  • inline:用于文本修饰或小图标等不需要设置宽高的元素
  • inline-block:需要水平排列但又要设置宽高的元素,如导航菜单项、按钮组等

下面分别举一些例子来佐证

block

<!-- 页面头部 -->
<header style="display: block; background: #333; color: white; padding: 20px; text-align: center;">
  <h1>网站标题</h1>
</header>

<!-- 导航栏 -->
<nav style="display: block; background: #444; padding: 10px;">
  <ul style="margin: 0; padding: 0;">
    <li style="display: inline-block; margin-right: 15px;"><a href="#" style="color: white;">首页</a></li>
    <li style="display: inline-block; margin-right: 15px;"><a href="#" style="color: white;">产品</a></li>
    <li style="display: inline-block;"><a href="#" style="color: white;">关于</a></li>
  </ul>
</nav>

<!-- 内容区域 -->
<main style="display: block; padding: 20px; background: #f5f5f5;">
  <p>这里是页面主要内容...</p>
</main>

<!-- 页脚 -->
<footer style="display: block; background: #333; color: white; text-align: center; padding: 10px;">
  <p2023 版权所有</p>
</footer>

image.png

inline

<p>
  这是一段包含 <strong style="display: inline; color: red;">强调文本</strong><a href="#" style="display: inline; text-decoration: underline;">超链接</a> 的段落。
</p>

<!-- 小图标(通过伪元素实现) -->
<style>
  .icon::before {
    content: "★";
    display: inline;
    color: gold;
    margin-right: 5px;
  }
</style>
<p><span class="icon">重要提示</span>:请阅读注意事项。</p>

image.png

inline-block

<!-- 导航菜单项 -->
<div style="background: #f0f0f0; padding: 10px;">
  <a href="#" style="display: inline-block; width: 100px; height: 40px; line-height: 40px; text-align: center; background: #4CAF50; color: white; margin-right: 5px;">首页</a>
  <a href="#" style="display: inline-block; width: 100px; height: 40px; line-height: 40px; text-align: center; background: #2196F3; color: white; margin-right: 5px;">产品</a>
  <a href="#" style="display: inline-block; width: 100px; height: 40px; line-height: 40px; text-align: center; background: #FF9800; color: white;">关于</a>
</div>

<!-- 按钮组 -->
<div style="margin-top: 20px;">
  <button style="display: inline-block; padding: 8px 16px; margin-right: 10px; background: #e0e0e0; border: none;">取消</button>
  <button style="display: inline-block; padding: 8px 16px; background: #4CAF50; color: white; border: none;">确认</button>
</div>

image.png

四、说一说你知道的隐藏元素的方法

在前端开发中,隐藏元素是一个常见但需要谨慎处理的操作。不同的隐藏方法会导致不同的渲染表现、可访问性影响和性能开销。

一、完全移除型隐藏

1. display: none - 彻底移除元素

.hide-element {
  display: none;
}

核心特性

  • 元素不会出现在渲染树中
  • 不占据任何文档流空间
  • 所有子元素都会被连带隐藏
  • 无法触发任何DOM事件
  • 屏幕阅读器完全忽略该元素
  • 触发浏览器重排(reflow)

性能影响:高(导致布局重新计算)

适用场景

  • 需要完全移除不需要的元素
  • 标签页切换内容区域
  • 响应式布局中在不同断点隐藏元素

示例

<div class="tab-content" style="display: none;">
  这个内容将在切换标签时显示
</div>

二、视觉隐藏但保留空间

2. visibility: hidden - 隐形占位

.invisible {
  visibility: hidden;
}

核心特性

  • 元素不可见但保留原有空间
  • 无法触发鼠标等交互事件
  • 只导致重绘(repaint),性能较好
  • 可通过visibility: visible显示子元素
  • 屏幕阅读器无法访问

性能影响:中等(仅重绘)

适用场景

  • 需要保留布局占位的隐藏
  • 实现自定义复选框/单选框样式
  • 需要保持布局稳定的场景

示例

<div class="placeholder" style="visibility: hidden;">
  这里的内容隐藏但仍占位
</div>

三、透明化隐藏

3. opacity: 0 - 完全透明

.transparent {
  opacity: 0;
}

核心特性

  • 元素完全透明但占据空间
  • 仍能触发所有DOM事件
  • 会创建新的复合层,适合动画
  • 屏幕阅读器可以访问
  • 子元素无法单独恢复可见性

性能影响:低(GPU加速)

适用场景

  • 需要淡入淡出动画
  • 需要隐藏但仍需交互的元素
  • 可访问性要求高的内容

示例

<button class="fade-button" style="opacity: 0;">
  这个按钮透明但仍可点击
</button>

四、定位移出型隐藏

4. position: absolute - 移出视口

.off-screen {
  position: absolute;
  left: -9999px;
  top: -9999px;
}

核心特性

  • 视觉上不可见且不占空间
  • 仍保留DOM位置和事件绑定
  • 屏幕阅读器可以访问
  • 不影响页面布局流

性能影响:高(导致重排)

适用场景

  • SEO优化需要隐藏但可抓取的内容
  • 可访问性要求高的隐藏内容
  • 需要隐藏但保留表单元素值

示例

<label for="search" class="visually-hidden">搜索框</label>
<input type="text" id="search">

五、层叠隐藏

5. z-index: 负值 - 下层遮盖

.under-layer {
  position: relative;
  z-index: -1;
}

核心特性

  • 元素被其他层叠元素遮盖
  • 仍占据原有文档流空间
  • 事件触发取决于遮盖元素
  • 屏幕阅读器可以访问

性能影响:低(复合层处理)

适用场景

  • 背景元素隐藏
  • 特殊视觉效果实现
  • 需要保留元素但置于底层的场景

示例

<div class="background-element" style="z-index: -1;">
  这个内容会被其他元素遮盖
</div>

六、裁剪型隐藏

6. clip/clip-path - 元素裁剪

.clipped {
  /* 传统clip方法(已废弃) */
  clip: rect(0 0 0 0);
  
  /* 现代clip-path方法 */
  clip-path: circle(0);
}

核心特性

  • 视觉隐藏但保留元素空间
  • 不响应交互事件
  • 屏幕阅读器行为不一致
  • 支持平滑动画过渡

性能影响:中等(GPU加速)

适用场景

  • 创意动画效果
  • 渐进式内容展示
  • 需要保留元素尺寸的隐藏

示例

<div class="expandable" style="clip-path: inset(0 100% 0 0);">
  这个内容可以通过动画展开
</div>

七、变形隐藏

7. transform: scale(0) - 零尺寸缩放

.scaled-zero {
  transform: scale(0);
}

核心特性

  • 元素视觉尺寸为零但保留布局空间
  • 不响应交互事件
  • 保持元素原本的盒模型特性
  • 屏幕阅读器可以访问
  • 支持平滑的缩放动画

性能影响:低(GPU加速)

适用场景

  • 需要缩放动画的元素
  • 需要保留布局空间的隐藏
  • 特殊交互效果的实现

示例

<button class="zoom-button" style="transform: scale(0);">
  点击后会放大显示
</button>

方法对比表

方法 占据空间 可交互性 可访问性 动画支持 性能影响 SEO友好
display: none
visibility: hidden ✔️ 有限
opacity: 0 ✔️ ✔️ ✔️ ✔️ ✔️
position: absolute ✔️ ✔️ ✔️
z-index: 负值 ✔️ 可能 ✔️ ✔️
clip-path ✔️ 可能 ✔️ 可能
transform: scale(0) ✔️ ✔️ ✔️ ✔️

选型建议

  1. 需要彻底移除元素
  • 首选:display: none
  • 场景:标签页切换、响应式布局隐藏
  1. 需要保留布局空间
  • 首选:visibility: hidden
  • 替代:opacity: 0(如需交互)
  • 场景:占位隐藏、自定义表单控件
  1. 需要动画效果
  • 淡入淡出:opacity
  • 缩放动画:transform: scale()
  • 裁剪动画:clip-path
  1. 需要可访问性支持
  • 首选:position: absolute移出视口
  • 替代:opacity: 0
  • 场景:屏幕阅读器可读的隐藏内容
  1. 需要SEO优化
  • 首选:position: absolute
  • 替代:z-index负值
  • 场景:隐藏关键词但需要被搜索引擎抓取

在给面试官介绍这部分知识的时候,建议采用下面的步骤
首先分类概述8种方法(完全移除/视觉隐藏/特殊技巧三大类),然后逐类分析核心方法的特性、性能差异和应用场景,接着通过布局影响、交互性等维度系统对比,最后结合项目实战经验说明选型策略,并延伸提及现代CSS特性。这种回答既展现知识体系完整性,又体现技术决策的思考过程。

五、display:none与visibility:hidden有何区别

1. 两者差异

(1)渲染机制差异

  • display: none
    浏览器会从 渲染树(Render Tree)  中完全移除该元素,后续布局计算时忽略其存在。

    <div style="display: none;">Hidden</div>
    <!-- 等同于DOM中删除此元素 -->
    
  • visibility: hidden
    元素仍保留在渲染树中,但会被标记为不可见(类似透明效果),浏览器仍会计算其布局。

    <div style="visibility: hidden;">Invisible</div>
    <!-- 类似于设置透明度为0,但保留占位 -->
    

(2)性能影响

  • display: none:触发 重排(Reflow) ,影响较大(尤其是频繁切换时)。
  • visibility: hidden:仅触发 重绘(Repaint) ,性能开销更小。

(3)子元素行为

visibility: hidden 会隐藏元素但保留占位,其子元素可通过 visibility: visible 重新显示;
display: none 会完全移除元素及其子元素,子元素无法通过 display: block 恢复显示。

<div style="visibility: hidden;">
  <span style="visibility: visible;">我仍会显示!</span>
</div>

<div style="display: none;">
  <span style="display: block;">我永远不会显示!</span>
</div>

2. 应用场景差异

何时用 display: none

  • 需要 彻底移除元素(如标签页切换、响应式布局隐藏侧边栏)。
  • 需要 减少DOM渲染压力(如长列表的懒加载)。

何时用 visibility: hidden

  • 需要 保持布局稳定(如占位隐藏即将加载的内容)。
  • 实现 自定义复选框/单选框 的视觉替换。
  • 需要 保留元素状态(如表单隐藏字段仍需提交数据)。

3. 可以给面试官整点”夜宵“

(1)延伸问题(前文讲过)

  • "如果希望隐藏元素但仍能被屏幕阅读器读取,你会怎么做?"
    答:使用 绝对定位移出视口 或 .visually-hidden 工具类(如Bootstrap的屏幕阅读器专用样式)。

(2)框架中的表现

  • React 中 v-if 对应 display: nonev-show 对应 visibility: hidden
  • CSS动画visibility 可配合 transition 实现延迟隐藏(避免元素突然消失)

4. 核心区别总结

特性 display: none visibility: hidden
是否占据布局空间 ❌ 完全移除,不占空间 ✔️ 隐藏但保留原有空间
是否触发重排/重绘 触发重排 (Reflow) 仅触发重绘 (Repaint)
子元素是否可显示 ❌ 子元素必然隐藏 ✔️ 子元素可设 visibility: visible 单独显示
是否响应事件 ❌ 无法触发任何事件 ❌ 无法触发事件(但DOM仍存在)
屏幕阅读器可访问性 ❌ 完全忽略 ❌ 无法访问

六、对盒模型的理解

盒模型是 CSS 布局的基础,描述了元素在页面中所占空间的计算方式。 每个元素都被视为一个矩形盒子,由四个主要部分组成:

一、盒模型的四个组成部分

1. 内容区(Content)

  • 包含元素的实际内容(文本、图片等)。
  • 由 width 和 height 属性控制。

2. 内边距(Padding)

  • 内容区与边框之间的距离。
  • 由 padding-toppadding-rightpadding-bottompadding-left 控制,也可简写为 padding

3. 边框(Border)

  • 围绕内边距和内容区的线条。
  • 由 border-widthborder-styleborder-color 控制,也可简写为 border

4. 外边距(Margin)

  • 元素与其他元素之间的距离。
  • 由 margin-topmargin-rightmargin-bottommargin-left 控制,也可简写为 margin

二、盒模型的宽度和高度计算

1. 标准盒模型(默认)

总宽度 = width + padding-left + padding-right + border-left-width + border-right-width
总高度 = height + padding-top + padding-bottom + border-top-width + border-bottom-width

示例

div {
  width: 200px;       /* 内容区宽度 */
  padding: 10px;      /* 内边距:上下左右各10px */
  border: 2px solid;  /* 边框:2px宽 */
  margin: 15px;       /* 外边距:上下左右各15px */
}

总宽度 = 200 + 10×2 + 2×2 = 224px
总高度同理。

2. 怪异盒模型(IE 盒模型)

通过 box-sizing: border-box 设置。
总宽度 = width(已包含 padding 和 border
总高度 = height(已包含 padding 和 border

示例

div {
  width: 200px;       /* 总宽度(包含padding和border) */
  padding: 10px;      /* 内边距:上下左右各10px */
  border: 2px solid;  /* 边框:2px宽 */
  box-sizing: border-box; /* 使用怪异盒模型 */
}

总宽度 = 200px(内容区宽度 = 200 - 10×2 - 2×2 = 176px)

下面我们横向介绍一下标准盒模型(content-box)和IE盒模型(border-box)的不同

标准盒模型的宽度 / 高度仅包含内容区,而 IE 盒模型的宽度 / 高度包含内容区、内边距和边框。

image.png

三、盒模型的关键特性

1. 外边距合并(Margin Collapsing)

  • 相邻元素的垂直外边距会合并为较大的一个。

  • 示例

    <div style="margin-bottom: 20px;">元素1</div>
    <div style="margin-top: 10px;">元素2</div>
    

    元素 1 和元素 2 之间的实际间距为 20px(取较大值)。

2. 内边距和边框不影响元素的位置

  • 增加内边距和边框会撑大元素,但不会改变其他元素的位置(除非超出父元素)。

3. 负外边距

  • 可用于将元素拉向其他元素,例如:margin-top: -10px 会使元素向上移动 10px。

四、最佳实践

1. 全局设置盒模型

* {
  box-sizing: border-box;
}

这样可以避免因默认盒模型导致的宽度计算问题。

2. 合理使用内边距和外边距

  • 内边距用于控制元素内部的空间。
  • 外边距用于控制元素与其他元素之间的空间。

3. 避免外边距合并问题

  • 使用 flex 或 grid 布局,它们的子元素不会发生外边距合并。

五、盒模型可视化示例

<style>
  .box {
    width: 200px;
    height: 100px;
    padding: 20px;
    border: 5px solid #333;
    margin: 30px;
    background: lightblue;
  }
</style>
<div class="box">内容区</div>

image.png

image.png

这个盒子的总尺寸计算:
  • 宽度:200(内容) + 20×2(内边距) + 5×2(边框) = 250px
  • 高度:100(内容) + 20×2(内边距) + 5×2(边框) = 150px
  • 外边距(30px)会影响与其他元素的距离,但不包含在盒子自身尺寸内。

★★★ 建议在项目中统一使用 box-sizing: border-box,以减少宽度计算的复杂性。

七、谈一谈CSS3的新特性

一、选择器增强

1. 属性选择器

  • 作用:基于元素属性或属性值来选择元素。

  • 示例

    /* 选择所有title属性以"image"开头的元素 */
    [title^="image"] { border: 1px solid red; }
    
    /* 选择所有href属性包含"example"的链接 */
    a[href*="example"] { color: blue; }
    
    /* 选择所有data-type属性以"pdf"结尾的元素 */
    [data-type$="pdf"] { background: #f0f0f0; }
    

2. 伪类选择器

  • 作用:选择处于特定状态或位置的元素。

  • 示例

    /* 选择每个ul的第二个li */
    li:nth-child(2) { font-weight: bold; }
    
    /* 选择每个section的第一个p */
    p:first-of-type { color: #666; }
    
    /* 选择未被访问的链接 */
    a:link { color: blue; }
    
    /* 选择鼠标悬停的按钮 */
    button:hover { background: #e63946; }
    

3. 伪元素

  • 作用:选择元素的特定部分或创建虚拟元素。

  • 示例

    /* 在每个blockquote前添加引号 */
    blockquote::before { content: "“"; font-size: 2em; }
    
    /* 选中的文本变为红色背景 */
    ::selection { background: red; color: white; }
    
    /* 首字母大写并设置特殊样式 */
    p::first-letter { font-size: 200%; color: #e63946; }
    

二、盒模型相关

1. box-sizing 属性

  • 作用:控制宽度和高度的计算方式。

  • 示例

    .standard-box {
      box-sizing: content-box; /* 默认值,宽度=内容区 */
      width: 200px;
      padding: 20px; /* 总宽度=200+20*2=240px */
    }
    
    .border-box {
      box-sizing: border-box; /* 宽度=内容区+padding+border */
      width: 200px;
      padding: 20px; /* 内容区宽度=200-20*2=160px */
    }
    

2. 多列布局

  • 作用:将内容分成多列,类似报纸排版。

  • 示例

    .news-container {
      column-count: 3; /* 分成3列 */
      column-gap: 20px; /* 列间距20px */
      column-rule: 1px solid #ccc; /* 列分隔线 */
    }
    

三、背景与边框

1. 背景增强

  • 作用:提供更灵活的背景控制。

  • 示例

    .hero {
      background-image: url(bg.jpg);
      background-size: cover; /* 覆盖整个容器 */
      background-position: center; /* 居中显示 */
      background-repeat: no-repeat;
      background-attachment: fixed; /* 固定背景(滚动时不移动) */
    }
    
    .gradient {
      background: linear-gradient(to right, #ff512f, #f09819); /* 线性渐变 */
    }
    
    

2. 边框增强

  • 作用:创建圆角、阴影和图片边框。

  • 示例

    .card {
      border-radius: 10px; /* 圆角 */
      box-shadow: 0 4px 8px rgba(0,0,0,0.2); /* 阴影 */
    }
    
    .fancy-border {
      border-image: url(border.png) 30 round; /* 图片边框 */
    }
    

四、文本效果

1. 文本阴影

  • 作用:为文本添加阴影效果。

  • 示例

    h1 {
      text-shadow: 2px 2px 4px rgba(0,0,0,0.5); /* 水平偏移、垂直偏移、模糊半径、颜色 */
    }
    

2. 文字溢出处理

  • 作用:控制文本溢出容器时的显示方式。

  • 示例

    .truncate {
      white-space: nowrap; /* 不换行 */
      overflow: hidden; /* 溢出隐藏 */
      text-overflow: ellipsis; /* 显示省略号 */
    }
    

3. 自定义字体

  • 作用:引入非系统字体。

  • 示例

    @font-face {
      font-family: 'MyFont';
      src: url('fonts/myfont.woff2') format('woff2');
      font-weight: normal;
      font-style: normal;
    }
    
    body {
      font-family: 'MyFont', sans-serif;
    }
    

五、2D/3D 转换

1. 2D 转换

  • 作用:对元素进行平移、旋转、缩放和倾斜。

  • 示例

    .box {
      transform: translate(50px, 20px); /* 平移 */
      transform: rotate(45deg); /* 旋转 */
      transform: scale(1.5); /* 缩放 */
      transform: skew(20deg, 10deg); /* 倾斜 */
      
      /* 组合转换 */
      transform: translate(50px) rotate(30deg) scale(1.2);
    }
    

2. 3D 转换

  • 作用:创建 3D 空间中的变换效果。

  • 示例

    .cube {
      transform: perspective(1000px) rotateY(45deg); /* 3D透视和旋转 */
      transform-style: preserve-3d; /* 保留3D空间 */
    }
    
    .card {
      transition: transform 0.5s;
    }
    
    .card:hover {
      transform: rotateY(180deg); /* 翻转动画 */
      backface-visibility: hidden; /* 背面不可见 */
    }
    

六、动画与过渡

1. 过渡(Transition)

  • 作用:在两个状态之间平滑过渡。

  • 示例

    .button {
      background: blue;
      color: white;
      transition: background 0.3s ease, transform 0.3s ease;
    }
    
    .button:hover {
      background: red;
      transform: scale(1.1);
    }
    

2. 动画(Animation)

  • 作用:创建复杂的多阶段动画。

  • 示例

    @keyframes fadeIn {
      from { opacity: 0; }
      to { opacity: 1; }
    }
    
    .element {
      animation: fadeIn 2s infinite alternate; /* 无限播放,交替反向 */
    }
    
    @keyframes bounce {
      0%, 100% { transform: translateY(0); }
      50% { transform: translateY(-20px); }
    }
    
    .ball {
      animation: bounce 1s ease infinite;
    }
    

七、弹性布局(Flexbox)

  • 作用:高效地布局、对齐和分配容器内的空间。

  • 示例

    .flex-container {
      display: flex;
      justify-content: space-between; /* 水平分布 */
      align-items: center; /* 垂直居中 */
      flex-wrap: wrap; /* 自动换行 */
    }
    
    .flex-item {
      flex: 1; /* 平均分配空间 */
    }
    

八、网格布局(Grid)

  • 作用:创建二维网格布局。

  • 示例

    .grid-container {
      display: grid;
      grid-template-columns: repeat(3, 1fr); /* 3列等宽 */
      grid-template-rows: auto 100px; /* 2行,第一行自动高度 */
      gap: 20px; /* 行列间距 */
    }
    
    .item1 {
      grid-column: 1 / 3; /* 跨越第1-2列 */
      grid-row: 1; /* 位于第1行 */
    }
    

九、媒体查询(Responsive Design)

  • 作用:根据设备屏幕尺寸应用不同的样式。

  • 示例

    /* 大屏幕 */
    .container {
      width: 80%;
      margin: 0 auto;
    }
    
    /* 小屏幕 */
    @media (max-width: 768px) {
      .container {
        width: 100%;
        padding: 0 15px;
      }
    }
    
    /* 打印样式 */
    @media print {
      body {
        font-size: 12pt;
        color: black;
      }
    }
    

十、其他特性

1. 滤镜(Filters)

  • 作用:对元素应用视觉效果(模糊、亮度等)。

  • 示例

    img {
      filter: grayscale(100%); /* 灰度 */
      filter: blur(5px); /* 模糊 */
      filter: brightness(0.5); /* 亮度降低 */
    }
    
    .hover-effect:hover {
      filter: contrast(150%); /* 增加对比度 */
    }
    

2. 计算(calc ())

  • 作用:动态计算 CSS 值。

  • 示例

    .sidebar {
      width: calc(25% - 20px); /* 宽度为父容器的25%减去20px */
    }
    
    .full-height {
      height: calc(100vh - 80px); /* 高度为视口高度减去80px */
    }
    

3. 多背景

  • 作用:为元素应用多层背景。

  • 示例

    .element {
      background: 
        url(top.png) top no-repeat,
        url(bottom.png) bottom no-repeat,
        linear-gradient(to bottom, #f0f0f0, #ccc);
    }
    

浏览器兼容性

大多数现代浏览器已全面支持上述特性,但部分旧版浏览器(如 IE)可能需要添加前缀或降级方案。建议使用工具如 Autoprefixer 自动处理前缀问题。

这些新特性让 CSS 从单纯的样式描述语言转变为强大的布局和动画工具,极大提升了前端开发的效率和网页的用户体验。

八、单行、多行内容的隐藏

一、单行文本溢出隐藏

核心属性
white-space: nowrap + overflow: hidden + text-overflow: ellipsis

示例代码

.single-line {
  white-space: nowrap;    /* 强制不换行 */
  overflow: hidden;       /* 溢出内容隐藏 */
  text-overflow: ellipsis; /* 显示省略号 */
  width: 200px;           /* 必须设置宽度 */
}

关键点

  1. 容器需有固定宽度(如 width 或 max-width
  2. 仅支持单行,无法处理多行文本
  3. 兼容性好(IE6+ 支持)

下面给大家一个小demo方便大家理解

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>单行文本溢出示例</title>
    <style>
        .container {
            max-width: 600px;
            margin: 20px auto;
            padding: 20px;
            font-family: Arial, sans-serif;
            background-color: #f9f9f9;
            border-radius: 8px;
        }
        
        .demo-box {
            margin: 20px 0;
        }
        
        .single-line {
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
            width: 200px;
            border: 1px solid #ccc;
            padding: 10px;
            margin: 10px 0;
            background-color: white;
        }
        
        .controls {
            display: flex;
            align-items: center;
            gap: 10px;
            margin-top: 15px;
        }
        
        input {
            padding: 8px;
            width: 80px;
            border: 1px solid #ddd;
            border-radius: 4px;
        }
        
        .code-block {
            background-color: #333;
            color: white;
            padding: 15px;
            border-radius: 4px;
            font-family: monospace;
            overflow-x: auto;
        }
    </style>
</head>
<body>
    <div class="container">
        <h2>单行文本溢出隐藏示例</h2>
        
        <div class="demo-box">
            <div class="single-line" id="demoText">
                这是一段很长的文本内容,当宽度不足以显示全部内容时,会自动显示省略号...
            </div>
            
            <div class="controls">
                <label for="widthInput">容器宽度 (px):</label>
                <input type="number" id="widthInput" value="200" min="50" max="500">
                <button onclick="updateWidth()">应用</button>
            </div>
        </div>
        
        <div class="code-block">
            <pre>
.single-line {
  white-space: nowrap;    /* 强制不换行 */
  overflow: hidden;       /* 溢出内容隐藏 */
  text-overflow: ellipsis; /* 显示省略号 */
  width: 200px;           /* 必须设置宽度 */
}</pre>
        </div>
    </div>
    
    <script>
        function updateWidth() {
            const width = document.getElementById('widthInput').value;
            document.getElementById('demoText').style.width = `${width}px`;
        }
    </script>
</body>
</html>
    

GIF.gif

二、多行文本溢出隐藏

方法 1:使用 -webkit-line-clamp(推荐现代浏览器)

核心属性
display: -webkit-box + -webkit-line-clamp: 2 + overflow: hidden

示例代码

.multi-line {
  display: -webkit-box;      /* 必须 */
  -webkit-box-orient: vertical; /* 必须 */
  -webkit-line-clamp: 2;     /* 显示的行数 */
  overflow: hidden;          /* 溢出隐藏 */
  text-overflow: ellipsis;   /* 省略号(可选) */
  width: 200px;              /* 容器宽度 */
}

关键点

  1. 仅支持 webkit 内核浏览器(Chrome、Safari 等)
  2. 简单高效,推荐用于移动端
  3. 行数固定,无法根据内容动态调整

下面给大家一个小demo方便大家理解

GIF.gif

方法 2:使用绝对定位 + 渐变遮罩(兼容所有浏览器)

原理
通过绝对定位覆盖一个带渐变的遮罩层,模拟省略号效果。

示例代码

.multi-line-fallback {
  position: relative;
  line-height: 1.5em;
  max-height: 3em;         /* 显示的最大高度(行数×行高) */
  overflow: hidden;
  width: 200px;
}

.multi-line-fallback::after {
  content: "...";
  position: absolute;
  bottom: 0;
  right: 0;
  padding-left: 40px;      /* 渐变区域宽度 */
  background: linear-gradient(to right, transparent, white 70%);
}

关键点

  1. 兼容性好(所有浏览器支持)
  2. 效果近似,但省略号位置可能不精确
  3. 需要手动计算高度(行数 × 行高)

下面给大家一个小demo方便大家理解

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>多行文本溢出 - 渐变遮罩法</title>
    <style>
        .container {
            max-width: 600px;
            margin: 20px auto;
            padding: 20px;
            font-family: Arial, sans-serif;
            background-color: #f9f9f9;
            border-radius: 8px;
        }
        
        .demo-box {
            margin: 20px 0;
        }
        
        .multi-line-mask {
            position: relative;
            line-height: 1.5em;
            max-height: 4.5em; /* 默认3行 */
            overflow: hidden;
            width: 200px;
            border: 1px solid #ccc;
            padding: 10px;
            margin: 10px 0;
            background-color: white;
        }
        
        .multi-line-mask::after {
            content: "";
            position: absolute;
            bottom: 0;
            right: 0;
            width: 60px;
            height: 1.5em;
            background: linear-gradient(to right, transparent, white 80%);
        }
        
        .controls {
            display: flex;
            align-items: center;
            gap: 10px;
            margin-top: 15px;
        }
        
        input {
            padding: 8px;
            width: 50px;
            border: 1px solid #ddd;
            border-radius: 4px;
        }
        
        .code-block {
            background-color: #333;
            color: white;
            padding: 15px;
            border-radius: 4px;
            font-family: monospace;
            overflow-x: auto;
        }
    </style>
</head>
<body>
    <div class="container">
        <h2>多行文本溢出 - 渐变遮罩法</h2>
        
![GIF.gif](https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/6f6a70af6f43432fa61c0421f5dc9ce3~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5aSp5aSp5omt56CB:q75.awebp?rk3s=f64ab15b&x-expires=1752841093&x-signature=6I%2BR74wk99R2Iolmqf2OZzne9qc%3D)
        <div class="demo-box">
            <div class="multi-line-mask" id="demoText">
                这是一段多行文本内容,当超过容器高度时会通过渐变遮罩隐藏多余内容。这种方法兼容性好,但省略号位置可能不够精确。这是一段多行文本内容,当超过容器高度时会通过渐变遮罩隐藏多余内容。
            </div>
            
            <div class="controls">
                <label for="linesInput">行数:</label>
                <input type="number" id="linesInput" value="3" min="1" max="10">
                <button onclick="updateLines()">应用</button>
            </div>
        </div>
        
        <div class="code-block">
            <pre>
.multi-line-mask {
  position: relative;
  line-height: 1.5em;
  max-height: 4.5em; /* 3行 */
  overflow: hidden;
}

.multi-line-mask::after {
  content: "";
  position: absolute;
  bottom: 0;
  right: 0;
  width: 60px;
  height: 1.5em;
  background: linear-gradient(to right, transparent, white 80%);
}</pre>
        </div>
    </div>
    
    <script>
        function updateLines() {
            const lines = document.getElementById('linesInput').value;
            document.getElementById('demoText').style.maxHeight = `${lines * 1.5}em`;
        }
    </script>
</body>
</html>
    

GIF.gif

方法 3:JavaScript 动态处理(最灵活)

原理
通过 JS 计算文本高度,超出时截断并添加省略号。

示例代码

function truncateText(element, maxLines) {
  const lineHeight = parseInt(getComputedStyle(element).lineHeight);
  const maxHeight = lineHeight * maxLines;
  
  if (element.scrollHeight > maxHeight) {
    element.style.overflow = 'hidden';
    element.style.height = maxHeight + 'px';
    
    // 添加省略号
    const text = element.textContent;
    while (element.scrollHeight > maxHeight) {
      element.textContent = text.slice(0, -1);
    }
    element.textContent += '...';
  }
}

// 使用方法
document.querySelectorAll('.truncate').forEach(el => {
  truncateText(el, 3); // 限制3行
});

关键点

  1. 完全自定义,可动态调整行数
  2. 性能开销较大,需遍历所有元素
  3. 适用于复杂场景(如响应式布局)
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>多行文本溢出 - JavaScript动态处理</title>
    <style>
        .container {
            max-width: 600px;
            margin: 20px auto;
            padding: 20px;
            font-family: Arial, sans-serif;
            background-color: #f9f9f9;
            border-radius: 8px;
        }
        
        .demo-box {
            margin: 20px 0;
        }
        
        .multi-line-js {
            line-height: 1.5em;
            border: 1px solid #ccc;
            padding: 10px;
            margin: 10px 0;
            background-color: white;
        }
        
        .controls {
            display: grid;
            grid-template-columns: auto 1fr;
            gap: 10px;
            margin-top: 15px;
        }
        
        input {
            padding: 8px;
            border: 1px solid #ddd;
            border-radius: 4px;
        }
        
        .code-block {
            background-color: #333;
            color: white;
            padding: 15px;
            border-radius: 4px;
            font-family: monospace;
            overflow-x: auto;
        }
    </style>
</head>
<body>
    <div class="container">
        <h2>多行文本溢出 - JavaScript动态处理</h2>
        
        <div class="demo-box">
            <div class="multi-line-js" id="demoText">
                这是一段多行文本内容,使用JavaScript可以根据容器高度和行数限制动态截断文本并添加省略号。这种方法灵活性最高,适用于各种复杂场景,但需要注意性能开销。这是一段多行文本内容,使用JavaScript可以根据容器高度和行数限制动态截断文本并添加省略号。这种方法灵活性最高,适用于各种复杂场景,但需要注意性能开销。
            </div>
            
            <div class="controls">
                <label for="linesInput">行数限制:</label>
                <input type="number" id="linesInput" value="3" min="1" max="10">
                
                <label for="widthInput">容器宽度 (px):</label>
                <input type="number" id="widthInput" value="300" min="100" max="500">
                
                <label for="customEllipsis">自定义省略号:</label>
                <input type="text" id="customEllipsis" value="...">
            </div>
            
            <button onclick="applyTruncation()">应用设置</button>
        </div>
        
        <div class="code-block">
            <pre>
function truncateText(element, maxLines, ellipsis = '...') {
  const lineHeight = parseInt(getComputedStyle(element).lineHeight);
  const maxHeight = lineHeight * maxLines;
  
  // 保存原始内容
  if (!element.dataset.originalText) {
    element.dataset.originalText = element.textContent;
  }
  
  // 恢复原始内容再处理
  element.textContent = element.dataset.originalText;
  
  if (element.scrollHeight > maxHeight) {
    element.style.overflow = 'hidden';
    element.style.height = maxHeight + 'px';
    
    // 截断文本
    let text = element.textContent;
    while (element.scrollHeight > maxHeight && text.length > 0) {
      text = text.slice(0, -1);
      element.textContent = text + ellipsis;
    }
  } else {
    // 内容未超出,恢复完整显示
    element.style.height = 'auto';
    element.textContent = element.dataset.originalText;
  }
}</pre>
        </div>
    </div>
    
    <script>
        function truncateText(element, maxLines, ellipsis = '...') {
            const lineHeight = parseInt(getComputedStyle(element).lineHeight);
            const maxHeight = lineHeight * maxLines;
            
            // 保存原始内容
            if (!element.dataset.originalText) {
                element.dataset.originalText = element.textContent;
            }
            
            // 恢复原始内容再处理
            element.textContent = element.dataset.originalText;
            
            if (element.scrollHeight > maxHeight) {
                element.style.overflow = 'hidden';
                element.style.height = maxHeight + 'px';
                
                // 截断文本
                let text = element.textContent;
                while (element.scrollHeight > maxHeight && text.length > 0) {
                    text = text.slice(0, -1);
                    element.textContent = text + ellipsis;
                }
            } else {
                // 内容未超出,恢复完整显示
                element.style.height = 'auto';
                element.textContent = element.dataset.originalText;
            }
        }
        
        function applyTruncation() {
            const element = document.getElementById('demoText');
            const lines = parseInt(document.getElementById('linesInput').value);
            const width = document.getElementById('widthInput').value;
            const ellipsis = document.getElementById('customEllipsis').value;
            
            element.style.width = `${width}px`;
            truncateText(element, lines, ellipsis);
        }
        
        // 初始化
        applyTruncation();
    </script>
</body>
</html>
    

GIF.gif

三、面试回答总结

单行文本溢出
" 对于单行文本溢出,我会使用 white-space: nowrap 防止换行,配合 overflow: hidden 和 text-overflow: ellipsis 显示省略号。这种方法简单直接,兼容性好,但需要确保容器有固定宽度。"

多行文本溢出
" 对于多行文本溢出,有几种解决方案:

  1. 优先使用 -webkit-line-clamp,它简单高效,但仅支持 webkit 内核浏览器。
  2. 对于兼容性要求高的场景,我会使用绝对定位 + 渐变遮罩的方式,通过 CSS 模拟省略号效果。
  3. 如果需要更灵活的控制(如动态调整行数),我会结合 JavaScript 计算文本高度并截断。"

补充说明
" 在实际项目中,我会根据业务需求选择方案。例如移动端可以优先使用 -webkit-line-clamp,而 PC 端则考虑兼容性更好的方案。同时,我也会考虑性能因素,避免在大型列表中使用 JS 动态处理。"

九、手写两栏布局的实现

两栏布局是一种网页设计模式,将页面横向划分为两个主要区域,通常左侧为固定宽度(如导航、侧边栏),右侧为自适应宽度(如主内容区),通过浮动、Flexbox、Grid 等技术实现。

正如下面的效果

image.png

一、浮动(Float)布局

考虑到对Float布局不是很熟悉的各位,给各位推荐一个b站的视频,五分钟即可入门浮动 b23.tv/ucCTDAi

核心原理:通过 float 属性使一侧元素浮动,另一侧自适应宽度。

这里注意,要给浮动元素预留空间,不要让主内容区的内容被浮动内容覆盖

示例代码

<style>
  .container {
    overflow: auto; /* 清除浮动 */
  }
  .sidebar {
    float: left;
    width: 30%;
  }
  .main {
    margin-left: 30%; /* 为浮动元素留出空间 */
  }
</style>
<div class="container">
  <div class="sidebar">侧边栏</div>
  <div class="main">主内容区</div>
</div>

仔细观察上述的代码,可以发现,浮动元素的宽度为(width: 30%;),而主内容区给浮动元素留的宽度也是(margin-left: 30%;),这也是前文提到的‘要给浮动元素预留空间,不要让主内容区的内容被浮动内容覆盖’

优点:兼容性好(IE6+)。
缺点:需要清除浮动,容易出现高度塌陷问题。
适用场景:需要兼容旧浏览器的项目。

二、Flexbox 布局

核心原理:使用 display: flex 实现弹性布局。

flex布局在这里的运用,主要是‘弹性的’占据剩余区域,即flex: 1; 这样就可以实现两栏布局

示例代码

<style>
  .container {
    display: flex;
  }
  .sidebar {
    width: 30%;
  }
  .main {
    flex: 1; /* 占据剩余空间 */
  }
</style>
<div class="container">
  <div class="sidebar">侧边栏</div>
  <div class="main">主内容区</div>
</div>

优点

  • 代码简洁,无需清除浮动
  • 支持响应式调整
  • 垂直居中简单(align-items: center

缺点:IE10+ 支持,旧浏览器不兼容。
适用场景:现代 Web 应用、移动端。

三、Grid 布局

核心原理:使用 display: grid 创建二维网格。

示例代码

<style>
  .container {
    display: grid;
    grid-template-columns: 30% 1fr; /* 两列布局 */
  }
</style>
<div class="container">
  <div class="sidebar">侧边栏</div>
  <div class="main">主内容区</div>
</div>

优点

  • 语法最简洁

  • 支持高级布局(如间距、自动填充)

  • 响应式调整更灵活(grid-template-columns

缺点:IE 不支持,Chrome/Firefox/Safari 完全支持。
适用场景:现代 Web 应用、管理后台。

四、绝对定位(Absolute)布局

核心原理:通过 position: absolute 固定一侧宽度,另一侧自适应。

示例代码

<style>
  .container {
    position: relative;
    height: 100vh; /* 需要指定高度 */
  }
  .sidebar {
    position: absolute;
    left: 0;
    width: 30%;
    height: 100%;
  }
  .main {
    margin-left: 30%;
    height: 100%;
  }
</style>
<div class="container">
  <div class="sidebar">侧边栏</div>
  <div class="main">主内容区</div>
</div>

优点:布局稳定,不受内容影响。
缺点

  • 脱离文档流,可能影响其他元素
  • 需要明确高度(如 height: 100%

适用场景:固定侧边栏的页面(如邮件客户端)。

五、表格布局(Table)

核心原理:使用 display: table 和 table-cell 模拟表格。

示例代码

<style>
  .container {
    display: table;
    width: 100%;
  }
  .sidebar, .main {
    display: table-cell;
  }
  .sidebar {
    width: 30%;
  }
</style>
<div class="container">
  <div class="sidebar">侧边栏</div>
  <div class="main">主内容区</div>
</div>

优点

  • 单元格高度自动匹配
  • 兼容性好(IE8+)

缺点

  • 语义不明确
  • 灵活性差,难以实现复杂布局

适用场景:简单的两栏布局,需要等高效果。

六、响应式实现

★★★ 这里是和其他面试者拉开差距的关键

方法:结合媒体查询(Media Query)实现不同屏幕下的布局变化。

示例代码

.container {
  display: flex;
  flex-direction: column; /* 移动端垂直排列 */
}

@media (min-width: 768px) {
  .container {
    flex-direction: row; /* 桌面端水平排列 */
  }
  .sidebar {
    width: 30%;
  }
  .main {
    flex: 1;
  }
}

优点

  • 适配各种设备屏幕
  • 移动端可优化为单列布局

适用场景:需要响应式设计的项目。

前三个实现方法是关键,是一定要记住的

面试回答总结

" 两栏布局的实现有多种方式,我会根据项目需求和兼容性要求选择合适的方案:

  1. 浮动布局适合需要兼容旧浏览器的场景,但需要处理清除浮动的问题。
  2. Flexbox现代项目的首选,代码简洁且支持灵活的对齐和响应式调整。
  3. Grid最强大的布局方式,特别适合复杂的二维布局,但兼容性稍差。
  4. 绝对定位:适合固定侧边栏的场景,但会脱离文档流。
  5. 表格布局:兼容性好,但语义不明确,灵活性较低。

在实际项目中,我会优先使用 Flexbox 或 Grid,并结合媒体查询实现响应式设计。例如,在移动端将两栏布局转为单列,提升用户体验。"

十、手写三栏布局的实现

三栏布局是前端开发里较为常见的页面结构,它主要包含左、中、右三个部分。

正如下面的实现

image.png

三栏布局稍微比两栏布局复杂一点点,如果看不懂,我推荐一个B站视频 b23.tv/9mLcRjd

浮动实现

左右两栏分别向左/右浮动脱离文档流,中间栏通过外边距(margin)避开浮动元素,形成三栏布局。

<!DOCTYPE html>
<html>
<head>
    <style>
        body {
            margin: 0;
            padding: 0;
        }
        
        .container {
            width: 100%;
        }
        
        .left {
            float: left;
            width: 200px;
            background-color: #f2f2f2;
            height: 300px;
        }
        
        .right {
            float: right;
            width: 200px;
            background-color: #f2f2f2;
            height: 300px;
        }
        
        .main {
            margin-left: 210px;  /* 大于左侧栏宽度 */
            margin-right: 210px; /* 大于右侧栏宽度 */
            background-color: #ccc;
            height: 300px;
        }
        
        .clearfix::after {
            content: "";
            display: table;
            clear: both;
        }
    </style>
</head>
<body>
    <div class="container clearfix">
        <div class="left">左侧栏</div>
        <div class="right">右侧栏</div>
        <div class="main">主要内容</div>
    </div>
</body>
</html>

image.png

Flexbox 实现

此方法利用 Flexbox 的弹性布局能力,能很方便地对列宽和对齐方式进行控制。

<style>
    .container {
        display: flex;
    }
    .left, .right {
        width: 200px;
        background: #f0f0f0;
    }
    .center {
        flex: 1;
        background: #e0e0e0;
    }
</style>
<div class="container">
    <div class="left">左列</div>
    <div class="center">中间列</div>
    <div class="right">右列</div>
</div>

Grid 实现

Grid 布局是专门为二维布局设计的,能够简洁地实现复杂的网格结构。

<style>
    .container {
        display: grid;
        grid-template-columns: 200px 1fr 200px;
    }
    .left, .right {
        background: #f0f0f0;
    }
    .center {
        background: #e0e0e0;
    }
</style>
<div class="container">
    <div class="left">左列</div>
    <div class="center">中间列</div>
    <div class="right">右列</div>
</div>

表格布局实现

该方法把容器当作表格,各列当作表格单元格来进行布局。

<style>
    .container {
        display: table;
        width: 100%;
    }
    .left, .right, .center {
        display: table-cell;
    }
    .left, .right {
        width: 200px;
        background: #f0f0f0;
    }
    .center {
        background: #e0e0e0;
    }
</style>
<div class="container">
    <div class="left">左列</div>
    <div class="center">中间列</div>
    <div class="right">右列</div>
</div>

绝对定位实现

这种方法通过绝对定位来固定左右两列的位置,中间列则利用边距来留出空间。

<style>
    .container {
        position: relative;
        height: 200px;
    }
    .left, .right {
        position: absolute;
        top: 0;
        width: 200px;
        background: #f0f0f0;
    }
    .left {
        left: 0;
    }
    .right {
        right: 0;
    }
    .center {
        margin: 0 200px;
        background: #e0e0e0;
        height: 100%;
    }
</style>
<div class="container">
    <div class="left">左列</div>
    <div class="center">中间列</div>
    <div class="right">右列</div>
</div>

下面的圣杯布局实现和双飞翼布局实现以及被现代技术淘汰,但是对于面试来说,还是有学习的必要的

圣杯布局实现

圣杯布局的特点是中间列优先加载,并且左右两列宽度固定。

通过 浮动 + 负边距(margin-left: -100%) + 相对定位(position: relative)让左右栏“挤”到中间栏两侧,并用父容器的 padding 预留空间

<style>
    .container {
        padding: 0 200px;
    }
    .columns {
        display: flex;
    }
    .center {
        flex: 1;
        order: 2;
        background: #e0e0e0;
    }
    .left {
        width: 200px;
        order: 1;
        margin-left: -100%;
        position: relative;
        right: 200px;
        background: #f0f0f0;
    }
    .right {
        width: 200px;
        order: 3;
        margin-right: -200px;
        background: #f0f0f0;
    }
</style>
<div class="container">
    <div class="columns">
        <div class="center">中间列</div>
        <div class="left">左列</div>
        <div class="right">右列</div>
    </div>
</div>

双飞翼布局实现

中间栏嵌套一层 div,用其 margin 预留左右空间,左右栏仅靠浮动 + 负边距(margin-left)定位,无需相对定位,简化实现。

<style>
    .container {
        overflow: hidden;
    }
    .center {
        float: left;
        width: 100%;
        background: #e0e0e0;
    }
    .center-inner {
        margin: 0 200px;
    }
    .left, .right {
        float: left;
        width: 200px;
        background: #f0f0f0;
    }
    .left {
        margin-left: -100%;
    }
    .right {
        margin-left: -200px;
    }
</style>
<div class="container">
    <div class="center">
        <div class="center-inner">中间列</div>
    </div>
    <div class="left">左列</div>
    <div class="right">右列</div>
</div>

响应式三栏布局实现

这种布局会依据屏幕尺寸自动调整为适合移动端的单列布局。

<style>
    .container {
        display: flex;
        flex-wrap: wrap;
    }
    .left, .right, .center {
        width: 100%;
    }
    @media (min-width: 768px) {
        .left, .right {
            width: 200px;
        }
        .center {
            flex: 1;
        }
    }
</style>
<div class="container">
    <div class="left">左列</div>
    <div class="center">中间列</div>
    <div class="right">右列</div>
</div>

在实际的项目开发中,建议优先考虑使用 Flexbox 或者 Grid 布局,因为它们的代码更简洁,也更容易维护。要是需要兼容旧版本的浏览器,浮动布局或者表格布局也是不错的选择。而圣杯布局和双飞翼布局则适用于需要中间列优先加载的特殊场景。

十一、水平垂直居中的实现方法

1. 行内元素 / 文本居中(单行)

通过将容器的 line-height 值设置为与容器高度相等,使单行文本在垂直方向上自动居中。同时使用 text-align: center 实现水平居中。

<style>
    .center {
        height: 100px;
        line-height: 100px;     /* 高度等于行高 */
        text-align: center;      /* 水平居中 */
        border: 1px solid #ccc;
    }
</style>
<div class="center">单行文本居中</div>

2. 行内元素 / 文本居中(多行)

将容器设置为 display: table-cell,模拟表格单元格的行为。利用 vertical-align: middle 实现垂直居中,配合 text-align: center 实现水平居中。

<style>
    .center {
        display: table-cell;
        width: 200px;
        height: 200px;
        text-align: center;      /* 水平居中 */
        vertical-align: middle;  /* 垂直居中 */
        border: 1px solid #ccc;
    }
</style>
<div class="center">多行文本居中示例<br>第二行文本</div>

3. 块级元素居中(已知宽高)

父元素设置 position: relative 作为定位参考。子元素使用 position: absolute 配合 top: 50% 和 left: 50% 将左上角定位到父元素中心。通过 margin-top 和 margin-left 的负值(各为元素宽高的一半)将元素向上和向左偏移,实现完全居中。

<style>
    .container {
        position: relative;
        height: 300px;
        border: 1px solid #ccc;
    }
    .center {
        position: absolute;
        top: 50%;                /* 顶部偏移50% */
        left: 50%;               /* 左侧偏移50% */
        width: 200px;
        height: 100px;
        margin-top: -50px;       /* 高度的负一半 */
        margin-left: -100px;     /* 宽度的负一半 */
        background: #f0f0f0;
    }
</style>
<div class="container">
    <div class="center">块级元素居中</div>
</div>

★★★ 4. 块级元素居中(未知宽高)

父元素设置 position: relative。子元素使用 position: absolute 和 top: 50%left: 50% 定位到父元素中心。通过 transform: translate(-50%, -50%) 动态将元素自身向上和向左偏移其宽度和高度的 50%,无需知道具体尺寸。

<style>
    .container {
        position: relative;
        height: 300px;
        border: 1px solid #ccc;
    }
    .center {
        position: absolute;
        top: 50%;                /* 顶部偏移50% */
        left: 50%;               /* 左侧偏移50% */
        transform: translate(-50%, -50%); /* 利用transform调整 */
        background: #f0f0f0;
    }
</style>
<div class="container">
    <div class="center">未知宽高元素居中</div>
</div>

5. Flexbox 居中(现代方案)

父元素设置 display: flex 或 display: inline-flex。使用 justify-content: center 实现水平居中,align-items: center 实现垂直居中。
优势:代码简洁,支持响应式布局,是现代前端开发的首选方案。

<style>
    .container {
        display: flex;
        justify-content: center; /* 水平居中 */
        align-items: center;     /* 垂直居中 */
        height: 300px;
        border: 1px solid #ccc;
    }
    .center {
        background: #f0f0f0;
        padding: 20px;
    }
</style>
<div class="container">
    <div class="center">Flexbox居中</div>
</div>

6. Grid 居中(现代方案)

父元素设置 display: grid 或 display: inline-grid。使用 place-items: center 同时实现水平和垂直居中(等同于 justify-items: center 和 align-items: center 的组合)。

<style>
    .container {
        display: grid;
        place-items: center;     /* 水平和垂直同时居中 */
        height: 300px;
        border: 1px solid #ccc;
    }
    .center {
        background: #f0f0f0;
        padding: 20px;
    }
</style>
<div class="container">
    <div class="center">Grid居中</div>
</div>

7. 绝对定位 + 弹性布局混合方案

父元素设置 position: relative。子元素使用 position: absolute 和 inset: 0(等同于 top: 0; right: 0; bottom: 0; left: 0)将元素扩展至父元素边界。通过 margin: auto 使浏览器自动计算并分配上下左右的边距,实现居中。

限制:需明确设置子元素的宽高,否则会填满父容器。

<style>
    .container {
        position: relative;
        height: 300px;
        border: 1px solid #ccc;
    }
    .center {
        position: absolute;
        inset: 0;                /* 等同于top:0;right:0;bottom:0;left:0; */
        margin: auto;            /* 自动计算边距 */
        width: 200px;
        height: 100px;
        background: #f0f0f0;
    }
</style>
<div class="container">
    <div class="center">绝对定位+弹性布局居中</div>
</div>

十二、谈一谈你对Flex的理解

这里给大家贴一篇我之前写过的文章掌握Flex布局:面向小白的Flex全面教程 - 掘金

Flexbox(Flexible Box Layout)是 CSS3 引入的一维布局模型,旨在为容器内的子元素提供弹性的空间分配和对齐方式。它的核心思想是让容器能够自动调整子元素的宽度、高度和排列顺序,以适应不同的屏幕尺寸和设备类型。

一、核心概念

  1. Flex 容器(Flex Container)
    应用 display: flex 或 display: inline-flex 的父元素,它定义了一个 Flex 布局的作用域。

  2. Flex 项目(Flex Items)
    容器内的直接子元素,它们会被 Flex 布局所控制。

  3. 主轴(Main Axis)和交叉轴(Cross Axis)

    • 主轴:由 flex-direction 定义的方向,默认为水平从左到右。
    • 交叉轴:垂直于主轴的方向,默认是垂直方向。

二、容器属性(控制整体布局)

1. flex-direction:定义主轴方向

.container {
  flex-direction: row | row-reverse | column | column-reverse;
}
  • row(默认):水平从左到右。
  • row-reverse:水平从右到左。
  • column:垂直从上到下。
  • column-reverse:垂直从下到上。

2. flex-wrap:控制换行

.container {
  flex-wrap: nowrap | wrap | wrap-reverse;
}
  • nowrap(默认):不换行,元素可能溢出。
  • wrap:换行,新行位于下方。
  • wrap-reverse:换行,新行位于上方。

3. flex-flowflex-direction 和 flex-wrap 的简写

.container {
  flex-flow: row wrap; /* 常用组合 */
}

4. justify-content:主轴对齐方式

.container {
  justify-content: flex-start | flex-end | center | space-between | space-around | space-evenly;
}
  • flex-start(默认):元素靠主轴起点。
  • flex-end:元素靠主轴终点。
  • center:元素居中。
  • space-between:两端对齐,间距平均分配。
  • space-around:每个元素两侧间距相等(边缘间距为中间间距的一半)。
  • space-evenly:所有间距完全相等。

5. align-items:交叉轴对齐方式

.container {
  align-items: stretch | flex-start | flex-end | center | baseline;
}
  • stretch(默认):元素拉伸填充容器高度 / 宽度。
  • flex-start:靠交叉轴起点。
  • flex-end:靠交叉轴终点。
  • center:居中。
  • baseline:元素基线对齐。

6. align-content:多行对齐方式(当存在换行时生效)

.container {
  align-content: stretch | flex-start | flex-end | center | space-between | space-around;
}

三、项目属性(控制单个元素)

1. order:定义元素排列顺序

.item {
  order: 0; /* 默认值,数值越小越靠前 */
}

2. flex-grow:定义元素的扩展比例

.item {
  flex-grow: 1; /* 默认0,不扩展;值为1时平均分配剩余空间 */
}

3. flex-shrink:定义元素的收缩比例

.item {
  flex-shrink: 1; /* 默认1,空间不足时等比例收缩;设为0则不收缩 */
}

4. flex-basis:定义元素在分配空间前的初始大小

.item {
  flex-basis: auto | 200px; /* 默认auto,使用元素自身宽度/高度 */
}

5. flexflex-growflex-shrinkflex-basis 的简写

.item {
  flex: 1 1 auto; /* 默认值 */
  flex: 1; /* 等同于 flex: 1 1 0 */
}

6. align-self:单独定义元素的交叉轴对齐方式

.item {
  align-self: auto | flex-start | flex-end | center | stretch;
}

四、你什么时候会使用Flex布局?

  1. 导航栏

    • 水平排列菜单项,支持响应式折叠。
    • 使用 justify-content: space-between 实现左右对齐。
  2. 卡片布局

    • 等高卡片自动适应容器宽度。
    • 使用 flex-wrap: wrap 和 justify-content: center 实现卡片自动换行和居中。
  3. 垂直居中

    • 使用 align-items: center 和 justify-content: center 快速实现元素的水平垂直居中。
  4. 自适应侧边栏

    • 主内容区域自动扩展,侧边栏固定宽度。
    • 使用 flex: 1 让主内容占满剩余空间。
  5. 响应式表单

    • 标签和输入框在小屏幕上垂直排列,大屏幕上水平排列。
    • 使用 flex-direction: column 和媒体查询实现。
  6. 底部固定页脚

    • 当内容不足时,页脚固定在底部;内容充足时,页脚随内容滚动。
    • 使用 min-height: 100vh 和 flex-direction: column 实现。

五、Flex的优缺点对比

优点

  • 简洁灵活:大幅减少浮动和定位的使用,代码更简洁。
  • 响应式友好:天然支持根据容器尺寸动态调整元素。
  • 对齐能力强:轻松实现水平和垂直居中、等间距分布。
  • 顺序灵活:通过 order 属性可随意改变元素显示顺序。

缺点

  • 兼容性问题:IE10 及以下不支持,需提供降级方案。
  • 二维布局能力有限:对于复杂的网格布局,Grid 更合适。

在向面试官介绍时,一定要介绍flex布局出现的原因,flex的关键参数,flex何时使用以及优缺点

十三、你是否理解BFC,在实际项目中如何运用(不是KFC)

BFC(Block Formatting Context,块级格式化上下文)  是CSS中的一个重要概念,它是页面上的一个独立的渲染区域,规定了内部的块级盒子如何布局,并且与外部区域互不影响。BFC可以看作是一个隔离的容器,容器内的元素布局不会影响到外部的元素。

BFC的特性(可以想象html的特性,因为html也是一个BFC):

  1. 内部盒子垂直排列:BFC内的块级盒子会按照垂直方向一个接一个地放置。
  2. 外边距折叠(Margin Collapse) :属于同一个BFC的两个相邻块级盒子的上下外边距会发生重叠。
  3. 独立布局:BFC的区域不会与浮动元素重叠,且可以包含浮动元素。
  4. 隔离性:BFC内的元素不会影响外部的元素,反之亦然。

如何创建BFC

可以通过以下CSS属性或条件触发BFC的创建:

  1. 根元素(<html> :整个页面默认是一个BFC。
  2. 浮动元素:元素的 float 值不为 none(如 float: left 或 float: right)。
  3. 绝对定位元素:元素的 position 为 absolute 或 fixed
  4. display: inline-block:设置为 inline-block 的元素。
  5. display: table-cell 或 table-caption:表格单元格或表格标题。
  6. overflow 不为 visible:如 overflow: hiddenautoscroll
  7. display: flow-root:专门用于创建BFC的属性(现代浏览器支持,无副作用)。
  8. Flex或Grid的直接子项display: flex 或 display: grid 的容器的直接子元素。

常见应用场景

  1. 清除浮动:父元素创建BFC后可以包含浮动子元素(避免高度塌陷)。

    .parent {
        overflow: hidden; /* 触发BFC */
    }
    
  2. 避免外边距折叠:将相邻元素放入不同的BFC中。

    <div class="bfc">
        <p>第一个段落</p>
    </div>
    <div class="bfc">
        <p>第二个段落</p>
    </div>
    
    .bfc {
        overflow: hidden; /* 创建BFC */
    }
    
  3. 阻止元素被浮动元素覆盖:非浮动元素通过BFC与浮动元素分栏。

    .content {
        overflow: hidden; /* 创建BFC,避免与浮动元素重叠 */
    }
    

在你对实际运用中,一般是如何创建BFC的?

  • 使用 display: flow-root 创建BFC,因为它无副作用(不会隐藏内容或产生滚动条)。

    .container {
        display: flow-root;
    }
    

十四、清除浮动的原因?如何清除?

清除浮动是 CSS 布局中的重要概念,主要用于解决浮动元素导致的父元素高度塌陷问题。

一、为什么需要清除浮动?

当子元素设置 float: left/right 后,会脱离正常的文档流,导致父元素无法计算其高度,出现高度塌陷(父元素高度为 0)。这会影响布局的正常显示,例如:

  • 父元素无法包裹浮动的子元素
  • 后续元素可能会与浮动元素重叠
  • 背景和边框无法正确显示

示例问题代码

<style>
  .parent {
    border: 1px solid red;
  }
  .child {
    float: left;
    width: 100px;
    height: 100px;
    background: #f0f0f0;
  }
</style>
<div class="parent">
  <div class="child"></div>
</div>
<!-- 父元素高度为0,边框无法包裹浮动子元素 -->

image.png

这里父元素并不能包含子元素,因为子元素浮动,而且父元素本身的大小比子元素小(或者没有大小)导致父元素高度塌陷(原本应该由子元素的大小撑开)

有的读者可能会思考——我将父元素的大小设置的比子元素大不就行了吗?

但是在实际的项目中子元素的大小很多情况下是不能确定的,所有清除浮动就很有必要

二、清除浮动的常见方式

1. 使用 clear 属性(传统方法)

在浮动元素后添加一个空元素,并设置 clear: both

<style>
  .parent {
    border: 1px solid #ccc;
  }
  .float {
    float: left;
  }
  .clear {
    clear: both;
  }
</style>
<div class="parent">
  <div class="float">浮动元素</div>
  <div class="clear"></div> <!-- 清除浮动 -->
</div>

缺点:需要额外的 HTML 元素,增加冗余代码。

2. BFC(块级格式化上下文)

通过触发父元素的 BFC 来包含浮动元素。常见方式:

.parent {
  overflow: hidden; /* 触发BFC */
}

原理:BFC 会包含内部所有浮动元素,计算高度时会考虑浮动子元素。

3. 伪元素清除法(推荐)

使用 ::after 伪元素在父元素末尾插入一个清除浮动的元素。

.parent::after {
  content: "";
  display: block;
  clear: both;
}

4. 浮动父元素本身

将父元素也设置为浮动:

.parent {
  float: left; /* 触发BFC */
}

缺点:可能影响后续元素布局,需要再次清除浮动。

5. 设置父元素为表格单元格

.parent {
  display: table-cell;
}

缺点:可能影响宽度和布局特性。

三、各方法对比

方法 优点 缺点 适用场景
空元素 + clear 兼容性好 增加 HTML 冗余 兼容老旧浏览器
overflow:hidden 代码简单 内容溢出会被隐藏 内容不会溢出的容器
display:flow-root 专门为清除浮动设计 IE 不支持 现代浏览器环境
伪元素清除法 无冗余 HTML,兼容性好 需要额外 CSS 类 大多数场景推荐使用
浮动父元素 简单直接 影响后续布局 临时解决方案

四、现代替代方案

在 Flexbox 和 Grid 布局中,浮动的使用场景大幅减少,因为它们会自动包含子元素,无需清除浮动:

.parent {
  display: flex; /* Flex布局 */
  /* 或 display: grid; */
}

清除浮动的核心目标是让父元素正确包含浮动的子元素,避免高度塌陷。在现代前端开发中,优先使用伪元素清除法或BFC 触发,并尽量用 Flexbox/Grid 替代浮动布局,以减少布局复杂度。

十五、position的属性有哪些,区别是什么

推荐一个position的讲解视频 b23.tv/wwag429

CSS 中的position属性用于控制元素在文档中的定位方式,共有五种主要取值:staticrelativeabsolutefixedsticky

一、position: static(默认值)

  • 特性:元素按照正常的文档流布局,toprightbottomleftz-index属性无效。

  • 示例

    .element {
      position: static; /* 默认值,无需显式声明 */
    }
    
  • 应用场景:大多数普通元素默认使用静态定位。

二、position: relative(相对定位)

  • 特性

    • 元素先按照正常文档流布局,然后相对于其正常位置进行偏移。
    • 偏移不会影响其他元素的布局(其他元素仍视为该元素在原位)。
    • 可通过toprightbottomleft调整位置。
    • 会创建新的层叠上下文(stacking context)。
  • 示例

    .element {
      position: relative;
      top: 10px; /* 向下偏移10px */
      left: 20px; /* 向右偏移20px */
    }
    
  • 应用场景:作为绝对定位元素的容器(父元素设置relative)。

为了方便大家了解relative,我提供一个实例,大家可以复制运行

各位,这里不能给实例了,超出最大字符限制了,下面是演示实例,大家凑合看吧

GIF.gif

三、position: absolute(绝对定位)

  • 特性

    • 元素脱离正常文档流,不再占据空间。
    • 相对于最近的已定位祖先元素(即position值为relativeabsolutefixedsticky的元素)定位。
    • 若无已定位祖先元素,则相对于初始包含块(通常是浏览器视口)。
    • 可通过toprightbottomleft精确控制位置。
  • 示例

    .parent {
      position: relative; /* 作为参考容器 */
    }
    .child {
      position: absolute;
      top: 0;
      right: 0; /* 定位到父元素右上角 */
    }
    
  • 应用场景:悬浮菜单、弹出框、绝对定位的图标等。

再给大家提供一个实例,可以复制运行

各位,这里不能给实例了,超出最大字符限制了,下面是演示实例,大家凑合看吧

GIF.gif

四、position: fixed(固定定位)

  • 特性

    • 元素脱离文档流,相对于浏览器视口固定位置。
    • 滚动页面时,元素位置保持不变。
    • 常用于创建导航栏、返回顶部按钮等。
  • 示例

    .navbar {
      position: fixed;
      top: 0;
      left: 0;
      width: 100%; /* 固定在顶部的导航栏 */
    }
    
  • 应用场景:固定导航、悬浮广告、模态框遮罩层。

五、position: sticky(粘性定位)

  • 特性

    • 混合了relativefixed的特性,初始时按正常文档流布局。
    • 当滚动到特定位置时(通过toprightbottomleft指定阈值),变为相对于视口固定。
    • 必须指定至少一个偏移值(如top: 0)才能生效。
  • 示例

    .sidebar {
      position: sticky;
      top: 20px; /* 滚动到距离视口顶部20px时固定 */
    }
    
  • 应用场景:滚动时固定的侧边栏、标题栏。

六、关键区别对比表

属性值 定位参考对象 是否脱离文档流 滚动时表现
static 正常文档流 随文档滚动
relative 元素自身正常位置 随文档滚动
absolute 最近的已定位祖先元素 随祖先元素滚动(若祖先固定则不动)
fixed 浏览器视口 固定不动
sticky 正常文档流(滚动到阈值后固定) 初始滚动,达到阈值后固定

十六、实现一个三角形

这一题看似是考察我们使用css画图的能力,但是实时却不是这样,这题考察的是css中的border特性

在 CSS 中实现三角形主要基于边框(border)的特性。当元素的宽度和高度为 0 时,其边框会被相邻边框均分,形成一个由边框组成的多边形。通过调整边框的宽度和颜色,可以轻松创建各种三角形。

一、实现原理

边框的本质

每个元素的边框实际上是由四个梯形组成的,当元素宽度和高度为 0 时,梯形退化为三角形。

二、实现代码

1. 向上的三角形

<style>
  .triangle-up {
    width: 0;
    height: 0;
    border-left: 50px solid transparent;
    border-right: 50px solid transparent;
    border-bottom: 100px solid red; /* 底边显示颜色 */
  }
</style>
<div class="triangle-up"></div>

屏幕截图 2025-07-11 192350.png

2. 向下的三角形

<style>
  .triangle-down {
    width: 0;
    height: 0;
    border-left: 50px solid transparent;
    border-right: 50px solid transparent;
    border-top: 100px solid blue; /* 顶边显示颜色 */
  }
</style>
<div class="triangle-down"></div>

屏幕截图 2025-07-11 192357.png

3. 向左的三角形

<style>
  .triangle-left {
    width: 0;
    height: 0;
    border-top: 50px solid transparent;
    border-bottom: 50px solid transparent;
    border-right: 100px solid green; /* 右边显示颜色 */
  }
</style>
<div class="triangle-left"></div>

屏幕截图 2025-07-11 192404.png

4. 向右的三角形

<style>
  .triangle-right {
    width: 0;
    height: 0;
    border-top: 50px solid transparent;
    border-bottom: 50px solid transparent;
    border-left: 100px solid purple; /* 左边显示颜色 */
  }
</style>
<div class="triangle-right"></div>

屏幕截图 2025-07-11 192410.png

5. 任意角度的三角形

通过调整各边的宽度比例,可以创建不同角度的三角形:

<style>
  .triangle-custom {
    width: 0;
    height: 0;
    border-left: 30px solid transparent;
    border-right: 70px solid transparent;
    border-bottom: 100px solid orange;
  }
</style>
<div class="triangle-custom"></div>

屏幕截图 2025-07-11 192416.png

最后给大家说明一下代码中可能困惑的地方

transparent是使不需要的边框透明,只有需要显示的边框我们才设置颜色

十七、如何解决1px 问题?

为何会产生1px问题?何为1px问题?

首先我们要明确三个概念,一个使逻辑像素,一个是物理像素,还有一个设备像素比(DPR)

  1. CSS 像素(逻辑像素)
    前端开发中使用的单位(如1px),是抽象的逻辑单位,用于描述元素的尺寸和位置。
  2. 物理像素
    设备屏幕实际的物理显示单元,例如 iPhone 13 的分辨率为 2532×1170 像素。
  3. 设备像素比(DPR)
    DPR = 物理像素 / CSS像素
  • 普通屏幕:DPR = 1,1 个 CSS 像素对应 1 个物理像素。
  • Retina 屏:DPR = 2 或 3,1 个 CSS 像素对应 4 个(2×2)或 9 个(3×3)物理像素。

由此我们可以得出当 CSS 中设置border: 1px时:

  • 在 DPR=1 的屏幕上,显示为 1 个物理像素宽。
  • 在 DPR=2 的屏幕上,实际渲染为 2 个物理像素宽,视觉上变粗(例如 iOS 设备)。
  • 在 DPR=3 的屏幕上,渲染为 3 个物理像素宽,更粗(例如部分 Android 设备)。

现在我们可以区解决1px问题了

1. 媒体查询 + transform 缩放(推荐)

通过检测设备像素比(DPR),使用伪元素和缩放创建精确的 1px 边框。

.border {
  position: relative;
}
/* 1倍屏 */
@media (-webkit-min-device-pixel-ratio: 1), (min-device-pixel-ratio: 1) {
  .border::after {
    content: '';
    position: absolute;
    left: 0;
    top: 0;
    width: 100%;
    height: 1px;
    background: #000;
  }
}
/* 2倍屏 */
@media (-webkit-min-device-pixel-ratio: 2), (min-device-pixel-ratio: 2) {
  .border::after {
    transform: scaleY(0.5);
    transform-origin: 0 0;
  }
}
/* 3倍屏 */
@media (-webkit-min-device-pixel-ratio: 3), (min-device-pixel-ratio: 3) {
  .border::after {
    transform: scaleY(0.333);
    transform-origin: 0 0;
  }
}

优点:精确控制物理像素,兼容性好(IE9+)。
缺点:代码量较大,需为每个方向的边框单独设置。

2. viewport 缩放(整页解决方案)

根据设备像素比动态调整 viewport 的缩放比例:

const scale = 1 / window.devicePixelRatio;
document.write(`<meta name="viewport" content="width=device-width, initial-scale=${scale}, maximum-scale=${scale}, minimum-scale=${scale}, user-scalable=no">`);

优点:一劳永逸,所有 1px 问题自动解决。
缺点:需配合 rem/em 布局,可能影响第三方组件。

3. box-shadow 模拟边框

利用阴影的扩散特性模拟极细边框:

.border {
  box-shadow: 0 0 0 1px #000; /* 1px边框 */
}

优点:代码简单,兼容性好。
缺点:阴影可能模糊,无法精确控制单边。

4. SVG 边框(现代方案)

使用内联 SVG 定义精确的 1px 边框:

.border {
  border: 1px solid transparent;
  background-origin: border-box;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1' height='1'%3E%3Crect x='0' y='0' width='1' height='1' fill='%23000'/%3E%3C/svg%3E");
}

优点:精确控制物理像素,支持各种形状。
缺点:代码复杂,IE 不支持。

5. CSS 渐变(适合单边边框)

使用线性渐变创建精确的 1px 线条:

.border-bottom {
  background: linear-gradient(to bottom, #000, #000 1px, transparent 1px) no-repeat;
  background-size: 100% 1px;
  background-position: bottom;
}

优点:简单灵活,兼容性好。
缺点:只能模拟单边边框。

三、各方案对比

方法 优点 缺点 兼容性
transform 缩放 精确控制,支持圆角 代码复杂,需多方向处理 现代浏览器
viewport 缩放 全局生效,简单直接 影响整体布局 所有浏览器
box-shadow 实现简单 效果模糊,不支持圆角 所有浏览器
SVG 边框 精确控制,支持复杂形状 代码复杂 IE 不支持
CSS 渐变 简单灵活 只能单边,不支持圆角 现代浏览器

那么这些方案有没有什么弊端?

  1. 圆角问题:transform 缩放方案可能导致圆角在某些浏览器中显示不流畅。
  2. 性能影响:大量使用伪元素或 transform 可能影响渲染性能。
  3. 混合方案:复杂项目中可能需要结合多种方案(如 viewport 缩放 + 局部 transform)。

优先考虑 viewport 缩放或 transform 缩放方案。

相信大家看到这里的时候还是会有一些疑问的,诸如——

1.1px问题只会影响到边框的设置吗,不会影响字体等其他的效果吗 2.只有1px时会出现这种问题吗?2px时会有给个问题吗?

下面回答一下这些疑问

1. 关于1px问题的影响范围

1px问题主要影响边框、细线和微小UI元素,但基本不影响字体。这是因为:

  • 边框/线条是纯色块渲染,高DPR下1CSS像素直接映射为多个物理像素时,浏览器无法智能优化,导致过粗或模糊;
  • 字体则自带抗锯齿和次像素渲染技术,操作系统和浏览器会动态调整显示方式,确保在不同DPI下保持视觉一致性。不过,极端情况下(如极小字号或1px文字描边)仍可能受影响。

2. 关于2px是否会出现类似问题

2px不会出现典型的"1px问题" ,原因在于:

  • 物理像素对齐:2px的宽度足够大,在高DPR设备(如DPR=2)下会直接渲染为4物理像素,浏览器不会触发抗锯齿优化,显示清晰且稳定;
  • 视觉容忍度:即使DPR=3时2px变为6物理像素,其粗细变化是线性且可预测的,而1px的细微偏差(如虚边或半像素渲染)更容易被察觉。因此,只有1px会因渲染策略不一致引发显著问题,2px及以上则无此困扰。

结语

CSS面试上篇就截取到这里了,上篇的考点是比较高频的考点。

CSS面试下篇也会尽快发出来

JavaScript事件循环深度解析:理解异步执行的本质

引言

你是否好奇过这段代码的执行顺序?为什么Promise总是比setTimeout先执行?让我们通过实际代码来揭开JavaScript事件循环的神秘面纱。

什么是Event Loop(事件循环)

**Event Loop(事件循环)**是JavaScript的执行机制,也是代码执行的开始。它是JavaScript引擎处理异步操作的核心机制,解决了单线程语言如何处理并发任务的问题。

Event Loop的本质

事件循环机制让JavaScript能够:

  • 非阻塞执行:处理耗时操作而不冻结用户界面
  • 任务调度:合理安排同步和异步任务的执行顺序
  • 性能优化:通过任务优先级实现高效的资源利用

Event Loop的工作原理

flowchart.png

关键特点

  • 循环执行:事件循环会不断重复这个过程
  • 阶段性处理:每个阶段处理特定类型的任务
  • 微任务优先:每个阶段结束后都会清空微任务队列

核心概念:单线程的智慧

JavaScript为什么是单线程?

// 一个 script 就是一个宏任务开始
// 同步任务
// js 调用栈
// cpu 计算

正如注释所说,JavaScript采用单线程设计有其深刻原因:

  • DOM操作安全:避免多线程同时修改DOM造成冲突
  • 简化编程模型:无需考虑线程同步和锁机制
  • 保证执行顺序:代码按预期顺序执行

单线程的执行策略

同一时刻只做一件事,但JavaScript通过巧妙的任务调度实现了高效执行:

同步任务优先级

  • 同步任务要尽快执行完
  • 渲染页面(重绘重排)
  • 响应用户的交互(优先)

耗时性任务的处理

  • setTimeout/setInterval - 定时器任务
  • fetch/ajax - 网络请求
  • eventListener - 事件监听

这些耗时任务被放入任务队列,避免阻塞主线程。

实战解析一:基础事件循环

让我们分析第一个核心示例:

// 同步任务
console.log('script start');

// 异步任务,宏任务 任务队列
setTimeout(()=>{
    console.log('setTimeout');
},0)

// .then 异步 微任务
// static 静态方法
Promise.resolve().then(()=>{
    console.log('promise');
})

// 同步任务
console.log('script end');

执行结果分析

输出顺序

script start
script end
promise
setTimeout

为什么是这个顺序?

  1. 同步代码优先script startscript end 立即执行
  2. 微任务抢跑Promise.then() 是微任务,优先级高于宏任务
  3. 宏任务排队setTimeout 是宏任务,最后执行

关键理解点

注释中提到的"异步任务 耗时性的任务 任务放到哪个地方?",答案是:

宏任务队列(script脚本就是宏任务):

  • setTimeout/setInterval - 定时器
  • fetch/ajax - 网络请求
  • eventListener - 事件监听
  • I/O操作

微任务队列(紧急的,优先的,同步任务执行完后的补充):

  • Promise.then() - Promise回调
  • MutationObserver - DOM变化监听
  • queueMicrotask - 手动微任务
  • process.nextTick() - Node.js专用

⚠️ 重要注意:Promise构造函数本身是同步执行的,只有Promise的.then().catch().finally()回调才是微任务!

实战解析二:多个Promise的执行

console.log('script start');

const promise1 = Promise.resolve('First Promise');
const promise2 = Promise.resolve('Second Promise');
const promise3 = new Promise(resolve => {
    resolve('Third Promise');
})

promise1.then(value => console.log(value));
promise2.then(value => console.log(value));
promise3.then(value => console.log(value));

setTimeout(()=>{
    console.log('下一把再相见');
},0)

console.log('同步end')

执行结果分析

输出顺序

script start
同步end
First Promise
Second Promise
Third Promise
下一把再相见

深度理解

  • 所有Promise.then都是微任务,会在当前宏任务结束后批量执行
  • 微任务队列会完全清空后才执行下一个宏任务
  • 这就是为什么所有Promise都在setTimeout之前执行

实战解析三:Promise构造函数的陷阱

var p4 = new Promise((resolve,reject)=>{
    // 宏任务
    // 先执行
    setTimeout(()=>{
        resolve(1000)// 将第一个 .then 的回调 放到微任务队列中
        console.log('哈哈哈')
    },0)
})

p4.then((res)=>{
    // 执行第一个.then 的回调 
    console.log('1')// 1
    console.log(res)// 1000
    console.log('2')// 2
}).then(()=>{
    console.log('第二次then')
}) 

执行结果分析

输出顺序

哈哈哈
1
1000
2
第二次then

关键理解

  1. Promise构造函数同步执行:setTimeout立即被调度
  2. resolve触发时机:只有当setTimeout执行时,resolve才被调用
  3. then链式执行:第一个then执行完后,立即执行第二个then

注释"将第一个 .then 的回调 放到微任务队列中"精准地说明了resolve的作用机制。

实战解析四:Node.js环境的特殊性

console.log('Start');
// node 微任务
// process 进程对象
process.nextTick(() => {
  console.log('Process Next Tick');
})
// 微任务
Promise.resolve().then(() => {
  console.log('Promise Resolved');
})
// 宏任务
setTimeout(() => {
  console.log('haha');
  Promise.resolve().then(() => {
    console.log('inner Promise')
  })
}, 0)
console.log('end');

执行结果分析

Node.js输出顺序

Start
end
Process Next Tick
Promise Resolved
haha
inner Promise

Node.js特殊规则

  • process.nextTick 优先级最高,甚至高于Promise.then
  • 这是Node.js独有的微任务,浏览器环境没有

实战解析五:DOM操作与微任务

// event loop 是JS 执行机制,也是代码执行的开始
// html 是第一个BFC 块级格式化上下文 
const target = document.createElement('div');
document.body.appendChild(target);
const observer = new MutationObserver(() => {
  console.log('微任务: MutationObserver');
})
// 监听target 节点的变化
observer.observe(target, {
  attributes: true,
  childList: true,
})

target.setAttribute('data-set', '123');
target.appendChild(document.createElement('span'));
target.setAttribute('style', 'background-color: green;');

执行结果分析

输出

微任务: MutationObserver

深度理解

  • MutationObserver会批量监听DOM变化
  • 多次DOM操作只触发一次回调
  • 在页面渲染前执行,性能优化的关键

注释"DOM 改变在页面渲染前 拿到DOM 有啥改变"完美解释了MutationObserver的价值:

  • 时机优势:在DOM更新后、页面渲染前执行
  • 性能优化:避免多次重绘重排
  • 批量处理:一次性获取所有DOM变化信息
  • 应用场景:组件库的响应式更新、性能监控

实战解析六:queueMicrotask的妙用

console.log('script start');
// 批量更新
// dom树 cssom layout 树 图层合并

queueMicrotask(()=>{
    // DOM 更新了,但不是渲染完了
    // 一个元素的高度 offsetHeight scrollTop
    // 立即重绘重排 耗性能
    console.log('微任务:queueMicrotask');
})

console.log('script end');

执行结果分析

输出顺序

script start
script end
微任务:queueMicrotask

性能优化关键

  • 在DOM更新后、页面渲染前执行
  • 避免"立即重绘重排 耗性能"的问题
  • 批量处理DOM操作的最佳时机

核心规律总结

基于所有代码示例的分析,事件循环遵循以下铁律:

执行优先级

  1. 同步代码 - 立即执行
  2. 微任务队列 - 批量清空
  3. 宏任务队列 - 逐个执行

微任务优先级(Node.js)

  1. process.nextTick - 最高优先级
  2. Promise.then - 标准微任务
  3. MutationObserver - DOM相关微任务
  4. queueMicrotask - 手动微任务

实际应用价值

性能优化

  • 使用微任务进行DOM批量更新
  • 避免不必要的重绘重排
  • 合理调度异步任务

代码调试

  • 理解异步代码的执行时机
  • 预测代码输出顺序
  • 解决时序相关的bug

结语

通过对这些实际代码的深度解析,我们不仅理解了事件循环的执行机制,更重要的是掌握了为什么会产生这样的执行结果。每一行注释都蕴含着深刻的理解,每一个输出顺序都有其必然的逻辑。

掌握事件循环,就是掌握了JavaScript异步编程的精髓。在实际开发中,这些知识将帮助你写出更高效、更可预测的代码。

在生产环境下,你真的有考虑到使用数组方法的健壮性吗?

场景

在一个风和日丽的清晨,刚进公司我就看到测试小哥眉头紧锁,疯狂的在工位上面摆弄自己的鼠标。突然抬头看到我进来,急急忙忙的说:火锅哥,快过来搂一眼!正式环境上突然图表的数据没了。昨天还好好的,今天早上领导突然要看效果,把链接一打开进入数据大屏就不行了。

看着测试要背锅的样子,只能帮忙排查一下问题呗! 如果是没有数据,那咱就先抓包看一下~

但是看到网络请求里面,后端所有数据都是正常返回的,为什么没渲染呢? 难道是前端代码报错导致打断了渲染?

咱从网络又切换到了控制台~ 好家伙果然是报错了。

image.png

然后马上打开了vscode,找到了这行代码的逻辑,如下:

image.png

原因就是res是后端接口的返回,按正常情况应该返回一个Array数组,结果实际返回了一个"null",那自然而然就JS报错了呗!

于是乎找到后端大佬说明了情况,谁知大佬这天来了大姨妈,心情及其暴躁~ 吼着说: 逻辑不会永远按正常情况返回,你前端不做代码健壮性处理?而且即使我返回的是null,与下面渲染逻辑有什么关系吗? 你该渲染还是渲染啊!

听到大佬这么回答,顿时我与测试都陷入了沉默。(JS的报错确实会打断后续逻辑的交付,大佬这么讲也没什么毛病)

测试:火锅哥,你改一下吧! 不然老板怪我没测出来,帮你点杯蜜雪呗!

我:行吧行吧~ 反正加一行判断的事~

image.png

改完准备发版的时候,后端大佬又来了一句,不只这里可能会返回null,其他的接口你也注意看一下哦!

听到大佬的这话,我彷佛天都要塌~ 那我的这个工作量太大了!每个都要去加判断呀,由于当时项目急着上线,很多这种接口的地方,都没做容错处理。

想到以后所有的项目都是找大佬对接接口,如果每次写代码都这样写,或者有稍微一不注意漏掉的地方,下次测试就直接给我提BUG了,这点逼绩效全部都要扣完。

于是乎我决定造一个轮子工具,针对处理数组的轮子工具,即使类型不是数组,也不能出现报错打断的情况。让逻辑继续走,顶多报一个警告提示出来,后续再处理!

safe-array-utils诞生,源码如下:


class SafeArrayWrapper {
  #array;
  constructor(input, error) {
    let array = [];
    const rawType = Object.prototype.toString.call(input);
    //首先做类型的判断:
    //1.如果是数组就不管,直接赋值
    //2.如果是类数组,就直接转换成数组
    //3.如果是其他类型,就直接抛警告 
    //4.最后不是数组类型的数据,全部赋值为空数组
    if (Array.isArray(input)) {
      array = input;
    } else if (
      rawType === '[object Arguments]' ||
      (typeof input === 'object' && input !== null && 'length' in input)
    ) {
      try {
        array = Array.from(input);
      } catch (e) {
        console.warn(`[SafeArray] 类数组转换失败`);
      }
    } else {
      console.warn(`[SafeArray] 输入不是合法数组或类数组。类型:${rawType},定位标记:${error || '无'}`);
    }

    this.#array = array;
  }
  

  //下面就是针对数组原型上面一些方法的操作了,保留原数组原型方法的特性

  static chainableMethods = ['map', 'filter', 'slice', 'concat', 'flat', 'flatMap', 'reverse', 'sort'];
  static terminalMethods = [
    'join', 'reduce', 'find', 'findIndex', 'includes',
    'indexOf', 'lastIndexOf', 'every', 'some', 'at',
    'toString', 'toLocaleString'
  ];

  static allowedMethods = [...this.chainableMethods, ...this.terminalMethods, 'value'];

  static #proxyMethod(methodName) {
    return function (...args) {
      const target = this.#array;

      if (typeof target[methodName] !== 'function') {
        console.error(`数组不存在名为 "${methodName}" 的方法`);
        return this;
      }

      const result = Array.prototype[methodName].apply(target, args);

      if (SafeArrayWrapper.chainableMethods.includes(methodName)) {
        return new SafeArrayWrapper(result).#array;
      } else {
        return result;
      }
    };
  }

  value() {
    return [...this.#array];
  }

  static {
    for (const method of this.allowedMethods) {
      this.prototype[method] = this.#proxyMethod(method);
    }
  }
}

function SafeArray(input, error) {
  return new SafeArrayWrapper(input, error);
}

export default SafeArray;

其实整个轮子的核心逻辑就是,类型是数组就还是走数组方法的操作,类型不是数组就抛出警告,同时把传入的值赋值为空数组。

同时为了使用方便,我把它打成了一个npm包,包名就叫:safe-array-utils

1.正常用法:

 import SafeArray from 'safe-array-utils'
 let arr = [1,2,3,4]
 
 //与使用数组方法一样,只是把数组包裹一层函数调用
 SafeArray(arr).forEach(el=>{
    console.log(el)
 })
 let newArr = SafeArray(arr).map(el=>{
    return el * 2
 })
 
 //同时也支持链式调用
   let arr2 = [1,2,3,4,undefined]
  let newArr = SafeArray(arr2).filter(_=>_).map(el=>{
    return el * 2
 })
 
 //包括其它的数组方法,如
 let arr2 = [1,2,3,4]
 SafeArray(arr2).at(1)
 SafeArray(arr2).join('-')
 

2.如果传入错误类型


//即使传的类型不对,也不会报错了。更不会打断下面的逻辑
 let newArr = SafeArray('null').map(el=>{
    return el * 2
 })
 console.log(newArr) // 空数组 []
 console.log('正常打印')
 

image.png

3.假如页面多处地方用到

    //可以传第二个参数,加一个标记定位
    SafeArray('null','第一处').map(el => {
      return el * 2
    })
    SafeArray('null','第二处').map(el => {
      return el * 2
    })
    SafeArray('null','第三处').map(el => {
      return el * 2
    })
  

image.png

怎么样,兄弟们~ 没有什么事是不能用造1个轮子解决的。如果有,那就造2个!

总结:

还是回到最初的问题,即使后端没按照数组的方式返回结果,我们也能正常走后续的逻辑,从而不会因为页面报错,影响页面上的其他功能。最后我想请问家人们你们平时写代码会注重代码的健壮性吗? 我想如果是项目工期短,时间紧。会很少有人关注这一块,甚至现在还有很多小公司不会去做单元测试。如果该文章对你有帮助,就请点赞+收藏吧!如果你们项目中也遇到这种情况。你会使用哪种方式去做呢?欢迎留言讨论~

从点击到执行:如何优雅地控制高频事件触发频率

从点击到执行:如何优雅地控制高频事件触发频率

在网页开发中,某些事件(如窗口调整大小、滚动、键盘输入或鼠标移动)可能会频繁的被触发。如果每次事件触发都执行相应的处理函数,尤其是这些函数涉及复杂的计算或网络请求时,会导致性能问题,甚至可能使页面变得缓慢或无响应。为了解决这个问题,通常会采用两种技术:防抖(Debouncing)节流(Throttling)

一、防抖

防抖的目的是确保某个函数在短时间内不会被频繁调用。只有在停止触发一段时间后才执行一次。就是说事件停止促发后并过了设置的时间。适用于搜索框的输入、窗口大小调整、表单提交按钮防止双击。

实现原理:当一个事件发生时,设置一个定时器,在指定的时间间隔后执行特定的函数。如果在这个时间间隔内该事件再次被触发,则重置定时器。这意味着只有当事件停止触发超过设定的时间后,函数才会被执行。

  • 以键盘输入为例看看不做防抖是什么情况

    每次按键都会立即触发 console.log(this.value);,如果用户快速输入(如输入 "hello"),console.log 会被调用 5 次,它进行了多次无意义的执行。

        <input type="text" id="test">
        <script>
            document.getElementById('test').addEventListener('keyup',function(){
                console.log(this.value);
            })
        </script>
    

    未防抖.gif

  • 防抖实现

    当用户在输入框(<input id="test">)中键入内容时,每次按键释放(keyup 事件)都会触发事件监听器。该监听器获取输入框的当前值(e.target.value)并传递给 debouncedDoWhat 函数。

    debouncedDoWhat 是一个经过防抖处理的函数,由 debounce(doWhat, 3000) 生成。防抖的核心逻辑是:

    1. 延迟执行:每次调用 debouncedDoWhat 时,它会检查是否已有待执行的定时器(timer)。如果有,则清除之前的定时器,重新开始计时。
    2. 稳定后执行:只有在用户停止输入 3 秒(3000ms) 后,才会真正调用 doWhat 函数,并打印输入框的最新值。
        <input type="text" id="test" />
        <script>
          document.getElementById("test").addEventListener("keyup", (e) => {
            debouncedDoWhat(e.target.value);
          });
    
          const debouncedDoWhat = debounce(doWhat, 3000);
    
          function doWhat(value) {
            console.log(value);
          }
    
          function debounce(fn, delay) {
            let timer = null;
            return function (...args) {
              const context = this;
              if (timer) {
                clearTimeout(timer);
              }
              timer = setTimeout( ()=> {
                fn.apply(context, args);
              }, delay);
            };
          }
        </script>
    

    防抖后的效果:

    防抖.gif

    关于上述代码this指向问题

    const debouncedDoWhat = debounce(doWhat, 3000);可知debouncedDoWhat实际是就是debounce返回的闭包函数。闭包函数中执行const context = this;捕获的是debouncedDoWhat调用时的this,而debouncedDoWhat是作为普通函数被调的,所以此时捕获的this指向window。在定时器中的函数是箭头函数,它的this指向的是返回函数也就是debouncedDoWhat,所以这里可以不使用context来保存this。最后执行fn.apply(context, args);将fn的this指向debouncedDoWhat指向的this。

          function debounce(fn, delay) {
            let timer = null;
            return function (...args) {
              if (timer) {
                clearTimeout(timer);
              }
              timer = setTimeout(()=> {
                fn.apply(this, args);
              }, delay);
            };
          }
    

二、节流

节流的目的是使函数在一段时间内触发一次,也就是说在规定的时间间隔内最多只执行一次事件处理函数,即使在这段时间内事件被多次触发。适用于滚动加载、鼠标移动等事件。

  • 防抖实现

    代码实现的功能是在一段时间内( 5秒)只执行一次函数,即使事件被频繁触发。

        <div>
          <input type="text" id="inputC" />
        </div>
        <script>
          function throttle(fn, delay) {
            let last; // 存储上一次函数成功执行的时间戳
    
            let deferTimer; // 存储 setTimeout 返回的 ID,用于清除定时器
    
            return function (...args) {
              let that = this; // 保存 this 上下文,确保在 setTimeout 中也能正确访问 this
    
              let now = +new Date(); // 获取当前时间戳(+new Date() 是一种快速获取时间戳的方式)
    
              // 判断是否在节流周期内
              if (last && now < last + delay) {
                clearTimeout(deferTimer); // 如果还在限制时间内,则清除之前的定时器,并重新设置新的定时器
                deferTimer = setTimeout(function () {
                  // 当定时器触发时,更新 last 时间为当前时间
                  last = now;
    
                  // 执行原始函数,并传递参数
                  fn.apply(that, [...args]);
                }, delay); // 延迟执行到下一个周期开始
              } else {
                // 如果是第一次触发或者不在节流周期内,直接执行函数
    
                // 更新 last 为当前时间
                last = now;
                fn.apply(that, [...args]);// 执行原始函数,并传递参数
              }
            };
          }
          
    
          // 使用示例
          document.getElementById("inputC").addEventListener(
            "keyup",
            throttle(function (e) {
              console.log(e.target.value);
            }, 5000)
          );
        </script>
    

    代码的执行逻辑

    1. 调用throttle函数后还会返回一个闭包函数,并形成一个闭包,其中的自由变量last记录的是上一层原始函数执行的时间。deferTimer记录的是定时器id,用于清除定时器

    2. 执行返回的闭包函数,首先保存当前上下文that并获取当前时间戳now

    3. 判断是否在节流周期内,若是第一次触发(last为undefined时)则直接执行原始函数。若不是第一次触发事件则进入if中

    4. 在if中首先清除上一次的定时器并设置新的定时器,在延迟时间(delay)后执行:

      在定时器中更新last为当前时间并使用apply调用原始函数,确保正确的this和参数传递

    总的来说就是

    • 若是事件是第一次触发则直接走else线执行一次,并设置上一次的执行时间点。

    • 若事件一直触发那么代码走的一直都是if线,这个定时器也一直处于刷新的状态而不会执行里面的原始函数。所以last一直都是上一次执行时的last,随着时间的推进会有last + delay < now,也就是说距离上一次函数执行已经过去了delay,这时候就会走else线执行原始函数

    • 若事件一直触发了几次之后停止触发(此时last + delay < now),由于存在一个定时器,这个定时器会在触发停止的delay时间后执行原始函数

      这里最后一次会延迟执行,也就是说这次执行到上一次执行的时间间隔大于delay,可以修改定时器的定时时间来解决

                  deferTimer = setTimeout(function () {
                    // 当定时器触发时,更新 last 时间为当前时间
                    last = now;
      
                    // 执行原始函数,并传递参数
                    fn.apply(that, [...args]);
                  }, delay+delay-now); // 距离下一次执行时间越近越小,直到为0
      

    运行效果

    节流.gif

面试官:说说 startTransition 和 useDeferredValue?我:我用它一行代码救了首页!

👨‍🏫 本系列由前端面试真题博主 Kincy 发起,每日更新一题,通勤路上轻松掌握高频知识点。

📢 如果你想第一时间获取更新,或与群友交流面试经验、内推信息,欢迎加入微信群(文末)!

🧠 目录导航:

  1. 🚀 这两个 API 到底干嘛的?(基本用法)
  2. 🔍 背后机制长什么样?(原理解析)
  3. 🧬 React 源码揭秘(调度器 Scheduler)
  4. 🛠 实战例子:让搜索不卡顿只需一行代码!
  5. 📌 总结一句话记住它们
  6. 🔮 明日预告:React 并发渲染是怎么调度的?

1️⃣ 🚀 它们是干嘛的?—— 基本用法来一发!

🔸 startTransition(fn):标记一个“可中断的更新”

适用于 非紧急更新,如输入后搜索、自动补全、筛选等 UI 延迟。

import { startTransition } from 'react';

const handleChange = (e) => {
  const value = e.target.value;
  setInput(value);

  // 让搜索这部分是“可打断的”
  startTransition(() => {
    setSearchQuery(value);
  });
};

🔸 useDeferredValue(value):将“值”变成低优先级的“影分身”

const deferredSearchQuery = useDeferredValue(searchQuery);

// 比如你传入一个搜索词,它返回的值会“稍微延迟更新”,保持界面流畅

2️⃣ 🔍 原理解析:React 是怎么安排这些更新的?

🧭 React 更新有两个等级:

  • 紧急更新:用户输入、点击(立刻响应)
  • 可延迟更新:不影响交互的内容(比如重新渲染列表)

startTransition() 就是告诉 React:

“嘿,这段更新你可以放轻松,用户不会介意它慢一点。”

useDeferredValue() 则是让某个值更新自动变慢,你不需要包一层 startTransition()

📊 类比对比:

场景 不使用 使用 startTransitionuseDeferredValue
用户输入后触发重计算 输入卡顿,页面掉帧 输入丝滑,计算稍后完成
大量 setState 立即触发,影响流畅度 分批处理,用户无感知

3️⃣ 🧬 源码味来了:React 怎么实现的调度?

🎯 React 使用 Scheduler 管理任务优先级

  • startTransition 的实质:包一层优先级更低的更新任务

  • 内部设置了一个叫 TransitionLane 的“通道”,优先级低于用户交互

    // 源码中类似这样的调度逻辑
    scheduleUpdateOnFiber(fiber, lane, eventTime);
    
  • useDeferredValue 本质上是用 useTransition 的简化封装,延迟 state 的传递与触发

    // 它背后其实也是用 transition lane 来挂更新任务
    deferValue = scheduleCallback(NormalPriority, () => setState(value));
    

📌 如果你看过调度系统源码,会知道 React 会根据 Lane 的优先级决定哪个任务先执行,哪个稍后执行,这就是并发特性核心。

4️⃣ 🛠 实战演练:一行代码让你的搜索不卡顿!

假设你写了一个搜索框,输入后实时过滤 1w 条数据:

function App() {
  const [input, setInput] = useState('');
  const [list, setList] = useState(bigDataSet);

  const handleChange = (e) => {
    const value = e.target.value;
    setInput(value);

    // 卡顿爆炸
    setList(bigDataSet.filter(item => item.includes(value)));
  };
}

🔥 使用 startTransition,丝滑升级:

const handleChange = (e) => {
  const value = e.target.value;
  setInput(value);

  startTransition(() => {
    setList(bigDataSet.filter(item => item.includes(value)));
  });
};

🪄 或者用 useDeferredValue

const deferredInput = useDeferredValue(input);
const filteredList = useMemo(
  () => bigDataSet.filter(item => item.includes(deferredInput)),
  [deferredInput]
);

5️⃣ 📌 总结一句话:

startTransition(fn) 是手动告诉 React:“这事儿不急”
useDeferredValue(val) 是自动让某个值 “延迟变动”

它们都基于 React 并发更新的“优先级调度”,本质是把不重要的 UI 更新往后排一排

6️⃣ 🔮 明日预告:React 的 Lane 模型到底是什么?

你知道 React 为什么能“同时处理多个更新”却又不会乱吗?

这一切的核心就在于 —— Lane 模型

它就像高速公路的“车道系统”,不同类型的更新(比如点击、输入、动画、数据加载)各走不同车道,React 再根据“优先级”来安排通行顺序。

📌 下一期我们将深挖:

  • Lane 是怎么分类的?
  • 为什么你 setState 多次,React 却合并得刚刚好?
  • startTransition 和 Lane 到底有什么关系?

敬请期待!

📢 互动彩蛋:

你在项目中用过 startTransitionuseDeferredValue 吗?有没有场景让你感受到明显的性能提升?评论区聊聊!

📚 本系列每天一题,持续更新中!
👉 添加我的微信:JKfog233,邀你加入【Hello World 进阶群】,一起成长、交流、内推、分享机会!

节流(throttle):给频繁操作加上冷却时间!🔥

节流(throttle):给频繁操作加上冷却时间!🔥

在游戏里,你放了一个大招,是不是要等冷却时间结束后才能再次释放?在 JavaScript 的世界里,也有一种技术叫做"节流",它就像给技能的释放加上了冷却时间,避免频繁操作导致页面卡顿甚至崩溃。今天,我们就来聊聊节流的底层机制,它和防抖(debounce)的区别,以及为什么我们需要它。

当事件变得"太热情" ❤️‍🔥

想象一下,你在一个搜索框中输入关键词,页面会根据输入内容实时联想并展示结果。看起来很方便,对吧?但是,如果用户输入速度很快,那么每按一次键都会触发一次网络请求,这可能会导致:

  1. 大量请求在短时间内发出,服务器压力剧增
  2. 请求返回顺序不一致,导致页面展示混乱
  3. 浏览器性能下降,页面卡顿甚至崩溃

再比如,你在滚动页面时,需要计算某些元素的位置(比如懒加载图片),如果每次滚动都触发,那么滚动一下可能会触发几十次事件!

这时候,我们就需要两种技术来优化:防抖(debounce)节流(throttle)。在防抖(Debounce)的底层机制:闭包与this指向的深度解析 🚀防抖(Debounce)的底层机制:闭包与this - 掘金这篇文章中已经介绍过防抖了,今天,我们重点讲解节流。

节流的基本概念 ⏱️

节流(throttle)的核心思想是:固定时间间隔内只执行一次操作。也就是说,无论事件触发多么频繁,执行函数都会按照固定的时间间隔执行一次。

image.png

节流的底层机制 🧠

让我们通过一个具体的例子来理解节流的实现。以下是一个常见的节流函数实现:

function throttle(fn, delay) {
  let last; // 上一次执行的时间戳
  let deferTimer; // setTimeout的ID

  return function(...args) {
    let that = this;
    let now = +new Date(); // 当前时间戳

    // 如果上次执行时间存在,且当前时间小于上次执行时间+延迟时间(即还在冷却中)
    if (last && now < last + delay) {
      clearTimeout(deferTimer); // 清除之前设置的定时器
      deferTimer = setTimeout(function() {
        last = now; // 更新上一次执行时间为当前时间
        fn.apply(that, args); // 执行函数
      }, delay);
    } else {
      // 第一次执行或者冷却时间已过
      last = now;
      fn.apply(that, args);
    }
  };
}

代码解析:

  1. 闭包保存状态lastdeferTimer 被闭包保存,用于记录上一次执行的时间和定时器ID。

  2. 返回函数:当我们调用 throttle(ajax, 2000) 时,它返回一个新的函数(即 throttleAjax),这个函数被绑定到事件监听器上。

  3. 时间戳判断:每次事件触发时,获取当前时间戳 now,并与上一次执行的时间戳 last 进行比较:

    • 如果距离上次执行的时间小于设定的间隔 delay,说明还在冷却期
    • 否则,说明已经过了冷却期,可以立即执行
  4. 定时器的作用:在冷却期内,每次事件触发都会重置定时器,保证在最后一次事件触发后的 delay 时间后执行一次函数。

这样,我们就实现了:在单位时间内,最多执行一次函数。

节流 vs 防抖:区别在哪? 🥊

这张图片形象地表示了地表示了debouncethrottle时间触发的频率的区别

image.png

屏幕录制_2025-07-11_002420.gif

很多人容易混淆节流和防抖,它们虽然都是用来控制函数执行频率的,但行为不同:

特性 节流(throttle) 防抖(debounce)
执行时机 固定时间间隔执行一次 事件停止触发后延迟执行
执行次数 单位时间内至少执行一次 只执行最后一次触发的事件
适用场景 滚动事件、窗口调整、输入联想 搜索框输入验证、表单提交
类比 技能冷却时间,固定间隔释放 电梯关门,等待最后一个人进入

用一个生活场景比喻:

  • 防抖:电梯门要等最后一个人进入后,等待一段时间(比如5秒)再关门
  • 节流:电梯门每隔10秒自动尝试关闭一次,如果有人进入就重新等待,但每隔10秒总会尝试关闭

为什么我们需要节流? 💡

节流的主要目的是优化性能避免不必要的资源浪费。具体来说:

  1. 减少函数执行频率:对于频繁触发的事件(如滚动、鼠标移动、窗口调整大小),节流可以大幅减少事件处理函数的执行次数,从而减轻浏览器负担。

  2. 保证用户体验:在搜索联想场景中,我们可能不需要每次按键都发送请求,而是每隔一定时间发送一次,这样既能减少请求数量,又能保证用户看到实时反馈。

  3. 避免请求混乱:在输入联想时,如果每次按键都发送请求,那么请求返回的顺序可能不一致,导致最终展示的结果不是最新的。通过节流,我们可以确保请求按固定间隔发送,减少混乱。

实际应用场景 🚀

1. 滚动事件优化

// 监听滚动事件,但最多每100ms执行一次
window.addEventListener('scroll', throttle(function() {
  // 计算元素位置、懒加载图片等
}, 100));

2. 窗口调整大小

// 窗口调整大小时重新布局,但避免过于频繁
window.addEventListener('resize', throttle(function() {
  // 重新计算布局
}, 200));

3. 输入联想搜索

// 搜索框输入联想,每500ms发送一次请求
input.addEventListener('input', throttle(function(e) {
  // 发送搜索请求
  fetchResults(e.target.value);
}, 500));

4. 游戏中的技能释放

// 防止玩家连续点击技能按钮
attackButton.addEventListener('click', throttle(function() {
  // 释放技能
  castSkill();
}, 1000)); // 技能冷却时间1秒

完整代码示例 🧩

下面是文章开头提供的完整代码,实现了输入框的节流功能:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>节流示例</title>
</head>
<body>
    <input type="text" id="inputC"/>
    <script>
        let inputC = document.getElementById('inputC')
        const ajax = function(content){//被节流的函数 
            // 模拟网络请求
            console.log('ajax request: ' + content)
        }
        // 节流函数
        function throttle(fn,delay){
            let last,//上一次执行的时间
            deferTimer;//timeout id
            return function(...args){
                let that = this;
                let now = +new Date();//当前时间戳
                if(last && now <last + delay){
                    clearTimeout(deferTimer);//清除定时器
                    deferTimer = setTimeout(function(){
                        last = now;
                        fn.apply(that,args)
                    },delay)
                }else{
                    last = now;
                    fn.apply(that,args)
                }
            }
        }
        // 创建节流版本的ajax函数
        let throttleAjax = throttle(ajax, 500)
        // 监听输入框的keyup事件
        inputC.addEventListener('keyup',function(e){
            throttleAjax(e.target.value)
        })
    </script>
</body>
</html>

屏幕录制_2025-07-10_235536.gif

在这个例子中,无论用户输入多快,ajax 函数至少会每500ms执行一次(防抖:只要用户输入的足够快,函数可能很久都不会执行)。这样,我们就有效地控制了请求频率。

总结 🎯

节流是一种非常实用的性能优化技术,它通过固定时间间隔内只执行一次函数,有效控制了函数执行的频率。与防抖不同,节流保证了在单位时间内至少执行一次,适用于需要持续反馈的场景(如滚动事件)。

在实际开发中,我们应该根据具体需求选择使用节流还是防抖:

  • 如果你关心最终状态,比如输入结束后的验证,使用防抖
  • 如果你需要固定间隔执行,比如滚动事件,使用节流

最后,希望你在项目中合理使用节流技术,为你的Web应用加上"冷却时间",让它运行得更流畅!🚀

闭包实战大全:从防抖节流到私有变量,解锁JS高级技巧 🚀

闭包实战大全:从防抖节流到私有变量,解锁JS高级技巧 🚀

"闭包是JavaScript中的魔法,它让函数拥有了记忆。" —— 某个前端魔法师

引言:闭包是什么?为什么重要?

闭包是JavaScript中最强大也最常被误解的概念之一。简单说,闭包就是函数与其词法作用域的组合,它让函数可以"记住"并访问其创建时的环境,即使该函数在其他地方执行。想更深入了解闭包的底层执行机制可以看看这篇文章揭秘JavaScript执行机制:作用域链、词法作用域与闭包揭秘JavaScript执行机制:作用域链、词法作用域与闭包 - 掘金

今天,我将带大家深入探索闭包的各种实际应用场景,结合代码示例,让你彻底掌握这个JavaScript核心概念!

无字动图.gif

一、防抖(Debounce)和节流(Throttle):控制事件触发频率和保证定期执行

防抖是闭包的经典应用场景,它确保事件处理函数在连续触发时只执行一次。

<!DOCTYPE html>
<html lang="en">
<body>
  <h2>没有防抖</h2>
  <input type="text" id="inputA">
  <h2>进行了防抖</h2>
  <input type="text" id="inputB">
  
  <script>
    function debounce(fn, delay) {
      return function(args) {
        clearTimeout(fn.id)
        fn.id = setTimeout(() => fn(args), delay)
      }
    }

    const inputA = document.getElementById('inputA')
    const inputB = document.getElementById('inputB')
    
    function ajax(content) {
      console.log('ajax request: ' + content)
    }

    // 普通输入 - 每次按键都触发
    inputA.addEventListener('keyup', e => ajax(e.target.value))
    
    // 防抖输入 - 停止输入500ms后才触发
    const debounceAjax = debounce(ajax, 500)
    inputB.addEventListener('keyup', e => debounceAjax(e.target.value))
  </script>
</body>
</html>

节流确保函数在指定时间间隔内最多执行一次(在一段时间内,连续触发中必会执行一次),特别适合scroll等高频事件。

function throttle(fn, delay) {
  let last, deferTimer
  return function(...args) {
    const that = this
    const now = +new Date()
    
    if (last && now < last + delay) {
      clearTimeout(deferTimer)
      deferTimer = setTimeout(() => {
        last = now
        fn.apply(that, args)
      }, delay)
    } else {
      last = now
      fn.apply(that, args)
    }
  }
}

// 使用示例
const throttleAjax = throttle(ajax, 500)
window.addEventListener('scroll', () => throttleAjax('scroll event'))

屏幕录制_2025-07-11_002420.gif

  1. 闭包的体现

    • debounce内部返回的匿名函数引用了外部的fndelayfn.id变量,形成闭包。
    • 闭包让这些变量始终存活,即使debounce函数已执行完毕。
  2. 为什么用闭包

    • 保存独立状态:每个防抖函数实例(如debounceAjax)都有自己的定时器 ID 和延迟时间,互不干扰。
    • 封装私有变量fn.iddelay无法被外部访问,保证逻辑安全。
    • 参数传递:闭包记住了fnajax,并能传递e.target.value

三、封装私有变量:实现真正封装

闭包是实现私有变量的最佳方式,让我们创建真正封装的类:

function Book(title, author, year) {
  // 私有变量
  let _title = title
  let _author = author
  let _year = year
  
  // 私有方法
  function getFullTitle() {
    return `${_title} by ${_author}`
  }
  
  // 公有API
  this.getTitle = () => _title
  this.getFullInfo = () => `${getFullTitle()}, published in ${_year}`
  this.updateYear = newYear => {
    if (typeof newYear === 'number' && newYear > 0) _year = newYear
    else console.error('Invalid year')
  }
}

const book = new Book('JS高级编程', '尼古拉斯', 2023)
console.log(book._title) // undefined - 真正私有!
console.log(book.getTitle()) // "JS高级编程"
book.updateYear(2024)

闭包如何实现封装?

  1. 构造函数中的局部变量是私有的
  2. 公共方法作为闭包捕获这些私有变量
  3. 外部无法直接访问私有变量,只能通过公共API

闭包的意义

  1. 数据隐藏与封装_title, _author, 和 _year 变量以及 getFullTitle 方法被定义在构造函数 Book 内部,这意味着它们对外部是不可访问的(即真正的私有)。这提供了一种机制来隐藏对象的内部状态,防止外部代码随意修改这些值。
  2. 保护对象的状态:通过只暴露必要的公有方法(如 getTitle, getFullInfo, updateYear),可以控制如何读取或修改内部状态。例如,updateYear 方法不仅允许更新年份,还包含了验证逻辑以确保新值的有效性。
  3. 保持函数上下文:内部函数(如 getFullTitle)能够访问构造函数内的局部变量(即使构造函数已经执行完毕),这是因为它们形成了闭包。这种特性使得这些函数能够在后续调用时依然能访问到创建时的作用域中的变量。

四、解决this丢失问题:闭包与上下文绑定

闭包结合箭头函数或bind方法可以完美解决JavaScript中的this指向问题:

<button id="myButton">Click Me</button>

<script>
  const obj = {
    message: "Hello from object",
    init() {
      const button = document.getElementById('myButton')
      
      // 方案1:使用箭头函数(闭包捕获this)
      button.addEventListener('click', () => {
        console.log(this.message) // 正确!
      })
      
      // 方案2:使用闭包保存this
      const that = this
      button.addEventListener('click', function() {
        console.log(that.message) // 正确!
      })
      
      // 方案3:使用bind
      button.addEventListener('click', function() {
        console.log(this.message) // 正确!
      }.bind(this))
    }
  }
  
  obj.init()
</script>

一、方案 1:箭头函数(闭包捕获 this)

button.addEventListener('click', () => {
  console.log(this.message) // 正确!
})

闭包体现

产生闭包的主体:箭头函数 () => { console.log(this.message) }

  • 箭头函数没有自己的this,它捕获的this来自定义时的上下文(即obj.init()中的this,指向obj)。

  • 即使事件处理函数在点击时才执行(此时this通常指向 DOM 元素),闭包仍保留了定义时的this值。

关键点

  • 箭头函数通过闭包隐式捕获了this变量。
  • 无论何时执行,this始终指向obj

为什么产生闭包
当箭头函数在 obj.init() 方法内部被定义时,它会捕获当前词法作用域中的 this 变量(此时 this 指向 obj)。即使该箭头函数作为事件处理函数在点击时才执行(此时事件处理函数的执行上下文通常会指向触发事件的 DOM 元素),它仍能通过闭包访问并使用定义时保存的 this 值(即 obj

普通函数不同之处是箭头函数的 this 绑定由定义时的上下文决定,而非调用时的上下文。

二、方案 2:变量保存 this(显式闭包)

const that = this
button.addEventListener('click', function() {
  console.log(that.message) // 正确!
})

闭包体现

  • this保存到变量that中,that被闭包捕获。
  • 事件处理函数(普通函数)的this指向 DOM 元素,但通过闭包访问的that始终指向obj

关键点

  • 显式创建变量that,并通过闭包保持其引用。
  • 普通函数的this被绕过,直接使用闭包中的that

普通函数的 this 指向由调用方式决定。当它作为事件处理函数被浏览器调用时(如点击按钮时),浏览器会默认将 this 绑定到触发事件的 DOM 元素(此处为 button)。因此我们要通过闭包访问外部函数的this

三、方案 3:使用 bind(函数绑定 + 闭包)

button.addEventListener('click', function() {
  console.log(this.message) // 正确!
}.bind(this))

闭包体现

  • bind(this)创建了一个新函数,其中this被永久绑定为obj.init()中的this
  • 闭包捕获的不是this本身,而是通过bind绑定的上下文。

关键点

  • bind返回的函数通过闭包记住了绑定的上下文。
  • 事件处理函数执行时,this已经被bind固定为obj

四、call,apply绑定与bind的区别

call,apply语法:

  • fn.call(thisArg, arg1, arg2, ...)

  • fn.apply(thisArg, [arg1, arg2, ...])

核心逻辑
当调用fn.call(obj)时,函数fn会立即执行,且执行期间this指向obj;执行结束后,fnthis指向不会被永久改变(下次正常调用时this仍遵循默认规则)。

不同处

call,apply绑定不涉及闭包, 闭包的核心是 函数在定义时捕获外部变量,并在后续执行时(即使脱离原作用域)仍能访问这些变量(本质是保留对原作用域的引用)。

call/apply的逻辑完全不同:

  1. 它们不创建新函数,只是对原函数进行 "即时调用";
  2. 它们不保留任何变量引用:绑定的this仅在函数执行的那一刻有效,执行结束后与原作用域、变量均无关联;
  3. 函数执行时的this是通过临时绑定机制(如上述的 "临时属性")实现的,而非通过闭包捕获外部变量。

因此,call/apply的工作过程中,没有 "函数记住外部作用域变量" 的行为,自然不涉及闭包。

方法 核心行为 是否创建新函数 是否依赖闭包 关键区别
call/apply 立即执行函数,临时绑定this 无状态,仅影响单次执行
bind 创建新函数,永久绑定this 有状态,通过闭包保留绑定上下文

简单说:call/apply是 "一次性临时借用上下文",bind是 "创建一个永久记住上下文的新函数"—— 后者需要闭包,前者不需要。

总结:三种方案的闭包对比

方案 闭包捕获对象 核心机制 特点
箭头函数 直接捕获this 箭头函数无自己的this 语法简洁,隐式保留上下文
变量保存 捕获变量that 显式保存this到变量 兼容性好,适用于所有函数类型
bind 方法 捕获绑定的上下文 通过bind创建新函数 语义明确,清晰表达绑定意图

五、记忆函数:提升性能的利器

闭包可以实现函数结果的缓存,避免重复计算:

function memoize(fn) {
  const cache = new Map()
  return function(...args) {
    const key = JSON.stringify(args)
    if (cache.has(key)) {
      console.log('从缓存中获取结果')
      return cache.get(key)
    }
    const result = fn.apply(this, args)
    cache.set(key, result)
    return result
  }
}

// 使用示例
const expensiveCalc = n => {
  console.log('执行复杂计算...')
  return n * n
}

const memoizedCalc = memoize(expensiveCalc)
console.log(memoizedCalc(5)) // 执行计算
console.log(memoizedCalc(5)) // 从缓存获取

六、模块模式:使用IIFE创建私有空间

立即执行函数表达式(IIFE)结合闭包创建模块:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>立即执行函数 IIFE</title>
</head>
<body>
    <script>
        const Counter = (function(){
            let count = 0;//私有变量 自由变量(不会被销毁,处于闭包当中)
            function increment(){
                return ++count;
            }
            function reset(){
                count = 0;
            }
            return function(){
                return{
                    getCount:function(){
                        return count;
                    },
                    increment:function(){
                        return increment();
                    },
                    reset:function(){
                        return reset();
                    }
                }
            }
        })();
        const counter1 = Counter()
        const counter2 = Counter()
        console.log(counter1.getCount())//0
        counter1.increment()//1
        console.log(counter2.getCount())//1
        //console.log(counter2.increment())//1
    </script>
</body>
</html>

可以这样理解,具体分析如下:

  1. Counter 的类型
    Counter 是一个函数
    因为 IIFE 执行后返回的是一个 function(){ ... }(外层函数返回的内部函数),所以 const Counter = ... 实际上是将这个函数赋值给了 Counter。
  2. counter1、counter2 的类型
    对象
    当调用 Counter() 时,执行了上述返回的函数,该函数返回一个包含 getCountincrementreset 方法的对象,因此 counter1 和 counter2 都是这样的对象实例。
  3. 闭包共享的原因
    所有通过 Counter() 创建的对象(counter1、counter2),其内部方法(getCount 等)共享同一个闭包。
    因为它们的定义都嵌套在最外层的 IIFE 中,共同访问 IIFE 作用域内的私有变量 count。因此,无论创建多少个对象实例,操作的都是同一个 count,会相互影响。

简单说:Counter 是 “造对象的函数”,counter1/counter2 是 “被造出的对象”,但所有对象共享同一个闭包作用域里的 count

闭包面试通关秘籍

当面试官问及闭包时,你可以这样回答:

  1. 基本概念:"闭包是函数与其词法环境的组合,即使函数在原始作用域外执行,也能访问该作用域"

  2. 核心价值

    • 创建私有变量和方法
    • 保持状态(如计数器)
    • 实现高阶函数(防抖、节流等)
    • 模块化开发
  3. 实际应用

    graph TD
      A[闭包应用] --> B[防抖节流]
      A --> C[私有变量封装]
      A --> D[函数柯里化]
      A --> E[记忆函数]
      A --> F[模块模式]
      A --> G[解决this问题]
    
  4. 注意事项

    • 避免内存泄漏(不再使用的闭包及时释放)
    • 不要过度使用(可能增加内存消耗)
    • 理解作用域链

结语:闭包的力量

闭包是JavaScript中真正强大的特性之一,它不仅仅是面试题中的常客,更是实际开发中不可或缺的工具。掌握闭包,你就能:

✅ 写出更优雅、高效的代码
✅ 解决复杂的业务问题
✅ 设计更好的软件架构
✅ 在面试中脱颖而出

闭包就像JavaScript的"超能力",现在你已经拥有了它,去创造令人惊叹的代码吧!💪

Suggestion.gif

"任何可以用JavaScript编写的应用,最终都将用JavaScript编写。" —— Atwood定律

❌