跳到主要内容字节跳动交易与广告前端一面面经深度解析 | 极客日志JavaScript大前端
字节跳动交易与广告前端一面面经深度解析
综述由AI生成记录了字节跳动交易与广告部门前端一面面试经历及深度解析。面试涵盖浏览器原理、CSS 布局、JS 核心机制、工程化、网络协议及框架原理。重点包括图片懒加载实现与兼容方案、文本溢出处理、闭包与防抖节流优化、事件冒泡与委托、Git 分支管理策略、跨域解决方案、React Hooks 使用规则、Vue2 与 Vue3 响应式差异以及虚拟 DOM key 的选择原则。内容旨在帮助求职者理解面试官考察逻辑,提升技术深度与场景应变能力。
RustyLab29 浏览 面经原文内容
📍面试公司:字节跳动
🕐面试时间:近期,用户上传于 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)
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
问题:原生兼容老浏览器怎么实现?
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;
}
});
window.addEventListener('resize', loadImages);
}
问题:考虑过兼容性吗?
if ('IntersectionObserver' in window) {
lazyLoadImages();
} else {
lazyLoadCompat();
}
📝 文本溢出
问题:单行溢出怎么实现
.single-line {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
问题:多行溢出怎么实现
.multi-line {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
问题:多行溢出有其他方法吗?
.multi-line-fallback {
position: relative;
line-height: 1.5em;
max-height: 4.5em;
overflow: hidden;
}
.multi-line-fallback::after {
content: "...";
position: absolute;
bottom: 0;
right: 0;
background: white;
padding-left: 0.2em;
}
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());
console.log(counter());
问题:防抖节流的应用
function debounce(fn, delay) {
let timer;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
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 搜索,第一次请求比第二次慢怎么办?
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('请求被取消');
}
}
};
}
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>
document.getElementById('parent').addEventListener('click', () => {
console.log('parent 捕获');
}, true);
document.getElementById('parent').addEventListener('click', () => {
console.log('parent 冒泡');
}, false);
document.getElementById('child').addEventListener('click', () => {
console.log('child 冒泡');
}, false);
</script>
问题:事件委托
document.querySelector('ul').addEventListener('click', (e) => {
const target = e.target;
if (target.tagName === 'LI') {
console.log('点击了', target.textContent);
}
});
🔧 Git 操作
问题:git merge 和 git rebase 的区别
| 维度 | git merge | git rebase |
|---|
| 提交历史 | 保留分支合并历史,有 merge commit | 线性历史,无 merge commit |
| 可读性 | 真实反映实际开发流程 | 更清晰简洁 |
| 冲突处理 | 一次处理 | 每个 commit 都可能处理冲突 |
| 安全性 | 安全,不会改写历史 | 危险,会改写历史 |
git checkout main
git merge feature
git checkout feature
git rebase 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 ❌ 端口不同
- 防止恶意网站读取另一个网站的敏感数据
- 限制恶意请求(CSRF 防护)
- 隔离不同来源的内容
问题:除了配置代理还能怎么办?
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');
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);
}
const ws = new WebSocket('wss://api.example.com');
window.addEventListener('message', (e) => {
if (e.origin === 'https://parent.com') {
console.log(e.data);
}
});
⚛️ React 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>;
}
function useCustomHook() {
const [value, setValue] = useState(0);
return [value, setValue];
}
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);
}
}
- 只能在函数组件或自定义 Hook 的顶层调用
- 不能在条件、循环、嵌套函数中调用
- 必须保证每次渲染时调用顺序一致
🔄 Vue2/3 响应式
问题:Vue2 和 Vue3 的响应式区别
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
get() {
return val;
},
set(newVal) {
if (newVal === val) return;
val = newVal;
}
});
}
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
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) {
}
return result;
}
});
}
🎯 虚拟 DOM 与 key
问题:为什么 key 不能用 index 实现?
const list = ['a', 'b', 'c'];
list.map((item, index) => <li key={index}>{item}</li>);
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>
<li key={`${item.type}-${item.index}`}>{item.name}</li>
- 列表是静态的,不会增删改
- 列表永远不会重新排序
- 数据量很小,性能影响可接受
📚 知识点速查表
| 知识点 | 核心要点 |
|---|
| 图片懒加载 | IntersectionObserver + scroll 降级 + 节流优化 |
| 文本溢出 | 单行 (white-space) + 多行 (-webkit-box) + JS 降级 |
| 闭包 | 函数 + 词法环境引用、内存泄漏注意 |
| 防抖节流 | 防抖 (最后一次) + 节流 (频率控制) + AbortController |
| 事件机制 | 捕获 (→) + 目标 + 冒泡 (←) + 委托 |
| Git | merge(保留历史) + rebase(线性历史) |
| 跨域 | 同源策略 + CORS/JSONP/代理/WebSocket |
| Hooks | 顶层调用 + 顺序一致 + 自定义 Hook |
| 响应式 | defineProperty(全量) vs Proxy(懒递归) |
| key | 唯一稳定 + 避免 index |
字节跳动的面试,是一场基础扎实度 + 场景应变力的双重检验。每一个问题都在追问'如果 xxx 怎么办',他们要的不是背答案的人,而是能应对各种边界情况的人。
相关免费在线工具
- 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