面试八股文里,最容易卡住的有两块:
- 「数据怎么存?」 ------ MySQL、Redis、localStorage、浏览器缓存,名字一大堆,到底啥关系?
- 「this 到底指向谁?」 ------ 五种场景,再加 var 污染 window 的冷门坑,背了八遍还在忘。
这篇一次讲透。
写在前面:八股不是死记硬背
背八股的最高境界,是理解它要解决什么问题。
存储是为了「下次还能找到 」,this 是为了「在运行时分清楚谁在干活」。
两个词的内核都不一样,但偏偏都跟前端工程的血泪史深度绑定。
五大存储方案:从 MySQL 到 localStorage
先一张总表镇场:
| 存储 | 位置 | 速度 | 容量 | 典型场景 |
|---|---|---|---|---|
| MySQL | 服务器 | 慢 | TB 级 | 主业务数据 |
| Redis | 内存 | 极快 | GB 级 | 热点缓存(KV) |
| 云盘 | 第三方服务 | 中 | TB 级 | 文件备份、协作 |
| 浏览器缓存 | 浏览器 | 快 | MB 级 | 静态资源、CDN 镜像 |
| localStorage | 浏览器 | 快 | 5MB 左右 | 前端 KV 持久化 |
典型链路:
文章列表第一次走 MySQL,缓存到 Redis,下次读 Redis 不再查 MySQL------MySQL 不至于被打爆。
再细想一层:localStorage 和 sessionStorage 是浏览器给前端的「KV 数据库 」,有限制(5MB、仅字符串、同源策略),但就是胜在不用走后端。
LLM 时代的 embedding 存储(向量数据库),本质还是「KV 缓存 + 语义索引」------速度最快,但容量有限、贵。
form 表单:别再用默认提交了
面试经典翻车现场:<form action="..."> 默认提交,会刷新整个页面。
html
xml
<!-- 这种坑,新手代码里常出现 -->
<form class="add-items" action="/add">
<input type="text" name="item" placeholder="Add a new tapas" required>
<input type="submit" value="+ Add Item">
</form>
为啥要避开?
| 维度 | form 默认提交 | fetch / ajax |
|---|---|---|
| 体验 | 整页刷新,闪烁 | 局部更新,无缝 |
| 状态 | 丢失 | 保留 |
| 错误处理 | 跳错误页 | 弹 toast |
| 现代框架支持 | 反人类 | 对接 React/Vue 一气呵成 |
正解:
js
javascript
const oForm = document.querySelector('.add-items');
// bind 返回一个全新函数,不立即执行
const addItemBind = addItem.bind(obj2);
oForm.addEventListener('submit', addItemBind);
function addItem(e) {
e.preventDefault(); // 阻止默认提交 → 阻止刷新页面
// ...走 ajax / fetch
}
.preventDefault() 是关键一行------不写它,表单照样给你刷一次页面。
写完记得用
fetch('/api/add', { method: 'POST', body: ... })替换默认行为,整条链路才顺。
this:五种场景
重点来了。this 的指向,看你怎么「叫」这个函数,而不是在哪里「写」的。
| 调用方式 | this 指向 | 例子 |
|---|---|---|
| 普通函数调用 | 全局 window(非严格模式)/ undefined(严格模式) | fn() |
| 对象方法调用 | 调用对象本身 | obj.say() |
| 构造函数调用 | 实例对象 | new Person() |
| 事件处理函数 | 事件触发元素 | btn.onclick = function(){...} |
| 手动指定 | 你指定谁就是谁 | fn.call(obj) |
为什么会有「引用式赋值」丢 this 的坑?
js
javascript
const obj = { name: '羊羊', say() { console.log(this.name) } };
const fn = obj.say; // 引用式赋值
fn(); // 输出 undefined,this 是 window
obj.say(); // 输出「羊羊」,this 是 obj
一赋值,函数的「债主」就变了。obj.say() 时,调它的债主是 obj;fn() 时,没人认领,债主默认是 window。
call / apply / bind 三兄弟
如果丢失的 this 想找回来,手动指定:
| 方法 | 参数形式 | 是否立即执行 | 返回值 |
|---|---|---|---|
| call | fn.call(thisArg, arg1, arg2, ...) |
立即执行 | 函数返回值 |
| apply | fn.apply(thisArg, [arg1, arg2, ...]) |
立即执行 | 函数返回值 |
| bind | fn.bind(thisArg, arg1, arg2, ...) |
不立即执行 | 一个新函数 |
看代码里这一段:
js
arduino
obj.speak.call(obj2); // 立即执行,this → obj2
obj.speak.apply(obj2, ['你好', '我是小羊']); // 数组传参
const fn2 = obj.speak.bind(obj2); // 返回新函数,不执行
fn2('你好', '我是小羊'); // 现在才执行
实战 tips:
- 已知参数 →
call - 参数是数组 →
apply - 想做事件回调 / 延时回调 →
bind(用addEventListener('submit', addItemBind)这种最干净)
var 污染 window:一行代码引发的血案
js
javascript
var name = '小小羊'; // 污染 window
let obj = {
name: '两只羊',
say() {
console.log(this.name); // 输出「两只羊」OK
setTimeout(function() {
console.log(this.name) // 输出「小小羊」!糟了
}, 1000);
}
}
obj.say();
为啥 setTimeout 里 this.name 变成「小小羊」?
| 声明方式 | 是否挂载到 window | 是否能通过 window.xxx 访问 |
|---|---|---|
| var | 是 | 是 |
| let | 否 | 否 |
| const | 否 | 否 |
定时器里 function() {} 是普通函数调用,this 默认指向 window,刚好在 window 上找到了 name。
修正方案------箭头函数,没有自己的 this:
js
javascript
setTimeout(() => {
console.log(this.name); // 输出「两只羊」✓
}, 1000);
箭头函数继承外层词法作用域的 this,只要外层是对象方法,里层就自动拿到对象。
完整代码 1:localStorage Todo 列表
完整能跑通的 demo(含新增/读取/删除):
js
javascript
// 增:用 localStorage.setItem 存一条
function saveTodo(item) {
const todos = JSON.parse(localStorage.getItem('todos') || '[]');
todos.push({ id: Date.now(), text: item, done: false });
localStorage.setItem('todos', JSON.stringify(todos));
}
// 查:渲染列表
function loadTodos() {
return JSON.parse(localStorage.getItem('todos') || '[]');
}
// 删:通过 id 移除
function removeTodo(id) {
const todos = loadTodos().filter(t => t.id !== id);
localStorage.setItem('todos', JSON.stringify(todos));
}
// 改:勾选已完成
function toggleTodo(id) {
const todos = loadTodos().map(t =>
t.id === id ? { ...t, done: !t.done } : t
);
localStorage.setItem('todos', JSON.stringify(todos));
}
完整代码 2:this 五合一案例
js
javascript
function normal() {
console.log('普通调用 this →', this === window ? 'window' : this);
}
const obj2 = { name: '小羊' };
normal(); // window
obj2.say = function () {
console.log('对象方法 this →', this.name);
};
obj2.say(); // 小羊
function Person(name) {
this.name = name;
console.log('构造函数 this →', this.name);
}
new Person('两只羊'); // 两只羊
const btn = document.querySelector('.lnk');
btn.addEventListener('click', function (e) {
e.preventDefault();
console.log('事件 this →', this === e.currentTarget ? '事件源' : this);
});
// call / apply / bind 三兄弟
obj2.speak = function (greet, intro) {
console.log(`${greet}, ${intro}, 我是${this.name}`);
};
obj2.speak.call({ name: '强制A' }, '你好', '我是前端');
obj2.speak.apply({ name: '强制B' }, ['你好', '我是后端']);
const fnBind = obj2.speak.bind({ name: '强制C' });
fnBind('你好', '我是测试');
| 调用方式 | this 指向 | 输出 |
|---|---|---|
normal() |
window | window |
obj2.say() |
obj2 | 小羊 |
new Person() |
新实例 | 两只羊 |
btn.addEventListener(...) |
事件源 | 事件源 |
.call({强制A}) |
{强制A} | 你好,我是前端,我是强制A |
踩坑提醒:这 5 个坑别再踩
- localStorage 只能存字符串 ------直接存对象会被调
toString(),得到[object Object],记得JSON.stringify+JSON.parse。 - localStorage 容量 ≈ 5MB------存大 JSON 容易爆,敏感数据别存(明文、token),localStorage 能被同源脚本读到。
- form 默认提交会刷新页面 ------一定写
e.preventDefault(),或者直接用<button type="button">+ JS 拦截。 - var 声明会污染 window ------能用
let/const就别用var,否则定时器 / 全局函数里 this 容易抽风。 - 箭头函数没有自己的 this ------别在箭头函数里再写
arguments或想用.call()改 this,它就根本不吃这套。
结尾:八股的终点,是工程化
八股文的尽头,不是背得多熟,是遇到问题知道去哪查。
下次被面试官问「localStorage 和 sessionStorage 区别」,或「箭头函数和普通函数 this 的差异」------别硬背,先想这句话:
存储是为了下次能找到,this 是为了运行时找对人。