面试官:聊聊 LocalStorage 和 this 指向?看这篇就够了

前端存储全景 + 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.onclickbtn
箭头函数 没有自己的 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 三兄弟对比、箭头函数原理等面试高频考点,建议收藏反复阅读。

相关推荐
weedsfly1 小时前
JS垃圾回收:从原理到项目实战,彻底根治内存泄漏
前端·javascript·面试
ethantan12 小时前
AI Agent 组成:像人一样思考的智能体
人工智能·程序员·架构
ethantan13 小时前
一篇讲解AI Agent 组成:像人一样思考的智能体
人工智能·后端·程序员
刻意思考17 小时前
Alpha系列
程序员
HjhIron17 小时前
面试常客:字符串算法从入门到进阶
算法·面试
大志说编程17 小时前
Agent面试真题06: 十分钟带你快速掌握Agent记忆管理高频面试题(附详细答案)
后端·面试·ai编程
众人皆醒我独醉17 小时前
Kubernetes 为什么不直接调度容器?非要套一层 Pod
面试
烬羽17 小时前
Harness Engineering:给 AI 这匹千里马,套上一副好挽具
程序员