普通视图

发现新文章,点击刷新页面。
昨天 — 2026年4月14日首页

VueUse 全面指南|Vue3组合式工具集实战

2026年4月14日 15:57

VueUse 是基于 Vue3 Composition API 开发的实用函数集合库,由 Vue 核心团队成员主导维护,收录了200+开箱即用的工具函数,覆盖 DOM 操作、浏览器 API、响应式状态管理、性能优化等几乎所有前端开发场景。其核心理念是“拒绝重复造轮子”,将开发中常用但繁琐的逻辑(如本地存储、鼠标监听、防抖节流)封装成可复用的组合式函数,让开发者专注于业务逻辑,大幅提升开发效率。

VueUse 完美适配 Vue3,原生支持 TypeScript,支持摇树优化(Tree Shaking),按需引入不冗余,同时兼容 Vue2(需使用对应版本)和 SSR 场景,是 Vue3 项目开发的必备工具库之一。

一、VueUse 核心特点

  • Composition API 原生适配:所有函数均基于 Vue3 setup 语法和 ref/reactive 构建,API 风格与 Vue3 原生语法高度一致,上手无压力,无需额外学习成本。
  • 类型友好:全程使用 TypeScript 编写,自带完整类型定义,开发时可获得精准代码提示,减少类型错误,适配 TS 项目开发需求。
  • 模块化设计:采用按需引入机制,仅打包用到的函数,避免引入全部模块造成的体积膨胀,优化项目打包性能。
  • 场景覆盖广泛:涵盖响应式状态、浏览器能力、DOM 操作、表单控制、网络请求、性能优化等200+场景,满足日常开发99%的需求。
  • 灵活通用:支持 CDN 引入(无需打包器),适配 Vite、Webpack、Nuxt 等多种构建工具,同时支持 SSR 友好,可搭配 Vue Router、Firebase 等插件使用。
  • 中文文档完善:官方提供中文文档,每个函数均有交互式演示,查询便捷,新手可快速上手。

二、环境安装(Vue3 实战首选)

VueUse 核心包为 @vueuse/core,包含绝大多数常用工具函数;若需特定场景(如音频、地图),可安装对应子包。以下是主流安装方式,推荐使用 npm 或 pnpm:

2.1 核心包安装(必装)

// npm 安装(推荐,适配绝大多数项目)
npm install @vueuse/core -S

// yarn 安装
yarn add @vueuse/core

// pnpm 安装(高效包管理,推荐)
pnpm add @vueuse/core

2.2 特定场景子包安装(按需选择)

若需使用音频、地图、Firebase 等特定功能,可单独安装对应子包:

// 音频相关工具(如播放、录音)
npm install @vueuse/sound -S

// 地图相关工具(如高德、百度地图集成)
npm install @vueuse/map -S

// Firebase 集成工具
npm install @vueuse/firebase -S

2.3 CDN 引入(无需打包器,快速测试)

适合快速演示或无需打包的简单项目,引入后可通过 window.VueUse 访问所有函数:

<script src="https://unpkg.com/@vueuse/shared"></script>
<script src="https://unpkg.com/@vueuse/core"></script>

2.4 Nuxt 项目适配

Nuxt 3 已内置 VueUse 支持,无需单独安装,仅需在配置文件中注册模块即可实现自动引入:

// nuxt.config.ts(Nuxt 3)
export default defineNuxtConfig({
  modules: ['@vueuse/nuxt']
})

三、核心用法(按场景分类,实战必备)

VueUse 的使用逻辑简单统一:按需引入所需函数,在 setup 语法中调用,即可获得响应式结果或封装好的逻辑,无需手动处理事件绑定、销毁等冗余操作。以下按高频场景分类讲解,代码可直接复制套用。

3.1 响应式状态与本地存储(最常用)

用于处理响应式状态切换、计数器、本地存储(localStorage/sessionStorage)等场景,自动处理 JSON 序列化和响应式同步,刷新页面数据不丢失。

3.1.1 useLocalStorage(本地持久化存储)

替代原生 localStorage,返回响应式 ref 对象,修改后自动同步到本地存储,适合存储用户偏好、登录态等需要持久化的数据:

<template>
  <div>
    <p>当前主题:{{ theme }}</p>
    <button @click="theme = theme === 'light' ? 'dark' : 'light'">切换主题</button>
  </div>
</template>

<script setup lang="ts">
// 按需引入
import { useLocalStorage } from '@vueuse/core'

// 第一个参数:localStorage 键名;第二个参数:默认值
const theme = useLocalStorage('app_theme', 'light')
// 修改值时,自动同步到 localStorage
// theme.value = 'dark'
</script>

3.1.2 useSessionStorage(会话级存储)

用法与 useLocalStorage 完全一致,区别在于数据存储在 sessionStorage 中,关闭页面后自动丢失,适合存储临时数据(如表单草稿):

import { useSessionStorage } from '@vueuse/core'

// 存储临时表单数据
const tempForm = useSessionStorage('temp_form', { username: '', password: '' })

3.1.3 useToggle(布尔值切换)

快速实现布尔值切换逻辑,适合弹窗显示/隐藏、开关状态等场景:

<template>
  <button @click="toggle">{{ isShow ? '隐藏' : '显示' }}弹窗</button>
  <div v-if="isShow" class="modal">弹窗内容</div>
</template>

<script setup lang="ts">
import { useToggle } from '@vueuse/core'

// 接收默认值,返回 [状态值, 切换函数]
const [isShow, toggle] = useToggle(false)
// 也可自定义切换值(如切换主题字符串)
// const [theme, toggleTheme] = useToggle('light', ['light', 'dark'])
</script>

3.1.4 useCounter(计数器工具)

封装计数器逻辑,支持增减、重置、设置值等操作,适合数量选择、分页页码等场景:

<template>
  <div>
    <button @click="dec()">-</button>
    <span>{{ count }}</span>
    <button @click="inc()">+</button>
    <button @click="reset()">重置</button>
    <button @click="set(10)">设为10</button>
  </div>
</template>

<script setup lang="ts">
import { useCounter } from '@vueuse/core'

// 默认值为0,可指定初始值和范围(如 min:0, max:10)
const { count, inc, dec, reset, set } = useCounter(0, { min: 0, max: 10 })
</script>

3.2 浏览器能力封装(简化原生 API)

将浏览器原生 API(如鼠标监听、网络状态、窗口尺寸)封装为响应式函数,自动处理事件绑定与销毁,避免内存泄漏。

3.2.1 useMouse(鼠标位置监听)

实时获取鼠标坐标,支持限制监听范围(如某元素内),适合鼠标跟随、悬浮交互等场景:

<template>
  <div>
    <p>鼠标位置:({{ x.toFixed(0) }}, {{ y.toFixed(0) }})</p>
    <div 
      class="follow" 
      :style="{ left: `${x + 10}px`, top: `${y + 10}px` }"
    ></div>
  </div>
</template>

<script setup lang="ts">
import { useMouse } from '@vueuse/core'

// 获取鼠标x、y坐标(响应式)
const { x, y } = useMouse()
// 限制监听范围(仅在id为container的元素内监听)
// const { x, y } = useMouse({ target: document.getElementById('container') })
</script>

<style scoped>
.follow {
  position: fixed;
  width: 10px;
  height: 10px;
  background: red;
  border-radius: 50%;
}
</style>

3.2.2 useNetwork(网络状态监听)

监听用户网络连接状态(在线/离线),适合提示用户网络异常、离线缓存等场景:

<template>
  <div v-if="!isOnline" class="offline-tip">
    ❌ 网络已断开,请检查网络连接
  </div>
</template>

<script setup lang="ts">
import { useNetwork } from '@vueuse/core'

const { isOnline, downlink } = useNetwork()
// isOnline:是否在线(布尔值)
// downlink:网络速度(Mbps)
</script>

3.2.3 useDark(深色模式切换)

快速实现深色/浅色模式切换,自动同步系统主题偏好,支持自定义主题类名:

<template>
  <div>
    <h1>当前模式:{{ isDark ? '🌙 深色' : '☀️ 浅色' }}</h1>
    <button @click="toggleDark()">切换主题</button>
  </div>
</template>

<script setup lang="ts">
import { useDark, useToggle } from '@vueuse/core'

// 监听系统深色模式,同步到 html 标签的 class(默认添加 dark 类)
const isDark = useDark({
  selector: 'html',
  valueDark: 'dark',
  valueLight: ''
})
// 结合 useToggle 实现切换
const toggleDark = useToggle(isDark)
</script>

<style>
html.dark {
  background-color: #121212;
  color: #fff;
}
</style>

3.2.4 useWindowSize(窗口尺寸监听)

实时获取窗口宽高,响应式更新,适合响应式布局、适配移动端/桌面端场景:

import { useWindowSize } from '@vueuse/core'
import { computed } from 'vue'

const { width, height } = useWindowSize()
// 判断是否为移动端(屏幕宽度 < 768px)
const isMobile = computed(() => width.value < 768)

3.3 表单与输入控制(优化交互体验)

封装防抖、节流、剪贴板等常用表单交互逻辑,简化输入框搜索、复制粘贴等功能开发。

3.3.1 useDebounce(防抖输入)

对输入值进行防抖处理,延迟执行逻辑,适合搜索框、输入验证等场景,避免频繁触发请求:

<template>
  <input 
    v-model="searchInput" 
    placeholder="请输入搜索关键词"
    style="width: 300px; padding: 8px;"
  />
</template>

<script setup lang="ts">
import { ref, watch } from 'vue'
import { useDebounce } from '@vueuse/core'

const searchInput = ref('')
// 防抖处理:延迟500ms,返回防抖后的响应式值
const debouncedInput = useDebounce(searchInput, 500)

// 监听防抖后的值,触发搜索逻辑
watch(debouncedInput, (val) => {
  if (val) {
    console.log('搜索关键词:', val)
    // 调用搜索接口...
  }
})
</script>

3.3.2 useThrottle(节流控制)

限制函数执行频率,适合滚动事件、resize 事件等频繁触发的场景,优化性能:

import { useThrottle } from '@vueuse/core'

// 对窗口滚动事件进行节流,200ms内仅执行一次
const scrollY = useThrottle(window.scrollY, 200)

3.3.3 useCopyToClipboard(剪贴板操作)

简化复制文本到剪贴板的逻辑,自带复制状态反馈,无需编写原生 API 代码:

<template>
  <div>
    <input v-model="copyText" placeholder="请输入要复制的内容" />
    <button @click="copy()">{{ copied ? '已复制✅' : '点击复制' }}</button>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { useCopyToClipboard } from '@vueuse/core'

const copyText = ref('https://vueuse.org')
// 接收复制源,返回 [复制函数, 复制状态]
const { copy, copied } = useCopyToClipboard({ source: copyText })
</script>

3.4 DOM 操作与交互(简化 DOM 操作)

封装常用 DOM 操作逻辑,自动处理元素监听、尺寸获取、拖拽等功能,避免手动操作 DOM 带来的冗余代码。

3.4.1 useScroll(滚动位置监听)

监听元素或窗口的滚动位置,适合滚动加载、回到顶部、滚动导航等场景:

<template>
  <div ref="container" class="scroll-container"&gt;
    <!-- 滚动内容 -->
  </div>
  <button @click="scrollToTop()" v-if="y > 100">回到顶部</button>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { useScroll } from '@vueuse/core'

const container = ref(null)
// 监听指定元素的滚动位置(默认监听窗口)
const { y, scrollTo } = useScroll(container)

// 回到顶部
const scrollToTop = () => {
  scrollTo({ top: 0, behavior: 'smooth' })
}
</script>

3.4.2 useElementSize(元素尺寸监听)

实时获取元素的宽高,响应式更新,适合自适应布局、元素尺寸变化监听等场景:

<template>
  <div ref="box" class="box">自适应盒子</div>
  <p>盒子尺寸:{{ width }}px × {{ height }}px</p>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { useElementSize } from '@vueuse/core'

const box = ref(null)
// 获取元素宽高(响应式)
const { width, height } = useElementSize(box)
</script>

3.4.3 onClickOutside(点击外部关闭)

监听元素外部的点击事件,适合弹窗、下拉菜单等场景,点击外部自动关闭:

<template>
  <button @click="isOpen = true">打开下拉菜单</button>
  <div ref="menu" v-if="isOpen" class="menu">
    下拉菜单内容
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { onClickOutside } from '@vueuse/core'

const isOpen = ref(false)
const menu = ref(null)

// 点击 menu 外部,关闭下拉菜单
onClickOutside(menu, () => {
  isOpen.value = false
})
</script>

3.5 网络请求(简化请求逻辑)

封装 fetch API,自带加载状态、错误处理,返回响应式数据,适合简单网络请求场景,可替代 axios 基础用法。

3.5.1 useFetch(通用网络请求)

<template>
  <div>
    <div v-if="isLoading">加载中...</div>
    <div v-if="error" class="error">请求失败:{{ error.message }}</div>
    <div v-if="data">{{ data.content }}</div>
  </div>
</template>

<script setup lang="ts">
import { useFetch } from '@vueuse/core'

// 发起 GET 请求,返回响应式数据、加载状态、错误信息
const { data, isLoading, error, execute } = useFetch('https://api.example.com/data', {
  method: 'GET',
  // 可选配置:请求头、参数等
  headers: { 'Content-Type': 'application/json' }
})

// 手动触发请求(如点击按钮发起请求)
// const handleFetch = () => execute()
</script>

四、VueUse 进阶技巧(实战提升)

4.1 函数组合使用

VueUse 的函数可自由组合,实现复杂功能,例如结合 useDark、useLocalStorage、useToggle 实现主题切换并持久化:

import { useDark, useToggle, useLocalStorage } from '@vueuse/core'

// 结合本地存储,持久化主题状态
const theme = useLocalStorage('app_theme', 'light')
const isDark = useDark({ valueDark: 'dark', valueLight: 'light' })
// 同步主题状态与本地存储
theme.value = isDark.value ? 'dark' : 'light'
// 切换主题时同步更新本地存储
const toggleTheme = useToggle(isDark, [false, true])
toggleTheme(() => {
  theme.value = isDark.value ? 'dark' : 'light'
})

4.2 自定义配置参数

大多数函数支持自定义配置,例如限制计数器范围、自定义本地存储键名、指定监听目标等,灵活适配业务需求:

// 1. 限制计数器范围(0-100)
const { count, inc } = useCounter(0, { min: 0, max: 100 })

// 2. 自定义本地存储键名和存储方式
const user = useLocalStorage('user_info', {}, {
  storage: sessionStorage, // 改用 sessionStorage 存储
  mergeDefaults: true // 合并默认值和存储值
})

// 3. 指定鼠标监听目标(仅在指定元素内监听)
const { x, y } = useMouse({ target: document.getElementById('container') })

4.3 避免常见误区

  • 不要全局引入所有函数:VueUse 支持摇树优化,按需引入即可,全局引入(如 import * as VueUse from '@vueuse/core')会导致打包体积膨胀。
  • 注意浏览器兼容性:部分函数(如 useBattery、useGeolocation)依赖浏览器原生 API,需做降级处理,避免在低版本浏览器中报错。
  • Vue2 适配:VueUse v12.0 及以上版本不再支持 Vue2,若使用 Vue2 项目,需安装 v11.x 版本:npm install @vueuse/core@11 -S

五、常用函数速查表(快速查询)

函数分类 常用函数 核心功能
响应式状态 useToggle、useCounter、useStorage 布尔值切换、计数器、响应式存储
浏览器能力 useMouse、useNetwork、useDark、useWindowSize 鼠标监听、网络状态、深色模式、窗口尺寸
表单控制 useDebounce、useThrottle、useCopyToClipboard 防抖、节流、剪贴板操作
DOM 操作 useScroll、useElementSize、onClickOutside 滚动监听、元素尺寸、点击外部关闭
网络请求 useFetch、useWebSocket 通用请求、WebSocket 连接

六、官方资源与学习渠道

总结:VueUse 是 Vue3 开发的“效率神器”,通过封装常用逻辑,大幅减少冗余代码,提升开发效率和代码可维护性。新手可从本文讲解的高频函数入手,结合官方文档,快速上手并应用到实际项目中,逐步掌握所有核心用法。

Vue3+Pinia实战完整版|从入门到精通,替代Vuex的状态管理首选

2026年4月14日 15:51

本文专为Vue3开发者打造,从Pinia基础认知入手,逐步讲解环境搭建、核心API用法,结合Vue3+TypeScript实战案例,覆盖日常开发99%场景,新手可直接套用代码,快速掌握Pinia全局状态管理,替代传统Vuex,提升开发效率。

核心定位:Pinia是Vue官方推荐的全局状态管理工具,2019年推出,旨在替代Vuex,采用组合式API风格,轻量、简洁且原生支持TS,适配Vue3和Vue2(本文重点聚焦Vue3+TS实战)。

一、Pinia基础认知(入门必看)

1.1 什么是Pinia

Pinia是一个用于跨组件、跨页面进行状态共享的全局状态管理库,功能与Vuex、Redux一致,但API更简洁,使用体验更贴近Vue3组合式API,本质上是Vuex5的最终实现形态——Vue官方团队在探索Vuex下一次迭代时,发现Pinia已满足大部分需求,最终决定用Pinia替代Vuex。

1.2 Pinia核心特点

  • 完整TS支持:无需手动编写复杂类型声明,原生支持类型推断,TS开发体验拉满,补全更流畅。
  • 极致轻量:压缩后体积仅1KB左右,无多余依赖,不增加项目负担。
  • 简化语法:移除Vuex中繁琐的mutations,仅保留state、getters、actions,降低学习和使用成本。
  • actions多支持:既支持同步操作,也支持异步操作(如接口请求),无需区分同步/异步逻辑。
  • 扁平化结构:无模块嵌套,只有store概念,每个store独立存在,可自由调用,无需管理复杂的命名空间。
  • 自动注册:store一旦创建,无需手动添加到全局,自动挂载,开箱即用。
  • 跨版本兼容:同时支持Vue3和Vue2,除初始化安装和SSR配置外,两者API完全一致。

1.3 Pinia与Vuex的核心区别

Pinia最初是为探索Vuex下一次迭代而设计,整合了Vuex核心团队的诸多想法,最终成为Vuex的替代方案,两者核心区别如下:

对比维度 Vuex Pinia
核心结构 State、Getters、Mutations(同步)、Actions(异步) State、Getters、Actions(同步+异步),无Mutations
版本适配 Vuex4适配Vue3,Vuex3适配Vue2,无法跨版本使用 最新版2.x,同时适配Vue3和Vue2
TS支持 需创建自定义复杂包装器,类型推断不友好 原生支持TS,类型推断完善,无需额外配置
模块结构 支持模块嵌套,需配置命名空间,逻辑繁琐 扁平化结构,无嵌套,store独立,可自由调用
注册方式 需手动注册store到全局 自动注册,创建后即可使用
API复杂度 API繁琐,需记住mutations提交、命名空间等规则 API简洁,贴近组合式API,上手成本低

1.4 适用场景

任何需要跨组件、跨页面共享状态的Vue3项目,无论是中小型项目(如个人博客、管理后台),还是大型项目(如电商平台),Pinia都能胜任,尤其适合TS开发的项目,能大幅提升开发效率和代码可维护性。

二、Vue3+Pinia环境搭建(实战第一步)

本章节以Vue3+TypeScript项目为例,讲解Pinia的安装、全局注册,步骤简洁,可直接复制命令和代码执行。

2.1 前提条件

已创建Vue3+TS项目(若未创建,执行命令:npm create vue@latest,选择TS、Pinia(可选,此处可跳过,后续手动安装))。

2.2 安装Pinia

打开终端,进入项目根目录,执行以下命令(三选一,推荐npm或yarn):

// npm 安装(推荐)
npm install pinia -S

// yarn 安装
yarn add pinia

// cnpm 安装
cnpm install pinia -S

2.3 全局注册Pinia(Vue3)

修改项目入口文件main.ts,引入并挂载Pinia实例,全局仅需配置一次:

// main.ts
import { createApp } from 'vue'
import App from './App.vue'
// 引入Pinia的createPinia方法
import { createPinia } from 'pinia'

// 创建Pinia实例
const pinia = createPinia()
// 创建Vue应用并挂载Pinia
const app = createApp(App)
app.use(pinia) // 挂载Pinia到Vue应用
app.mount('#app')

补充:Vue2中注册方式略有不同(需引入PiniaVuePlugin),本文聚焦Vue3,Vue2用法可参考文末补充说明。

三、Pinia核心用法(Vue3+TS实战)

Pinia的核心是Store(仓库),每个Store对应一个独立的状态模块,通过defineStore方法创建,包含state(状态)、getters(计算属性)、actions(业务逻辑)三部分,以下逐一讲解。

3.1 初始化Store(核心步骤)

推荐在项目根目录下创建src/store文件夹,用于存放所有Store文件,按业务模块划分(如用户模块、购物车模块),规范命名(如userStore.tscartStore.ts)。

步骤:先定义Store名称枚举(避免重复),再创建Store实例。

第一步:定义Store名称枚举(可选,推荐)

创建src/store/store-name.ts,用于统一管理Store名称,避免重复(尤其多Store场景):

// src/store/store-name.ts
// 用枚举定义Store名称,唯一且直观
export const enum Names {
  Test = 'TEST', // 测试Store名称
  User = 'USER', // 用户Store名称
  Cart = 'CART'  // 购物车Store名称
}

第二步:创建Store实例

创建src/store/index.ts(或按模块拆分,如userStore.ts),使用defineStore方法创建Store,核心包含state、getters、actions:

// src/store/index.ts
import { defineStore } from 'pinia';
import { Names } from './store-name'; // 引入Store名称枚举

// defineStore接收两个参数:
// 1. 唯一标识(必须与枚举值一致,全局唯一,不可重复)
// 2. 配置对象(包含state、getters、actions)
export const useTestStore = defineStore(Names.Test, {
  // 1. state:存储全局状态,必须是箭头函数(避免SSR数据污染,优化TS类型推导)
  state: () => {
    return {
      current: 1, // 数字类型状态
      name: '小马', // 字符串类型状态
      list: [1, 2, 3] // 数组类型状态
    };
  },

  // 2. getters:类似组件的computed,用于修饰状态,有缓存功能
  getters: {
    // 方式一:接收state作为参数(推荐,类型推断更友好)
    myGetCount(state) {
      // 缓存特性:页面多次使用,仅执行一次计算
      console.log('getters被调用');
      return state.current + 1;
    },

    // 方式二:不传递参数,使用this访问state(需指定返回值类型,否则TS推导失败)
    myGetName(): string {
      return `姓名:${this.name}`;
    },

    // 进阶:getters依赖其他getters
    myGetCombined(): string {
      return `${this.myGetName()},计数+1:${this.myGetCount}`;
    }
  },

  // 3. actions:类似组件的methods,用于修改state,支持同步和异步
  actions: {
    // 同步action:修改state(不能用箭头函数,否则this指向异常)
    setCurrentParam(num: number) {
      this.current += num; // 直接通过this访问state并修改
    },

    // 同步action:批量修改多个状态
    updateState(newCurrent: number, newName: string) {
      this.current = newCurrent;
      this.name = newName;
    },

    // 异步action:结合async/await(如接口请求)
    async fetchData() {
      // 模拟接口请求(实际开发中替换为真实接口)
      const res = await new Promise((resolve) => {
        setTimeout(() => {
          resolve({ current: 10, name: '异步更新后' });
        }, 1000);
      });
      // 异步请求成功后,修改state
      const data = res as { current: number; name: string };
      this.current = data.current;
      this.name = data.name;
    }
  },
});

3.2 组件中使用Store(核心实战)

在Vue3组件(<script setup lang="ts">)中,引入Store实例,即可访问、修改状态,调用actions,以下是完整示例。

3.2.1 基础使用(访问state、getters)

<template>
  <div class="pinia-demo">
    <h3>基础使用</h3>
    <!-- 直接访问state -->
    <p>当前计数:{{ testStore.current }}</p>
    <p>姓名:{{ testStore.name }}</p>
    <!-- 访问getters(直接当作属性使用,无需调用) -->
    <p>计数+1:{{ testStore.myGetCount }}</p>
    <p>组合getters:{{ testStore.myGetCombined }}</p>
  </div>
</template>

<script setup lang="ts">
// 1. 引入Store实例
import { useTestStore } from '@/store';

// 2. 创建Store实例(Pinia自动管理单例,多次调用返回同一个实例)
const testStore = useTestStore();
</script>

3.2.2 修改state(5种方式,实战常用)

Pinia提供多种修改state的方式,按需选择,推荐使用$patch(批量修改)和actions(业务逻辑封装)。

<template>
  <div class="pinia-demo">
    <h3>修改state</h3>
    <p>当前计数:{{ testStore.current }}</p>
    <button @click="handleDirectModify">1.直接修改</button>
    <button @click="handlePatchObj">2.$patch对象批量修改</button>
    <button @click="handlePatchFn">3.$patch函数自定义修改</button>
    <button @click="handleReplaceState">4.$state替换整个状态</button>
    <button @click="handleActionsModify">5.通过actions修改</button>
  </div>
</template>

<script setup lang="ts">
import { useTestStore } from '@/store';
const testStore = useTestStore();

// 方式1:直接修改(简单场景可用,不推荐复杂场景)
const handleDirectModify = () => {
  testStore.current++; // 直接修改单个状态
  // testStore.name = '新姓名'; // 直接修改单个状态
};

// 方式2:$patch对象形式(批量修改多个状态,推荐简单批量场景)
const handlePatchObj = () => {
  testStore.$patch({
    current: 10,
    name: '批量修改后',
    list: [4, 5, 6]
  });
};

// 方式3:$patch函数形式(自定义修改逻辑,推荐复杂场景)
const handlePatchFn = () => {
  testStore.$patch((state) => {
    state.current += 5; // 复杂计算修改
    state.list.push(7); // 数组操作
    if (state.current > 20) {
      state.name = '计数超标';
    }
  });
};

// 方式4:$state替换整个状态(需修改所有属性,不推荐常规场景)
const handleReplaceState = () => {
  testStore.$state = {
    current: 0,
    name: '替换整个状态',
    list: []
  };
};

// 方式5:通过actions修改(推荐,封装业务逻辑,便于维护和复用)
const handleActionsModify = () => {
  testStore.setCurrentParam(3); // 调用同步action
  // testStore.updateState(15, 'actions修改'); // 调用同步action
  // testStore.fetchData(); // 调用异步action
};
</script>

3.2.3 响应式解构state(关键技巧)

直接解构state会丢失响应性(Pinia的state默认用reactive处理,与Vue3 reactive解构规则一致),需使用Pinia提供的storeToRefs方法,实现响应式解构。

<template>
  <div class="pinia-demo">
    <h3>响应式解构</h3>
    <p>解构后计数:{{ current }}</p>
    <p>解构后姓名:{{ name }}</p>
    <button @click="handleChange">修改解构后的值</button>
  </div>
</template>

<script setup lang="ts">
import { useTestStore } from '@/store';
import { storeToRefs } from 'pinia'; // 引入storeToRefs

const testStore = useTestStore();

// 错误写法:直接解构,失去响应性
// const { current, name } = testStore;

// 正确写法:用storeToRefs解构,保持响应性
const { current, name } = storeToRefs(testStore);

// 修改解构后的值(需用.value,因为storeToRefs会将状态转为ref)
const handleChange = () => {
  current.value++;
  name.value = '解构后修改';
};
</script>

3.2.4 调用异步actions(实战常用)

actions支持async/await,可直接在组件中调用异步action,处理接口请求等异步逻辑,示例如下:

<template>
  <div class="pinia-demo">
    <h3>异步actions</h3>
    <p>当前计数:{{ testStore.current }}</p>
    <p>姓名:{{ testStore.name }}</p>
    <button @click="handleFetchData" :disabled="loading">
      {{ loading ? '加载中...' : '异步请求更新' }}
    </button>
  </div>
</template>

<script setup lang="ts">
import { useTestStore } from '@/store';
import { ref } from 'vue';

const testStore = useTestStore();
const loading = ref(false);

// 调用异步action
const handleFetchData = async () => {
  loading.value = true;
  try {
    await testStore.fetchData(); // 等待异步action执行完成
  } catch (err) {
    console.error('异步请求失败:', err);
  } finally {
    loading.value = false;
  }
};
</script>

3.3 多Store使用(实战场景)

Pinia无模块嵌套,多个Store独立存在,可在组件中同时引入多个Store,也可在一个Store中引入另一个Store(实现Store间通信)。

3.3.1 组件中引入多个Store

// src/store/userStore.ts(新增用户Store)
import { defineStore } from 'pinia';
import { Names } from './store-name';

export const useUserStore = defineStore(Names.User, {
  state: () => ({
    token: '',
    userInfo: { name: '游客', age: 18 }
  }),
  actions: {
    login(token: string, userInfo: any) {
      this.token = token;
      this.userInfo = userInfo;
    },
    logout() {
      this.token = '';
      this.userInfo = { name: '游客', age: 18 };
    }
  }
});

// 组件中使用多个Store
<script setup lang="ts">
import { useTestStore } from '@/store';
import { useUserStore } from '@/store/userStore';

const testStore = useTestStore();
const userStore = useUserStore();

// 调用不同Store的方法
const handleLogin = () => {
  userStore.login('abc123', { name: '小明', age: 20 });
};
</script>

3.3.2 Store间通信(一个Store调用另一个Store)

在一个Store的actions中,引入另一个Store实例,即可实现Store间的数据交互:

// src/store/cartStore.ts(购物车Store)
import { defineStore } from 'pinia';
import { Names } from './store-name';
import { useUserStore } from './userStore'; // 引入用户Store

export const useCartStore = defineStore(Names.Cart, {
  state: () => ({
    cartList: [] as { id: number; name: string; price: number }[]
  }),
  actions: {
    // 添加商品到购物车(需判断用户是否登录)
    addToCart(goods: { id: number; name: string; price: number }) {
      const userStore = useUserStore(); // 实例化用户Store
      if (!userStore.token) {
        alert('请先登录');
        return;
      }
      this.cartList.push(goods);
    }
  }
});

四、Vue3+Pinia实战案例(模拟电商场景)

结合前面的核心用法,实现一个简单的电商场景实战案例,包含「用户登录/退出」「购物车添加/删除」「全局状态共享」,整合多Store、异步actions、响应式解构等核心知识点,可直接复制到项目中使用。

4.1 实战准备(创建3个Store)

创建store-name.tsuserStore.ts(用户)、cartStore.ts(购物车)、goodsStore.ts(商品),代码如下:

// 1. store-name.ts(Store名称枚举)
export const enum Names {
  User = 'USER',
  Cart = 'CART',
  Goods = 'GOODS'
}

// 2. userStore.ts(用户Store)
import { defineStore } from 'pinia';
import { Names } from './store-name';

// 定义用户信息类型(TS类型约束)
interface UserInfo {
  name: string;
  age: number;
  avatar: string;
}

export const useUserStore = defineStore(Names.User, {
  state: () => ({
    token: localStorage.getItem('token') || '', // 持久化存储token
    userInfo: {} as UserInfo
  }),
  actions: {
    // 登录(异步,模拟接口请求)
    async login(account: string, password: string) {
      // 模拟接口请求
      const res = await new Promise((resolve) => {
        setTimeout(() => {
          resolve({
            token: 'pinia_demo_token_123',
            userInfo: { name: '小明', age: 22, avatar: 'https://picsum.photos/200/200' }
          });
        }, 1000);
      });
      const data = res as { token: string; userInfo: UserInfo };
      this.token = data.token;
      this.userInfo = data.userInfo;
      // 本地持久化token(避免页面刷新丢失)
      localStorage.setItem('token', data.token);
    },
    // 退出登录
    logout() {
      this.token = '';
      this.userInfo = {} as UserInfo;
      localStorage.removeItem('token');
    }
  }
});

// 3. goodsStore.ts(商品Store)
import { defineStore } from 'pinia';
import { Names } from './store-name';

// 商品类型约束
interface Goods {
  id: number;
  name: string;
  price: number;
  img: string;
  stock: number;
}

export const useGoodsStore = defineStore(Names.Goods, {
  state: () => ({
    goodsList: [] as Goods[] // 商品列表
  }),
  actions: {
    // 异步获取商品列表(模拟接口)
    async fetchGoodsList() {
      const res = await new Promise((resolve) => {
        setTimeout(() => {
          resolve([
            { id: 1, name: 'Vue3实战教程', price: 99, img: 'https://picsum.photos/200/200', stock: 100 },
            { id: 2, name: 'Pinia入门手册', price: 59, img: 'https://picsum.photos/200/200', stock: 50 },
            { id: 3, name: 'TS入门到精通', price: 79, img: 'https://picsum.photos/200/200', stock: 80 }
          ]);
        }, 800);
      });
      this.goodsList = res as Goods[];
    }
  }
});

// 4. cartStore.ts(购物车Store)
import { defineStore } from 'pinia';
import { Names } from './store-name';
import { useUserStore } from './userStore';
import { Goods } from './goodsStore'; // 复用商品类型

export const useCartStore = defineStore(Names.Cart, {
  state: () => ({
    cartList: [] as { goods: Goods; count: number }[] // 购物车列表(商品+数量)
  }),
  getters: {
    // 计算购物车总价格
    cartTotalPrice(state) {
      return state.cartList.reduce((total, item) => {
        return total + item.goods.price * item.count;
      }, 0);
    },
    // 计算购物车商品总数
    cartTotalCount(state) {
      return state.cartList.reduce((total, item) => total + item.count, 0);
    }
  },
  actions: {
    // 添加商品到购物车
    addToCart(goods: Goods, count: number = 1) {
      const userStore = useUserStore();
      if (!userStore.token) {
        alert('请先登录');
        return;
      }
      // 判断商品是否已在购物车中
      const existingItem = this.cartList.find(item => item.goods.id === goods.id);
      if (existingItem) {
        existingItem.count += count;
      } else {
        this.cartList.push({ goods, count });
      }
    },
    // 从购物车删除商品
    removeFromCart(goodsId: number) {
      this.cartList = this.cartList.filter(item => item.goods.id !== goodsId);
    },
    // 修改购物车商品数量
    updateCartCount(goodsId: number, count: number) {
      const item = this.cartList.find(item => item.goods.id === goodsId);
      if (item) {
        item.count = count;
      }
    },
    // 清空购物车
    clearCart() {
      this.cartList = [];
    }
  }
});

4.2 实战组件开发(3个核心组件)

4.2.1 登录组件(Login.vue)

<template>
  <div class="login-container">
    <h2>用户登录</h2>
    <div class="form-item">
      <label>账号:</label>
      <input v-model="account" type="text" placeholder="请输入账号" />
    </div>
    <div class="form-item">
      <label>密码:</label>
      <input v-model="password" type="password" placeholder="请输入密码" />
    </div>
    <button @click="handleLogin" :disabled="loading">
      {{ loading ? '登录中...' : '登录' }}
    </button>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue';
import { useUserStore } from '@/store/userStore';
import { useRouter } from 'vue-router'; // 路由跳转(需配置路由)

const userStore = useUserStore();
const router = useRouter();
const account = ref('');
const password = ref('');
const loading = ref(false);

const handleLogin = async () => {
  if (!account.value || !password.value) {
    alert('请输入账号和密码');
    return;
  }
  loading.value = true;
  try {
    await userStore.login(account.value, password.value);
    alert('登录成功');
    router.push('/home'); // 登录成功跳转首页
  } catch (err) {
    alert('登录失败,请重试');
  } finally {
    loading.value = false;
  }
};
</script>

<style scoped>
.login-container {
  width: 300px;
  margin: 100px auto;
  text-align: center;
}
.form-item {
  margin: 15px 0;
  text-align: left;
}
input {
  width: 100%;
  padding: 8px;
  margin-top: 5px;
}
button {
  width: 100%;
  padding: 10px;
  background: #42b983;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}
button:disabled {
  background: #ccc;
  cursor: not-allowed;
}
</style>

4.2.2 首页组件(Home.vue)

<template>
  <div class="home-container">
    <header class="home-header">
      <h1>Pinia电商实战</h1>
      <div class="user-info">
        <img v-if="userInfo.avatar" :src="userInfo.avatar" alt="用户头像" class="avatar" />
        <span v-if="userInfo.name">欢迎您,{{ userInfo.name }}</span>
        <button @click="handleLogout" v-if="token">退出登录</button>
        <button @click="toLogin" v-else>去登录</button>
        <div class="cart-icon" @click="toCart">
          购物车({{ cartTotalCount }})
        </div>
      </header>

      <section class="goods-list">
        <h2>商品列表</h2>
        <div class="goods-item" v-for="goods in goodsList" :key="goods.id">
          <img :src="goods.img" alt="商品图片" class="goods-img" />
          <div class="goods-info">
            <h3>{{ goods.name }}</h3>
            <p class="price">¥{{ goods.price }}</p>
            <p class="stock">库存:{{ goods.stock }}</p>
            <button @click="addToCart(goods)">加入购物车</button>
          </div>
        </div>
      </section>
    </div>
</template>

<script setup lang="ts">
import { onMounted } from 'vue';
import { useUserStore } from '@/store/userStore';
import { useGoodsStore } from '@/store/goodsStore';
import { useCartStore } from '@/store/cartStore';
import { storeToRefs } from 'pinia';
import { useRouter } from 'vue-router';

// 实例化Store
const userStore = useUserStore();
const goodsStore = useGoodsStore();
const cartStore = useCartStore();
const router = useRouter();

// 响应式解构状态(避免失去响应性)
const { token, userInfo } = storeToRefs(userStore);
const { goodsList } = storeToRefs(goodsStore);
const { cartTotalCount, addToCart } = cartStore;

// 组件挂载时,获取商品列表
onMounted(() => {
  goodsStore.fetchGoodsList();
});

// 退出登录
const handleLogout = () => {
  userStore.logout();
  alert('退出成功');
  router.push('/login');
};

// 跳转登录页
const toLogin = () => {
  router.push('/login');
};

// 跳转购物车页
const toCart = () => {
  if (!token.value) {
    alert('请先登录');
    router.push('/login');
    return;
  }
  router.push('/cart');
};
</script>

<style scoped>
.home-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 10px 20px;
  border-bottom: 1px solid #eee;
}
.avatar {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  margin-right: 10px;
}
.user-info {
  display: flex;
  align-items: center;
}
.user-info button {
  margin-left: 20px;
  padding: 5px 10px;
  cursor: pointer;
}
.cart-icon {
  margin-left: 20px;
  cursor: pointer;
  font-weight: bold;
}
.goods-list {
  padding: 20px;
}
.goods-item {
  display: flex;
  margin: 20px 0;
  border-bottom: 1px solid #eee;
  padding-bottom: 20px;
}
.goods-img {
  width: 100px;
  height: 100px;
  margin-right: 20px;
}
.goods-info {
  flex: 1;
}
.price {
  color: #ff4400;
  font-size: 18px;
  font-weight: bold;
}
.stock {
  color: #666;
  margin: 10px 0;
}
.goods-info button {
  padding: 8px 15px;
  background: #ff4400;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}
</style>

4.2.3 购物车组件(Cart.vue)

<template>
  <div class="cart-container">
    <h2>我的购物车</h2>
    <div class="cart-empty" v-if="cartList.length === 0">
      购物车为空,快去添加商品吧!
    </div>
    <div class="cart-list" v-else>
      <div class="cart-item" v-for="item in cartList" :key="item.goods.id">
        <img :src="item.goods.img" alt="商品图片" class="cart-img" />
        <div class="cart-info">
          <h3>{{ item.goods.name }}</h3>
          <p class="price">¥{{ item.goods.price }}</p>
          <div class="count-control">
            <button @click="updateCount(item.goods.id, item.count - 1)" :disabled="item.count <= 1">-</button>
            <span>{{ item.count }}</span>
            <button @click="updateCount(item.goods.id, item.count + 1)" :disabled="item.count >= item.goods.stock">+</button>
          </div>
        </div>
        <button class="delete-btn" @click="removeFromCart(item.goods.id)">删除</button>
      </div>
      <div class="cart-footer">
        <button class="clear-btn" @click="clearCart">清空购物车</button>
        <div class="total-info">
          合计:<span class="total-price">¥{{ cartTotalPrice.toFixed(2) }}</span>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { useCartStore } from '@/store/cartStore';
import { useUserStore } from '@/store/userStore';
import { storeToRefs } from 'pinia';
import { onMounted } from 'vue';
import { useRouter } from 'vue-router';

const cartStore = useCartStore();
const userStore = useUserStore();
const router = useRouter();

// 响应式解构
const { cartList, cartTotalPrice } = storeToRefs(cartStore);
const { token } = storeToRefs(userStore);
const { updateCartCount, removeFromCart, clearCart } = cartStore;

// 组件挂载时,判断是否登录
onMounted(() => {
  if (!token.value) {
    alert('请先登录');
    router.push('/login');
  }
});

// 修改商品数量
const updateCount = (goodsId: number, count: number) => {
  updateCartCount(goodsId, count);
};
</script>

<style scoped>
.cart-container {
  padding: 20px;
}
.cart-empty {
  text-align: center;
  padding: 50px;
  color: #666;
  font-size: 18px;
}
.cart-item {
  display: flex;
  align-items: center;
  margin: 20px 0;
  border-bottom: 1px solid #eee;
  padding-bottom: 20px;
}
.cart-img {
  width: 80px;
  height: 80px;
  margin-right: 20px;
}
.cart-info {
  flex: 1;
}
.price {
  color: #ff4400;
  font-weight: bold;
  margin: 10px 0;
}
.count-control {
  display: flex;
  align-items: center;
}
.count-control button {
  width: 30px;
  height: 30px;
  border: 1px solid #eee;
  background: #fff;
  cursor: pointer;
}
.count-control button:disabled {
  background: #eee;
  cursor: not-allowed;
}
.count-control span {
  width: 60px;
  text-align: center;
}
.delete-btn {
  padding: 8px 15px;
  background: #ff0000;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}
.cart-footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-top: 30px;
}
.clear-btn {
  padding: 8px 15px;
  background: #666;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}
.total-info {
  font-size: 18px;
  font-weight: bold;
}
.total-price {
  color: #ff4400;
  margin-left: 10px;
}
</style>

4.3 路由配置(router/index.ts)

配置路由,实现组件跳转,需先安装vue-router:npm install vue-router@4 -S,然后配置路由:

// src/router/index.ts
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
import Login from '@/views/Login.vue';
import Home from '@/views/Home.vue';
import Cart from '@/views/Cart.vue';

const routes: RouteRecordRaw[] = [
  { path: '/', redirect: '/home' },
  { path: '/login', component: Login },
  { path: '/home', component: Home },
  { path: '/cart', component: Cart }
];

const router = createRouter({
  history: createWebHistory(),
  routes
});

export default router;

4.4 入口文件配置(main.ts)

// main.ts
import { createApp } from 'vue'
import App from './App.vue'
import { createPinia } from 'pinia'
import router from './router' // 引入路由

const app = createApp(App)
app.use(createPinia()) // 挂载Pinia
app.use(router) // 挂载路由
app.mount('#app')

4.5 实战效果说明

  1. 未登录状态下,点击「加入购物车」「购物车图标」,会提示登录并跳转登录页;

  2. 登录成功后,跳转首页,显示用户信息,可查看商品列表、添加商品到购物车;

  3. 购物车页面可修改商品数量、删除商品、清空购物车,实时显示合计价格和商品总数;

  4. 退出登录后,清空用户状态和token,购物车状态保留(可结合持久化插件优化,见下文)。

五、Pinia进阶技巧(实战必备)

5.1 数据持久化(避免页面刷新丢失)

Pinia默认不持久化数据,页面刷新后state会重置,可使用pinia-plugin-persistedstate插件实现本地存储(localStorage/sessionStorage)。

// 安装插件
npm install pinia-plugin-persistedstate -S
// main.ts 配置插件
import { createApp } from 'vue'
import App from './App.vue'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' // 引入插件
import router from './router'

const pinia = createPinia()
pinia.use(piniaPluginPersistedstate) // 挂载插件

const app = createApp(App)
app.use(pinia)
app.use(router)
app.mount('#app')

在Store中配置持久化(以购物车Store为例):

export const useCartStore = defineStore(Names.Cart, {
  state: () => ({ /* ... */ }),
  getters: { /* ... */ },
  actions: { /* ... */ },
  // 配置持久化
  persist: {
    key: 'cartStore', // 存储的key(localStorage中的key)
    storage: localStorage, // 存储方式(localStorage/sessionStorage)
    paths: ['cartList'] // 需要持久化的state字段(默认全部持久化)
  }
});

5.2 调试工具使用(Vue DevTools)

Pinia支持Vue DevTools,可实时查看Store状态、跟踪actions执行,便于调试:

  • Vue3中,安装Vue DevTools扩展,打开开发者工具,切换到「Pinia」面板,即可查看所有Store的state、getters;
  • 支持跟踪actions执行记录,可查看每次actions调用的参数和状态变化;
  • Vue3中暂不支持time-travel功能(时间回溯),Vue2中支持(需配合Vuex接口)。

5.3 模块热更新(HMR)

Pinia支持模块热更新,修改Store代码后,无需重新加载页面,即可生效,且会保留现有状态,提升开发效率,无需额外配置,Vue3项目默认支持。

六、常见问题与解决方案(实战避坑)

  • 问题1:组件中解构state后,修改值不生效? 解决方案:使用storeToRefs解构,修改时需加.value(如current.value++),直接解构会丢失响应性。
  • 问题2:actions中使用this指向异常? 解决方案:actions中的方法不能用箭头函数,需用普通函数,否则this无法指向Store实例。
  • 问题3:页面刷新后,Pinia状态丢失? 解决方案:使用pinia-plugin-persistedstate插件,配置持久化存储。
  • 问题4:多个Store之间无法通信? 解决方案:在需要通信的Store中,引入目标Store实例,即可访问其state和actions。
  • 问题5:TS类型推断失败,提示“this类型为any”? 解决方案:getters中使用this时,需指定返回值类型;actions中修改state时,确保state字段类型与赋值类型一致。
  • 问题6:Vue2中使用Pinia报错? 解决方案:Vue2中需额外引入PiniaVuePlugin,注册方式参考上传文档中的Vue2配置。

七、总结

Pinia是Vue3官方推荐的状态管理工具,相比Vuex,它更简洁、轻量、易上手,原生支持TS,完美适配组合式API,是Vue3项目的首选状态管理方案。

本文从基础认知、环境搭建、核心用法,到完整实战案例,覆盖了Pinia开发的全流程,重点讲解了Vue3+TS下的实战技巧,新手可按步骤搭建环境、编写代码,快速上手;老手可通过实战案例查漏补缺,优化项目中的状态管理逻辑。

核心要点:Store是Pinia的核心,每个Store包含state、getters、actions;修改state推荐使用$patch和actions;解构state需用storeToRefs保持响应性;结合插件可实现数据持久化,提升用户体验。

React 18+ 所有Hooks全解析(含实战示例,新手零踩坑)

2026年4月14日 15:24

React Hooks 是React 16.8+ 引入的核心特性,彻底替代了类组件,让函数组件拥有状态管理、副作用处理、性能优化等能力。本文整理了React 18+ 全部内置Hooks,按「基础必用、副作用、状态进阶、引用、上下文、性能优化、并发、工具类」分类,每个Hook都包含「核心作用、语法、实战示例、适用场景、注意事项」,覆盖日常开发99%的场景,新手可直接套用,老手可查漏补缺。

核心原则:所有Hooks仅能在「函数组件顶层」或「自定义Hook顶层」调用,不能在条件判断、循环、嵌套函数中调用(React通过调用顺序识别Hooks,避免逻辑混乱)。

一、基础必用Hooks(入门必备,100%高频)

这类Hooks是React开发的基础,几乎所有函数组件都会用到,必须熟练掌握。

1. useState —— 函数组件状态管理(核心)

核心作用

给函数组件添加响应式状态,返回「当前状态值 + 状态更新函数」,是最基础、最常用的Hook。

语法

// 基础用法:初始值直接传入
const [state, setState] = useState(initialState);

// 函数式初始值:仅首次渲染执行(适合昂贵计算,避免每次渲染重复计算)
const [state, setState] = useState(() => {
  return computeInitialValue(); // 昂贵计算逻辑
});

// 函数式更新:依赖上一次的状态值(避免闭包陷阱)
setState(prevState => newState);

实战示例

import { useState } from 'react';

// 示例1:简单计数器(基础状态)
function Counter() {
  // 声明count状态,初始值为0,setCount为更新函数
  const [count, setCount] = useState(0);
  
  // 函数式更新:依赖上一次的count值,避免闭包问题
  const increment = () => setCount(prev => prev + 1);
  const decrement = () => setCount(prev => prev - 1);
  
  return (
    
      当前计数:{count}
      <button onClick={increment}>+1<button onClick={-1
  );
}

// 示例2:对象状态(需注意更新方式)
function UserForm() {
  // 声明对象类型状态
  const [user, setUser] = useState({ name: '', age: 18 });
  
  // 对象更新:必须返回新对象(不能直接修改原对象,React无法检测变化)
  const updateName = (e) => {
    setUser(prev => ({ ...prev, name: e.target.value }));
  };
  
  return (
    <input value={姓名" />
      姓名:{user.name},年龄:{user.age}
  );
}

适用场景

简单响应式状态管理:计数器、开关(布尔值)、输入框值、弹窗显示/隐藏、简单数组/对象状态。

注意事项

  • 状态更新是「替换而非合并」:如果状态是对象/数组,必须返回新引用(使用扩展运算符...mapfilter等),不能直接修改原状态(如user.name = 'xxx'无效)。
  • 初始值仅首次渲染生效:即使初始值是函数,也只会在组件首次挂载时执行一次,后续渲染不会重复执行。
  • 避免闭包陷阱:如果更新状态依赖上一次的状态值(如计数器累加),必须使用「函数式更新」(prev => prev + 1),否则可能获取到旧状态。
  • 状态更新是异步的:React会批量处理状态更新,在同一个事件处理函数中多次调用setState,只会触发一次渲染(如setCount(1); setCount(2);最终只显示2)。

2. useEffect —— 副作用处理(核心)

核心作用

处理函数组件的「副作用」,替代类组件的componentDidMount(挂载)、componentDidUpdate(更新)、componentWillUnmount(卸载)三个生命周期钩子,统一管理副作用逻辑。

常见副作用:数据请求、DOM操作、定时器/计时器、事件监听、订阅(如WebSocket)等。

语法

// setup:副作用函数,执行副作用逻辑
// dependencies:依赖数组,控制setup函数的执行时机(可选)
useEffect(setup, dependencies?);

// setup函数可返回一个「清理函数」,用于组件卸载/依赖变化时清理副作用
useEffect(() => {
  // 副作用逻辑(如请求、定时器)
  return () => {
    // 清理逻辑(如取消请求、清除定时器、移除事件监听)
  };
}, [dependencies]);

执行时机(关键)

  • 不传递依赖数组(useEffect(setup)):每次组件渲染(挂载+更新)都会执行setup函数,慎用(性能较差)。
  • 传递空依赖数组(useEffect(setup, [])):仅在组件挂载时执行一次setup函数,卸载时执行清理函数(对应componentDidMount + componentWillUnmount)。
  • 传递依赖数组(useEffect(setup, [a, b])):组件挂载时执行一次,后续只有当依赖数组中的ab发生变化时,才会执行setup函数(对应componentDidMount + componentDidUpdate),卸载时执行清理函数。

实战示例

import { useState, useEffect } from 'react';

// 示例1:数据请求(挂载时请求,卸载时取消请求)
function UserList() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    // 创建AbortController,用于取消请求
    const controller = new AbortController();
    const signal = controller.signal;

    // 异步请求函数
    const fetchUsers = async () => {
      try {
        const res = await fetch('/api/users', { signal });
        const data = await res.json();
        setUsers(data);
      } catch (err) {
        // 忽略取消请求导致的错误
        if (err.name !== 'AbortError') {
          console.error('请求失败:', err);
        }
      } finally {
        setLoading(false);
      }
    };

    fetchUsers();

    // 清理函数:组件卸载时取消请求
    return () => controller.abort();
  }, []); // 空依赖,仅挂载执行

  if (loading) return 加载中...;
  return (
    
      {users.map(user => (
        <div key={{user.name}
      ))}
  );
}

// 示例2:定时器(挂载时启动,卸载时清除)
function Timer() {
  const [time, setTime] = useState(0);

  useEffect(() => {
    // 启动定时器
    const timer = setInterval(() => {
      setTime(prev => prev + 1);
    }, 1000);

    // 清理函数:卸载时清除定时器(避免内存泄漏)
    return () => clearInterval(timer);
  }, []); // 空依赖,仅挂载执行

  return 当前时间:{time}秒;
}

适用场景

数据请求、DOM操作(如设置文档标题、操作DOM元素)、定时器/计时器、事件监听(如window.addEventListener)、订阅(如WebSocket连接)、清理无用资源(避免内存泄漏)。

注意事项

  • 清理函数的执行时机:组件卸载时、setup函数再次执行前(依赖变化时),必须清理可能导致内存泄漏的资源(定时器、事件监听、WebSocket、未完成的请求)。
  • 依赖数组必须完整:如果setup函数中使用了组件内的变量、状态、函数,必须将其加入依赖数组,否则可能获取到旧值(闭包陷阱);若依赖数组为空,setup函数中不能使用任何组件内的动态值。
  • 避免无限循环:如果setup函数中修改了依赖数组中的值,会导致依赖变化,再次触发setup函数,形成无限循环(如useEffect(() => setCount(prev => prev + 1), [count]))。
  • 副作用函数是异步的:不能直接给setup函数加async(会导致清理函数失效),若需执行异步逻辑,需在setup函数内部定义异步函数并调用。

二、副作用进阶Hooks(18+新增,替代useEffect特定场景)

React 18 新增了两个副作用Hook,专门解决useEffect的部分痛点,针对性更强,在特定场景下更推荐使用。

1. useLayoutEffect —— 同步DOM副作用

核心作用

useEffect功能类似,区别在于「执行时机」:useLayoutEffect在DOM更新后、浏览器绘制前同步执行,useEffect在浏览器绘制后异步执行。适合需要同步操作DOM、避免页面闪烁的场景。

语法

useLayoutEffect(setup, dependencies?);
// 语法与useEffect完全一致,仅执行时机不同

实战示例

import { useLayoutEffect, useState, useRef } from 'react';

// 示例:同步调整DOM位置,避免页面闪烁
function LayoutDemo() {
  const [width, setWidth] = useState(0);
  const ref = useRef(null);

  // useLayoutEffect:DOM更新后立即执行,同步获取DOM尺寸并调整
  useLayoutEffect(() => {
    if (ref.current) {
      // 同步获取DOM宽度
      setWidth(ref.current.offsetWidth);
      // 同步调整DOM位置(避免闪烁)
      ref.current.style.marginLeft = `${(window.innerWidth - width) / 2}px`;
    }
  }, [width]);

  return <div ref={0px', background: 'red' }} />;
}

适用场景

需要同步操作DOM、获取DOM尺寸/位置、调整DOM样式(避免页面闪烁)的场景,如弹窗居中、滚动位置调整、DOM尺寸监听。

注意事项

  • 执行时机:DOM更新后、浏览器绘制前,同步执行,会阻塞浏览器绘制,若逻辑过久会影响页面性能,慎用。
  • 与useEffect的区别:大部分场景用useEffect即可,只有需要同步操作DOM、避免闪烁时,才用useLayoutEffect
  • 服务器端渲染(SSR):useLayoutEffect在服务器端不会执行,若需兼容SSR,优先用useEffect,或在客户端判断后执行。

2. useInsertionEffect —— CSS-in-JS 专用副作用

核心作用

React 18 新增,专门用于 CSS-in-JS 库(如Styled Components、Emotion),在DOM更新前插入CSS样式,避免样式闪烁,是useLayoutEffect的补充。

语法

useInsertionEffect(setup, dependencies?);
// 语法与useEffect、useLayoutEffect一致,执行时机不同

执行时机

DOM更新前执行,早于useLayoutEffect,专门用于插入CSS样式,确保样式在DOM渲染前生效,避免样式闪烁。

适用场景

仅用于 CSS-in-JS 库的样式插入,普通开发场景几乎用不到,日常开发可忽略。

注意事项

  • 不能访问DOM:执行时机在DOM更新前,无法获取DOM元素(如ref.currentnull),仅能插入CSS样式。
  • 非CSS-in-JS不用:普通CSS、Sass/Less等场景,无需使用,用useEffectuseLayoutEffect即可。

三、状态进阶Hooks(复杂状态管理)

当状态逻辑复杂(如依赖多个状态、状态更新依赖其他状态)时,使用这类Hook,简化状态管理逻辑。

1. useReducer —— 复杂状态管理(替代useState)

核心作用

用于管理复杂状态(如状态是对象/数组、状态更新依赖多个因素、多个状态相互关联),通过「 reducer 函数」统一处理状态更新逻辑,类似Redux的核心思想,比useState更适合复杂场景。

语法

// reducer:状态更新函数,接收当前状态和动作,返回新状态
// initialState:初始状态
// init:可选,初始化函数,用于延迟初始化状态(适合昂贵计算)
const [state, dispatch] = useReducer(reducer, initialState, init?);

// reducer函数格式
function reducer(state, action) {
  switch (action.type) {
    case 'TYPE1':
      return newState1; // 根据action返回新状态
    case 'TYPE2':
      return newState2;
    default:
      return state; // 默认返回原状态,避免报错
  }
}

实战示例

import { useReducer, useState } from 'react';

// 1. 定义reducer函数:统一处理状态更新
function todoReducer(state, action) {
  switch (action.type) {
    case 'ADD_TODO':
      // 添加待办事项,返回新数组(不能修改原数组)
      return [...state, { id: Date.now(), text: action.payload, done: false }];
    case 'TOGGLE_TODO':
      // 切换待办状态
      return state.map(todo => 
        todo.id === action.payload ? { ...todo, done: !todo.done } : todo
      );
    case 'DELETE_TODO':
      // 删除待办事项
      return state.filter(todo => todo.id !== action.payload);
    default:
      return state;
  }
}

// 2. 使用useReducer管理状态
function TodoList() {
  // 初始状态:空数组
  const [todos, dispatch] = useReducer(todoReducer, []);
  const [text, setText] = useState('');

  // 触发状态更新:通过dispatch发送action
  const addTodo = () => {
    if (!text.trim()) return;
    dispatch({ type: 'ADD_TODO', payload: text }); // payload:传递参数
    setText('');
  };

  return (
    <input value={e) => setText(e.target.value)} />
      <button onClick={}>添加待办
        {todos.map(todo => (
         <li 
            key={one ? 'line-through' : 'none' }}
            onClick={() => dispatch({ type: 'TOGGLE_TODO', payload: todo.id })}
          >
            {todo.text}<button onClick={ dispatch({ type: 'DELETE_TODO', payload: todo.id })}>删除
        ))}
      
  );
}

适用场景

复杂状态管理:待办列表、购物车、表单多字段状态、多个状态相互关联(如登录状态+用户信息+权限)。

注意事项

  • reducer必须是纯函数:不能修改原状态(必须返回新状态),不能包含副作用(如请求、DOM操作),仅处理状态更新逻辑。
  • action格式规范:推荐使用{ type: 'XXX', payload: 数据 }的格式,type用大写字符串,便于维护和调试。
  • 与useState的选择:简单状态用useState,复杂状态(多状态关联、多更新逻辑)用useReducer
  • 延迟初始化:如果初始状态需要昂贵计算,可使用第三个参数init函数,仅首次渲染执行一次。

四、引用Hook(获取DOM/保存持久值)

用于获取DOM元素、保存组件渲染周期内的持久值(不随渲染变化),核心是useRef,衍生出useImperativeHandle用于暴露组件内部方法。

1. useRef —— 获取DOM/保存持久值(高频)

核心作用

有两个核心用途:① 获取DOM元素或组件实例;② 保存组件渲染周期内的持久值(不随渲染重新初始化,修改不会触发组件重新渲染)。

语法

// initialValue:初始值,可选(默认undefined)
const ref = useRef(initialValue);

// 获取DOM:通过ref.current访问DOM元素
// 保存持久值:通过ref.current修改/访问,修改不会触发渲染

实战示例

import { useRef, useState, useEffect } from 'react';

// 示例1:获取DOM元素(如输入框聚焦)
function InputFocus() {
  // 创建ref,绑定到输入框
  const inputRef = useRef(null);

  useEffect(() => {
    // 组件挂载时,让输入框自动聚焦(访问ref.current获取DOM)
    inputRef.current?.focus();
  }, []);

  return <input ref={聚焦" />;
}

// 示例2:保存持久值(不触发渲染)
function TimerWithRef() {
  const [time, setTime] = useState(0);
  // 保存定时器ID,持久化存储,不随渲染变化
  const timerRef = useRef(null);

  useEffect(() => {
    // 启动定时器,将ID存入ref
    timerRef.current = setInterval(() => {
      setTime(prev => prev + 1);
    }, 1000);

    // 清理定时器:从ref中获取ID
    return () => clearInterval(timerRef.current);
  }, []);

  // 暂停定时器:修改ref.current,不会触发组件渲染
  const pause = () => {
    clearInterval(timerRef.current);
  };

  return (
    时间:{time}秒<button onClick={暂停 );
}

适用场景

获取DOM元素(输入框聚焦、获取DOM尺寸)、保存定时器ID、保存WebSocket连接、保存组件渲染周期内的临时值(不触发渲染)。

注意事项

  • ref.current修改不触发渲染:与useState不同,修改ref.current的值不会导致组件重新渲染,适合保存不需要触发渲染的临时值。
  • DOM ref的初始化时机:组件挂载完成后,ref.current才会指向DOM元素,挂载前为null,需用?.避免报错。
  • 函数组件不能直接获取实例:如果给函数组件绑定ref,需配合forwardRefuseImperativeHandle,才能暴露组件内部方法/属性。

2. useImperativeHandle —— 暴露组件内部方法(进阶)

核心作用

配合forwardRef使用,让父组件通过ref获取子组件的内部方法/属性,自定义暴露给父组件的内容,避免暴露整个子组件实例,提升封装性。

语法

// ref:父组件传递的ref
// createHandle:函数,返回暴露给父组件的方法/属性对象
// dependencies:依赖数组,依赖变化时重新生成暴露的内容
useImperativeHandle(ref, createHandle, dependencies?);

// 必须配合forwardRef包裹子组件,才能接收父组件传递的ref
const Child = forwardRef((props, ref) => {
  useImperativeHandle(ref, () => ({
    // 暴露给父组件的方法/属性
    method1: () => {},
    property1: ''
  }), []);
  return 子组件;
});

实战示例

import { useRef, forwardRef, useImperativeHandle } from 'react';

// 子组件:配合forwardRef和useImperativeHandle,暴露内部方法
const ChildInput = forwardRef((props, ref) => {
  const inputRef = useRef(null);

  // 暴露给父组件的方法:聚焦输入框、清空输入框
  useImperativeHandle(ref, () => ({
    focus: () => inputRef.current?.focus(),
    clear: () => inputRef.current.value = ''
  }), []); // 空依赖,暴露的方法不随依赖变化

  return <input ref={" />;
});

// 父组件:通过ref调用子组件暴露的方法
function Parent() {
  const childRef = useRef(null);

  const handleFocus = () => {
    // 调用子组件暴露的focus方法
    childRef.current?.focus();
  };

  const handleClear = () => {
    // 调用子组件暴露的clear方法
    childRef.current?.clear();
  };

  return (
    <ChildInput ref={childRef} />
      <button onClick={聚焦输入框<button onClick={清空输入框
  );
}

适用场景

父组件需要调用子组件内部方法的场景:输入框聚焦/清空、弹窗显示/隐藏、子组件表单提交等。

注意事项

  • 必须配合forwardRef:子组件必须用forwardRef包裹,才能接收父组件传递的ref,否则无法使用useImperativeHandle
  • 避免过度暴露:仅暴露父组件需要的方法/属性,不要暴露整个子组件的内部状态,保持子组件的封装性。
  • 依赖数组:如果暴露的方法依赖子组件的状态/ props,必须将其加入依赖数组,否则可能获取到旧值。

五、上下文Hook(跨组件状态共享)

用于跨组件共享状态(无需逐层传递props),核心是useContext,配合createContext使用,替代类组件的Context API

1. useContext —— 跨组件状态共享(高频)

核心作用

接收一个Context对象(由createContext创建),获取Context中共享的状态和方法,实现跨组件(父子、祖孙、兄弟)状态共享,无需逐层传递props(解决props drilling问题)。

语法

// 1. 创建Context对象(可设置默认值,可选)
const MyContext = createContext(defaultValue);

// 2. 用Provider包裹组件树,提供共享状态
<MyContext.Provider value={共享的状态/方法}>
  子组件树
</MyContext.Provider>

// 3. 在子组件中使用useContext获取共享内容
const context = useContext(MyContext);

实战示例

import { createContext, useContext, useState } from 'react';

// 1. 创建Context(主题上下文,默认值为light)
const ThemeContext = createContext('light');

// 2. 顶层组件:提供共享状态(主题切换)
function App() {
  const [theme, setTheme] = useState('light');

  // 切换主题的方法
  const toggleTheme = () => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light');
  };

  // 用Provider包裹子组件,传递共享的theme和toggleTheme
  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      <div style={light' ? '#fff' : '#333',
        color: theme === 'light' ? '#333' : '#fff',
        minHeight: '100vh'
      }}>
        <Header />
        <Content />
      </ThemeContext.Provider>
  );
}

// 3. 子组件Header:使用useContext获取共享主题
function Header() {
  const { theme } = useContext(ThemeContext);
  return 当前主题:{theme};
}

// 4. 子组件Content:使用useContext获取共享方法,切换主题
function Content() {
  const { toggleTheme } = useContext(ThemeContext);
  return <button onClick={切换主题;
}

适用场景

跨组件状态共享:主题切换、用户登录状态、语言切换、全局配置(如接口基础地址)等。

注意事项

  • 必须有Provider包裹:如果子组件使用useContext,但没有被对应的Context.Provider包裹,会获取到Context的默认值(注意:默认值仅在没有Provider时生效,不是 fallback 值)。
  • 状态更新会触发所有使用该Context的组件重新渲染:如果共享状态频繁更新,可能导致性能问题,可配合useMemo优化。
  • Context嵌套:多个Context可以嵌套使用,子组件会获取最近的Provider提供的值。
  • TypeScript适配:创建Context时,建议指定类型,避免类型错误(如createContext<{ theme: string; toggleTheme: () => void } | undefined>(undefined))。

六、性能优化Hooks(避免不必要渲染)

React组件默认会在父组件渲染时重新渲染,这类Hook用于优化性能,避免不必要的渲染,提升组件运行效率。

1. useMemo —— 缓存计算结果(高频)

核心作用

缓存「昂贵计算的结果」,仅当依赖数组中的值发生变化时,才重新计算结果;否则直接返回缓存的结果,避免每次渲染都重复执行昂贵计算,提升性能。

语法

// compute:需要缓存结果的计算函数(返回计算结果)
// dependencies:依赖数组,仅当依赖变化时,重新执行compute
const memoizedValue = useMemo(compute, dependencies);

实战示例

import { useMemo, useState } from 'react';

// 示例:缓存昂贵计算结果(如数组排序、过滤)
function ExpensiveCompute() {
  const [list, setList] = useState([3, 1, 4, 1, 5, 9, 2, 6]);
  const [count, setCount] = useState(0);

  // 昂贵计算:数组排序(假设数组很大,排序耗时)
  // useMemo缓存排序结果,仅当list变化时重新排序
  const sortedList = useMemo(() => {
    console.log('重新排序'); // 仅list变化时打印
    return [...list].sort((a, b) => a - b);
  }, [list]); // 依赖list,count变化不重新计算

  return (
   排序后:{sortedList.join(',')}<button onClick={ setCount(prev => prev + 1)}>计数:{count}<button onClick={ setList([...list, Math.random()])}>添加元素
  );
}

适用场景

有昂贵计算的场景:数组排序、过滤、复杂数据处理、大量DOM节点渲染前的计算等。

注意事项

  • 不要过度使用:简单计算(如a + b)无需使用useMemo,缓存本身也有性能开销,仅用于昂贵计算。
  • 依赖数组必须完整:compute函数中使用的所有变量、状态、props,都必须加入依赖数组,否则可能返回缓存的旧结果。
  • 返回值是缓存的结果:useMemo返回的是计算结果,不是函数,与useCallback(缓存函数)区分开。

2. useCallback —— 缓存函数(高频)

核心作用

缓存函数引用,仅当依赖数组中的值发生变化时,才重新创建函数;否则直接返回缓存的函数引用,避免因函数引用变化导致子组件不必要的重新渲染(尤其配合React.memo使用)。

语法

// callback:需要缓存的函数
// dependencies:依赖数组,仅当依赖变化时,重新创建函数
const memoizedCallback = useCallback(callback, dependencies);

实战示例

import { useCallback, useState, memo } from 'react';

// 子组件:用memo包裹,避免不必要的渲染(仅props变化时渲染)
const Child = memo(({ onClick, name }) => {
  console.log('子组件渲染'); // 仅onClick或name变化时打印
  return <button onClick={{name};
});

// 父组件:用useCallback缓存函数,避免子组件频繁渲染
function Parent() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('按钮');

  // 缓存onClick函数,仅当name变化时重新创建函数
  const handleClick = useCallback(() => {
    console.log('点击按钮:', name);
  }, [name]); // 依赖name,count变化不重新创建函数

  return (
    <Child onClick={handleClick} name={name} />
      <button onClick={ setCount(prev => prev + 1)}>计数:{count}<button onClick={ setName('新按钮')}>修改按钮名称
  );
}

适用场景

父组件向子组件传递函数,且子组件用React.memo包裹的场景:避免因父组件渲染导致函数引用变化,进而触发子组件不必要的渲染。

注意事项

  • 配合React.memo使用:useCallback本身不会阻止父组件渲染,需配合React.memo(包裹子组件),才能避免子组件不必要的渲染。
  • 依赖数组必须完整:缓存的函数中使用的所有变量、状态、props,都必须加入依赖数组,否则可能获取到旧值(闭包陷阱)。
  • 与useMemo的区别:useCallback缓存函数引用,useMemo缓存计算结果;useCallback(fn, deps) 等价于 useMemo(() => fn, deps)

3. useMemoizedFn —— 缓存函数(无依赖,18+新增)

核心作用

React 18 新增(需导入react-dom),用于缓存函数引用,无需依赖数组,函数内部能获取到最新的组件状态/ props,避免闭包陷阱,比useCallback更简洁。

语法

import { useMemoizedFn } from 'react-dom';

// callback:需要缓存的函数,无需依赖数组
const memoizedFn = useMemoizedFn(callback);

实战示例

import { useState, memo } from 'react';
import { useMemoizedFn } from 'react-dom';

const Child = memo(({ onClick }) => {
  console.log('子组件渲染');
  return <button onClick={点击;
});

function Parent() {
  const [count, setCount] = useState(0);

  // 无需依赖数组,函数内部能获取到最新的count
  const handleClick = useMemoizedFn(() => {
    console.log('当前计数:', count); // 始终获取最新count
  });

  return (
    <Child onClick={handleClick} />
     <button onClick={ setCount(prev => prev + 1)}>计数:{count}
  );
}

适用场景

需要缓存函数,且函数内部依赖组件状态/ props,不想手动维护依赖数组的场景(简化useCallback的使用)。

注意事项

  • 无需依赖数组:函数内部能自动获取最新的组件状态/ props,无需手动添加依赖,避免遗漏依赖导致的闭包陷阱。
  • 需导入react-dom:useMemoizedFn不是从react导入,而是从react-dom导入,注意导入路径。
  • React版本要求:仅支持React 18+,低版本React无法使用。

七、并发模式Hooks(React 18+ 新增,并发渲染)

React 18 引入并发渲染机制,这类Hook用于配合并发模式,控制组件的渲染优先级、中断/恢复渲染,提升用户体验。

1. useTransition —— 低优先级更新(高频)

核心作用

将某些状态更新标记为「低优先级」,让高优先级更新(如输入框输入、按钮点击)优先执行,避免低优先级更新(如大量数据渲染)阻塞UI,提升用户体验。

语法

// startTransition:函数,包裹低优先级更新逻辑
// isPending:布尔值,标记低优先级更新是否正在进行(可用于显示加载状态)
const [isPending, startTransition] = useTransition();

// 使用:将低优先级更新逻辑放入startTransition
startTransition(() => {
  setLowPriorityState(newValue);
});

实战示例

import { useTransition, useState } from 'react';

// 示例:输入框搜索(高优先级)+ 搜索结果渲染(低优先级)
function Search() {
  const [searchText, setSearchText] = useState('');
  const [results, setResults] = useState([]);
  const [isPending, startTransition] = useTransition();

  // 输入框变化:高优先级更新,优先执行,不阻塞输入
  const handleInputChange = (e) => {
    const value = e.target.value;
    setSearchText(value);

    // 搜索结果渲染:低优先级更新,在高优先级更新完成后执行
    startTransition(() => {
      // 模拟昂贵的搜索逻辑(如过滤大量数据)
      const filteredResults = Array(10000).fill(0).filter((_, index) => 
        index.toString().includes(value)
      );
      setResults(filteredResults);
    });
  };

  return (
    <input 
        type="text" 
        value={ 
        onChange={handleInputChange} 
        placeholder="搜索"
      />
      {/* 低优先级更新正在进行时,显示加载状态 */}
      {isPending && 加载中...}
      
        {results.map((item, index) => (
          <li key={{index}
        ))}
     
  );
}

适用场景

高优先级操作与低优先级操作并存的场景:输入框搜索、下拉框筛选、大量数据渲染、路由切换前的准备工作等。

注意事项

  • 低优先级更新可被中断:如果有更高优先级的更新(如输入框输入),低优先级更新会被中断,等高优先级更新完成后再恢复,避免阻塞UI。
  • 不能用于紧急更新:如表单提交、按钮点击后的状态更新(高优先级),不能用useTransition,否则会导致更新延迟。
  • isPending状态:用于显示低优先级更新的加载状态,提升用户体验,避免用户误以为页面卡死。

2. useDeferredValue —— 延迟更新值(进阶)

核心作用

useTransition类似,用于延迟更新某个值,将其标记为低优先级,优先保证高优先级更新的响应速度;区别在于useDeferredValue是“延迟值”,useTransition是“延迟更新逻辑”。

语法

// value:需要延迟的原始值
// options:可选配置,如timeoutMs(延迟时间)
const deferredValue = useDeferredValue(value, options?);

实战示例

import { useDeferredValue, useState } from 'react';

// 示例:输入框(高优先级)+ 延迟显示输入值(低优先级)
function DeferredDemo() {
  const [inputValue, setInputValue] = useState('');
  // 延迟更新inputValue,优先保证输入框响应
  const deferredInput = useDeferredValue(inputValue);

  return (
    <input 
        type="text" 
        value={        onChange={(e) => setInputValue(e.target.value)}
        placeholder="输入内容"
      />
      {/* 延迟显示输入值,避免输入时卡顿 */}
      延迟显示:{deferredInput}
  );
}

适用场景

需要延迟显示某个值,优先保证高优先级操作的场景:输入框实时显示、大量数据渲染时的延迟更新等,与useTransition互补。

注意事项

  • 与useTransition的区别:useDeferredValue是延迟“值”,useTransition是延迟“更新逻辑”;useDeferredValue更适合简单的“值延迟”场景,useTransition更适合复杂的“更新逻辑延迟”场景。
  • 延迟值是只读的:deferredValue是延迟后的只读值,不能直接修改,修改需通过原始值(如inputValue)。

八、工具类Hooks(辅助开发,低频但实用)

这类Hooks用于辅助开发,解决特定场景的问题,使用频率较低,但需要时能大幅简化代码。

1. useDebugValue —— 自定义Hook调试(调试用)

核心作用

用于自定义Hook中,在React开发者工具中显示自定义Hook的调试信息,方便调试,不影响组件功能。

语法

// label:调试信息(字符串或函数,函数返回调试信息)
useDebugValue(label);

// 示例:自定义Hook中使用
function useCustomHook() {
  // 自定义Hook逻辑
  useDebugValue('自定义Hook调试信息');
  return ...;
}

适用场景

开发自定义Hook时,用于调试,方便在React开发者工具中查看自定义Hook的状态和信息。

注意事项

  • 仅用于调试:生产环境中,useDebugValue会被自动移除,不会影响性能。
  • 适合复杂自定义Hook:简单自定义Hook无需使用,复杂自定义Hook(多状态、多逻辑)使用,提升调试效率。

2. useId —— 生成唯一ID(高频)

核心作用

生成跨渲染、跨组件的唯一ID,用于表单标签关联(labelfor属性与inputid属性)、 accessibility(无障碍)开发,避免ID重复。

语法

// 生成唯一ID
const id = useId();

// 生成带前缀的唯一ID(多个ID关联时使用)
const id = useId();
const inputId = `${id}-input`;
const labelId = `${id}-label`;

实战示例

import { useId } from 'react';

// 示例1:表单标签关联,生成唯一ID
function FormInput() {
  // 生成唯一ID
  const id = useId();

  return (

      {/* label
昨天以前首页

23种Promise高效用法,彻底搞定前端异步痛点,新手也能秒上手

2026年4月13日 11:47

前端人必看!你是不是也被异步操作逼疯过😭

回调地狱嵌套一层又一层,代码乱得像“蜘蛛网”;并发请求太多导致接口拥堵,页面卡顿崩溃;异步请求超时、无法取消,报错无从排查;不知道怎么顺序/并行执行异步任务,越写越乱……

其实这些前端异步痛点,Promise早就给出了完美解决方案!作为JS异步编程的核心,Promise不仅能替代繁琐的回调函数,还能优雅处理并发、超时、重试等复杂场景,是前端工程师必备的核心技能,也是面试高频考点。

今天这篇文章,结合实战场景,把23种Promise高效用法逐一拆解,每一种都配「核心作用+完整可复制代码+通俗解读+实战场景」,从基础到进阶,小白能看懂,老手能查漏补缺,收藏这一篇,再也不用东拼西凑找资料,异步开发效率直接翻倍!

核心提醒:Promise的核心是“异步操作的状态管理”,三种状态(pending/fulfilled/rejected)一旦改变就无法逆转,掌握它的核心用法,能解决80%的前端异步问题,剩下20%靠这23种实战技巧搞定!

一、基础核心用法(3种)—— 入门必备,筑牢基础

这3种是Promise最基础、最常用的用法,是后续高级用法的前提,新手务必先掌握,熟练运用就能告别基础异步烦恼。

1. 使用async/await简化Promise(最常用)

核心作用:用同步代码的写法,实现异步操作,彻底告别回调地狱,代码逻辑更清晰、更易维护,是日常开发中最推荐的异步写法。

通俗解读:async关键字标记函数为异步函数,await关键字“等待”异步操作完成,只有await后面的异步操作结束,才会执行下一行代码,避免嵌套。

// 基础用法:处理单个异步请求
async function asyncFunction() {
  try {
    // 等待异步操作完成(比如接口请求)
    const result = await fetch('/api/data');
    const data = await result.json();
    // 异步操作成功后,执行后续逻辑
    console.log('接口请求成功:', data);
    return data;
  } catch (error) {
    // 捕获异步操作中的所有错误
    console.error('接口请求失败:', error);
  }
}

// 实战用法:结合业务场景(获取用户信息并渲染)
async function getUserInfo() {
  try {
    const response = await fetch('/api/user/info');
    const user = await response.json();
    // 渲染用户信息到页面
    document.querySelector('.user-name').textContent = user.name;
    document.querySelector('.user-age').textContent = user.age;
  } catch (err) {
    // 错误处理(比如提示用户获取失败)
    alert('获取用户信息失败,请重试!');
  }
}
// 调用异步函数
getUserInfo();

2. 使用Promises替代回调函数

核心作用:将传统的回调函数(回调地狱的根源),转化为Promise链式调用,代码更优雅,错误处理更统一。

通俗解读:很多老项目或原生API仍使用回调函数(比如setTimeout、fs.readFile),通过封装,将其转化为Promise,就能享受链式调用的便捷。

// 核心封装:将回调函数转为Promise
const callbackToPromise = (fn, ...args) => {
  return new Promise((resolve, reject) => {
    // 执行原回调函数,在回调中触发Promise的resolve/reject
    fn(...args, (error, result) => {
      if (error) {
        // 回调报错,触发reject
        reject(error);
      } else {
        // 回调成功,触发resolve
        resolve(result);
      }
    });
  });
};

// 实战用法:封装setTimeout(回调转Promise)
const delay = callbackToPromise(setTimeout, 1000);
// 链式调用,替代回调嵌套
delay.then(() => {
  console.log('1秒后执行');
  return callbackToPromise(setTimeout, 2000);
}).then(() => {
  console.log('再等2秒执行');
}).catch(err => {
  console.error('出错了:', err);
});

3. 使用Promise实现一个延时函数

核心作用:异步延时操作,常用于防抖、节流、模拟接口请求延迟等场景,比原生setTimeout更灵活,可结合await使用。

// 基础封装:异步延时函数
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));

// 实战用法1:结合await使用(同步写法实现延时)
async function delayDemo() {
  console.log('开始执行');
  await delay(1500); // 延时1.5秒
  console.log('1.5秒后执行');
  await delay(2000); // 再延时2秒
  console.log('总共延时3.5秒后执行');
}

// 实战用法2:模拟接口请求延迟
async function mockFetch() {
  console.log('正在请求接口...');
  // 模拟接口延迟1秒
  await delay(1000);
  return { code: 200, data: '模拟接口返回数据', msg: 'success' };
}

mockFetch().then(res => console.log(res)).catch(err => console.error(err));

二、并发控制用法(4种)—— 解决多异步拥堵,提升性能

日常开发中,经常需要同时执行多个异步请求(比如批量获取数据),但并发过多会导致接口拥堵、页面卡顿,这4种用法能优雅控制并发,兼顾效率和性能。

4. 映射并发Promises并处理结果数组(Promise.all)

核心作用:并行执行多个Promise,等待所有Promise都成功(fulfilled)后,返回所有结果的数组;只要有一个失败(rejected),就立即触发catch,适合“所有异步操作都必须成功”的场景。

// 基础用法:并发请求多个接口
const fetchUrls = urls => {
  // 映射所有url为fetch Promise
  const fetchPromises = urls.map(url => 
    fetch(url).then(response => response.json())
  );
  // 等待所有Promise执行完成
  return Promise.all(fetchPromises);
};

// 实战用法:批量获取用户列表、商品列表、分类列表
const urls = [
  '/api/users',
  '/api/goods',
  '/api/categories'
];

fetchUrls(urls).then(results => {
  const [users, goods, categories] = results;
  console.log('用户列表:', users);
  console.log('商品列表:', goods);
  console.log('分类列表:', categories);
  // 所有数据获取完成后,渲染页面
}).catch(error => {
  console.error('某个接口请求失败:', error);
});

5. 并发控制(限制并发数量)

核心作用:Promise.all会同时执行所有Promise,当并发数量过多(比如几十上百个请求),会给服务器造成压力,通过此方法可限制同时执行的Promise数量,避免拥堵。

// 核心封装:并发控制函数
const concurrentPromises = (promises, limit) => {
  return new Promise((resolve, reject) => {
    let i = 0; // 当前执行到的Promise索引
    let result = []; // 存储所有Promise的结果
    // 执行单个Promise的函数
    const executor = () => {
      // 所有Promise都执行完成,返回结果
      if (i >= promises.length) {
        return resolve(result);
      }
      // 取出当前要执行的Promise
      const promise = promises[i++];
      // 执行Promise并处理结果
      Promise.resolve(promise)
        .then(value => {
          result.push(value);
          // 继续执行下一个Promise(递归调用)
          if (i < promises.length) {
            executor();
          } else {
            // 最后一个Promise执行完成,返回结果
            resolve(result);
          }
        })
        .catch(reject); // 任意一个Promise失败,直接触发reject
    };

    // 初始化,同时执行limit个Promise
    for (let j = 0; j < limit && j < promises.length; j++) {
      executor();
    }
  });
};

// 实战用法:限制最多同时执行3个请求
const allPromises = [
  fetch('/api/data1').then(res => res.json()),
  fetch('/api/data2').then(res => res.json()),
  fetch('/api/data3').then(res => res.json()),
  fetch('/api/data4').then(res => res.json()),
  fetch('/api/data5').then(res => res.json())
];

// 限制并发数量为3
concurrentPromises(allPromises, 3).then(results => {
  console.log('所有请求完成,结果:', results);
}).catch(err => {
  console.error('请求失败:', err);
});

6. 使用Promise.allSettled处理多个异步操作

核心作用:与Promise.all相反,无论每个Promise是成功还是失败,都会等待所有Promise执行完成,返回每个Promise的状态和结果,适合“不需要所有异步都成功”的场景(比如批量统计接口状态)。

// 基础用法:统计多个接口的执行状态
const promises = [
  fetch('/api/endpoint1'),
  fetch('/api/endpoint2'),
  fetch('/api/endpoint3') // 假设这个接口会失败
];

Promise.allSettled(promises).then(results => {
  results.forEach((result, index) => {
    if (result.status === 'fulfilled') {
      // 成功的Promise,处理结果
      console.log(`接口${index + 1}请求成功:`, result.value);
    } else {
      // 失败的Promise,处理错误
      console.error(`接口${index + 1}请求失败:`, result.reason);
    }
  });
});

// 实战用法:批量上传文件,统计成功/失败数量
async function batchUpload(files) {
  const uploadPromises = files.map(file => 
    fetch('/api/upload', {
      method: 'POST',
      body: file
    }).then(res => res.json())
  );

  const results = await Promise.allSettled(uploadPromises);
  const successCount = results.filter(r => r.status === 'fulfilled').length;
  const failCount = results.filter(r => r.status === 'rejected').length;
  console.log(`上传完成:成功${successCount}个,失败${failCount}个`);
}

7. 处理多个Promises的最快响应(Promise.race)

核心作用:并行执行多个Promise,只关心“最快完成”的那个Promise的结果(无论成功还是失败),适合“取最快响应”的场景(比如多节点接口容错)。

// 基础用法:获取多个接口中最快的响应
const promises = [
  fetch('/api/node1').then(res => res.json()), // 节点1,响应时间约500ms
  fetch('/api/node2').then(res => res.json()), // 节点2,响应时间约300ms
  fetch('/api/node3').then(res => res.json())  // 节点3,响应时间约800ms
];

Promise.race(promises)
  .then(value => {
    console.log('最快响应的接口数据:', value); // 会输出节点2的数据
    // 使用最快的响应数据渲染页面,提升用户体验
  })
  .catch(reason => {
    console.error('最早失败的接口:', reason); // 若最快的接口失败,直接触发
  });

// 实战用法:多CDN资源加载,取最快的那个
const loadResource = (urls) => {
  const loadPromises = urls.map(url => 
    new Promise((resolve) => {
      const img = new Image();
      img.onload = () => resolve(url);
      img.src = url;
    })
  );
  return Promise.race(loadPromises);
};

// 加载3个CDN的图片,取最快加载完成的
const imgUrls = [
  'https://cdn1.example.com/img.jpg',
  'https://cdn2.example.com/img.jpg',
  'https://cdn3.example.com/img.jpg'
];

loadResource(imgUrls).then(fastUrl => {
  console.log('最快加载的图片CDN:', fastUrl);
  document.querySelector('.img').src = fastUrl;
});

三、异常与状态处理用法(5种)—— 规避异步报错,提升代码健壮性

异步操作中,报错、超时、状态未知是常见问题,这5种用法能优雅处理这些异常,避免页面崩溃,让代码更健壮、更易维护。

8. Promise超时处理

核心作用:给Promise设置超时时间,若超过指定时间仍未完成(未resolve/reject),则自动reject,避免异步操作“卡死”,提升用户体验。

// 核心封装:带超时的Promise
const promiseWithTimeout = (promise, ms) =>
  Promise.race([
    promise, // 原异步操作
    // 超时定时器,超过ms毫秒后reject
    new Promise((resolve, reject) =>
      setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms)
    )
  ]);

// 实战用法:给接口请求设置3秒超时
async function fetchWithTimeout(url, ms = 3000) {
  try {
    const response = await promiseWithTimeout(fetch(url), ms);
    return await response.json();
  } catch (error) {
    if (error.message.includes('Timeout')) {
      // 处理超时逻辑(提示用户、重试等)
      alert('接口请求超时,请检查网络后重试!');
    } else {
      console.error('接口请求失败:', error);
    }
  }
}

// 调用:3秒内未响应则超时
fetchWithTimeout('/api/data', 3000);

9. Promise的取消

核心作用:JavaScript原生Promise无法直接取消,但通过封装,可模拟取消逻辑(比如用户切换页面、取消请求),避免无效异步操作浪费资源。

// 核心封装:可取消的Promise
const cancellablePromise = promise => {
  let isCanceled = false; // 取消标记

  // 包装原Promise,添加取消逻辑
  const wrappedPromise = new Promise((resolve, reject) => {
    promise.then(
      value => {
        // 若已取消,触发reject(携带取消标记)
        isCanceled ? reject({ isCanceled, value }) : resolve(value);
      },
      error => {
        // 若已取消,触发reject(携带取消标记)
        isCanceled ? reject({ isCanceled, error }) : reject(error);
      }
    );
  });

  // 返回Promise和取消方法
  return {
    promise: wrappedPromise,
    cancel() {
      isCanceled = true; // 标记为已取消
    }
  };
};

// 实战用法:用户取消接口请求
const { promise, cancel } = cancellablePromise(
  fetch('/api/largeData').then(res => res.json())
);

// 监听取消按钮点击
document.querySelector('.cancel-btn').addEventListener('click', () => {
  cancel(); // 取消Promise
  console.log('请求已取消');
});

// 处理Promise结果
promise.then(data => {
  console.log('请求成功:', data);
}).catch(err => {
  if (err.isCanceled) {
    // 处理取消逻辑(不提示错误)
  } else {
    console.error('请求失败:', err);
  }
});

10. 检测Promise状态

核心作用:原生Promise不允许直接查询状态(pending/fulfilled/rejected),通过此方法可获取Promise的当前状态,用于调试或特殊业务逻辑。

// 核心封装:检测Promise状态
const reflectPromise = promise =>
  promise.then(
    value => ({ status: 'fulfilled', value }), // 成功:返回状态和结果
    error => ({ status: 'rejected', error })   // 失败:返回状态和错误
  );

// 实战用法:检测多个Promise的状态
const promise1 = fetch('/api/data1').then(res => res.json());
const promise2 = new Promise((resolve, reject) => setTimeout(reject, 1000));

// 检测两个Promise的状态
Promise.all([reflectPromise(promise1), reflectPromise(promise2)]).then(results => {
  results.forEach((result, index) => {
    console.log(`Promise${index + 1}状态:`, result.status);
    if (result.status === 'fulfilled') {
      console.log('结果:', result.value);
    } else {
      console.log('错误:', result.error);
    }
  });
});

11. 确保Promise只解决一次

核心作用:避免因代码逻辑问题(比如重复调用resolve),导致Promise被多次解决(fulfilled),引发后续逻辑异常。

// 核心封装:确保只resolve一次的Promise
const onceResolvedPromise = executor => {
  let isResolved = false; // 标记是否已resolve
  return new Promise((resolve, reject) => {
    executor(
      value => {
        // 只有未resolve过,才执行resolve
        if (!isResolved) {
          isResolved = true;
          resolve(value);
        }
      },
      reject // reject可多次调用,不影响
    );
  });
};

// 实战用法:避免重复提交(比如表单提交)
const submitForm = (formData) => {
  return onceResolvedPromise((resolve, reject) => {
    // 模拟表单提交接口
    fetch('/api/submit', {
      method: 'POST',
      body: formData
    }).then(res => res.json())
      .then(data => {
        resolve(data);
        // 模拟重复调用resolve(无效)
        resolve('重复resolve');
      })
      .catch(reject);
  });
};

// 调用:即使内部重复resolve,也只执行一次
submitForm({ name: '张三' }).then(data => {
  console.log('提交成功:', data); // 只输出一次
}).catch(err => {
  console.error('提交失败:', err);
});

12. 同时执行多个异步任务并处理中途的失败

核心作用:类似Promise.allSettled,但更灵活,可自定义失败处理逻辑,适合“允许部分异步失败,同时处理成功和失败结果”的场景。

// 结合reflectPromise,处理部分失败的场景
const promises = [
  fetch('/api/data1').then(res => res.json()), // 成功
  fetch('/api/data2').then(res => res.json()), // 失败
  fetch('/api/data3').then(res => res.json())  // 成功
];

// 先通过reflectPromise获取所有状态,再分别处理
Promise.all(promises.map(reflectPromise)).then(results => {
  // 处理成功的结果
  const successResults = results.filter(r => r.status === 'fulfilled').map(r => r.value);
  console.log('成功的结果:', successResults);

  // 处理失败的结果(比如记录日志、提示用户)
  const failResults = results.filter(r => r.status === 'rejected').map(r => r.error);
  failResults.forEach((err, index) => {
    console.error(`任务${index + 1}失败:`, err);
    // 可选:失败重试
    // retryPromise(() => fetch(`/api/data${index + 2}`).then(res => res.json()), 3, 1000);
  });
});

四、流程控制用法(6种)—— 优雅管理异步流程,避免混乱

异步流程混乱是前端异步开发的常见问题,比如顺序执行、条件执行、动态执行等,这6种用法能帮你精准控制异步流程,让代码逻辑更清晰。

13. 顺序执行Promise数组

核心作用:按顺序执行一组Promise,前一个异步操作完成后,再执行下一个,适合“依赖前一个结果”的场景(比如分步提交、依赖接口)。

// 核心封装:顺序执行Promise数组
const sequencePromises = promises =>
  promises.reduce(
    (prev, next) => prev.then(() => next()), // 前一个完成,执行下一个
    Promise.resolve() // 初始值:一个已resolve的Promise
  );

// 实战用法:分步提交表单(先验证、再提交、最后提示)
const step1 = () => new Promise(resolve => {
  console.log('步骤1:验证表单');
  setTimeout(() => resolve(), 1000);
});

const step2 = () => new Promise(resolve => {
  console.log('步骤2:提交表单');
  setTimeout(() => resolve(), 1500);
});

const step3 = () => new Promise(resolve => {
  console.log('步骤3:提示提交成功');
  setTimeout(() => resolve(), 500);
});

// 顺序执行3个步骤
sequencePromises([step1, step2, step3]).then(() => {
  console.log('所有步骤执行完成');
});

14. 基于条件的Promise链

核心作用:根据条件判断,决定是否执行下一个Promise,适合“分支异步逻辑”(比如根据用户权限,决定是否获取敏感数据)。

// 核心封装:条件Promise链
const conditionalPromise = (conditionFn, promise) => 
  conditionFn() ? promise : Promise.resolve(); // 条件不满足,返回已resolve的Promise

// 实战用法:根据用户权限,决定是否获取敏感数据
// 模拟判断用户是否有权限
const hasPermission = () => {
  const user = JSON.parse(localStorage.getItem('user'));
  return user?.role === 'admin'; // 管理员有权限
};

// 敏感数据接口(只有管理员能获取)
const fetchSensitiveData = () => 
  fetch('/api/sensitive').then(res => res.json());

// 条件执行:有权限则获取,无权限则跳过
conditionalPromise(hasPermission, fetchSensitiveData())
  .then(data => {
    if (data) {
      console.log('敏感数据:', data);
      // 渲染敏感数据
    } else {
      console.log('无权限获取敏感数据');
    }
  })
  .catch(err => console.error('获取失败:', err));

15. Promise的重试逻辑

核心作用:当Promise因暂时性错误(比如网络波动、接口临时不可用)失败时,自动重试指定次数,提升接口成功率。

// 核心封装:带重试的Promise
const retryPromise = (promiseFn, maxAttempts, interval) => {
  return new Promise((resolve, reject) => {
    // 递归重试函数
    const attempt = attemptNumber => {
      // 达到最大重试次数,触发reject
      if (attemptNumber === maxAttempts) {
        reject(new Error('Max attempts reached'));
        return;
      }
      // 执行Promise,失败则重试
      promiseFn().then(resolve).catch(() => {
        // 间隔interval毫秒后,重试下一次
        setTimeout(() => {
          attempt(attemptNumber + 1);
        }, interval);
      });
    };
    // 开始第一次尝试
    attempt(0);
  });
};

// 实战用法:接口请求失败后,重试3次,每次间隔1秒
const fetchData = () => fetch('/api/data').then(res => res.json());

retryPromise(fetchData, 3, 1000)
  .then(data => console.log('请求成功:', data))
  .catch(err => {
    console.error('重试3次仍失败:', err);
    alert('请求失败,请稍后再试!');
  });

16. Promise-pipeline(管道化异步操作)

核心作用:将多个异步操作串联成“管道”,前一个操作的结果作为后一个操作的输入,适合“多步骤异步处理”(比如数据获取→处理→保存)。

// 核心封装:Promise管道函数
const promisePipe = (...fns) => value => 
  fns.reduce((p, f) => p.then(f), Promise.resolve(value));

// 实战用法:数据处理管道(获取数据→格式化→保存)
// 步骤1:获取数据
const fetchData = () => fetch('/api/rawData').then(res => res.json());
// 步骤2:格式化数据
const formatData = (data) => {
  return new Promise(resolve => {
    const formatted = data.map(item => ({
      id: item.id,
      name: item.name,
      time: new Date(item.time).toLocaleString()
    }));
    resolve(formatted);
  });
};
// 步骤3:保存数据
const saveData = (data) => fetch('/api/save', {
  method: 'POST',
  body: JSON.stringify(data),
  headers: { 'Content-Type': 'application/json' }
}).then(res => res.json());

// 管道化执行:fetchData → formatData → saveData
const dataPipeline = promisePipe(fetchData, formatData, saveData);
dataPipeline().then(res => {
  console.log('数据处理完成:', res);
}).catch(err => {
  console.error('数据处理失败:', err);
});

17. 动态生成Promise链

核心作用:根据不同条件,动态生成异步操作链,适合“不确定异步步骤数量”的场景(比如根据用户选择,执行不同的异步操作)。

// 实战用法:根据用户选择的操作,动态生成Promise链
// 模拟用户选择的操作(可动态变化)
const userActions = [
  () => fetch('/api/action1').then(res => res.json()),
  () => fetch('/api/action2').then(res => res.json()),
  () => fetch('/api/action3').then(res => res.json())
];

// 动态生成Promise链
const promiseChain = userActions.reduce((chain, currentTask) => {
  return chain.then(currentTask); // 依次添加异步任务到链中
}, Promise.resolve()); // 初始值

// 执行动态生成的Promise链
promiseChain.then(results => {
  console.log('所有动态操作完成:', results);
}).catch(err => {
  console.error('动态操作失败:', err);
});

18. 连续获取不确定数量的数据页

核心作用:获取分页数据时,不知道总页数,通过递归方式,自动获取所有分页数据,直到没有下一页,适合“无限滚动”“批量导出”场景。

// 核心封装:递归获取所有分页数据
async function fetchPages(apiEndpoint, page = 1, allResults = []) {
  try {
    // 发起分页请求
    const response = await fetch(`${apiEndpoint}?page=${page}`);
    const data = await response.json();
    // 合并当前页数据
    const newResults = allResults.concat(data.results);
    // 判断是否有下一页,有则继续递归,无则返回所有数据
    if (data.nextPage) {
      return fetchPages(apiEndpoint, page + 1, newResults);
    } else {
      return newResults;
    }
  } catch (error) {
    console.error('获取分页数据失败:', error);
    throw error; // 抛出错误,让调用者处理
  }
}

// 实战用法:获取所有用户分页数据(不知道总页数)
fetchPages('/api/users')
  .then(allUsers => {
    console.log('所有用户数据:', allUsers);
    // 渲染所有用户数据
  })
  .catch(err => {
    alert('获取用户数据失败,请重试!');
  });

五、高级进阶用法(5种)—— 提升编码上限,面试加分项

这5种用法属于进阶技巧,在复杂项目中经常用到,掌握它们,能让你在异步开发中更游刃有余,也是前端面试的高频加分项。

19. 使用Generators管理异步流程

核心作用:将async/await与Generators配合,创建可控制的异步流程管理器,适合“复杂异步流程控制”(比如暂停、继续、中断异步操作)。

// 基础用法:Generator配合Promise管理流程
function* asyncGenerator() {
  console.log('开始执行异步流程');
  //  yield后面跟Promise,暂停等待Promise完成
  const result1 = yield fetch('/api/data1').then(res => res.json());
  console.log('第一个异步操作完成:', result1);
  
  // 第二个异步操作,依赖第一个的结果
  const result2 = yield fetch(`/api/data2?userId=${result1.id}`).then(res => res.json());
  console.log('第二个异步操作完成:', result2);
  
  return result2; // 返回最终结果
}

// 执行Generator函数
function runGenerator(generator) {
  const iterator = generator();
  const handle = (result) => {
    if (result.done) return result.value;
    // 处理yield返回的Promise
    result.value.then(res => {
      handle(iterator.next(res));
    }).catch(err => {
      iterator.throw(err);
    });
  };
  handle(iterator.next());
}

// 调用:执行异步流程
runGenerator(asyncGenerator);

20. 流式处理大型数据集

核心作用:处理大型数据集时,避免一次性加载所有数据导致内存过载,通过“流式处理”(分块处理),提升性能和用户体验。

// 实战用法:流式处理大型Excel数据(分块读取、分块处理)
async function processLargeDataSet(dataSet) {
  // dataSet:大型数据集(比如10万条数据),按块分割
  for (const dataChunk of dataSet) {
    // 分块处理数据(每个块单独异步处理)
    const processedChunk = await processData(dataChunk); // 处理单个块
    // 分块保存数据,避免一次性保存导致卡顿
    await saveProcessedData(processedChunk);
    console.log('处理完成一个数据块');
  }
  console.log('所有大型数据处理完成');
}

// 模拟分块处理函数
function processData(chunk) {
  return new Promise(resolve => {
    // 模拟数据处理(比如格式化、过滤)
    const processed = chunk.map(item => item * 2);
    setTimeout(() => resolve(processed), 500);
  });
}

// 模拟分块保存函数
function saveProcessedData(chunk) {
  return fetch('/api/saveChunk', {
    method: 'POST',
    body: JSON.stringify(chunk),
    headers: { 'Content-Type': 'application/json' }
  }).then(res => res.json());
}

// 模拟大型数据集(10个块,每个块1万条数据)
const largeDataSet = Array(10).fill(0).map(() => Array(10000).fill(0).map((_, i) => i));
// 流式处理
processLargeDataSet(largeDataSet);

21. 使用Promise实现简易的异步锁

核心作用:在多线程/多异步场景中,确保同一时间只有一个异步操作执行(比如防止重复提交、并发修改数据),避免数据冲突。

// 核心封装:简易异步锁
let lock = Promise.resolve(); // 初始锁:已解锁状态

// 获取锁
const acquireLock = () => {
  let release;
  // 等待锁释放的Promise
  const waitLock = new Promise(resolve => {
    release = resolve; // 释放锁的方法
  });
  // 尝试获取锁:当前锁释放后,返回释放锁的方法
  const tryAcquireLock = lock.then(() => release);
  // 更新锁状态:当前锁变为等待状态
  lock = waitLock;
  return tryAcquireLock;
};

// 实战用法:防止重复提交表单
async function submitForm(formData) {
  // 获取锁,获取成功才能执行提交
  const release = await acquireLock();
  try {
    console.log('开始提交表单');
    // 模拟表单提交(异步操作)
    const res = await fetch('/api/submit', {
      method: 'POST',
      body: formData
    }).then(res => res.json());
    console.log('表单提交成功:', res);
    return res;
  } catch (err) {
    console.error('表单提交失败:', err);
  } finally {
    // 无论成功失败,都释放锁
    release();
    console.log('锁已释放');
  }
}

// 模拟多次点击提交按钮(只会执行一次提交)
submitForm({ name: '张三' });
submitForm({ name: '张三' });
submitForm({ name: '张三' });

22. 组合多个Promise操作为一个函数

核心作用:将多个相关的Promise操作合并为一个函数,实现代码复用,减少冗余,提升代码可维护性。

// 实战用法:组合“获取数据+处理数据”为一个函数
// 处理数据的辅助函数
const processData = (data) => {
  return new Promise(resolve => {
    // 模拟数据处理(过滤、格式化)
    const processed = data.filter(item => item.status === 1).map(item => ({
      id: item.id,
      name: item.name,
      time: item.createTime
    }));
    resolve(processed);
  });
};

// 组合多个Promise操作
const fetchDataAndProcess = async url => {
  // 步骤1:获取数据
  const response = await fetch(url);
  const rawData = await response.json();
  // 步骤2:处理数据
  const processedData = await processData(rawData);
  // 返回处理后的结果
  return processedData;
};

// 复用函数:获取不同接口的数据并处理
fetchDataAndProcess('/api/users')
  .then(users => console.log('处理后的用户数据:', users))
  .catch(err => console.error('处理失败:', err));

fetchDataAndProcess('/api/goods')
  .then(goods => console.log('处理后的商品数据:', goods))
  .catch(err => console.error('处理失败:', err));

23. 处理可选的异步操作

核心作用:处理“可选的异步操作”,当条件满足时执行异步操作,不满足时返回默认值,避免多余的错误处理。

// 核心封装:处理可选异步操作
async function optionallyAsyncTask(condition, asyncOperation, fallbackValue) {
  if (condition) {
    // 条件满足,执行异步操作
    return await asyncOperation;
  } else {
    // 条件不满足,返回默认值
    return fallbackValue;
  }
}

// 实战用法:根据用户是否登录,决定是否获取用户个性化数据
const isLogin = () => !!localStorage.getItem('token'); // 判断用户是否登录
const fetchPersonalData = () => fetch('/api/personal').then(res => res.json()); // 个性化数据接口

// 调用:登录则获取个性化数据,未登录则返回默认数据
optionallyAsyncTask(isLogin(), fetchPersonalData(), {
  name: '游客',
  recommend: []
}).then(data => {
  console.log('用户数据:', data);
  // 渲染用户数据(无论是否登录,都有默认值,避免报错)
  document.querySelector('.user-info').textContent = JSON.stringify(data);
});

六、Promise高频避坑总结(新手必记,少踩雷)

很多新手学完Promise,一写代码就报错,不是语法错了,就是用法不对,整理了5个高频坑,记牢就能避开!

  • 坑1:忘记处理Promise的reject——Promise失败时,若未用catch捕获,会报未捕获错误,导致页面崩溃,务必给每个Promise添加catch处理。
  • 坑2:混淆Promise.all和Promise.allSettled——前者只要一个失败就整体失败,后者无论成败都会等待所有执行完成,按需选择。
  • 坑3:认为Promise可以直接取消——原生Promise无法取消,需通过封装(添加取消标记)模拟取消逻辑。
  • 坑4:await后面跟非Promise值——await只能等待Promise或async函数,若跟普通值,会直接返回该值,相当于同步执行,无意义。
  • 坑5:重复调用resolve/reject——Promise状态一旦改变就无法逆转,重复调用resolve/reject无效,还可能导致逻辑混乱,可用onceResolvedPromise避免。

七、面试高频考点(新手必记,轻松拿捏面试官)

Promise是前端面试高频考点,不用死记硬背,记住这5个核心问题,面试时直接套用即可:

  • Q1:Promise有哪三种状态?状态能否逆转? A:pending(等待中)、fulfilled(成功)、rejected(失败);状态一旦改变(pending→fulfilled或pending→rejected),就无法逆转。
  • Q2:Promise.all和Promise.race的区别? A:Promise.all等待所有Promise成功,一个失败则整体失败;Promise.race只取最快完成的Promise结果,无论成功还是失败。
  • Q3:async/await和Promise的关系? A:asyncawait是Promise的语法糖,async函数本质上返回一个Promise,await只能在async函数中使用,用于等待Promise完成。
  • Q4:如何实现Promise的取消? A:原生Promise无法直接取消,可通过添加取消标记(isCanceled),在Promise.then/catch中判断标记,实现模拟取消。
  • Q5:Promise的错误捕获有哪些方式? A:两种方式:一是给每个Promise添加.catch();二是在async/await中用try/catch捕获所有异步错误。

八、最后说几句掏心窝的话

Promise不难,核心就是“异步状态管理”,23种用法看似多,但不用死记硬背——日常开发中,高频使用的也就10种左右(async/await、Promise.all、超时处理、顺序执行等),剩下的可作为储备,用到时翻这篇文章即可。

它不是前端进阶的“加分项”,而是“必备项”——现在前端开发几乎所有异步操作都离不开Promise,不学真的会被淘汰。这篇文章整理了所有用法的实战代码、通俗解读和避坑细节,代码可直接复制练习,建议收藏起来,开发时遇到问题就翻一翻,慢慢就熟练了。

如果觉得有用,记得点赞+收藏,关注我,后续更新更多前端小白干货,一起从新手进阶成资深开发者💪

ES6模块化保姆级教程,彻底告别全局污染,新手也能秒上手

2026年4月13日 10:40

前端人必看!你是不是也遇到过这些坑😭

写代码时,变量、函数越写越多,不小心就全局污染,导致代码冲突报错;引入多个JS文件,顺序乱了就崩;想复用一段代码,只能复制粘贴,后期维护堪比“拆炸弹”……

其实这些问题,ES6模块化早就帮我们解决了!它是浏览器端和服务器端通用的模块化规范,不用再额外学习AMD、CMD、CommonJS等复杂规范,新手入门零压力,学会它,代码整洁度、复用性直接翻倍,面试也能轻松加分!

今天这篇文章,结合实战代码,把ES6模块化拆解得明明白白,从核心概念到3种用法,从基础语法到避坑细节,小白能看懂,老手能查漏补缺,收藏这一篇,再也不用东拼西凑找资料!

核心提醒:ES6模块化的核心是“拆分代码、按需导入、按需导出”,每个JS文件都是独立模块,互不干扰,彻底解决全局污染和代码复用难题,是前端工程化的基础!

一、先搞懂:ES6模块化到底是什么?(新手必看)

在ES6出现之前,前端没有统一的模块化规范,开发者只能用AMD、CMD、CommonJS等规范,不同规范用法不同,学习成本高,还不通用——浏览器端用AMD,服务器端用CommonJS,切换起来很麻烦。

而ES6模块化的出现,直接统一了浏览器和服务器端的模块化标准,它的核心定义很简单,记住3点就够了:

  • 每个JS文件都是一个独立的模块,模块内部的变量、函数、类,默认都是私有的,不会污染全局作用域;
  • 想要使用其他模块的内容,用 import 关键字导入;
  • 想要把自己模块的内容共享给其他模块,用 export 关键字导出。

举个通俗的例子:ES6模块化就像一个个独立的“文件盒子”,每个盒子里装着自己的代码(变量、函数),盒子之间可以互相“借东西”(导入导出),但不会打乱彼此的内容,也不会影响外面的环境。

二、ES6模块化3种核心用法(实战为王,代码可直接复制)

ES6模块化主要有3种用法,覆盖所有开发场景,其中“默认导出/导入”和“按需导出/导入”最常用,一定要重点掌握,第三种“直接导入执行”按需了解即可。

1. 用法一:默认导出(export default)与默认导入(import)

核心作用:一个模块只能有一次默认导出,用于导出模块的“主要内容”(比如一个核心函数、一个对象),导入时可以自由命名,非常灵活。

✅ 默认导出语法(导出文件:比如 namedModule.js)

// 方式1:导出单个变量/函数/对象(推荐)
const user = {
  name: "前端小白",
  age: 22,
  skill: "ES6"
};
export default user; // 默认导出,每个模块只能写一次

// 方式2:直接导出(无需提前定义)
// export default {
//   name: "前端小白",
//   age: 22
// };

// 方式3:导出函数
// export default function sayHello() {
//   console.log("Hello ES6模块化!");
// }

✅ 默认导入语法(导入文件:比如 index.js)

// 语法:import 接收名称 from '模块标识符(文件路径)'
import userInfo from './namedModule.js'; // 接收名称可任意命名,合法即可

console.log(userInfo); 
// 输出:{name: "前端小白", age: 22, skill: "ES6"}

⚠️ 关键细节

每个模块中,只允许使用唯一的一次 export default,如果写多次,会直接报错;默认导入时,接收名称可以任意命名(比如把userInfo改成myUser),不影响使用。

2. 用法二:按需导出(export)与按需导入(import {})

核心作用:一个模块可以多次按需导出,用于导出模块的“多个零散内容”(比如多个变量、多个函数),导入时必须和导出的名称保持一致,也可以重命名,灵活性更高,是日常开发中最常用的方式。

✅ 按需导出语法(导出文件:比如 demandModule.js)

// 按需导出单个变量/函数(可多次导出)
export let s1 = 'aaa';
export let s2 = 'ccc';
export function say() {
  console.log("我是按需导出的函数");
}

// 也可以先定义,再批量按需导出(推荐,代码更整洁)
// let s1 = 'aaa';
// let s2 = 'ccc';
// function say() {
//   console.log("我是按需导出的函数");
// }
// export { s1, s2, say };

✅ 按需导入语法(导入文件:比如 index.js)

// 语法:import { 导出名称1, 导出名称2 } from '模块标识符'
// 基础用法:导入指定内容
import { s1, s2, say } from './demandModule.js';
console.log(s1); // 输出:aaa
console.log(s2); // 输出:ccc
say(); // 输出:我是按需导出的函数

// 进阶用法1:重命名(用as关键字,解决名称冲突)
import { s1, s2 as str2, say } from './demandModule.js';
console.log(str2); // 输出:ccc(s2重命名为str2)

// 进阶用法2:按需导入 + 默认导入(结合使用,最常用)
// 假设demandModule.js同时有默认导出和按需导出
import info, { s1, s2 as str2, say } from './demandModule.js';
console.log(info); // 输出:默认导出的内容(比如{ a: 20 })
console.log(s1); // 输出:aaa
console.log(str2); // 输出:ccc

⚠️ 关键细节

  • 每个模块中,可以使用多次按需导出,没有数量限制;
  • 按需导入的成员名称,必须和按需导出的名称完全一致,否则会报错;
  • 如果导入的名称和当前模块的变量冲突,可以用 as 关键字重命名;
  • 按需导入可以和默认导入一起使用,满足复杂场景需求。

3. 用法三:直接导入并执行模块中的代码

核心作用:不需要导入模块中的任何内容,只需要执行模块中的代码(比如模块中是一段初始化代码、打印日志、创建DOM等),语法非常简单。

✅ 语法示例

// 导出文件:initModule.js
console.log("模块代码执行了!");
// 比如一段初始化代码
function init() {
  console.log("初始化完成,页面可以正常使用~");
}
init(); // 模块内部直接执行

// 导入文件:index.js(直接导入,不接收任何内容)
import './initModule.js';
// 执行后会输出:模块代码执行了!  初始化完成,页面可以正常使用~

这种用法场景较少,常见于初始化配置、全局注册组件等场景,不需要复用模块内容,只需要执行模块中的代码即可。

三、ES6模块化必避坑(新手常犯错误,看完少踩雷)

很多新手学完模块化,一写代码就报错,不是语法错了,就是用法不对,整理了4个高频坑,记牢就能避开!

坑1:忘记给script标签加type="module"

在浏览器中直接运行模块化代码时,如果script标签没有加type="module",浏览器会把JS文件当作普通脚本解析,遇到import/export会直接报错(SyntaxError: Unexpected token 'export')。

<!-- ❌ 错误写法:会报错 -->
<!-- ✅ 正确写法:必须加type="module" -->

加了type="module"后,浏览器会按模块化规范解析文件,同时开启严格模式、私有作用域,支持import/export语法。

坑2:一个模块写多个export default

默认导出(export default)每个模块只能有一次,写多次会直接报错,若需要导出多个内容,优先用按需导出,或把多个内容包装成一个对象,再默认导出。

// ❌ 错误写法:多个export default
export default { a: 10 };
export default { b: 20 }; // 报错!

// ✅ 正确写法1:用按需导出
export { a: 10, b: 20 };

// ✅ 正确写法2:包装成一个对象默认导出
export default {
  a: 10,
  b: 20
};

坑3:按需导入时名称和导出名称不一致

按需导入的核心规则:导入名称必须和导出名称完全一致,除非用as关键字重命名,否则会提示“未定义”错误。

// 导出文件
export let s1 = 'aaa';

// ❌ 错误写法:导入名称不一致
import { s2 } from './demandModule.js'; // 报错:s2 is not defined

// ✅ 正确写法1:名称一致
import { s1 } from './demandModule.js';

// ✅ 正确写法2:用as重命名
import { s1 as str1 } from './demandModule.js';

坑4:导入变量后直接修改

ES6模块化的导入变量是只读的引用,不是值的拷贝,不能直接修改导入的变量,否则会报错;如果需要修改,可在导出模块中定义修改方法,再导入使用。

// 导出文件:counter.js
export let count = 0;
export function increment() {
  count++; // 导出模块内部修改变量
}

// 导入文件:index.js
import { count, increment } from './counter.js';
console.log(count); // 0
increment();
console.log(count); // 1(同步更新)

// ❌ 错误写法:直接修改导入的变量
// count = 5; // 报错:Assignment to constant variable.

四、ES6模块化实战场景(贴合真实开发,直接复用)

学会了语法,还要知道在实际开发中怎么用,以下3个高频场景,覆盖大部分前端开发需求,代码可直接复制使用。

场景1:封装工具函数(最常用)

把常用的工具函数(比如格式化时间、防抖节流)封装成一个模块,按需导入使用,避免重复写代码,方便维护。

// 工具模块:utils.js
// 按需导出多个工具函数
export function formatTime(time) {
  // 格式化时间:YYYY-MM-DD
  return new Date(time).toLocaleDateString().replace(///g, '-');
}

export function debounce(fn, delay) {
  // 防抖函数
  let timer = null;
  return function() {
    clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(this, arguments);
    }, delay);
  };
}

// 导入使用:index.js
import { formatTime, debounce } from './utils.js';

// 使用格式化时间函数
console.log(formatTime(new Date())); // 输出:2026-04-13

// 使用防抖函数
const handleClick = debounce(() => {
  console.log("防抖触发");
}, 500);

场景2:拆分组件(前端工程化基础)

在Vue、React等框架中,模块化拆分组件是基础操作,每个组件是一个独立模块,导出组件,再在其他组件中导入使用。

// 组件模块:Button.js
// 模拟Vue组件导出
export default function Button(props) {
  return ``;
}

// 导入使用:App.js
import Button from './Button.js';

// 渲染按钮组件
document.body.innerHTML = Button({
  color: 'red',
  text: '点击我'
});

场景3:统一入口文件(优化导入路径)

当项目模块较多时,可创建一个入口文件(index.js),统一导出所有模块,其他文件只需导入入口文件,简化导入路径。

// 入口文件:index.js
// 统一导出其他模块
export { default as Button } from './components/Button.js';
export { formatTime, debounce } from './utils.js';
export { userInfo } from './user.js';

// 导入使用:app.js
// 只需导入入口文件,即可获取所有模块
import { Button, formatTime, userInfo } from './index.js';

五、面试高频考点(新手必记,轻松拿捏面试官)

ES6模块化是前端面试高频考点,不用死记硬背,记住这4个核心问题,面试时直接套用即可:

  • Q1:ES6模块化和CommonJS的区别? A:ES6模块化是浏览器和服务器端通用,用import/export;CommonJS主要用于服务器端(Node.js),用require/module.exports;ES6模块化是静态导入(编译时解析),CommonJS是动态导入(运行时解析)。
  • Q2:export default和export的区别? A:export default每个模块只能有一次,导出单个内容,导入时可任意命名;export可多次使用,导出多个内容,导入时名称必须一致(可重命名)。
  • Q3:为什么import/export在浏览器中会报错? A:因为script标签没有加type="module",浏览器会把JS文件当作普通脚本解析,不支持模块化语法。
  • Q4:ES6模块化的导入变量为什么不能直接修改? A:导入的变量是只读的引用(Live Bindings),不是值的拷贝,直接修改会违反模块化的封装原则,需在导出模块中定义修改方法。

六、最后说几句掏心窝的话

ES6模块化不难,核心就是“导入(import)”和“导出(export)”,记住3种用法和4个避坑点,就能轻松上手。它不是前端进阶的“加分项”,而是“必备项”——现在前端开发几乎全员使用模块化,不学真的会被淘汰。

这篇文章整理了模块化的核心语法、实战案例、避坑细节和面试考点,代码可直接复制练习,建议收藏起来,开发时遇到问题就翻一翻,慢慢就熟练了。

如果觉得有用,记得点赞+收藏,关注我,后续更新更多前端小白干货,一起从新手进阶成资深开发者💪

ES6 40个数组方法保姆级拆解

2026年4月13日 10:34

前端人必看!数组是JS开发中最常用的数据结构,没有之一✨

很多开发者写数组操作,还在死磕for循环写几十行冗余代码,要么用错方法导致bug频发,要么不知道哪个方法更高效——其实ES6+早已贴心提供了40个数组方法,覆盖遍历、筛选、修改、转换等所有场景,学会它们,编码效率直接翻倍,面试也能轻松拿捏面试官!

今天这篇文章,结合实战场景,把40个数组方法逐一拆解,每个方法都配「核心特点+可复制代码+避坑提示」,从基础到进阶,小白能看懂,老手能查漏补缺,收藏这一篇,再也不用东拼西凑找资料!

核心提醒:很多人觉得“不用学完40个”,但实际开发中,选对方法能少写50%代码!比如筛选数据用filter,批量转换用map,累加计算用reduce,找元素用find——精准匹配场景,才是高效开发的关键👇

一、基础遍历类(3个)—— 替代for循环,简洁不冗余

核心作用:遍历数组元素,执行指定操作,告别手动维护索引的麻烦,代码可读性拉满,是日常开发使用频率最高的一类方法。

1. Array.forEach() —— 基础遍历神器

核心特点:遍历数组,对每个元素执行回调函数,无返回值;第一个参数始终是当前元素,还可接收索引、原数组作为可选参数。

避坑提示:无法通过break中断遍历,若需中断,可改用for...of循环;回调函数自身可修改原数组,需谨慎使用。

// 基础用法:遍历并打印每个元素
const array = ['a', 'b', 'c'];
array.forEach((e) => console.log(e));
// 输出结果:a、b、c(依次打印)

// 进阶用法:获取元素+索引
array.forEach((e, index) => {
  console.log(`索引${index}${e}`);
});
// 输出结果:索引0:a、索引1:b、索引2:c

2. Array.keys() —— 遍历数组索引

核心特点:返回一个数组迭代器对象,包含数组所有元素的索引,可通过for...of循环遍历获取所有索引。

const array = ['a', 'b', 'c', 'd'];
const iterator = array.keys();
// 遍历所有索引
for (const key of iterator) {
  console.log(key);
}
// 输出结果:0、1、2、3

3. Array.values() —— 遍历数组值

核心特点:返回一个数组迭代器对象,包含数组所有元素的值,可通过for...of循环遍历获取所有元素,与forEach功能互补。

const array = ['a', 'b', 'c', 'd'];
const iterator = array.values();
// 遍历所有元素值
for (const key of iterator) {
  console.log(key);
}
// 输出结果:a、b、c、d

二、筛选查找类(7个)—— 精准定位元素,告别手动判断

核心作用:根据指定条件,快速查找、筛选数组中的元素或索引,替代繁琐的if-else判断,减少代码冗余,提升开发效率。

4. Array.map() —— 批量转换数组

核心特点:遍历数组,对每个元素执行回调函数,返回一个新数组(长度与原数组一致),不改变原数组,常用于数据格式转换。

避坑提示:若回调函数无返回值,新数组会充满undefined,务必确保每个元素都有返回值。

// 基础用法:将数组中每个元素乘以2
const array = [1, 4, 9, 16];
const map1 = array.map((x) => x * 2);
console.log(map1); // 输出:[2, 8, 18, 32]
console.log(array); // 输出:[1, 4, 9, 16](原数组未改变)

// 实战用法:提取接口数据中的指定字段
const users = [{name: '张三'}, {name: '李四'}, {name: '王五'}];
const userNames = users.map(user => user.name);
console.log(userNames); // 输出:['张三', '李四', '王五']

5. Array.filter() —— 筛选符合条件的元素

核心特点:遍历数组,返回一个新数组,包含所有满足回调函数条件的元素,不改变原数组,常用于数据筛选、去重预处理。

// 基础用法:筛选长度大于6的单词
const words = ['spray', 'elite', 'exuberant', 'destruction', 'present'];
const result = words.filter((word) => word.length > 6);
console.log(result); // 输出:['exuberant', 'destruction', 'present']

// 实战用法:筛选有库存的商品
const products = [
  { name: '手机', price: 5999, inStock: true },
  { name: '电脑', price: 8999, inStock: false },
  { name: '耳机', price: 799, inStock: true }
];
const inStockProducts = products.filter(product => product.inStock);
console.log(inStockProducts); // 输出:[{name: '手机', ...}, {name: '耳机', ...}]

6. Array.find() —— 查找首个符合条件的元素

核心特点:遍历数组,返回首个满足回调函数条件的元素(返回元素本身);若找不到,返回undefined,适合查找单个目标元素。

const array = [5, 12, 8, 130, 44];
// 查找第一个大于10的元素
console.log(array.find((e) => e > 10)); // 输出:12

// 实战用法:查找指定id的用户
const users = [{id: 1, name: '张三'}, {id: 2, name: '李四'}];
const targetUser = users.find(user => user.id === 2);
console.log(targetUser); // 输出:{id: 2, name: '李四'}

7. Array.findIndex() —— 查找首个符合条件的元素索引

核心特点:与find()功能类似,区别在于返回首个满足条件的元素索引;若找不到,返回-1,常用于需要获取元素位置的场景。

const array = [5, 12, 8, 130, 44];
// 查找第一个大于45的元素索引
console.log(array.findIndex((e) => e > 45)); // 输出:3(元素130的索引)

8. Array.indexOf() —— 查找指定元素的索引

核心特点:查找数组中第一个目标元素的索引,若找不到返回-1;可设置第二个参数,指定起始查询位置,适合精确查找已知元素。

const beasts = ['ant', 'bison', 'camel', 'duck', 'bison'];
console.log(beasts.indexOf('bison')); // 输出:1(第一个bison的索引)
console.log(beasts.indexOf('bison', 2)); // 输出:4(从索引2开始查找的第一个bison)
console.log(beasts.indexOf('giraffe')); // 输出:-1(找不到该元素)

9. Array.lastIndexOf() —— 从末尾查找指定元素

核心特点:与indexOf()相反,从数组末尾开始搜索,返回第一个目标元素的索引;若找不到返回-1,适合查找元素最后出现的位置。

const array = ['ant', 'bison', 'camel', 'duck', 'bison'];
console.log(array.lastIndexOf('bison')); // 输出:4(最后一个bison的索引)
console.log(array.lastIndexOf('camel')); // 输出:2(camel的索引)

10. Array.includes() —— 判断元素是否存在

核心特点:判断数组是否包含指定元素,返回true/false,语法简洁,替代传统的indexOf() !== -1,可读性更高。

避坑提示:与some()区别:includes()直接判断元素是否存在,some()判断是否有元素满足自定义条件。

const array = [1, 30, 39, 29, 10, 13];
console.log(array.includes(30)); // 输出:true
console.log(array.includes(100)); // 输出:false

三、数组修改类(8个)—— 增删改查,灵活操作数组

核心作用:直接修改数组(或返回修改后的结果),涵盖元素添加、删除、替换、排序等操作,满足日常数组修改的所有需求,注意区分“是否改变原数组”。

11. Array.push() —— 末尾添加元素

核心特点:往数组末尾添加一个或多个元素,改变原数组,返回添加后数组的新长度。

const array = ['ant', 'duck', 'bison'];
// 添加单个元素
const count = array.push('cows');
console.log(count); // 输出:4(添加后的数组长度)
console.log(array); // 输出:["ant", "duck", "bison", "cows"]

// 批量添加元素
array.push('cat', 'dog');
console.log(array); // 输出:["ant", "duck", "bison", "cows", "cat", "dog"]

12. Array.unshift() —— 开头添加元素

核心特点:往数组开头添加一个或多个元素,改变原数组,返回添加后数组的新长度。

const array = [1, 10, 13];
// 批量添加元素到开头
console.log(array.unshift(4, 5)); // 输出:5(添加后的数组长度)
console.log(array); // 输出:[4, 5, 1, 10, 13]

13. Array.pop() —— 删除末尾元素

核心特点:删除数组最后一个元素,改变原数组,返回被删除的元素;若数组为空,返回undefined。

const array = ['ant', 'duck', 'bison'];
console.log(array.pop()); // 输出:bison(被删除的元素)
console.log(array); // 输出:["ant", "duck"]
// 继续删除
array.pop();
console.log(array); // 输出:["ant"]

14. Array.shift() —— 删除开头元素

核心特点:删除数组第一个元素,改变原数组,返回被删除的元素;若数组为空,返回undefined。

const array = [1, 6, 10, 13];
const firstElement = array.shift();
console.log(firstElement); // 输出:1(被删除的元素)
console.log(array); // 输出:[6, 10, 13]

15. Array.splice() —— 万能修改方法

核心特点:可实现“添加、删除、替换”三种功能,改变原数组;返回被删除的元素组成的数组,若未删除元素则返回空数组。

语法:array.splice(起始索引, 删除个数, 要添加的元素)

const array = ['ant', 'bison', 'camel', 'duck', 'bison'];
// 1. 插入元素(不删除):在索引1处插入'Feb'
array.splice(1, 0, 'Feb');
console.log(array); // 输出:["ant", "Feb", "bison", "camel", "duck", "bison"]

// 2. 替换元素:在索引4处删除1个元素,插入'May'
array.splice(4, 1, 'May');
console.log(array); // 输出:["ant", "Feb", "bison", "camel", "May", "bison"]

// 3. 删除元素:在索引1处删除1个元素
array.splice(1, 1);
console.log(array); // 输出:["ant", "bison", "camel", "May", "bison"]

16. Array.sort() —— 数组排序

核心特点:对数组元素进行排序,改变原数组;默认按字符串Unicode码排序,数字排序需手动传入比较函数。

// 字符串排序(默认)
const array1 = ['ant', 'bison', 'camel', 'duck'];
array1.sort();
console.log(array1); // 输出:["ant", "bison", "camel", "duck"]

// 数字排序(需传入比较函数)
const array2 = [3, 1, 4, 1, 5, 9];
// 升序排序
array2.sort((a, b) => a - b);
console.log(array2); // 输出:[1, 1, 3, 4, 5, 9]
// 降序排序
array2.sort((a, b) => b - a);
console.log(array2); // 输出:[9, 5, 4, 3, 1, 1]

17. Array.reverse() —— 数组反转

核心特点:反转数组中元素的顺序,改变原数组,语法简单,常用于需要倒序排列的场景。

const array = ['ant', 'bison', 'camel', 'duck'];
array.reverse();
console.log(array); // 输出:["duck", "camel", "bison", "ant"]

18. Array.fill() —— 填充数组

核心特点:用指定内容填充数组,改变原数组;可指定填充的起始索引和结束索引(不包含结束索引),不指定则填充整个数组。

const array = [1, 6, 10, 13];
// 填充索引2-4(不包含4)为0
console.log(array.fill(0, 2, 4)); // 输出:[1, 6, 0, 0]
// 填充索引1及以后为5
console.log(array.fill(5, 1)); // 输出:[1, 5, 5, 5]
// 填充整个数组为6
console.log(array.fill(6)); // 输出:[6, 6, 6, 6]

四、数组转换类(7个)—— 格式转换,适配不同场景

核心作用:将数组转换为其他格式(字符串、迭代器、新数组等),或从其他格式转为数组,满足数据展示、传递等不同需求,大多不改变原数组。

19. Array.toString() —— 数组转字符串

核心特点:将数组转为字符串,元素用逗号分隔,不改变原数组;简单直接,但无法自定义分隔符。

const array = [1, 6, 'a', '1a'];
console.log(array.toString()); // 输出:'1,6,a,1a'

20. Array.join() —— 自定义分隔符转字符串

核心特点:将数组用指定符号连接成字符串,不改变原数组;省略分隔符则用逗号分隔,可自定义任意分隔符(空字符串、横线等)。

const array = ['ant', 'duck', 'bison'];
console.log(array.join()); // 输出:ant,duck,bison(默认逗号分隔)
console.log(array.join('')); // 输出:antduckbison(无分隔符)
console.log(array.join('-')); // 输出:ant-duck-bison(横线分隔)

21. Array.concat() —— 合并数组

核心特点:合并两个或多个数组,返回一个新数组,不改变原数组;可替代扩展运算符(...),语法更简洁。

const array1 = ['a', 'b', 'c'];
const array2 = ['d', 'e', 'f'];
const array3 = array1.concat(array2);
console.log(array3); // 输出:['a', 'b', 'c', 'd', 'e', 'f']
console.log(array1); // 输出:['a', 'b', 'c'](原数组未改变)

22. Array.slice() —— 截取数组

核心特点:截取数组中的指定片段,返回一个新数组,不改变原数组;可指定起始索引和结束索引(不包含结束索引),支持负数索引(从末尾开始计数)。

const array = ['ant', 'bison', 'camel', 'duck', 'bison'];
console.log(array.slice(2)); // 输出:["camel", "duck", "bison"](从索引2开始截取)
console.log(array.slice(2, 4)); // 输出:["camel", "duck"](截取索引2-4,不包含4)
console.log(array.slice(-2)); // 输出:["duck", "bison"](从倒数第二个开始截取)
console.log(array.slice()); // 输出:原数组(相当于浅拷贝)

23. Array.from() —— 类数组转数组

核心特点:将类数组(如字符串、DOM集合、JSON)转为真正的数组,返回新数组;最简单的应用是克隆数组,还可传入回调函数处理元素。

// 字符串转数组
console.log(Array.from('foo')); // 输出:["f", "o", "o"]

// 克隆数组并处理元素
console.log(Array.from([1, 2, 3], x => x + x)); // 输出:[2, 4, 6]

// JSON(类数组)转数组
const json = { '0': 'a', '1': 'b', '2': 'c', length: 3 };
console.log(Array.from(json)); // 输出:['a', 'b', 'c']

24. Array.of() —— 创建新数组

核心特点:创建一个新的Array实例,将传入的参数作为数组元素;与new Array()区别:Array.of(3)创建[3],new Array(3)创建长度为3的空数组。

console.log(Array.of('foo', 2, 'bar', true)); // 输出:["foo", 2, "bar", true]
console.log(Array.of()); // 输出:[](空数组)
console.log(Array.of(3)); // 输出:[3]
console.log(new Array(3)); // 输出:[empty × 3](空数组,长度为3)

25. Array.entries() —— 生成键值对迭代器

核心特点:返回一个新的数组迭代器对象,包含数组中每个索引的键/值对([索引, 元素]),可通过for...of循环遍历。

const array = ['a', 'b', 'c', 'd'];
const iterator = array.entries();
for (const key of iterator) {
  console.log(key[0], key[1]);
}
// 输出结果:0 "a"、1 "b"、2 "c"、3 "d"

五、高级操作类(10个)—— 复杂场景必备,提升编码上限

核心作用:应对更复杂的数组操作(累加、扁平化、批量处理等),是前端进阶的关键,学会这些,能轻松处理复杂数据逻辑,面试加分项!

26. Array.reduce() —— 万能累加器

核心特点:从左到右遍历数组,通过回调函数将数组元素“累积”为一个值(数字、对象、数组等),不改变原数组,功能最灵活,可替代sum、map+filter等组合用法。

语法:array.reduce((累加器, 当前值, 索引, 原数组) => {}, 初始值)

// 1. 基础用法:数组求和
const array = [1, 2, 3, 4];
const initialValue = 0;
const sumWithInitial = array.reduce(
  (accumulator, currentValue) => accumulator + currentValue,
  initialValue
);
console.log(sumWithInitial); // 输出:10

// 2. 进阶用法:数组转对象(按id分组)
const users = [{id: 1, name: '张三'}, {id: 2, name: '李四'}];
const userMap = users.reduce((acc, user) => {
  acc[user.id] = user;
  return acc;
}, {});
console.log(userMap); // 输出:{1: {id:1, name:'张三'}, 2: {id:2, name:'李四'}}

27. Array.reduceRight() —— 反向累加

核心特点:与reduce()功能一致,区别在于从数组末尾开始遍历累加,最终返回一个单个值,适合需要反向处理数组的场景。

const array = [2, 3, 4, 5, 6, 7, 8, 9];
const sumWithInitial = array.reduceRight((pv, cv) => {
  console.log(`当前值: ${cv}, 上一次累积值: ${pv}`);
  return 2 * cv;
});
console.log(sumWithInitial); // 输出:36(从9开始反向计算:2*9=18 → 2*8=16 → ...最终结果36)

28. Array.flat() —— 数组扁平化

核心特点:将嵌套数组(多维数组)展开,返回一个新数组,不改变原数组;默认展开1层,可传入数字指定展开层数(Infinity表示无限层)。

// 基础用法:展开1层嵌套数组
const array = [0, 1, 2, [3, 4]];
const result = array.flat();
console.log(result); // 输出:[0, 1, 2, 3, 4]

// 进阶用法:展开多层嵌套数组
const array2 = [0, 1, [2, [3, [4, 5]]]];
console.log(array2.flat(2)); // 输出:[0, 1, 2, 3, [4, 5]](展开2层)
console.log(array2.flat(Infinity)); // 输出:[0, 1, 2, 3, 4, 5](无限层展开)

29. Array.flatMap() —— 映射+扁平化

核心特点:先对数组每个元素执行map()操作,再对结果执行flat()操作(默认展开1层),返回一个新数组,不改变原数组,简化map+flat的组合写法。

const array = [2, 3, 4];
// 对每个元素映射,再扁平化
const result = array.flatMap(num => (num === 2 ? [2, 2] : 1));
console.log(result); // 输出:[2, 2, 1, 1]
// 等价于:array.map(...).flat(1)

30. Array.copyWithin() —— 数组内部拷贝

核心特点:从数组的指定位置拷贝元素,粘贴到数组的另一个指定位置,改变原数组;语法:array.copyWithin(粘贴位置, 拷贝起始位置, 拷贝结束位置)。

const array = ['a', 'b', 'c', 'd', 'e'];
// 从索引3拷贝1个元素(d),粘贴到索引0
console.log(array.copyWithin(0, 3, 4)); // 输出:["d", "b", "c", "d", "e"]
// 从索引3拷贝所有元素(d、e),粘贴到索引1
console.log(array.copyWithin(1, 3)); // 输出:["d", "d", "e", "d", "e"]

31. Array.at() —— 按索引获取元素

核心特点:根据指定索引获取数组中的元素,支持负数索引(从末尾开始计数),语法比array[index]更灵活,可避免负数索引返回undefined的问题。

const array = [5, 12, 8, 130, 44];
console.log(array.at(2)); // 输出:8(索引2对应的元素)
console.log(array.at(-1)); // 输出:44(倒数第一个元素)
console.log(array.at(-3)); // 输出:8(倒数第三个元素)

32. Array.findLast() —— 查找最后一个符合条件的元素

核心特点:与find()功能类似,区别在于从数组末尾开始查找,返回最后一个满足条件的元素;若找不到,返回undefined。

const array = [5, 12, 8, 130, 44];
// 查找最后一个大于45的元素
const found = array.findLast(el => el > 45);
console.log(found); // 输出:130

33. Array.fromAsync() —— 异步类数组转数组

核心特点:将异步类数组(如异步迭代器、Promise数组)转为数组,返回一个新的Promise实例,其履行值是转换后的新数组,适合异步场景。

// 示例:将异步迭代器转为数组
async function getArray() {
  const asyncIterator = (async function* () {
    yield 1;
    yield 2;
    yield 3;
  })();
  const array = await Array.fromAsync(asyncIterator);
  console.log(array); // 输出:[1, 2, 3]
}
getArray();

34. Array.with() —— 替换指定索引元素

核心特点:将数组中指定索引的元素替换为目标值,返回一个新数组,不改变原数组,语法简洁,替代splice()的替换功能(无需改变原数组)。

const arr = [1, 2, 3, 4, 5];
// 将索引2的元素替换为6
console.log(arr.with(2, 6)); // 输出:[1, 2, 6, 4, 5]
console.log(arr); // 输出:[1, 2, 3, 4, 5](原数组未改变)

35. Array.toSorted() —— 排序不改变原数组

核心特点:与sort()功能一致,对数组元素进行排序,但不改变原数组,返回排序后的新数组,避免sort()修改原数组的副作用。

const array = ['ant', 'bison', 'camel', 'duck'];
const sortedMonths = array.toSorted();
console.log(sortedMonths); // 输出:["ant", "bison", "camel", "duck"](排序后)
console.log(array); // 输出:['ant', 'bison', 'camel', 'duck'](原数组未改变)

36. Array.toLocaleString() —— 本地化字符串转换

核心特点:将数组转为本地化字符串,元素用逗号分隔,不改变原数组;与toString()区别:会根据当前地区的语言和格式规则转换(如数字、日期格式)。

const array = [1234, new Date(), 'hello'];
// 本地化转换(根据当前地区格式)
console.log(array.toLocaleString()); 
// 输出示例:1,234, 2026/4/13 10:30:00, hello

37. Array.isArray() —— 判断是否为数组

核心特点:判断一个值是否为数组,返回true/false;注意:若值是TypedArray实例(如Int16Array),始终返回false,是最可靠的数组判断方法。

console.log(Array.isArray([1, 3, 5])); // 输出:true
console.log(Array.isArray('[]')); // 输出:false(字符串不是数组)
console.log(Array.isArray(new Array(5))); // 输出:true(new Array创建的是数组)
console.log(Array.isArray(new Int16Array([15, 33]))); // 输出:false(TypedArray实例)

38. Array.valueOf() —— 返回数组本身

核心特点:返回数组本身,不做任何修改和转换,常用于确保变量是数组类型,避免类型转换错误。

const fruits = ['a', 'b', 'c', 'd'];
console.log(fruits.valueOf()); // 输出:["a", "b", "c", "d"](返回数组本身)

39. Array.some() —— 判断是否有元素满足条件

核心特点:遍历数组,只要有一个元素满足回调函数条件,就返回true;所有元素都不满足,返回false,常用于“判断是否存在符合条件的元素”。

const array = [1, 2, 3, 4, 5];
// 判断数组中是否有偶数
const even = (el) => el % 2 === 0;
console.log(array.some(even)); // 输出:true

40. Array.every() —— 判断所有元素是否满足条件

核心特点:与some()相反,遍历数组,所有元素都满足回调函数条件,才返回true;只要有一个元素不满足,返回false,常用于“校验所有元素是否符合规则”。

const array = [1, 30, 39, 29, 10, 13];
// 判断所有元素是否都小于20
console.log(array.every((item) => item < 20)); // 输出:false(30、39大于20)

二、面试+实战高频避坑总结(必记)

很多开发者用错数组方法,不是不会用,而是没分清“是否改变原数组”“适用场景”,整理了4个高频避坑点,记牢少踩bug!

  • 改变原数组的方法(10个):push、unshift、pop、shift、splice、sort、reverse、fill、copyWithin
  • 不改变原数组的方法(30个):除上述10个外,其余30个均不改变原数组,可放心使用
  • 高频混淆方法:map(返回等长新数组)vs filter(返回筛选后数组)、some(有一个满足即true)vs every(所有满足才true)、slice(不改变原数组)vs splice(改变原数组)
  • 高效组合用法:map+filter(先转换再筛选)、flatMap(映射+扁平化)、reduce(替代sum、数组转对象等)

三、最后说几句掏心窝的话

40个数组方法看似多,但不用死记硬背——日常开发中,高频使用的也就10个左右(forEach、map、filter、push、splice、slice、reduce、find、includes、sort),剩下的可作为储备,用到时翻这篇文章即可。

前端开发的核心是“高效编码”,选对数组方法,能帮你少写冗余代码、减少bug,还能提升代码可读性——这篇文章整理了所有方法的实战代码和避坑点,建议收藏,面试前过一遍,开发时直接复制使用!

如果觉得有用,记得点赞+收藏,关注我,后续更新更多前端干货,一起从新手进阶成资深开发者💪

救命!ES6入门到精通,前端小白也能秒上手

2026年4月13日 10:24

谁懂啊家人们!前端入门绕不开ES6,可网上的教程要么太晦涩,要么代码零散,新手看了直接劝退😭

其实ES6根本没那么难!它不是全新的语言,只是JavaScript的“升级补丁”——把ES5里繁琐的写法简化,新增了超多实用功能,学会它,写代码效率直接翻倍,面试也能轻松拿捏!

今天就结合实战代码,把ES6核心知识点拆解得明明白白,从基础到进阶,小白也能跟着敲、跟着会,收藏这一篇就够了,再也不用东拼西凑找资料!

一、先搞懂:ES和JS到底是什么关系?(新手必看)

很多小白刚入门就被“ES6”“JavaScript”搞懵,其实一句话就能说清:

ES 是 ECMAScript 的简写,是 JavaScript 的“核心语法标准”;而 JS 由 3 部分组成:ECMAScript(核心)+ DOM(文档对象模型)+ BOM(浏览器对象模型)。简单说,ES 就是 JS 的“灵魂”,学 JS 必学 ES!

以前我们学的大多是 ES5 语法,而 ES6 及以后的版本,做了大量优化,解决了 ES5 的很多痛点(比如变量提升、代码冗余),现在前端开发几乎全员用 ES6+ 写法,不学真的会被淘汰!

💡 开发工具推荐:VS Code(免费又强大),必装 2 个插件:

  • View in Browser:一键在浏览器中查看效果
  • JavaScript (ES6) code snippets:ES6 代码片段,一键生成,提升编码速度

二、ES6 核心知识点(实操为王,代码可直接复制)

这部分是重点!每个知识点都配了「代码示例+通俗解释」,敲一遍就懂,建议边看边练,记得更牢~

1. 变量声明:let/const 替代 var(彻底解决变量提升坑)

ES5 里我们用 var 声明变量,会有“变量提升”“重复声明”“全局污染”三个大坑,而 ES6 的 let 和 const 直接解决了这些问题,用法超简单!

✨ let 用法(声明局部变量)

// 1. 不允许未定义就使用(避免变量提升)
// console.log(k); // 报错:Uncaught ReferenceError: k is not defined

// 2. 不允许重复声明
let k = 10;
// let k = 101; // 报错:Uncaught SyntaxError: Identifier 'k' has already been declared

// 3. 块级作用域(只在当前代码块有效)
for (let j = 0; j < 5; j++) {
  console.log("循环里的j:" + j); // 正常输出 0-4
}
// console.log("循环外的j:" + j); // 报错:j is not defined

✨ const 用法(声明常量)

// 声明常量,指向的内存地址不能修改
const x = 2;
// x = 991; // 报错:Uncaught TypeError: Assignment to constant variable.

// 注意:如果常量是对象/数组,内部属性可以修改
const obj = { name: "jspang" };
obj.name = "技术胖"; // 正常生效,不报错

小技巧:能⽤ const 就⽤ const,需要修改的变量再⽤ let,避免全局污染!

2. 变量解构赋值:简化赋值,少写冗余代码

以前给多个变量赋值,要写多行代码,ES6 的解构赋值,一行就能搞定,还支持数组、对象、字符串解构,超实用!

✨ 数组解构

// ES5 写法
let a = 0; let b = 1; let c = 2;

// ES6 解构写法(简洁!)
let [a, b, c] = [1, 2, 3];
console.log(a); // 1
console.log(b); // 2
console.log(c); // 3

// 嵌套数组解构
let [a, [b, c], d] = [1, [2, 3], 4];
console.log(a); // 1,b:2,c:3,d:4

✨ 对象解构(最常用,重点记!)

// 核心:变量名必须和对象属性名一致
let { foo, bar } = { foo: 'JSPang', bar: '技术胖' };
console.log(foo + bar); // 输出:JSPang技术胖

// 圆括号用法(当变量已经声明时)
let foo;
({ foo } = { foo: 'JSPang' }); // 必须加圆括号,否则报错
console.log(foo); // JSPang

✨ 解构默认值(避免 undefined)

// 当解构的值不存在时,使用默认值
let [a, b = "JS"] = ['张三'];
console.log(a + b); // 张三JS

// 注意:undefined 和 null 的区别
let [a, b = "JSPang"] = ['技术胖', undefined]; // undefined 用默认值
console.log(a + b); // 技术胖JSPang

let [a, b = "JSPang"] = ['技术胖', null]; // null 不用默认值
console.log(a + b); // 技术胖null

3. 字符串扩展:新增方法,简化字符串操作

ES6 给字符串新增了 includes()、startsWith()、endsWith()、repeat() 等方法,替代了传统的 indexOf(),写法更简洁,语义更清晰!

let str = "https://www.baidu.com/";

// 1. includes():判断是否包含指定字符串(返回true/false)
console.log(str.includes("www")); // true
console.log(str.includes("yyy")); // false

// 2. startsWith():判断是否以指定字符串开头
console.log(str.startsWith("https")); // true
console.log(str.startsWith("baidu", 12)); // 从第12位开始,是否以baidu开头?true

// 3. endsWith():判断是否以指定字符串结尾
console.log(str.endsWith("com")); // false(原字符串结尾是/)
console.log(str.endsWith("www", 11)); // 前11位是否以www结尾?true

// 4. repeat():复制字符串
console.log('jspang|'.repeat(3)); // jspang|jspang|jspang|

4. 数组扩展:新增方法,搞定数组操作

ES6 给数组新增了 Array.from()、Array.of()、find()、filter()、map() 等方法,再也不用手动写循环,效率翻倍!

// 1. Array.from():将类数组(如JSON)转为真正的数组
let json = {
  '0': 'jspang',
  '1': '技术胖',
  '2': '大胖逼逼叨',
  length: 3
};
let arr = Array.from(json);
console.log(arr); // ['jspang', '技术胖', '大胖逼逼叨']

// 2. Array.of():将任意值转为数组
let arr1 = Array.of(3, 4, 5, 6);
console.log(arr1); // [3,4,5,6]

// 3. find():查找数组中第一个满足条件的元素
let arr2 = [1,2,3,4,5,6];
console.log(arr2.find(value => value > 5)); // 6

// 4. filter():过滤数组(返回满足条件的新数组)
let num = [1, 5, 5, 9];
let num1 = num.filter(x => x != 5); // 过滤掉5
console.log(num1); // [1,9]

// 5. map():映射数组(对每个元素做处理,返回新数组)
let arr3 = ['jspang','技术胖','前端教程'];
console.log(arr3.map(x => 'web')); // ['web', 'web', 'web']

5. 扩展运算符(...):万能简化神器

扩展运算符(...)是 ES6 最常用的语法之一,能拆分数组、对象,简化函数参数传递,解决数组浅拷贝问题,用法超灵活!

// 1. 简化函数参数
let add = (...c) => {
  let sum = 0;
  for (const num of c) {
    sum += num;
  }
  return sum;
};
let num = [1, 5, 5, 9];
console.log(add(...num)); // 20(相当于add(1,5,5,9))

// 2. 数组浅拷贝(避免修改新数组影响原数组)
let arr1 = ['www','jspang','com'];
let arr2 = [...arr1]; // 浅拷贝
arr2.push('shengHongYu');
console.log(arr1); // ['www','jspang','com'](原数组不变)
console.log(arr2); // ['www','jspang','com','shengHongYu']

6. 箭头函数:简化函数写法,告别this坑

ES6 的箭头函数,把 function 关键字简化成 =>,代码更简洁,还解决了传统函数中 this 指向混乱的问题,前端面试高频考点!

// ES5 函数写法
function fun1(x, y) {
  return x + y;
}

// ES6 箭头函数写法(简化!)
let fun1 = (x, y) => x + y; // 只有一句执行语句,可省略{}和return
console.log(fun1(2, 6)); // 8

// 带默认值的箭头函数
let fun3 = (x, y = 1) => x + y;
console.log(fun3(4)); // 5(y默认值为1)

// 注意:箭头函数没有自己的this,this指向外层作用域
const obj = {
  name: "技术胖",
  say: () => {
    console.log(this.name); // undefined(this指向window,不是obj)
  }
};
obj.say();

7. Set/WeakSet:数组去重神器

Set 是 ES6 新增的数据结构,和数组类似,但不允许有重复值,天生适合数组去重,还有 add()、delete()、has() 等方法,用法简单!

// 1. 声明Set(自动去重)
let setArr = new Set(['jspang','技术胖','web','jspang']);
console.log(setArr); // Set {"jspang", "技术胖", "web"}(重复值被自动过滤)

// 2. 常用方法
setArr.add('前端职场'); // 新增元素
setArr.delete('jspang'); // 删除元素
console.log(setArr.has('技术胖')); // true(判断是否存在)
setArr.clear(); // 清空Set

// 3. 数组去重(实战常用)
let arr = [1,2,2,3,3,3];
let newArr = [...new Set(arr)];
console.log(newArr); // [1,2,3]

8. Map:比对象更灵活的键值对

Map 和对象类似,都是键值对结构,但 Map 的键可以是任意类型(数字、数组、函数、对象),而对象的键只能是字符串/ Symbol,灵活性更高!

// 声明Map并添加键值对
const map = new Map();
let num = 123;
let arr = [1,2,3];
map.set(num, "数字键");
map.set(arr, "数组键");
map.set('name', "技术胖");

// 常用方法
console.log(map.get(num)); // 数字键(获取值)
console.log(map.has('name')); // true(判断键是否存在)
map.delete(arr); // 删除指定键值对
console.log(map.size); // 2(获取键值对数量)
map.clear(); // 清空Map

9. Promise:解决回调地狱,异步编程神器

以前写异步代码(如请求接口、定时器),会出现“回调嵌套回调”的情况,也就是回调地狱,代码混乱难维护,而 Promise 完美解决了这个问题!

// 实战案例:模拟异步操作(洗菜做饭→吃饭→收拾桌子)
let state = 1; // 1表示成功,0表示失败

// 第一步:洗菜做饭
function step1(resolve, reject) {
  console.log('1.开始-洗菜做饭');
  if (state == 1) {
    resolve('洗菜做饭--完成'); // 成功,执行then
  } else {
    reject('洗菜做饭--出错'); // 失败,执行catch
  }
}

// 第二步:吃饭
function step2(resolve, reject) {
  console.log('2.开始-坐下来吃饭');
  if (state == 1) {
    resolve('坐下来吃饭--完成');
  } else {
    reject('坐下来吃饭--出错');
  }
}

// 第三步:收拾桌子
function step3(resolve, reject) {
  console.log('3.开始-收拾桌子洗碗');
  if (state == 1) {
    resolve('收拾桌子洗碗--完成');
  } else {
    reject('收拾桌子洗碗--出错');
  }
}

// 链式调用,避免回调地狱
new Promise(step1)
  .then(val => {
    console.log(val);
    return new Promise(step2); // 执行下一步
  })
  .then(val => {
    console.log(val);
    return new Promise(step3);
  })
  .then(val => {
    console.log(val);
  })
  .catch(err => {
    console.log(err); // 捕获任意一步的错误
  });

10. Class:面向对象编程,简化构造函数

ES6 引入了 Class(类)的概念,简化了 ES5 中构造函数的写法,让面向对象编程更直观,还支持继承,适合大型项目开发!

// 声明类
class Coder {
  // 构造函数(初始化属性)
  constructor(a, b) {
    this.a = a;
    this.b = b;
  }

  // 类的方法
  name(val) {
    console.log(val);
    return val;
  }

  skill(val) {
    console.log(this.name('jspang') + ':' + 'Skill:' + val);
  }

  add() {
    return this.a + this.b;
  }
}

// 实例化类
let jspang = new Coder(1, 2);
jspang.name('jspang'); // 输出:jspang
jspang.skill('web'); // 输出:jspang:Skill:web
console.log(jspang.add()); // 3

// 类的继承(extends关键字)
class htmler extends Coder {}
let pang = new htmler;
pang.name('技术胖'); // 输出:技术胖(继承了Coder类的方法)

三、ES6 必背面试考点(小白必记)

学会以上知识点,日常开发足够用了,但如果要面试,这几个考点一定要记牢,避免踩坑!

  • let/const 和 var 的区别(变量提升、重复声明、块级作用域)
  • 箭头函数和普通函数的区别(this 指向、arguments、不能作为构造函数)
  • Promise 的三种状态(pending、fulfilled、rejected)及链式调用
  • Set 和 Array 的区别(去重、无索引)
  • Map 和对象的区别(键的类型、遍历方式)

四、最后说几句掏心窝的话

很多小白觉得 ES6 难,其实是因为一开始就啃复杂的概念,忽略了“实操”。ES6 的核心是“简化代码、提高效率”,所有知识点都围绕这个核心,只要多敲代码、多练案例,3-7 天就能掌握核心用法!

这篇文章整理了 ES6 最常用、最核心的知识点,代码可直接复制练习,建议收藏起来,遇到不会的就翻一翻,慢慢就熟练了~

如果觉得有用,记得点赞+收藏,关注我,后续更新更多前端小白干货,一起从入门到精通!💪

Vue3 代码编写规范 | 避坑指南+团队协作标准

2026年4月10日 23:05

一、Vue3 通用基础规范(必看!统一编码底线)

1.1 编码格式规范:避免格式混乱,提升代码可读性

  • 缩进:统一使用4个空格缩进(禁止使用Tab),确保不同编辑器渲染一致。
  • 换行:每个独立代码块之间空1行,逻辑相关的代码块紧密排列,提升可读性。
  • 分号:语句结尾统一添加分号,避免因自动分号插入(ASI)导致的语法歧义。
  • 引号:模板内属性使用双引号(""),Script中字符串优先使用单引号(''),特殊场景(如嵌套引号)可灵活切换。
  • 注释:关键逻辑、复杂业务代码必须添加注释,注释需简洁明了,说明“为什么做”而非“做了什么”;组件开头可添加类注释,说明组件功能、Props、使用场景。

1.2 命名规范:一眼看懂用途,降低协作成本

核心原则:JS/TS领域遵循camelCase(小驼峰)/PascalCase(大驼峰),HTML领域使用kebab-case(连字符),保持项目内命名一致性,提升代码可读性与协作效率。

  • 变量/函数:使用camelCase,首字母小写,动词开头命名函数(如handleClick、fetchData),名词开头命名变量(如userInfo、goodsList)。
  • 常量:使用UPPER_SNAKE_CASE(全大写下划线分隔),如const API_BASE_URL = 'api.example.com'。
  • 类/组件:使用PascalCase,首字母大写,组件名需为多个单词(根组件App除外),避免与HTML原生元素冲突,如UserProfile、GoodsCard而非Todo、Button。
  • 自定义指令:使用kebab-case,如v-focus、v-scroll-to,符合HTML属性命名规范。

二、Vue3 单文件组件(SFC)规范(核心重点!避坑关键)

2.1 组件结构规范:固定结构,避免渲染异常

单文件组件(.vue)内部顺序固定为:template → script → style,每个部分独立成块,结构清晰;template内最多包含一个顶级元素,避免多根节点导致的渲染异常。

<!-- 正确示例 -->
<template>
    <div class="user-profile">
        <!-- 组件内容 -->
    </div>
</template>

<script setup>
// 逻辑代码
</script>

<style scoped>
// 样式代码
</style>

2.2 Template 规范:高效渲染,减少性能损耗

  • 指令使用:v-bind、v-on可使用简写(:、@),v-slot使用#简写;指令顺序统一为:v-for → v-if → v-bind → v-on,如<div v-for="item in list" :key="item.id" v-if="item.visible" @click="handleClick">
  • v-for 要求:必须搭配key,key值需为唯一标识(如id),禁止使用index作为key;避免在v-for内使用v-if,可通过计算属性过滤数据后再渲染,提升性能。
  • 组件引用:模板中使用组件时,优先使用PascalCase标签(如),明确区分原生HTML元素;DOM模板中必须使用kebab-case(如),因HTML不区分大小写。
  • 属性绑定:多个属性分行书写,每个属性占一行,提升可读性;布尔属性直接写属性名,如而非。

2.3 Script 规范:简洁高效,符合Vue3最佳实践

2.3.1 语法选择:优先<script setup>,拒绝混合语法

优先使用<script setup>语法(Vue3推荐),简洁高效;复杂组件(如需要生命周期钩子、Props验证、 emits定义)可结合Options API,但同一项目内语法需统一,禁止混合使用。

2.3.2 导入顺序:规范排序,提升代码可维护性

导入语句按以下顺序排列,不同类别之间空1行,提升可读性:

  1. Vue内置API(如ref、computed、watch);
  2. 第三方库(如Pinia、Axios、Element Plus);
  3. 项目内部组件(如子组件、基础组件);
  4. 工具函数、常量、样式文件;
  5. API接口请求函数。
<script setup>
// 1. Vue内置API
import { ref, computed, watch } from 'vue';
// 2. 第三方库
import { useUserStore } from 'pinia';
import axios from 'axios';
// 3. 内部组件
import BaseButton from './BaseButton.vue';
import UserCard from '@/components/UserCard.vue';
// 4. 工具函数/常量
import { formatDate } from '@/utils/format';
import { API_BASE_URL } from '@/constants';
// 5. API接口
import { fetchUserInfo } from '@/api/user';
</script>

2.3.3 Props 规范:严谨定义,避免传参异常

  • 命名:Props定义使用camelCase(如userName),模板中传递时使用kebab-case(如user-name),Vue会自动完成转换。
  • 定义:Props需详细定义,至少指定类型;必填项标注required: true,可选值通过validator验证,提升组件可维护性与容错性。
// 正确示例
const props = defineProps({
    // 基础类型定义
    userId: {
        type: Number,
        required: true,
        validator: (value) => value > 0 // 验证值为正整数
    },
    // 布尔类型,推荐前缀is
    isDisabled: {
        type: Boolean,
        default: false
    },
    // 数组/对象类型,默认值需用函数返回,避免引用共享
    goodsList: {
        type: Array,
        default: () => []
    },
    userInfo: {
        type: Object,
        default: () => ({
            name: '',
            age: 0
        })
    }
});

2.3.4 Emits 规范:明确声明,避免事件混乱

  • 命名:定义时使用camelCase(如updateValue),模板中监听时使用kebab-case(如@update-value),符合HTML属性命名习惯。
  • 定义:通过defineEmits明确声明组件触发的事件,禁止隐式触发事件;事件参数需清晰,避免传递过多参数,复杂参数建议封装为对象。
// 正确示例
const emit = defineEmits(['updateValue', 'deleteItem']);

// 触发事件(传递单个参数)
const handleValueChange = (value) => {
    emit('updateValue', value);
};

// 触发事件(传递复杂参数,封装为对象)
const handleDelete = (id, name) => {
    emit('deleteItem', { id, name });
};

2.3.5 异步逻辑规范:优雅处理,避免报错中断

  • 优先使用async/await语法,禁止使用Promise链式调用(then/catch),代码更易读且便于调试。
  • 所有async/await必须包裹try/catch,或在调用时用.catch()捕获错误,避免控制台报错和逻辑中断;错误处理需友好,可结合UI提示反馈给用户。
  • 高频触发的异步请求(如搜索输入框)必须加防抖,避免无效请求,推荐用组合式函数useDebounce封装复用。
// 正确示例(async/await + try/catch)
const fetchUser = async () => {
    try {
        const res = await fetchUserInfo(); // 调用异步接口
        return res.data;
    } catch (err) {
        console.error('获取用户信息失败:', err);
        ElMessage.error('加载失败,请重试');
        throw err; // 如需上层处理,可重新抛出错误
    }
};

// 错误示例(Promise链式调用)
const fetchUser = () => {
    return fetchUserInfo()
        .then(res => res.data)
        .catch(err => {
            console.error('获取用户信息失败:', err);
            ElMessage.error('加载失败,请重试');
            throw err;
        });
};

2.3.6 TypeScript 规范:强类型约束,减少类型报错

  • 禁止滥用any类型:除非明确兼容所有类型(如第三方库无类型声明),否则必须用具体类型、unknown或泛型;若用any,需加注释说明原因。
  • 接口(interface)与类型别名(type)区分:定义对象/类的结构用interface(支持扩展、实现);定义联合类型、交叉类型或简单类型别名用type。
  • Props/Emits 类型:使用TypeScript时,优先通过泛型定义Props和Emits类型,提升类型安全性。
// 正确示例(interface定义对象结构)
interface Goods {
    id: number;
    name: string;
    price: number;
    stock: number;
}
const goods: Goods = { id: 1, name: '手机', price: 5999, stock: 100 };

// 正确示例(type定义联合类型)
type GoodsCategory = 'electronics' | 'clothes' | 'food';

// Props类型定义
interface Props {
    userId: number;
    isDisabled?: boolean;
}
const props = defineProps<Props>();

// Emits类型定义
const emit = defineEmits<{
    (e: 'updateValue', value: string): void;
    (e: 'deleteItem', params: { id: number; name: string }): void;
}>();

2.4 Style 规范:避免污染,提升样式复用性

  • 作用域:组件样式优先使用scoped(如),避免样式污染;全局样式统一放在src/styles目录下,禁止在组件内写全局样式(除非特殊需求)。
  • 命名:样式类名使用kebab-case,与组件名、功能对应,如.user-profile、goods-card;避免使用无意义的类名(如box1、content2)。
  • 样式顺序:按“布局 → 尺寸 → 样式 → 交互”的顺序编写,如position → width → background → hover。
  • 复用:公共样式(如颜色、字体、间距)提取为变量,统一管理;重复使用的样式封装为Mixin或自定义样式类,提升复用性。

三、Vue3 组件设计规范(高复用+低耦合,团队必守)

3.1 组件拆分原则:拒绝大组件,提升可维护性

  • 单一职责:一个组件只负责一个功能,避免“大组件”(代码超过500行),复杂功能拆分为多个子组件,如将用户列表拆分为UserList(列表容器)、UserItem(列表项)、UserSearch(搜索框)。
  • 高复用低耦合:可复用组件(如按钮、输入框)提取为基础组件(放在src/components/base目录),组件间通过Props传递数据、Emits触发事件,禁止直接操作父/子组件数据。
  • 命名区分:基础组件统一前缀Base(如BaseButton、BaseInput),业务组件按功能命名(如OrderList、PaymentForm),布局组件前缀Layout(如LayoutHeader、LayoutSidebar)。

3.2 组件通信规范:清晰传参,避免数据混乱

  • 父子组件:父传子用Props,子传父用Emits,禁止子组件直接修改Props(单向数据流);复杂数据可通过v-model双向绑定(Vue3支持多v-model)。
  • 跨层级组件:优先使用Pinia状态管理,或使用provide/inject(适用于深层组件通信,需明确注入类型),禁止使用EventBus(易造成事件混乱)。
  • 同级组件:通过父组件中转(子传父 → 父传另一个子),或使用Pinia共享状态,避免直接通信。

四、Vue3 Pinia 状态管理规范(替代Vuex,简洁高效)

4.1 Store 设计原则:模块化拆分,避免冗余

  • 模块化:按业务模块拆分Store(如userStore、cartStore、goodsStore),避免单一Store过大;Store命名统一前缀use(如useUserStore),使用camelCase命名法。
  • 状态划分:State(状态)、Getters(计算属性)、Actions(异步/同步操作)分离,禁止在Getters中修改State,禁止在组件中直接修改Store的State(需通过Actions)。

4.2 状态操作规范:规范调用,避免状态异常

// stores/user.ts 正确示例
import { defineStore } from 'pinia';
import { fetchUserInfo } from '@/api/user';

export const useUserStore = defineStore('user', () => {
    // State:定义状态,使用ref/reactive
    const userInfo = ref({
        id: 0,
        name: '',
        avatar: ''
    });
    const isLogin = ref(false);

    // Getters:计算属性,依赖State,只读
    const userNickname = computed(() => userInfo.value.name || '未知用户');

    // Actions:处理同步/异步操作,修改State
    const setUserInfo = (info) => {
        userInfo.value = info;
        isLogin.value = true;
    };

    const logout = () => {
        userInfo.value = { id: 0, name: '', avatar: '' };
        isLogin.value = false;
    };

    // 异步Action,使用async/await
    const loadUserInfo = async (userId) => {
        try {
            const res = await fetchUserInfo(userId);
            setUserInfo(res.data);
        } catch (err) {
            console.error('加载用户信息失败:', err);
            throw err;
        }
    };

    return { userInfo, isLogin, userNickname, setUserInfo, logout, loadUserInfo };
});

五、Vue3 Vue Router 路由规范(优化体验,避免路由踩坑)

  • 路由命名:路由name使用kebab-case(如user-profile),与组件名、路径对应,提升可读性;路由path使用kebab-case(如/user/profile),符合URL命名规范。
  • 路由懒加载:所有路由组件均使用懒加载(() => import('组件路径')),减少首屏加载时间;基础组件无需懒加载。
  • 路由守卫:全局守卫用于权限控制(如登录验证),路由独享守卫用于单个路由的特殊控制,组件内守卫用于组件内的生命周期控制;避免在守卫中写复杂业务逻辑。
  • 参数传递:路径参数(params)用于必填参数(如/user/:id),查询参数(query)用于可选参数(如/list?page=1&size=10);接收参数时需做类型校验。
// router/index.ts 正确示例
import { createRouter, createWebHistory } from 'vue-router';

const routes = [
    {
        path: '/',
        name: 'home',
        component: () => import('@/views/Home.vue'),
        meta: { title: '首页', requiresAuth: false }
    },
    {
        path: '/user/:id',
        name: 'user-profile',
        component: () => import('@/views/UserProfile.vue'),
        meta: { title: '用户详情', requiresAuth: true },
        props: true // 自动将params转为Props传递给组件
    },
    {
        path: '/404',
        name: '404',
        component: () => import('@/views/404.vue')
    },
    {
        path: '/:pathMatch(.*)*',
        redirect: '/404' // 路由匹配失败,重定向到404
    }
];

const router = createRouter({
    history: createWebHistory(import.meta.env.BASE_URL),
    routes
});

// 全局前置守卫:登录验证
router.beforeEach((to, from, next) => {
    const userStore = useUserStore();
    if (to.meta.requiresAuth && !userStore.isLogin) {
        next('/login');
    } else {
        document.title = to.meta.title || 'Vue3 项目';
        next();
    }
});

export default router;

六、Vue3 工程化与协作规范(团队高效协作必备)

6.1 文件目录规范:结构清晰,便于维护

项目目录结构清晰,按功能模块划分,便于维护和协作,推荐目录结构如下:

src/
├── assets/          // 静态资源(图片、字体、图标等),命名使用kebab-case
│   ├── images/
│   ├── fonts/
│   └── icons/
├── components/      // 公共组件
│   ├── base/        // 基础组件(BaseButton、BaseInput等)
│   ├── layout/      // 布局组件(LayoutHeader、LayoutSidebar等)
│   └── business/    // 业务组件(OrderList、GoodsCard等)
├── views/           // 页面视图组件,命名使用PascalCase
│   ├── Home.vue
│   ├── UserProfile.vue
│   └── Order/
│       ├── OrderList.vue
│       └── OrderDetail.vue
├── stores/          // Pinia状态管理,命名使用useXXXStore.ts
│   ├── useUserStore.ts
│   └── useCartStore.ts
├── router/          // 路由配置
│   └── index.ts
├── api/             // API接口封装,按模块划分
│   ├── user.ts
│   └── goods.ts
├── utils/           // 工具函数,命名使用camelCase
│   ├── format.ts
│   └── request.ts
├── constants/       // 常量定义
│   └── index.ts
├── styles/          // 全局样式
│   ├── index.scss
│   └── variables.scss
├── composables/     // 组合式函数,复用逻辑
│   └── useDebounce.ts
└── App.vue          // 根组件

6.2 代码提交规范(Git Commit):清晰可追溯,便于审查

采用Conventional Commits标准,提交信息清晰,便于代码审查和版本回溯,格式为:(): 。

  • type(提交类型):feat(新功能)、fix(Bug修复)、docs(文档变更)、style(代码样式调整,不影响逻辑)、refactor(重构,不修复Bug也不增加功能)、test(测试相关)、chore(构建/工具变更)。
  • scope(范围):指定提交影响的模块(如user、router、goods),无明确范围可省略。
  • subject(描述):简洁明了,说明提交内容,首字母小写,结尾不加句号。
// 示例
feat(user): add password reset UI
fix(router): handle 404 redirect
chore(deps): upgrade axios to 1.2.0
docs: update component usage documentation

6.3 代码校验规范:统一格式,减少冲突

  • 工具配置:项目必须集成ESLint、Prettier,统一代码格式;安装依赖:npm install -D eslint prettier eslint-plugin-vue @typescript-eslint/parser eslint-config-prettier husky lint-staged。
  • 自动校验:配置pre-commit钩子(husky + lint-staged),提交代码时自动校验格式,不符合规范的代码禁止提交;开发过程中使用编辑器插件(如ESLint、Prettier)实时校验。
  • ESLint配置:继承vue3-recommended规范,结合项目需求调整规则,禁止禁用必要的校验规则(如禁止滥用any、禁止Props修改)。

七、Vue3 性能与安全规范(优化体验+规避风险)

7.1 性能优化规范:提速降耗,提升用户体验

  • 响应式优化:避免过度使用reactive,简单数据使用ref;大数据列表使用v-virtual-scroller(虚拟滚动),减少DOM渲染数量。
  • 计算属性与监听:computed用于依赖状态的计算(缓存结果),watch用于监听状态变化并执行副作用(如请求接口);避免在watch中写复杂逻辑,避免监听过多状态。
  • 资源优化:静态资源(图片)压缩,使用CDN加载;路由懒加载、组件懒加载;避免重复请求(添加请求缓存、防抖节流)。
  • DOM优化:减少DOM操作,避免在模板中使用复杂表达式;使用v-show替代v-if(频繁切换场景),v-if替代v-show(一次性渲染场景)。

7.2 安全规范:规避漏洞,保障项目稳定

  • XSS防护:避免直接插入HTML(如v-html),若必须使用,需对内容进行过滤;禁止使用eval、with等危险语法。
  • 接口安全:请求接口时添加token验证;敏感数据(如密码)加密后传输;接口返回数据需做类型校验,避免恶意数据导致的报错。
  • 依赖安全:定期更新项目依赖,避免使用存在安全漏洞的依赖包;安装依赖前检查依赖安全性(如使用npm audit)。

八、Vue3 补充规范(细节拉满,避免踩坑)

  • 兼容性:兼容主流浏览器(Chrome、Edge、Firefox最新版本),如需兼容旧浏览器(如IE11),需添加相应的polyfill。
  • 可维护性:代码书写简洁,避免冗余(如重复代码封装为函数/组件);注释清晰,便于后续维护和他人理解。
  • 一致性:项目内所有代码严格遵循本规范,团队成员需统一认知;新增规范需团队讨论确认后补充,避免个人风格差异导致的代码混乱。
  • 废弃代码:禁止保留无用代码(如注释掉的代码、未使用的变量/函数/组件),提交代码前删除废弃内容,保持代码整洁。

前端必看!JS高频实用案例(单行代码+实战场景+十大排序)

2026年4月10日 17:14

一、JS单行高频实用案例(25个,直接复制可用)

1. 变量值交换(不使用临时变量)

适用场景:快速交换两个变量的值,无需额外声明临时变量,简化代码,适用于简单值交换场景。

// 核心单行代码(适用于数字、字符串等基本类型)
let a = 10, b = 20;
[a, b] = [b, a]; // 解构赋值实现交换
// 结果: a = 20, b = 10

// 补充案例(字符串交换)
let str1 = 'hello', str2 = 'world';
[str1, str2] = [str2, str1];
// 结果: str1 = 'world', str2 = 'hello'

总结:利用数组解构赋值,简洁实现两个变量值的交换,无需临时变量,代码简洁易读,仅适用于基本数据类型;若为引用类型(对象、数组),交换的是引用地址,原数据会受影响。

2. 快速生成随机数(指定范围)

适用场景:生成指定区间内的随机整数(如随机抽奖、随机排序、模拟随机数据),前端高频使用场景。

// 核心单行代码(min=最小值,max=最大值,包含min和max)
const getRandomNum = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
// 案例1:生成1-10的随机整数
const random1 = getRandomNum(1, 10);
// 结果: 1-10之间的任意整数(如5、8、10)

// 案例2:生成100-200的随机整数
const random2 = getRandomNum(100, 200);
// 结果: 100-200之间的任意整数(如156、199、100)

总结:Math.random()生成0-1(不包含1)的随机小数,乘以(max-min+1)可将范围扩展到0-(max-min),Math.floor()取整后加min,即可得到指定区间内的随机整数,灵活适配各类随机场景。

3. 浅克隆对象(复制顶层属性)

适用场景:快速复制对象的顶层属性,生成新对象,适用于简单对象(无嵌套对象)的复制,避免修改原对象。

const originalObj = { name: '张三', age: 24 }; // 修正原文档拼写错误(original0bj→originalObj)
const clonedObj = { ...originalObj }; // 核心单行代码
// 结果: clonedObj = { name: '张三', age: 24 }
// 验证:修改clonedObj.age = 25,originalObj.age仍为24

总结:利用扩展运算符(...)复制对象的所有可枚举自身属性,生成新对象。注意:若对象包含嵌套对象,嵌套对象仍为引用类型,修改嵌套对象会影响原对象,此时需使用深克隆。

4. 合并对象

适用场景:将多个对象合并为一个新对象,重复属性会被后一个对象覆盖。

const obj1 = { name: '张三' };
const obj2 = { age: 22 };
const mergedObj = { ...obj1, ...obj2 }; // 核心单行代码
// 结果: mergedObj = { name: '张三', age: 22 }

总结:扩展运算符可快速合并多个对象,若存在重复属性,后面的对象属性会覆盖前面的。例如:const obj3 = { name: '李四', gender: '男' }; 合并后mergedObj = { name: '李四', age:22, gender: '男' }。

5. 清理数组(删除所有假值)

适用场景:快速过滤数组中的假值,保留有效数据。

const arr = [0, 1, false, 2, '', 3, null, NaN, undefined]; // 补充完整假值案例
const cleanedArray = arr.filter(Boolean); // 核心单行代码
// 结果: cleanedArray = [1, 2, 3]

总结:Array.prototype.filter() 结合Boolean函数,自动过滤所有假值(0、false、null、''、NaN、undefined),无需手动判断,高效简洁。

6. 将NodeList转换为数组

适用场景:获取DOM元素集合(NodeList)后,需使用数组方法(如map、filter)操作时。

// 修正原文档语法疏漏(补充扩展运算符包裹)
const nodesArray = [...document.querySelectorAll('div')]; 
// 结果: nodesArray 为包含所有div元素的数组,可使用map、filter等方法

总结:扩展运算符可将类数组(NodeList、arguments等)转换为真正的数组,从而使用数组的所有方法。例如:nodesArray.map(div => div.style.color = 'red'),可批量修改所有div的字体颜色。

7. 检查数组是否满足指定条件

适用场景:判断数组中是否存在满足条件的元素(some),或所有元素是否都满足条件(every)。

// 案例1:检查数组中是否存在负数
const arr1 = [1, 2, 3, -5, 4];&#xA;const hasNegativeNumbers = arr1.some(num => num < 0);
// 结果: hasNegativeNumbers = true

// 案例2:检查数组所有元素是否均为正数
const allPositive = arr1.every(num => num > 0);
// 结果: allPositive = false

// 补充案例:检查数组中是否有大于10的元素
const arr2 = [5, 8, 12, 3];
const hasGreaterThan10 = arr2.some(num => num > 10);
// 结果: hasGreaterThan10 = true

总结:some() 只要有一个元素满足条件就返回true,every() 需所有元素满足条件才返回true,两者均为短路操作(找到符合条件/不符合条件的元素后立即停止遍历)。

8. 将文本复制到剪贴板

适用场景:实现点击按钮复制文本、复制链接等功能。

// 核心单行代码(异步操作,可结合async/await使用)
navigator.clipboard.writeText('Text to copy'); // 修正原文档拼写(Textto→Text to)

// 完整示例(结合按钮点击)
document.querySelector('#copyBtn').addEventListener('click', async () => {
  await navigator.clipboard.writeText('要复制的文本');
  alert('复制成功!');
});

总结:使用Clipboard API实现文本复制,比传统的“创建input复制”更简洁,但需注意浏览器兼容性(IE不支持),且需在HTTPS协议或本地环境下使用。

9. 删除数组重复项

适用场景:快速去重,适用于基本数据类型(数字、字符串、布尔值等)的数组。

const arr = [1, 2, 2, 3, 4, 4, 5, 5, 5];
const unique = [...new Set(arr)]; // 核心单行代码
// 结果: unique = [1, 2, 3, 4, 5]

总结:利用Set对象“值唯一”的特性,结合扩展运算符将Set转换为数组,实现快速去重。注意:若数组包含对象,此方法无法去重(对象引用不同),需额外处理。

10. 取两个数组的交集

适用场景:获取两个数组中共同存在的元素。

const arr1 = [1, 2, 3, 4];
const arr2 = [2, 4, 6, 8];
const intersection = arr1.filter(value => arr2.includes(value)); // 核心单行代码
// 结果: intersection = [2, 4]

// 补充案例(字符串数组交集)
const arr3 = ['a', 'b', 'c'];
const arr4 = ['b', 'c', 'd'];
const strIntersection = arr3.filter(value => arr4.includes(value));
// 结果: strIntersection = ['b', 'c']

总结:通过filter()遍历第一个数组,使用includes()判断元素是否存在于第二个数组中,筛选出共同元素。若数组元素较多,可先将第二个数组转为Set,提升查询效率:const arr2Set = new Set(arr2); const intersection = arr1.filter(value => arr2Set.has(value))。

11. 求数组元素的总和

适用场景:快速计算数组中所有基本数据类型(数字)的总和。

const arr = [1, 2, 3, 4];
const sum = arr.reduce((total, value) => total + value, 0); // 核心单行代码
// 结果: sum = 10

// 补充案例(含负数和小数)
const arr2 = [1.5, 2.5, -3, 4];
const sum2 = arr2.reduce((total, value) => total + value, 0);
// 结果: sum2 = 5

总结:reduce() 方法接收回调函数和初始值,回调函数中的total为累加值,value为当前元素,遍历数组并累加所有元素。初始值设为0,避免数组为空时返回undefined。

12. 根据指定条件,给对象的属性赋值

适用场景:根据条件动态给对象添加属性,避免冗余的if-else语句。

const condition = true;
const value = '你好, 世界';
const newObject = { ...(condition && { key: value }) }; // 核心单行代码
// 结果: newObject = { key: '你好, 世界' }

// 补充案例(条件为false)
const condition2 = false;
const newObject2 = { ...(condition2 && { key: value }) };
// 结果: newObject2 = {}

总结:利用短路求值(&&),当条件为true时,返回{key: value},并通过扩展运算符添加到新对象中;当条件为false时,短路返回false,扩展运算符会忽略false,不添加任何属性。

13. 使用变量作为对象的键

适用场景:动态设置对象的键名(键名不确定,需通过变量指定)。

const dynamicKey = 'name';
const value = '张三';
const obj = { [dynamicKey]: value }; // 核心单行代码(计算属性名)
// 结果: obj = { name: '张三' }

// 补充案例(动态切换键名)
const dynamicKey2 = 'age';
const obj2 = { [dynamicKey2]: 24 };
// 结果: obj2 = { age: 24 }

总结:通过计算属性名(方括号包裹变量),可将变量的值作为对象的键名,灵活适配动态场景,例如根据接口返回值动态设置对象键名。

14. 离线状态检查器

适用场景:检测用户浏览器的网络连接状态,提示用户当前是否在线。

const isOnline = navigator.onLine ? '在线' : '离线'; // 核心单行代码
// 结果: 网络正常时返回'在线',断开时返回'离线'

// 完整示例(实时监听网络状态)
window.addEventListener('online', () => console.log('网络已连接,当前状态:在线'));
window.addEventListener('offline', () => console.log('网络已断开,当前状态:离线'));

总结:利用navigator.onLine属性结合三元运算符,快速判断网络状态。注意:navigator.onLine仅能检测是否有网络连接,无法判断网络是否能正常访问互联网(如连接了无网络的WiFi)。

15. 离开页面弹出确认对话框

适用场景:防止用户误操作关闭页面,导致未保存的数据丢失(如表单填写、编辑内容)。

// 核心单行代码
window.onbeforeunload = () => '你确定要离开吗?未保存的内容将丢失!'; // 补充提示信息

// 补充:现代浏览器对提示文本的限制(部分浏览器不显示自定义文本,仅显示默认提示)
window.onbeforeunload = (e) => {
  e.preventDefault();
  e.returnValue = '';
  return '你确定要离开吗?';
};

总结:监听window的onbeforeunload事件,当用户关闭页面、刷新页面或跳转页面时,会弹出确认对话框。注意:现代浏览器为了安全,可能会忽略自定义提示文本,仅显示浏览器默认提示。

16. 对象数组,根据对象的某个key求对应值的总和

适用场景:统计对象数组中,指定属性的所有值的总和(如统计订单金额、商品数量等)。

const arrayOfObjects = [{ x: 1 }, { x: 2 }, { x: 3 }];
// 核心单行函数(可复用)
const sumBy = (arr, key) => arr.reduce((acc, obj) => acc + obj[key], 0);
const total = sumBy(arrayOfObjects, 'x'); // 传入数组和指定key
// 结果: total = 6

// 补充案例(统计订单金额)
const orders = [{ amount: 100 }, { amount: 200 }, { amount: 150 }];
const totalAmount = sumBy(orders, 'amount');
// 结果: totalAmount = 450

总结:封装sumBy函数,利用reduce()遍历对象数组,累加指定key对应的属性值,可灵活复用,适用于各种对象数组的统计场景。

17. 将URL问号后面的查询字符串转为对象

适用场景:快速解析URL中的查询参数,方便获取参数值(如页面跳转传参、接口请求参数解析)。

const query = 'name=John&age=30&gender=male'; // 补充多参数案例
// 核心单行代码(修正原文档拼写错误:0bject→Object)
const parseQuery = query => Object.fromEntries(new URLSearchParams(query));
const queryObj = parseQuery(query);
// 结果: queryObj = { name: 'John', age: '30', gender: 'male' }

总结:URLSearchParams用于解析查询字符串,返回可迭代对象,再通过Object.fromEntries()将其转换为对象,解析后的参数值均为字符串,若需数字类型,需手动转换(如Number(queryObj.age))。

18. 将秒数转换为时间格式的字符串(HH:MM:SS)

适用场景:将秒数转换为标准时间格式,如视频时长、倒计时显示等。

const seconds = 3661; // 1小时1分1秒(修正原文档注释错误:3600秒为1小时)
// 核心单行代码(修正原文档拼写错误:toIsostring→toISOString、substr→slice)
const toTimeString = seconds => new Date(seconds * 1000).toISOString().slice(11, 19);
const timeStr = toTimeString(seconds);
// 结果: timeStr = '01:01:01'

// 补充案例(不足1小时、不足1分钟)
const seconds2 = 61; // 1分1秒
const timeStr2 = toTimeString(seconds2);
// 结果: timeStr2 = '00:01:01'

const seconds3 = 5; // 5秒
const timeStr3 = toTimeString(seconds3);
// 结果: timeStr3 = '00:00:05'

总结:将秒数乘以1000(转换为毫秒),创建Date对象,再通过toISOString()获取标准时间字符串,最后截取时间部分(HH:MM:SS),适用于所有正秒数的转换。

19. 求某对象所有属性值的最大值

适用场景:快速获取对象中所有属性值的最大值(如统计最高分、最大金额等)。

const scores = { math: 95, chinese: 99, english: 88 }; // 数学、语文、英语成绩
// 核心单行代码(修正原文档拼写错误:0bject→Object、max0bjectValue→maxObjectValue)
const maxObjectValue = obj => Math.max(...Object.values(obj));
const maxScore = maxObjectValue(scores);
// 结果: maxScore = 99

// 补充案例(数字属性值含小数)
const prices = { apple: 5.9, banana: 3.5, orange: 4.8 };
const maxPrice = maxObjectValue(prices);
// 结果: maxPrice = 5.9

总结:Object.values(obj)提取对象所有属性值,生成数组,再通过扩展运算符将数组元素作为Math.max()的参数,获取最大值。注意:对象属性值必须为数字类型,否则会返回NaN。

20. 判断对象的值中是否包含某个值

适用场景:检查对象的所有属性值中,是否存在指定的值。

const person = { name: '张三', age: 30, gender: '男' }; // 补充多属性案例
// 核心单行代码(修正原文档拼写错误:0bject→Object)
const hasValue = (obj, value) => Object.values(obj).includes(value);
// 案例1:判断是否包含30
const has30 = hasValue(person, 30); // 结果: true
// 案例2:判断是否包含'女'
const hasFemale = hasValue(person, '女'); // 结果: false

总结:Object.values(obj)获取对象所有属性值的数组,再通过includes()判断指定值是否在数组中,适用于快速检查对象值的存在性。

21. 安全访问深度嵌套的对象属性

适用场景:访问嵌套层级较深的对象属性,避免因中间属性不存在导致的TypeError错误。

// 案例1:中间属性存在
const user = { profile: { name: '张三' } };
const userName = user.profile?.name ?? '匿名'; // 核心单行代码
// 结果: userName = '张三'

// 案例2:中间属性不存在(profile为undefined)
const user2 = { };
const userName2 = user2.profile?.name ?? '匿名';
// 结果: userName2 = '匿名'

// 补充:区分??和||的差异
const user3 = { profile: { name: '' } };
const userName3 = user3.profile?.name ?? '匿名'; // 结果: ''(空字符串不是null/undefined,不触发默认值)
const userName4 = user3.profile?.name || '匿名'; // 结果: '匿名'(空字符串是假值,触发默认值)

总结:可选链运算符(?.):当中间属性为null/undefined时,短路返回undefined,避免报错;空值合并运算符(??):仅当左侧为null/undefined时,返回右侧默认值,不影响其他假值(如''、0)。两者结合,可安全访问深度嵌套属性并设置默认值。

22. 条件执行语句

适用场景:无需if语句,简洁实现“条件为真时执行函数/赋值”。

// 案例1:条件执行函数
const isEligible = true;
const performAction = () => console.log('执行操作');
isEligible && performAction(); // 核心单行代码(条件为真时执行)
// 结果: 输出'执行操作'

// 案例2:条件赋值(修正原文档语法,补充括号)
const isEligible2 = true;
let value = '';
isEligible2 && (value = '条件达成'); // 核心单行代码
// 结果: value = '条件达成'

// 补充案例:条件为false时不执行
const isEligible3 = false;
isEligible3 && performAction(); // 无输出

总结:利用逻辑AND(&&)的短路特性,左侧为true时,才执行右侧的函数或赋值语句;左侧为false时,直接短路,不执行右侧代码。适用于简单的条件判断场景,简化代码。

23. 创建包含指定数字范围的数组

适用场景:快速生成连续数字数组(如分页页码、循环计数等)。

// 案例1:创建1-5的数组
const range1 = Array.from({ length: 5 }, (_, i) => i + 1); // 核心单行代码
// 结果: range1 = [1, 2, 3, 4, 5]

// 案例2:创建5-10的数组
const range2 = Array.from({ length: 6 }, (_, i) => i + 5);
// 结果: range2 = [5, 6, 7, 8, 9, 10]

// 案例3:创建0-4的数组(简化)
const range3 = Array.from({ length: 5 }, (_, i) => i);
// 结果: range3 = [0, 1, 2, 3, 4]

总结:Array.from()接收两个参数:类数组对象(设置length指定数组长度)和映射函数,映射函数通过索引(i)生成指定范围的数字。下划线(_)表示未使用的参数(当前元素),是JS中的常用惯例。

24. 提取文件扩展名

适用场景:获取文件名中的扩展名(如判断文件类型、上传文件校验等)。

const fileName1 = 'example.png';
const fileName2 = 'document.pdf';
const fileName3 = 'image.jpg';
const fileName4 = 'text'; // 无扩展名
// 核心单行代码(修正原文档拼写错误:lastIndex0f→lastIndexOf、getFileExtension返回值错误)
const getFileExtension = str => str.slice(((str.lastIndexOf(".") - 1) >>> 0) + 2);
// 案例验证
console.log(getFileExtension(fileName1)); // 结果: 'png'
console.log(getFileExtension(fileName2)); // 结果: 'pdf'
console.log(getFileExtension(fileName3)); // 结果: 'jpg'
console.log(getFileExtension(fileName4)); // 结果: ''(无扩展名返回空字符串)

总结:通过lastIndexOf(".")找到最后一个点号的位置,使用位运算符(>>>)确保即使未找到点号(返回-1),操作也安全,最终截取点号后面的字符串作为扩展名。

25. 切换元素的class

适用场景:动态添加/移除元素的class(如菜单显示/隐藏、按钮选中/取消、表单状态切换等)。

// 核心代码(修正原文档语法错误,补充括号)
const element = document.querySelector('.myelement');
const toggleClass = (el, className) => el.classList.toggle(className);
// 切换class(存在则删除,不存在则添加)
toggleClass(element, 'active');

// 完整示例(点击按钮切换元素class)
document.querySelector('#toggleBtn').addEventListener('click', () => {
  toggleClass(element, 'active');
});

总结:classList.toggle()方法自动判断元素是否包含指定class,包含则删除,不包含则添加,无需手动判断,简洁高效,是前端开发中动态控制元素样式的常用方法。

二、JS实战高频案例(18个,覆盖前端常用场景)

1. 防抖函数(避免高频事件频繁触发)

适用场景:防止输入框输入、窗口resize、滚动等高频事件频繁触发函数,适用于搜索联想、输入验证等场景。

// 核心单行防抖函数(简化版)
const debounce = (fn, delay = 500) => {
  let timer = null;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
};
// 使用示例(输入框搜索)
const search = (value) => console.log('搜索:', value);
const debouncedSearch = debounce(search, 300);
// 输入框输入时调用,仅在停止输入300ms后触发
document.querySelector('#searchInput').addEventListener('input', (e) => {
  debouncedSearch(e.target.value);
});

2. 节流函数(限制函数触发频率)

适用场景:限制函数在指定时间内只能触发一次,适用于滚动加载、按钮点击防重复提交等场景。

// 核心单行节流函数(简化版)
const throttle = (fn, interval = 1000) => {
  let lastTime = 0;
  return (...args) => {
    const now = Date.now();
    if (now - lastTime >= interval) {
      fn.apply(this, args);
      lastTime = now;
    }
  };
};
// 使用示例(滚动加载)
const loadMore = () => console.log('加载更多数据');
const throttledLoadMore = throttle(loadMore, 2000);
// 滚动时调用,每2000ms只能触发一次
window.addEventListener('scroll', throttledLoadMore);

3. 深克隆对象(解决嵌套对象复制问题)

适用场景:复制包含嵌套对象的复杂对象,确保修改克隆对象不影响原对象,适用于数据备份、复杂数据处理。

// 核心单行深克隆(简单场景,不支持函数、Symbol等特殊类型)
const deepClone = obj => JSON.parse(JSON.stringify(obj));
// 示例
const originalObj = {
  name: '张三',
  age: 24,
  address: { city: '北京', area: '朝阳' } // 嵌套对象
};
const clonedObj = deepClone(originalObj);
clonedObj.address.city = '上海'; // 修改克隆对象的嵌套属性
console.log(originalObj.address.city); // 结果: '北京'(原对象不受影响)

// 补充:复杂场景深克隆(支持函数、Symbol,需使用递归)
const deepCloneAdvanced = (obj) => {
  if (obj === null || typeof obj !== 'object') return obj;
  const clone = Array.isArray(obj) ? [] : {};
  for (let key in obj) {
    clone[key] = deepCloneAdvanced(obj[key]);
  }
  return clone;
};

4. 精准检查数据类型

适用场景:精准判断数据类型(如区分数组、对象、null、函数等),避免typeof的局限性,适用于数据校验场景。

// 核心单行函数
const getType = (data) => Object.prototype.toString.call(data).slice(8, -1).toLowerCase();
// 案例验证
console.log(getType(123)); // 结果: 'number'
console.log(getType('abc')); // 结果: 'string'
console.log(getType([])); // 结果: 'array'
console.log(getType({})); // 结果: 'object'
console.log(getType(null)); // 结果: 'null'
console.log(getType(() => {})); // 结果: 'function'
console.log(getType(new Date())); // 结果: 'date'

5. 数组扁平化(多维转一维)

适用场景:将多维数组转为一维数组,适用于数据处理、数组遍历等场景,简化数据操作。

// 案例1:二维数组扁平化(核心单行代码)
const arr1 = [1, [2, 3], [4, [5, 6]]];
const flatArr1 = arr1.flat(1); // 参数1表示扁平化1层
// 结果: [1, 2, 3, 4, [5, 6]]

// 案例2:多维数组扁平化(不限层级)
const flatArr2 = arr1.flat(Infinity); // Infinity表示扁平化所有层级
// 结果: [1, 2, 3, 4, 5, 6]

// 补充案例(手动实现扁平化,不使用flat方法)
const flatArr3 = arr1.reduce((acc, item) => acc.concat(Array.isArray(item) ? flatArr3(item) : item), []);
// 结果: [1, 2, 3, 4, 5, 6]

6. 数组排序(数字/字符串通用)

适用场景:对数组进行升序/降序排序,适用于数据展示、排名统计等场景,解决sort默认字符串排序的问题。

// 案例1:数字数组升序排序
const nums = [3, 1, 4, 1, 5, 9, 2, 6];
const sortedNumsAsc = [...nums].sort((a, b) => a - b); // 展开数组避免修改原数组
// 结果: [1, 1, 2, 3, 4, 5, 6, 9]

// 案例2:数字数组降序排序
const sortedNumsDesc = [...nums].sort((a, b) => b - a);
// 结果: [9, 6, 5, 4, 3, 2, 1, 1]

// 案例3:字符串数组排序(按字母顺序)
const strs = ['banana', 'apple', 'cherry', 'date'];
const sortedStrs = [...strs].sort();
// 结果: ['apple', 'banana', 'cherry', 'date']

7. 日期格式化(指定格式)

适用场景:将Date对象转换为指定格式的日期字符串(如YYYY-MM-DD、HH:MM:SS),适用于时间展示、日志记录等。

// 核心单行函数(格式化YYYY-MM-DD HH:MM:SS)
const formatDate = (date = new Date()) => {
  const year = date.getFullYear();
  const month = String(date.getMonth() + 1).padStart(2, '0'); // 月份从0开始,补0
  const day = String(date.getDate()).padStart(2, '0');
  const hour = String(date.getHours()).padStart(2, '0');
  const minute = String(date.getMinutes()).padStart(2, '0');
  const second = String(date.getSeconds()).padStart(2, '0');
  return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
};
// 调用函数
console.log(formatDate()); // 结果: 2026-04-10 14:30:00(当前时间)
console.log(formatDate(new Date(2026, 3, 10))); // 结果: 2026-04-10 00:00:00

8. 字符串数组与数字数组互换

适用场景:快速转换数组元素类型,适用于数据格式转换(如接口返回字符串数组,需转为数字进行计算)。

// 案例1:数字数组转为字符串数组
var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
const strArr = arr.map(String);
// 结果: ['1', '2', '3', '4', '5', '6', '7', '8', '9']

// 案例2:字符串数组转为数字数组
var a = ['1', '2', '3', '4', '5', '6', '7', '8', '9'];
const numArr = a.map(Number);
// 结果: [1, 2, 3, 4, 5, 6, 7, 8, 9]

// 补充案例(处理异常值)
var b = ['1', '2', 'abc', '4'];
const numArr2 = b.map(Number);
// 结果: [1, 2, NaN, 4](无法转换的字符串会转为NaN)

9. 对象数组匹配更新指定元素

适用场景:根据条件更新对象数组中的指定元素,适用于待办状态修改、数据编辑等前端高频场景。

const todos = [
  { id: '001', name: '吃饭', done: true },
  { id: '002', name: '睡觉', done: true }
];
const id = '002';
const done = false;
const newTodos = todos.map((todoObj) => {
  // 匹配id,更新done状态,其余元素不变
  if (todoObj.id === id) return { ...todoObj, done };
  else return todoObj;
});
// 结果: newTodos = [
//   { id: '001', name: '吃饭', done: true },
//   { id: '002', name: '睡觉', done: false }
// ]

10. 对象数组删除指定元素

适用场景:过滤删除对象数组中指定条件的元素,适用于待办删除、无效数据清理等场景。

const todos = [
  { id: '001', name: '吃饭', done: true },
  { id: '002', name: '睡觉', done: true }
];
const id = '002';
// 核心代码:过滤掉id为002的元素
const newTodos = todos.filter((todoObj) => todoObj.id !== id);
// 结果: newTodos = [
//   { id: '001', name: '吃饭', done: true }
// ]

11. 对象数组查找指定元素

适用场景:根据key快速查找对象数组中符合条件的元素,适用于详情页数据获取、数据查询等场景。

const items = [
  { id: 1, name: 'Item 1' },
  { id: 2, name: 'Item 2' },
  { id: 3, name: 'Item 3' }
];
const key = 2; // 要查找的id
// 核心代码:找到id为2的对象
const item = items.find(item => item.id === key);
console.log(item); // 结果: { id: 2, name: 'Item 2' }

12. 对象数组查找指定元素索引

适用场景:快速获取符合条件的元素在数组中的索引,适用于修改、删除指定位置的元素。

const contents = [
  { language: 'zh_CN', alias: "", description: "" },
  { language: 'td_CN', alias: "", description: "" },
  { language: 'en_US', alias: "", description: "" },
  { language: 'ja_JP', alias: "", description: "" }
];
// 核心代码:查找language为zh_CN的元素索引
const index = contents.findIndex(item => item.language === 'zh_CN');
console.log(index); // 结果: 0(数组索引从0开始)

13. 对象数组过滤非空属性元素

适用场景:筛选出对象数组中指定属性不为空的元素,适用于有效数据筛选、表单数据校验等场景。

const contents = [
  { language: 'zh_CN', alias: "1", description: "" },
  { language: 'td_CN', alias: "", description: "" },
  { language: 'en_US', alias: "", description: "" },
  { language: 'ja_JP', alias: "", description: "" }
];
// 核心代码:过滤出alias值不为空的元素
const filteredContents = contents.filter(item => item.alias !== "");
console.log(filteredContents);
// 结果: [{ "language": "zh_CN", "alias": "1", "description": "" }]

14. 对象数组求交集(根据属性匹配)

适用场景:获取两个对象数组中属性匹配的元素,适用于数据对比、重复数据筛选等场景。

let todos1 = [
  { id: "001", name: "吃饭", done: true },
  { id: "002", name: "睡觉", done: true },
];
let todos2 = [
  { id: "001", name: "吃饭", done: true },
  { id: "002", name: "睡觉", done: true },
  { id: "003", name: "学习", done: true },
];
// 核心代码:找出todos2中与todos1 id匹配的元素
todos2 = todos2.filter((item1) =>
  todos1.some((item2) => item2.id === item1.id)
);
console.log(todos2);
// 结果: [
//   { id: "001", name: "吃饭", done: true },
//   { id: "002", name: "睡觉", done: true }
// ]

15. 数组头部添加元素(不修改原数组)

const todos = [
  { id: '001', name: '吃饭', done: true },
  { id: '002', name: '睡觉', done: true }
];
const todoObj = { id: '003', name: '敲码', done: true };
const newTodos = [todoObj, ...todos]; // 核心代码
// 结果: newTodos = [
//   { id: '003', name: '敲码', done: true },
//   { id: '001', name: '吃饭', done: true },
//   { id: '002', name: '睡觉', done: true }
// ]

16. 删除数组指定下标的元素(2种常用方式)

适用场景:明确知道数组中要删除元素的下标,需删除指定位置元素(如删除列表指定索引项、清理数组特定位置无效数据),前端开发高频场景。

// 方式1:splice方法(修改原数组,简洁高效,最常用)
const arr1 = [10, 20, 30, 40, 50];
const index1 = 2; // 要删除的下标(删除元素30)
arr1.splice(index1, 1); // 核心代码:参数1=下标,参数2=删除个数
// 结果: arr1 = [10, 20, 40, 50](原数组被修改)

// 方式2:slice方法(不修改原数组,生成新数组,推荐需保留原数组场景)
const arr2 = [10, 20, 30, 40, 50];
const index2 = 2;
const newArr2 = arr2.slice(0, index2).concat(arr2.slice(index2 + 1)); // 核心代码
// 结果: newArr2 = [10, 20, 40, 50],arr2仍为[10, 20, 30, 40, 50](原数组不变)

// 补充:边界处理(下标越界时,两种方式均不报错,splice无操作,slice返回原数组)
const arr3 = [10, 20];
const index3 = 5; // 下标越界
arr3.splice(index3, 1); // 无操作,arr3仍为[10, 20]
const newArr3 = arr3.slice(0, index3).concat(arr3.slice(index3 + 1)); // newArr3 = [10, 20]

// 补充案例(对象数组删除指定下标元素)
const objArr = [
  { id: 1, name: '苹果' },
  { id: 2, name: '香蕉' },
  { id: 3, name: '橙子' }
];
const objIndex = 1; // 删除下标为1的“香蕉”
// 方式1(修改原数组)
objArr.splice(objIndex, 1);
// 结果: objArr = [{ id: 1, name: '苹果' }, { id: 3, name: '橙子' }]
// 方式2(不修改原数组)
const newObjArr = objArr.slice(0, objIndex).concat(objArr.slice(objIndex + 1));
// 结果: newObjArr = [{ id: 1, name: '苹果' }, { id: 3, name: '橙子' }]

总结:两种方式各有适用场景——splice方法修改原数组,代码简洁,适合无需保留原数组的场景;slice方法不修改原数组,避免污染原始数据,适合需保留原数组(如数据备份、回滚)的场景。注意:下标从0开始,需做好边界判断,避免下标越界导致无效操作。

17. 对象数组多层数据,只保留两层结构

适用场景:处理多层嵌套的对象数组(如接口返回的复杂数据),需简化结构、只保留两层数据(顶层对象 + 一层子对象/子数组),适用于数据展示、表格渲染等无需深层数据的场景,避免冗余数据影响性能。

// 核心函数:递归/遍历处理,只保留两层数据(顶层 + 一层子级)
// 思路:遍历顶层对象数组,仅保留顶层属性和第一层子级,删除子级中的嵌套数据
const keepTwoLayers = (arr) => {
  // 遍历顶层数组,处理每个顶层对象
  return arr.map(item => {
    // 复制顶层对象(避免修改原数据)
    const newItem = {...item};
    // 遍历顶层对象的每个属性,判断是否为对象/数组(即子级)
    for (let key in newItem) {
      const value = newItem[key];
      // 若子级是对象(非null)或数组,仅保留其自身属性,删除嵌套层级
      if (typeof value === 'object' && value !== null) {
        // 数组:保留数组元素,但元素若为对象,仅保留其自身属性(不嵌套)
        if (Array.isArray(value)) {
          newItem[key] = value.map(subItem => {
            return typeof subItem === 'object' && subItem !== null ? {...subItem} : subItem;
          });
        } else {
          // 普通对象:仅保留自身属性,删除嵌套属性
          newItem[key] = {...value};
        }
      }
    }
    return newItem;
  });
};

// 示例:多层嵌套对象数组(模拟接口返回的复杂数据)
const complexArr = [
  {
    id: 1,
    name: '商品分类1',
    info: {
      desc: '电子产品',
      detail: { // 三层嵌套,需删除
        createTime: '2026-01-01',
        updateTime: '2026-04-10'
      },
      tags: [
        { name: '热门', type: { id: 1, name: '推荐' } }, // 三层嵌套,需删除
        { name: '新品', type: { id: 2, name: '新品' } }
      ]
    }
  },
  {
    id: 2,
    name: '商品分类2',
    info: {
      desc: '生活用品',
      detail: { // 三层嵌套,需删除
        createTime: '2026-02-01',
        updateTime: '2026-04-05'
      },
      tags: [
        { name: '热销', type: { id: 3, name: '热销' } }
      ]
    }
  }
];

// 调用函数,只保留两层结构
const twoLayerArr = keepTwoLayers(complexArr);
console.log(twoLayerArr);
// 结果:
// [
//   {
//     id: 1,
//     name: '商品分类1',
//     info: { desc: '电子产品', detail: {}, tags: [ { name: '热门' }, { name: '新品' } ] }
//   },
//   {
//     id: 2,
//     name: '商品分类2',
//     info: { desc: '生活用品', detail: {}, tags: [ { name: '热销' } ] }
//   }
// ]

总结:keepTwoLayers函数通过map遍历顶层对象数组,复制每个顶层对象,再遍历其属性,对对象/数组类型的子级进行处理——数组元素若为对象则仅保留自身属性,普通对象仅保留自身属性,删除所有三层及以上的嵌套数据。该方法可灵活处理各类多层嵌套对象数组,简化数据结构,避免冗余嵌套影响前端渲染性能,适用于表格展示、列表渲染等无需深层数据的场景。

18. 批量修改对象数组的指定属性(批量更新)

适用场景:批量修改对象数组中所有元素的指定属性,或根据条件批量更新属性值,适用于批量操作(如批量修改状态、批量设置默认值),前端开发高频场景。

// 案例1:批量修改所有元素的指定属性(统一设置默认值)
const products = [
  { id: 1, name: '手机', stock: 100, isSale: false },
  { id: 2, name: '电脑', stock: 50, isSale: false },
  { id: 3, name: '平板', stock: 80, isSale: false }
];
// 核心代码:批量将isSale设为true,stock统一减10
const updatedProducts1 = products.map(item => ({
  ...item,
  isSale: true,
  stock: item.stock - 10
}));
// 结果:所有商品isSale为true,stock均减少10

// 案例2:根据条件批量修改属性(满足条件的元素才更新)
// 批量将stock>60的商品isSale设为true,其余不变
const updatedProducts2 = products.map(item => {
  if (item.stock > 60) {
    return {...item, isSale: true};
  }
  return item; // 不满足条件则返回原对象
});
// 结果:id为1、3的商品isSale为true,id为2的商品保持不变

// 补充:批量修改多个不同属性(按需设置)
const updatedProducts3 = products.map(item => ({
  ...item,
  price: item.id * 1000, // 新增属性并赋值
  stock: item.stock > 60 ? item.stock - 10 : item.stock, // 条件赋值
  isSale: item.stock > 60 // 条件赋值(布尔值)
}));
console.log(updatedProducts3);
// 结果:所有商品新增price属性,stock按需减少,isSale按条件设置

总结:利用map方法遍历对象数组,通过对象扩展运算符(...)保留原对象属性,同时修改指定属性值,可实现统一批量修改或条件批量修改。该方法不修改原数组,生成新数组,避免污染原始数据,适用于批量操作场景,代码简洁且可复用,是前端批量处理对象数组的常用方式。

三、十大排序算法(前端实战版)

排序算法是前端数据处理的核心基础,以下整理十大常用排序算法,先通过表格对比各算法关键信息,再详细说明核心原理、JS实现代码(简洁可复制)、适用场景及优缺点,贴合前端开发实际需求,避免复杂冗余,重点适配数组排序场景。

排序算法 核心特点 时间复杂度 空间复杂度 稳定性 前端适用场景
冒泡排序 相邻元素对比,逐步冒泡至末尾 O(n²) O(1) 稳定 少量数据,简单场景
选择排序 每次选最小/大元素,与首位交换 O(n²) O(1) 不稳定 少量数据,对稳定性无要求
插入排序 分已排序/未排序,逐个插入合适位置 O(n²)(接近有序时O(n)) O(1) 稳定 少量数据、数据接近有序(如表单排序)
希尔排序 插入排序优化,按步长分组排序 O(nlogn) O(1) 不稳定 中量数据
快速排序 分治法,选基准值分组递归排序 O(nlogn) O(logn) 不稳定 大量数据,前端最常用
归并排序 分治法,拆分后合并有序子数组 O(nlogn) O(n) 稳定 大量数据,对稳定性有要求
堆排序 利用大顶堆特性,逐步取出堆顶元素 O(nlogn) O(1) 不稳定 大量数据,对空间要求严格
计数排序 非比较排序,统计元素次数后重构数组 O(n + k)(k为元素范围) O(k) 稳定 元素为整数、范围较小(如分数排序)
桶排序 非比较排序,分桶排序后合并 O(n + k) O(n + k) 稳定 元素分布均匀、大量数据(如数据统计)
基数排序 非比较排序,按位数依次排序 O(n * k)(k为最大数位数) O(n + k) 稳定 整数、字符串,大量数据

1. 冒泡排序(Bubble Sort)

核心原理:重复遍历数组,每次比较相邻两个元素,将较大元素“冒泡”到数组末尾,逐步完成排序。

// 优化版:添加标志位,无交换时直接退出(提升效率)
const bubbleSort = (arr) => {
  const newArr = [...arr]; // 不修改原数组
  const len = newArr.length;
  for (let i = 0; i < len - 1; i++) {
    let hasSwap = false; // 标志位:是否发生交换
    for (let j = 0; j < len - 1 - i; j++) {
      if (newArr[j] > newArr[j + 1]) {
        [newArr[j], newArr[j + 1]] = [newArr[j + 1], newArr[j]]; // 交换元素
        hasSwap = true;
      }
    }
    if (!hasSwap) break; // 无交换,说明已排序完成
  }
  return newArr;
};
// 示例
const arr1 = [3, 1, 4, 1, 5, 9];
console.log(bubbleSort(arr1)); // 结果:[1, 1, 3, 4, 5, 9]

优缺点:简单易理解,空间复杂度低(O(1));时间复杂度O(n²),数据量大时效率极低,适用于少量数据排序。

2. 选择排序(Selection Sort)

核心原理:每次遍历未排序部分,找到最小(或最大)元素,与未排序部分的第一个元素交换,逐步缩小未排序范围。

const selectionSort = (arr) => {
  const newArr = [...arr];
  const len = newArr.length;
  for (let i = 0; i < len - 1; i++) {
    let minIndex = i; // 记录最小元素下标
    // 找到未排序部分的最小元素
    for (let j = i + 1; j < len; j++) {
      if (newArr[j] < newArr[minIndex]) {
        minIndex = j;
      }
    }
    // 交换最小元素与未排序部分第一个元素
    [newArr[i], newArr[minIndex]] = [newArr[minIndex], newArr[i]];
  }
  return newArr;
};
// 示例
const arr2 = [7, 2, 5, 0, 3];
console.log(selectionSort(arr2)); // 结果:[0, 2, 3, 5, 7]

优缺点:实现简单,空间复杂度O(1);时间复杂度O(n²),不稳定(相同元素可能改变相对位置),适用于数据量小、对稳定性无要求的场景。

3. 插入排序(Insertion Sort)

核心原理:将数组分为“已排序”和“未排序”两部分,每次从无排序部分取一个元素,插入到已排序部分的合适位置。

const insertionSort = (arr) => {
  const newArr = [...arr];
  const len = newArr.length;
  for (let i = 1; i < len; i++) {
    const current = newArr[i]; // 未排序部分的当前元素
    let j = i - 1; // 已排序部分的最后一个下标
    // 找到插入位置
    while (j >= 0 && newArr[j] > current) {
      newArr[j + 1] = newArr[j]; // 元素后移
      j--;
    }
    newArr[j + 1] = current; // 插入当前元素
  }
  return newArr;
};
// 示例
const arr3 = [6, 3, 8, 2, 9];
console.log(insertionSort(arr3)); // 结果:[2, 3, 6, 8, 9]

优缺点:稳定排序,数据接近有序时效率极高(时间复杂度接近O(n));时间复杂度O(n²),适用于少量数据、数据接近有序的场景(如表单排序)。

4. 希尔排序(Shell Sort)

核心原理:插入排序的优化版,将数组按“步长”分组,对每组进行插入排序,逐步缩小步长,最终步长为1时完成排序。

const shellSort = (arr) => {
  const newArr = [...arr];
  const len = newArr.length;
  // 步长初始为数组长度的一半,逐步缩小为1
  for (let gap = Math.floor(len / 2); gap > 0; gap = Math.floor(gap / 2)) {
    // 对每组进行插入排序
    for (let i = gap; i < len; i++) {
      const current = newArr[i];
      let j = i - gap;
      while (j >= 0 && newArr[j] > current) {
        newArr[j + gap] = newArr[j];
        j -= gap;
      }
      newArr[j + gap] = current;
    }
  }
  return newArr;
};
// 示例
const arr4 = [10, 5, 12, 3, 7, 1];
console.log(shellSort(arr4)); // 结果:[1, 3, 5, 7, 10, 12]

优缺点:效率高于插入/冒泡/选择排序,时间复杂度O(nlogn);不稳定,适用于中量数据排序。

5. 快速排序(Quick Sort)

核心原理:分治法,选择一个“基准值”,将数组分为“小于基准”“等于基准”“大于基准”三部分,递归对左右两部分排序,效率极高。

// 简洁版:递归实现,基准值选数组中间元素
const quickSort = (arr) => {
  if (arr.length <= 1) return arr; // 递归终止条件
  const newArr = [...arr];
  const midIndex = Math.floor(newArr.length / 2);
  const pivot = newArr.splice(midIndex, 1)[0]; // 基准值(删除并获取)
  const left = []; // 小于基准的元素
  const right = []; // 大于基准的元素
  // 分组
  for (let item of newArr) {
    item < pivot ? left.push(item) : right.push(item);
  }
  // 递归排序左右两部分,合并结果
  return [...quickSort(left), pivot, ...quickSort(right)];
};
// 示例
const arr5 = [8, 3, 1, 7, 0, 10, 2];
console.log(quickSort(arr5)); // 结果:[0, 1, 2, 3, 7, 8, 10]

优缺点:效率极高,时间复杂度O(nlogn);不稳定,空间复杂度O(logn),适用于大量数据排序(前端最常用的排序算法)。

6. 归并排序(Merge Sort)

核心原理:分治法,将数组递归拆分为两个子数组,直到每个子数组只有一个元素,再逐步合并两个有序子数组,最终得到有序数组。

// 合并两个有序数组
const merge = (left, right) => {
  const result = [];
  let i = 0, j = 0;
  // 对比两个数组,按顺序合并
  while (i < left.length && j < right.length) {
    left[i] < right[j] ? result.push(left[i++]) : result.push(right[j++]);
  }
  // 合并剩余元素
  return [...result, ...left.slice(i), ...right.slice(j)];
};

// 归并排序主函数
const mergeSort = (arr) => {
  if (arr.length <= 1) return arr; // 递归终止条件
  const mid = Math.floor(arr.length / 2);
  const left = arr.slice(0, mid); // 左子数组
  const right = arr.slice(mid); // 右子数组
  // 递归拆分 + 合并
  return merge(mergeSort(left), mergeSort(right));
};
// 示例
const arr6 = [5, 2, 9, 1, 5, 6];
console.log(mergeSort(arr6)); // 结果:[1, 2, 5, 5, 6, 9]

优缺点:稳定排序,时间复杂度O(nlogn);空间复杂度O(n),适用于对稳定性有要求、大量数据的排序场景。

7. 堆排序(Heap Sort)

核心原理:利用堆(大顶堆/小顶堆)的特性,将数组构建为大顶堆(最大值在堆顶),每次取出堆顶元素,再调整堆结构,重复直至排序完成。

// 调整堆结构(大顶堆)
const adjustHeap = (arr, parentIndex, len) => {
  const temp = arr[parentIndex]; // 父节点
  let childIndex = 2 * parentIndex + 1; // 左子节点下标
  while (childIndex < len) {
    // 找到左右子节点中较大的一个
    if (childIndex + 1 < len && arr[childIndex + 1] > arr[childIndex]) {
      childIndex++;
    }
    // 父节点大于子节点,无需调整
    if (temp >= arr[childIndex]) break;
    // 子节点上移
    arr[parentIndex] = arr[childIndex];
    parentIndex = childIndex;
    childIndex = 2 * parentIndex + 1;
  }
  arr[parentIndex] = temp; // 插入父节点到正确位置
};

// 堆排序主函数
const heapSort = (arr) => {
  const newArr = [...arr];
  const len = newArr.length;
  // 1. 构建大顶堆(从最后一个非叶子节点开始调整)
  for (let i = Math.floor(len / 2) - 1; i >= 0; i--) {
    adjustHeap(newArr, i, len);
  }
  // 2. 逐步取出堆顶元素,调整堆结构
  for (let i = len - 1; i > 0; i--) {
    [newArr[0], newArr[i]] = [newArr[i], newArr[0]]; // 堆顶与末尾元素交换
    adjustHeap(newArr, 0, i); // 调整剩余堆结构
  }
  return newArr;
};
// 示例
const arr7 = [3, 9, 2, 10, 4, 7];
console.log(heapSort(arr7)); // 结果:[2, 3, 4, 7, 9, 10]

优缺点:效率高,时间复杂度O(nlogn);不稳定,空间复杂度O(1),适用于大量数据、对空间要求严格的场景。

8. 计数排序(Counting Sort)

核心原理:非比较排序,统计数组中每个元素出现的次数,根据元素大小顺序,依次输出对应次数的元素,适用于元素范围较小的整数数组。

const countingSort = (arr) => {
  if (arr.length <= 1) return arr;
  const newArr = [...arr];
  const max = Math.max(...newArr); // 找到数组最大值
  const min = Math.min(...newArr); // 找到数组最小值
  const countArr = new Array(max - min + 1).fill(0); // 计数数组

  // 统计每个元素出现的次数
  for (let item of newArr) {
    countArr[item - min]++;
  }

  // 构建排序后的数组
  let index = 0;
  for (let i = 0; i < countArr.length; i++) {
    while (countArr[i] > 0) {
      newArr[index++] = i + min;
      countArr[i]--;
    }
  }
  return newArr;
};
// 示例(元素范围较小的整数数组)
const arr8 = [2, 0, 2, 1, 1, 0];
console.log(countingSort(arr8)); // 结果:[0, 0, 1, 1, 2, 2]

优缺点:稳定排序,时间复杂度O(n + k)(k为元素范围);空间复杂度O(k),仅适用于元素为整数、范围较小的场景(如考试分数排序)。

9. 桶排序(Bucket Sort)

核心原理:非比较排序,将数组元素按范围分到不同的“桶”中,对每个桶内的元素进行排序(可使用其他排序算法),最后合并所有桶的元素。

// 桶排序主函数,默认分5个桶
const bucketSort = (arr, bucketCount = 5) => {
  if (arr.length <= 1) return arr;
  const newArr = [...arr];
  const max = Math.max(...newArr);
  const min = Math.min(...newArr);
  const bucketSize = Math.ceil((max - min + 1) / bucketCount); // 每个桶的范围大小
  const buckets = new Array(bucketCount).fill(0).map(() => []); // 初始化桶

  // 将元素分到对应桶中
  for (let item of newArr) {
    const bucketIndex = Math.floor((item - min) / bucketSize);
    buckets[bucketIndex].push(item);
  }

  // 对每个桶排序,合并结果(这里使用插入排序,也可替换为快速排序)
  return buckets.reduce((result, bucket) => {
    return [...result, ...insertionSort(bucket)]; // 复用前面的插入排序
  }, []);
};
// 示例
const arr9 = [4, 2, 8, 1, 5, 7, 3, 6];
console.log(bucketSort(arr9)); // 结果:[1, 2, 3, 4, 5, 6, 7, 8]

优缺点:稳定排序,时间复杂度O(n + k);空间复杂度O(n + k),适用于元素分布均匀、大量数据的排序场景(如数据统计)。

10. 基数排序(Radix Sort)

核心原理:非比较排序,按元素的“位数”(个位、十位、百位...)依次排序,从低位到高位,每次排序后元素按当前位数有序,最终得到完整有序数组。

// 获取数字的某一位(个位=0,十位=1,百位=2...)
const getDigit = (num, digit) => {
  return Math.floor(Math.abs(num) / Math.pow(10, digit)) % 10;
};

// 基数排序主函数
const radixSort = (arr) => {
  if (arr.length <= 1) return arr;
  const newArr = [...arr];
  // 找到数组中最大数的位数
  const maxDigit = Math.max(...newArr).toString().length;

  // 按每一位排序,从个位到最高位
  for (let digit = 0; digit< maxDigit; digit++) {
    const buckets = new Array(10).fill(0).map(() => []); // 0-9共10个桶
    // 按当前位数将元素分到对应桶中
    for (let item of newArr) {
      const bucketIndex = getDigit(item, digit);
      buckets[bucketIndex].push(item);
    }
    // 合并桶,更新数组
    newArr.splice(0, newArr.length, ...buckets.flat());
  }
  return newArr;
};
// 示例(正整数数组)
const arr10 = [123, 45, 6, 789, 10, 23];
console.log(radixSort(arr10)); // 结果:[6, 10, 23, 45, 123, 789]

优缺点:稳定排序,时间复杂度O(n * k)(k为最大数的位数);空间复杂度O(n + k),适用于整数、字符串等可按“位”排序的大量数据场景。

排序算法总结(前端选型参考)

  1. 少量数据(n < 100):优先用插入排序、冒泡排序(简单易实现);

  2. 中大量数据(n > 100):优先用快速排序(效率最高)、归并排序(稳定);

  3. 元素范围小的整数数组:用计数排序、桶排序(效率高于比较排序);

  4. 对稳定性有要求:用归并排序、插入排序、计数排序、桶排序、基数排序;

  5. 对空间有要求:用堆排序、快速排序、冒泡排序、选择排序、插入排序、希尔排序。

  6. 所有代码均为前端实战高频场景,可直接复制到项目中使用,部分代码需根据实际需求(如DOM选择器、属性名)微调;

  7. 单行代码优先简化实现,兼顾简洁性和实用性,复杂场景补充完整示例,适配不同开发需求;

  8. 注意浏览器兼容性:部分API(如Clipboard API、可选链运算符??、扩展运算符)不支持IE浏览器,若需兼容IE,需额外做兼容处理;

  9. 对象/数组操作均优先采用不修改原数据的方式(如扩展运算符、map、filter),避免意外污染原始数据,提升代码可维护性。

Sass与Less全面对比(含语法+场景+选型)

2026年4月10日 10:31

Sass(Syntactically Awesome Style Sheets)和 Less(Leaner Style Sheets)是目前最主流的两款CSS预处理器,二者核心目标一致——扩展CSS的功能,解决原生CSS无变量、无嵌套、无复用性等痛点,让样式开发更高效、代码更易维护。但两者在起源、语法细节、功能特性、生态支持等方面存在显著差异,选择时需结合项目规模、团队习惯和需求场景综合判断。

一、核心差异总览(表格清晰对比)

对比维度 Sass Less
起源与底层实现 2007年诞生,最初基于Ruby实现,目前官方推荐使用Dart Sass(更易维护、性能更优),Node Sass已停更废弃。 2009年诞生,基于JavaScript实现,依赖Node.js环境编译,学习门槛相对较低,可在浏览器端直接解析(不推荐生产环境)。
语法风格 支持两种语法:① SCSS(.scss后缀):兼容原生CSS,使用大括号和分号,目前最常用;② 缩进式(.sass后缀):无大括号和分号,靠缩进区分代码块,格式要求严格。 仅支持一种语法(.less后缀),完全兼容原生CSS,必须使用大括号和分号,写法与原生CSS高度一致,上手更轻松。
变量声明 使用$符号定义变量,支持!default设置默认值(仅在变量未定义时生效),作用域严格,局部变量不影响全局,重定义未加!default的变量会报错。 使用@符号定义变量,采用“懒求值”机制,同名变量后声明会覆盖前声明(无论是否在嵌套块内),局部变量可直接覆盖全局变量,无报错提示。
嵌套与父选择器 支持选择器嵌套,父选择器&解析严格,要求符号与选择器间无多余空格(如&:hover合法),带空格会编译报错,避免隐性错误。 支持选择器嵌套,父选择器&解析宽松,允许省略空格或多余空格(如&.disabled& .disabled均合法),易出现“编译成功但结果异常”的情况。
混合(Mixins) @mixin定义混合宏,@include调用,支持参数、默认值和可变参数,功能灵活,可配合逻辑控制实现复杂复用逻辑。 用类选择器(可加括号,加括号不输出到CSS)定义混合,直接通过类名调用,支持参数和默认值,功能相对基础,无复杂逻辑支持。
继承 @extend实现继承,支持“占位符选择器”(%开头),仅用于继承,不生成冗余CSS,复用效率更高。 @extend实现继承,但不支持占位符选择器,被继承的类会被编译到最终CSS中,易产生冗余代码。
逻辑控制 支持完整的逻辑控制:@if/@else条件判断、@for/@each/@while循环,还可通过@function自定义函数,适合复杂动态样式生成。 逻辑控制能力较弱,仅支持简单的when条件判断和递归循环(需手动终止),无原生自定义函数功能,复杂逻辑需通过混合模拟。
模块化机制 采用现代化模块系统,通过@use(直接使用模块)和@forward(转发模块成员)实现模块化,自动单例加载,支持命名空间和私有成员,彻底避免命名冲突和重复加载。 依赖@import实现模块化,无命名空间和单例加载机制,多次导入同一文件会重复编译,易造成全局污染和代码冗余,无原生私有成员支持。
内置函数 内置函数丰富,涵盖颜色处理、字符串操作、数学计算等,支持颜色对象运算,类型安全,边界值处理更严谨(如纯黑颜色调整),还可通过内置模块(如sass:math、sass:color)扩展功能。 内置函数相对基础,主要支持简单的颜色调整(如lighten、darken),函数参数和返回值类型不统一,颜色操作仅支持字符串拼接,无法参与复杂运算,易出现解析异常。
生态与框架支持 生态更成熟,社区活跃,插件丰富,主流框架(Bootstrap 4+、Angular、Vue CLI)均优先支持,与Webpack、Vite等构建工具集成流畅,Dart Sass编译速度快,适合大型项目。 生态相对小众,早期用于Bootstrap 3,目前在部分企业级老项目中仍有应用,与构建工具集成时存在配置限制(如Vite不支持javascriptEnabled配置),适合小型项目或老项目维护。
学习门槛 中等,SCSS语法兼容CSS,基础用法易上手,但高级特性(逻辑控制、模块化)需额外学习,缩进式语法对格式要求较高。 低,语法完全贴近原生CSS,无额外格式要求,基础功能简单易懂,适合刚接触预处理器的开发者快速上手。

二、核心语法对比(附代码示例)

以下针对最常用的核心功能,对比两者的语法差异,所有示例均采用各自最主流的语法(Sass用SCSS,Less用默认语法)。

1. 变量声明与使用

// 定义全局变量,设置默认值(未定义时生效)
$primary-color: #2563eb !default;
$font-size: 16px;

// 局部变量(仅在.box内生效,不影响全局)
.box {
  $local-color: #6c757d;
  color: $primary-color; // 全局变量:#2563eb
  background: $local-color; // 局部变量:#6c757d
  font-size: $font-size; // 全局变量:16px
}

// 重定义带!default的变量(合法,覆盖默认值)
$primary-color: #1d4ed8;
.text {
  color: $primary-color; // 覆盖后:#1d4ed8
}
// 定义全局变量
@primary-color: #2563eb;
@font-size: 16px;

// 局部变量(覆盖全局变量)
.box {
  @primary-color: #6c757d;
  color: @primary-color; // 局部变量:#6c757d(覆盖全局)
  font-size: @font-size; // 全局变量:16px
}

// 重定义变量(直接覆盖,无报错)
@primary-color: #1d4ed8;
.text {
  color: @primary-color; // 覆盖后:#1d4ed8
}

2. 选择器嵌套与父选择器

.nav {
  width: 100%;
  height: 60px;
  
  // 子选择器嵌套
  > li {
    float: left;
    margin: 0 10px;
    
    // 父选择器&(严格解析,无空格)
    &:hover {
      color: $primary-color;
    }
    &.active {
      font-weight: bold;
    }
  }
}
// 编译后无冗余,&解析准确
.nav {
  width: 100%;
  height: 60px;
  
  // 子选择器嵌套(与Sass一致)
  > li {
    float: left;
    margin: 0 10px;
    
    // 父选择器&(宽松解析,允许空格)
    & :hover { // 多余空格,编译为.nav li :hover(非预期)
      color: @primary-color;
    }
    &.active {
      font-weight: bold;
    }
  }
}
// 易因空格问题导致样式异常,需格外注意

3. 混合(Mixins)用法

// 定义带参数、默认值的混合
@mixin flex-center($direction: row) {
  display: flex;
  flex-direction: $direction;
  justify-content: center;
  align-items: center;
}

// 调用混合(传递参数)
.box {
  @include flex-center(column);
  width: 300px;
  height: 200px;
}

// 调用混合(使用默认值)
.card {
  @include flex-center;
  background: #fff;
}
// 定义带参数、默认值的混合(加括号不输出到CSS)
.flex-center(@direction: row) {
  display: flex;
  flex-direction: @direction;
  justify-content: center;
  align-items: center;
}

// 调用混合(直接使用类名,传递参数)
.box {
  .flex-center(column);
  width: 300px;
  height: 200px;
}

// 调用混合(使用默认值)
.card {
  .flex-center;
  background: #fff;
}

4. 继承用法

// 占位符选择器(仅用于继承,不输出到CSS)
%button-base {
  padding: 8px 16px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

// 继承占位符样式
.primary-btn {
  @extend %button-base;
  background: $primary-color;
  color: #fff;
}

// 继承占位符样式
.success-btn {
  @extend %button-base;
  background: #16a34a;
  color: #fff;
}
// 编译后无%button-base相关样式,无冗余
// 普通类选择器(会被编译到CSS中)
.button-base {
  padding: 8px 16px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

// 继承普通类样式
.primary-btn {
  @extend .button-base;
  background: @primary-color;
  color: #fff;
}

// 继承普通类样式
.success-btn {
  @extend .button-base;
  background: #16a34a;
  color: #fff;
}
// 编译后会保留.button-base样式,产生冗余

5. 逻辑控制用法

// 条件判断
$theme: dark;
.box {
  @if $theme == dark {
    background: #111;
    color: #fff;
  } @else {
    background: #fff;
    color: #333;
  }
}

// 循环(生成col-1到col-4)
@for $i from 1 to 5 {
  .col-#{$i} {
    width: 100% / $i;
    float: left;
  }
}
// 条件判断(通过when)
@theme: dark;
.box when (@theme = dark) {
  background: #111;
  color: #fff;
}
.box when not (@theme = dark) {
  background: #fff;
  color: #333;
}

// 递归循环(需手动终止)
.loop(@n) when (@n > 0) {
  .col-@{n} {
    width: 100% / @n;
    float: left;
  }
  .loop(@n - 1); // 递归调用,直到@n<=0
}
.loop(4); // 生成col-4到col-1

6. 模块化导入用法

// 1. 拆分模块:_variables.scss(局部文件,下划线开头不单独编译)
$primary-color: #2563eb;
$-private-var: 10px; // 私有成员(-/_开头,外部无法访问)

// 2. 主文件导入:main.scss
@use "./variables"; // 默认命名空间:variables,单例加载
@use "./variables" as v; // 自定义命名空间:v(重复导入仅加载一次)

.box {
  color: variables.$primary-color; // 通过命名空间访问
  padding: v.$primary-color;
  // margin: variables.$-private-var; // 报错:私有成员无法访问
}

// 3. 转发模块(供其他文件使用)
@forward "./variables" as var-*; // 转发所有成员,加前缀var-
// 1. 拆分模块:variables.less
@primary-color: #2563eb;
@private-var: 10px; // 无私有成员,外部可直接访问

// 2. 主文件导入:main.less
@import "./variables.less"; // 无命名空间,全局注入
@import "./variables.less"; // 重复导入,重复编译,产生冗余

.box {
  color: @primary-color; // 直接访问,无隔离
  padding: @private-var; // 无私有限制,可访问所有成员
}

三、编译环境与工具集成

1. Sass 编译环境

  • 主流实现:目前推荐使用Dart Sass(npm安装:npm install -g sass),替代已废弃的Node Sass,编译速度快、兼容性好,支持所有新特性。
  • 工具集成:与Webpack(sass-loader)、Vite(内置支持)、VS Code(Live Sass Compiler插件)集成流畅,支持source map调试,additionalData选项可注入全局变量,支持函数回调配置。
  • 编译命令:sass input.scss output.css(实时监听:sass --watch input.scss:output.css)。

2. Less 编译环境

  • 安装方式:基于Node.js,npm安装:npm install -g less,可通过less.js在浏览器端直接解析(仅适合开发调试,生产环境不推荐)。
  • 工具集成:与Webpack(less-loader)、Vite(内置支持)集成,但存在配置限制(如Vite不支持javascriptEnabled: true,无法运行JS表达式),additionalData选项仅支持字符串配置,不支持函数回调。
  • 编译命令:lessc input.less output.css(实时监听需借助第三方工具)。

四、项目选型建议

选型核心:结合项目规模、团队技术栈、功能需求,而非单纯追求“更强大”,优先保证开发效率和可维护性。

1. 优先选择 Sass(SCSS)的场景

  • 中大型项目/团队协作:需要复杂逻辑控制(如动态主题、批量样式生成)、严格的模块化隔离,避免命名冲突,Sass的@use/@forward、私有成员、逻辑控制等特性可大幅提升可维护性和协作效率。
  • 新项目开发:追求长期可维护性,希望适配主流技术生态,Sass的社区支持更完善、框架兼容性更好,后续扩展更便捷,是目前官方和行业推荐的首选方案。
  • 需要丰富的内置函数和高级特性:如复杂颜色处理、自定义函数、灵活的变量配置(!default),适合搭建设计系统或多主题项目,Sass的类型安全和函数链式调用更稳定可靠。
  • 使用主流前端框架:如Bootstrap 4+、Angular、Vue 3,这些框架均优先支持Sass,集成更流畅,减少配置成本。

2. 优先选择 Less 的场景

  • 小型项目/快速原型开发:需求简单,仅需变量、嵌套、基础混合等功能,Less语法贴近原生CSS,上手快、配置简单,可快速完成开发任务。
  • 维护旧项目:项目已基于Less开发,短期内无法迁移,继续使用Less可降低迁移成本,避免影响项目正常运行,Less的兼容性可保证旧代码稳定编译。
  • 团队成员不熟悉预处理器:团队以原生CSS开发为主,Less学习成本低,无需额外学习复杂语法,可快速过渡到预处理器开发模式。
  • 简单的浏览器端调试需求:Less可通过less.js直接在浏览器端解析,无需搭建复杂的编译环境,适合快速调试样式

五、常见问题与避坑指南

实际开发中,无论是Sass还是Less,都容易遇到语法、编译或集成相关的问题,以下梳理高频坑点及解决方案,帮助规避不必要的麻烦。

1. Sass 常见避坑点

  • 坑点1:混淆Node Sass与Dart Sass,导致编译报错。解决方案:彻底卸载Node Sass(npm uninstall node-sass),安装Dart Sass(npm install sass),确保项目依赖中无node-sass,避免版本冲突。
  • 坑点2:@use导入路径错误,提示“找不到模块”。解决方案:导入时省略下划线和文件后缀(如导入_variables.scss,写@use "./variables"),路径以当前文件为基准,避免绝对路径,跨目录导入需正确拼接相对路径(如@use "../utils/variables")。
  • 坑点3:误将SCSS语法用在缩进式Sass文件中,导致编译失败。解决方案:统一项目语法风格,优先使用SCSS(.scss后缀),若使用缩进式Sass(.sass后缀),需严格遵循“无大括号、无分号、靠缩进区分代码块”的规则。
  • 坑点4:重定义未加!default的变量,导致报错。解决方案:全局公共变量建议加!default(方便后续覆盖),局部变量仅在当前模块内使用,避免与全局变量重名,若需重定义全局变量,确保先导入变量文件,再重定义。

2. Less 常见避坑点

  • 坑点1:父选择器&添加多余空格,导致样式解析异常。解决方案:严格控制&与后续选择器的空格(如&:hover而非& :hover),避免编译后生成非预期的选择器(如.nav li :hover)。
  • 坑点2:Vite项目中启用javascriptEnabled: true,导致编译报错。解决方案:Vite内置的Less编译器不支持该配置,若需运行JS表达式,可改用Webpack+less-loader,或避免在Less中写入JS逻辑。
  • 坑点3:多次导入同一文件,导致CSS冗余。解决方案:尽量减少重复导入,可将公共模块(如变量、混合)集中在一个入口文件导入,再引入该入口文件,避免多文件重复导入同一模块。
  • 坑点4:变量覆盖导致样式异常,难以排查。解决方案:规范变量命名(如加模块前缀@btn-primary-color),避免全局变量与局部变量重名,复杂项目可按模块拆分变量文件,减少覆盖风险。

3. 通用避坑点

  • 避免嵌套过深:无论是Sass还是Less,嵌套层级建议不超过3层,否则会编译出冗长的选择器,影响CSS性能,且不利于代码维护。
  • 规范文件命名:局部模块文件(不单独编译的文件)建议以下划线开头(如_variables.scss_mixins.less),区分全局入口文件,避免编译生成多余的CSS文件。
  • 慎用!important:预处理器中尽量避免使用!important,若需提高样式优先级,可通过调整选择器权重(如增加父选择器)实现,否则会导致样式优先级混乱,难以调试。

六、实战对比总结

Sass和Less本质上都是为了解决原生CSS的痛点,提升样式开发效率,但两者的定位和适用场景有明显区分,无需纠结“谁更好”,只需结合自身需求选择即可,核心总结如下:

  • 从功能强大度来看:Sass > Less,Sass的模块化、逻辑控制、内置函数等高级特性,更适合复杂项目和设计系统搭建,能解决更多场景下的开发痛点。
  • 从学习成本来看:Less < Sass,Less语法与原生CSS高度一致,上手门槛极低,适合新手或原生CSS开发者快速过渡,Sass的高级特性需要额外投入时间学习。
  • 从生态和未来趋势来看:Sass更具优势,官方持续更新维护,主流框架和构建工具优先支持,Node Sass的废弃也推动了Dart Sass的普及,而Less生态相对停滞,仅适合维护旧项目或小型项目。
  • 从团队协作来看:Sass更适合团队协作,严格的作用域、命名空间和私有成员机制,能有效避免命名冲突,清晰的依赖关系也便于代码维护和迭代;Less无模块化隔离,大型团队协作易出现问题。

最后补充一句:无论是选择Sass还是Less,核心是“规范使用”——统一语法风格、合理拆分模块、规范变量命名,才能真正发挥预处理器的优势,让样式代码更高效、更易维护。如果是新建项目,优先选择Sass(SCSS),贴合行业主流;如果是维护旧项目或快速开发,Less也是不错的选择。

七、快速选型对照表(便捷参考)

项目/团队情况 推荐选择 核心原因
中大型项目、团队协作 Sass(SCSS) 模块化强、无命名冲突、支持复杂逻辑,可维护性高
小型项目、快速原型开发 Less 上手快、配置简单,满足基础需求,开发效率高
新建项目、追求长期维护 Sass(SCSS) 生态成熟、官方推荐,适配主流框架,扩展便捷
旧项目维护(基于Less) Less 降低迁移成本,保证旧代码稳定编译,无需额外学习
新手开发者、原生CSS过渡 Less 语法贴近原生CSS,学习成本低,快速上手无压力
搭建设计系统、多主题项目 Sass(SCSS) 内置函数丰富、变量配置灵活,支持复杂动态样式生成

SCSS中@use与@import的区别

2026年4月10日 10:16

SCSS(Sassy CSS)中@use@import均用于实现样式模块化,实现代码复用,但二者在作用域、加载机制、命名空间等核心特性上差异显著。其中@import是SCSS早期的导入语法,存在全局污染、重复加载等问题,而@use是Sass 3.8+推出的新版模块化语法,旨在解决@import的缺陷,目前已被官方推荐作为首选导入方式,未来@import将逐步被弃用。

一、核心差异对比(表格清晰呈现)

对比维度 @import(旧版语法) @use(新版推荐)
作用域 全局作用域,导入的变量、混合宏(mixin)、函数会直接注入当前文件的全局作用域,易造成命名冲突和变量污染。 局部作用域,导入的内容被封装在独立模块中,需通过命名空间访问,从根本上避免全局污染和命名冲突。
加载机制 多次导入同一文件时,会重复加载、重复编译,增加编译时间,可能导致输出CSS冗余。 自动实现单例加载,无论导入多少次同一文件,仅加载、编译一次,提升编译效率,避免冗余代码。
命名空间 无命名空间,导入的所有成员(变量、mixin等)可直接访问,无需前缀,易引发命名冲突,需通过冗长命名规避冲突。 默认以导入文件的文件名作为命名空间,也可自定义命名空间;访问成员时需加上命名空间前缀,可通过as *省略前缀(慎用)。
私有成员支持 不支持私有成员,导入文件中所有定义的变量、mixin均可被外部访问,无法实现成员隐藏。 支持私有成员,以-_为前缀的变量、mixin视为私有,仅能在定义文件内部使用,外部无法访问,实现更好的封装性。
依赖关系 依赖关系混乱,无法清晰判断变量、mixin的来源,不利于大型项目维护和团队协作。 依赖关系显式化,通过命名空间可明确知道每个成员的来源,代码可维护性大幅提升,适合大型项目和团队协作。
变量配置 通过重定义变量覆盖默认值(需在@import前定义),但全局变量易被意外修改,配置逻辑不清晰。 支持通过with语句针对性配置模块变量,不影响全局,配置逻辑更严谨、可追溯。
官方支持 已被官方不推荐使用,计划逐步弃用,仅为兼容旧代码保留,部分新特性不支持。 官方推荐首选语法,支持所有新特性,是SCSS模块化开发的标准方案,与@forward配合实现更完善的模块化体系。

上述表格已清晰列出@use@import的所有核心区别,接下来我们重点拆解最影响开发效率和代码质量的两个特性——重复加载和命名空间。

二、重点特性详解(重复加载+命名空间)

2.1 重复加载(性能与冗余核心差异)

重复加载是@import最突出的问题之一,会直接影响样式文件性能和代码冗余度,而@use通过单例加载机制完美解决了这一问题。

  • 当使用 @import 导入模块时,如果在多个文件中多次导入同一个文件,会导致重复加载的问题。
  • 这意味着被导入的文件将在每个使用了 @import 的文件中都被加载一次,导致样式表中包含多份相同的样式,从而影响性能和增加文件大小。

我们通过一个实际示例,直观感受重复加载的问题:

// _variables.scss(被重复导入的模块)
$primary-color: #007bff;
$secondary-color: #6c757d;

// styles1.scss
@import 'variables';
body {
  background-color: $primary-color;
}

// styles2.scss
@import 'variables';
button {
  background-color: $secondary-color;
}

示例解析:我们有两个样式文件 styles1.scss 和 styles2.scss,它们分别使用 @import 导入了同一个 _variables.scss 文件。由于 styles1.scss 和 styles2.scss 都导入了 _variables.scss,在编译这两个样式文件时,_variables.scss 将被加载两次。

编译后的结果如下所示:

// 编译后的 styles1.cssbody {
  background-color: #007bff;
}

// 编译后的 styles2.cssbutton {
  background-color: #6c757d;
}

可以看到,虽然编译后的CSS中未直接显示重复的变量定义,但 _variables.scss 中的内容在编译过程中被加载了两次,不仅增加了编译时间,若模块中包含实际样式(而非仅变量),会导致CSS文件中出现多份相同样式,增加文件大小、影响页面加载性能,还可能引发潜在的样式冲突。

而使用 @use 导入方式可以避免重复加载问题,因为它会确保每个模块只加载一次,即使在多个文件中导入。这样可以优化编译性能,并保持样式表的精简和一致性。

2.2 命名空间(避免冲突的核心机制)

@import 没有命名空间机制,这是导致其命名冲突的核心原因;而 @use 内置命名空间功能,可灵活隔离模块成员,提升代码可读性和可维护性,具体分为三种使用场景。

2.2.1 不使用as:直接以文件名作为命名空间

当在 @use 后面直接跟上文件路径,且不使用 as 关键字指定命名空间时,会将导入的模块整体作为一个命名空间,且使用被导入文件的名称作为命名空间标识(省略下划线前缀)。

// _variables.scss
$primary-color: #007bff;
$secondary-color: #6c757d;

// styles.scss
@use 'variables.scss';  // 省略下划线,默认命名空间为variables
body {   
  background-color: variables.$primary-color; // 通过命名空间访问变量
}  
button {   
  background-color: variables.$secondary-color;
}

示例解析:styles.scss 使用 @use 直接导入了 variables.scss 文件,未指定自定义命名空间,因此 _variables.scss 中的所有内容被封装在 variables 命名空间下,访问时需加上 variables.前缀,避免与当前文件的其他变量冲突。

2.2.2 使用as xxx:自定义命名空间

通过 as 关键字可以为导入的模块自定义命名空间,让命名更简洁、贴合业务场景,进一步提升代码可读性。

// _variables.scss(与上例一致)
$primary-color: #007bff;
$secondary-color: #6c757d; 

// styles.scss
@use 'variables.scss' as customVars;  // 自定义命名空间为customVars
body {   
  background-color: customVars.$primary-color; 
}  
button {   
  background-color: customVars.$secondary-color; 
}

示例解析:通过 as customVars 为导入的模块创建了自定义命名空间 customVars,后续访问模块中的变量时,需通过 customVars. 前缀,既避免了命名冲突,又让变量来源更清晰。

2.2.3 特殊情况:使用as * 导入(无命名空间)

如果在 @use 后面使用 as *,表示将导入的模块的所有内容直接合并到当前文件中,不创建任何命名空间,导入的变量、mixin、函数等可直接使用。

// _variables.scss(与上例一致)
$primary-color: #007bff;
$secondary-color: #6c757d;

// styles.scss
@use 'variables.scss' as *;  // 无命名空间,直接合并内容
body {   
  background-color: $primary-color; // 直接访问变量,无需前缀
}  
button {   
  background-color: $secondary-color;
}

注意:这种方式会丧失命名空间的隔离优势,与 @import 类似,易引发命名冲突,仅建议在变量统一管理、无冲突风险的简单场景使用。

三、语法用法补充(其他核心用法)

3.1 @import 其他常见问题

除了重复加载,@import 还存在全局污染、依赖混乱等问题,以下是基础用法回顾及问题总结:

// _variables.scss
$color: red;
$font-size: 16px;

// _utils.scss
$color: blue; // 与variables.scss中的$color重名
@mixin flex-center {
  display: flex;
  justify-content: center;
  align-items: center;
}

// main.scss
@import "./variables";
@import "./utils"; // 重名变量被覆盖
@import "./variables"; // 重复加载,增加编译冗余

.box {
  color: $color; // 输出blue(被utils.scss中的$color覆盖,意外污染)
  font-size: $font-size; // 输出16px
  @include flex-center;
}

编译后CSS:

.box {
  color: blue;
  font-size: 16px;
  display: flex;
  justify-content: center;
  align-items: center;
}

问题总结:@import导入的变量会全局覆盖,重复导入同一文件会重复编译,无法区分成员来源,维护难度高。

3.2 @use 其他优势用法

除了命名空间和单例加载,@use 还支持私有成员、变量配置等优势特性,以下是补充示例:

// _theme.scss
$-private-var: 10px; // 私有变量(仅文件内可用,前缀-/_)
$primary-color: #3498db !default; // 默认变量,可被配置覆盖
$secondary-color: #2ecc71 !default;

// main.scss
// 自定义命名空间为t,并通过with配置变量
@use "./theme" as t with (
  $primary-color: #e74c3c, // 覆盖默认主色
  $secondary-color: #f39c12
);

.box {
  background: t.$primary-color; // 输出#e74c3c(配置后的值)
  color: t.$secondary-color; // 输出#f39c12(配置后的值)
  // margin: t.$-private-var; // 报错:私有变量无法访问
}

编译后CSS:

.box {
  background: #e74c3c;
  color: #f39c12;
}

四、实际开发场景选择建议

1. 优先使用@use的场景

  • 新建SCSS项目:全程使用@use,配合@forward实现模块化拆分(如按变量、mixin、组件拆分文件),提升代码可维护性和可扩展性。
  • 大型项目/团队协作:通过命名空间隔离和显式依赖,避免命名冲突,清晰区分成员来源,降低协作成本和维护难度。
  • 需要封装私有成员:当部分变量、mixin仅需在当前文件使用,无需暴露给外部时,使用@use的私有成员特性,实现代码封装。
  • 使用Sass新特性:@use支持所有Sass新特性(如内置模块导入),而@import不支持部分新特性,无法适配未来升级需求。

2. 临时使用@import的场景

  • 维护旧项目:当项目中大量使用@import,短期内无法全部迁移时,可临时保留,逐步替换为@use,避免影响项目正常运行。
  • 导入纯CSS文件:虽然@use也可导入纯CSS文件,但@import语法更简洁,且无需处理命名空间(仅适用于简单场景)。

五、补充注意事项

  • @use导入文件时,可省略文件扩展名(.scss、.sass),也可省略下划线前缀(如导入_variables.scss,可写为@use "./variables")。
  • @use的as *语法可省略命名空间,直接访问导入的成员,但会丧失命名空间的隔离优势,易引发冲突,仅建议在变量统一管理的简单场景使用。
  • 迁移旧项目时,需注意:@use中以-_开头的变量视为私有,若旧代码中存在此类命名的变量,导入后会无法访问,需修改变量命名或调整访问方式。
  • @use与@forward的配合:@forward用于转发模块成员(不直接使用),适合库开发或入口文件整合;@use用于直接使用模块成员,二者配合可实现更灵活的模块化体系。
  • Sass内置模块(如sass:math、sass:color)需通过@use导入才能使用,无法通过@import导入,这也是官方推荐@use的重要原因之一。

六、总结

推荐使用 @use 来导入模块,以获得更好的模块化支持、性能优化和避免全局污染问题。其核心优势在于局部作用域、命名空间隔离、单例加载、私有成员支持,彻底解决了@import的全局污染、重复加载、依赖混乱等问题,提升了代码的可维护性、可扩展性和协作效率。而 @import 在新版本 Sass 中已不再推荐使用,并且未来可能会被废弃,仅适合临时维护旧项目,新建项目或项目升级时,应优先采用@use + @forward的模块化方案,遵循官方推荐的开发规范,避免后续维护成本增加。

学习Less,看这篇就够了(从入门到实战)

2026年4月10日 09:58

Less(Leaner Style Sheets)是CSS预处理器,在原生CSS基础上增加变量、嵌套、混合、函数、运算、模块化等编程特性,让CSS更易维护、复用、扩展,最终编译成标准CSS运行。本文从环境搭建、核心语法、进阶技巧、实战规范全流程覆盖,新增多个优雅使用案例(含Less源码与编译后CSS对比),直接上手可用。

一、Less基础:是什么、为什么、怎么用

1.1 核心优势(为什么用Less)

  • 变量统一管理:颜色、尺寸、字体等全局配置,一改全改
  • 嵌套结构:完全匹配HTML层级,代码更直观、减少重复选择器
  • 混合(Mixin):复用样式片段,像函数一样传参
  • 运算/函数:支持加减乘除、颜色明暗、单位转换
  • 模块化:拆分文件、导入合并,便于团队协作与维护
  • 兼容原生CSS:所有CSS代码可直接写在Less中,零学习门槛

1.2 环境搭建(3种方式,最快1分钟)

方式1:浏览器直接运行(学习/原型)


<!-- 1. 引入Less文件,rel必须是stylesheet/less -->
<link rel="stylesheet/less" type="text/css" href="styles.less" />
<!-- 2. 引入Less编译器(CDN,已替换为可用链接) -->
<script src="https://cdn.bootcdn.net/ajax/libs/less.js/4.2.0/less.min.js"></script>

⚠️ 仅适合开发调试,生产环境禁止使用(性能差、依赖JS);原CDN链接报错“link dead”,已替换为稳定可用版本

方式2:VSCode自动编译(推荐,日常开发)

  1. 安装插件:Easy LESS
  2. 新建.less文件,保存时自动生成同名.css
  3. 配置(可选,settings.json):

"less.compile": {
  "out": "../css/", // 输出到css文件夹
  "compress": true, // 压缩CSS
  "sourceMap": false
}

方式3:命令行编译(项目构建)

  1. 安装Node.js,全局安装Less:

npm install -g less
lessc -v # 验证安装
  1. 编译命令:

lessc styles.less styles.css # 基础编译
lessc styles.less styles.min.css --compress # 压缩输出

二、Less核心语法(必掌握,直接套用)

2.1 变量(@变量名:值)—— 统一管理,一改全改

变量以@开头,可存颜色、尺寸、字体、路径等,支持插值(选择器、属性、URL)。


// 1. 基础变量定义(语义化命名,便于维护)
@primary: #2563eb; // 主色
@success: #16a34a; // 成功色
@font-size: 16px; // 基础字体
@spacing: 20px; // 基础间距
@img-path: "../images"; // 图片路径

// 2. 变量使用(结合运算,减少硬编码)
.btn {
  background: @primary;
  font-size: @font-size;
  padding: @spacing/2; // 10px,无需手动计算
  margin: @spacing;
}

// 3. 变量插值(复用选择器、属性、URL,避免重复书写)
@selector: card;
@prop: width;
.@{selector} { // 编译为 .card
  @{prop}: 300px; // 编译为 width:300px
  background: url("@{img-path}/bg.png");
}

编译后CSS:


.btn {
  background: #2563eb;
  font-size: 16px;
  padding: 10px;
  margin: 20px;
}
.card {
  width: 300px;
  background: url("../images/bg.png");
}

优雅要点:变量语义化命名,通过插值复用选择器和路径,运算替代硬编码,后续修改主色、间距时,仅需修改变量值,无需逐行修改样式。

2.2 嵌套规则(&父选择器)—— 匹配HTML结构,告别重复

Less允许选择器嵌套,&代表当前父选择器,用于伪类、交集选择器、兄弟选择器,避免重复书写父选择器。


// HTML结构:<div class="header"><nav class="nav"><a href="#" class="active">首页</a></nav></div>
.header {
  width: 100%;
  height: 60px;
  background: #fff;
  box-shadow: 0 2px 8px rgba(0,0,0,0.1); // 增加阴影,提升质感
  .nav {
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100%;
    a {
      color: #333;
      text-decoration: none;
      padding: 0 @spacing/2; // 复用间距变量
      margin: 0 @spacing;
      // & 代表父选择器 .nav a,避免书写 .nav a:hover
      &:hover { 
        color: @primary;
        transition: color 0.3s ease; // 过渡效果,更优雅
      }
      &.active { 
        font-weight: bold; 
        color: @primary;
        border-bottom: 2px solid @primary;
      }
    }
  }
}

编译后CSS(对比:选择器自动拼接,无需手动重复):


.header {
  width: 100%;
  height: 60px;
  background: #fff;
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.header .nav {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100%;
}
.header .nav a {
  color: #333;
  text-decoration: none;
  padding: 0 10px;
  margin: 0 20px;
}
.header .nav a:hover {
  color: #2563eb;
  transition: color 0.3s ease;
}
.header .nav a.active {
  font-weight: bold;
  color: #2563eb;
  border-bottom: 2px solid #2563eb;
}

优雅要点:嵌套结构与HTML完全对应,可读性极强;&的使用避免重复书写父选择器(如.header .nav a),同时结合过渡效果,提升交互质感。

2.3 混合(Mixin)—— 复用样式,支持传参(核心)

Mixin是可复用的样式片段,带()不输出到CSS,不带()会输出;支持参数、默认值、条件判断,可封装公共样式,避免重复编码。

(1)基础Mixin(无参)—— 封装公共样式


// 定义:清除浮动(公共Mixin,可全局调用)
.clearfix() {
  &::after {
    content: "";
    display: block;
    clear: both;
    height: 0;
    visibility: hidden;
  }
}

// 定义:居中布局(公共Mixin,复用性强)
.center() {
  display: flex;
  justify-content: center;
  align-items: center;
}

// 使用:多个容器复用,无需重复书写样式
.container {
  .clearfix(); // 调用清除浮动
  width: 1200px;
  margin: 0 auto;
}
.modal {
  .center(); // 调用居中布局
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0,0,0,0.5);
}

编译后CSS(对比:Mixin样式自动注入,无需重复书写):


.container {
  width: 1200px;
  margin: 0 auto;
}
.container::after {
  content: "";
  display: block;
  clear: both;
  height: 0;
  visibility: hidden;
}
.modal {
  display: flex;
  justify-content: center;
  align-items: center;
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0,0,0,0.5);
}

(2)带参数Mixin(默认值、多参数)—— 动态生成样式


// 定义:按钮样式(带参数+默认值,灵活适配不同场景)
.btn(@bg: @primary, @color: #fff, @radius: 4px, @padding: 8px 16px) {
  display: inline-block;
  padding: @padding;
  background: @bg;
  color: @color;
  border-radius: @radius;
  cursor: pointer;
  border: none;
  outline: none;
  transition: all 0.3s ease; // 统一过渡效果
  &:hover { 
    filter: brightness(0.9); // hover变暗,无需单独写样式
    transform: translateY(-2px); // 轻微上浮,提升交互
  }
  &:active {
    transform: translateY(0);
  }
}

// 使用:按需传参,无需重复书写按钮基础样式
.btn-primary { .btn(); } // 使用默认值(主色按钮)
.btn-success { .btn(@success); } // 传单个参数(成功色按钮)
.btn-round { .btn(@primary, #fff, 50%, 10px 20px); } // 传全部参数(圆形按钮)
.btn-small { .btn(@primary, #fff, 4px, 4px 8px); } // 传部分参数(小尺寸按钮)

编译后CSS(对比:自动生成不同样式的按钮,代码简洁):


.btn-primary {
  display: inline-block;
  padding: 8px 16px;
  background: #2563eb;
  color: #fff;
  border-radius: 4px;
  cursor: pointer;
  border: none;
  outline: none;
  transition: all 0.3s ease;
}
.btn-primary:hover {
  filter: brightness(0.9);
  transform: translateY(-2px);
}
.btn-primary:active {
  transform: translateY(0);
}

.btn-success {
  display: inline-block;
  padding: 8px 16px;
  background: #16a34a;
  color: #fff;
  border-radius: 4px;
  cursor: pointer;
  border: none;
  outline: none;
  transition: all 0.3s ease;
}
.btn-success:hover {
  filter: brightness(0.9);
  transform: translateY(-2px);
}
.btn-success:active {
  transform: translateY(0);
}

.btn-round {
  display: inline-block;
  padding: 10px 20px;
  background: #2563eb;
  color: #fff;
  border-radius: 50%;
  cursor: pointer;
  border: none;
  outline: none;
  transition: all 0.3s ease;
}
.btn-round:hover {
  filter: brightness(0.9);
  transform: translateY(-2px);
}
.btn-round:active {
  transform: translateY(0);
}

.btn-small {
  display: inline-block;
  padding: 4px 8px;
  background: #2563eb;
  color: #fff;
  border-radius: 4px;
  cursor: pointer;
  border: none;
  outline: none;
  transition: all 0.3s ease;
}
.btn-small:hover {
  filter: brightness(0.9);
  transform: translateY(-2px);
}
.btn-small:active {
  transform: translateY(0);
}

(3)命名空间(组织Mixin,避免冲突)


// 命名空间:统一管理公共Mixin,避免与业务样式冲突
#utils() { // 带(),不输出到CSS
  .clearfix() { 
    &::after {
      content: "";
      display: block;
      clear: both;
      height: 0;
      visibility: hidden;
    }
  }
  .center() { 
    display: flex;
    justify-content: center;
    align-items: center; 
  }
  .shadow() { // 新增阴影Mixin
    box-shadow: 0 2px 12px rgba(0,0,0,0.1);
  }
}

// 调用:通过命名空间调用,清晰区分公共样式与业务样式
.box { 
  #utils.center(); 
  #utils.shadow();
  width: 300px;
  height: 200px;
  background: #fff;
}

编译后CSS:


.box {
  display: flex;
  justify-content: center;
  align-items: center;
  box-shadow: 0 2px 12px rgba(0,0,0,0.1);
  width: 300px;
  height: 200px;
  background: #fff;
}

2.4 运算(+ - * /)—— 自动计算,减少手动计算

支持颜色、数值、单位运算,Less自动处理单位(优先左侧单位),避免手动计算错误,代码更优雅。


@base: 20px;
@width: 1000px;
@card-width: @width / 5; // 200px,自动计算栅格宽度
@light-primary: lighten(@primary, 10%); // 主色变亮10%,无需手动计算色值

.box {
  width: @width - 40px; // 960px,自适应宽度
  padding: @base * 1.5; // 30px,间距按比例调整
  margin: (@base / 2); // 10px,统一间距
}
.card {
  width: @card-width;
  background: @light-primary;
  margin: @base;
}

⚠️ 除法建议加括号:(100px / 2),避免与CSS语法冲突

编译后CSS(对比:自动计算数值和颜色,无需手动计算):


.box {
  width: 960px;
  padding: 30px;
  margin: 10px;
}
.card {
  width: 200px;
  background: #3b82f6; // 自动计算的亮主色
  margin: 20px;
}

2.5 内置函数—— 颜色、字符串、数学处理(常用)

Less提供大量内置函数,无需定义直接用,提升效率,让样式更优雅。


// 颜色函数(最常用,自动处理色值)
@primary: #2563eb;
@dark-primary: darken(@primary, 10%); // 主色变暗10%
@fade-primary: fade(@primary, 50%); // 主色半透明
@saturate-primary: saturate(@primary, 20%); // 主色增加饱和度

// 数学函数(自动处理数值)
@base-font: 16px;
@title-font: ceil(@base-font * 1.5); // 24px,向上取整
@sub-font: floor(@base-font * 0.8); // 12px,向下取整

// 使用:结合函数和变量,样式更灵活
.title {
  font-size: @title-font;
  color: @dark-primary;
}
.sub-title {
  font-size: @sub-font;
  color: @fade-primary;
}
.btn {
  background: @saturate-primary;
  &:hover {
    background: @dark-primary;
  }
}

编译后CSS:


.title {
  font-size: 24px;
  color: #1d4ed8; // 变暗后的主色
}
.sub-title {
  font-size: 12px;
  color: rgba(37, 99, 235, 0.5); // 半透明主色
}
.btn {
  background: #1d4ed8; // 增加饱和度后的主色
}
.btn:hover {
  background: #1d4ed8;
}

2.6 导入(@import)—— 模块化拆分,代码解耦

拆分变量、Mixin、公共样式,通过@import合并,支持省略.less后缀,让代码结构更清晰,便于维护。


// 1. 拆分文件(按功能拆分,各司其职)
// variables.less → 全局变量(单独管理,一改全改)
@primary: #2563eb;
@font-size: 16px;
@spacing: 20px;

// mixins.less → 公共混合(单独管理,全局复用)
.clearfix() { ... }
.btn(@bg: @primary) { ... }
.center() { ... }

// base.less → 基础样式(reset、全局样式)
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
body {
  font-family: "Microsoft YaHei", sans-serif;
  font-size: @font-size;
  color: #333;
}

// 2. 主文件导入(统一入口,结构清晰)
@import "variables"; // 省略.less
@import "mixins";
@import "base";

// 编写业务样式(仅关注业务,无需关注公共样式)
.header {
  .clearfix();
  height: 60px;
  .nav {
    .center();
    a {
      color: #333;
      &:hover { color: @primary; }
    }
  }
}

⚠️ @import (reference) "mixins.less"; → 仅导入Mixin,不输出到CSS

编译后CSS(对比:所有导入的样式自动合并,结构清晰):


* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
body {
  font-family: "Microsoft YaHei", sans-serif;
  font-size: 16px;
  color: #333;
}
.header {
  height: 60px;
}
.header::after {
  content: "";
  display: block;
  clear: both;
  height: 0;
  visibility: hidden;
}
.header .nav {
  display: flex;
  justify-content: center;
  align-items: center;
}
.header .nav a {
  color: #333;
}
.header .nav a:hover {
  color: #2563eb;
}

2.7 作用域与注释

  • 作用域:就近原则,局部变量覆盖全局变量,类似JS,可灵活控制变量作用范围

// 全局变量(整个项目可用)
@color: red;

// 局部变量(仅在.box内可用,不影响全局)
.box {
  @color: blue;
  color: @color; // blue(局部优先)
}
// 其他模块仍使用全局变量
.text {
  color: @color; // red
}

编译后CSS:


.box {
  color: blue;
}
.text {
  color: red;
}
  • 注释:

    • 单行注释:// 注释 → 编译后不保留(用于开发备注,不污染生产CSS)
    • 多行注释:/* 注释 */ → 编译后保留(用于生产环境备注,如版权信息)

三、Less进阶技巧(提升效率,避坑)

3.1 父选择器&高级用法


// 1. 前缀拼接(批量生成同类样式,避免重复)
.btn {
  display: inline-block;
  padding: 8px 16px;
  border-radius: 4px;
  &-primary { background: @primary; color: #fff; } // .btn-primary
  &-success { background: @success; color: #fff; } // .btn-success
  &-warning { background: #f59e0b; color: #fff; } // .btn-warning
  &-disabled { background: #ccc; color: #666; cursor: not-allowed; } // .btn-disabled
}

// 2. 多层嵌套&(精准定位子元素,避免冗长选择器)
.list {
  width: 100%;
  &-item {
    padding: @spacing;
    border-bottom: 1px solid #eee;
    &:last-child { border-bottom: none; } // .list-item:last-child
    &-title { font-weight: bold; color: #333; } // .list-item-title
    &-content { color: #666; margin-top: 8px; } // .list-item-content
  }
}

编译后CSS(对比:自动拼接选择器,批量生成样式,代码简洁):


.btn {
  display: inline-block;
  padding: 8px 16px;
  border-radius: 4px;
}
.btn-primary {
  background: #2563eb;
  color: #fff;
}
.btn-success {
  background: #16a34a;
  color: #fff;
}
.btn-warning {
  background: #f59e0b;
  color: #fff;
}
.btn-disabled {
  background: #ccc;
  color: #666;
  cursor: not-allowed;
}

.list {
  width: 100%;
}
.list-item {
  padding: 20px;
  border-bottom: 1px solid #eee;
}
.list-item:last-child {
  border-bottom: none;
}
.list-item-title {
  font-weight: bold;
  color: #333;
}
.list-item-content {
  color: #666;
  margin-top: 8px;
}

3.2 条件Mixin(when)—— 动态生成样式


// 定义:根据尺寸生成不同按钮(条件判断,灵活适配)
.btn(@size) when (@size = large) {
  padding: 12px 24px;
  font-size: 18px;
  border-radius: 6px;
}
.btn(@size) when (@size = small) {
  padding: 4px 8px;
  font-size: 14px;
  border-radius: 3px;
}
// 新增条件:根据主题生成不同颜色
.btn(@size, @theme) when (@theme = dark) {
  .btn(@size);
  background: #333;
  color: #fff;
}

// 使用:按需传入条件,自动生成对应样式
.btn-lg { .btn(large); } // 大尺寸按钮
.btn-sm { .btn(small); } // 小尺寸按钮
.btn-lg-dark { .btn(large, dark); } // 大尺寸深色按钮

编译后CSS:


.btn-lg {
  padding: 12px 24px;
  font-size: 18px;
  border-radius: 6px;
}
.btn-sm {
  padding: 4px 8px;
  font-size: 14px;
  border-radius: 3px;
}
.btn-lg-dark {
  padding: 12px 24px;
  font-size: 18px;
  border-radius: 6px;
  background: #333;
  color: #fff;
}

3.3 循环(for)—— 批量生成样式(Less 3.9+)


// 生成1-5列栅格(循环遍历,无需手动写5个样式)
.generate-columns(@n, @i: 1) when (@i =< @n) {
  .col-@{i} {
    width: (@i * 100% / @n);
    float: left;
    padding: @spacing/2;
    box-sizing: border-box;
  }
  .generate-columns(@n, @i + 1); // 递归循环
}
.generate-columns(5); // 生成col-1到col-5

// 生成不同尺寸的margin样式(批量生成,复用性强)
.generate-margin(@n, @i: 1) when (@i =< @n) {
  .mt-@{i} { margin-top: @i * 8px; }
  .mb-@{i} { margin-bottom: @i * 8px; }
  .generate-margin(@n, @i + 1);
}
.generate-margin(5); // 生成mt-1~mt-5、mb-1~mb-5

编译后CSS(对比:自动生成10个margin样式+5个栅格样式,无需手动书写):


.col-1 {
  width: 20%;
  float: left;
  padding: 10px;
  box-sizing: border-box;
}
.col-2 {
  width: 40%;
  float: left;
  padding: 10px;
  box-sizing: border-box;
}
.col-3 {
  width: 60%;
  float: left;
  padding: 10px;
  box-sizing: border-box;
}
.col-4 {
  width: 80%;
  float: left;
  padding: 10px;
  box-sizing: border-box;
}
.col-5 {
  width: 100%;
  float: left;
  padding: 10px;
  box-sizing: border-box;
}

.mt-1 { margin-top: 8px; }
.mb-1 { margin-bottom: 8px; }
.mt-2 { margin-top: 16px; }
.mb-2 { margin-bottom: 16px; }
.mt-3 { margin-top: 24px; }
.mb-3 { margin-bottom: 24px; }
.mt-4 { margin-top: 32px; }
.mb-4 { margin-bottom: 32px; }
.mt-5 { margin-top: 40px; }
.mb-5 { margin-bottom: 40px; }

3.4 映射(Maps)—— 像对象一样取值(Less 3.5+)


// 定义颜色映射(类似JS对象,统一管理所有颜色,便于查找和修改)
@colors: {
  primary: #2563eb;
  success: #16a34a;
  warning: #f59e0b;
  danger: #ef4444;
  dark: #333;
  light: #f5f5f5;
};

// 定义尺寸映射(统一管理尺寸,避免硬编码)
@sizes: {
  small: 14px;
  base: 16px;
  large: 18px;
  xlarge: 24px;
};

// 使用:通过映射取值,代码更简洁,维护更方便
.btn {
  font-size: @sizes[base];
  &-primary { background: @colors[primary]; }
  &-success { background: @colors[success]; }
  &-warning { background: @colors[warning]; }
}
.title {
  font-size: @sizes[xlarge];
  color: @colors[dark];
}

编译后CSS:


.btn {
  font-size: 16px;
}
.btn-primary {
  background: #2563eb;
}
.btn-success {
  background: #16a34a;
}
.btn-warning {
  background: #f59e0b;
}
.title {
  font-size: 24px;
  color: #333;
}

四、实战规范与常见问题(避坑指南)

4.1 项目规范(推荐)

  1. 文件结构:

src/
├── less/
│   ├── variables.less   # 全局变量(颜色、尺寸、字体)
│   ├── mixins.less      # 公共混合(清除浮动、按钮、居中)
│   ├── base.less        # 基础样式(reset、全局)
│   ├── components/      # 组件(按钮、卡片、导航)
│   └── main.less        # 主入口(导入所有)
  1. 命名:变量用@xxx-xxx(@primary-color),Mixin用小驼峰/短横线,语义化

4.2 常见问题与解决

  1. 嵌套过深:最多3层,避免编译后选择器过长、性能差
  2. 变量污染:全局变量放单独文件,局部变量仅在模块内使用
  3. 编译报错:检查括号、分号、变量定义,优先用Easy LESS实时提示
  4. 单位冲突:运算时统一单位,或用unit()函数转换

五、Less vs Sass(快速对比,选择更合适)

特性 Less Sass
变量符号 @ $
编译环境 Node.js/浏览器 Ruby/Node.js
语法 接近CSS,易上手 缩进/花括号两种
循环/条件 支持(Less 3.9+) 原生支持,更强大
生态 轻量,适合中小型项目 功能全,适合大型项目

六、总结与下一步

Less核心就是把CSS变成可维护的代码:变量统一、嵌套清晰、Mixin复用、函数简化、模块化拆分。新增的优雅案例均贴合实际开发,通过Less源码与编译后CSS对比,可清晰看到Less如何简化编码、提升效率。

CSS3 position 属性全面理解(实战版)

2026年4月9日 23:07

CSS3 的 position 属性,核心作用是控制元素在页面中的定位方式,决定元素如何脱离正常文档流、如何相对于父元素/视口进行定位,是布局中实现“悬浮、固定、层叠”等效果的核心属性,常用且易混淆,本篇将从基础到实战,彻底讲懂。

核心原则:position 的取值决定元素的定位模式,配合 toprightbottomleft(以下简称“方位属性”)控制具体位置,未设置方位属性时,元素默认保持原有位置。

一、position 5个取值(必学,按常用度排序)

position 有5个核心取值,其中 static 是默认值,relativeabsolutefixed 是高频常用值,sticky 是粘性定位(场景化常用),逐一拆解如下。

1. static(默认值:静态定位)

最基础的定位方式,元素遵循正常文档流,自上而下、从左到右排列,不受方位属性(top/right等)的影响,也不会脱离文档流。

/* 默认无需主动设置,所有元素默认都是 static */
.box {
  position: static;
  top: 20px; /* 无效,static 不识别方位属性 */
  left: 20px; /* 无效 */
  width: 100px;
  height: 100px;
  background: #00aaff;
}

特点:无法通过方位属性调整位置,完全遵循正常布局,一般用于“取消已设置的定位”(如覆盖父元素的定位)。

2. relative(相对定位)

元素不脱离正常文档流,保留自身在文档流中的原有位置,方位属性(top/right等)相对于自身原有位置进行偏移,不会影响其他元素的布局。

.box {
  position: relative;
  top: 20px; /* 相对于自身原有位置,向下偏移20px */
  left: 30px; /* 相对于自身原有位置,向右偏移30px */
  width: 100px;
  height: 100px;
  background: #ff7d00;
}

关键要点:

  • 不脱离文档流,自身原有位置会被保留(其他元素不会填充进来);
  • 偏移量相对于“自身原本的位置”(而非父元素);
  • 常用场景:作为 absolute 定位的“参考容器”(子绝父相),或微调元素位置。

3. absolute(绝对定位)【重点】

元素完全脱离正常文档流,不再保留自身原有位置(其他元素会填充其空位),方位属性相对于最近的已定位祖先元素(position 不为 static 的祖先)定位;若没有已定位祖先,则相对于根元素(html)定位。

/* 父容器设置 relative(作为参考) */
.parent {
  position: relative;
  width: 300px;
  height: 300px;
  background: #f5f5f5;
}
/* 子元素设置 absolute(相对于父容器定位) */
.child {
  position: absolute;
  top: 50px; /* 相对于父容器顶部,向下50px */
  right: 50px; /* 相对于父容器右侧,向左50px */
  width: 100px;
  height: 100px;
  background: #00cc66;
}

关键要点(避坑核心):

  • 脱离文档流,不占据页面空间,会“悬浮”在其他元素上方;
  • 定位参考物:优先找“最近的、position 为 relative/absolute/fixed/sticky”的祖先元素,没有则相对于 html(页面);
  • 高频场景:弹窗、下拉菜单、元素悬浮、精准定位(如按钮右上角的角标);
  • 易错点:若父元素未设置定位,子元素 absolute 会相对于页面定位,导致布局错乱。

4. fixed(固定定位)

元素完全脱离正常文档流,方位属性相对于浏览器视口(viewport) 定位,无论页面如何滚动,元素始终固定在视口的指定位置,不随页面滚动而移动。

/* 页面右下角固定按钮 */
.fixed-btn {
  position: fixed;
  bottom: 30px; /* 相对于视口底部,向上30px */
  right: 30px; /* 相对于视口右侧,向左30px */
  width: 60px;
  height: 60px;
  border-radius: 50%;
  background: #ff4444;
  color: white;
  text-align: center;
  line-height: 60px;
}

关键要点:

  • 脱离文档流,不占据页面空间,始终悬浮在视口固定位置;
  • 定位参考物是“浏览器视口”,与父元素无关;
  • 常用场景:固定导航栏、回到顶部按钮、悬浮客服按钮。

5. sticky(粘性定位)

“相对定位 + 固定定位”的结合体,元素默认遵循正常文档流,当页面滚动到指定位置时,自动切换为固定定位,固定在视口的指定位置;滚动超出范围后,恢复为相对定位。

/* 粘性导航栏 */
.sticky-nav {
  position: sticky;
  top: 0; /* 滚动到顶部距离为0时,固定 */
  width: 100%;
  height: 60px;
  background: #333;
  color: white;
  line-height: 60px;
  padding: 0 20px;
}

关键要点:

  • 未滚动到指定位置时,遵循正常文档流;滚动到阈值(top/right等设置的值)时,变为固定定位;
  • 必须设置方位属性(top/right/bottom/left),否则粘性效果无效;
  • 常用场景:粘性导航栏、列表标题悬浮(滚动时标题固定在顶部)。

二、核心对比(快速区分,避坑关键)

取值 是否脱离文档流 定位参考物 核心场景
static 无(不识别方位属性) 默认布局、取消定位
relative 自身原有位置 作为absolute参考容器、微调位置
absolute 最近的已定位祖先 / html 弹窗、悬浮元素、精准定位
fixed 浏览器视口 固定导航、回到顶部按钮
sticky 视滚动状态(未固定时否,固定时是) 正常流时自身,固定时视口 粘性导航、悬浮标题

三、实战案例(直接复制套用,覆盖高频场景)

案例1:子绝父相(最常用,精准定位)

场景:卡片内部的按钮、角标,相对于卡片精准定位,不影响卡片布局。

/* 父卡片 - 相对定位(参考容器) */
.card {
  position: relative;
  width: 280px;
  height: 380px;
  background: white;
  border-radius: 8px;
  box-shadow: 0 2px 12px rgba(0,0,0,0.1);
  padding: 20px;
}
/* 子元素 - 绝对定位(相对于卡片) */
.card-badge {
  position: absolute;
  top: 10px;
  right: 10px;
  background: #ff4444;
  color: white;
  padding: 2px 8px;
  border-radius: 12px;
  font-size: 12px;
}
.card-btn {
  position: absolute;
  bottom: 20px;
  left: 50%;
  transform: translateX(-50%); /* 水平居中 */
  width: 80%;
  height: 40px;
  background: #00aaff;
  color: white;
  border: none;
  border-radius: 4px;
}

案例2:固定回到顶部按钮

场景:页面滚动时,右下角固定显示回到顶部按钮,点击可返回页面顶部。

.back-top {
  position: fixed;
  bottom: 30px;
  right: 30px;
  width: 50px;
  height: 50px;
  border-radius: 50%;
  background: rgba(0, 170, 255, 0.8);
  color: white;
  text-align: center;
  line-height: 50px;
  cursor: pointer;
  transition: all 0.3s;
}
.back-top:hover {
  background: #00aaff;
  transform: scale(1.1);
}

案例3:粘性导航栏

场景:页面顶部导航栏,滚动页面时,导航栏固定在视口顶部,不随页面滚动消失。

/* 页面头部 */
.header {
  height: 100px;
  background: #f5f5f5;
  line-height: 100px;
  text-align: center;
  font-size: 24px;
}
/* 粘性导航 */
.sticky-nav {
  position: sticky;
  top: 0;
  width: 100%;
  height: 60px;
  background: #333;
  color: white;
  padding: 0 20px;
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
/* 导航菜单 */
.nav-menu {
  display: flex;
  gap: 30px;
  line-height: 60px;
}
/* 页面内容(用于测试滚动) */
.content {
  height: 1500px;
  padding: 20px;
}

四、避坑注意事项(必看)

  • absolute 定位的元素,若父元素未设置定位(position 为 static),会相对于 html 定位,容易导致布局错乱,建议遵循“子绝父相”原则。
  • fixed 定位的元素,会脱离文档流且相对于视口定位,若父元素有 transform 属性(如 scale、translate),会导致 fixed 定位失效(参考物变为父元素)。
  • sticky 定位必须设置方位属性(top/right等),否则无法触发粘性效果;且父元素不能有 overflow: hidden 属性,否则粘性效果失效。
  • 脱离文档流的元素(absolute、fixed),会覆盖在未脱离文档流的元素上方,可通过 z-index 属性调整层级(z-index 值越大,层级越高)。
  • relative 定位的元素,虽然不脱离文档流,但会创建“层叠上下文”,其内部的绝对定位元素会相对于它定位,而非相对于更外层的元素。

五、总结(快速记忆)

  1. 默认 static:不脱离流,方位无效;

  2. relative:不脱离流,相对于自身,做参考容器;

  3. absolute:脱离流,相对于已定位祖先,精准定位;

  4. fixed:脱离流,相对于视口,固定不动;

  5. sticky:混合定位,滚动触发固定,需设方位属性。

记住“子绝父相”“fixed 相对视口”“sticky 需设方位”这3个关键点,就能解决90%的定位场景,剩下的靠实战熟练即可。

零基础学会 Flex 布局(实战版)

2026年4月9日 23:00

Flex 布局(弹性布局)是 CSS 中最常用、最灵活的布局方式,核心作用是快速实现元素的对齐、分布、自适应,替代传统的 float、position 布局,适配所有现代浏览器,上手简单且实用性极强。

核心原则:给父容器设置 display: flex,父容器称为「flex 容器」,其直接子元素称为「flex 项目」,通过控制容器和项目的属性,实现各种布局效果。

一、核心基础(必学,3分钟掌握)

1. 开启 Flex 布局(第一步)

给需要布局的父容器添加 display: flex,即可开启弹性布局,项目会默认横向排列(水平主轴)。

/* 父容器 */
.flex-container {
  display: flex; /* 开启flex布局 */
  width: 100%;
  height: 300px;
  background: #f5f5f5;
}

/* 子项目 */
.flex-item {
  width: 100px;
  height: 100px;
  background: #00aaff;
  margin: 10px;
}

效果:3个项目横向排列,默认在容器顶部对齐,自动填满水平方向(未填充满时,项目之间有间隙)。

2. 两个核心概念(必记)

  • 主轴:项目排列的主要方向(默认「水平方向」,从左到右)
  • 交叉轴:与主轴垂直的方向(默认「垂直方向」,从上到下)

后续所有属性,都是围绕「主轴」和「交叉轴」的对齐、分布来设置。

二、容器属性(控制整体布局,最常用6个)

所有属性均设置在「父容器」上,控制子项目的排列方式,重点记前4个。

1. flex-direction(控制主轴方向,核心)

决定项目是横向、纵向排列,解决“水平/垂直布局”问题。

属性值 效果 常用场景
row(默认) 主轴水平,项目从左到右 导航栏、横向卡片
column 主轴垂直,项目从上到下 侧边栏、纵向列表
row-reverse 主轴水平,项目从右到左 反向排列(少见)
column-reverse 主轴垂直,项目从下到上 反向排列(少见)

2. justify-content(主轴对齐,最常用)

控制项目在「主轴」上的对齐方式,解决“水平/垂直居中、分布”问题。

属性值 效果(主轴水平时) 高频场景
flex-start(默认) 项目靠左对齐 普通列表
center 项目水平居中 登录框、卡片居中
flex-end 项目靠右对齐 右侧按钮组
space-between 两端对齐,项目之间间距相等 导航栏(左右分布)
space-around 项目两侧间距相等,整体间距均匀 卡片布局、商品列表

3. align-items(交叉轴对齐,核心)

控制项目在「交叉轴」上的对齐方式,解决“垂直/水平居中”的另一半问题。

属性值 效果(主轴水平时) 高频场景
stretch(默认) 项目高度拉伸至与容器一致 等高卡片、导航栏
center 项目垂直居中 文字+图标对齐、卡片内容居中
flex-start 项目靠上对齐 顶部列表
flex-end 项目靠下对齐 底部按钮组

4. flex-wrap(控制换行,避免溢出)

默认情况下,项目会强制在一行显示,超出容器会溢出,用这个属性控制换行。

属性值 效果
nowrap(默认) 不换行,项目会被压缩
wrap 自动换行,超出一行时,新行在下
wrap-reverse 自动换行,超出一行时,新行在上

5. align-content(多行交叉轴对齐,少见)

只有当项目换行(flex-wrap: wrap)时才生效,控制多行项目在交叉轴上的整体对齐方式,用法和 justify-content 类似(center、space-between 等)。

6. flex-flow(简写属性,偷懒必备)

flex-direction + flex-wrap 的简写,顺序任意,默认值:flex-flow: row nowrap。

/* 等价于 flex-direction: column; flex-wrap: wrap; */
.flex-container {
  flex-flow: column wrap;
}

三、项目属性(控制单个项目,常用3个)

属性设置在「子项目」上,控制单个项目的大小、对齐方式,按需使用。

1. flex(核心,控制项目占比)

最常用属性,控制项目在主轴上的占比,替代 width/height,实现自适应,语法:flex: 数字(数字越大,占比越大)。

.flex-container {
  display: flex;
  width: 100%;
  height: 200px;
  background: #f5f5f5;
}
/* 三个项目,占比 1:2:1 */
.item1 { flex: 1; background: #00aaff; }
.item2 { flex: 2; background: #ff7d00; }
.item3 { flex: 1; background: #00cc66; }

效果:容器宽度被分成 4 份,item2 占 2 份,其余各占 1 份,自适应容器宽度。

补充:flex 的完整写法是 flex: flex-grow flex-shrink flex-basis,日常用简写(数字)即可。

2. align-self(单个项目交叉轴对齐)

覆盖父容器的 align-items 属性,单独控制某个项目的交叉轴对齐方式。

.flex-container {
  display: flex;
  align-items: center; /* 所有项目垂直居中 */
}
.item-special {
  align-self: flex-end; /* 单个项目靠下对齐 */
}

补充:flex 的完整写法是 flex: flex-grow flex-shrink flex-basis,日常用简写(数字)即可。

3. order(控制项目顺序)

默认所有项目 order: 0,数字越小,项目越靠前;可以用负数,实现“不改变HTML结构,调整显示顺序”。

.item1 { order: 2; } /* 第3个显示 */
.item2 { order: 1; } /* 第2个显示 */
.item3 { order: 0; } /* 第1个显示 */

四、实战案例(直接复制套用,覆盖80%场景)

案例1:水平居中 + 垂直居中(最常用)

适用于登录框、弹窗内容、卡片居中,一步到位。

.container {
  display: flex;
  justify-content: center; /* 主轴居中(水平) */
  align-items: center; /* 交叉轴居中(垂直) */
  width: 100vw; /* 视口宽度 */
  height: 100vh; /* 视口高度 */
  background: #f5f5f5;
}
.center-box {
  width: 300px;
  height: 200px;
  background: white;
  border-radius: 8px;
  text-align: center;
  line-height: 200px;
}

案例2:导航栏(两端对齐)

适用于网站导航,左侧logo、右侧菜单,两端分布。

.nav {
  display: flex;
  justify-content: space-between; /* 两端对齐 */
  align-items: center; /* 垂直居中 */
  padding: 0 20px;
  height: 60px;
  background: #333;
  color: white;
}
.nav-logo {
  font-size: 20px;
  font-weight: bold;
}
.nav-menu {
  display: flex; /* 菜单横向排列 */
  gap: 20px; /* 菜单之间间距 */
}

案例3:自适应卡片布局(换行)

适用于商品列表、卡片展示,自适应屏幕宽度,自动换行。

.card-container {
  display: flex;
  flex-wrap: wrap; /* 自动换行 */
  gap: 20px; /* 卡片之间间距 */
  padding: 20px;
  background: #f5f5f5;
}
.card {
  flex: 1; /* 自适应占比 */
  min-width: 250px; /* 最小宽度,避免太窄 */
  height: 200px;
  background: white;
  border-radius: 8px;
  padding: 15px;
}

案例4:垂直布局(侧边栏)

适用于侧边导航、垂直列表,纵向排列。

.sidebar {
  display: flex;
  flex-direction: column; /* 垂直排列 */
  width: 200px;
  height: 500px;
  background: #333;
  color: white;
}
.sidebar-item {
  padding: 15px 20px;
  border-bottom: 1px solid #444;
}
.sidebar-item:last-child {
  border-bottom: none;
}

五、注意事项(避坑关键)

  • 开启 flex 布局后,子项目的 float、clear、vertical-align 属性会失效,无需再用。
  • flex: 1 等价于 flex: 1 1 0%,会让项目自适应填充剩余空间,优先于固定 width/height。
  • justify-content 控制主轴,align-items 控制交叉轴,方向由 flex-direction 决定(别搞反)。
  • 项目换行时,用 gap 控制间距(比 margin 更简洁,不会出现最后一个项目多余间距)。
  • 兼容问题:Flex 布局支持 IE10+,现代浏览器(Chrome、Edge、Firefox、Safari)均完美支持,无需加前缀。

六、总结(快速记忆)

  1. 开启:父容器 display: flex

  2. 方向:flex-direction(row/column);

  3. 对齐:主轴 justify-content,交叉轴 align-items;

  4. 换行:flex-wrap: wrap;

  5. 占比:项目 flex: 数字;

记住这5点,就能解决80%的布局问题,剩下的靠实战熟练即可。

Vue 中实现文字滚动(跑马灯)的多种方式

2026年4月9日 22:51

在 Vue 里实现文字滚动(跑马灯) ,最常用、最稳的就两种:

  1. CSS 动画纯实现(简单、性能好)
  2. JS 控制滚动(可暂停、可控制速度)

下面直接给你可复制粘贴的 Vue 组件代码


方式1:纯 CSS 跑马灯(推荐)

Marquee.vue

<template>
  <div class="marquee-wrap">
    <div class="marquee-content">
      {{ text }}
    </div>
  </div>
</template>

<script setup>
const text = '这里是需要滚动的文字,Vue 跑马灯效果,从右向左无限滚动~';
</script>

<style scoped>
.marquee-wrap {
  width: 100%;
  overflow: hidden;
  white-space: nowrap;
  background: #f5f5f5;
  padding: 8px 16px;
  border-radius: 8px;
}

.marquee-content {
  display: inline-block;
  animation: marquee 15s linear infinite;
}

@keyframes marquee {
  0% {
    transform: translateX(100%);
  }
  100% {
    transform: translateX(-100%);
  }
}
</style>

特点:

  • 一行无限滚动
  • 无 JS,性能最好
  • 鼠标悬浮暂停版往下看

方式2:hover 暂停 + 无缝滚动(更常用)

<template>
  <div class="box">
    <div class="marquee" @mouseenter="pause" @mouseleave="play">
      <div class="text" :style="{ animationPlayState }">
        {{ content }}
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue';

const content = 'Vue3 无缝跑马灯,鼠标移入暂停,移出继续滚动~';
const animationPlayState = ref('running');

const pause = () => {
  animationPlayState.value = 'paused';
};
const play = () => {
  animationPlayState.value = 'running';
};
</script>

<style scoped>
.box {
  width: 100%;
  overflow: hidden;
  background: #f9f9f9;
  padding: 10px;
  border-radius: 6px;
}
.marquee {
  white-space: nowrap;
}
.text {
  display: inline-block;
  animation: move 12s linear infinite;
}
@keyframes move {
  0% { transform: translateX(100%); }
  100% { transform: translateX(-100%); }
}
</style>

方式3:真正无缝(无空白,首尾衔接)

适合公告、长文本:

<template>
  <div class="wrap">
    <div class="box">
      <span class="txt1">{{ text }}</span>
      <span class="txt2">{{ text }}</span>
    </div>
  </div>
</template>

<script setup>
const text = '这里是真正无缝跑马灯,没有空白间隔,一直循环滚动';
</script>

<style scoped>
.wrap {
  width: 100%;
  overflow: hidden;
  background: #fff8e1;
  padding: 8px 0;
}
.box {
  display: flex;
  width: max-content;
  animation: scroll 10s linear infinite;
}
.txt1, .txt2 {
  padding: 0 20px;
}
@keyframes scroll {
  0% { transform: translateX(0); }
  100% { transform: translateX(-50%); }
}
</style>

方式4:JS 控制滚动(可变速、可停止)

<template>
  <div class="box" style="overflow: hidden">
    <div class="text" :style="{ marginLeft: `${left}px` }">
      JS 控制跑马灯,可随时停止、加速、减速
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
const left = ref(300);
let timer = null;

onMounted(() => {
  timer = setInterval(() => {
    left.value -= 1;
    if (left.value < -300) left.value = 300;
  }, 20);
});
onUnmounted(() => clearInterval(timer));
</script>

CSS3 阴影完全指南:box-shadow、text-shadow 详解

2026年4月9日 22:48

shadow 里最常用的两个阴影属性:

  • text-shadow:文字阴影(简单)
  • box-shadow:盒子/元素阴影(常用、功能强)

我用最通俗、能直接上手的方式给你讲清楚,附带复制即用的代码。


一、text-shadow 文字阴影

专门给文字加阴影,语法超级简单。

基础语法

text-shadow: 水平偏移 垂直偏移 模糊度 颜色;

参数解释

  1. 水平偏移:正数向右,负数向左
  2. 垂直偏移:正数向下,负数向上
  3. 模糊度:值越大越模糊(不能为负)
  4. 颜色:阴影颜色(可省略,默认文字颜色)

示例代码

/* 基础阴影 */
text-shadow: 2px 2px 4px #333;

/* 发光效果(最常用!) */
text-shadow: 0 0 8px #00aaff;

/* 多重阴影(用逗号分隔) */
text-shadow: 1px 1px 2px black, 0 0 1em blue, 0 0 0.2em blue;

二、box-shadow 盒子阴影(重点!)

div、图片、按钮、卡片加阴影,90% 网页美化都用它。

完整语法

box-shadow: 水平偏移 垂直偏移 模糊度 扩散半径 颜色 内外阴影;

6 个参数(必须记)

  1. h-shadow:水平偏移(必需)
  2. v-shadow:垂直偏移(必需)
  3. blur:模糊半径(越大越柔)
  4. spread:扩散半径(扩大/缩小阴影)
  5. color:阴影颜色
  6. inset内阴影(不加就是外阴影)

最常用写法(外阴影)

/* 卡片柔和阴影(推荐!) */
box-shadow: 0 2px 12px rgba(0,0,0,0.1);

内阴影(inset)

/* 凹陷效果 */
box-shadow: inset 0 0 10px #000;

多重阴影

box-shadow: 
  0 0 10px red,
  0 0 20px blue,
  0 0 30px green;

三、最实用的 6 种阴影效果(直接复制)

1. 标准卡片阴影(最常用)

box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);

2. 悬浮抬起效果(hover)

div{
  transition: 0.3s;
}
div:hover{
  box-shadow: 0 8px 24px rgba(0,0,0,0.15);
  transform: translateY(-3px);
}

3. 发光效果

box-shadow: 0 0 12px #00aaff;

4. 内阴影(凹陷)

box-shadow: inset 0 2px 8px rgba(0,0,0,0.2);

5. 细边框阴影(代替 border)

box-shadow: 0 0 0 1px #ccc;

6. 文字发光

text-shadow: 0 0 6px #fff, 0 0 12px #00aaff;

四、快速区分 & 记忆

属性 作用 最常用格式
text-shadow 文字阴影 2px 2px 4px #333
box-shadow 盒子阴影 0 2px 12px rgba(0,0,0,0.1)
  • 偏移:正右下,负左上
  • 模糊:越大越柔
  • 扩散:越大越大
  • 内阴影:加 inset

五、小技巧(高手必备)

  1. 阴影用 rgba 更自然:rgba(0,0,0,0.1)
  2. 垂直偏移 > 水平偏移更符合视觉习惯
  3. 配合 transition 做 hover 动画超好看
  4. 多重阴影 = 高级质感

总结

  • text-shadow:给文字加阴影,4个参数,简单
  • box-shadow:给盒子加阴影,6个参数,支持内外、多层
  • 最实用:0 2px 12px rgba(0,0,0,0.1) 卡片阴影
  • 想凹陷就加 inset

需要我给你做一个可在线调试的阴影演示页面吗?直接复制就能用!

❌
❌