【前端】前端面试题
前端面试题
闭包
1. 定义:
闭包(Closure) 是指一个函数能够访问并记住其外部作用域中的变量,即使外部函数已经执行完毕。闭包由两部分组成:
- 一个函数(通常是内部函数)。
- 该函数被创建时所在的作用域(即外部函数的变量环境)
functionouter(){let count =0;// 外部函数的变量functioninner(){ count++;// 内部函数访问外部变量 console.log(count);}return inner;}const counter =outer();counter();// 输出 1counter();// 输出 22. 闭包的核心原理
- 作用域链:函数在定义时,会记住自己的词法环境(即外部作用域)。当内部函数访问变量时,会沿着作用域链向上查找。
- 变量持久化:闭包使得外部函数的变量不会被垃圾回收,因为内部函数仍持有对它们的引用
3. 闭包的常见用途
3.1 私有变量封装
通过闭包隐藏内部变量,仅暴露操作接口:
functioncreateCounter(){let count =0;// 私有变量return{increment:()=> count++,getValue:()=> count };}const counter =createCounter(); counter.increment(); console.log(counter.getValue());// 输出 1// 无法直接访问 count,避免被外部修改3.2 函数柯里化(Currying)
将多参数函数转换为单参数链式调用:
functionadd(a){returnfunction(b){return a + b;};}const add5 =add(5); console.log(add5(3));// 输出 83.3 事件处理与回调
在事件监听中保留上下文变量:
functionsetupButton(){const button = document.getElementById('myButton');let clicks =0; button.addEventListener('click',function(){ clicks++; console.log(`按钮被点击了 ${clicks} 次`);});}// 每次点击都会更新同一个 clicks 变量4. 闭包的陷阱与解决方案
4.1 循环中的闭包问题
问题:循环中创建的闭包共享同一个变量,导致意外结果。
for(var i =0; i <3; i++){setTimeout(function(){ console.log(i);// 输出 3, 3, 3},100);}解决方案:使用 IIFE 或 let 创建块级作用域。
// 使用 IIFEfor(var i =0; i <3; i++){(function(j){setTimeout(function(){ console.log(j);// 输出 0, 1, 2},100);})(i);}// 使用 let(块级作用域)for(let i =0; i <3; i++){setTimeout(function(){ console.log(i);// 输出 0, 1, 2},100);}4.2 内存泄漏
问题:闭包长期持有外部变量引用,导致内存无法释放。
functionheavyProcess(){const largeData =newArray(1000000).fill('data');returnfunction(){ console.log(largeData.length);};}const leak =heavyProcess();// largeData 被闭包引用,无法回收解决方案:在不需要时解除引用。
leak =null;// 手动解除对闭包的引用5. 闭包面试题示例
题目:以下代码输出什么?为什么?
functioncreateFunctions(){const result =[];for(var i =0; i <3; i++){ result.push(function(){ console.log(i);});}return result;}const funcs =createFunctions(); funcs[0]();// 输出 3 funcs[1]();// 输出 3 funcs[2]();// 输出 3答案:所有函数都输出 3,因为它们共享同一个变量 i(var 声明的变量在函数作用域中)。
改进方法:使用 let 或闭包隔离作用域。
事件循环(Event Loop)
定义
事件循环是JavaScript处理异步任务的核心机制。由于JavaScript是单线程语言,事件循环通过任务队列(Task Queue)和调用栈(Call Stack)的协作,实现了非阻塞的异步执行模型。
核心概念
- 调用栈(Call Stack)
- 用于存储函数调用的栈结构,遵循“后进先出”原则。
- 当函数执行时,会被推入调用栈;执行完毕后,会从调用栈中弹出。
- 任务队列(Task Queue)
- 用于存储异步任务的回调函数。
- 任务队列分为宏任务队列和微任务队列。
- 宏任务(Macro Task)与微任务(Micro Task)
- 宏任务:包括
setTimeout、setInterval、I/O操作、UI渲染等。 - 微任务:包括
Promise.then、MutationObserver、process.nextTick(Node.js)等。
- 宏任务:包括
事件循环的执行流程
- 同步任务执行:
- 同步任务直接进入调用栈执行,直到调用栈为空。
- 微任务执行:
- 调用栈为空后,事件循环会检查微任务队列,依次执行所有微任务,直到微任务队列为空。
- 宏任务执行:
- 每次从宏任务队列中取出一个任务执行,执行完毕后再次检查微任务队列并执行所有微任务。
- UI渲染:
- 如果需要进行UI渲染,浏览器会在宏任务执行后执行渲染操作。
- 循环继续:
- 重复上述步骤,直到所有任务队列为空。
事件循环示例
console.log('1');// 同步任务 setTimeout(()=>{ console.log('2');// 宏任务 },0); Promise.resolve().then(()=>{ console.log('3');// 微任务 }); console.log('4');// 同步任务 // 输出顺序:1 → 4 → 3 → 2 执行步骤解析:
- 同步任务
console.log('1')和console.log('4')依次执行。 - 微任务
Promise.then回调执行,输出3。 - 宏任务
setTimeout回调执行,输出2。
应用场景
- 异步编程:
- 通过
Promise、async/await处理异步任务,避免回调地狱。
- 通过
- 性能优化:
- 将耗时任务拆分为多个微任务,避免阻塞主线程。
- 动画与渲染:
- 使用
requestAnimationFrame结合事件循环实现流畅的动画效果。
- 使用
注意事项
- 微任务优先级高于宏任务:
- 微任务会在当前宏任务执行完毕后立即执行,而宏任务需要等待下一轮事件循环。
- 避免阻塞主线程:
- 长时间运行的同步任务会导致页面卡顿,应将其拆分为异步任务。
- 理解不同环境的事件循环:
- 浏览器与Node.js的事件循环机制略有不同,Node.js引入了
process.nextTick和setImmediate。
- 浏览器与Node.js的事件循环机制略有不同,Node.js引入了
总结
事件循环是JavaScript异步编程的核心机制,通过调用栈、任务队列和事件循环的协作,实现了非阻塞的执行模型。理解事件循环的执行顺序(同步任务 → 微任务 → 宏任务)是掌握异步编程的关键。在实际开发中,合理利用事件循环机制可以提升代码性能和用户体验。
BFC(块级格式化上下文)
定义
BFC(Block Formatting Context,块级格式化上下文)是Web页面渲染时的一种布局环境。它是一个独立的渲染区域,内部的元素布局不会影响外部元素。
触发条件
以下CSS属性可以触发BFC:
- 根元素(
<html>)。 -
float 值不为none。 -
position 值为absolute 或fixed。 -
display 值为inline-block、table-cell、table-caption、flex、inline-flex、grid、inline-grid。 -
overflow 值不为visible(如hidden、auto、scroll)。
特性
- 内部元素垂直排列: BFC内部的块级元素会按照垂直方向依次排列。
- 避免外边距重叠: BFC可以阻止相邻元素的外边距(margin)重叠。
- 包含浮动元素: BFC可以包含浮动元素,避免父元素高度塌陷。
- 阻止元素被浮动元素覆盖: BFC区域不会与浮动元素重叠。
应用场景
- 隔离布局BFC内部的布局不会影响外部元素,适合实现独立的UI组件。
实现多栏布局利用BFC阻止元素被浮动元素覆盖,实现多栏布局:
.left{float: left;width: 200px;}.right{overflow: hidden;/* 触发BFC */}避免外边距重叠相邻元素的外边距会重叠,通过触发BFC可以避免:
.box{display: inline-block;/* 触发BFC */}清除浮动当父元素包含浮动子元素时,父元素的高度会塌陷。通过触发BFC可以解决此问题:
.parent{overflow: hidden;/* 触发BFC */}代码示例
实现多栏布局
<divclass="left">左侧栏</div><divclass="right">右侧栏</div><style>.left{float: left;width: 200px;background: #ccc;}.right{overflow: hidden;/* 触发BFC */background: #f0f0f0;}</style>避免外边距重叠
<divclass="box"style="margin: 20px;">元素1</div><divclass="box"style="margin: 20px;">元素2</div><style>.box{display: inline-block;/* 触发BFC */width: 100%;}</style>清除浮动
<divclass="parent"><divclass="child"style="float: left;">浮动元素</div></div><style>.parent{overflow: hidden;/* 触发BFC */border: 1px solid #000;}</style>注意事项
- 性能影响: 过度使用BFC(如大量使用
overflow: hidden)可能会导致性能问题。 - 兼容性: 不同浏览器对BFC的实现可能略有差异,需进行兼容性测试。
- 合理使用: BFC适用于特定场景,不应滥用。例如,清除浮动优先使用
clearfix方法。
总结
BFC是CSS布局中的重要概念,通过触发BFC可以解决浮动、外边距重叠等问题,实现更灵活的布局。理解BFC的触发条件和特性,有助于编写更健壮和可维护的CSS代码。在实际开发中,应根据需求合理使用BFC,避免过度依赖。
内存泄漏
定义
内存泄漏(Memory Leak)是指程序中已不再使用的内存未被释放,导致内存占用持续增加,最终可能引发性能下降甚至崩溃。在JavaScript中,内存泄漏通常由不当的引用管理引起。
内存管理机制
- 垃圾回收(GC) JavaScript通过垃圾回收机制自动管理内存,主要算法包括:
- 引用计数:记录对象的引用次数,当引用数为0时回收内存。
- 标记清除:从根对象(如
window)出发,标记所有可达对象,清除未标记的对象。
- 常见回收策略
- 新生代:存放短生命周期对象,使用Scavenge算法。
- 老生代:存放长生命周期对象,使用标记清除或标记整理算法。
常见的内存泄漏场景
未释放的缓存或 Map缓存或Map中存储的对象未及时清理,会导致内存占用持续增加。
let cache =newMap();functionsetCache(key, value){ cache.set(key, value);}// 未清理cache,内存泄漏 DOM 引用未清除保存了DOM元素的引用,即使元素被移除,内存也无法释放。
let elements ={button: document.getElementById('button'),}; document.body.removeChild(elements.button);// DOM已移除,但引用仍在 闭包引用闭包会保留对外部作用域的引用,如果闭包未释放,相关内存也无法回收。
functioncreateClosure(){let largeData =newArray(1000000).fill('data');return()=> console.log(largeData);// largeData一直被引用 }const closure =createClosure();未清理的定时器或回调函数定时器或事件监听器未及时清除,会导致相关对象无法回收。
let data =fetchData();setInterval(()=>{process(data);// data一直被引用,无法回收 },1000);意外全局变量未使用var、let或const声明的变量会挂载到全局对象(如window),导致内存无法释放。
functionleak(){ globalVar ='这是一个全局变量';// 未使用var/let/const }内存泄漏的检测与排查
- 浏览器开发者工具
- Memory面板:使用Heap Snapshot分析内存占用,查找未释放的对象。
- Performance面板:记录内存使用情况,观察内存占用是否持续增加。
- 工具库
- Chrome DevTools:提供内存分析功能。
- Node.js内存分析工具:如
node --inspect结合Chrome DevTools。
- 代码检查
- 检查全局变量、定时器、闭包、DOM引用等常见问题。
避免内存泄漏的最佳实践
- 清除定时器、事件监听器、DOM引用等。
- 使用
WeakMap或WeakSet存储临时数据,避免强引用导致内存无法释放。 - 避免在闭包中保留不必要的引用。
- 定期清理缓存或设置缓存失效策略。
清理缓存
let cache =newMap();functionsetCache(key, value, ttl){ cache.set(key, value);setTimeout(()=> cache.delete(key), ttl);// 设置缓存失效时间 }优化闭包
functioncreateClosure(){let largeData =newArray(1000000).fill('data');return()=>{ console.log('Closure executed');// 不引用largeData };}使用弱引用
let weakMap =newWeakMap();let key ={}; weakMap.set(key,'data');// key被回收时,数据也会被回收 及时清除引用
let timer =setInterval(()=>{},1000);clearInterval(timer);// 清除定时器 总结
内存泄漏是JavaScript开发中的常见问题,通常由不当的引用管理引起。通过理解垃圾回收机制、熟悉常见的内存泄漏场景,并借助开发者工具进行排查,可以有效避免内存泄漏问题。在实际开发中,遵循最佳实践(如及时清除引用、使用弱引用等)是保证应用性能的关键。
Vue 的虚拟 DOM
定义
虚拟 DOM(Virtual DOM)是一种用 JavaScript 对象表示真实 DOM 结构的技术。Vue 通过虚拟 DOM 实现高效的 DOM 更新,减少直接操作真实 DOM 的开销。
工作原理
- 生成虚拟 DOM
- Vue 将模板编译为渲染函数,渲染函数执行后生成虚拟 DOM 树。
- 虚拟 DOM 树是一个 JavaScript 对象,描述了真实 DOM 的结构和属性。
- Diff 算法
- 当数据变化时,Vue 会生成新的虚拟 DOM 树,并与旧的虚拟 DOM 树进行对比(Diff 算法)。
- Diff 算法找出两棵树之间的差异,只更新发生变化的部分。
- 更新真实 DOM
- 根据 Diff 算法的结果,Vue 将变化应用到真实 DOM 上,完成视图更新。
优点
- 性能优化
- 减少直接操作真实 DOM 的次数,避免频繁的 DOM 操作带来的性能损耗。
- 通过批量更新和差异更新,提高渲染效率。
- 跨平台能力
- 虚拟 DOM 是一个抽象层,可以映射到不同的平台(如浏览器、移动端、服务器端)。
- 简化开发
- 开发者只需关注数据逻辑,无需手动操作 DOM,提高开发效率。
实现细节
- Diff 算法核心逻辑
- 同层比较:只比较同一层级的节点,不跨层级比较。
- Key 值优化:通过
key 属性识别节点,避免不必要的节点销毁和重建。 - 节点复用:如果节点类型和
key 值相同,则复用节点,只更新属性或子节点。
- 批量更新Vue 将多次数据变化合并为一次更新,减少重复渲染。
虚拟 DOM 的结构虚拟 DOM 是一个 JavaScript 对象,包含标签名、属性、子节点等信息。
const vnode ={tag:'div',attrs:{id:'app'},children:[{tag:'p',attrs:{},children:['Hello, Vue!']}]};应用场景
- 复杂视图更新在数据频繁变化的场景中,虚拟 DOM 可以显著提升性能。
- 跨平台开发虚拟 DOM 可以映射到不同的平台,如通过
Weex 开发移动端应用。 - 服务端渲染(SSR) 虚拟 DOM 可以在服务器端生成 HTML 字符串,提高首屏加载速度。
局限性
- 首次渲染性能虚拟 DOM 需要在首次渲染时生成虚拟 DOM 树,相比直接操作真实 DOM,会有一定的性能开销。
- 内存占用虚拟 DOM 需要维护一份虚拟 DOM 树,会增加内存占用。
- 不适合简单场景在简单的静态页面中,虚拟 DOM 的优势不明显,反而会增加复杂性。
MVVM
定义
MVVM(Model-View-ViewModel)是一种软件架构模式,主要用于分离 UI 逻辑与业务逻辑。它将应用程序分为三个核心部分:
- Model:数据模型,负责管理应用程序的数据和业务逻辑。
- View:视图,负责呈现用户界面(UI)。
- ViewModel:视图模型,负责连接 View 和 Model,处理 UI 逻辑和数据绑定。
核心概念
- 数据绑定
- View 和 ViewModel 之间通过数据绑定机制实现双向通信。
- 当 Model 数据变化时,ViewModel 自动更新 View;当用户操作 View 时,ViewModel 自动更新 Model。
- 命令绑定
- View 中的用户操作(如点击按钮)通过命令绑定触发 ViewModel 中的方法。
- 依赖注入
- ViewModel 可以依赖外部服务(如 API 调用、数据存储),通过依赖注入实现解耦。
工作流程
- 用户操作 View用户在 View 中进行操作(如输入文本、点击按钮)。
- ViewModel 处理逻辑ViewModel 接收用户操作,更新 Model 或执行相关逻辑。
- Model 更新数据Model 负责处理业务逻辑和数据更新。
- View 自动更新ViewModel 将 Model 的变化通过数据绑定同步到 View,更新 UI。
优点
- 分离关注点
- 将 UI 逻辑与业务逻辑分离,提高代码的可维护性和可测试性。
- 双向数据绑定
- 自动同步 View 和 Model 的数据,减少手动操作 DOM 的代码。
- 提高开发效率
- 通过数据绑定和命令绑定,减少重复代码,简化开发流程。
- 易于测试
- ViewModel 可以独立于 View 进行测试,提高测试覆盖率。
缺点
- 学习成本高
- MVVM 涉及的概念(如数据绑定、命令绑定)需要一定的学习成本。
- 性能开销
- 数据绑定机制可能会带来一定的性能开销,特别是在大规模数据更新时。
- 框架依赖
- MVVM 通常依赖于特定的框架(如 Vue、Angular),增加了项目的技术栈复杂度。
应用场景
- 前端框架
- Vue、Angular、Knockout 等前端框架都采用了 MVVM 模式。
- 复杂 UI 应用
- 在需要频繁更新 UI 的复杂应用中,MVVM 可以提高开发效率和性能。
- 跨平台开发
- 通过 MVVM 模式,可以实现代码的跨平台复用(如 Web、移动端)。
Vue2 与 Vue3 响应式原理
Vue2 的响应式原理
Vue2 使用 Object.defineProperty 实现响应式,其核心机制如下:
- 数据劫持
- 通过
Object.defineProperty 劫持对象的属性,定义getter 和setter。 - 当访问属性时触发
getter,当修改属性时触发setter。
- 通过
- 依赖收集
- 在
getter 中收集依赖(Watcher),在setter 中通知依赖更新。
- 在
- 数组处理
- Vue2 无法直接劫持数组的变化,因此通过重写数组方法(如
push、pop 等)实现响应式。
- Vue2 无法直接劫持数组的变化,因此通过重写数组方法(如
示例:
const data ={name:'Vue2'}; Object.defineProperty(data,'name',{get(){ console.log('获取 name');returnthis._name;},set(newValue){ console.log('更新 name');this._name = newValue;}});局限性:
- 无法检测新增/删除属性: 使用
Vue.set 或Vue.delete 解决。 - 数组响应式局限性: 无法检测通过索引直接修改数组元素(如
arr = 1)。
Vue3 的响应式原理
Vue3 使用 Proxy 实现响应式,其核心机制如下:
- 数据代理
- 通过
Proxy 代理整个对象,拦截对对象的所有操作(如读取、赋值、删除等)。
- 通过
- 依赖收集
- 在
get 拦截器中收集依赖(Effect),在set 拦截器中通知依赖更新。
- 在
- 数组处理
-
Proxy 可以直接拦截数组的变化,无需重写数组方法。
-
示例:
const data ={name:'Vue3'};const proxy =newProxy(data,{get(target, key){ console.log('获取', key);return target[key];},set(target, key, newValue){ console.log('更新', key); target[key]= newValue;returntrue;}});优势:
- 全面拦截: 支持新增/删除属性、数组索引操作等。
- 性能提升:
Proxy 是语言层面的特性,性能优于Object.defineProperty。 - 简化代码: 无需处理特殊 API(如
Vue.set)。
Vue2 与 Vue3 响应式原理的对比
| 特性 | Vue2(Object.defineProperty) | Vue3(Proxy) |
|---|---|---|
| 数据劫持方式 | 劫持对象的属性 | 代理整个对象 |
| 新增/删除属性 | 不支持 | 支持 |
| 数组响应式 | 需重写数组方法 | 直接拦截数组操作 |
| 性能 | 较低 | 较高 |
| 代码复杂度 | 较高 | 较低 |
Vue3 的其他响应式优化
- Ref 和 Reactive
-
ref:用于包装基本数据类型,通过.value 访问。 -
reactive:用于包装对象,直接访问属性。
-
- Effect 代替 Watcher
- Vue3 使用
Effect 作为依赖单元,更加灵活和高效。
- Vue3 使用
- Tree-shaking 支持
- Vue3 的响应式模块支持 Tree-shaking,减少打包体积。
代码示例对比
Vue3 响应式
const data ={name:'Vue3'};const proxy =newProxy(data,{get(target, key){ console.log('获取', key);return target[key];},set(target, key, newValue){ console.log('更新', key); target[key]= newValue;returntrue;}});Vue2 响应式
const data ={name:'Vue2'}; Object.defineProperty(data,'name',{get(){ console.log('获取 name');returnthis._name;},set(newValue){ console.log('更新 name');this._name = newValue;}});深拷贝与浅拷贝
拷贝的定义
- 浅拷贝(Shallow Copy)
- 只复制对象的第一层属性,如果属性是引用类型(如对象、数组),则复制引用地址,新旧对象共享引用类型的属性。
- 深拷贝(Deep Copy)
- 递归复制对象的所有层级,新旧对象完全独立,不共享任何属性。
浅拷贝的实现方式
- 数组的浅拷贝方法
slice、concat:
const arr =[1,2,{a:3}];const shallowCopy = arr.slice();Object.assign
const obj ={a:1,b:{c:2}};const shallowCopy = Object.assign({}, obj);**扩展运算符(** ... )
const obj ={a:1,b:{c:2}};const shallowCopy ={...obj };深拷贝的实现方式
- 通过 JSON 序列化实现深拷贝,但有以下局限性:
- 无法复制函数、
undefined、Symbol等特殊类型。 - 无法处理循环引用。
- 无法复制函数、
- 手动实现深拷贝,支持所有数据类型并处理循环引用。
- 使用第三方库
lodash 的 cloneDeep 方法:
import _ from'lodash';const obj ={a:1,b:{c:2}};const deepCopy = _.cloneDeep(obj);递归实现深拷贝
functiondeepClone(obj, map =newMap()){if(typeof obj !=='object'|| obj ===null)return obj;if(map.has(obj))return map.get(obj);// 处理循环引用 const clone = Array.isArray(obj)?[]:{}; map.set(obj, clone);for(let key in obj){if(obj.hasOwnProperty(key)){ clone[key]=deepClone(obj[key], map);}}return clone;}JSON.parse(JSON.stringify(obj))
const obj ={a:1,b:{c:2}};const deepCopy =JSON.parse(JSON.stringify(obj));浅拷贝与深拷贝的对比
| 特性 | 浅拷贝 | 深拷贝 |
|---|---|---|
| 拷贝层级 | 仅第一层 | 所有层级 |
| 引用类型属性 | 共享引用地址 | 完全独立 |
| 性能 | 较快 | 较慢(递归或序列化开销) |
| 适用场景 | 简单对象,无需深层复制 | 复杂对象,需完全独立副本 |
代码示例
深拷贝示例
const obj ={a:1,b:{c:2}};const deepCopy =JSON.parse(JSON.stringify(obj)); deepCopy.b.c =3; console.log(obj.b.c);// 输出:2(完全独立) 浅拷贝示例
const obj ={a:1,b:{c:2}};const shallowCopy ={...obj }; shallowCopy.b.c =3; console.log(obj.b.c);// 输出:3(共享引用) 深拷贝的注意事项
- 循环引用问题如果对象存在循环引用(如
obj.a = obj),需要额外处理,否则会导致栈溢出。 - 特殊数据类型如
Function、RegExp、Map、Set 等,需要特殊处理。 - 性能问题深拷贝的性能开销较大,尤其是在处理大型对象时,需谨慎使用。
总结
- 浅拷贝:适用于简单对象,仅复制第一层属性,性能较高。
- 深拷贝:适用于复杂对象,递归复制所有层级,确保对象完全独立,但性能较低。
- 选择建议:
- 如果对象结构简单且无需深层复制,使用浅拷贝。
- 如果对象结构复杂或需要完全独立副本,使用深拷贝。
- 处理循环引用或特殊数据类型时,建议使用递归实现或第三方库(如
lodash)。
npm 开发依赖与生产依赖
在 npm 项目中,依赖包分为 开发依赖 和 生产依赖,分别用于不同的场景。以下是它们的定义、区别及使用方式:
开发依赖(devDependencies)
- 定义
- 仅在开发环境中使用的依赖包,例如构建工具、测试框架、代码格式化工具等。
- 这些依赖不会随项目发布到生产环境。
- 安装方式
- 常见开发依赖
- 测试工具:
jest、mocha、chai - 构建工具:
webpack、vite、babel - 代码格式化:
eslint、prettier - 类型检查:
typescript
- 测试工具:
示例在 package.json 中:
"devDependencies":{"eslint":"^8.0.0","webpack":"^5.0.0","jest":"^27.0.0"}使用 --save-dev 或 -D 选项将包安装为开发依赖:
npminstall<package> --save-dev 或
npminstall<package>-D生产依赖(dependencies)
- 定义
- 项目运行所必需的依赖包,例如框架、库、工具等。
- 这些依赖会随项目发布到生产环境。
- 安装方式
- 常见生产依赖
- 框架:
express、koa、nestjs - UI 库:
react、vue、angular - 工具库:
lodash、axios、moment
- 框架:
示例在 package.json 中:
"dependencies":{"express":"^4.0.0","lodash":"^4.0.0","react":"^18.0.0"}如果省略选项,默认安装为生产依赖:
npminstall<package>使用 --save 或 -S 选项将包安装为生产依赖:
npminstall<package>--save或
npminstall<package>-S开发依赖与生产依赖的区别
| 特性 | 开发依赖(devDependencies) | 生产依赖(dependencies) |
|---|---|---|
| 使用环境 | 仅用于开发环境 | 用于生产环境 |
| 是否发布到生产 | 否 | 是 |
| 安装命令 | npm install <package> --save-dev | npm install <package> --save |
| 示例工具 | eslint、webpack、jest | express、react、lodash |
如何选择依赖类型
- 开发依赖
- 如果包仅在开发阶段使用(如测试、构建、格式化等),则安装为开发依赖。
- 生产依赖
- 如果包是项目运行所必需的(如框架、库、工具等),则安装为生产依赖。
- 特殊情况
-
peerDependencies:用于插件或库开发,指定兼容的宿主包。 -
optionalDependencies:可选依赖,安装失败不影响项目运行。
-
最佳实践
- 明确区分依赖类型
- 开发依赖与生产依赖应严格区分,避免将不必要的包发布到生产环境。
- 使用
package-lock.json- 锁定依赖版本,确保团队环境一致。
- 定期更新依赖
- 使用
npm outdated 检查过期依赖,定期更新以修复漏洞。
- 使用
- 清理无用依赖
- 使用
npm prune 或npm uninstall 清理未使用的依赖。
- 使用
package.json 中 ^ 和 ~ 的区别
在 package.json 中,^ 和 ~ 是用于定义依赖版本范围的符号。它们决定了 npm 或 yarn 在安装或更新依赖时允许的版本范围。以下是它们的详细区别:
版本号格式
- 语义化版本号(SemVer)
- 格式:
主版本号.次版本号.修订版本号 - 示例:
1.2.3-
1:主版本号(Major),不兼容的 API 变更。 -
2:次版本号(Minor),向后兼容的功能新增。 -
3:修订版本号(Patch),向后兼容的问题修复。
-
- 格式:
- 版本范围符号
-
^ 和~ 是定义版本范围的前缀符号。
-
^ 的含义
- 定义
- 允许更新到最新的次版本和修订版本,但不更新主版本。
- 即:保持主版本号不变,次版本号和修订版本号可以更新。
- 规则
- 如果版本号是
^1.2.3,则允许更新的版本范围是>=1.2.3 <2.0.0。
- 如果版本号是
- 示例
-
^1.2.3:允许更新到1.3.0、1.4.0,但不允许更新到2.0.0。 -
^0.2.3:允许更新到0.2.4、0.3.0,但不允许更新到1.0.0。 -
^0.0.3:仅允许更新到0.0.4,不允许更新到0.1.0。
-
~ 的含义
- 定义
- 允许更新到最新的修订版本,但不更新次版本和主版本。
- 即:保持主版本号和次版本号不变,修订版本号可以更新。
- 规则
- 如果版本号是
~1.2.3,则允许更新的版本范围是>=1.2.3 <1.3.0。
- 如果版本号是
- 示例
-
~1.2.3:允许更新到1.2.4、1.2.5,但不允许更新到1.3.0。 -
~0.2.3:允许更新到0.2.4、0.2.5,但不允许更新到0.3.0。 -
~0.0.3:仅允许更新到0.0.4,不允许更新到0.1.0。
-
^ 和 ~ 的区别
| 特性 | ^(兼容版本) | ~(修订版本) |
|---|---|---|
| 更新范围 | 主版本不变,次版本和修订版本可更新 | 主版本和次版本不变,仅修订版本可更新 |
| 示例 | ^1.2.3 允许更新到 1.3.0 | ~1.2.3 允许更新到 1.2.4 |
| 适用场景 | 希望自动获取新功能,但避免破坏性变更 | 仅希望修复问题,不引入新功能 |
如何选择
- 使用
^ 的场景- 希望获取新功能和问题修复,但避免不兼容的变更。
- 适用于大多数依赖包。
- 使用
~ 的场景- 仅希望获取问题修复,不引入新功能。
- 适用于对稳定性要求较高的项目。
- 固定版本
- 如果希望完全锁定版本,避免任何更新,可以直接指定版本号(如
1.2.3)。
- 如果希望完全锁定版本,避免任何更新,可以直接指定版本号(如
示例
- 允许更新的版本范围:
>=4.17.21 <5.0.0。 - 允许更新的版本范围:
>=4.17.21 <4.18.0。 - 仅允许使用
4.17.21 版本。
固定版本示例
"dependencies":{"lodash":"4.17.21"} ~ 示例
"dependencies":{"lodash":"~4.17.21"} ^ 示例
"dependencies":{"lodash":"^4.17.21"}package.json 中的各种 dependencies
package.json 是 Node.js 项目的核心配置文件,用于管理项目的依赖、脚本、版本等信息。其中,dependencies 是定义项目依赖的关键部分。以下是 package.json 中各种依赖类型的详细说明:
主要依赖类型
-
dependencies- 定义:项目运行所必需的依赖包。
- 安装方式:
npm install <package> 或yarn add <package>。
-
devDependencies- 定义:仅用于开发环境的依赖包(如测试工具、构建工具等)。
- 安装方式:
npm install <package> --save-dev 或yarn add <package> --dev。
-
peerDependencies- 定义:与当前包兼容的宿主包,通常用于插件或库的开发。
- 特点:不会自动安装,需要用户手动安装。
-
optionalDependencies- 定义:可选的依赖包,即使安装失败也不会影响项目运行。
- 特点:优先级高于
dependencies,如果安装失败会静默忽略。
-
bundledDependencies- 定义:打包发布时需要包含的依赖包(通常是一个数组)。
- 特点:与
dependencies 和devDependencies 不同,需要手动列出包名。
示例:
"bundledDependencies":["lodash","express"]示例:
"optionalDependencies":{"fsevents":"^2.3.2"}示例:
"peerDependencies":{"react":">=16.8.0"}示例:
"devDependencies":{"eslint":"^7.32.0","webpack":"5.51.1"}示例:
"dependencies":{"lodash":"^4.17.21","express":"4.17.1"}ES6 模块化
ES6(ECMAScript 2015)引入了官方的模块化语法,提供了 import 和 export 关键字来实现模块的导入和导出。以下是 ES6 模块化的核心概念和用法:
定义
模块化是将代码拆分为独立模块的开发方式,每个模块具有独立的作用域,通过导入和导出实现模块间的依赖管理。ES6 模块化具有以下特点:
- 静态加载:模块的依赖关系在编译时确定,支持静态分析和优化。
- 独立作用域:每个模块拥有独立的作用域,避免全局污染。
- 严格模式:模块默认运行在严格模式下。
模块的导出(export)
- 使用
export 关键字导出变量、函数或类。 - 导入时需要使用相同的名称。
- 使用
export default 导出模块的默认值。 - 导入时可以使用任意名称。
- 同时使用命名导出和默认导出。
混合导出
// module.js exportconst name ='ES6';exportdefaultfunctiongreet(){ console.log('Hello, ES6!');}默认导出
// module.js const name ='ES6';exportdefault name;命名导出
// module.js exportconst name ='ES6';exportfunctiongreet(){ console.log('Hello, ES6!');}模块的导入(import)
- 使用
import { ... } 导入命名导出。 - 使用
import ... from 导入默认导出。 - 使用
import * as ... 导入模块的所有导出。 - 使用
import() 动态加载模块,返回一个 Promise。
动态导入
// app.js import('./module.js').then(module=>{ console.log(module.name);// 输出:ES6 });导入全部导出
// app.js import*as module from'./module.js'; console.log(module.name);// 输出:ES6 module.greet();// 输出:Hello, ES6! 导入默认导出
// app.js import myModule from'./module.js'; console.log(myModule);// 输出:ES6 导入命名导出
// app.js import{ name, greet }from'./module.js'; console.log(name);// 输出:ES6 greet();// 输出:Hello, ES6! 模块化的优势
- 代码组织
- 将代码拆分为独立模块,提高代码的可读性和可维护性。
- 依赖管理
- 明确模块间的依赖关系,避免全局变量污染。
- 静态分析
- 支持静态分析和优化,如 Tree-shaking(移除未使用的代码)。
- 复用性
- 模块可以复用,减少重复代码。
模块化的使用场景
- 前端开发
- 使用 ES6 模块化组织前端代码,结合打包工具(如 Webpack、Vite)进行构建。
- Node.js 开发
- 在 Node.js 中使用 ES6 模块化(需将文件扩展名改为
.mjs 或在package.json 中设置"type": "module")。
- 在 Node.js 中使用 ES6 模块化(需将文件扩展名改为
- 库开发
- 开发独立的库或工具,通过模块化提供清晰的 API。
注意事项
- 现代浏览器支持原生 ES6 模块化,但需在
<script> 标签中添加type="module" 属性。 - Node.js 支持
- 在 Node.js 中使用 ES6 模块化时,需将文件扩展名改为
.mjs 或在package.json 中设置"type": "module"。
- 在 Node.js 中使用 ES6 模块化时,需将文件扩展名改为
- 文件路径
- 导入模块时需使用完整的文件路径(包括扩展名),或使用构建工具处理路径别名。
浏览器支持
<scripttype="module"src="app.js"></script>Vue 中 key 的作用及为什么不能用 index 作为 key 详解
key 的作用
在 Vue 中,key 是用于标识虚拟 DOM 元素的特殊属性,其主要作用包括:
- 唯一标识
-
key 用于唯一标识每个虚拟 DOM 节点,帮助 Vue 识别哪些节点是新增的、删除的或需要更新的。
-
- 优化渲染性能
- 在列表渲染时,
key 可以帮助 Vue 更高效地复用和更新 DOM 节点,减少不必要的 DOM 操作。
- 在列表渲染时,
- 维持组件状态
- 在动态组件或条件渲染中,
key 可以确保组件在切换时正确销毁和重建,避免状态混乱。
- 在动态组件或条件渲染中,
key 的使用场景
- 在
v-for 中使用key 标识每个列表项。 - 在动态组件中使用
key 确保组件正确切换。 - 在条件渲染中使用
key 确保元素正确销毁和重建。
条件渲染
<template> <div v-if="show" key="a">内容A</div> <div v-else key="b">内容B</div> </template> 动态组件
<template> <component :is="currentComponent" :key="currentComponent"></component> </template> 列表渲染
<template> <ul> <li v-for="item in items" :key="item.id">{{ item.name }}</li> </ul> </template> 为什么不能用 index 作为 key
在列表渲染中,使用 index 作为 key 可能会导致以下问题:
- 无法正确复用节点
-
index 是数组的下标,当列表顺序变化时,index 会重新分配,导致 Vue 无法正确复用 DOM 节点,降低渲染性能。
-
- 状态混乱
- 如果列表项是带有状态的组件,使用
index 作为key 会导致状态错乱。例如,删除中间项后,后续项的index 会发生变化,状态会被错误地保留。
- 如果列表项是带有状态的组件,使用
- 示例问题
- 如果删除
B,列表变为['A', 'C'],index 会重新分配:-
A 的key 仍然是0。 -
C 的key 从2 变为1。
-
- 由于
key 变化,Vue 会销毁并重新创建C 节点,而不是复用原来的节点。
- 如果删除
假设有一个列表 ['A', 'B', 'C'],使用 index 作为 key:
<template> <ul> <li v-for="(item, index) in items" :key="index">{{ item }}</li> </ul> </template> 正确的 key 使用方式
- 使用列表项的唯一标识(如
id)作为key。 - 避免动态生成
key- 避免使用随机数或时间戳作为
key,否则会导致节点频繁销毁和重建。
- 避免使用随机数或时间戳作为
使用唯一标识
<template> <ul> <li v-for="item in items" :key="item.id">{{ item.name }}</li> </ul> </template> CSS 选择器优先级
CSS 选择器优先级决定了当多个规则应用于同一个元素时,哪条规则会生效。以下是优先级规则及其计算方式的详细说明:
优先级规则
CSS 选择器优先级由 4 个级别组成,按权重从高到低依次为:
- 直接在 HTML 元素中通过
style 属性定义的样式。 - 使用
# 定义的 ID 选择器。 - 类选择器(
.class)、属性选择器([type="text"])、伪类选择器(:hover)。 - 元素选择器(
div)、伪元素选择器(::before)。
元素选择器、伪元素选择器(权重:1)
div{color: black;}::before{content:' ';}类选择器、属性选择器、伪类选择器(权重:10)
.myClass{color: green;}[type="text"]{color: yellow;}a:hover{color: purple;}ID 选择器(权重:100)
#myId{color: blue;}内联样式(权重:1000)
<divstyle="color: red;">Hello</div>优先级计算方式
- 计算规则
- 将选择器的权重相加,得到总权重值。
- 总权重值越高,优先级越高。
- 第一条规则的总权重最高,因此会生效。
- 注意事项
- 相同权重:如果优先级相同,后定义的样式会覆盖前面的样式。
!important:在样式声明后添加 !important 可以覆盖所有优先级。
div{color: red !important;}示例
#myId .myClass div{color: red;}/* 权重:100 + 10 + 1 = 111 */.myClass div{color: blue;}/* 权重:10 + 1 = 11 */div{color: green;}/* 权重:1 */优先级示例
- 最终颜色为
blue,因为 ID 选择器的优先级最高。 - 最终颜色为
green,因为类选择器的优先级高于元素选择器。 - 最终颜色为
red,因为内联样式的优先级最高。
示例 3
<divstyle="color: red;">Hello</div>div{color: green;}/* 权重:1 */示例 2
<divclass="myClass">Hello</div>.myClass{color: green;}/* 权重:10 */div{color: red;}/* 权重:1 */示例 1
<divid="myId"class="myClass">Hello</div>#myId{color: blue;}/* 权重:100 */.myClass{color: green;}/* 权重:10 */div{color: red;}/* 权重:1 */优先级优化建议
- 避免过度使用
!important-
!important 会破坏优先级规则,增加维护难度。
-
- 减少 ID 选择器的使用
- ID 选择器的优先级过高,可能导致样式难以覆盖。
- 使用类选择器
- 类选择器的优先级适中,适合大多数场景。
- 遵循 CSS 层叠规则
- 相同优先级时,后定义的样式会覆盖前面的样式。
JavaScript 阻塞问题
JavaScript 是单线程语言,意味着它一次只能执行一个任务。如果某个任务耗时较长,可能会导致阻塞,影响页面的响应和渲染。以下是 JavaScript 阻塞问题的详细说明:
JavaScript 阻塞的表现
- 页面渲染阻塞
- 当 JavaScript 代码执行时,浏览器会暂停页面渲染,直到代码执行完毕。
- 例如,长时间运行的脚本会导致页面卡顿或无响应。
- 事件处理阻塞
- 如果 JavaScript 代码执行时间过长,用户的操作(如点击、滚动)可能无法及时响应。
- 网络请求阻塞
- JavaScript 代码的执行会阻塞网络请求的处理,影响数据的加载和显示。
JavaScript 阻塞的原因
- 同步代码会逐行执行,如果某行代码耗时较长,后续代码会被阻塞。
- DOM 操作
- 复杂的 DOM 操作(如大量元素插入、样式计算)会导致页面重排和重绘,影响性能。
- CPU 密集型任务
- 如加密解密、图像处理等任务会占用大量 CPU 资源,导致阻塞。
同步代码执行
for(let i =0; i <1000000000; i++){}// 长时间运行的循环 console.log('执行完毕');// 被阻塞 解决 JavaScript 阻塞的方法
- 使用异步 API(如
setTimeout、Promise、async/await)将耗时任务放到事件循环中执行,避免阻塞主线程。 - 使用 Web Workers 将耗时任务放到后台线程中执行,避免阻塞主线程。
- 将大任务拆分为多个小任务,分批执行,避免长时间占用主线程。
- 使用
DocumentFragment 减少 DOM 操作次数,避免频繁重排和重绘。 - 将动画或渲染任务放到
requestAnimationFrame 中执行,确保与浏览器的渲染周期同步。
使用requestAnimationFrame
functionanimate(){// 执行动画逻辑 requestAnimationFrame(animate);}animate();优化 DOM 操作
const fragment = document.createDocumentFragment();for(let i =0; i <1000; i++){const div = document.createElement('div'); fragment.appendChild(div);} document.body.appendChild(fragment);任务拆分
functionprocessChunk(start, end){for(let i = start; i < end; i++){// 处理任务 }if(end <1000000){setTimeout(()=>processChunk(end, end +1000),0);}}processChunk(0,1000);Web Workers
// 主线程 const worker =newWorker('worker.js'); worker.postMessage('开始任务'); worker.onmessage=(event)=>{ console.log('收到消息:', event.data);};// worker.js self.onmessage=(event)=>{ console.log('收到消息:', event.data); self.postMessage('任务完成');};异步编程
console.log('开始');setTimeout(()=>{ console.log('异步任务');},0); console.log('结束');// 输出顺序:开始 → 结束 → 异步任务 JavaScript 阻塞的示例
异步代码非阻塞
console.log('开始');setTimeout(()=>{for(let i =0; i <1000000000; i++){}// 长时间运行的循环 console.log('异步任务');},0); console.log('结束');// 页面不卡顿,用户操作可响应 同步代码阻塞
console.log('开始');for(let i =0; i <1000000000; i++){}// 长时间运行的循环 console.log('结束');// 页面卡顿,用户操作无响应 总结
JavaScript 的阻塞问题主要由同步代码、复杂 DOM 操作和 CPU 密集型任务引起。通过以下方法可以避免阻塞:
- 使用异步编程(如
Promise、async/await)。 - 使用 Web Workers 将耗时任务放到后台线程。
- 将大任务拆分为多个小任务,分批执行。
- 优化 DOM 操作,减少重排和重绘。
- 使用
requestAnimationFrame 确保动画与渲染同步。
Promise 功能与用法
Promise 是 JavaScript 中用于处理异步操作的对象,它解决了传统回调函数嵌套过深(“回调地狱”)的问题,提供了更清晰和可读性更高的代码结构。
核心概念
- 状态Promise 有三种状态:
- Pending(进行中) :初始状态,表示异步操作尚未完成。
- Fulfilled(已成功) :异步操作成功完成,返回结果值。
- Rejected(已失败) :异步操作失败,返回错误信息。
- 特点
- 不可逆:状态一旦从 Pending 变为 Fulfilled 或 Rejected,就不能再改变。
- 链式调用:通过
.then() 和.catch() 方法实现链式调用,避免回调嵌套。
基本用法
- 使用
.then() 处理成功状态(Fulfilled)。 - 使用
.catch() 处理失败状态(Rejected)。
最终处理使用 .finally() 在 Promise 完成后执行清理操作,无论成功或失败。
promise .then((result)=> console.log('成功:', result)).catch((error)=> console.log('失败:', error)).finally(()=> console.log('操作完成'));链式调用每个 .then() 返回一个新的 Promise,可以继续调用 .then() 或 .catch()。
promise .then((result)=>{ console.log('第一步:', result);return'第二步';}).then((result)=>{ console.log('第二步:', result);}).catch((error)=>{ console.log('失败:', error);});处理结果
promise .then((result)=>{ console.log('成功:', result);}).catch((error)=>{ console.log('失败:', error);});创建 Promise使用 new Promise() 构造函数创建 Promise 对象,传入一个执行器函数(executor)。
const promise =newPromise((resolve, reject)=>{// 异步操作 if(/* 操作成功 */){resolve('成功结果');}else{reject('失败原因');}});Promise 的静态方法
Promise.allSettled() 接收一个 Promise 数组,返回所有 Promise 的结果(无论成功或失败)。
const promises =[ Promise.resolve('任务1'), Promise.reject('任务2失败'),]; Promise.allSettled(promises).then((results)=> console.log('所有结果:', results));Promise.race() 接收一个 Promise 数组,返回第一个完成(无论成功或失败)的 Promise 的结果。
const promises =[newPromise((resolve)=>setTimeout(()=>resolve('任务1'),1000)),newPromise((resolve)=>setTimeout(()=>resolve('任务2'),500)),]; Promise.race(promises).then((result)=> console.log('第一个完成:', result));Promise.all() 接收一个 Promise 数组,当所有 Promise 都成功时返回结果数组,如果有一个失败则立即返回失败原因。
const promises =[ Promise.resolve('任务1'), Promise.resolve('任务2'),]; Promise.all(promises).then((results)=> console.log('全部成功:', results)).catch((error)=> console.log('失败:', error));Promise.reject() 返回一个已失败的 Promise 对象。
const rejectedPromise = Promise.reject('失败'); rejectedPromise.catch((error)=> console.log(error));// 输出:失败 Promise.resolve() 返回一个已成功的 Promise 对象。
const resolvedPromise = Promise.resolve('成功'); resolvedPromise.then((result)=> console.log(result));// 输出:成功 Promise 的实际应用
避免回调地狱使用 Promise 链式调用代替嵌套回调。
functionstep1(){return Promise.resolve('第一步');}functionstep2(data){return Promise.resolve(data +' → 第二步');}step1().then(step2).then((result)=> console.log(result));处理异步操作如网络请求、定时器、文件读取等。
functionfetchData(){returnnewPromise((resolve, reject)=>{setTimeout(()=>{resolve('数据加载完成');},1000);});}fetchData().then((result)=> console.log(result)).catch((error)=> console.log(error));JavaScript 处理大量数据的策略
在处理大量数据时,JavaScript 可能会面临性能瓶颈,如内存占用过高、页面卡顿等问题。以下是处理大量数据的常用策略和优化方法:
优化数据加载
- 将数据分页加载,每次只加载当前页的数据,减少一次性加载的数据量。
- 在用户滚动到页面底部时动态加载更多数据。
- 按需加载
- 只加载用户当前需要的数据,例如根据搜索条件过滤数据。
懒加载
window.addEventListener('scroll',()=>{if(window.scrollY + window.innerHeight >= document.body.offsetHeight){loadMoreData();}});分页加载
functionloadData(page, pageSize){const start =(page -1)* pageSize;const end = start + pageSize;return data.slice(start, end);}const pageData =loadData(1,100);// 加载第一页的100条数据 优化数据处理
- 将数据处理的逻辑放到后台线程中执行,避免阻塞主线程。
- 将大数据集拆分为多个小批次处理,避免一次性处理过多数据。
- 使用高效算法
- 选择时间复杂度更低的算法,例如用哈希表(
Map)替代数组查找。
- 选择时间复杂度更低的算法,例如用哈希表(
分批处理
functionprocessInBatches(data, batchSize, processFn){let index =0;functionnextBatch(){const batch = data.slice(index, index + batchSize);processFn(batch); index += batchSize;if(index < data.length){setTimeout(nextBatch,0);// 下一批次 }}nextBatch();}processInBatches(data,1000,(batch)=>{ console.log('处理批次:', batch);});使用 Web Workers
// 主线程 const worker =newWorker('worker.js'); worker.postMessage(data); worker.onmessage=(event)=>{ console.log('处理结果:', event.data);};// worker.js self.onmessage=(event)=>{const result =processData(event.data);// 处理数据 self.postMessage(result);};优化数据存储
- 对于数值型数据,使用
TypedArray 或DataView 减少内存占用。 - 对数据进行压缩,减少内存占用和网络传输时间。
- 对于需要在客户端存储的大量数据,使用 IndexedDB 代替
localStorage。
使用 IndexedDB
const request = indexedDB.open('myDatabase',1); request.onsuccess=(event)=>{const db = event.target.result;const transaction = db.transaction('myStore','readwrite');const store = transaction.objectStore('myStore'); store.add(data,'key');};压缩数据
const compressedData =JSON.stringify(data);const decompressedData =JSON.parse(compressedData);使用 TypedArray
const buffer =newArrayBuffer(1024);// 1KB 缓冲区 const intArray =newInt32Array(buffer);// 32位整数数组 优化数据渲染
- 只渲染可见区域的数据,减少 DOM 元素的数量。
- 将渲染任务放到
requestAnimationFrame 中执行,确保与浏览器的渲染周期同步。 - 使用
DocumentFragment 或innerHTML 减少 DOM 操作次数。
减少 DOM 操作
const fragment = document.createDocumentFragment(); data.forEach((item)=>{const div = document.createElement('div'); div.textContent = item; fragment.appendChild(div);}); container.appendChild(fragment);使用requestAnimationFrame
functionrender(){// 渲染逻辑 requestAnimationFrame(render);}render();虚拟列表
functionrenderVirtualList(data, container, itemHeight){const visibleCount = Math.ceil(container.clientHeight / itemHeight);const start = Math.floor(container.scrollTop / itemHeight);const end = start + visibleCount;const visibleData = data.slice(start, end);renderItems(visibleData);} window.addEventListener('scroll',()=>{renderVirtualList(data, container,50);});工具和库
- Lodash
- 提供高性能的数据处理函数,如
_.chunk(分批处理)、_.debounce(防抖)。
- 提供高性能的数据处理函数,如
- Immutable.js
- 提供不可变数据结构,减少数据操作的内存开销。
- D3.js
- 用于处理大规模数据可视化的库。
总结
处理大量数据时,JavaScript 可以通过以下策略优化性能:
- 优化数据加载:分页加载、懒加载、按需加载。
- 优化数据处理:使用 Web Workers、分批处理、高效算法。
- 优化数据存储:使用
TypedArray、压缩数据、IndexedDB。 - 优化数据渲染:虚拟列表、
requestAnimationFrame、减少 DOM 操作。
前端项目兼容性处理指南
在前端开发中,兼容性问题是一个常见的挑战。为了确保项目在不同浏览器、设备和操作系统上正常运行,以下是兼容性处理的详细策略和方法:
浏览器兼容性
- 明确目标浏览器
- 根据项目需求,确定需要支持的浏览器及其版本。
- 使用工具(如 Can I Use)检查浏览器对特定特性的支持情况。
- 对于不支持的新特性,使用 Polyfill 提供兼容性支持。
- 使用 Autoprefixer 自动添加 CSS 前缀,确保样式在不同浏览器中一致。
- 使用 Babel 将 ES6+ 代码转换为 ES5,确保在旧版浏览器中正常运行。
- 针对 IE 浏览器使用条件注释加载特定样式或脚本。
条件注释(仅限 IE)
<!--[if IE]> <link rel="stylesheet" href="ie.css"> <![endif]-->JavaScript 语法降级
// .babelrc {"presets":["@babel/preset-env"]}CSS 前缀
// package.json "browserslist":["last 2 versions","> 1%","not dead"]使用 Polyfill
// 示例:使用 core-js 提供 Promise 的 Polyfill import'core-js/features/promise';设备兼容性
- 使用媒体查询(Media Queries)适配不同屏幕尺寸。
- 使用现代布局方案(如 Flexbox 和 Grid)替代传统的浮动布局。
- 在 HTML 中设置视口(Viewport),确保页面在移动设备上正常显示。
视口设置
<metaname="viewport"content="width=device-width, initial-scale=1.0">Flexbox 和 Grid 布局
.container{display: flex;justify-content: space-between;}响应式设计
@media(max-width: 768px){.container{width: 100%;}}操作系统兼容性
- 使用跨平台字体(如 Arial、Helvetica、sans-serif)确保字体在不同操作系统上一致。
- 文件路径兼容性
- 在 Windows 和 Unix 系统中,使用正斜杠
/ 作为文件路径分隔符。
- 在 Windows 和 Unix 系统中,使用正斜杠
- 键盘和输入兼容性
- 确保表单和输入控件在不同操作系统上正常工作。
字体兼容性
body{font-family: Arial, Helvetica, sans-serif;}性能兼容性
- 代码压缩
- 使用工具(如 Webpack、Vite)对 JavaScript 和 CSS 进行压缩和混淆。
- 使用 WebP 格式图片,并为不支持 WebP 的浏览器提供回退方案。
- 对图片和资源进行懒加载,减少初始页面加载时间。
懒加载
<imgdata-src="image.jpg"alt="Lazy Loaded Image"class="lazyload">图片优化
<picture><sourcesrcset="image.webp"type="image/webp"><imgsrc="image.jpg"alt="Fallback Image"></picture>测试和调试
- 浏览器测试
- 使用浏览器开发者工具(如 Chrome DevTools)进行调试。
- 使用工具(如 BrowserStack)测试不同浏览器和设备上的兼容性。
- 自动化测试
- 使用工具(如 Jest、Cypress)编写自动化测试脚本,确保功能在不同环境中正常工作。
- 用户反馈
- 收集用户反馈,及时修复兼容性问题。
总结
前端项目兼容性处理的核心策略包括:
- 浏览器兼容性:使用 Polyfill、CSS 前缀、Babel 等工具。
- 设备兼容性:采用响应式设计、Flexbox 和 Grid 布局。
- 操作系统兼容性:注意字体、文件路径和输入兼容性。
- 性能兼容性:优化代码、图片和资源加载。
- 测试和调试:通过自动化测试和用户反馈确保兼容性。
使用 CSS 绘制椭圆的详细方法
在 CSS 中,可以通过 border-radius 属性绘制椭圆。以下是实现椭圆的几种方法:
使用 border-radius 绘制椭圆
border-radius 是绘制椭圆的关键属性。它的值可以是百分比或长度单位,用于定义元素的圆角半径。
-
width 和height 分别定义椭圆的宽度和高度。 -
border-radius: 50% 将矩形变为椭圆。 - 通过调整
width 和height 的比例,可以绘制不同方向的椭圆。 - 使用
transform: rotate() 旋转椭圆。
倾斜椭圆
.ellipse{width: 200px;height: 100px;background-color: #ff6347;border-radius: 50%;transform:rotate(45deg);}垂直椭圆
.ellipse{width: 100px;height: 200px;background-color: #ff6347;border-radius: 50%;}水平椭圆
.ellipse{width: 200px;height: 100px;background-color: #ff6347;border-radius: 50%;}使用 border-radius 的四个值绘制椭圆
border-radius 可以分别设置四个角的半径,通过调整值可以绘制更复杂的椭圆。
.ellipse{width: 200px;height: 100px;background-color: #ff6347;border-radius: 50% 50% 50% 50% / 60% 60% 40% 40%;}-
border-radius: 50% 50% 50% 50% / 60% 60% 40% 40% 分别定义水平和垂直方向的圆角半径。
使用 clip-path 绘制椭圆
clip-path 是另一种绘制椭圆的方法,通过裁剪路径实现。
.ellipse{width: 200px;height: 100px;background-color: #ff6347;clip-path:ellipse(50% 50% at 50% 50%);}-
ellipse(50% 50% at 50% 50%):定义椭圆的水平和垂直半径,以及中心点位置。
使用 SVG 绘制椭圆
SVG 是一种矢量图形格式,适合绘制复杂的椭圆。
<svgwidth="200"height="100"><ellipsecx="100"cy="50"rx="100"ry="50"fill="#ff6347"/></svg>-
cx 和cy:椭圆中心点的坐标。 -
rx 和ry:椭圆的水平和垂直半径。
总结
在 CSS 中绘制椭圆的主要方法包括:
-
border-radius:通过设置border-radius: 50% 将矩形变为椭圆。 -
clip-path:使用clip-path: ellipse() 裁剪路径绘制椭圆。 - SVG:使用 SVG 的
<ellipse> 元素绘制椭圆。
setTimeout 和 setInterval
setTimeout 和 setInterval 是 JavaScript 中用于定时执行代码的两个核心 API。以下是它们的区别、使用场景以及 setTimeout 设置为 0 时的行为分析。
setTimeout 和 setInterval 的区别
| 特性 | setTimeout | setInterval |
|---|---|---|
| 功能 | 在指定延迟后执行一次回调函数。 | 每隔指定时间重复执行回调函数。 |
| 语法 | setTimeout(callback, delay) | setInterval(callback, delay) |
| 停止方法 | clearTimeout(timeoutId) | clearInterval(intervalId) |
| 适用场景 | 延迟执行、单次任务。 | 重复执行、轮询任务。 |
setTimeout 设置为 0 的行为
- 现象当
setTimeout 的延迟时间设置为0 时,回调函数不会立即执行,而是会被放入事件队列中,等待当前调用栈清空后再执行。 - 原因
- JavaScript 是单线程语言,使用事件循环机制处理异步任务。
-
setTimeout 的回调函数属于宏任务,即使延迟时间为0,也会被放入宏任务队列中,等待当前同步代码和微任务执行完毕后再执行。
- 应用场景
- 将代码延迟到下一个事件循环执行,避免阻塞当前任务。
- 在 DOM 操作后执行回调,确保 DOM 更新完成。
示例
console.log('开始');setTimeout(()=>{ console.log('setTimeout');},0); console.log('结束');// 输出顺序:开始 → 结束 → setTimeout 为什么会有 setTimeout 和 setInterval 这两个 API
-
setTimeout 的作用- 用于延迟执行一次性任务。
- 例如:延迟显示提示信息、延迟执行动画等。
-
setInterval 的作用- 用于重复执行周期性任务。
- 例如:轮询服务器数据、定时更新 UI 等。
- 设计初衷
-
setTimeout 和setInterval 分别针对单次任务和重复任务,提供了更灵活的定时器功能。 - 它们的实现基于事件循环机制,确保了异步任务的执行顺序和可控性。
-
注意事项
- 如果回调函数执行时间超过延迟时间,会导致多个回调函数堆积执行。
- 解决方法:使用
setTimeout 递归调用替代setInterval。 - 性能优化
- 避免在
setTimeout 或setInterval 中执行耗时操作,以免阻塞主线程。 - 使用
requestAnimationFrame 替代setTimeout 执行动画,确保与浏览器的渲染周期同步。
- 避免在
setInterval 的问题
functionrepeat(){ console.log('重复执行');setTimeout(repeat,1000);}repeat();总结
-
setTimeout:用于延迟执行一次性任务,延迟时间为0 时会在下一个事件循环执行。 -
setInterval:用于重复执行周期性任务,但需注意回调函数执行时间过长的问题。 - 设计原因:
setTimeout 和setInterval 分别针对单次任务和重复任务,提供了灵活的定时器功能。
判断项目性能好不好的方法
一、核心性能指标
- 加载性能
- 首次内容绘制(FCP) :页面首次渲染文本、图片等内容的耗时。
- 最大内容绘制(LCP) :页面最大内容元素渲染的耗时。
- 首字节时间(TTFB) :从请求发出到收到第一个字节的时间。
- 交互性能
- 首次输入延迟(FID) :用户首次与页面交互(如点击按钮)到页面响应的耗时。
- 总阻塞时间(TBT) :页面主线程被长时间任务阻塞的总时间。
- 视觉稳定性
- 累积布局偏移(CLS) :页面布局变化的频率和幅度,衡量视觉稳定性。
- 资源优化
- 资源加载时间(如 JavaScript、CSS、图片)。
- 资源压缩率(如 Gzip、Brotli)。
- 内存占用
- 内存泄漏检测,确保内存使用量在合理范围内。
二、性能检测工具
- Lighthouse
- 集成在 Chrome DevTools 中,提供全面的性能分析报告。
- 使用步骤:
- 打开 Chrome DevTools(F12)。
- 切换到 Lighthouse 标签页。
- 点击 Generate report 生成报告。
- WebPageTest
- 提供详细的页面加载性能分析,支持多地点、多设备测试。
- 网址:https://www.webpagetest.org/
- Chrome DevTools
- Performance 面板:记录页面运行时性能,分析任务耗时和内存占用。
- Network 面板:分析资源加载时间和网络请求。
- Google Analytics
- 监控用户实际访问页面的性能数据,如页面加载时间、交互延迟等。
三、检测某个功能或组件的性能
- 在代码中插入计时器,测量某个函数的执行时间。
- 高精度计时,测量代码片段的执行时间。
- 使用 Chrome DevTools 的 Performance 面板
- 记录页面运行时性能,定位某个功能或组件的性能瓶颈。
- 使用步骤:
- 打开 Chrome DevTools(F12)。
- 切换到 Performance 面板。
- 点击 Record 按钮,执行需要测试的功能或组件。
- 停止记录后,分析火焰图中的任务耗时。
- 使用 React Profiler(React 项目)
- 分析 React 组件的渲染性能。
- 使用步骤:
- 在 React 开发者工具中切换到 Profiler 面板。
- 点击 Record 按钮,执行需要测试的组件。
- 停止记录后,分析组件的渲染时间和次数。
- 使用
why-did-you-render (React 项目)- 检测 React 组件的不必要渲染。
示例:
import whyDidYouRender from'@welldone-software/why-did-you-render';whyDidYouRender(React);使用performance.now()
const start = performance.now();myFunction();// 需要测试的函数 const end = performance.now(); console.log(`耗时:${end - start} 毫秒`);使用console.time 和console.timeEnd
console.time('myFunction');myFunction();// 需要测试的函数 console.timeEnd('myFunction');四、优化性能的建议
- 减少主线程任务
- 将耗时任务放到 Web Workers 中执行,避免阻塞主线程。
- 优化资源加载
- 使用懒加载、分块加载(Code Splitting)减少初始加载时间。
- 减少重排和重绘
- 使用
transform 和opacity 替代直接修改布局属性。
- 使用
- 缓存数据
- 使用 Service Worker 或 IndexedDB 缓存数据,减少网络请求。
- 压缩资源
- 使用 Gzip 或 Brotli 压缩 JavaScript、CSS 和图片资源。
五、总结
判断项目性能好不好的核心指标包括加载性能、交互性能、视觉稳定性和资源优化。通过 Lighthouse、WebPageTest 等工具可以全面评估项目性能,而 console.time、performance.now() 和 Chrome DevTools 的 Performance 面板则适合检测某个功能或组件的性能。结合优化建议,可以显著提升项目的性能和用户体验。
单独检测某个功能或组件性能的方法
在开发过程中,有时需要针对某个功能或组件进行性能检测,以定位瓶颈并优化代码。以下是详细的检测方法和工具:
一、使用 console.time 和 console.timeEnd
- 作用
- 测量代码片段的执行时间。
- 输出结果:
myFunction: 0.123ms
- 适用场景
- 快速测量函数的执行时间,适合简单的性能检测。
示例
console.time('myFunction');myFunction();// 需要测试的函数 console.timeEnd('myFunction');二、使用 performance.now()
- 作用
- 提供高精度计时,测量代码片段的执行时间。
- 适用场景
- 需要高精度计时的性能检测。
示例
const start = performance.now();myFunction();// 需要测试的函数 const end = performance.now(); console.log(`耗时:${end - start} 毫秒`);三、使用 Chrome DevTools 的 Performance 面板
- 作用
- 记录页面运行时性能,定位某个功能或组件的性能瓶颈。
- 使用步骤
- 打开 Chrome DevTools(F12)。
- 切换到 Performance 面板。
- 点击 Record 按钮,执行需要测试的功能或组件。
- 停止记录后,分析火焰图中的任务耗时。
- 适用场景
- 需要详细分析任务耗时、内存占用和渲染性能的复杂场景。
四、使用 React Profiler(React 项目)
- 作用
- 分析 React 组件的渲染性能。
- 使用步骤
- 在 React 开发者工具中切换到 Profiler 面板。
- 点击 Record 按钮,执行需要测试的组件。
- 停止记录后,分析组件的渲染时间和次数。
- 示例
- 输出结果:组件的渲染时间、渲染次数和性能瓶颈。
- 适用场景
- 检测 React 组件的渲染性能,优化不必要的渲染。
五、使用 why-did-you-render(React 项目)
- 作用
- 检测 React 组件的不必要渲染。
- 适用场景
- 检测和优化 React 组件的渲染性能。
配置
import whyDidYouRender from'@welldone-software/why-did-you-render';whyDidYouRender(React);安装
npminstall @welldone-software/why-did-you-render --save-dev 六、使用自定义性能检测工具
- 作用
- 根据项目需求,自定义性能检测逻辑。
- 适用场景
- 需要灵活自定义性能检测逻辑的场景。
示例
classPerformanceTracker{constructor(){this.metrics ={};}start(name){this.metrics[name]= performance.now();}end(name){const duration = performance.now()-this.metrics[name]; console.log(`${name} 耗时:${duration} 毫秒`);}}const tracker =newPerformanceTracker(); tracker.start('myFunction');myFunction();// 需要测试的函数 tracker.end('myFunction');七、总结
检测某个功能或组件性能的常用方法包括:
-
console.time 和console.timeEnd:快速测量函数执行时间。 -
performance.now() :高精度计时。 - Chrome DevTools 的 Performance 面板:详细分析任务耗时和内存占用。
- React Profiler:检测 React 组件的渲染性能。
-
why-did-you-render:检测 React 组件的不必要渲染。 - 自定义性能检测工具:根据需求灵活实现。
常见的网络请求状态码
HTTP 状态码是服务器对客户端请求的响应结果,用于表示请求的处理状态。以下是常见的状态码及其含义:
一、状态码分类
HTTP 状态码由 3 位数字组成,分为 5 大类:
- 1xx(信息性状态码)
- 表示请求已被接收,需要继续处理。
- 2xx(成功状态码)
- 表示请求已成功被服务器接收、理解并处理。
- 3xx(重定向状态码)
- 表示需要客户端进一步操作以完成请求。
- 4xx(客户端错误状态码)
- 表示客户端发送的请求有错误。
- 5xx(服务器错误状态码)
- 表示服务器在处理请求时发生错误。
二、常见状态码
- 1xx 信息性状态码
- 100 Continue:服务器已收到请求头,客户端应继续发送请求体。
- 101 Switching Protocols:服务器同意切换协议(如升级到 WebSocket)。
- 2xx 成功状态码
- 200 OK:请求成功,服务器返回了所需的数据。
- 201 Created:请求成功,并创建了新资源(如 POST 请求)。
- 204 No Content:请求成功,但响应中没有返回内容。
- 3xx 重定向状态码
- 301 Moved Permanently:请求的资源已永久移动到新位置。
- 302 Found:请求的资源临时移动到新位置。
- 304 Not Modified:资源未修改,客户端可使用缓存版本。
- 4xx 客户端错误状态码
- 400 Bad Request:请求语法错误,服务器无法理解。
- 401 Unauthorized:请求需要身份验证。
- 403 Forbidden:服务器拒绝请求,客户端无访问权限。
- 404 Not Found:请求的资源不存在。
- 5xx 服务器错误状态码
- 500 Internal Server Error:服务器内部错误,无法完成请求。
- 502 Bad Gateway:服务器作为网关或代理时,从上游服务器收到无效响应。
- 503 Service Unavailable:服务器暂时无法处理请求(如过载或维护)。
- 504 Gateway Timeout:服务器作为网关或代理时,未及时从上游服务器收到响应。
三、状态码的应用场景
- 调试和排查问题
- 通过状态码快速定位请求失败的原因。
- 优化用户体验
- 根据状态码显示友好的错误提示(如 404 页面)。
- 监控和报警
- 监控服务器返回的状态码,及时发现异常并报警。
四、总结
HTTP 状态码是网络请求的重要组成部分,分为 1xx(信息性)、2xx(成功)、3xx(重定向)、4xx(客户端错误)和 5xx(服务器错误)五大类。理解常见的状态码及其含义,有助于开发、调试和优化网络请求。
箭头函数与普通函数的区别
箭头函数(Arrow Function)是 ES6 引入的一种简洁的函数语法,与普通函数(Function)在语法、行为和作用域上有显著区别。以下是两者的详细对比:
一、语法区别
- 语法简洁,适合单行函数。
- 使用
function 关键字定义。
普通函数
functionadd(a, b){return a + b;}箭头函数
constadd=(a, b)=> a + b;二、this 指向区别
- 没有自己的
this,this 继承自外层作用域(词法作用域)。 - 有自己的
this,this 指向调用该函数的对象。
普通函数
const obj ={value:42,getValue:function(){ console.log(this.value);// 输出:42 }}; obj.getValue();箭头函数
const obj ={value:42,getValue:()=>{ console.log(this.value);// 输出:undefined }}; obj.getValue();三、arguments 对象区别
- 没有
arguments 对象,需使用剩余参数(Rest Parameters)获取参数。 - 有
arguments 对象,包含所有传入的参数。
普通函数
functionshowArgs(){ console.log(arguments);}showArgs(1,2,3);// 输出:{ 0: 1, 1: 2, 2: 3 } 箭头函数
constshowArgs=(...args)=>{ console.log(args);};showArgs(1,2,3);// 输出:[1, 2, 3] 四、构造函数区别
- 不能作为构造函数使用,不能使用
new 关键字。 - 可以作为构造函数使用,可以使用
new 关键字。
普通函数
functionFoo(){}const foo =newFoo();// 正常执行 箭头函数
constFoo=()=>{};const foo =newFoo();// 报错:Foo is not a constructor 五、原型链区别
- 没有
prototype 属性,不能作为构造函数。 - 有
prototype 属性,可以作为构造函数。
普通函数
functionFoo(){} console.log(Foo.prototype);// 输出:{ constructor: Foo } 箭头函数
constFoo=()=>{}; console.log(Foo.prototype);// 输出:undefined 六、适用场景
- 适合短小简单的函数,如回调函数、数组方法等。
- 适合需要
this、arguments 或作为构造函数的场景。
普通函数
functionPerson(name){this.name = name;}const person =newPerson('Alice');箭头函数
const numbers =[1,2,3];const doubled = numbers.map(n=> n *2);七、总结
| 特性 | 箭头函数 | 普通函数 |
|---|---|---|
| 语法 | 简洁,适合单行函数 | 使用 function 关键字定义 |
this 指向 | 继承自外层作用域 | 指向调用函数的对象 |
arguments 对象 | 无,需使用剩余参数 | 有 arguments 对象 |
| 构造函数 | 不能作为构造函数 | 可以作为构造函数 |
| 原型链 | 无 prototype 属性 | 有 prototype 属性 |
根据需求选择合适的函数类型,可以提升代码的可读性和可维护性。
防抖(Debounce)与节流(Throttle)的区别
防抖和节流是两种常见的性能优化技术,用于控制函数的执行频率,避免高频触发导致的性能问题。以下是它们的详细区别和应用场景:
一、防抖(Debounce)
- 定义
- 在事件被触发后,等待一段时间再执行函数。如果在这段时间内事件再次被触发,则重新计时。
- 实现原理
- 使用
setTimeout 延迟执行函数,每次触发事件时清除之前的定时器并重新计时。
- 使用
- 应用场景
- 搜索框输入:用户停止输入后再触发搜索请求。
- 窗口调整大小:用户停止调整窗口大小后再重新计算布局。
代码实现
functiondebounce(func, delay){let timer;returnfunction(...args){clearTimeout(timer); timer =setTimeout(()=>func.apply(this, args), delay);};}二、节流(Throttle)
- 定义
- 在事件被触发后,每隔一段时间执行一次函数,忽略中间的事件触发。
- 实现原理
- 使用时间戳或
setTimeout 控制函数的执行频率。
- 使用时间戳或
- 应用场景
- 滚动事件:每隔一段时间触发一次滚动处理函数。
- 按钮点击:防止用户频繁点击按钮导致多次提交。
代码实现
functionthrottle(func, delay){let lastTime =0;returnfunction(...args){const now = Date.now();if(now - lastTime >= delay){func.apply(this, args); lastTime = now;}};}三、防抖与节流的区别
| 特性 | 防抖(Debounce) | 节流(Throttle) |
|---|---|---|
| 执行时机 | 事件停止触发后执行 | 每隔一段时间执行一次 |
| 触发频率 | 高频触发时只执行最后一次 | 高频触发时按固定频率执行 |
| 实现原理 | 使用 setTimeout 延迟执行 | 使用时间戳或 setTimeout 控制频率 |
| 应用场景 | 搜索框输入、窗口调整大小 | 滚动事件、按钮点击 |
四、代码示例对比
节流示例
const throttledScroll =throttle(()=>{ console.log('滚动事件处理');},500);// 用户滚动时触发 window.addEventListener('scroll', throttledScroll);防抖示例
const debouncedSearch =debounce(()=>{ console.log('搜索请求已发送');},500);// 用户输入时触发 input.addEventListener('input', debouncedSearch);五、总结
- 防抖:适合在事件停止触发后执行一次的场景,如搜索框输入、窗口调整大小。
- 节流:适合按固定频率执行一次的场景,如滚动事件、按钮点击。
JavaScript 宏任务与微任务的详解
JavaScript 是单线程语言,通过事件循环(Event Loop)机制处理异步任务。为了更高效地管理任务,JavaScript 将异步任务分为宏任务(Macro Task)和微任务(Micro Task)。以下是宏任务和微任务的存在原因及其区别:
一、宏任务与微任务的定义
- 宏任务(Macro Task)
- 代表较大的任务单元,通常包括:
-
setTimeout、setInterval - I/O 操作(如文件读取、网络请求)
- UI 渲染
-
script(整体代码)
-
- 代表较大的任务单元,通常包括:
- 微任务(Micro Task)
- 代表较小的任务单元,通常包括:
-
Promise.then、Promise.catch、Promise.finally -
MutationObserver -
queueMicrotask
-
- 代表较小的任务单元,通常包括:
二、事件循环中的执行顺序
- 事件循环流程
- 执行一个宏任务(如
script 整体代码)。 - 执行所有微任务。
- 渲染页面(如果需要)。
- 执行下一个宏任务。
- 输出顺序:
开始 → 结束 → Promise → setTimeout
- 执行一个宏任务(如
示例
console.log('开始');setTimeout(()=>{ console.log('setTimeout');},0); Promise.resolve().then(()=>{ console.log('Promise');}); console.log('结束');三、为什么要有宏任务和微任务
- 任务优先级
- 微任务的优先级高于宏任务,确保高优先级的任务(如 Promise 回调)能及时执行。
- 任务粒度
- 宏任务代表较大的任务单元(如定时器、I/O 操作),适合处理耗时较长的任务。
- 微任务代表较小的任务单元(如 Promise 回调),适合处理轻量级的任务。
- 用户体验
- 通过微任务优先执行,可以更快地响应用户操作(如 Promise 回调),提升用户体验。
- 任务调度
- 宏任务和微任务的划分,使得 JavaScript 可以更灵活地调度任务,避免长时间阻塞主线程。
四、宏任务与微任务的区别
| 特性 | 宏任务(Macro Task) | 微任务(Micro Task) |
|---|---|---|
| 任务类型 | setTimeout、setInterval、I/O 操作 | Promise.then、MutationObserver |
| 执行顺序 | 在微任务之后执行 | 在宏任务之后、渲染之前执行 |
| 优先级 | 低 | 高 |
| 任务粒度 | 较大 | 较小 |
五、总结
JavaScript 引入宏任务和微任务的原因包括:
- 优先级管理:微任务优先执行,确保高优先级任务及时处理。
- 任务粒度:宏任务处理耗时较长的任务,微任务处理轻量级任务。
- 用户体验:通过微任务优先执行,提升用户体验。
- 任务调度:灵活调度任务,避免长时间阻塞主线程。
跨域问题
跨域问题是由浏览器的 同源策略(Same-Origin Policy)引起的安全机制,用于防止不同源的网站之间进行恶意数据交互。以下是跨域问题的原因、表现及解决方案:
一、什么是跨域
- 同源策略
- 同源策略要求协议、域名和端口完全相同,否则视为跨域。
- 示例:
-
https://www.example.com 和https://api.example.com 不同源(域名不同)。 -
http://example.com 和https://example.com 不同源(协议不同)。 -
http://example.com:80 和http://example.com:8080 不同源(端口不同)。
-
- 跨域的表现
- 浏览器阻止跨域请求,导致请求失败。
- 常见的错误信息: Access to XMLHttpRequest at 'https://api.example.com' from origin 'https://www.example.com' has been blocked by CORS policy.
二、跨域问题的解决方案
- CORS(跨域资源共享)
- 服务器通过设置响应头允许跨域请求。
- 常用响应头:
-
Access-Control-Allow-Origin:允许的源(如* 或https://www.example.com)。 -
Access-Control-Allow-Methods:允许的 HTTP 方法(如GET, POST)。 -
Access-Control-Allow-Headers:允许的请求头(如Content-Type)。
-
- JSONP(JSON with Padding)
- 利用
<script> 标签不受同源策略限制的特性,通过回调函数获取跨域数据。 - 限制:仅支持
GET 请求。
- 利用
- 代理服务器
- 通过同源的后端服务器代理跨域请求。
- 示例:
- 前端请求:
https://www.example.com/api/data - 后端服务器转发请求:
https://api.example.com/data
- 前端请求:
- WebSocket
- WebSocket 不受同源策略限制,可用于跨域通信。
- Nginx 反向代理
- 使用 Nginx 配置反向代理,将跨域请求转发到目标服务器。
- PostMessage
- 用于跨窗口通信,支持跨域。
示例:
// 发送消息 window.postMessage('Hello','https://www.example.com');// 接收消息 window.addEventListener('message',(event)=>{if(event.origin !=='https://www.example.com')return; console.log(event.data);});示例配置:
server { location /api { proxy_pass https://api.example.com; } } 示例:
const socket =newWebSocket('wss://api.example.com'); socket.onmessage=(event)=>{ console.log(event.data);};示例:
<script>functionhandleResponse(data){ console.log(data);}</script><scriptsrc="https://api.example.com/data?callback=handleResponse"></script>示例:
Access-Control-Allow-Origin: * Access-Control-Allow-Methods: GET, POST, OPTIONS Access-Control-Allow-Headers: Content-Type 三、常见的跨域场景
- 前端调用后端 API
- 前端:
https://www.example.com - 后端:
https://api.example.com - 解决方案:CORS、代理服务器、Nginx 反向代理。
- 前端:
- 跨域资源共享
- 不同域名之间的资源访问(如图片、字体)。
- 解决方案:CORS、JSONP。
- 跨窗口通信
- 不同窗口或 iframe 之间的通信。
- 解决方案:PostMessage。
四、总结
跨域问题是由浏览器的同源策略引起的,常见解决方案包括:
- CORS:通过服务器设置响应头允许跨域请求。
- JSONP:利用
<script> 标签获取跨域数据。 - 代理服务器:通过后端服务器代理跨域请求。
- WebSocket:用于跨域实时通信。
- Nginx 反向代理:通过 Nginx 配置转发跨域请求。
- PostMessage:用于跨窗口通信。
提高前端项目加载速度的方法
前端项目的加载速度直接影响用户体验和 SEO 排名。以下是提高前端项目加载速度的常用方法:
一、优化资源加载
- 压缩资源
- 使用工具(如 Webpack、Vite)压缩 JavaScript、CSS 和 HTML 文件。
- 使用 Gzip/Brotli 压缩
- 在服务器端启用 Gzip 或 Brotli 压缩,减少资源传输体积。
- 图片优化
- 使用 WebP 格式图片,并为不支持 WebP 的浏览器提供回退方案。
- 使用工具(如 ImageOptim、TinyPNG)压缩图片。
- 使用 CDN
- 将静态资源(如 JavaScript、CSS、图片)托管到 CDN,加速资源加载。
示例:
<picture><sourcesrcset="image.webp"type="image/webp"><imgsrc="image.jpg"alt="Fallback Image"></picture>示例(Nginx 配置):
gzip on; gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; 示例:
npminstall terser-webpack-plugin --save-dev 二、减少请求次数
- 合并文件
- 将多个 JavaScript 或 CSS 文件合并为一个文件,减少 HTTP 请求次数。
- 使用雪碧图(Sprite)
- 将多个小图标合并为一张雪碧图,减少图片请求次数。
- HTTP/2
- 使用 HTTP/2 协议,支持多路复用,减少请求开销。
示例:
.icon{background-image:url('sprite.png');background-position: -10px -20px;}三、代码优化
- 代码分割(Code Splitting)
- 使用动态导入(Dynamic Import)将代码拆分为多个小块,按需加载。
- Tree Shaking
- 使用工具(如 Webpack、Rollup)移除未使用的代码。
- 延迟加载(Lazy Load)
- 延迟加载非关键资源(如图片、视频)。
示例:
<imgdata-src="image.jpg"alt="Lazy Loaded Image"class="lazyload">示例:
import{ func1 }from'module';func1();示例:
import('./module').then(module=>{ module.default();});四、缓存优化
- 浏览器缓存
- 设置
Cache-Control 和ETag 响应头,利用浏览器缓存静态资源。
- 设置
- Service Worker
- 使用 Service Worker 缓存资源,支持离线访问。
示例:
self.addEventListener('install',(event)=>{ event.waitUntil( caches.open('v1').then((cache)=>{return cache.addAll(['/','/styles.css','/app.js']);}));});示例(Nginx 配置):
location /static { expires 1y; add_header Cache-Control "public"; } 五、渲染优化
- 减少重排和重绘
- 使用
transform 和opacity 替代直接修改布局属性。
- 使用
- 使用
requestAnimationFrame- 将动画逻辑放到
requestAnimationFrame 中执行,确保与浏览器的渲染周期同步。
- 将动画逻辑放到
- 关键渲染路径优化
- 将关键 CSS 内联到 HTML 中,优先加载关键资源。
示例:
<style>/* 关键 CSS */</style>示例:
functionanimate(){// 动画逻辑 requestAnimationFrame(animate);}animate();示例:
.element{transform:translateX(100px);opacity: 0.5;}六、工具和监控
- 性能分析工具
- 使用 Lighthouse、WebPageTest 等工具分析页面性能。
- 性能监控
- 使用 Google Analytics、New Relic 等工具监控页面加载速度。
七、总结
提高前端项目加载速度的核心方法包括:
- 优化资源加载:压缩资源、使用 CDN、优化图片。
- 减少请求次数:合并文件、使用雪碧图、HTTP/2。
- 代码优化:代码分割、Tree Shaking、延迟加载。
- 缓存优化:设置浏览器缓存、使用 Service Worker。
- 渲染优化:减少重排和重绘、使用
requestAnimationFrame。 - 工具和监控:使用性能分析工具和监控工具持续优化。
页签之间消息传递的方法
在浏览器中,多个页签之间可以通过以下几种方式进行消息传递:
一、使用 localStorage 或 sessionStorage
- 原理
-
localStorage 和sessionStorage 是浏览器提供的存储机制,可以在同一源的不同页签之间共享数据。
-
- 实现步骤
- 在页签 A 中设置数据并触发
storage 事件。 - 在页签 B 中监听
storage 事件,获取数据。
- 在页签 A 中设置数据并触发
- 注意事项
- 只能在同一源的不同页签之间共享数据。
-
storage 事件不会在当前页签触发。
示例代码
// 页签 A:设置数据 localStorage.setItem('message','Hello from Tab A');// 页签 B:监听 storage 事件 window.addEventListener('storage',(event)=>{if(event.key ==='message'){ console.log('收到消息:', event.newValue);}});二、使用 BroadcastChannel
- 原理
-
BroadcastChannel 是浏览器提供的 API,允许同一源的不同页签之间通过消息通道进行通信。
-
- 实现步骤
- 在页签 A 和页签 B 中创建相同的
BroadcastChannel。 - 在页签 A 中发送消息,在页签 B 中接收消息。
- 在页签 A 和页签 B 中创建相同的
- 注意事项
- 只能在同一源的不同页签之间通信。
- 需要手动关闭
BroadcastChannel。
示例代码
// 页签 A:发送消息 const channel =newBroadcastChannel('myChannel'); channel.postMessage('Hello from Tab A');// 页签 B:接收消息 const channel =newBroadcastChannel('myChannel'); channel.onmessage=(event)=>{ console.log('收到消息:', event.data);};三、使用 SharedWorker
- 原理
-
SharedWorker 是浏览器提供的 Web Worker,允许同一源的不同页签之间共享一个后台线程进行通信。
-
- 实现步骤
- 在页签 A 和页签 B 中创建相同的
SharedWorker。 - 在页签 A 中发送消息,在页签 B 中接收消息。
- 在页签 A 和页签 B 中创建相同的
- 注意事项
- 只能在同一源的不同页签之间通信。
-
SharedWorker 需要额外的 JavaScript 文件。
示例代码
// sharedWorker.js const connections =[]; self.onconnect=(event)=>{const port = event.ports; connections.push(port); port.onmessage=(event)=>{ connections.forEach((conn)=>{if(conn !== port) conn.postMessage(event.data);});};};// 页签 A:发送消息 const worker =newSharedWorker('sharedWorker.js'); worker.port.postMessage('Hello from Tab A');// 页签 B:接收消息 const worker =newSharedWorker('sharedWorker.js'); worker.port.onmessage=(event)=>{ console.log('收到消息:', event.data);};四、使用 window.open 和 postMessage
- 原理
- 通过
window.open 打开新页签,并使用postMessage 进行跨页签通信。
- 通过
- 实现步骤
- 在页签 A 中使用
window.open 打开页签 B,并保存页签 B 的引用。 - 在页签 A 中使用
postMessage 发送消息,在页签 B 中监听message 事件。
- 在页签 A 中使用
- 注意事项
- 可以在不同源的页签之间通信,但需要处理跨域问题。
- 需要手动管理页签的引用。
示例代码
// 页签 A:打开页签 B 并发送消息 const tabB = window.open('tabB.html');setTimeout(()=>{ tabB.postMessage('Hello from Tab A','*');},1000);// 页签 B:接收消息 window.addEventListener('message',(event)=>{ console.log('收到消息:', event.data);});五、总结
| 方法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
localStorage | 简单易用,无需额外 API | 只能在同一源页签之间通信 | 简单数据共享 |
BroadcastChannel | 支持消息通道,通信更灵活 | 只能在同一源页签之间通信 | 需要频繁通信的场景 |
SharedWorker | 支持后台线程通信,性能更好 | 需要额外的 JavaScript 文件 | 复杂数据共享和通信 |
window.open + postMessage | 支持跨域通信,灵活性强 | 需要手动管理页签引用 | 跨域页签通信 |
根据具体需求选择合适的方法,可以实现页签之间的高效消息传递。
使元素水平居中的方法
在前端开发中,实现元素水平居中是常见的需求。以下是多种实现方法及其适用场景:
一、行内元素水平居中
- 适用于行内元素(如文本、图片)。
- 适用于单行文本元素。
使用line-height
.container{height: 100px;line-height: 100px;}使用text-align: center
.container{text-align: center;}二、块级元素水平居中
- 适用于固定宽度的块级元素。
- 适用于任意宽度的块级元素。
- 适用于任意宽度的块级元素。
- 适用于未知宽度的块级元素。
- 适用于需要兼容旧浏览器的场景。
使用表格布局
.container{display: table;margin: 0 auto;}使用绝对定位 +transform
.element{position: absolute;left: 50%;transform:translateX(-50%);}使用 Grid
.container{display: grid;place-items: center;}使用 Flexbox
.container{display: flex;justify-content: center;}使用margin: 0 auto
.element{width: 200px;margin: 0 auto;}三、行内块级元素水平居中
- 适用于行内块级元素。
- 适用于行内块级元素。
使用 Flexbox
.container{display: flex;justify-content: center;}使用text-align: center
.container{text-align: center;}.element{display: inline-block;}四、浮动元素水平居中
- 适用于浮动元素。
使用margin: 0 auto + 清除浮动
.element{float: left;width: 200px;margin: 0 auto;}.container::after{content:'';display: table;clear: both;}五、总结
| 方法 | 适用场景 | 示例代码 |
|---|---|---|
text-align: center | 行内元素、行内块级元素 | .container { text-align: center; } |
line-height | 单行文本元素 | .container { line-height: 100px; } |
margin: 0 auto | 固定宽度的块级元素 | .element { margin: 0 auto; } |
| Flexbox | 任意宽度的块级元素 | .container { display: flex; justify-content: center; } |
| Grid | 任意宽度的块级元素 | .container { display: grid; place-items: center; } |
绝对定位 + transform | 未知宽度的块级元素 | .element { position: absolute; left: 50%; transform: translateX(-50%); } |
| 表格布局 | 需要兼容旧浏览器的场景 | .container { display: table; margin: 0 auto; } |
根据具体需求选择合适的方法,可以轻松实现元素水平居中。
let、const 和 var 的区别详解
在 JavaScript 中,let、const 和 var 是用于声明变量的关键字,它们在作用域、提升和重复声明等方面有显著区别。以下是它们的详细对比:
一、作用域
-
var- 函数作用域:在函数内部声明的变量,只能在函数内部访问。
- 全局作用域:在函数外部声明的变量,可以在全局访问。
-
let 和const- 块级作用域:在
{} 块内部声明的变量,只能在块内部访问。
- 块级作用域:在
示例
if(true){var a =1;let b =2;const c =3;} console.log(a);// 输出:1 console.log(b);// 报错:b is not defined console.log(c);// 报错:c is not defined 二、变量提升
-
var- 变量会被提升到函数或全局作用域的顶部,但赋值不会被提升。
-
let 和const- 变量会被提升到块级作用域的顶部,但在声明之前访问会触发“暂时性死区”(Temporal Dead Zone,TDZ)。
示例:
console.log(b);// 报错:Cannot access 'b' before initialization let b =2;示例:
console.log(a);// 输出:undefined var a =1;三、重复声明
-
var- 允许重复声明,后面的声明会覆盖前面的声明。
-
let 和const- 不允许重复声明,会报错。
示例:
let b =1;let b =2;// 报错:Identifier 'b' has already been declared 示例:
var a =1;var a =2; console.log(a);// 输出:2 四、值的修改
-
var 和let- 声明的变量可以重新赋值。
-
const- 声明的变量不可重新赋值(常量),但对象或数组的属性可以修改。
示例:
const c =5; c =6;// 报错:Assignment to constant variable const obj ={key:'value'}; obj.key ='newValue';// 允许修改属性 示例:
var a =1; a =2;let b =3; b =4;五、总结
| 特性 | var | let | const |
|---|---|---|---|
| 作用域 | 函数作用域 | 块级作用域 | 块级作用域 |
| 变量提升 | 提升(赋值不提升) | 提升(存在 TDZ) | 提升(存在 TDZ) |
| 重复声明 | 允许 | 不允许 | 不允许 |
| 值的修改 | 允许 | 允许 | 不允许(对象属性可修改) |
六、使用建议
- 优先使用
const- 如果变量的值不需要修改,使用
const 可以避免意外赋值。
- 如果变量的值不需要修改,使用
- 其次使用
let- 如果变量的值需要修改,使用
let 可以避免var 的作用域问题。
- 如果变量的值需要修改,使用
- 避免使用
var-
var 的作用域和提升机制容易导致代码难以维护,建议使用let 和const 替代。
-
作用域与作用域链
作用域(Scope)和作用域链(Scope Chain)是 JavaScript 中理解变量和函数访问规则的核心概念。以下是它们的详细说明:
一、作用域(Scope)
- 定义
- 作用域是指变量和函数的可访问范围,决定了代码中哪些部分可以访问特定的变量或函数。
- 类型
- 全局作用域(Global Scope) :在函数和代码块外部声明的变量,可以在整个程序中访问。
- 函数作用域(Function Scope) :在函数内部声明的变量,只能在函数内部访问。
- 块级作用域(Block Scope) :在
{} 块内部声明的变量(使用let 或const),只能在块内部访问。
示例
// 全局作用域 const globalVar ='Global';functionmyFunction(){// 函数作用域 const functionVar ='Function'; console.log(globalVar);// 输出:Global }if(true){// 块级作用域 const blockVar ='Block'; console.log(globalVar);// 输出:Global } console.log(blockVar);// 报错:blockVar is not defined 二、作用域链(Scope Chain)
- 定义
- 作用域链是 JavaScript 查找变量和函数的一种机制,它由当前作用域和所有父级作用域组成。
- 当访问一个变量时,JavaScript 会从当前作用域开始查找,如果找不到,则逐级向上查找父级作用域,直到全局作用域。
- 形成过程
- 每次调用函数时,都会创建一个新的作用域,并将其添加到作用域链中。
- 作用域链的顶端是当前作用域,底端是全局作用域。
- 在
innerFunction 中访问变量时,作用域链的顺序为:innerFunction →outerFunction →global
示例
const globalVar ='Global';functionouterFunction(){const outerVar ='Outer';functioninnerFunction(){const innerVar ='Inner'; console.log(globalVar);// 输出:Global console.log(outerVar);// 输出:Outer console.log(innerVar);// 输出:Inner }innerFunction();}outerFunction();三、作用域链的作用
- 变量查找
- JavaScript 通过作用域链查找变量,确保变量在正确的作用域内访问。
- 闭包(Closure)
- 闭包是函数与其词法作用域的组合,通过作用域链可以访问父级作用域的变量。
示例:
functionouterFunction(){const outerVar ='Outer';functioninnerFunction(){ console.log(outerVar);// 输出:Outer }return innerFunction;}const closure =outerFunction();closure();四、总结
- 作用域
- 决定了变量和函数的可访问范围,分为全局作用域、函数作用域和块级作用域。
- 作用域链
- 是 JavaScript 查找变量和函数的机制,由当前作用域和所有父级作用域组成。
- 通过作用域链,可以实现变量查找和闭包功能。
JavaScript 继承的几种方式
JavaScript 是基于原型的语言,没有类的概念,但可以通过多种方式实现继承。以下是常见的继承方式及其优缺点:
一、原型链继承
- 原理
- 将子类的原型对象指向父类的实例,从而继承父类的属性和方法。
- 优点
- 简单易用。
- 缺点
- 父类的引用类型属性会被所有子类实例共享。
- 无法向父类构造函数传参。
实现
functionParent(){this.name ='Parent';}Parent.prototype.sayHello=function(){ console.log('Hello from '+this.name);};functionChild(){}Child.prototype =newParent();const child =newChild(); child.sayHello();// 输出:Hello from Parent 二、构造函数继承
- 原理
- 在子类构造函数中调用父类构造函数,从而继承父类的属性。
- 优点
- 解决了原型链继承中引用类型属性共享的问题。
- 可以向父类构造函数传参。
- 缺点
- 无法继承父类原型上的方法。
实现
functionParent(name){this.name = name;}functionChild(name){Parent.call(this, name);}const child =newChild('Child'); console.log(child.name);// 输出:Child 三、组合继承
- 原理
- 结合原型链继承和构造函数继承,既继承父类的属性,又继承父类原型上的方法。
- 优点
- 解决了原型链继承和构造函数继承的缺点。
- 缺点
- 父类构造函数被调用了两次,存在性能开销。
实现
functionParent(name){this.name = name;}Parent.prototype.sayHello=function(){ console.log('Hello from '+this.name);};functionChild(name){Parent.call(this, name);}Child.prototype =newParent();const child =newChild('Child'); child.sayHello();// 输出:Hello from Child 四、原型式继承
- 原理
- 基于已有对象创建新对象,类似于原型链继承。
- 优点
- 简单易用,适合基于已有对象创建新对象。
- 缺点
- 引用类型属性会被所有子对象共享。
实现
const parent ={name:'Parent',sayHello:function(){ console.log('Hello from '+this.name);}};const child = Object.create(parent); child.name ='Child'; child.sayHello();// 输出:Hello from Child 五、寄生式继承
- 原理
- 在原型式继承的基础上,增强对象的功能。
- 优点
- 可以在继承的基础上增强对象的功能。
- 缺点
- 引用类型属性会被所有子对象共享。
实现
functioncreateChild(parent){const child = Object.create(parent); child.sayHi=function(){ console.log('Hi from '+this.name);};return child;}const parent ={name:'Parent'};const child =createChild(parent); child.sayHi();// 输出:Hi from Parent 六、寄生组合式继承
- 原理
- 结合寄生式继承和组合继承,优化组合继承的性能问题。
- 优点
- 解决了组合继承的性能问题,是最理想的继承方式。
- 缺点
- 实现较为复杂。
实现
functioninheritPrototype(Child, Parent){const prototype = Object.create(Parent.prototype); prototype.constructor = Child;Child.prototype = prototype;}functionParent(name){this.name = name;}Parent.prototype.sayHello=function(){ console.log('Hello from '+this.name);};functionChild(name){Parent.call(this, name);}inheritPrototype(Child, Parent);const child =newChild('Child'); child.sayHello();// 输出:Hello from Child 七、ES6 类继承
- 原理
- 使用
class 和extends 关键字实现继承。
- 使用
- 优点
- 语法简洁,易于理解。
- 解决了传统继承方式的缺点。
- 缺点
- 需要支持 ES6 的浏览器或环境。
实现
classParent{constructor(name){this.name = name;}sayHello(){ console.log('Hello from '+this.name);}}classChildextendsParent{constructor(name){super(name);}}const child =newChild('Child'); child.sayHello();// 输出:Hello from Child 八、总结
| 继承方式 | 优点 | 缺点 |
|---|---|---|
| 原型链继承 | 简单易用 | 引用类型属性共享,无法传参 |
| 构造函数继承 | 解决引用类型属性共享,支持传参 | 无法继承父类原型上的方法 |
| 组合继承 | 结合原型链和构造函数继承 | 父类构造函数被调用两次 |
| 原型式继承 | 简单易用,适合基于已有对象创建新对象 | 引用类型属性共享 |
| 寄生式继承 | 增强对象功能 | 引用类型属性共享 |
| 寄生组合式继承 | 解决组合继承的性能问题 | 实现复杂 |
| ES6 类继承 | 语法简洁,解决传统继承方式的缺点 | 需要支持 ES6 的浏览器或环境 |
根据需求选择合适的继承方式,可以编写出更高效、更易维护的 JavaScript 代码。
前端缓存与数据持久化的理解
前端缓存和数据持久化是提升应用性能、优化用户体验的重要手段。以下是它们的详细说明和应用场景:
一、前端缓存
前端缓存是指将数据或资源存储在客户端(如浏览器),以减少网络请求、加快资源加载速度。
- HTTP 缓存
- 使用 Service Worker 拦截网络请求,实现离线缓存和资源预加载。
- LocalStorage 和 SessionStorage
- 提供结构化数据的存储,适合存储大量数据。
IndexedDB
const request = indexedDB.open('myDatabase',1); request.onsuccess=(event)=>{const db = event.target.result;const transaction = db.transaction('myStore','readwrite');const store = transaction.objectStore('myStore'); store.add({id:1,name:'John'});};SessionStorage:会话级存储数据,关闭浏览器后数据清空。
sessionStorage.setItem('key','value'); console.log(sessionStorage.getItem('key'));// 输出:value LocalStorage:持久化存储数据,关闭浏览器后数据仍然存在。
localStorage.setItem('key','value'); console.log(localStorage.getItem('key'));// 输出:value Service Worker 缓存
self.addEventListener('install',(event)=>{ event.waitUntil( caches.open('v1').then((cache)=>{return cache.addAll(['/','/styles.css','/app.js']);}));});协商缓存:通过 ETag 和 Last-Modified 响应头验证资源是否过期。
ETag: "abc123" Last-Modified: Wed, 01 Jan 2020 00:00:00 GMT 强缓存:通过 Cache-Control 和 Expires 响应头控制资源的缓存时间。
Cache-Control: max-age=3600 Expires: Wed, 01 Jan 2025 00:00:00 GMT 二、前端数据持久化
前端数据持久化是指将数据存储在客户端,即使页面刷新或关闭浏览器后数据仍然存在。
- 适合存储简单的键值对数据。
- 适合存储会话级数据,关闭浏览器后数据清空。
- 适合存储大量结构化数据,支持复杂查询。
- 适合存储小量数据,支持设置过期时间。
- 使用
FileReader 和Blob 缓存文件数据。
文件缓存
const file =newBlob(['Hello, world!'],{type:'text/plain'});const reader =newFileReader(); reader.onload=(event)=>{ console.log(event.target.result);// 输出:Hello, world! }; reader.readAsText(file);Cookies
document.cookie ='username=John; expires=Wed, 01 Jan 2025 00:00:00 GMT; path=/'; console.log(document.cookie);// 输出:username=John IndexedDB
const request = indexedDB.open('myDatabase',1); request.onsuccess=(event)=>{const db = event.target.result;const transaction = db.transaction('myStore','readwrite');const store = transaction.objectStore('myStore'); store.add({id:1,name:'John'});};SessionStorage
sessionStorage.setItem('token','abc123'); console.log(sessionStorage.getItem('token'));// 输出:abc123 LocalStorage
localStorage.setItem('user',JSON.stringify({name:'Alice'}));const user =JSON.parse(localStorage.getItem('user')); console.log(user);// 输出:{ name: 'Alice' } 三、应用场景
- 前端缓存
- 减少网络请求,加快资源加载速度。
- 实现离线访问和资源预加载。
- 数据持久化
- 保存用户偏好设置(如主题、语言)。
- 存储用户登录状态(如 Token)。
- 缓存 API 响应数据,减少服务器请求。
四、总结
- 前端缓存
- 通过 HTTP 缓存、Service Worker、LocalStorage 等方式缓存资源,提升性能。
- 数据持久化
- 使用 LocalStorage、SessionStorage、IndexedDB 等方式持久化数据,确保数据在页面刷新或关闭后仍然存在。
require 和 import 的区别详解
require 和 import 是 JavaScript 中用于加载模块的两种方式,分别用于 CommonJS 和 ES6 模块系统。以下是它们的详细对比:
一、语法和使用方式
-
require (CommonJS)- 动态加载:可以在代码的任何位置使用
require。
- 动态加载:可以在代码的任何位置使用
-
import (ES6 Modules)- 静态加载:必须在模块的顶层使用
import,不能在代码块中动态加载。
- 静态加载:必须在模块的顶层使用
示例:
import fs from'fs';const data = fs.readFileSync('file.txt','utf8');语法:
import module from'module-name';示例:
const fs =require('fs');const data = fs.readFileSync('file.txt','utf8');语法:
const module =require('module-name');二、加载方式
-
require- 同步加载:模块加载是同步执行的,会阻塞后续代码的执行。
- 运行时加载:模块在运行时加载并执行。
-
import- 异步加载:模块加载是异步的,不会阻塞后续代码的执行。
- 编译时加载:模块在代码编译时加载,支持静态分析。
三、模块导出
-
require- 使用
module.exports 或exports 导出模块。
- 使用
-
import- 使用
export 或export default 导出模块。
- 使用
示例:
// module.js exportconst foo ='bar';// app.js import{ foo }from'./module'; console.log(foo);// 输出:bar 示例:
// module.js module.exports ={foo:'bar'};// app.js const module =require('./module'); console.log(module.foo);// 输出:bar 四、适用场景
-
require- 适用于 Node.js 环境,主要用于后端开发。
- 支持动态加载,适合需要根据条件加载模块的场景。
-
import- 适用于现代浏览器和前端开发,支持静态分析和 Tree Shaking。
- 适合模块化开发和代码优化。
五、兼容性
-
require- 在 Node.js 环境中原生支持,但在浏览器中需要使用打包工具(如 Webpack、Browserify)进行转换。
-
import- 在现代浏览器中支持,但在 Node.js 中需要使用
.mjs 文件或在package.json 中设置"type": "module"。
- 在现代浏览器中支持,但在 Node.js 中需要使用
六、总结
| 特性 | require(CommonJS) | import(ES6 Modules) |
|---|---|---|
| 语法 | const module = require('module') | import module from 'module' |
| 加载方式 | 同步加载,运行时加载 | 异步加载,编译时加载 |
| 模块导出 | module.exports 或 exports | export 或 export default |
| 适用场景 | Node.js 环境,动态加载 | 现代浏览器,模块化开发 |
| 兼容性 | Node.js 原生支持 | 现代浏览器支持,Node.js 需配置 |
px、em 和 rem 的区别详解
px、em 和 rem 是 CSS 中常用的长度单位,它们在网页布局和响应式设计中扮演着重要角色。以下是它们的详细对比:
一、px(像素)
- 定义
-
px 是绝对单位,表示屏幕上的一个像素点。
-
- 特点
- 固定大小,不受父元素或根元素的影响。
- 适合需要精确控制尺寸的场景。
示例
.element{width: 100px;height: 50px;}二、em
- 定义
-
em 是相对单位,基于当前元素的字体大小(font-size)。
-
- 特点
- 默认情况下,
1em 等于当前元素的font-size 值。 - 如果当前元素未设置
font-size,则继承父元素的font-size。 - 适合需要根据字体大小调整布局的场景。
- 默认情况下,
示例
.parent{font-size: 16px;}.child{font-size: 1.5em;/* 16px * 1.5 = 24px */padding: 1em;/* 24px */}三、rem
- 定义
-
rem 是相对单位,基于根元素(<html>)的字体大小(font-size)。
-
- 特点
- 默认情况下,
1rem 等于根元素的font-size 值(通常为16px)。 - 不受父元素
font-size 的影响,适合全局统一的布局调整。
- 默认情况下,
示例
html{font-size: 16px;}.element{font-size: 1.5rem;/* 16px * 1.5 = 24px */padding: 1rem;/* 16px */}四、对比与总结
| 特性 | px | em | rem |
|---|---|---|---|
| 定义 | 绝对单位,表示像素 | 相对单位,基于当前元素的 font-size | 相对单位,基于根元素的 font-size |
| 特点 | 固定大小,不受其他元素影响 | 受当前元素和父元素的 font-size 影响 | 仅受根元素的 font-size 影响 |
| 适用场景 | 需要精确控制尺寸的场景 | 需要根据字体大小调整布局的场景 | 全局统一的布局调整 |
五、使用建议
-
px- 适合需要精确控制尺寸的场景,如边框、阴影等。
-
em- 适合需要根据字体大小调整布局的场景,如按钮、标题等。
-
rem- 适合全局统一的布局调整,如间距、容器宽度等。
六、示例
html{font-size: 16px;}.container{width: 100%;padding: 1rem;/* 16px */}.title{font-size: 2rem;/* 32px */margin-bottom: 1em;/* 32px */}.button{font-size: 1.25em;/* 继承父元素的字体大小 */padding: 0.5em 1em;/* 根据字体大小调整 */}通过理解 px、em 和 rem 的区别,可以根据需求选择合适的单位,实现更灵活的布局和响应式设计。
常见的浏览器兼容性问题
在开发前端项目时,浏览器兼容性问题是一个常见的挑战。以下是常见的兼容性问题及其解决方案:
一、CSS 兼容性问题
- 问题:IE 盒模型的
width 和height 包括padding 和border,而标准盒模型不包括。 - 解决方案:使用
box-sizing: border-box 统一盒模型。 - 问题:旧版浏览器(如 IE 10)对 Flexbox 的支持不完善。
- 解决方案:使用
autoprefixer 自动添加浏览器前缀。 - 问题:旧版浏览器(如 IE 11)不支持 CSS Grid。
- 解决方案:使用
@supports 提供回退方案。
CSS Grid 兼容性
.container{display: grid;}@supportsnot(display: grid){.container{display: flex;}}Flexbox 兼容性
.container{display: flex;}盒模型差异
*{box-sizing: border-box;}二、JavaScript 兼容性问题
- 问题:旧版浏览器(如 IE 11)不支持 ES6+ 语法(如
let、const、箭头函数)。 - 解决方案:使用 Babel 将 ES6+ 代码转换为 ES5。
- 问题:旧版浏览器不支持现代 API(如
fetch、Promise)。 - 解决方案:使用 Polyfill 提供兼容性支持。
API 兼容性
npminstall whatwg-fetch --saveimport'whatwg-fetch';fetch('https://api.example.com/data').then(response=> response.json()).then(data=> console.log(data));ES6+ 语法兼容性
npminstall @babel/core @babel/preset-env --save-dev // .babelrc {"presets":["@babel/preset-env"]}三、HTML 兼容性问题
- 问题:旧版浏览器(如 IE 8)不支持 HTML5 标签(如
<header>、<footer>)。 - 解决方案:使用
html5shiv 提供兼容性支持。 - 表单控件兼容性
- 问题:旧版浏览器对 HTML5 表单控件(如
date、range)的支持不完善。 - 解决方案:使用 Polyfill 或 JavaScript 库(如
jQuery UI)。
- 问题:旧版浏览器对 HTML5 表单控件(如
HTML5 标签兼容性
<!--[if lt IE 9]> <script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js"></script> <![endif]-->四、其他兼容性问题
- 问题:不同操作系统和浏览器的默认字体不同,可能导致样式不一致。
- 解决方案:使用 Web 安全字体或
@font-face 加载自定义字体。 - 问题:旧版浏览器不支持 WebP 格式图片。
- 解决方案:使用
<picture> 标签提供回退方案。
图片兼容性
<picture><sourcesrcset="image.webp"type="image/webp"><imgsrc="image.jpg"alt="Fallback Image"></picture>字体兼容性
body{font-family: Arial, sans-serif;}五、总结
| 问题类型 | 常见问题 | 解决方案 |
|---|---|---|
| CSS 兼容性 | 盒模型差异、Flexbox、CSS Grid | 使用 box-sizing、autoprefixer、@supports |
| JavaScript 兼容性 | ES6+ 语法、现代 API | 使用 Babel、Polyfill |
| HTML 兼容性 | HTML5 标签、表单控件 | 使用 html5shiv、Polyfill |
| 其他兼容性 | 字体、图片 | 使用 Web 安全字体、<picture> 标签 |
通过理解常见的浏览器兼容性问题及其解决方案,可以有效提升前端项目的兼容性和用户体验。
CSS 盒模型
CSS 盒模型(Box Model)是网页布局的基础,它定义了元素的尺寸、内边距、边框和外边距之间的关系。以下是盒模型的详细说明:
一、盒模型的组成部分
盒模型由以下四部分组成:
- 内容区域(Content)
- 元素的实际内容,如文本、图片等。
- 通过
width 和height 设置内容区域的尺寸。
- 内边距(Padding)
- 内容区域与边框之间的空白区域。
- 通过
padding 设置内边距。
- 边框(Border)
- 内边距与外边距之间的边框。
- 通过
border 设置边框的样式、宽度和颜色。
- 外边距(Margin)
- 元素与其他元素之间的空白区域。
- 通过
margin 设置外边距。
二、标准盒模型与 IE 盒模型
- 标准盒模型(Content-Box)
- 元素的
width 和height 仅包括内容区域。 - 总宽度 =
width +padding +border +margin - 总高度 =
height +padding +border +margin
- 元素的
- IE 盒模型(Border-Box)
- 元素的
width 和height 包括内容区域、内边距和边框。 - 总宽度 =
width +margin - 总高度 =
height +margin - 使用
box-sizing 属性切换盒模型。
- 元素的
切换盒模型
/* 标准盒模型 */.element{box-sizing: content-box;}/* IE 盒模型 */.element{box-sizing: border-box;}三、盒模型的计算
- 总宽度 =
200px +10px * 2 +5px * 2 +20px * 2 =270px - 总高度 =
height +padding * 2 +border * 2 +margin * 2 - 总宽度 =
200px +20px * 2 =240px - 总高度 =
height +margin * 2
IE 盒模型计算
.element{width: 200px;padding: 10px;border: 5px solid black;margin: 20px;box-sizing: border-box;}标准盒模型计算
.element{width: 200px;padding: 10px;border: 5px solid black;margin: 20px;box-sizing: content-box;}四、盒模型的应用
- 使用
padding 和margin 调整元素之间的间距。 - 使用
border 设置元素的边框样式。 - 使用
box-sizing: border-box 简化布局计算。
盒模型切换
*{box-sizing: border-box;}边框样式
.element{border: 2px solid red;border-radius: 10px;}布局控制
.container{padding: 20px;}.item{margin: 10px;}五、总结
- 盒模型的组成部分
- 内容区域、内边距、边框和外边距。
- 盒模型的类型
- 标准盒模型:
width 和height 仅包括内容区域。 - IE 盒模型:
width 和height 包括内容区域、内边距和边框。
- 标准盒模型:
- 盒模型的应用
- 使用
padding 和margin 调整布局,使用box-sizing 切换盒模型。
- 使用
ES6(ECMAScript 2015)新特性
ES6 是 JavaScript 的重要更新,引入了许多新特性,使代码更简洁、更易读、更强大。以下是 ES6 的主要新特性:
一、变量声明
-
let 用于声明块级作用域的变量。 -
const 用于声明常量,值不可重新赋值。
let 和const
let x =10;const y =20;二、箭头函数
- 使用
=> 定义函数,简化函数书写。 - 特点
- 没有自己的
this,this 继承自外层作用域。 - 不能作为构造函数使用。
- 没有自己的
语法
constadd=(a, b)=> a + b;三、模板字符串
- 使用反引号(
`)定义字符串,支持换行和嵌入变量。
语法
const name ='Alice';const message =`Hello, ${name}!`;四、解构赋值
对象解构
const{ name, age }={name:'Alice',age:25}; console.log(name);// 输出:Alice 数组解构
const[a, b]=[1,2]; console.log(a);// 输出:1 五、默认参数
- 为函数参数设置默认值。
语法
functiongreet(name ='Guest'){ console.log(`Hello, ${name}!`);}greet();// 输出:Hello, Guest! 六、扩展运算符
对象扩展
const obj1 ={a:1,b:2};const obj2 ={...obj1,c:3}; console.log(obj2);// 输出:{ a: 1, b: 2, c: 3 } 数组扩展
const arr1 =[1,2];const arr2 =[...arr1,3,4]; console.log(arr2);// 输出:[1, 2, 3, 4] 七、类(Class)
- 使用
class 关键字定义类,支持构造函数和继承。
语法
classPerson{constructor(name){this.name = name;}greet(){ console.log(`Hello, ${this.name}!`);}}const person =newPerson('Alice'); person.greet();// 输出:Hello, Alice! 八、模块化
导入模块
// app.js import{ add }from'./module'; console.log(add(1,2));// 输出:3 导出模块
// module.js exportconstadd=(a, b)=> a + b;九、Promise
- 用于处理异步操作,支持链式调用。
语法
const promise =newPromise((resolve, reject)=>{setTimeout(()=>resolve('Success'),1000);}); promise.then(result=> console.log(result));// 输出:Success 十、Symbol
- 创建唯一的标识符,避免属性名冲突。
语法
const id =Symbol('id');const obj ={[id]:123}; console.log(obj[id]);// 输出:123 十一、迭代器和生成器
- 使用
Symbol.iterator 定义迭代器。 - 使用
function* 定义生成器函数。
生成器
function*generator(){yield1;yield2;}const gen =generator(); console.log(gen.next().value);// 输出:1 迭代器
const iterable ={[Symbol.iterator](){let count =0;return{next(){return{value: count++,done: count >3};}};}};for(const value of iterable){ console.log(value);// 输出:0, 1, 2 }十二、Set 和 Map
- 存储唯一值的集合。
- 存储键值对的集合,键可以是任意类型。
Map
const map =newMap(); map.set('name','Alice'); console.log(map.get('name'));// 输出:Alice Set
const set =newSet([1,2,3]); set.add(4); console.log(set.has(2));// 输出:true HTTP 强缓存与协商缓存
HTTP 缓存是提升网页性能的重要手段,分为强缓存和协商缓存。以下是它们的详细说明:
一、强缓存
- 定义
- 强缓存是指浏览器直接从本地缓存中读取资源,不发送请求到服务器。
- 实现方式
- 特点
- 缓存期间不发送请求到服务器,直接从缓存中读取资源。
- 适合缓存静态资源(如 CSS、JavaScript、图片)。
示例
Cache-Control: max-age=3600 Expires: Wed, 01 Jan 2025 00:00:00 GMT Expires:通过指定过期时间设置缓存的有效时间。
Expires: Wed, 01 Jan 2025 00:00:00 GMT Cache-Control:通过 max-age 设置缓存的有效时间。
Cache-Control: max-age=3600 二、协商缓存
- 定义
- 协商缓存是指浏览器发送请求到服务器,由服务器判断资源是否过期,决定是否使用缓存。
- 实现方式
- 服务器返回资源的最后修改时间(
Last-Modified)。 - 浏览器下次请求时带上
If-Modified-Since,服务器判断资源是否修改。 - 服务器返回资源的唯一标识(
ETag)。 - 浏览器下次请求时带上
If-None-Match,服务器判断资源是否修改。
- 服务器返回资源的最后修改时间(
- 特点
- 每次请求都会发送到服务器,由服务器判断是否使用缓存。
- 适合缓存动态资源(如 API 响应)。
示例
Last-Modified: Wed, 01 Jan 2020 00:00:00 GMT ETag: "abc123" ETag 和If-None-Match:
ETag: "abc123" If-None-Match: "abc123" Last-Modified 和If-Modified-Since:
Last-Modified: Wed, 01 Jan 2020 00:00:00 GMT If-Modified-Since: Wed, 01 Jan 2020 00:00:00 GMT 三、强缓存与协商缓存的区别
| 特性 | 强缓存 | 协商缓存 |
|---|---|---|
| 是否发送请求 | 否 | 是 |
| 缓存判断方式 | 浏览器直接判断 | 服务器判断 |
| 实现机制 | Cache-Control、Expires | Last-Modified、ETag |
| 适用场景 | 静态资源 | 动态资源 |
四、缓存策略建议
- 静态资源
- 使用强缓存,设置较长的缓存时间(如
max-age=31536000)。 - 通过文件哈希或版本号更新缓存(如
style.[hash].css)。
- 使用强缓存,设置较长的缓存时间(如
- 动态资源
- 使用协商缓存,确保数据的最新性。
- 设置较短的缓存时间(如
max-age=60)。
五、总结
- 强缓存
- 通过
Cache-Control 和Expires 实现,适合缓存静态资源。
- 通过
- 协商缓存
- 通过
Last-Modified 和ETag 实现,适合缓存动态资源。
- 通过
Vue Router 的两种模式
Vue Router 是 Vue.js 官方的路由管理器,支持两种模式:Hash 模式和History 模式。以下是它们的详细说明:
一、Hash 模式
- 定义
- Hash 模式使用 URL 的哈希部分(
#)来实现路由。
- Hash 模式使用 URL 的哈希部分(
- 特点
- URL 示例:
http://example.com/#/home - 哈希部分的变化不会导致页面刷新。
- 兼容性好,支持所有浏览器。
- URL 示例:
- 优点
- 无需服务器配置,适合静态站点。
- 兼容性好,支持旧版浏览器。
- 缺点
- URL 中包含
#,不够美观。
- URL 中包含
配置
const router =newVueRouter({mode:'hash',routes:[{path:'/home',component: Home },{path:'/about',component: About }]});二、History 模式
- 定义
- History 模式使用 HTML5 的
history.pushState API 来实现路由。
- History 模式使用 HTML5 的
- 特点
- URL 示例:
http://example.com/home - URL 更美观,不包含
#。 - 需要服务器配置,避免刷新时返回 404 错误。
- URL 示例:
- 服务器配置
- 优点
- URL 更美观,不包含
#。 - 支持 SEO,适合动态站点。
- URL 更美观,不包含
- 缺点
- 需要服务器配置,兼容性较差。
Apache:
<IfModule mod_rewrite.c> RewriteEngine On RewriteBase / RewriteRule ^index\.html$ - [L] RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule . /index.html [L] </IfModule> Nginx:
location / { try_files $uri $uri/ /index.html; } 配置
const router =newVueRouter({mode:'history',routes:[{path:'/home',component: Home },{path:'/about',component: About }]});三、Hash 模式与 History 模式的对比
| 特性 | Hash 模式 | History 模式 |
|---|---|---|
| URL 示例 | http://example.com/#/home | http://example.com/home |
| 是否需要服务器配置 | 否 | 是 |
| 兼容性 | 兼容所有浏览器 | 需要 HTML5 支持 |
| SEO 支持 | 不支持 | 支持 |
| 适用场景 | 静态站点 | 动态站点 |
四、总结
- Hash 模式
- 适合静态站点,无需服务器配置,兼容性好。
- History 模式
- 适合动态站点,URL 更美观,支持 SEO,但需要服务器配置。
JavaScript 的缺点
JavaScript 是一种强大的脚本语言,广泛应用于前端和后端开发。然而,它也存在一些缺点,以下是主要的缺点及其影响:
一、单线程模型
- 问题
- JavaScript 是单线程的,同一时间只能执行一个任务。
- 长时间运行的任务会阻塞主线程,导致页面卡顿或无响应。
- 解决方案
- 使用异步编程(如
Promise、async/await)。 - 使用 Web Workers 在后台执行耗时任务。
- 使用异步编程(如
二、弱类型语言
- 问题
- JavaScript 是弱类型语言,变量的类型可以动态改变,容易引发运行时错误。
- 解决方案
- 使用 TypeScript 提供静态类型检查。
- 使用工具(如 ESLint)进行代码检查。
示例:
let x =10; x ='Hello';// 类型改变,可能导致错误 三、浏览器兼容性问题
- 问题
- 不同浏览器对 JavaScript 的支持不一致,可能导致代码在不同浏览器中表现不同。
- 示例:IE 浏览器不支持 ES6+ 语法。
- 解决方案
- 使用 Babel 将 ES6+ 代码转换为 ES5。
- 使用 Polyfill 提供兼容性支持。
四、安全问题
- 问题
- JavaScript 代码在客户端执行,容易被恶意用户篡改或攻击。
- 常见安全问题:XSS(跨站脚本攻击)、CSRF(跨站请求伪造)。
- 解决方案
- 对用户输入进行严格的验证和过滤。
- 使用 HTTPS 加密数据传输。
五、性能问题
- 问题
- JavaScript 是解释型语言,执行速度比编译型语言慢。
- 复杂的 DOM 操作和频繁的重绘/重排会影响性能。
- 解决方案
- 使用虚拟 DOM(如 React)优化 DOM 操作。
- 使用
requestAnimationFrame 优化动画性能。
六、调试困难
- 问题
- JavaScript 的错误提示不够明确,调试起来比较困难。
- 异步代码的调试更加复杂。
- 解决方案
- 使用开发者工具(如 Chrome DevTools)进行调试。
- 使用
console.log 或debugger 语句辅助调试。
七、生态系统碎片化
- 问题
- JavaScript 的生态系统庞大且碎片化,工具和库的选择过多,学习成本高。
- 示例:前端框架有 React、Vue、Angular 等。
- 解决方案
- 根据项目需求选择合适的工具和库。
- 关注主流技术趋势,避免过度依赖小众工具。
八、总结
| 缺点 | 问题描述 | 解决方案 |
|---|---|---|
| 单线程模型 | 长时间任务阻塞主线程 | 使用异步编程、Web Workers |
| 弱类型语言 | 变量类型动态改变,容易引发运行时错误 | 使用 TypeScript、ESLint |
| 浏览器兼容性 | 不同浏览器支持不一致 | 使用 Babel、Polyfill |
| 安全问题 | 容易被恶意用户篡改或攻击 | 验证用户输入、使用 HTTPS |
| 性能问题 | 执行速度慢,复杂 DOM 操作影响性能 | 使用虚拟 DOM、优化动画 |
| 调试困难 | 错误提示不明确,异步代码调试复杂 | 使用开发者工具、console.log |
| 生态系统碎片化 | 工具和库选择过多,学习成本高 | 根据需求选择工具,关注主流技术趋势 |
理解 JavaScript 的缺点及其解决方案,可以帮助开发者更好地应对挑战,编写更高效、更安全的代码。
JavaScript 原型
JavaScript 是一种基于原型的语言,原型(Prototype)是 JavaScript 实现继承和共享属性和方法的核心机制。以下是原型的详细说明:
一、原型的概念
- 原型对象(Prototype Object)
- 每个 JavaScript 对象(除了
null)都有一个原型对象,对象从原型对象继承属性和方法。
- 每个 JavaScript 对象(除了
- 原型链(Prototype Chain)
- 当访问对象的属性或方法时,如果对象本身没有该属性或方法,JavaScript 会沿着原型链向上查找,直到找到为止。
二、原型的使用
- 每个对象都有一个
__proto__ 属性,指向其原型对象。 - 每个函数都有一个
prototype 属性,指向该函数的原型对象。 - 通过构造函数创建的对象,其原型对象为构造函数的
prototype 属性。
构造函数与原型
functionPerson(name){this.name = name;}Person.prototype.greet=function(){ console.log(`Hello, ${this.name}!`);};const person =newPerson('Alice'); person.greet();// 输出:Hello, Alice! prototype 属性
functionPerson(){} console.log(Person.prototype);// 输出:Person {} __proto__ 属性
const obj ={}; console.log(obj.__proto__);// 输出:Object.prototype 三、原型链的示例
-
obj 本身没有toString 方法,但它的原型对象Object.prototype 有toString 方法。 - 原型链图示 obj -> Object.prototype -> null
示例
const obj ={}; console.log(obj.toString());// 输出:[object Object] 四、原型的继承
- 通过将子类的原型对象指向父类的实例,实现继承。
- 使用
Object.create() 创建新对象,并指定其原型对象。
Object.create()
const parent ={name:'Parent',sayHello:function(){ console.log(`Hello, ${this.name}!`);}};const child = Object.create(parent); child.name ='Child'; child.sayHello();// 输出:Hello, Child! 原型继承
functionParent(){this.name ='Parent';}Parent.prototype.sayHello=function(){ console.log(`Hello, ${this.name}!`);};functionChild(){}Child.prototype =newParent();const child =newChild(); child.sayHello();// 输出:Hello, Parent! 五、总结
- 原型对象
- 每个对象都有一个原型对象,对象从原型对象继承属性和方法。
- 原型链
- 当访问对象的属性或方法时,JavaScript 会沿着原型链向上查找。
- 原型继承
- 通过将子类的原型对象指向父类的实例,实现继承。
XSS 和 CSRF 详解及防御措施
XSS(跨站脚本攻击)和 CSRF(跨站请求伪造)是两种常见的 Web 安全漏洞,以下是它们的详细说明及防御措施:
一、XSS(跨站脚本攻击)
- 定义
- XSS 是指攻击者在网页中注入恶意脚本,当其他用户访问该网页时,脚本会在用户浏览器中执行。
- 类型
- 存储型 XSS:恶意脚本存储在服务器中,用户访问时触发。
- 反射型 XSS:恶意脚本通过 URL 参数传递,用户点击链接时触发。
- DOM 型 XSS:恶意脚本通过修改 DOM 触发,不依赖服务器响应。
- 危害
- 窃取用户 Cookie 或会话信息。
- 篡改网页内容,诱导用户进行恶意操作。
- 防御措施
- 输出编码:在输出用户输入时进行编码,防止脚本执行。
- 使用
Content-Security-Policy 限制脚本加载来源。
- 使用
- 使用
HttpOnly 标记 Cookie,防止 JavaScript 访问。
- 输出编码:在输出用户输入时进行编码,防止脚本执行。
设置 HTTP 头:
Content-Security-Policy: default-src 'self' Set-Cookie: sessionId=123; HttpOnly; Secure 输入过滤:对用户输入进行严格的验证和过滤,移除或转义特殊字符。
functionescapeHTML(str){return str.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''');}二、CSRF(跨站请求伪造)
- 定义
- CSRF 是指攻击者诱导用户在已登录的网站上执行非预期的操作。
- 攻击流程
- 用户登录目标网站,获得认证 Cookie。
- 用户访问攻击者的网站,攻击者诱导用户发送恶意请求。
- 目标网站接收到请求,误认为是用户的操作。
- 危害
- 执行非预期的操作(如转账、修改密码)。
- 防御措施
- 检查
Referer 头,确保请求来自合法来源。 - 在表单或请求中添加 CSRF Token,服务器验证 Token 的有效性。
- 使用
SameSite 属性限制 Cookie 的发送范围。
- 检查
设置 SameSite Cookie:
Set-Cookie: sessionId=123; SameSite=Strict; Secure 使用 CSRF Token:
<formaction="/transfer"method="POST"><inputtype="hidden"name="_csrf"value="csrf_token"><buttontype="submit">Submit</button></form>验证请求来源:
if(req.headers.referer !=='https://example.com'){return res.status(403).send('Forbidden');}三、总结
| 攻击类型 | 定义 | 防御措施 |
|---|---|---|
| XSS | 攻击者在网页中注入恶意脚本 | 输入过滤、输出编码、设置 HTTP 头 |
| CSRF | 攻击者诱导用户执行非预期的操作 | 验证请求来源、使用 CSRF Token、设置 SameSite Cookie |
通过理解 XSS 和 CSRF 的原理及防御措施,可以有效提升 Web 应用的安全性,保护用户数据和隐私。
HTTP 详解
HTTP(HyperText Transfer Protocol,超文本传输协议)是用于在 Web 浏览器和服务器之间传输数据的协议。以下是 HTTP 的详细说明:
一、HTTP 的基本概念
- 定义
- HTTP 是一种应用层协议,用于在客户端(如浏览器)和服务器之间传输超文本(如 HTML、CSS、JavaScript)。
- 特点
- 无状态:每次请求都是独立的,服务器不会保留客户端的状态信息。
- 无连接:每次请求完成后,连接会关闭(HTTP/1.1 支持持久连接)。
- 简单快速:协议简单,通信速度快。
二、HTTP 的工作流程
- 客户端发送请求
- 客户端(如浏览器)向服务器发送 HTTP 请求,请求中包含请求方法、URL、请求头和请求体。
- 服务器处理请求
- 服务器接收请求,根据请求内容进行处理,并生成响应。
- 服务器返回响应
- 服务器向客户端发送 HTTP 响应,响应中包含状态码、响应头和响应体。
- 客户端处理响应
- 客户端接收响应,根据响应内容进行处理(如渲染页面、执行脚本)。
三、HTTP 请求和响应
- HTTP 请求
- 请求体:包含请求的数据(如表单数据、JSON)。
- HTTP 响应
- 响应体:包含响应的数据(如 HTML、JSON)。
响应头:包含响应的元信息(如 Content-Type、Content-Length)。
Content-Type: text/html Content-Length: 1234 状态行:包含 HTTP 版本、状态码和状态描述。
HTTP/1.1 200 OK 请求头:包含请求的元信息(如 Host、User-Agent)。
Host: example.com User-Agent: Mozilla/5.0 请求行:包含请求方法、URL 和 HTTP 版本。
GET /index.html HTTP/1.1 四、HTTP 方法
- 请求获取指定资源,没有请求体。
- 向指定资源提交数据,有请求体。
- 更新指定资源,有请求体。
- 删除指定资源,没有请求体。
DELETE
DELETE /delete HTTP/1.1 PUT
PUT /update HTTP/1.1 Content-Type: application/json {"name": "Bob"} POST
POST /submit HTTP/1.1 Content-Type: application/json {"name": "Alice"} GET
GET /index.html HTTP/1.1 五、HTTP 状态码
- 1xx(信息性状态码)
- 表示请求已被接收,需要继续处理。
- 示例:
100 Continue
- 2xx(成功状态码)
- 表示请求已成功被服务器接收、理解并处理。
- 示例:
200 OK、201 Created
- 3xx(重定向状态码)
- 表示需要客户端进一步操作以完成请求。
- 示例:
301 Moved Permanently、302 Found
- 4xx(客户端错误状态码)
- 表示客户端发送的请求有错误。
- 示例:
400 Bad Request、404 Not Found
- 5xx(服务器错误状态码)
- 表示服务器在处理请求时发生错误。
- 示例:
500 Internal Server Error、503 Service Unavailable
六、HTTP 版本
- HTTP/1.0
- 每次请求完成后,连接会关闭。
- HTTP/1.1
- 支持持久连接,允许多个请求复用同一个连接。
- HTTP/2
- 支持多路复用,减少延迟,提高性能。
- HTTP/3
- 基于 QUIC 协议,进一步优化性能。
七、总结
HTTP 是 Web 开发的基础协议,用于在客户端和服务器之间传输数据。理解 HTTP 的工作流程、请求和响应、方法、状态码和版本,可以帮助开发者更好地进行 Web 开发,优化应用性能
JavaScript 分片(Chunking)
分片(Chunking)是一种将大数据集或任务分割成多个小块(Chunk)进行处理的技术,常用于优化性能、减少内存占用或实现渐进式加载。以下是 JavaScript 中实现分片的常见方法:
一、分片的应用场景
- 大数据处理
- 将大数据集分割成小块,分批处理,避免内存溢出。
- 文件上传
- 将大文件分割成多个小文件上传,提高上传效率和容错性。
- 渐进式加载
- 分批加载数据或资源,提升用户体验。
- 任务调度
- 将耗时任务分割成多个小任务,避免阻塞主线程。
二、分片的实现方法
1. 数组分片
将大数组分割成多个小数组,分批处理。
functionchunkArray(array, size){const chunks =[];for(let i =0; i < array.length; i += size){ chunks.push(array.slice(i, i + size));}return chunks;}const data =[1,2,3,4,5,6,7,8,9];const chunks =chunkArray(data,3); console.log(chunks);// 输出:[[1, 2, 3], [4, 5, 6], [7, 8, 9]] 2. 文件分片
将大文件分割成多个小文件,用于上传或处理。
functionchunkFile(file, size){const chunks =[];let start =0;while(start < file.size){ chunks.push(file.slice(start, start + size)); start += size;}return chunks;}const file =newFile(['a'.repeat(1024*1024)],'example.txt');const chunks =chunkFile(file,1024*100);// 每片 100KB console.log(chunks);// 输出:[Blob, Blob, ...] 3. 任务分片
将耗时任务分割成多个小任务,使用 setTimeout 或 requestAnimationFrame 分批执行。
functionchunkTask(task, data, size, callback){let index =0;functionrun(){for(let i =0; i < size && index < data.length; i++){task(data[index]); index++;}if(index < data.length){setTimeout(run,0);// 分批执行 }else{callback();}}run();}const data =newArray(1000).fill(0).map((_, i)=> i);chunkTask((item)=> console.log(item), data,100,()=> console.log('任务完成'));4. 数据流分片
使用 ReadableStream 将数据流分割成多个小块,实现渐进式加载或处理。
asyncfunctionchunkStream(stream, size){const reader = stream.getReader();const chunks =[];let result;while(!(result =await reader.read()).done){ chunks.push(result.value.slice(0, size));}return chunks;}const stream =newReadableStream({start(controller){for(let i =0; i <10; i++){ controller.enqueue(newUint8Array([i]));} controller.close();}});chunkStream(stream,2).then(chunks=> console.log(chunks));5. 使用 Web Workers 分片
将任务分片后交给 Web Workers 并行处理。
// main.js const worker =newWorker('worker.js');const data =newArray(1000).fill(0).map((_, i)=> i);const chunkSize =100;for(let i =0; i < data.length; i += chunkSize){const chunk = data.slice(i, i + chunkSize); worker.postMessage(chunk);} worker.onmessage=(event)=>{ console.log('处理结果:', event.data);};// worker.js self.onmessage=(event)=>{const result = event.data.map(item=> item *2); self.postMessage(result);};三、总结
| 分片类型 | 适用场景 | 实现方法 |
|---|---|---|
| 数组分片 | 处理大数据集 | 使用 slice 分割数组 |
| 文件分片 | 大文件上传或处理 | 使用 File.slice 分割文件 |
| 任务分片 | 避免阻塞主线程 | 使用 setTimeout 或 requestAnimationFrame |
| 数据流分片 | 渐进式加载或处理数据流 | 使用 ReadableStream 分割数据流 |
| Web Workers | 并行处理任务 | 使用 Web Workers 分片任务 |
通过分片技术,可以优化 JavaScript 应用的性能、内存占用和用户体验。根据具体需求选择合适的分片方法,实现高效
懒加载(Lazy Loading)的原理
懒加载是一种优化技术,延迟加载非关键资源(如图片、视频、脚本等),直到用户需要访问它们时再加载。以下是懒加载的原理、实现方法和应用场景:
一、懒加载的原理
- 延迟加载
- 懒加载的核心思想是延迟加载非关键资源,减少页面初始加载时间。
- 按需加载
- 当资源进入用户的可视区域(Viewport)时,再加载资源。
- 事件监听
- 通过监听
scroll、resize 等事件,判断资源是否进入可视区域。
- 通过监听
二、懒加载的实现方法
1. 图片懒加载
将图片的 src 属性替换为 data-src,当图片进入可视区域时,再将 data-src 的值赋给 src。
<imgdata-src="image.jpg"alt="Lazy Loaded Image"class="lazyload">document.addEventListener('DOMContentLoaded',()=>{const images = document.querySelectorAll('.lazyload');constlazyLoad=()=>{ images.forEach(img=>{if(img.getBoundingClientRect().top < window.innerHeight &&!img.src){ img.src = img.dataset.src;}});}; window.addEventListener('scroll', lazyLoad); window.addEventListener('resize', lazyLoad);lazyLoad();});2. 视频懒加载
将视频的 src 属性替换为 data-src,当视频进入可视区域时,再加载视频。
<videodata-src="video.mp4"controlsclass="lazyload"></video>document.addEventListener('DOMContentLoaded',()=>{const videos = document.querySelectorAll('.lazyload');constlazyLoad=()=>{ videos.forEach(video=>{if(video.getBoundingClientRect().top < window.innerHeight &&!video.src){ video.src = video.dataset.src; video.load();}});}; window.addEventListener('scroll', lazyLoad); window.addEventListener('resize', lazyLoad);lazyLoad();});3. 使用 Intersection Observer API
Intersection Observer API 是一种更高效的懒加载实现方式,无需监听 scroll 和 resize 事件。
document.addEventListener('DOMContentLoaded',()=>{const images = document.querySelectorAll('.lazyload');const observer =newIntersectionObserver((entries, observer)=>{ entries.forEach(entry=>{if(entry.isIntersecting){const img = entry.target; img.src = img.dataset.src; observer.unobserve(img);}});}); images.forEach(img=> observer.observe(img));});三、懒加载的应用场景
- 图片和视频
- 延迟加载页面中的图片和视频,减少初始加载时间。
- 无限滚动
- 在无限滚动页面中,延迟加载新内容,提升性能。
- 模块化加载
- 延迟加载非关键模块(如脚本、样式),优化页面加载速度。
四、总结
懒加载的核心原理是延迟加载非关键资源,直到用户需要访问它们时再加载。常见的实现方法包括:
- 图片和视频懒加载:通过监听
scroll 和resize 事件实现。 - Intersection Observer API:更高效的懒加载实现方式。
通过懒加载技术,可以显著提升页面加载速度,优化用户体验,减少不必要的资源消耗。
脱离文档流的方法
脱离文档流是指元素不再占据页面布局中的空间,其他元素会忽略其位置。以下是常见的脱离文档流的方法及其特点:
一、position: absolute
- 特点
- 元素脱离文档流,相对于最近的非
static 定位的祖先元素定位。 - 如果没有非
static 定位的祖先元素,则相对于body 定位。
- 元素脱离文档流,相对于最近的非
示例
.element{position: absolute;top: 10px;left: 10px;}二、position: fixed
- 特点
- 元素脱离文档流,相对于浏览器窗口定位。
- 即使页面滚动,元素的位置也不会改变。
示例
.element{position: fixed;top: 10px;left: 10px;}三、float
- 特点
- 元素脱离文档流,向左或向右浮动,其他内容会环绕其周围。
- 需要清除浮动,避免影响后续布局。
示例
.element{float: left;}四、display: none
- 特点
- 元素脱离文档流,并且不占据任何空间。
- 元素及其子元素都会被隐藏。
示例
.element{display: none;}五、visibility: hidden
- 特点
- 元素脱离文档流,但仍然占据空间。
- 元素不可见,但其子元素可以通过
visibility: visible 显示。
示例
.element{visibility: hidden;}六、总结
| 方法 | 特点 | 示例 |
|---|---|---|
position: absolute | 相对于最近的非 static 祖先元素定位 | position: absolute; top: 10px; |
position: fixed | 相对于浏览器窗口定位 | position: fixed; top: 10px; |
float | 脱离文档流,内容环绕其周围 | float: left; |
display: none | 脱离文档流,不占据空间 | display: none; |
visibility: hidden | 脱离文档流,占据空间 | visibility: hidden; |
通过理解这些脱离文档流的方法,可以更好地控制页面布局和元素行为。
JavaScript 中堆和栈的存储方式
在 JavaScript 中,堆(Heap)和栈(Stack)是两种不同的内存存储方式,用于存储不同类型的数据。以下是它们的详细说明:
一、栈(Stack)
- 定义
- 栈是一种后进先出(LIFO)的数据结构,用于存储基本类型的值和函数调用栈。
- 存储内容
- 基本类型:
undefined、null、boolean、number、string、symbol、bigint。 - 函数调用栈:函数的执行上下文(包括局部变量、参数、返回地址等)。
- 基本类型:
- 特点
- 内存分配和释放由系统自动管理。
- 访问速度快,但存储空间有限。
- 数据大小固定,生命周期短(函数执行完毕后自动释放)。
示例
let a =10;// 基本类型,存储在栈中 functionfoo(){let b =20;// 局部变量,存储在栈中 }foo();二、堆(Heap)
- 定义
- 堆是一种动态分配内存的区域,用于存储引用类型的值。
- 存储内容
- 引用类型:
object、array、function、date 等。
- 引用类型:
- 特点
- 内存分配和释放由开发者或垃圾回收机制管理。
- 访问速度较慢,但存储空间较大。
- 数据大小不固定,生命周期较长(需要手动释放或由垃圾回收机制回收)。
示例
let obj ={name:'Alice'};// 引用类型,存储在堆中 let arr =[1,2,3];// 引用类型,存储在堆中 三、堆和栈的存储方式
- 基本类型的存储
- 基本类型的值直接存储在栈中,变量名指向栈中的值。
- 引用类型的存储
- 引用类型的值存储在堆中,变量名指向栈中的内存地址,该地址指向堆中的实际值。
示例
let a =10;// 基本类型,存储在栈中 let b = a;// 复制值,b 也存储在栈中 b =20;// 修改 b,不影响 a let obj1 ={name:'Alice'};// 引用类型,堆中存储对象,栈中存储地址 let obj2 = obj1;// 复制地址,obj2 和 obj1 指向同一对象 obj2.name ='Bob';// 修改 obj2,obj1 也会被修改 四、总结
| 特性 | 栈(Stack) | 堆(Heap) |
|---|---|---|
| 存储内容 | 基本类型、函数调用栈 | 引用类型 |
| 内存管理 | 系统自动管理 | 开发者或垃圾回收机制管理 |
| 访问速度 | 快 | 慢 |
| 存储空间 | 有限 | 较大 |
| 生命周期 | 短(函数执行完毕后自动释放) | 长(需要手动释放或由垃圾回收机制回收) |
理解堆和栈的存储方式,可以帮助开发者更好地管理内存,优化 JavaScript 应用的性能。
v-model 原理
v-model 是 Vue.js 中用于实现双向数据绑定的指令,常用于表单元素(如 <input>、<select>、<textarea>)。以下是 v-model 的实现原理:
一、v-model 的基本用法
- 效果
- 当用户输入内容时,
message 的值会自动更新。 - 当
message 的值改变时,输入框的内容也会自动更新。
- 当用户输入内容时,
语法
<inputv-model="message"/>二、v-model 的实现原理
-
v-model 的本质-
v-model 是v-bind 和v-on 的语法糖,结合了属性绑定和事件监听。
-
-
v-model 的展开形式- 对于
<textarea> 和<select> 元素,v-model 的展开形式类似。
- 对于
-
v-model 的工作流程- 初始化:将
message 的值绑定到输入框的value 属性。 - 输入事件:监听输入框的
input 事件,将用户输入的值赋给message。 - 数据更新:当
message 的值改变时,更新输入框的value 属性。
- 初始化:将
对于 <input> 元素,v-model 的展开形式如下:
<input:value="message"@input="message = $event.target.value"/>三、v-model 的源码解析
- 源码位置
-
v-model 的实现位于 Vue 源码的src/platforms/web/compiler/directives/model.js。
-
- 核心逻辑
- 根据不同的表单元素,生成对应的
v-bind 和v-on 指令。 - 监听表单元素的事件(如
input、change),更新绑定的数据。
- 根据不同的表单元素,生成对应的
示例代码
functionmodel(el, dir, _warn){const value = dir.value;const modifiers = dir.modifiers;const tag = el.tag;const type = el.attrsMap.type;if(tag ==='input'&& type ==='checkbox'){// 处理复选框 }elseif(tag ==='input'&& type ==='radio'){// 处理单选框 }elseif(tag ==='input'|| tag ==='textarea'){// 处理输入框和文本域 genDefaultModel(el, value, modifiers);}elseif(tag ==='select'){// 处理下拉框 }else{// 其他情况 }}四、自定义组件的 v-model
- 默认实现
- 默认情况下,
v-model 在自定义组件中绑定value 属性和input 事件。 - 使用
model 选项自定义v-model 的属性和事件。
- 默认情况下,
自定义v-model
exportdefault{model:{prop:'checked',event:'change'},props:['checked'],template:` <input type="checkbox" :checked="checked" @change="$emit('change', $event.target.checked)" /> `};五、总结
-
v-model 的本质-
v-model 是v-bind 和v-on 的语法糖,用于实现双向数据绑定。
-
-
v-model 的工作流程- 初始化时将数据绑定到表单元素的
value 属性。 - 监听表单元素的事件,更新绑定的数据。
- 初始化时将数据绑定到表单元素的
- 自定义组件的
v-model- 使用
model 选项自定义v-model 的属性和事件。
- 使用
Vue 组件通信的多种方式
在 Vue.js 中,组件通信是开发复杂应用的关键。以下是 Vue 组件通信的多种方式及其适用场景:
一、父子组件通信
-
props 和$emit - 实现父子组件的双向数据绑定。
v-model
// 子组件 exportdefault{model:{prop:'value',event:'input'},props:['value']};// 父组件 <Child v-model="message"/>子组件向父组件传递数据:通过 $emit 触发事件。
// 子组件 this.$emit('update', newValue);// 父组件 <Child @update="handleUpdate"/>父组件向子组件传递数据:通过 props。
// 父组件 <Child :message="message"/>// 子组件 exportdefault{props:['message']};二、兄弟组件通信
- 兄弟组件通过父组件共享数据。
- 创建一个 Vue 实例作为事件中心,实现组件间的通信。
使用 Event Bus
// eventBus.js import Vue from'vue';exportconst EventBus =newVue();// 组件 A EventBus.$emit('update', newValue);// 组件 B EventBus.$on('update',value=>{ console.log(value);});通过父组件中转
// 父组件 <ChildA @update="handleUpdate"/><ChildB :message="message"/>三、跨层级组件通信
- 祖先组件通过
provide 提供数据,后代组件通过inject 注入数据。 - 使用 Vuex 进行全局状态管理,实现组件间的数据共享。
Vuex
// store.js exportdefaultnewVuex.Store({state:{message:'Hello'},mutations:{updateMessage(state, newValue){ state.message = newValue;}}});// 组件 A this.$store.commit('updateMessage', newValue);// 组件 B this.$store.state.message;provide 和inject
// 祖先组件 exportdefault{provide(){return{message:this.message };}};// 后代组件 exportdefault{inject:['message']};四、其他通信方式
- 通过
$refs 直接访问子组件的属性和方法。 - 通过
$parent 访问父组件,通过$children 访问子组件。
$parent 和$children
this.$parent.methodName();this.$children.methodName(); $refs
// 父组件 <Child ref="child"/>this.$refs.child.methodName();五、总结
| 通信方式 | 适用场景 | 示例 |
|---|---|---|
props 和$emit | 父子组件通信 | <Child :message="message" @update="handleUpdate" /> |
v-model | 父子组件双向绑定 | <Child v-model="message" /> |
| Event Bus | 兄弟组件或任意组件通信 | EventBus.$emit('update', newValue) |
provide 和inject | 跨层级组件通信 | provide: { message }, inject: ['message'] |
| Vuex | 全局状态管理 | this.$store.commit('updateMessage', newValue) |
$refs | 直接访问子组件 | this.$refs.child.methodName() |
$parent 和$children | 访问父组件或子组件 | this.$parent.methodName() |
通过理解这些组件通信方式,可以根据具体需求选择最合适的方案,实现高效的组件交互和数据共享。
Diff 算法
Diff 算法是虚拟 DOM(Virtual DOM)的核心算法,用于高效地比较两个虚拟 DOM 树的差异,并最小化实际 DOM 的操作。以下是 Diff 算法的详细说明:
一、Diff 算法的基本概念
- 虚拟 DOM
- 虚拟 DOM 是一个轻量级的 JavaScript 对象,用于描述真实 DOM 的结构和属性。
- Diff 算法的目标
- 比较新旧虚拟 DOM 树的差异,找出需要更新的部分,最小化 DOM 操作。
- Diff 算法的策略
- 只进行同层级比较,不跨层级比较。
- 通过唯一标识(如
key)优化列表项的对比。
二、Diff 算法的实现步骤
1. 同层级节点比较
- 节点类型不同
- 如果节点类型不同(如
div 变为span),直接替换整个节点。
- 如果节点类型不同(如
- 节点类型相同
- 如果节点类型相同,比较节点的属性和子节点。
2. 属性比较
- 更新属性
- 比较新旧节点的属性,更新变化的属性。
- 删除属性
- 如果旧节点有而新节点没有的属性,删除该属性。
- 添加属性
- 如果新节点有而旧节点没有的属性,添加该属性。
3. 子节点比较
- 无子节点
- 如果新旧节点都没有子节点,无需处理。
- 有子节点
- 如果新旧节点都有子节点,使用列表对比算法(如
key 对比)进行优化。
- 如果新旧节点都有子节点,使用列表对比算法(如
三、列表对比算法
- 无
key 的情况- 按顺序对比子节点,性能较差。
- 有
key 的情况- 使用
key 作为唯一标识,优化列表项的对比。 - 通过
key 找到新旧节点中相同的节点,进行更新。
- 使用
示例
const oldChildren =[{key:'a',value:'A'},{key:'b',value:'B'},{key:'c',value:'C'}];const newChildren =[{key:'c',value:'C'},{key:'a',value:'A'},{key:'d',value:'D'}];// 使用 key 对比,更新节点 四、Diff 算法的优化
- 唯一标识(
key )- 通过
key 优化列表项的对比,减少不必要的 DOM 操作。
- 通过
- 批量更新
- 将多次 DOM 操作合并为一次,提升性能。
- 虚拟 DOM 的分层比较
- 只进行同层级比较,不跨层级比较,简化算法复杂度。
五、Diff 算法的源码解析
- 源码位置
- Vue 的 Diff 算法实现位于
src/core/vdom/patch.js。
- Vue 的 Diff 算法实现位于
- 核心逻辑
- 通过
patchVnode 函数比较新旧节点。 - 通过
updateChildren 函数比较子节点列表。
- 通过
示例代码
functionpatchVnode(oldVnode, vnode){if(oldVnode === vnode)return;const elm = vnode.elm = oldVnode.elm;const oldCh = oldVnode.children;const ch = vnode.children;if(vnode.data){updateAttrs(oldVnode, vnode);}if(isUndef(vnode.text)){if(isDef(oldCh)&&isDef(ch)){if(oldCh !== ch)updateChildren(elm, oldCh, ch);}elseif(isDef(ch)){addVnodes(elm,null, ch,0, ch.length -1);}elseif(isDef(oldCh)){removeVnodes(elm, oldCh,0, oldCh.length -1);}}elseif(oldVnode.text !== vnode.text){setTextContent(elm, vnode.text);}}六、总结
- Diff 算法的目标
- 找出新旧虚拟 DOM 树的差异,最小化 DOM 操作。
- Diff 算法的策略
- 只进行同层级比较,不跨层级比较。
- 通过
key 优化列表项的对比。
- Diff 算法的优化
- 使用
key 作为唯一标识,批量更新 DOM 操作。
- 使用
理解 Diff 算法的原理和实现,可以帮助开发者更好地优化 Vue 应用的性能。
Webpack 打包配置
Webpack 是一个模块打包工具,用于将 JavaScript、CSS、图片等资源打包成静态文件。以下是 Webpack 的基本配置及其详细说明:
一、Webpack 的基本概念
- 入口(Entry)
- 指定打包的入口文件,Webpack 从入口文件开始构建依赖图。
- 输出(Output)
- 指定打包后的文件输出路径和文件名。
- 加载器(Loader)
- 用于处理非 JavaScript 文件(如 CSS、图片),将其转换为模块。
- 插件(Plugin)
- 用于执行更复杂的任务(如代码压缩、资源优化)。
- 模式(Mode)
- 指定打包模式(如开发模式
development 或生产模式production)。
- 指定打包模式(如开发模式
二、Webpack 的基本配置
1. 安装 Webpack
npminstall webpack webpack-cli --save-dev 2. 创建配置文件
在项目根目录下创建 webpack.config.js 文件。
3. 基本配置示例
const path =require('path'); module.exports ={mode:'development',// 打包模式 entry:'./src/index.js',// 入口文件 output:{filename:'bundle.js',// 输出文件名 path: path.resolve(__dirname,'dist')// 输出路径 },module:{rules:[{test:/\.css$/,// 匹配 CSS 文件 use:['style-loader','css-loader']// 使用加载器 },{test:/\.(png|jpg|gif)$/,// 匹配图片文件 use:['file-loader']// 使用加载器 }]},plugins:[]// 插件配置 };三、Webpack 的常用配置
1. 多入口配置
module.exports ={entry:{app:'./src/app.js',admin:'./src/admin.js'},output:{filename:'[name].bundle.js',// 使用 [name] 占位符 path: path.resolve(__dirname,'dist')}};2. 加载器配置
图片加载器
npminstall file-loader --save-dev module.exports ={module:{rules:[{test:/\.(png|jpg|gif)$/,use:['file-loader']}]}};Babel 加载器
npminstall babel-loader @babel/core @babel/preset-env --save-dev module.exports ={module:{rules:[{test:/\.js$/,exclude:/node_modules/,use:{loader:'babel-loader',options:{presets:['@babel/preset-env']}}}]}};CSS 加载器
npminstall style-loader css-loader --save-dev module.exports ={module:{rules:[{test:/\.css$/,use:['style-loader','css-loader']}]}};3. 插件配置
代码压缩插件
const TerserPlugin =require('terser-webpack-plugin'); module.exports ={optimization:{minimizer:[newTerserPlugin()]}};HTML 插件
npminstall html-webpack-plugin --save-dev const HtmlWebpackPlugin =require('html-webpack-plugin'); module.exports ={plugins:[newHtmlWebpackPlugin({template:'./src/index.html'// 指定模板文件 })]};4. 开发服务器配置
npminstall webpack-dev-server --save-dev module.exports ={devServer:{contentBase:'./dist',// 服务器根目录 port:8080,// 端口号 hot:true// 启用热更新 }};四、总结
- Webpack 的基本配置
- 入口、输出、加载器、插件、模式。
- 常用配置
- 多入口、CSS 加载器、Babel 加载器、图片加载器、HTML 插件、代码压缩插件、开发服务器。
通过理解 Webpack 的配置和常用插件,可以高效地打包和管理前端项目。
浏览器输入 URL 到网页呈现的详细过程
当用户在浏览器中输入 URL 并按下回车键后,浏览器会执行一系列操作,最终将网页呈现给用户。以下是这一过程的详细步骤:
一、URL 解析
- 解析协议、域名、端口和路径
- 浏览器解析 URL,提取协议(如
http 或https)、域名(如www.example.com)、端口(默认80 或443)和路径(如/index.html)。
- 浏览器解析 URL,提取协议(如
- 检查缓存
- 浏览器检查缓存中是否有该 URL 对应的资源,如果有且未过期,则直接使用缓存资源。
二、DNS 解析
- 查询 DNS 缓存
- 浏览器检查本地 DNS 缓存(如浏览器缓存、操作系统缓存),如果有域名对应的 IP 地址,则直接使用。
- DNS 递归查询
- 如果本地缓存中没有,浏览器向 DNS 服务器发起递归查询,获取域名对应的 IP 地址。
三、建立 TCP 连接
- 三次握手
- 浏览器与服务器通过 TCP 协议建立连接,进行三次握手:
- 客户端发送
SYN 报文。 - 服务器回复
SYN-ACK 报文。 - 客户端发送
ACK 报文。
- 客户端发送
- 浏览器与服务器通过 TCP 协议建立连接,进行三次握手:
- HTTPS 加密(可选)
- 如果使用 HTTPS 协议,浏览器与服务器进行 TLS 握手,协商加密算法和密钥,建立安全连接。
四、发送 HTTP 请求
- 构造 HTTP 请求
- 浏览器构造 HTTP 请求报文,包括请求行(如
GET /index.html HTTP/1.1)、请求头(如Host、User-Agent)和请求体(如 POST 数据)。
- 浏览器构造 HTTP 请求报文,包括请求行(如
- 发送请求
- 浏览器通过 TCP 连接将 HTTP 请求发送到服务器。
五、服务器处理请求
- 解析请求
- 服务器解析 HTTP 请求,确定请求的资源和方法。
- 处理请求
- 服务器根据请求内容执行相应的操作(如读取文件、查询数据库)。
- 生成响应
- 服务器生成 HTTP 响应,包括状态行(如
HTTP/1.1 200 OK)、响应头(如Content-Type、Content-Length)和响应体(如 HTML 内容)。
- 服务器生成 HTTP 响应,包括状态行(如
六、接收 HTTP 响应
- 接收响应
- 浏览器接收服务器返回的 HTTP 响应。
- 检查状态码
- 浏览器检查状态码(如
200 表示成功,404 表示未找到),决定后续操作。
- 浏览器检查状态码(如
七、解析和渲染页面
- 解析 HTML
- 浏览器解析 HTML 文档,构建 DOM 树。
- 加载外部资源
- 浏览器加载 CSS、JavaScript、图片等外部资源。
- 构建 CSSOM 树
- 浏览器解析 CSS,构建 CSSOM 树。
- 构建渲染树
- 浏览器将 DOM 树和 CSSOM 树合并,构建渲染树。
- 布局和绘制
- 浏览器计算渲染树中每个节点的位置和大小(布局),然后将页面绘制到屏幕上(绘制)。
八、执行 JavaScript
- 解析和执行 JavaScript
- 浏览器解析并执行 JavaScript 代码,可能修改 DOM 或 CSSOM,触发重绘或重排。
- 触发事件
- 浏览器触发
DOMContentLoaded 和load 事件,表示页面加载完成。
- 浏览器触发
九、总结
| 步骤 | 描述 |
|---|---|
| URL 解析 | 解析协议、域名、端口和路径,检查缓存 |
| DNS 解析 | 查询 DNS 缓存,递归查询域名对应的 IP 地址 |
| 建立 TCP 连接 | 通过三次握手建立连接,HTTPS 进行 TLS 握手 |
| 发送 HTTP 请求 | 构造并发送 HTTP 请求到服务器 |
| 服务器处理请求 | 解析请求,处理请求,生成 HTTP 响应 |
| 接收 HTTP 响应 | 接收并检查服务器返回的 HTTP 响应 |
| 解析和渲染页面 | 构建 DOM 树、CSSOM 树、渲染树,布局和绘制 |
| 执行 JavaScript | 解析和执行 JavaScript,触发页面事件 |
前端的优化方法
前端优化是提升网页性能、用户体验和 SEO 效果的关键。以下是常见的前端优化方法及其详细说明:
一、减少 HTTP 请求
- 合并文件
- 将多个 CSS 或 JavaScript 文件合并为一个文件,减少请求次数。
- 使用雪碧图
- 将多个小图标合并为一张雪碧图,通过
background-position 显示不同图标。
- 将多个小图标合并为一张雪碧图,通过
- 内联资源
- 将小型的 CSS 或 JavaScript 代码直接内联到 HTML 中,减少请求次数。
二、使用缓存
- 通过设置
Cache-Control 和Expires 头,缓存静态资源。 - 使用 Service Worker 缓存资源,实现离线访问。
Service Worker 缓存
self.addEventListener('install',(event)=>{ event.waitUntil( caches.open('v1').then((cache)=>{return cache.addAll(['/','/styles.css','/app.js']);}));});浏览器缓存
Cache-Control: max-age=31536000 三、优化资源加载
- 将非关键资源(如图片、视频)延迟加载,提升页面初始加载速度。
- 使用
async 或defer 属性异步加载 JavaScript 文件。 - 使用
<link rel="preload"> 预加载关键资源。
预加载关键资源
<linkrel="preload"href="styles.css"as="style">异步加载脚本
<scriptsrc="app.js"async></script>延迟加载
<imgdata-src="image.jpg"alt="Lazy Loaded Image"class="lazyload">四、优化代码
- 压缩代码
- 使用工具(如 Webpack、Gulp)压缩 JavaScript、CSS 和 HTML 代码。
- 移除未使用的代码
- 使用 Tree Shaking 移除未使用的 JavaScript 代码。
- 优化 CSS
- 避免使用
@import,减少 CSS 嵌套层级,移除未使用的样式。
- 避免使用
五、优化图片
- 使用合适的格式
- 使用 WebP、JPEG XR 等现代图片格式,减少文件大小。
- 压缩图片
- 使用工具(如 TinyPNG、ImageOptim)压缩图片。
- 使用
<picture> 和srcset 提供不同分辨率的图片。
响应式图片
<picture><sourcesrcset="image.webp"type="image/webp"><imgsrc="image.jpg"alt="Fallback Image"></picture>六、优化渲染性能
- 减少重绘和重排
- 使用
transform 和opacity 替代top、left 等属性,减少重排。 - 使用
requestAnimationFrame 优化动画性能。
- 使用
- 使用虚拟 DOM
- 使用虚拟 DOM(如 React)优化 DOM 操作。
使用requestAnimationFrame
functionanimate(){// 动画逻辑 requestAnimationFrame(animate);}requestAnimationFrame(animate);七、优化 SEO
- 语义化 HTML
- 使用语义化标签(如
<header>、<article>)提升 SEO 效果。 - 设置
title、description 和keywords 等meta 标签。 - 使用 JSON-LD 或 Microdata 添加结构化数据。
- 使用语义化标签(如
使用结构化数据
<scripttype="application/ld+json">{"@context":"https://schema.org","@type":"Article","headline":"Article Title"}</script>优化meta 标签
<metaname="description"content="Page description">八、总结
| 优化方法 | 描述 |
|---|---|
| 减少 HTTP 请求 | 合并文件、使用雪碧图、内联资源 |
| 使用缓存 | 浏览器缓存、Service Worker 缓存 |
| 优化资源加载 | 延迟加载、异步加载脚本、预加载关键资源 |
| 优化代码 | 压缩代码、移除未使用的代码、优化 CSS |
| 优化图片 | 使用合适的格式、压缩图片、响应式图片 |
| 优化渲染性能 | 减少重绘和重排、使用 requestAnimationFrame、虚拟 DOM |
| 优化 SEO | 语义化 HTML、优化 meta 标签、结构化数据 |
通过理解这些前端优化方法,可以显著提升网页性能、用户体验和 SEO 效果。