前端八股文面经大全:字节跳动交易与广告前端一面(2026-2-10)·面经深度解析

前言

大家好,我是木斯佳。

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

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

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

这个专栏的初衷很简单:拒绝过时的、流水线式的PDF引流贴,专注于收集和整理当下最新、最真实的前端面试资料。我会在每一份面经和八股文的基础上,尝试从面试官的角度去拆解问题背后的逻辑,而不仅仅是提供一份静态的背诵答案。无论你是校招还是社招,目标是中大厂还是新兴团队,只要是真实发生、有价值的面试经历,我都会在这个专栏里为你沉淀下来。

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

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

面经原文内容

📍面试公司:字节跳动

🕐面试时间:近期,用户上传于02-10

💻面试岗位:前端 - 中国交易与广告职位

👥面试形式:双面试官(一个主面,一个闭麦旁听)

⏱️面试时长:1小时

❓面试问题:

开场

  • 自我介绍
  • 实习经历拷打

CSS/浏览器

  • 图片懒加载实现(IntersectionObserver)
  • 考虑过兼容性吗?
  • 原生兼容老浏览器怎么实现图片懒加载?(没答出来)
  • 多行溢出怎么实现(display: -webkit-box)
  • 单行溢出怎么实现
  • 考虑过兼容性吗?多行溢出有其他方法实现吗?

JS核心

  • 跨域知道吗?
  • 能说一下闭包吗?(提到了防抖节流)
  • 防抖节流的应用场景
  • input搜索防抖,但万一第一次请求比第二次慢怎么办?(没答出来)
  • 节流的应用场景(鼠标频繁触发)
  • 事件冒泡和捕获
  • 事件委托

工程化

  • git merge和git rebase的区别

网络

  • 为什么要设置跨域?(域名、端口号、协议)
  • 遇到跨域问题,除了配置代理还能怎么办?

框架(Vue/React)

  • Vue多还是React多?(答Vue)
  • 知道hooks吗?(React hooks)
  • hooks在React什么地方都能使用吗?(答得不好,转问Vue)
  • Vue2和Vue3的响应式区别
  • 虚拟DOM diff算法中为什么key不能用index实现

来源:牛客网 smile丶snow

📝 字节跳动交易与广告前端一面·面经深度解析

🎯 面试整体画像

维度 特征
部门定位 字节跳动 - 中国交易与广告(核心变现业务)
面试风格 基础纵深 + 场景追问 + 兼容性敏感
难度评级 ⭐⭐⭐⭐(四星,考察全面且深入)
考察重心 浏览器原理、JS核心、工程化、框架原理

🖼️ 图片懒加载

问题:图片懒加载实现(IntersectionObserver)

javascript 复制代码
// IntersectionObserver实现
function lazyLoadImages() {
  const images = document.querySelectorAll('img[data-src]')
  
  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)
      }
    })
  }, {
    root: null,
    rootMargin: '0px',
    threshold: 0.1
  })
  
  images.forEach(img => observer.observe(img))
}

问题:原生兼容老浏览器怎么实现?

javascript 复制代码
// 降级方案:scroll事件 + getBoundingClientRect
function lazyLoadCompat() {
  const images = document.querySelectorAll('img[data-src]')
  
  function loadImages() {
    images.forEach(img => {
      const rect = img.getBoundingClientRect()
      const isVisible = rect.top < window.innerHeight && rect.bottom > 0
      
      if (isVisible && img.dataset.src) {
        img.src = img.dataset.src
        img.removeAttribute('data-src')
      }
    })
  }
  
  // 初始加载
  loadImages()
  
  // 滚动监听(节流优化)
  let ticking = false
  window.addEventListener('scroll', () => {
    if (!ticking) {
      requestAnimationFrame(() => {
        loadImages()
        ticking = false
      })
      ticking = true
    }
  })
  
  // resize也要检查
  window.addEventListener('resize', loadImages)
}

问题:考虑过兼容性吗?

javascript 复制代码
// 完整兼容方案
if ('IntersectionObserver' in window) {
  // 使用IntersectionObserver
  lazyLoadImages()
} else {
  // 使用scroll降级方案
  lazyLoadCompat()
}

📝 文本溢出

问题:单行溢出怎么实现

css 复制代码
.single-line {
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

问题:多行溢出怎么实现

css 复制代码
/* WebKit私有属性 */
.multi-line {
  display: -webkit-box;
  -webkit-line-clamp: 3;  /* 显示3行 */
  -webkit-box-orient: vertical;
  overflow: hidden;
}

问题:多行溢出有其他方法吗?

css 复制代码
/* 方案1:伪元素模拟 */
.multi-line-fallback {
  position: relative;
  line-height: 1.5em;
  max-height: 4.5em; /* 3行 * 1.5em */
  overflow: hidden;
}

.multi-line-fallback::after {
  content: "...";
  position: absolute;
  bottom: 0;
  right: 0;
  background: white;
  padding-left: 0.2em;
}
javascript 复制代码
// 方案2:JS动态计算
function truncateText(selector, lines) {
  const el = document.querySelector(selector)
  const lineHeight = parseInt(getComputedStyle(el).lineHeight)
  const maxHeight = lineHeight * lines
  
  if (el.scrollHeight > maxHeight) {
    // 二分查找截断位置
    let low = 0, high = el.textContent.length
    while (low < high) {
      const mid = Math.floor((low + high) / 2)
      el.textContent = el.textContent.slice(0, mid) + '...'
      if (el.scrollHeight > maxHeight) {
        high = mid
      } else {
        low = mid + 1
      }
    }
    el.textContent = el.textContent.slice(0, low - 1) + '...'
  }
}

🔒 闭包与防抖节流

问题:说一下闭包

javascript 复制代码
// 闭包:函数 + 词法环境的引用
function createCounter() {
  let count = 0  // 被内部函数引用
  
  return function() {
    count++
    return count
  }
}

const counter = createCounter()
console.log(counter()) // 1
console.log(counter()) // 2
// count变量没有被销毁,形成闭包

问题:防抖节流的应用

javascript 复制代码
// 防抖:只执行最后一次
function debounce(fn, delay) {
  let timer
  return function(...args) {
    clearTimeout(timer)
    timer = setTimeout(() => fn.apply(this, args), delay)
  }
}
// 应用:搜索输入、窗口resize、自动保存

// 节流:控制执行频率
function throttle(fn, interval) {
  let last = 0
  return function(...args) {
    const now = Date.now()
    if (now - last >= interval) {
      fn.apply(this, args)
      last = now
    }
  }
}
// 应用:滚动加载、鼠标移动、拖拽

问题:input搜索,第一次请求比第二次慢怎么办?

javascript 复制代码
// 问题场景:
// 输入"ab" → 请求1 (查"ab") 延迟3秒
// 输入"abc" → 请求2 (查"abc") 延迟1秒
// 请求2先返回,显示"abc"
// 请求1后返回,覆盖显示"ab" ❌

// 解决方案1:AbortController取消旧请求
function createSearch() {
  let currentController = null
  
  return async (keyword) => {
    // 取消上一次请求
    if (currentController) {
      currentController.abort()
    }
    
    currentController = new AbortController()
    
    try {
      const response = await fetch(`/api/search?q=${keyword}`, {
        signal: currentController.signal
      })
      const data = await response.json()
      renderResults(data)
    } catch (err) {
      if (err.name === 'AbortError') {
        console.log('请求被取消')
      }
    }
  }
}

// 解决方案2:请求序列号(忽略过期请求)
function createSearchWithSeq() {
  let lastRequestId = 0
  
  return async (keyword) => {
    const requestId = ++lastRequestId
    
    const data = await fetch(`/api/search?q=${keyword}`).then(r => r.json())
    
    // 只处理最新的请求
    if (requestId === lastRequestId) {
      renderResults(data)
    }
  }
}

🔄 事件机制

问题:事件冒泡和捕获

html 复制代码
<div id="parent">
  <button id="child">点击</button>
</div>

<script>
// 捕获阶段:window → document → html → body → ... → 目标元素
// 冒泡阶段:目标元素 → ... → body → html → document → window

document.getElementById('parent').addEventListener('click', () => {
  console.log('parent 捕获')
}, true) // true = 捕获阶段

document.getElementById('parent').addEventListener('click', () => {
  console.log('parent 冒泡')
}, false) // false = 冒泡阶段

document.getElementById('child').addEventListener('click', () => {
  console.log('child 冒泡')
}, false)

// 点击输出顺序:
// parent 捕获
// child 冒泡
// parent 冒泡
</script>

问题:事件委托

javascript 复制代码
// 利用冒泡,父元素统一处理子元素事件
document.querySelector('ul').addEventListener('click', (e) => {
  const target = e.target
  
  if (target.tagName === 'LI') {
    console.log('点击了', target.textContent)
    // 处理逻辑...
  }
})

// 优点:
// 1. 减少事件监听器数量(从N个变成1个)
// 2. 动态添加的元素也能被处理

🔧 Git操作

问题:git merge和git rebase的区别

维度 git merge git rebase
提交历史 保留分支合并历史,有merge commit 线性历史,无merge commit
可读性 真实反映实际开发流程 更清晰简洁
冲突处理 一次处理 每个commit都可能处理冲突
安全性 安全,不会改写历史 危险,会改写历史
bash 复制代码
# git merge
git checkout main
git merge feature
# 结果:main ← feature 有合并节点

# git rebase(个人分支用)
git checkout feature
git rebase main        # 将feature的提交"移动"到main之后
git checkout main
git merge feature      # 快进合并,得到线性历史

使用原则

  • 公共分支(main/develop):用merge
  • 个人分支(feature):用rebase整理后合并

🌐 跨域

问题:为什么要设置跨域?

同源策略:协议、域名、端口三者相同

javascript 复制代码
// 同源示例
https://example.com/page1 和 https://example.com/page2 ✅

// 不同源示例
http://example.com 和 https://example.com  ❌ 协议不同
example.com 和 api.example.com               ❌ 域名不同
example.com:8080 和 example.com:3000         ❌ 端口不同

作用

  1. 防止恶意网站读取另一个网站的敏感数据
  2. 限制恶意请求(CSRF防护)
  3. 隔离不同来源的内容

问题:除了配置代理还能怎么办?

javascript 复制代码
// 1. CORS(后端配置)
// 后端设置响应头
res.setHeader('Access-Control-Allow-Origin', 'https://example.com')
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT')
res.setHeader('Access-Control-Allow-Headers', 'Content-Type')
res.setHeader('Access-Control-Allow-Credentials', 'true')

// 2. JSONP(只支持GET)
function jsonp(url, callback) {
  const script = document.createElement('script')
  const callbackName = 'jsonp_cb_' + Date.now()
  
  window[callbackName] = (data) => {
    callback(data)
    delete window[callbackName]
    document.body.removeChild(script)
  }
  
  script.src = `${url}?callback=${callbackName}`
  document.body.appendChild(script)
}

// 3. Nginx反向代理
// nginx配置
location /api/ {
  proxy_pass http://backend-server/;
  proxy_set_header Host $host;
}

// 4. WebSocket(不受同源策略限制)
const ws = new WebSocket('wss://api.example.com')

// 5. postMessage(跨域窗口通信)
// 父页面
iframe.contentWindow.postMessage(data, 'https://child.com')
// 子页面
window.addEventListener('message', (e) => {
  if (e.origin === 'https://parent.com') {
    console.log(e.data)
  }
})

⚛️ React Hooks

问题:知道hooks吗?

javascript 复制代码
// 常用Hooks
const [state, setState] = useState(initialState)        // 状态
useEffect(() => {                                        // 副作用
  // 执行
  return () => { /* 清理 */ }
}, [dependencies])

const context = useContext(Context)                      // 上下文
const [state, dispatch] = useReducer(reducer, init)     // 复杂状态
const memoizedFn = useCallback(fn, deps)                 // 缓存函数
const memoizedValue = useMemo(() => compute(a, b), [a, b]) // 缓存值
const ref = useRef(initialValue)                          // 引用

问题:hooks在React什么地方都能使用吗?

javascript 复制代码
// ✅ 可以在函数组件中使用
function Component() {
  const [count, setCount] = useState(0)
  return <div>{count}</div>
}

// ✅ 可以在自定义Hook中使用
function useCustomHook() {
  const [value, setValue] = useState(0)
  return [value, setValue]
}

// ❌ 不能在class组件中使用
class Component extends React.Component {
  render() {
    const [count, setCount] = useState(0) // 错误!
  }
}

// ❌ 不能在普通函数中使用
function normalFunction() {
  const [count, setCount] = useState(0) // 错误!
}

// ❌ 不能在条件语句中使用
function Component() {
  if (condition) {
    const [count, setCount] = useState(0) // 错误!
  }
}

// ❌ 不能在循环中使用
function Component() {
  for (let i = 0; i < 10; i++) {
    const [count, setCount] = useState(0) // 错误!
  }
}

Hooks规则

  1. 只能在函数组件或自定义Hook的顶层调用
  2. 不能在条件、循环、嵌套函数中调用
  3. 必须保证每次渲染时调用顺序一致

🔄 Vue2/3响应式

问题:Vue2和Vue3的响应式区别

javascript 复制代码
// Vue2:Object.defineProperty
function defineReactive(obj, key, val) {
  Object.defineProperty(obj, key, {
    get() {
      // 依赖收集
      return val
    },
    set(newVal) {
      if (newVal === val) return
      val = newVal
      // 触发更新
    }
  })
}

// 缺点:
// 1. 无法检测新增/删除属性(需用Vue.set/Vue.delete)
// 2. 无法直接监听数组变化(需要重写数组方法)
// 3. 需要递归遍历所有属性(性能开销)

// Vue3:Proxy
function 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
    }
  })
}

// 优点:
// 1. 可以监听新增/删除属性
// 2. 可以监听数组索引和length变化
// 3. 懒递归(访问时才递归)

🎯 虚拟DOM与key

问题:为什么key不能用index实现?

javascript 复制代码
// 场景:列表渲染
const list = ['a', 'b', 'c']

// ❌ 使用index作为key
list.map((item, index) => <li key={index}>{item}</li>)

// ✅ 使用唯一id作为key
list.map(item => <li key={item.id}>{item.name}</li>)

问题演示

javascript 复制代码
// 初始列表:['a', 'b', 'c']
// key:        0    1    2

// 在开头插入'x'
// 新列表:['x', 'a', 'b', 'c']
// key:     0    1    2    3

// diff过程:
// key0: a → x (内容变化,需要更新)
// key1: b → a (内容变化,需要更新)
// key2: c → b (内容变化,需要更新)
// key3: 新增c

// 本应只插入一个节点,却导致后面所有节点都更新
// 性能下降,且可能导致状态错误(输入框内容错位)

正确做法

javascript 复制代码
// 使用唯一标识
<li key={item.id}>{item.name}</li>

// 如果没有id,可以使用复合标识
<li key={`${item.type}-${item.index}`}>{item.name}</li>

可以用index的例外情况

  • 列表是静态的,不会增删改
  • 列表永远不会重新排序
  • 数据量很小,性能影响可接受

📚 知识点速查表

知识点 核心要点
图片懒加载 IntersectionObserver + scroll降级 + 节流优化
文本溢出 单行(white-space) + 多行(-webkit-box) + JS降级
闭包 函数+词法环境引用、内存泄漏注意
防抖节流 防抖(最后一次) + 节流(频率控制) + AbortController
事件机制 捕获(→) + 目标 + 冒泡(←) + 委托
Git merge(保留历史) + rebase(线性历史)
跨域 同源策略 + CORS/JSONP/代理/WebSocket
Hooks 顶层调用 + 顺序一致 + 自定义Hook
响应式 defineProperty(全量) vs Proxy(懒递归)
key 唯一稳定 + 避免index

📌 最后一句:

字节跳动的面试,是一场基础扎实度 + 场景应变力 的双重检验。

每一个问题都在追问"如果xxx怎么办",

他们要的不是背答案的人,

而是能应对各种边界情况的人

相关推荐
a1117762 小时前
纸张生成器(html开源)
前端·开源·html
心.c2 小时前
虚拟滚动列表
前端·javascript·vue.js·js
祯民2 小时前
《复合型 AI Agent 开发:从理论到实践》实体书上架
前端
NEXT063 小时前
深拷贝与浅拷贝的区别
前端·javascript·面试
不写八个3 小时前
PixiJS教程(一):快速搭建环境启动项目
前端·pixijs
菜鸟小芯3 小时前
【GLM-5 陪练式前端新手入门】第二篇:CSS 让网页从 “能用” 变 “好看”
前端·css
We་ct3 小时前
LeetCode 112. 路径总和:两种解法详解
前端·算法·leetcode·typescript
Hello.Reader4 小时前
Tauri 项目结构前端壳 + Rust 内核,怎么协作、怎么构建、怎么扩展
开发语言·前端·rust
Cache技术分享4 小时前
331. Java Stream API - Java Stream 实战案例:找出合作最多的作者对
前端·后端