一道线上面试题

前言

来了掘金这么久,看过也收藏过这么多文章,都没写过文章,一时兴起想写文章了,那就从最简单、最多人喜欢写的面试题类型写吧!

这道面试题来源于一家广东无良公司的前端线上笔试,不过时间不是今年的,它可是让我念念不忘,为什么说这家是无良公司呢?

  1. 单休,工资低,月薪不到 5k,不交社保,工资避税以现金发放,正式员工还没年终。
  2. 新人延长试用期后,等项目做得差不多了执行劝退。
  3. 工作氛围差,领导以施压新人为乐,喜欢 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 树的结构。

以下是代码的实现思路和原理:

  1. 创建一个名为result的根节点对象,该对象包含一个tag属性,初始值为'root',以及一个children属性,初始化为空数组。result代表整个 DOM 树的根。
  2. 初始化一个名为currentParent的变量,开始时将其设置为result,表示当前处理的父节点。
  3. 初始化一个名为stack的数组,该数组用于在遇到结束标签时保存先前的currentParent,以便之后可以重新设置currentParent
  4. 使用正则表达式tagReg来匹配 HTML 标签,这个正则表达式可以匹配形如 <tag></tag><tag/> 这样的标签。
  5. 通过循环遍历 HTML 字符串,查找匹配的标签。在循环中,根据正则匹配结果,判断标签是开始标签、结束标签还是自闭合标签。
  6. 如果是结束标签,检查 stack 数组中是否还有父节点,如果有,则弹出一个父节点,将 currentParent 更新为 stack 中的最后一个元素,表示回到上一层父节点。
  7. 如果是开始标签,创建一个新的节点对象newParent,将其标签名设置为匹配的标签,初始化children为一个空数组。然后将newParent添加到currentParentchildren中,并将currentParent添加到stack数组中。最后,将newParent设置为currentParent,以便下一次迭代中的子元素可以将其添加到这个新父节点下。
  8. 如果不是标签,只是文本内容或其他字符,继续往前移动索引。
  9. 最终,返回result的第一个子节点,这就是整个 DOM 树的根节点。
  10. 最后,使用JSON.stringify将 DOM 树表示为 JSON 字符串,并在控制台输出。

结尾

为什么封面和文章图片选的都是进击的巨人呢?因为这就像推翻象牙塔的墙,接受社会的毒打啊!

在掘金发文章真难,怎么审核还不通过?

好了,本人在掘金的第一篇文章写完了,不知各位看官对这公司和面试题有何感想呢?

请在下方留下你的评论,分享到此结束,感谢您的阅读,期待您的留言!

相关推荐
yanlele7 小时前
前端面试第 75 期 - 2025.07.06 更新前端面试问题总结(12道题)
前端·javascript·面试
前端小巷子11 小时前
Web开发中的文件上传
前端·javascript·面试
你这个年龄怎么睡得着的13 小时前
为什么 JavaScript 中 'str' 不是对象,却能调用方法?
前端·javascript·面试
牛客企业服务14 小时前
2025年AI面试推荐榜单,数字化招聘转型优选
人工智能·python·算法·面试·职场和发展·金融·求职招聘
Penk是个码农16 小时前
web前端面试-- MVC、MVP、MVVM 架构模式对比
前端·面试·mvc
MrSkye16 小时前
🔥JavaScript 入门必知:代码如何运行、变量提升与 let/const🔥
前端·javascript·面试
爱学习的茄子16 小时前
深入理解JavaScript闭包:从入门到精通的实战指南
前端·javascript·面试
程序员爱钓鱼17 小时前
Go 语言泛型 — 泛型语法与示例
后端·面试·go
天涯学馆1 天前
前端开发也能用 WebAssembly?这些场景超实用!
前端·javascript·面试
然我1 天前
别再只用 base64!HTML5 的 Blob 才是二进制处理的王者,面试常考
前端·面试·html