常考面试题:场景题系列(一)

1,328 阅读5分钟

前言

在面试中场景题是经常会考到的,本文将介绍几个常考的场景题,分别是数组、对象的扁平化,自动重新发送请求,事件循环,虚拟DOM渲染。

正文

场景一:数组的扁平化

问题描述: 给定一个可能包含多层嵌套数组的数组,将其转换为一个单一维度的数组。例如,将数组 [1, [2, 3, [4]]] 转换为 [1, 2, 3, 4]

示例代码:

const arr = [1, [2, 3, [4]]];

// 方法一:使用ES6的flat方法
const arr1 = arr.flat(Infinity);
console.log(arr1);  // 输出: [1, 2, 3, 4]

// 方法二:手写一个flatArr方法
function flatArr(arr) {
    let res = [];
    for (let item of arr) {
        if (Array.isArray(item)) {
            // 如果当前项还是数组,则递归调用flatArr
            res = res.concat(flatArr(item));
        } else {
            // 否则直接将元素加入结果数组
            res.push(item);
        }
    }
    return res;
}
console.log(flatArr(arr));  // 输出: [1, 2, 3, 4]

// 方法三:使用字符串分割和转换
function flatArrWithString(arr) {
    let res = arr.toString();
    return res.split(',').map(item => Number(item));
}

console.log(flatArrWithString(arr));  // 输出: [1, 2, 3, 4]

解释:

  1. 使用ES6的flat方法:

    • arr.flat(Infinity) 是ES6提供的方法,Infinity可以无限次地展开数组,也可以自己写。
    • 这种方法简单易用,但在旧版浏览器中可能不支持。
  2. 手写的flatArr方法:

    • flatArr 函数通过递归方式检查数组中的每一项,如果是数组,则继续展开,通过concat方法拼接;如果不是,则直接添加到结果数组中。
    • 这种方法适用于任何版本的JavaScript,并且易于理解和实现。
  3. 使用字符串分割和转换:

    • 将数组转换为字符串,然后通过逗号分割,最后再转换回数字。

场景二:对象的扁平化

问题描述: 给定一个可能包含多层嵌套的对象,将其转换为扁平化的形式。例子在为下面

示例代码:

const obj={
    a:1,
    b:[1,2,{c:true},[3]],
    d:{e:2,f:3},
    g:null
}

// const obj2={
//      a:1,
//     'b[0]':1,
//     'b[1]':1,
//     'b[2].c':true,
//     'b[3][0]':3,
//     'd.e':2,
//     'd.f':3,
//     // g:null  
// }

// 把obj转化为obj2

const flattenRes=flattenObj(obj)
console.log(flattenRes);
function flattenObj(obj) {
    let res = {};  // 初始化一个空对象用来存储结果
    const help = (target, oldKey) => {  // 定义一个辅助函数
        for (let key in target) {  // 遍历传入对象的所有属性
            let newKey;
            if (oldKey) {  // 如果存在父级键名
                if (Array.isArray(target)) {
                    newKey = `${oldKey}[${key}]`;  // 数组的情况下,键名以方括号形式表示层级
                } else {
                    newKey = `${oldKey}.${key}`;  // 对象的情况下,键名以点的形式表示层级
                }
            } else {
                if (Array.isArray(target)) {
                    newKey = `[${key}]`;  // 如果顶级元素是数组,则直接使用方括号表示
                } else {
                    newKey = key;  // 否则使用属性名本身作为键名
                }
            }
            if (Object.prototype.toString.call(target[key]) === '[object Object]' || Array.isArray(target[key])) {
                help(target[key], newKey);  // 如果值还是对象或者数组,则递归调用
            } else if (target[key] !== null && target[key] !== undefined) {
                res[newKey] = target[key];  // 如果值不是 null 或者 undefined,则将其存入结果对象
            }
        }
    };
    help(obj, '');  // 调用辅助函数开始扁平化过程
    return res;  // 返回扁平化后的对象
}

解释:

  • flattenObj 函数使用递归方式遍历对象的所有属性。
  • 如果遇到嵌套的对象或数组,则继续调用自身进行扁平化。
  • 最终结果是一个新的对象,其中的键名表示了原对象中属性的层级关系。

场景三:实现自动重新发送请求

问题描述: 实现一个函数,用于发送请求。如果请求失败,则自动重新发送请求,直到请求成功或者达到规定的最大尝试次数为止。

示例代码:

function getData(){
    console.log('发送请求')
    const n=Math.random()
    return new Promise((resolve,reject)=>{
        setTimeout(()=>{
            if(n>0.9){
                resolve(n)
            }else{
                reject(n)
            }
        },1000)
    })
}

 function again(promiseFn, times = 5) {
    let err = null
    return new Promise(async (resolve, reject) => {
      while (times) {
        try {
          let ret = await promiseFn()
          resolve(ret)
          break;
        } catch (error) {
          times -= 1
          err = error
        }
      }
      if (!times) {
        reject(err)
      }
    })
  }

again(getData).then(res=>{
    console.log(`发送成功:${res}`)
})
.catch(error=>{
    console.log(`发送失败:${error}`);    
})

效果:

请求成功的效果:

image.png

请求失败的效果:

image.png

解释:

  • getData: 人为模拟了一个发送请求的过程,并且我们在这里用随机生成的数字模拟了请求可能失败。
  • again 函数负责处理请求的重试逻辑,如果请求成功就把结果resolve出来,停止循环,如果请求失败并且还有剩余次数,则再次调用promiseFn即上面的getData
  • 最后,我们通过 .then 和 .catch 方法来处理请求的结果或错误。

场景四:事件循环的理解

问题描述: 分析以下代码的执行顺序。

示例代码:

setImmediate(() => {
    console.log(1);
  },0)
  setTimeout(() => {
    console.log(2);
  }, 0)
  new Promise((resolve, reject) => {
    console.log(3);
    resolve()
    console.log(4);
  }).then(() => {
    console.log(5);
  })
  async function test() {
    const a = await 9
    console.log(a);
    const b = await new Promise((resolve) => {
      resolve(10);
    })
    console.log(b);
  }
  test()
  console.log(6);
  process.nextTick(() => {
    console.log(7);
  })
  console.log(8);

  //打印结果:
  //3
  //4
  //6
  //8
  //7
  //5
  //9
  //10
  //2
  //1

解释:

v8从上到下执行过程

  • setImmediate(() => { console.log(1); });异步代码,放入宏任务队列中,在当前事件循环结束时执行,(当前事件循环最后一个执行)。
  • setTimeout(() => { console.log(2); }, 0); 异步代码,放入宏任务队列中。
  • console.log(3); 和 console.log(4); 在Promise构造函数中,是同步代码立即执行,打印3,4
  • then(() => { console.log(5); }); 异步代码,放入微任务队列中。
  • test()的调用带来了const a = await 9;,有await后面代码不用管,放入微任务队列中。
  • console.log(6); 同步代码立即执行,打印6
  • process.nextTick(() => { console.log(7); }); 异步代码,放入微任务队列中(会在当前同步代码执行完之后立即执行,可以理解为现实生活中大家都排队,但是它上面有关系,我上面有人,第一个执行)。
  • console.log(8);同步代码立即执行,打印8

同步代码执行完,执行异步代码

  • process.nextTick(() => { console.log(7); });执行打印7
  • then(() => { console.log(5); });执行打印5
  • console.log(a)执行打印9
  • const b = await new Promise有await后面代码不用管,放入当前事件循环微任务队列的末尾。
  • 这时只有console.log(b)一个微任务,执行打印10;

微任务代码执行完,执行宏任务代码

  • setTimeout(() => { console.log(2); }, 0);执行打印2
  • setImmediate(() => { console.log(1); });执行打印1

场景五:虚拟DOM渲染

问题描述: 给定一个虚拟DOM节点,将其渲染到页面上。

示例代码:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
  <div id="root"></div>

  <script>
    //给定的dom代码
    const vnode = {
      tag: 'div',
      attrs: {
        id: 'app',
        className: 'box',
        style: {width: '100px', height: '100px',background: 'red'}
      },
      children: [
        {
          tag: 'span',
          children: [{
            tag: 'a',
            children: [],
          }],
        },
        {
          tag: 'span',
          children: [{
            tag: 'a',
            children: [],
          }]
        }
      ]
    }
    
    render(vnode, document.getElementById('root'));  
    function render(vnode, container) {  // 定义 render 函数,接受虚拟 DOM 和容器作为参数
      const newDom = createDom(vnode);  // 调用 createDom 函数创建 DOM 节点
      container.appendChild(newDom);  // 将创建的 DOM 节点添加到容器中
    }

    function createDom(vnode) {  // 定义 createDom 函数,用于创建 DOM 节点
      const { tag, attrs, children } = vnode;  // 解构 vnode 的属性
      const dom = document.createElement(tag);  // 创建 DOM 节点
      if (typeof attrs === 'object' && attrs !== null) {  // 如果 attrs 是一个对象且不为空
        updateProps(dom, {}, attrs);  // 更新 DOM 节点的属性
      }
      if (children.length > 0) {  // 如果有子节点
        reconcileChildren(children, dom);  // 递归渲染子节点
      }
      return dom;  // 返回创建的 DOM 节点
    }

    function updateProps(dom, oldProps = {}, newProps = {}) {  // 更新 DOM 节点的属性
      for (const key in newProps) {  // 遍历新的属性对象
        if (key === 'style') {  // 如果是样式属性
          let styleObj = newProps[key];  // 获取样式对象
          for (let attr in styleObj) {  // 遍历样式对象
            dom.style[attr] = styleObj[attr];  // 设置 DOM 节点的样式属性
          }
        } else {  // 其他属性
          dom[key] = newProps[key];  // 设置 DOM 节点的其他属性
        }
      }
    }

    function reconcileChildren(children, dom) {  // 递归渲染子节点
      for (let child of children) {  // 遍历子节点
        render(child, dom);  // 递归调用 render 函数渲染子节点
      }
    }
  </script>
</body>

</html>

效果:

image.png 解释:

  • vnode 定义了一个虚拟DOM树。
  • render 函数负责将虚拟DOM树渲染到页面上的指定容器中。
  • createDom 函数创建真实的DOM节点,并设置其属性。
  • updateProps 函数更新DOM节点的属性。
  • reconcileChildren 函数递归地处理子节点,确保所有子节点都被正确地渲染到DOM树中。

总结

本文到此就结束了,希望这五个场景题能对你有所帮助,感谢你的阅读!