一道线上面试题

前言

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

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

  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 字符串,并在控制台输出。

结尾

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

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

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

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

相关推荐
一个 00 后的码农4 小时前
25轻化工程研究生复试面试问题汇总 轻化工程专业知识问题很全! 轻化工程复试全流程攻略 轻化工程考研复试真题汇总
面试·面试问题·25考研·考研复试·考研调剂·面试真题·轻化工程
刘小炮吖i4 小时前
Java基础常见的面试题(易错!!)
java·面试·职场和发展
拉不动的猪5 小时前
刷刷题17(webpack)
前端·javascript·面试
why技术5 小时前
可以说是一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
后端·面试
白初&7 小时前
安全面试4
安全·面试·职场和发展
张胤尘7 小时前
Lua | 每日一练 (3)
开发语言·面试·lua
Distance失落心8 小时前
java基于数组实现队列(四)
java·开发语言·数据结构·算法·面试·java-ee·intellij-idea
Luo_LA8 小时前
【Java 面试 八股文】JVM 虚拟机篇
java·jvm·面试
Pandaconda8 小时前
【Golang 面试题】每日 3 题(六十五)
开发语言·经验分享·笔记·后端·面试·golang·go
Good Note9 小时前
Golang的静态强类型、编译型、并发型
java·数据库·redis·后端·mysql·面试·golang