前端八股文面经大全:小红书前端一面(2026-2-3)·面经深度解析

前言

大家好,我是木斯佳。

在这个春节假期,当大家都在谈论返乡、团圆与休息时,作为一名技术人,我的思考却不由自主地转向了行业的「冬」与「春」。

相信很多人都感受到了,在AI浪潮的席卷之下,前端领域的门槛在变高,纯粹的"增删改查"岗位正在肉眼可见地减少。曾经热闹非凡的面经分享,如今也沉寂了许多。但我们都知道,市场的潮水退去,留下的才是真正在踏实准备、努力沉淀的人。学习的需求,从未消失,只是变得更加务实和深入。

正值春节,也是复盘与规划的好时机。结合CSDN这次「春节代码贺新年」活动所提倡的"用技术视角记录春节、复盘成长",我决定在这个假期持续更新专栏,帮助年后参加春招的同学。

这个专栏的初衷很简单:拒绝过时的、流水线式的PDF引流贴,专注于收集和整理当下最新、最真实的前端面试资料。

我会在每一份面经和八股文的基础上,尝试从面试官的角度去拆解问题背后的逻辑,而不仅仅是提供一份静态的背诵答案。无论你是校招还是社招,目标是中大厂还是新兴团队,只要是真实发生、有价值的面试经历,我都会在这个专栏里为你沉淀下来。

温馨提示:市面上的面经鱼龙混杂,甄别真伪、把握时效,是我们对抗内卷最有效的武器。

在这个假期,让我们一起充电,为下一个技术春天做好准备。

面经原文内容

📍面试公司:小红书

🕐面试时间: 2 月 3 日

💻面试岗位:前端实习

❓面试问题:

自我介绍一下

  • 你提到的项目:XX项目开发过程中遇到了什么困难。我的困难是双 token 的处理,然后讨论了一些双 token 的东西
  • 公司实习项目干了什么,然后讨论了一些问题,我是怎么处理的,面试官追问细节
  • vue3 响应式原理
  • vue 父子组件通信
  • 平时怎么写代码的
  • 手写三栏布局,多种情况,聊了一些 grid 和 flex还有追问如果是平均分要怎么写
    flex:1 是什么
  • 代码输出题,宏任务和微任务
  • 手写 promise.all,遇到一些问题,我用foreach同步代码遍历没有判断结果的数量对不对就直接resolve了
  • 反问,问了一些业务和我的一些不足点
    🙌面试感想:

直接面了 45 分钟,面试官人很好,反而问的八股很少,都是结合在手撕和项目上面,然后也是体验到要你写代码的面试了,我去面试体验 100 分,面试反馈直接写能不能给面试官加薪了,不管结果怎么样,确实是一个很棒的体验!!!然后面试官推荐我去看一些 js 的书,也可以巩固一下之类的,我觉得很对

来源: 牛客网 小海c

📝 小红书前端一面·面经深度解析

🎯 面试整体画像

维度 特征
部门定位 小红书 - 社区/电商业务前端
面试风格 项目驱动型 + 场景追问型 + 手写实战型
难度评级 ⭐⭐⭐(三星,偏重实战与原理)
考察重心 双Token机制、Vue响应式、布局实现、Promise手写
面试体验 ⭐⭐⭐⭐⭐(候选人评价极高)

💡 面经关键点:

小红书面试非常重视项目实战能力代码实现质量

八股问得少,但问到的都是核心原理,且会结合手写验证


🔐 双Token机制·项目难点深挖

问题:项目开发中遇到的困难(双Token处理)

✅ 完整答案结构:

1. 什么是双Token机制?
Token类型 有效期 存储位置 用途
Access Token 短(15分钟-2小时) 内存/ localStorage 请求认证
Refresh Token 长(7-30天) httpOnly Cookie / 安全存储 刷新Access Token
2. 为什么需要双Token?
javascript 复制代码
// 单Token的问题
- Token被盗用后,攻击者可以在有效期内任意访问
- Token无法在服务端主动失效(除非黑名单)
- 频繁登录体验差

// 双Token的优势
- 短时效Access Token降低被盗风险
- Refresh Token可以安全存储在httpOnly Cookie中
- 可以在服务端主动失效Refresh Token(如修改密码)
3. 前端实现细节
javascript 复制代码
// axios拦截器实现自动刷新
let isRefreshing = false;
let failedQueue = [];

const processQueue = (error, token = null) => {
  failedQueue.forEach(prom => {
    if (error) {
      prom.reject(error);
    } else {
      prom.resolve(token);
    }
  });
  failedQueue = [];
};

// 请求拦截器
axios.interceptors.request.use(config => {
  const token = localStorage.getItem('accessToken');
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

// 响应拦截器
axios.interceptors.response.use(
  response => response,
  async error => {
    const originalRequest = error.config;
    
    // 如果是401且不是刷新token请求
    if (error.response?.status === 401 && !originalRequest._retry) {
      if (isRefreshing) {
        // 正在刷新中,将请求加入队列
        return new Promise((resolve, reject) => {
          failedQueue.push({ resolve, reject });
        })
          .then(token => {
            originalRequest.headers.Authorization = `Bearer ${token}`;
            return axios(originalRequest);
          })
          .catch(err => Promise.reject(err));
      }
      
      originalRequest._retry = true;
      isRefreshing = true;
      
      try {
        const refreshToken = localStorage.getItem('refreshToken');
        const response = await axios.post('/auth/refresh', {
          refreshToken
        });
        
        const { accessToken, refreshToken: newRefreshToken } = response.data;
        
        localStorage.setItem('accessToken', accessToken);
        if (newRefreshToken) {
          localStorage.setItem('refreshToken', newRefreshToken);
        }
        
        processQueue(null, accessToken);
        
        originalRequest.headers.Authorization = `Bearer ${accessToken}`;
        return axios(originalRequest);
      } catch (refreshError) {
        processQueue(refreshError, null);
        // 刷新失败,跳转登录
        window.location.href = '/login';
        return Promise.reject(refreshError);
      } finally {
        isRefreshing = false;
      }
    }
    
    return Promise.reject(error);
  }
);
4. 面试官追问细节

追问1:刷新Token时,多个并发请求怎么处理?

使用队列机制,第一个请求触发刷新,其他请求排队等待新Token

追问2:Refresh Token被盗怎么办?

javascript 复制代码
// 解决方案:
1. 存储在httpOnly Cookie中(防止XSS)
2. 绑定设备指纹(UserAgent + IP)
3. 定期轮换Refresh Token
4. 登出时服务端加入黑名单

追问3:Token存储在localStorage vs Cookie?

javascript 复制代码
// localStorage
✅ 优点:简单、跨标签页共享
❌ 缺点:XSS风险

// Cookie(httpOnly)
✅ 优点:防止XSS
❌ 缺点:CSRF风险(可通过SameSite缓解)

// 最佳实践:
Access Token → 内存(变量) + 必要时存localStorage
Refresh Token → httpOnly Cookie + Secure + SameSite

⚛️ Vue3响应式原理·深度解析

问题:Vue3响应式原理

✅ 完整答案:

1. 核心机制:Proxy
javascript 复制代码
// Vue2使用Object.defineProperty
- 无法检测新增/删除属性
- 无法直接监听数组变化(需要hack)
- 需要递归遍历所有属性

// Vue3使用Proxy
const reactive = (target) => {
  return new Proxy(target, {
    get(target, key, receiver) {
      // 依赖收集
      track(target, key);
      return Reflect.get(target, key, receiver);
    },
    set(target, key, value, receiver) {
      const oldValue = target[key];
      const result = Reflect.set(target, key, value, receiver);
      if (oldValue !== value) {
        // 触发更新
        trigger(target, key);
      }
      return result;
    },
    deleteProperty(target, key) {
      const hadKey = Reflect.has(target, key);
      const result = Reflect.deleteProperty(target, key);
      if (hadKey) {
        trigger(target, key);
      }
      return result;
    }
  });
};
2. 依赖收集(track)
javascript 复制代码
// 数据结构
targetMap = WeakMap({
  target: Map({
    key: Set([effect1, effect2])
  })
})

let activeEffect;

class ReactiveEffect {
  constructor(fn) {
    this.fn = fn;
    this.deps = [];
  }
  
  run() {
    activeEffect = this;
    const result = this.fn(); // 执行时会触发get
    activeEffect = null;
    return result;
  }
}

function track(target, key) {
  if (!activeEffect) return;
  
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()));
  }
  
  let dep = depsMap.get(key);
  if (!dep) {
    depsMap.set(key, (dep = new Set()));
  }
  
  if (!dep.has(activeEffect)) {
    dep.add(activeEffect);
    activeEffect.deps.push(dep);
  }
}
3. 触发更新(trigger)
javascript 复制代码
function trigger(target, key) {
  const depsMap = targetMap.get(target);
  if (!depsMap) return;
  
  const effects = depsMap.get(key);
  if (effects) {
    effects.forEach(effect => {
      if (effect !== activeEffect) {
        effect.run(); // 重新执行
      }
    });
  }
}
4. ref vs reactive
javascript 复制代码
// reactive:处理对象
const state = reactive({ count: 0 });
state.count++; // 自动追踪

// ref:处理基本类型
const count = ref(0);
count.value++; // 需要通过.value访问

// ref的原理
function ref(value) {
  return new RefImpl(value);
}

class RefImpl {
  constructor(value) {
    this._value = value;
    this.__v_isRef = true;
  }
  
  get value() {
    track(this, 'value');
    return this._value;
  }
  
  set value(newVal) {
    this._value = newVal;
    trigger(this, 'value');
  }
}
5. 面试官追问:computed原理
javascript 复制代码
function computed(getter) {
  let value;
  let dirty = true;
  
  const effect = new ReactiveEffect(getter, () => {
    dirty = true; // 依赖变化时标记为脏
  });
  
  return {
    get value() {
      if (dirty) {
        value = effect.run();
        dirty = false;
      }
      return value;
    }
  };
}

🔄 Vue父子组件通信·全场景总结

问题:Vue父子组件通信方式

✅ 完整总结:

1. Props / Emit(最常用)
vue 复制代码
<!-- 父组件 -->
<template>
  <Child 
    :message="parentMsg"
    @update="handleUpdate"
  />
</template>

<!-- 子组件 -->
<script setup>
const props = defineProps({
  message: String
});

const emit = defineEmits(['update']);

const handleClick = () => {
  emit('update', 'new value');
};
</script>
2. v-model(双向绑定)
vue 复制代码
<!-- 父组件 -->
<Child v-model:title="pageTitle" />

<!-- 子组件 -->
<script setup>
const props = defineProps(['title']);
const emit = defineEmits(['update:title']);

const updateTitle = () => {
  emit('update:title', 'new title');
};
</script>
3. ref / expose(直接调用)
vue 复制代码
<!-- 父组件 -->
<template>
  <Child ref="childRef" />
</template>

<script setup>
const childRef = ref(null);

const callChildMethod = () => {
  childRef.value.someMethod();
};
</script>

<!-- 子组件 -->
<script setup>
const someMethod = () => {
  console.log('child method');
};

// 暴露给父组件
defineExpose({ someMethod });
</script>
4. provide / inject(跨层级)
javascript 复制代码
// 祖先组件
import { provide } from 'vue';
provide('theme', 'dark');

// 后代组件
import { inject } from 'vue';
const theme = inject('theme', 'light'); // 第二个参数为默认值
5. 事件总线(mitt)
javascript 复制代码
// event-bus.js
import mitt from 'mitt';
export const emitter = mitt();

// 组件A
import { emitter } from './event-bus';
emitter.emit('some-event', data);

// 组件B
import { emitter } from './event-bus';
emitter.on('some-event', (data) => {
  console.log(data);
});
6. Vuex / Pinia(全局状态)
javascript 复制代码
// store.js
import { defineStore } from 'pinia';

export const useMainStore = defineStore('main', {
  state: () => ({
    count: 0
  }),
  actions: {
    increment() {
      this.count++;
    }
  }
});

// 任何组件
const store = useMainStore();
store.increment();

🎨 三栏布局·手写实战

问题:手写三栏布局,多种情况

✅ 完整实现:

1. 经典圣杯布局(中间自适应,左右固定)
html 复制代码
<!-- Flexbox实现 -->
<div class="flex-container">
  <div class="left">左侧固定 200px</div>
  <div class="center">中间自适应</div>
  <div class="right">右侧固定 200px</div>
</div>

<style>
.flex-container {
  display: flex;
  height: 200px;
}
.left {
  width: 200px;
  background: #f0f0f0;
}
.center {
  flex: 1; /* 自动占据剩余空间 */
  background: #e0e0e0;
}
.right {
  width: 200px;
  background: #d0d0d0;
}
</style>
2. Grid实现
html 复制代码
<div class="grid-container">
  <div class="left">左侧固定</div>
  <div class="center">中间自适应</div>
  <div class="right">右侧固定</div>
</div>

<style>
.grid-container {
  display: grid;
  grid-template-columns: 200px 1fr 200px; /* 固定 自适应 固定 */
  height: 200px;
}
.left { background: #f0f0f0; }
.center { background: #e0e0e0; }
.right { background: #d0d0d0; }
</style>
3. 双飞翼布局(中间优先加载)
html 复制代码
<div class="container">
  <div class="center">
    <div class="center-content">中间内容(优先加载)</div>
  </div>
  <div class="left">左侧</div>
  <div class="right">右侧</div>
</div>

<style>
.container {
  display: flex;
}
.center {
  order: 2;
  flex: 1;
  margin: 0 200px; /* 给左右留空间 */
}
.left {
  order: 1;
  width: 200px;
  margin-left: -100%;
}
.right {
  order: 3;
  width: 200px;
  margin-left: -200px;
}
</style>
4. 平均分三列
html 复制代码
<!-- 三列平均分布 -->
<div class="equal-container">
  <div class="item">1/3</div>
  <div class="item">1/3</div>
  <div class="item">1/3</div>
</div>

<style>
/* Flex实现 */
.equal-container {
  display: flex;
}
.item {
  flex: 1; /* 每个item占据相等空间 */
  height: 200px;
}

/* Grid实现 */
.equal-container {
  display: grid;
  grid-template-columns: repeat(3, 1fr); /* 三个等分列 */
}
</style>
5. 面试官追问:flex:1 是什么?
css 复制代码
/* flex: 1 是简写 */
flex: 1 1 0%;
/* 相当于:
  flex-grow: 1;    // 有剩余空间时放大
  flex-shrink: 1;  // 空间不足时缩小
  flex-basis: 0%;  // 初始大小为0
*/

/* 其他常见值 */
flex: 0 1 auto;  /* 默认值 */
flex: 2;         /* flex: 2 1 0% */
flex: auto;      /* flex: 1 1 auto */
flex: none;      /* flex: 0 0 auto */

🔄 事件循环·代码输出题

问题:宏任务和微任务执行顺序

✅ 完整解析:

1. 基础概念回顾
javascript 复制代码
// 宏任务(MacroTask)
- setTimeout
- setInterval
- setImmediate (Node)
- I/O
- UI渲染

// 微任务(MicroTask)
- Promise.then/catch/finally
- MutationObserver
- queueMicrotask
- process.nextTick (Node)
2. 常见考题
javascript 复制代码
console.log('1: 同步');

setTimeout(() => {
  console.log('2: setTimeout');
}, 0);

Promise.resolve().then(() => {
  console.log('3: Promise');
}).then(() => {
  console.log('4: Promise chain');
});

console.log('5: 同步');

// 输出顺序:
// 1: 同步
// 5: 同步
// 3: Promise
// 4: Promise chain (微任务产生的微任务)
// 2: setTimeout

// 关键点:微任务产生的微任务会在当前宏任务结束前执行,不是下一帧!
3. 进阶考题(async/await)
javascript 复制代码
async function async1() {
  console.log('async1 start');
  await async2();
  console.log('async1 end');
}

async function async2() {
  console.log('async2');
}

console.log('script start');

setTimeout(() => {
  console.log('setTimeout');
}, 0);

async1();

new Promise((resolve) => {
  console.log('promise1');
  resolve();
}).then(() => {
  console.log('promise2');
});

console.log('script end');

// 输出顺序:
// script start
// async1 start
// async2
// promise1
// script end
// async1 end  (await后面的相当于then)
// promise2
// setTimeout
4. 候选人复盘的关键点

"我以为微任务产生的微任务是丢到下一帧处理的,这个搞错了"

javascript 复制代码
// ❌ 错误理解
微任务队列: [task1]
执行 task1 → 产生微任务 task1-1
task1-1 放到"下一帧"的微任务队列

// ✅ 正确理解
微任务队列: [task1]
执行 task1 → 产生微任务 task1-1
task1-1 放到**当前微任务队列末尾**
继续执行直到队列清空

🛠️ 手写Promise.all·常见错误

问题:手写Promise.all

✅ 完整实现:

1. 标准实现
javascript 复制代码
function promiseAll(promises) {
  return new Promise((resolve, reject) => {
    if (!Array.isArray(promises)) {
      return reject(new TypeError('参数必须是数组'));
    }
    
    const results = [];
    let completed = 0;
    
    // 处理空数组
    if (promises.length === 0) {
      return resolve(results);
    }
    
    promises.forEach((promise, index) => {
      // 确保每个元素都是Promise
      Promise.resolve(promise)
        .then(value => {
          results[index] = value;
          completed++;
          
          if (completed === promises.length) {
            resolve(results);
          }
        })
        .catch(reject); // 任何一个失败,直接reject
    });
  });
}
2. 候选人实现分析
javascript 复制代码
// ❌ 不友好实现(用forEach同步遍历,直接resolve)
function promiseAllWrong(promises) {
  return new Promise((resolve, reject) => {
    const results = [];
    
    promises.forEach((promise, index) => {
      promise.then(value => {
        results[index] = value;
        // 这里直接resolve,没有判断是否所有都完成
        resolve(results); // ❌ 第一个完成就resolve了
      }).catch(reject);
    });
  });
}

// 问题:第一个Promise完成就直接resolve了
// 没有等待所有Promise完成
3. 完善版(考虑各种情况)
javascript 复制代码
function promiseAllComplete(promises) {
  return new Promise((resolve, reject) => {
    if (!Array.isArray(promises)) {
      return reject(new TypeError('参数必须是数组'));
    }
    
    const results = new Array(promises.length);
    let completed = 0;
    let rejected = false;
    
    if (promises.length === 0) {
      return resolve(results);
    }
    
    promises.forEach((promise, index) => {
      Promise.resolve(promise)
        .then(value => {
          if (rejected) return;
          
          results[index] = value;
          completed++;
          
          if (completed === promises.length) {
            resolve(results);
          }
        })
        .catch(error => {
          if (!rejected) {
            rejected = true;
            reject(error);
          }
        });
    });
  });
}
4. 相关API对比
javascript 复制代码
// Promise.allSettled - 等待所有完成,不关心成功/失败
Promise.allSettled = function(promises) {
  return new Promise(resolve => {
    const results = [];
    let completed = 0;
    
    promises.forEach((promise, index) => {
      Promise.resolve(promise)
        .then(value => {
          results[index] = { status: 'fulfilled', value };
        })
        .catch(reason => {
          results[index] = { status: 'rejected', reason };
        })
        .finally(() => {
          completed++;
          if (completed === promises.length) {
            resolve(results);
          }
        });
    });
  });
};

// Promise.race - 谁先完成就返回谁
Promise.race = function(promises) {
  return new Promise((resolve, reject) => {
    promises.forEach(promise => {
      Promise.resolve(promise)
        .then(resolve)
        .catch(reject);
    });
  });
};

🎁 附:小红书面试复习清单

知识点 掌握程度 重点方向
双Token机制 ⭐⭐⭐⭐ 实现、并发处理、安全
Vue3响应式 ⭐⭐⭐⭐ Proxy、依赖收集、ref/reactive
组件通信 ⭐⭐⭐ props/emit、provide/inject、v-model
三栏布局 ⭐⭐⭐ flex、grid、圣杯/双飞翼
flex:1 ⭐⭐⭐ 含义、计算规则
事件循环 ⭐⭐⭐⭐ 宏任务/微任务、async/await
Promise.all ⭐⭐⭐⭐ 手写实现、边界情况
错误复盘 ⭐⭐⭐⭐ 微任务连续处理、Promise计数

📌 最后一句:

小红书的面试,是一场技术实战 + 学习能力 的温暖对话。

他们不只想要一个能写代码的人,

他们想要一个能一起解决问题、一起成长的人

当面试官推荐你看书的时候,他不是在说你不行,而是在说:
"我看好你,希望你能变得更好。"

相关推荐
心之语歌2 小时前
flutter 父子组件互相调用方法,值更新
前端·javascript·flutter
geovindu2 小时前
python: State Pattern
python·状态模式
岱宗夫up2 小时前
FastAPI进阶3:云原生架构与DevOps最佳实践
前端·python·云原生·架构·前端框架·fastapi·devops
赛博切图仔2 小时前
告别“打字机”:Generative UI 如何重塑 AI 时代的前端交互?
前端·人工智能·ui
wangbing11252 小时前
开发指南141-类和字节数组转换
java·服务器·前端
~央千澈~2 小时前
抖音弹幕游戏开发之第15集:添加配置文件·优雅草云桧·卓伊凡
java·前端·python
wsad05322 小时前
Shell 脚本中的多行注释和 Here Document 语法解析
前端·chrome
J2虾虾3 小时前
Vite前端项目构建
前端
HelloReader3 小时前
Tauri 用“系统 WebView + 原生能力”构建更小更快的跨平台应用
前端·javascript·后端