前端面试手撕代码(字节)
字节一面给了两道手撕代码,还是比较偏向场景,不是leetcode那种逻辑题。
题目一:实现 Compile 函数
这是一道经典的前端面试题,一般面试中如果你提到框架原理,面试官都会想到这道题。 之前拼多多面试也考了,但是比这个复杂,要求还要渲染对象中的数组juejin.cn/post/747562...
题目要求
将模板字符串中的插值(如 {{user.name}}
)编译为具体的值。例如,输入模板 "Hello, {{user.name}}!"
和数据 {user: {name: "Alice"}}
,输出 "Hello, Alice!"
。
实现思路
- 正则匹配插值 :使用正则表达式(如
/\{\{([^}]+)\}\}/g
)定位模板中的插值表达式,提取变量路径(如user.name
)。 - 数据路径解析 :将变量路径按
.
分割为层级数组(如["user", "name"]
),逐层访问数据对象的属性。
代码示例:
javascript
function compile(template) {
const regex = /\{\{([^}]+)\}\}/g;
return function(data) {
return template.replace(regex, (match, path) => {
const keys = path.trim().split('.');
return keys.reduce((obj, key) => obj?.[key], data) || '';
});
};
}
// 使用示例
const template = "Hello, {{user.name}}! Age: {{age}}";
const render = compile(template);
console.log(render({ user: { name: "Alice" }, age: 25 }));
// 输出: "Hello, Alice! Age: 25"
优化点 :
• 缓存正则表达式 :避免重复创建正则对象提升性能。
• 处理深层嵌套 :通过 reduce
逐层安全访问属性,避免因路径错误导致的报错。
法二:使用with函数实现
javascript
function compile(template, context) {
// 替换模板中的插值表达式为 JavaScript 模板字符串语法
const compiledTemplate = template.replace(
/{{(.*?)}}/g,
(match, p1) => `\${${p1.trim()}}`
);
// 动态生成一个函数,用于替换模板中的插值表达式
const compiledFunction = new Function(
"context",
`
with (context) {
return \`${compiledTemplate}\`;
}
`
);
// 调用函数并返回结果
return compiledFunction(context);
}
const template =
"Hello, ${user.name}! Your balance is${user.balance}. You have ${user.items[0]} in your cart. and${user.items[2].kk}";
const exprObj = {
user: {
name: "Alice",
balance: 100.5,
items: ["Item1", "Item2", { kk: 1 }],
},
};
const compiledString = compile(template, exprObj);
console.log(compiledString);
使用with函数是非常取巧的,不仅代码少,而且处理了所有情况,面试如果想要快速实现,可以使用第法二。
题目二:树形菜单渲染
题目要求
给定树形结构数据,将其渲染为 HTML 菜单,并支持点击展开/收起子节点。例如输入:
javascript
const treeData = [
{
"id": 1,
"parent_id": 0,
"name": "北京",
"children": [
{
"id": 3,
"parent_id": 1,
"name": "海淀区",
"children": []
},
{
"id": 4,
"parent_id": 1,
"name": "朝阳区",
"children": []
}
]
}
];
输出可交互的树形菜单,点击父节点时切换子节点的显示状态。
完整实现方案
以下代码通过 DOM 操作 和事件绑定实现动态渲染与交互:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Tree Directory Component</title>
<style>
.tree-node { cursor: pointer; margin-left: 20px; }
.children { display: none; margin-left: 20px; } /* 默认隐藏子节点 */
</style>
</head>
<body>
<div id="tree"></div>
<script>
const treeData = [/* 此处省略,结构与上文一致 */];
// 递归创建树节点
function createTreeNode(node, parentElement) {
const div = document.createElement('div');
div.className = 'tree-node';
div.textContent = node.name;
// 点击事件:切换子节点显隐
div.onclick = function() {
const childrenDiv = parentElement.querySelector('.children');
if (childrenDiv) {
childrenDiv.style.display =
childrenDiv.style.display === 'none' ? 'block' : 'none';
}
};
parentElement.appendChild(div);
// 递归处理子节点
if (node.children?.length) {
const childrenDiv = document.createElement('div');
childrenDiv.className = 'children';
node.children.forEach(child => createTreeNode(child, childrenDiv));
parentElement.appendChild(childrenDiv);
}
}
// 渲染根节点
const treeElement = document.getElementById('tree');
treeData.forEach(node => createTreeNode(node, treeElement));
</script>
</body>
</html>
代码解析
-
HTML 结构
• 仅需一个
<div id="tree">
容器,所有节点通过 JavaScript 动态生成。• 样式通过 CSS 类名控制(
.tree-node
定义节点样式,.children
控制子节点缩进与显隐)。 -
核心逻辑
• 递归构建节点 :
createTreeNode
函数递归生成<div>
元素,每个节点绑定onclick
事件。• 事件处理 :点击父节点时,通过
querySelector
查找子容器,切换其display
属性实现展开/收起。• DOM 操作优化:直接操作 DOM 元素而非拼接字符串,便于动态更新和事件绑定。
-
交互扩展思路
• 动画效果 :添加 CSS
transition
属性实现平滑展开效果。• 懒加载 :点击父节点时动态请求子节点数据(需后端配合)。
• 多选/折叠 :通过
dataset
属性标记节点状态,实现全选或折叠全部子节点。
总结
字节一面考察的两道题都是经典的场景题,因该是面试中问到了vue的原理,所以顺带考了compile和render的题。