一、DOM操作与事件处理:用户交互的基石
1.1 原生点击事件的三种绑定方式
前端与用户的交互始于事件,而点击事件是最基础的交互方式。原生JavaScript绑定点击事件主要有以下三种方式:
(1)HTML内联绑定(不推荐)
直接在HTML标签中通过onclick
属性定义事件逻辑:
javascript
<button onclick="handleClick()">点击我</button>
<script>
function handleClick() {
console.log('内联事件触发');
}
</script>
缺点:HTML与JS逻辑耦合,无法动态修改事件,维护成本高。
(2)DOM属性绑定(单事件)
通过JS获取元素后,直接为onclick
属性赋值:
javascript
const btn = document.getElementById('btn');
btn.onclick = function() {
console.log('DOM属性事件触发');
};
特点:只能绑定一个同类型事件(后绑定的会覆盖前一个),适合简单场景。
(3)addEventListener
(推荐)
通过addEventListener
方法绑定事件,支持多事件监听、事件冒泡/捕获控制:
javascript
const btn = document.getElementById('btn');
// 第三个参数:useCapture(布尔值,默认false=冒泡阶段)
btn.addEventListener('click', function(e) {
console.log('冒泡阶段触发');
}, false);
// 捕获阶段绑定(需设置为true)
document.body.addEventListener('click', function(e) {
console.log('捕获阶段触发');
}, true);
优势:
- 支持同一元素绑定多个同类型事件(按注册顺序执行);
- 可通过
removeEventListener
精准解绑; - 支持事件流控制(冒泡/捕获)。
1.2 如何获取DOM元素?
要操作DOM元素,首先需要获取其引用。以下是8种常用方法:
方法 | 说明 | 示例 |
---|---|---|
getElementById() |
根据ID获取单个元素(唯一) | document.getElementById('box') |
querySelector() |
CSS选择器获取首个匹配元素 | document.querySelector('.btn') |
querySelectorAll() |
CSS选择器获取所有匹配元素(返回NodeList) | document.querySelectorAll('li') |
getElementsByClassName() |
根据类名获取元素集合(HTMLCollection,动态更新) | document.getElementsByClassName('item') |
getElementsByTagName() |
根据标签名获取元素集合(HTMLCollection,动态更新) | document.getElementsByTagName('div') |
getElementsByName() |
根据name 属性获取元素集合(NodeList,静态) |
document.getElementsByName('user') |
document.body |
直接获取<body> 元素 |
const body = document.body |
document.documentElement |
直接获取<html> 元素 |
const html = document.documentElement |
1.3 事件传播机制:捕获→目标→冒泡
当点击一个元素时,事件会经历三个阶段:
- 捕获阶段 :事件从
window
开始,逐层向下传播到目标元素的父节点; - 目标阶段:事件到达目标元素本身;
- 冒泡阶段 :事件从目标元素开始,逐层向上传播到
window
。
关键操作:
- 阻止事件冒泡:
e.stopPropagation()
; - 阻止默认行为(如链接跳转):
e.preventDefault()
; - 停止事件传播(同时阻止冒泡和默认行为):
e.stopImmediatePropagation()
(仅适用于当前元素的其他监听器)。
示例:阻止表单提交
javascript
const form = document.querySelector('form');
form.addEventListener('submit', function(e) {
e.preventDefault(); // 阻止表单默认提交行为
console.log('表单验证通过,异步提交');
});
二、数据存储:从Cookie到现代存储方案
2.1 Cookie的本质与局限
Cookie是服务器通过Set-Cookie
响应头写入客户端的键值对数据,随每次HTTP请求自动携带到服务器。其核心特性如下:
特性 | 说明 |
---|---|
存储容量 | 约4KB(不同浏览器略有差异) |
生命周期 | 默认会话结束失效(可通过Expires 或Max-Age 设置持久化) |
作用域 | 由Domain (主域+子域)和Path (路径)控制 |
安全性 | 明文传输(易被篡改),可通过HttpOnly (禁止JS访问)防XSS,Secure (仅HTTPS传输) |
同步性 | 每次请求自动携带,可能影响性能 |
典型场景 :用户登录状态(如sessionId
)、购物车临时数据。
2.2 现代存储方案对比
随着前端应用复杂度提升,Cookie的局限性逐渐显现,以下是更常用的替代方案:
方案 | 容量 | 生命周期 | 同步/异步 | 典型用途 | API示例 |
---|---|---|---|---|---|
localStorage |
5-10MB | 手动清除(永久有效) | 同步 | 长期配置(主题、语言) | localStorage.setItem('theme', 'dark') |
sessionStorage |
5-10MB | 标签页关闭时失效 | 同步 | 临时会话数据 | sessionStorage.removeItem('token') |
IndexedDB |
250MB+ | 永久(需手动清理) | 异步 | 大量结构化数据(如离线文档) | const db = await indexedDB.open('myDB') |
Cache API |
无限制 | 手动控制(Service Worker管理) | 异步 | 缓存静态资源(PWA) | caches.open('v1').then(cache => cache.add('/img/logo.png')) |
选择建议:
- 需长期保留的小量数据 →
localStorage
; - 临时会话数据 →
sessionStorage
; - 大量结构化数据或离线应用 →
IndexedDB
; - 静态资源缓存 →
Cache API
(配合Service Worker)。
三、ES6+语法与异步编程:现代前端的基石
3.1 ES6核心语法速览
ES6(ECMAScript 2015)是前端语法的重要里程碑,以下是最常用特性:
(1)箭头函数(=>
)
简化函数写法,绑定词法作用域的this
(无独立this
):
javascript
// 传统函数
const add = function(a, b) { return a + b; };
// 箭头函数(单表达式可省略大括号和return)
const add = (a, b) => a + b;
// 多表达式需大括号
const double = (num) => {
if (num > 0) return num * 2;
return num;
};
(2)解构赋值
快速提取对象/数组的值,简化变量声明:
javascript
// 对象解构
const user = { name: 'Xback', age: 23 };
const { name, age } = user; // name='Xback', age=23
// 数组解构
const [first, second] = [1, 2]; // first=1, second=2
// 重命名
const { name: userName } = user; // userName='Xback'
(3)模板字符串(`````)
支持变量嵌入和多行字符串:
javascript
const message = `用户${user.name}(${user.age}岁)登录成功`;
// 输出:"用户Xback(23岁)登录成功"
(4)类(class
)
面向对象的语法糖,替代原型链:
javascript
class Person {
constructor(name) {
this.name = name;
}
sayHello() {
console.log(`Hello, I'm ${this.name}`);
}
}
const alice = new Person('Xback');
alice.sayHello(); // "Hello, I'm Xback"
3.2 异步编程:从回调到async/await
前端应用中,网络请求、文件读写等操作均为异步,需避免阻塞主线程。ES6后的异步解决方案演进如下:
(1)回调地狱(Callback Hell)
早期通过回调函数处理异步,但多层嵌套会导致代码可读性差、错误难以捕获:
javascript
fetch('/api1')
.then(res => res.json())
.then(data1 => {
fetch(`/api2?id=${data1.id}`)
.then(res => res.json())
.then(data2 => {
// 更多嵌套...
});
});
(2)Promise:链式调用
Promise通过then/catch
实现线性链式调用,解决回调地狱:
javascript
fetch('/api1')
.then(res => res.json())
.then(data1 => fetch(`/api2?id=${data1.id}`))
.then(res => res.json())
.then(data2 => console.log(data2))
.catch(err => console.error('请求失败', err));
(3)async/await
:同步写法
基于Promise的语法糖,用async
标记函数,await
等待Promise解决,代码更接近同步逻辑:
javascript
async function fetchAllData() {
try {
const res1 = await fetch('/api1');
const data1 = await res1.json();
const res2 = await fetch(`/api2?id=${data1.id}`);
const data2 = await res2.json();
return [data1, data2];
} catch (err) {
console.error('请求失败', err);
}
}
// 调用
fetchAllData().then(([data1, data2]) => {
console.log('所有数据加载完成', data1, data2);
});
(4)并发请求:Promise.all
若需同时发起多个请求(如获取用户信息、商品列表),可用Promise.all
并行执行,提升效率:
javascript
async function fetchConcurrent() {
const urls = ['/api/user', '/api/products', '/api/cart'];
// 并行发起请求
const promises = urls.map(url => fetch(url).then(res => res.json()));
// 等待所有请求完成
const [user, products, cart] = await Promise.all(promises);
return { user, products, cart };
}
3.3 函数进阶:call
/apply
/bind
与Proxy
(1)call
/apply
/bind
:修改this
指向
三者均可显式绑定函数的this
,但参数传递和执行时机不同:
方法 | 参数形式 | 执行时机 | 返回值 | 典型场景 |
---|---|---|---|---|
call |
参数列表(逗号分隔) | 立即执行 | 函数返回值 | 借用其他对象的方法 |
apply |
参数数组 | 立即执行 | 函数返回值 | 传递数组参数(如Math.max ) |
bind |
参数列表(可选) | 延迟执行 | 绑定后的新函数 | 事件回调固定this |
示例:
javascript
const obj = { x: 10 };
function sum(y) { return this.x + y; }
sum.call(obj, 5); // 15(立即执行,参数列表)
sum.apply(obj, [5]); // 15(立即执行,参数数组)
const boundSum = sum.bind(obj, 5);
boundSum(); // 15(延迟执行,参数部分预设)
(2)Proxy
:对象代理
Proxy
用于创建一个对象的代理,拦截并自定义对象的基本操作(如属性读取、赋值),是实现响应式数据(如Vue 3)、数据校验等功能的利器:
javascript
const target = { name: 'Alice', age: 25 };
const handler = {
get(obj, prop) {
if (prop in obj) {
return obj[prop].toUpperCase(); // 读取时转换为大写
}
return '属性不存在';
},
set(obj, prop, value) {
if (prop === 'age' && typeof value !== 'number') {
throw new Error('年龄必须是数字');
}
obj[prop] = value; // 设置时校验类型
return true; // 必须返回true表示设置成功
}
};
const proxy = new Proxy(target, handler);
console.log(proxy.name); // "ALICE"(触发get)
proxy.age = '26'; // 抛出错误(类型校验失败)
四、性能优化与工程实践
4.1 图片懒加载:提升首屏加载速度
图片懒加载的核心思想是:仅当图片进入可视区域时再加载真实资源,减少首屏请求量。
实现步骤:
- 占位符替换 :将
<img>
的src
属性替换为data-src
(存储真实URL),避免初始加载; - 视口监听 :使用
Intersection Observer API
监听图片是否进入视口; - 资源加载 :当图片进入视口时,将
data-src
赋值给src
,触发加载。
代码示例:
javascript
// 1. 获取所有带data-src的图片
const lazyImages = document.querySelectorAll('img[data-src]');
// 2. 创建Intersection Observer实例
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) { // 图片进入视口
const img = entry.target;
img.src = img.dataset.src; // 加载真实图片
img.removeAttribute('data-src'); // 移除占位符属性
observer.unobserve(img); // 停止监听已加载的图片
}
});
});
// 3. 开始监听所有图片
lazyImages.forEach(img => observer.observe(img));
关键技术点:
Intersection Observer API
:现代浏览器提供的视口监听接口,替代传统的滚动事件+getBoundingClientRect
(性能更优);data-*
属性:HTML5自定义数据属性,用于存储额外信息。
4.2 内存泄漏:原因与解决方案
内存泄漏指程序中不再使用的内存未被释放,导致内存占用持续增长,最终可能引发页面卡顿或崩溃。常见原因及解决方法:
(1)未移除的事件监听
原因 :为元素添加事件监听后,若元素被移除但监听器未解绑,监听器仍会引用元素,导致其无法被垃圾回收。
解决 :在元素移除前调用removeEventListener
解绑监听器,或使用once: true
(仅触发一次后自动移除)。
(2)未释放的闭包
原因 :闭包会长期持有外部函数的变量引用,若闭包被全局变量引用,变量无法被回收。
解决 :避免不必要的闭包,或在闭包中使用weakMap
/weakSet
(弱引用,不阻止垃圾回收)。
(3)定时器未清除
原因 :setInterval
未及时清除,导致回调函数持续引用上下文。
解决 :在组件卸载或不需要定时器时调用clearInterval
/clearTimeout
。
(4)DOM引用残留
原因 :JS中保留了已移除DOM元素的引用(如cache
对象),导致DOM无法被回收。
解决 :移除DOM后,同步清除JS中的引用(如delete cache[key]
)。
检测工具 :Chrome DevTools的Memory
面板(记录堆快照,对比前后内存变化)。
五、前端工具链与生态
5.1 Git:版本控制的核心技能
Git是前端开发的必备工具,以下是常用命令及场景:
场景 | 命令示例 | 说明 |
---|---|---|
克隆仓库 | git clone https://github.com/xxx/repo.git |
从远程仓库复制代码到本地 |
创建并切换分支 | git checkout -b feature/new-module |
新建feature/new-module 分支并切换 |
添加修改 | git add . |
添加所有修改到暂存区 |
提交修改 | git commit -m "feat: 新增用户模块" |
提交暂存区的修改到本地仓库 |
推送分支到远程 | git push origin feature/new-module |
将本地分支推送到远程仓库 |
拉取远程更新 | git pull origin main |
拉取远程main 分支并合并到本地 |
查看提交历史 | git log --oneline |
简洁查看提交记录 |
回退到指定版本 | git reset --hard abc123 |
回退到哈希为abc123 的提交(危险操作) |
可视化工具:VS Code内置Git面板、SourceTree(适合复杂操作,如合并冲突解决)。
5.2 调试技巧:定位问题的关键
调试是开发者的核心能力,浏览器开发者工具(DevTools)是最常用的工具。
(1)网络面板(Network)
作用:监控所有网络请求,分析请求耗时、状态码、响应内容。
关键信息:
- 状态码 :
200
(成功)、404
(资源不存在)、500
(服务器错误); - 请求头(Request Headers) :
User-Agent
:浏览器/设备信息;Cookie
:当前请求携带的Cookie;Content-Type
:请求体数据类型(如application/json
);
- 响应头(Response Headers) :
Cache-Control
:缓存策略(如max-age=3600
);Set-Cookie
:服务器设置的Cookie;
- Timing:请求各阶段耗时(DNS查询、TCP连接、TTFB、下载)。
(2)元素面板(Elements)
作用:查看和修改DOM结构、CSS样式,调试布局问题。
常用功能:
- 选择DOM元素(左上角箭头图标);
- 实时编辑CSS(
Styles
面板); - 查看盒模型(
Computed
面板); - 断点调试(
Event Listeners
面板添加事件断点)。
六、框架与生态:从Vue到Uniapp
6.1 Uniapp与Vue的区别
Uniapp是基于Vue的跨端开发框架,目标是"一套代码,多端运行"(小程序、H5、App)。与标准Vue的主要差异:
特性 | Uniapp | 标准Vue(Web) |
---|---|---|
运行环境 | 小程序引擎(如微信JSCore)、H5、App | 浏览器(JS引擎) |
模板语法 | 支持Vue模板,但部分指令受限(如v-html ) |
完整Vue模板语法 |
组件库 | 需使用Uniapp组件(如<uni-list> ) |
可使用任意第三方Vue组件库 |
跨端适配 | 自动处理不同端的API差异(如uni.request ) |
需手动适配不同浏览器 |
构建流程 | 基于vue-cli ,但增加多端编译步骤 |
直接打包为浏览器可识别的代码 |
6.2 Element UI与TypeScript
Element UI是基于Vue的PC端组件库,而TypeScript(TS)是JavaScript的超集,添加了静态类型系统。
TS的核心优势:
- 类型检查:编译时捕获类型错误(如函数参数类型不匹配);
- 代码提示:IDE(如VS Code)可自动推断变量类型,提升开发效率;
- 重构安全:修改变量名或类型时,TS会提示所有受影响的代码位置。
示例:TS定义组件Props
javascript
import { defineComponent, PropType } from 'vue';
export default defineComponent({
props: {
// 定义用户对象(必传)
user: {
type: Object as PropType<{ name: string; age: number }>,
required: true
},
// 定义可选数字(默认值10)
count: {
type: Number,
default: 10
}
},
setup(props) {
console.log(props.user.name); // 类型推断:string
}
});