前端存储全景 + JavaScript this 指向彻底搞懂 🚀
从前端存储方案到 JS 核心难点
this,一文打通面试高频考点
开篇:数据存储的世界 🌍
在正式进入前端存储之前,我们先看看日常开发中会遇到哪些存储场景:
| 存储类型 | 典型场景 | 特点 |
|---|---|---|
| MySQL 关系型数据库 | 用户信息、订单数据 | 结构化、持久化、支持复杂查询 |
| Redis 缓存 | 热门文章列表、Session | KV 存储、内存级速度 |
| 浏览器缓存 | 静态资源、页面回退 | HTTP Cache、Service Worker |
| LocalStorage | 用户偏好、草稿保存 | 前端持久化、API 简洁 |
| 云盘/文件 | JSON/CSV/Excel 导出 | 文件级存储、跨平台共享 |
| LLM Embedding | 向量数据库、智能检索 | 数据智能化、语义搜索 |
核心思想:
第一次从 MySQL 读取文章列表 → 把结果放到 Redis → 以后都走 Redis,不用每次都查 MySQL。 因为 MySQL 的性能相对于代码执行是有瓶颈的,缓存是性能优化的第一利器。
一、Web Storage:前端专属的"小数据库" 💾
1.1 LocalStorage vs SessionStorage
HTML5 给前端带来了两个超好用的存储 API:
javascript
// 📦 LocalStorage --- 永久存储(除非手动删除)
localStorage.setItem('username', '梅西')
localStorage.getItem('username') // '梅西'
localStorage.removeItem('username')
localStorage.clear() // 清空所有
// ⏳ SessionStorage --- 仅当前会话有效(关闭标签页即消失)
sessionStorage.setItem('token', 'abc123')
sessionStorage.getItem('token') // 'abc123'
// 关闭标签页后... 没了!
| 对比维度 | LocalStorage | SessionStorage |
|---|---|---|
| 生命周期 | 永久(手动删除) | 标签页关闭即清除 |
| 作用域 | 同源共享 | 同源 + 同标签页 |
| 容量 | ~5MB | ~5MB |
| API | 完全一致 | 完全一致 |
| 典型场景 | 主题偏好、草稿、登录态 | 表单临时数据、单次会话状态 |
1.2 实战:用 LocalStorage 做一个待办清单
我们来看一个实际例子------一个可以持久化保存的 Tapas(小食)列表:
html
<!-- index.html -->
<div class="wrapper">
<h2>LOCAL TAPAS</h2>
<ul class="plates">
<li>Loading Tapas...</li>
</ul>
<form class="add-items">
<input type="text" name="item" placeholder="Add a new tapas" >
<input type="submit" value="+ Add Item">
</form>
</div>
javascript
// 核心逻辑:利用 localStorage 实现数据持久化
const addItems = document.querySelector('.add-items')
const platesList = document.querySelector('.plates')
let items = JSON.parse(localStorage.getItem('items')) || []
function addItem(e) {
e.preventDefault() // ⚠️ 阻止表单默认提交(否则页面会刷新!)
const text = this.querySelector('[name=item]').value
items.push({ text, done: false })
localStorage.setItem('items', JSON.stringify(items)) // 写入 localStorage
renderList()
this.reset() // 清空输入框
}
function renderList() {
platesList.innerHTML = items.map((item, i) => `
<li>
<input type="checkbox" data-index="${i}" ${item.done ? 'checked' : ''}>
<label>${item.text}</label>
</li>
`).join('')
}
addItems.addEventListener('submit', addItem)
renderList()
📌 知识点 :LocalStorage 只能存字符串!存对象需要用
JSON.stringify()序列化,取出来用JSON.parse()反序列化。
二、Form 表单:为什么我们不用默认提交?📝
2.1 传统 Form 的"坑"
html
<!-- ❌ 传统方式:点击提交 → action 地址 → 页面刷新 -->
<form action="/api/submit" method="POST">
<input name="username">
<button type="submit">提交</button>
</form>
问题在哪? 默认的表单提交会刷新整个页面 ,用户体验很差。在现代 SPA(单页应用)中,我们几乎100% 使用 AJAX + preventDefault 的方式:
javascript
// ✅ 现代方式:AJAX 提交,页面不刷新
oForm.addEventListener('submit', function(e) {
e.preventDefault() // 🔑 阻止默认刷新行为
const formData = new FormData(this)
// fetch/ajax 提交数据...
})
一句话总结 :
e.preventDefault()是前端表单处理的"第一行代码"。
三、JavaScript this 指向:前端面试必考题 🎯
this 是 JavaScript 中最让人困惑的概念之一。记住一个核心原则:
this是在函数运行时确定的,不是在声明时确定的。this指向调用函数的对象。
3.1 五种 this 指向场景,一张表搞定
| 场景 | this 指向 | 代码示例 |
|---|---|---|
| 普通函数调用 | window(严格模式下 undefined) |
fn() → window |
| 对象方法调用 | 调用该方法的对象 | obj.say() → obj |
| 构造函数调用 | 新创建的实例对象 | new Person() → 实例 |
| 事件处理函数 | 触发事件的 DOM 元素 | btn.onclick → btn |
| 箭头函数 | 没有自己的 this,继承外层 | () => {} → 外层作用域 |
3.2 逐个击破
🔹 场景一:普通函数调用
javascript
var name = '佳明' // var 声明的变量会挂载到 window 上!
let age = 25 // let 不会污染 window 对象
function sayName() {
console.log(this.name) // '佳明' --- this 指向 window
}
sayName()
💡 知识点 :
var声明的变量会污染 window 对象 ,let/const不会。这就是为什么现代 JS 推荐用let/const。
🔹 场景二:对象方法调用 --- 注意"引用式赋值"的坑!
javascript
let obj = {
name: '赖庆庆',
say: function() {
console.log(this.name)
}
}
obj.say() // ✅ '赖庆庆' --- this 指向 obj
// ⚠️ 经典陷阱:引用式赋值
const fn = obj.say
fn() // ❌ undefined(严格模式)或 window.name --- this 丢失了!
为什么会这样?
fn只是一个普通函数引用,调用时不再是obj.fn()的形式,this就指向了window。这也是 React 类组件需要this.handleClick = this.handleClick.bind(this)的原因。
🔹 场景三:事件处理函数
javascript
// this 默认指向触发事件的 DOM 元素
document.querySelector('.lnk').addEventListener('click', function(e) {
console.log(this) // 指向 .lnk 这个 DOM 元素
e.preventDefault()
})
🔹 场景四:箭头函数 --- this 的"静态绑定"
javascript
var name = '梅西'
let obj = {
name: '姆巴佩',
say: function() {
// ✅ 箭头函数没有自己的 this,继承外层(say 方法的 this)
setTimeout(() => {
console.log(this.name) // '姆巴佩'
}, 1000)
// ❌ 普通函数作为 setTimeout 回调,this 指向 window
setTimeout(function() {
console.log(this.name) // '梅西'(window.name)
}, 1000)
}
}
obj.say()
🎯 核心区别 :箭头函数的
this在定义时 就已经确定了(词法作用域),普通函数的this在调用时 确定。这就是为什么箭头函数是解决this丢失问题的"银弹"。
3.3 手动指定 this:call、apply、bind 三兄弟
当你需要强制改变 this 指向时,这三兄弟就派上用场了:
javascript
let obj = {
name: '赖庆庆',
speak: function(a, b) {
console.log(a, b)
console.log(this.name)
}
}
let obj2 = { name: '甜总' }
// 📌 call --- 逐个传参,立即执行
obj.speak.call(obj2, '你好', '我是甜总')
// 输出: '你好' '我是甜总' '甜总'
// 📌 apply --- 数组传参,立即执行
obj.speak.apply(obj2, ['你好', '我是甜总'])
// 输出: '你好' '我是甜总' '甜总'
// 📌 bind --- 返回新函数,不立即执行 ⭐
const boundFn = obj.speak.bind(obj2)
boundFn('你好', '我是甜总')
// 输出: '你好' '我是甜总' '甜总'
| 方法 | 传参方式 | 是否立即执行 | 返回值 |
|---|---|---|---|
call |
逐个参数 (thisArg, arg1, arg2, ...) |
✅ 立即 | 函数返回值 |
apply |
数组 (thisArg, [arg1, arg2]) |
✅ 立即 | 函数返回值 |
bind |
逐个参数 (thisArg, arg1, arg2, ...) |
❌ 不立即 | 新函数 |
3.4 实战:bind 在事件处理中的应用
这是实际开发中最常见的 bind 使用场景:
javascript
const oForm = document.querySelector('.add-items')
let obj = {
name: '赖庆庆'
}
function addItem(e) {
e.preventDefault() // 阻止表单刷新
console.log(this) // 指向 obj(因为我们用 bind 绑定了!)
}
// 🎯 事件处理函数需要的是"函数引用",不能是调用结果
// ❌ 错误:oForm.addEventListener('submit', addItem.call(obj))
// --- call 会立即执行,返回 undefined
// ✅ 正确:bind 返回一个新函数,满足异步回调的需求
const addItemBind = addItem.bind(obj)
oForm.addEventListener('submit', addItemBind)
💡 为什么不用 call/apply? 因为
addEventListener需要的是一个函数引用 (稍后触发),而 call/apply 会立即执行 函数。bind 返回一个绑定了 this 的新函数,完美适配事件回调的场景。
四、总结:一张图理清全部知识点 🧠
scss
┌─────────────────────────────────────────────────────┐
│ 前端存储体系 │
├───────────┬───────────┬───────────┬────────────────┤
│ MySQL │ Redis │ 浏览器缓存 │ Web Storage │
│ 关系型DB │ KV 缓存 │ HTTP Cache│ Local/Session │
│ 持久化 │ 性能加速 │ 资源缓存 │ 前端持久化 │
└───────────┴───────────┴───────────┴────────────────┘
┌─────────────────────────────────────────────────────┐
│ JavaScript this │
├─────────────────────────────────────────────────────┤
│ 调用方式 → this 指向 │
│ 普通函数 fn() → window / undefined(strict) │
│ 对象方法 o.fn() → 对象 o │
│ 构造函数 new F() → 实例对象 │
│ 事件处理 → 触发元素 │
│ 箭头函数 ()=>{} → 外层作用域(词法绑定) │
├─────────────────────────────────────────────────────┤
│ 手动改变 this: │
│ call(thisArg, a, b) 立即执行,逐个传参 │
│ apply(thisArg, [a, b]) 立即执行,数组传参 │
│ bind(thisArg, a, b) 返回新函数,不立即执行 ⭐ │
└─────────────────────────────────────────────────────┘
五、面试高频追问 🔥
Q1: LocalStorage 和 Cookie 的区别?
| 维度 | LocalStorage | Cookie |
|---|---|---|
| 容量 | ~5MB | ~4KB |
| 发送 | 不自动发送 | 每次 HTTP 请求自动携带 |
| API | getItem/setItem |
document.cookie(字符串操作) |
| 用途 | 客户端持久化 | 服务端会话标识 |
Q2: 箭头函数能当构造函数吗?
不能。箭头函数没有自己的 this,也没有 prototype,所以 new 会报错。
Q3: 为什么 React 类组件要 .bind(this)?
因为事件处理函数是引用式赋值传给 onClick 的,调用时 this 会丢失。用 bind 或箭头函数来解决。
jsx
// ❌ this 丢失
<button onClick={this.handleClick}>Click</button>
// ✅ 方案1:构造函数中 bind
this.handleClick = this.handleClick.bind(this)
// ✅ 方案2:箭头函数(最常用)
<button onClick={() => this.handleClick()}>Click</button>
如果这篇文章对你有帮助,欢迎点赞收藏!后续会继续更新前端核心知识点的深度解析 🎉
本文涵盖了 LocalStorage 实战、this 指向全场景、call/apply/bind 三兄弟对比、箭头函数原理等面试高频考点,建议收藏反复阅读。