跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
JavaScript大前端

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

记录了字节跳动交易与广告部门前端一面面试经历及深度解析。面试涵盖浏览器原理、CSS 布局、JS 核心机制、工程化、网络协议及框架原理。重点包括图片懒加载实现与兼容方案、文本溢出处理、闭包与防抖节流优化、事件冒泡与委托、Git 分支管理策略、跨域解决方案、React Hooks 使用规则、Vue2 与 Vue3 响应式差异以及虚拟 DOM key 的选择原则。内容旨在帮助求职者理解面试官考察逻辑,提升技术深度与场景应变能力。

RustyLab发布于 2026/4/5更新于 2026/10/19111 浏览
字节跳动交易与广告前端一面面经深度解析

面经原文内容

📍面试公司:字节跳动

🕐面试时间:近期,用户上传于 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 实现

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

🎯 面试整体画像

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

🖼️ 图片懒加载

问题:图片懒加载实现(IntersectionObserver)
// IntersectionObserver 实现
function lazyLoadImages() {
  const images = document.();
   observer =  ( {
    entries.( {
       (entry.) {
         img = entry.;
        img. = img..;
        img.();
        observer.(img);
      }
    });
  }, { : , : , :  });
  images.( observer.(img));
}
querySelectorAll
'img[data-src]'
const
new
IntersectionObserver
(entries) =>
forEach
entry =>
if
isIntersecting
const
target
src
dataset
src
removeAttribute
'data-src'
unobserve
root
null
rootMargin
'0px'
threshold
0.1
forEach
img =>
observe
问题:原生兼容老浏览器怎么实现?
// 降级方案: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);
}
问题:考虑过兼容性吗?
// 完整兼容方案
if ('IntersectionObserver' in window) {
  lazyLoadImages();
} else {
  lazyLoadCompat();
}

📝 文本溢出

问题:单行溢出怎么实现
.single-line {
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}
问题:多行溢出怎么实现
/* WebKit 私有属性 */
.multi-line {
  display: -webkit-box;
  -webkit-line-clamp: 3; /* 显示 3 行 */
  -webkit-box-orient: vertical;
  overflow: hidden;
}
问题:多行溢出有其他方法吗?
/* 方案 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;
}
// 方案 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) + '...';
  }
}

🔒 闭包与防抖节流

问题:说一下闭包
// 闭包:函数 + 词法环境的引用
function createCounter() {
  let count = 0; // 被内部函数引用
  return function() {
    count++;
    return count;
  };
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
// count 变量没有被销毁,形成闭包
问题:防抖节流的应用
// 防抖:只执行最后一次
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 搜索,第一次请求比第二次慢怎么办?
// 问题场景:
// 输入"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);
    }
  };
}

🔄 事件机制

问题:事件冒泡和捕获
<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>
问题:事件委托
// 利用冒泡,父元素统一处理子元素事件
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 mergegit rebase
提交历史保留分支合并历史,有 merge commit线性历史,无 merge commit
可读性真实反映实际开发流程更清晰简洁
冲突处理一次处理每个 commit 都可能处理冲突
安全性安全,不会改写历史危险,会改写历史
# 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 整理后合并

🌐 跨域

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

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

// 同源示例
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. 隔离不同来源的内容
问题:除了配置代理还能怎么办?
// 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 吗?
// 常用 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 什么地方都能使用吗?
// ✅ 可以在函数组件中使用
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 的响应式区别
// 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 实现?
// 场景:列表渲染
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>);

问题演示:

// 初始列表:['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
// 本应只插入一个节点,却导致后面所有节点都更新
// 性能下降,且可能导致状态错误(输入框内容错位)

正确做法:

// 使用唯一标识
<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
事件机制捕获 (→) + 目标 + 冒泡 (←) + 委托
Gitmerge(保留历史) + rebase(线性历史)
跨域同源策略 + CORS/JSONP/代理/WebSocket
Hooks顶层调用 + 顺序一致 + 自定义 Hook
响应式defineProperty(全量) vs Proxy(懒递归)
key唯一稳定 + 避免 index

建议:

字节跳动的面试,是一场基础扎实度 + 场景应变力的双重检验。每一个问题都在追问'如果 xxx 怎么办',他们要的不是背答案的人,而是能应对各种边界情况的人。

目录

  1. 面经原文内容
  2. 📝 字节跳动交易与广告前端一面·面经深度解析
  3. 🎯 面试整体画像
  4. 🖼️ 图片懒加载
  5. 问题:图片懒加载实现(IntersectionObserver)
  6. 问题:原生兼容老浏览器怎么实现?
  7. 问题:考虑过兼容性吗?
  8. 📝 文本溢出
  9. 问题:单行溢出怎么实现
  10. 问题:多行溢出怎么实现
  11. 问题:多行溢出有其他方法吗?
  12. 🔒 闭包与防抖节流
  13. 问题:说一下闭包
  14. 问题:防抖节流的应用
  15. 问题:input 搜索,第一次请求比第二次慢怎么办?
  16. 🔄 事件机制
  17. 问题:事件冒泡和捕获
  18. 问题:事件委托
  19. 🔧 Git 操作
  20. 问题:git merge 和 git rebase 的区别
  21. git merge
  22. git rebase(个人分支用)
  23. 🌐 跨域
  24. 问题:为什么要设置跨域?
  25. 问题:除了配置代理还能怎么办?
  26. ⚛️ React Hooks
  27. 问题:知道 hooks 吗?
  28. 问题:hooks 在 React 什么地方都能使用吗?
  29. 🔄 Vue2/3 响应式
  30. 问题:Vue2 和 Vue3 的响应式区别
  31. 🎯 虚拟 DOM 与 key
  32. 问题:为什么 key 不能用 index 实现?
  33. 📚 知识点速查表

更多推荐文章

查看全部
  • 模型预测控制(MPC)算法
  • 深入理解 ES6 核心语法:进制、Symbol 与类继承
  • Leather Dress Collection 基于 Stable Diffusion 1.5 的皮革设计实践
  • DeepSeek-R1 大模型基于 MS-Swift 框架的部署与微调实践
  • LangChain 工具调用与结构化输出实战
  • 红黑树进阶:手撕 STL 源码实现 map 和 set
  • Mac 版 Word 缺失仿宋 GB2312 字体修复指南
  • OpenCode 开源 AI 编程助手工具介绍
  • MySQL 迁移 TCO 全景账本:隐性成本与工程化工具链实测
  • 鸿蒙 6 正式发布:功能升级构建跨设备安全流畅新生态
  • C 语言 Web 开发实战:CGI、FastCGI 与 Nginx 模块详解
  • 小米智能家居接入 Home Assistant 配置指南
  • macOS 彻底卸载 Python 的完整指南
  • 近五年体内微纳米机器人赋能肿瘤精准治疗综述:聚焦 GBM
  • Qwen2.5-7B-Instruct 工具调用集成心知天气示例
  • nanobind C++/Python 高性能绑定实战指南
  • AI 辅助开发:利用 DeepSeek 构建贪吃蛇游戏
  • 2026 年 3 月全球 AI 前沿动态:Agent、模型与工具链演进
  • 二分查找实战:山峰数组峰顶索引与寻找峰值
  • 基于 OpenClaw 快速搭建飞书 AI 机器人
  • 相关免费在线工具

    • Keycode 信息

      查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online

    • Escape 与 Native 编解码

      JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online

    • JavaScript / HTML 格式化

      使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online

    • JavaScript 压缩与混淆

      Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online

    • Base64 字符串编码/解码

      将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

    • Base64 文件转换器

      将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online