先拿百度的面试题开开胃

1,939 阅读7分钟

面试的节奏不能停,今天我们就来感受一下百度的面试题,不仅涵盖八股,更多还是实际开发中的场景题和代码题。

image.png

Baidu

手风琴

如何去实现一个类似于手风琴效果的页面;

chrome-capture-2024-9-3.gif

手风琴效果本质上是通过块级元素来实现的,元素是从上到下排列的。

布局结构:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>手风琴</title>
    <link rel="stylesheet" href="common.css">
</head>
<body>
    <ul class="accordion">
        <li>1</li>
        <li>2</li>
        <li>3</li>
        <li>4</li>
        <li>5</li>
        <li>6</li>
    </ul>
</body>
</html>

Stylus 使用

在 CSS 这块,我们可以用 stylus 去实现;Stylus 是 CSS 的预处理器,可以通过简化语法来减少代码的冗余,特别是在编写手风琴效果的时候,Stylus 的缩进和模块化特性可以极大简化编写过程

common.styl

*
    margin 0
    padding 0
    box-sizing border-box 

ul,li
    list-style none

.accordion
    display flex
    width 600px
    height 200px
    li 
        flex 1
        cursor pointer
        line-height 200px
        text-align center
        color white
        transition flex 500ms
        &:nth-child(1)
            background-color #f66
        &:nth-child(2)
            background-color #66f
        &:nth-child(3)
            background-color #f90
        &:nth-child(4)
            background-color #09f
        &:nth-child(5)
            background-color #9c3
        &:nth-child(6)
            background-color #3c9
        &:hover
            flex 2

编译命令:

stylus common.styl -o common.css
  • -o 参数用于指定输出文件。

  • 使用 -w 选项可以持续监听 .styl 文件的变化:

stylus -w common.styl -o common.css

不仅方便编写,stylus 还给 css 引入了编程特性

编译后的 CSS 文件:

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
ul,
li {
  list-style: none;
}
.accordion {
  display: flex;
  width: 600px;
  height: 200px;
}
.accordion li {
  flex: 1;
  cursor: pointer;
  line-height: 200px;
  text-align: center;
  color: #fff;
  transition: flex 500ms;/*为子元素的弹性伸缩加一个0.5s的过度动画 更加平滑*/
}
.accordion li:nth-child(1) {
  background-color: #f66;
}
.accordion li:nth-child(2) {
  background-color: #66f;
}
.accordion li:nth-child(3) {
  background-color: #f90;
}
.accordion li:nth-child(4) {
  background-color: #09f;
}
.accordion li:nth-child(5) {
  background-color: #9c3;
}
.accordion li:nth-child(6) {
  background-color: #3c9;
}
.accordion li:hover {
  flex: 2;
}

数组的扁平化

假设有一个数组 arr 为 [1, [2, [3, 4] ] ],要怎么去扁平化得到 [ 1, 2, 3, 4 ]呢?

递归

第一眼望去,用递归解决是一个不错的解决方案;

// 展平 递归 (大问题拆分成多个类似的子问题)
const arr = [1,[2,[3,4]]]

function flatten(arr) {
    let result = []
    // len 缓存了值,arr是对象, .length 耗时
    for (let i = 0 , len = arr.length ; i < len ; i++){
        // 数组,递归
        if (Array.isArray(arr[i])){
            result = result.concat(flatten(arr[i])); // concat() 方法用于连接两个或更多的数组,并返回一个新的数组,原数组不会被改变
        }else{
            result.push(arr[i])
        }
        // 否则 加入数组
    }
    return result;
}
console.log(flatten(arr));

每次递归都去判断数组中的每个值是否为一个新数组,递归直至数组中再无嵌套数组。

toString

除了递归,我们还可以用数组身上的 toString 方法去把数组转化为字符串,完成扁平化处理。

// 数组对象 toString 一下,1,2,3,4 扁平化的希望了
// console.log([1,[2,[3,[4,[5,6]]]]].toString(),typeof [1,[2,[3,4]]].toString());// 类型转换,变成了字符串
var arr = [1,[2,[3,[4,[5,6]]]]]

// 扁平化
function flatten(arr) {
    var strArr = arr.toString();// ,隔开的字符串
    console.log(strArr);
    
    var numArr = strArr.split(',');
    var result = []
    for (var i = 0,len = numArr.length; i < len; i++){
        // result.push(parseInt(numArr[i]))
        // numArr[i]字符串,js 可以强制类型转换 (Number() 显示类型转换)
        // result.push(Number(numArr[i]))
        // + 和 Number() 是一样的,+,一是拼接字符串,二是数值计算
        // 三是类型转换 隐式类型转换
        result.push(+numArr[i])
    }
    return result;
    console.log(result);
}
console.log(flatten(arr));

Array.prototype.toString() 方法会将数组中的每个元素调用其各自的 toString() 方法,如果该元素是一个数组,它就会调用那个数组的 toString() 方法,这样就隐式地处理了嵌套结构

map 优化

对于上面的代码,我们还可以用 map 方法进一步去优化;

var arr = [1,[2,[3,[4,[5,6]]]]]

function flatten(arr) {
    // map;对集合(如数组)中的每个元素应用一个指定的函数,并返回一个新的集合,新集合中的元素是原集合元素经过该函数处理后的结果
    return arr.toString().split(',').map(function(item){
        // console.log(item);
        return +item
    })
}
console.log(flatten(arr));

Event Loop

JavaScript 是一种单线程语言,意味着它一次只能执行一个任务。为了确保即使在复杂的交互和任务处理下页面也不会卡顿,JavaScript 引擎采用了 事件循环(Event Loop) 机制,来协调同步和异步代码的执行。

1. JavaScript 引擎的单线程架构

JavaScript 在单线程环境中运行,这意味着它不能同时执行多个任务。所有的代码都会在同一个线程上顺序执行。然而,实际开发中需要处理异步操作(如 I/O 操作、定时器、HTTP 请求等),为了在执行异步任务时不阻塞页面的渲染和响应,JavaScript 引入了事件循环机制

2. 同步代码与异步代码

  • 同步代码:在调用栈中依次执行的代码,任务完成之前不会进行其他操作。比如常见的变量赋值、函数调用等。
  • 异步代码:不会立即执行,而是放入相应的任务队列,等待某些条件满足后再执行。典型的例子包括setTimeoutPromise、I/O 操作等。

3. 执行栈与事件循环

当 JavaScript 程序开始执行时,所有的代码会被放入 调用栈(Execution Stack) 中,调用栈中的代码会逐行执行。当遇到异步任务时,如setTimeoutPromise,它们会被分配到不同的任务队列中,等待执行。

JavaScript 的任务类型分为两大类:宏任务(macro task)微任务(micro task)

4. 宏任务与微任务

  • 宏任务(macro task)

    • 常见的宏任务包括:script(整体代码)、setTimeoutsetIntervalsetImmediate、I/O 操作、UI 渲染等。
    • 宏任务会被存放在一个队列中,等待事件循环依次处理。
  • 微任务(micro task)

    • 微任务通常优先级高于宏任务,根据事件循环的机制,当一个宏任务执行完毕后,JavaScript 引擎会立即检查并执行微任务队列中的所有任务,然后才开始执行下一个宏任务。
    • 常见的微任务包括:Promise 回调、process.nextTickMutationObserver 等。
    • 微任务存放在独立的微任务队列中,且会在每个宏任务执行完成后立刻执行所有微任务。

5. 事件循环的执行过程

事件循环的核心任务是确保调用栈中的同步代码执行完毕后,再依次处理宏任务和微任务。具体的过程如下:

  1. 初始状态:调用栈为空,栈底为全局作用域。宏任务队列中只有一个script,即整个脚本的同步代码,微任务队列为空。
  2. 执行同步代码:全局上下文被推入调用栈,开始执行同步代码。如果在执行过程中产生了异步任务(如定时器、Promise 等),这些任务会被分别加入宏任务队列或微任务队列。
  3. script 移出宏任务队列:当所有同步代码执行完毕后,script任务从宏任务队列中移除。
  4. 执行微任务:如果在执行宏任务期间产生了微任务,那么事件循环会在当前宏任务完成后立即执行所有微任务,直到微任务队列被清空
  5. 事件循环继续:事件循环会继续执行下一个宏任务,完成之后再次检查微任务队列,依次反复,直到所有任务都完成。
  6. 渲染操作:在每次事件循环的结尾,JavaScript 引擎会执行页面的渲染操作,更新 UI 界面。

小结

在 JavaScript 的事件循环(Event Loop)中,页面的重新渲染(UI 更新)通常发生在 宏任务和微任务执行完之后,但在下一个宏任务开始之前

  1. 执行宏任务:浏览器首先执行当前的宏任务(例如脚本、定时器、I/O 操作等)。
  2. 执行微任务:当宏任务完成后,浏览器会检查并执行微任务队列中的任务(如 Promise)。
  3. 页面渲染:在所有宏任务和微任务执行完成后,如果有 DOM 变化或需要更新的部分,浏览器会在此时进行页面的渲染。
  4. 进入下一个宏任务:渲染完毕后,浏览器开始执行下一个宏任务。

因此,页面的渲染操作通常发生在宏任务和微任务全部执行完成之后,并且在下一个宏任务开始之前。如果微任务持续添加,渲染可能会被延迟,从而导致页面卡顿或更新不及时。