
前言
来了掘金这么久,看过也收藏过这么多文章,都没写过文章,一时兴起想写文章了,那就从最简单、最多人喜欢写的面试题类型写吧!
这道面试题来源于一家广东无良公司的前端线上笔试,不过时间不是今年的,它可是让我念念不忘,为什么说这家是无良公司呢?
- 单休,工资低,月薪不到 5k,不交社保,工资避税以现金发放,正式员工还没年终。
- 新人延长试用期后,等项目做得差不多了执行劝退。
- 工作氛围差,领导以施压新人为乐,喜欢 PUA ,让下属自生自灭。
所以说了这么多,这种公司会出什么面试题呢?一起来看看吧,主角闪亮登场,噔噔噔!
手写真实 DOM 转虚拟 DOM
js
// 不使用浏览器 API 实现简单的 html 源代码解析函数,返回所有节点树关系结构的数据
function parse(html) {
// 在这里完成这个函数的实现
}
// 在不修改下面代码的情况下,能满足下面列举的使用
let doc = parse(`
<html>
<head>
<title>Hello</title>
</head>
<body>
<div id="container">
<div class="header"></div>
<div class="content"></div>
<div class="footer"></div>
</div>
</body>
</html>
`);
console.log(JSON.stringify(doc, undefined, 2));
// 运行结果输出
// {
// tag: 'html',
// children: [
// {
// tag: 'head',
// children: [
// {
// tag: 'title',
// children: []
// }
// ]
// },
// {
// tag: 'body',
// children: [
// {
// tag: 'div',
// children: [
// {
// tag: 'div',
// children: []
// },
// {
// tag: 'div',
// children: []
// },
// {
// tag: 'div',
// children: []
// }
// ]
// }
// ]
// },
// ]
// }
什么?你做出来了!恭喜你,获得一份月薪不到 5k 的前端工作!(虽然还有一轮面试)
什么?你还是想入职!小年轻,你来,你来,看我整不整你就完事了!
什么?你做不出来!题目和你在其它地方看的不一样?无所谓,出着玩的,一般人都做不出来,何况线上笔试还是限时的,那当然是只好有请 ChatGPT AI + 人工提示调试完成啦!顺便一提,那时还没有出现 ChatGPT 。
使用浏览器 API 实现的版本
你说不使用浏览器 API 实现就不使用吗?那我偏要使用浏览器 API 实现,该怎么实现呢?
js
function parse(html) {
let parser = new DOMParser();
// 将 HTML 字符串解析为 DOM
let docNode = parser.parseFromString(html, "text/html");
// 获取 DOM 的根元素,即 <html> 元素
let dom = docNode.firstChild;
// 定义一个递归函数 converter,它接受一个 DOM 节点作为参数,并将其转化为 JavaScript 对象。
function converter(dom) {
const obj = {
tag: dom.tagName.toLowerCase(),
// 使用 Array.from(dom.children) 获取当前 DOM 节点的子节点,
// 通过 .map(converter) 递归地调用 converter 函数,
// 将子节点转化为对象,并将这些子节点对象作为数组存储在 children 属性中。
children: Array.from(dom.children).map(converter)
};
return obj;
}
return converter(dom);
}
let doc = parse(`
<html>
<head>
<title>Hello</title>
</head>
<body>
<div id="container">
<div class="header"></div>
<div class="content"></div>
<div class="footer"></div>
</div>
</body>
</html>
`);
console.log(JSON.stringify(doc, undefined, 2));
有请解说:
这段代码通过递归解析 HTML DOM,将其转化为嵌套的 JavaScript 对象,其中包括标签名和子元素,
最终输出为 JSON 字符串表示 HTML 文档的结构。
不使用浏览器 API 实现的版本 一
不带注释版:
js
function parse(html) {
let result = { tag: 'root', children: [] };
let currentParent = result;
let stack = [];
const tagReg = /<(\/?)(\w+)(\s*\/?)>/;
let i = 0;
while (i < html.length) {
const match = html.slice(i).match(tagReg);
if (match) {
let tag = match[2];
i += match.index + match[0].length;
if (match[1] === "/") {
if (stack.length > 0) {
currentParent = stack.pop();
}
} else if (match[3] !== "/") {
let newParent = {
tag: tag,
children: [],
};
if (currentParent.children) {
currentParent.children.push(newParent);
}
if (Object.keys(currentParent).length > 0) {
stack.push(currentParent);
}
currentParent = newParent;
}
} else {
i++;
}
}
return result.children[0];
}
let html = `
<html>
<head>
<title>Hello</title>
</head>
<body>
<div>
<div></div>
<div></div>
<div></div>
</div>
</body>
</html>
`;
let doc = parse(html);
console.log(JSON.stringify(doc, undefined, 2));
带注释版:
js
function parse(html) {
let result = { tag: 'root', children: [] };
let currentParent = result;
// 该数组用于在遇到结束标签时保存先前的 currentParent ,以便之后可以重新设置 currentParent
let stack = [];
const tagReg = /<(\/?)(\w+)(\s*\/?)>/;
let i = 0;
// html.length 模板字符串的长度包含空格及换行
while (i < html.length) {
const match = html.slice(i).match(tagReg);
if (match) {
// (\w+) 匹配到标签名
let tag = match[2];
// 寻找下一个标签
// match.index 表示 match[0] 第一个字符所在的索引,举例:第一次 match[0] 为 <html> ,第二次 match[0] 为 <head>
i += match.index + match[0].length;
// (/?) 匹配到如果是结束标签,检查 stack 数组,如果不为空,则弹出一个父节点,将其设置为 currentParent
if (match[1] === "/") {
if (stack.length > 0) {
// 第五次遇到 </title>
// currentParent 赋值为 { tag: 'head', children: Array(1) }
// 第六次遇到 </head>
// currentParent 赋值为 { tag: 'html', children: Array(1) }
currentParent = stack.pop();
}
} else if (match[3] !== "/") {
// 如果是开始标签,创建一个新的节点对象 newParent ,将其标签名设置为匹配的标签,初始化 children 为一个空数组。
// 第一次遇到 <html> newParent: { tag: 'html' , children: [] }
// 第二次遇到 <head> newParent: { tag: 'head', children: [] }
// 第三次遇到 <title> newParent: { tag: 'title', children: [] }
// 第七次遇到 <body> newParent: { tag: 'body', children: [] }
let newParent = {
tag: tag,
children: [],
};
// 然后将 newParent 添加到 currentParent 的 children 中,并将 currentParent 添加到 stack 数组中。
if (currentParent.children) {
// 第一次 currentParent { tag: 'root', children: [{ tag: 'html' , children: [] }] }
// 第二次 currentParent { tag: 'html', children: [{ tag: 'head', children: [] }] }
// 第三次 currentParent { tag: 'head', children: [{ tag: 'title', children: [] }] }
currentParent.children.push(newParent);
}
if (Object.keys(currentParent).length > 0) {
// 第一次 stack: [{ tag: 'root', children: [{ tag: 'html' , children: [] }] } ]
// 第二次 stack: [{ tag: 'root', children: Array(1) } , { tag: 'html', children: Array(1) }]
// 第三次 stack: [{ tag: 'root', children: Array(1) } , { tag: 'html', children: Array(1) } , { tag: 'head', children: Array(1) }]
stack.push(currentParent);
}
// 最后,将 newParent 设置为 currentParent ,以便下一次迭代中的子元素可以将其添加到这个新父节点下。
// 第一次 currentParent: { tag: 'html' , children: [] }
// 第二次 currentParent: { tag: 'head', children: [] }
// 第三次 currentParent:{ tag: 'title', children: [] }
currentParent = newParent;
}
} else {
// 如果不是标签,只是文本内容或其他字符,继续往前移动
i++;
}
}
return result.children[0];
}
let html = `
<html>
<head>
<title>Hello</title>
</head>
<body>
<div>
<div></div>
<div></div>
<div></div>
</div>
</body>
</html>
`;
let doc = parse(html);
console.log(JSON.stringify(doc, undefined, 2));
再次有请解说:
这段 JavaScript 代码实现了一个不同的 HTML 解析器,它通过扫描 HTML 字符串并使用栈来构建 DOM 树的结构。
以下是代码的实现思路和原理:
- 创建一个名为
result
的根节点对象,该对象包含一个tag
属性,初始值为'root'
,以及一个children
属性,初始化为空数组。result
代表整个 DOM 树的根。 - 初始化一个名为
currentParent
的变量,开始时将其设置为result
,表示当前处理的父节点。 - 初始化一个名为
stack
的数组,该数组用于在遇到结束标签时保存先前的currentParent
,以便之后可以重新设置currentParent
。 - 使用正则表达式
tagReg
来匹配 HTML 标签,这个正则表达式可以匹配形如<tag>
、</tag>
、<tag/>
这样的标签。 - 通过循环遍历 HTML 字符串,查找匹配的标签。在循环中,根据正则匹配结果,判断标签是开始标签、结束标签还是自闭合标签。
- 如果是结束标签,检查
stack
数组中是否还有父节点,如果有,则弹出一个父节点,将currentParent
更新为stack
中的最后一个元素,表示回到上一层父节点。 - 如果是开始标签,创建一个新的节点对象
newParent
,将其标签名设置为匹配的标签,初始化children
为一个空数组。然后将newParent
添加到currentParent
的children
中,并将currentParent
添加到stack
数组中。最后,将newParent
设置为currentParent
,以便下一次迭代中的子元素可以将其添加到这个新父节点下。 - 如果不是标签,只是文本内容或其他字符,继续往前移动索引。
- 最终,返回
result
的第一个子节点,这就是整个 DOM 树的根节点。 - 最后,使用
JSON.stringify
将 DOM 树表示为 JSON 字符串,并在控制台输出。
结尾
为什么封面和文章图片选的都是进击的巨人呢?因为这就像推翻象牙塔的墙,接受社会的毒打啊!
在掘金发文章真难,怎么审核还不通过?
好了,本人在掘金的第一篇文章写完了,不知各位看官对这公司和面试题有何感想呢?
请在下方留下你的评论,分享到此结束,感谢您的阅读,期待您的留言!