中高级前端大厂面试秘籍,为你保驾护航金三银四,直通大厂(上)

CSS

层叠上下文

彻底搞懂CSS层叠上下文、层叠等级、层叠顺序、z-index

深入理解CSS中的层叠上下文和层叠顺序

比较元素处于上下的方法:

  • 先看要比较的两个元素是否处于同一个层叠上下文中:
    • 如果是,层叠等级大的在上面(判断层叠等级大小——看“层叠顺序”图)
    • 如果不是,先比较它们所处的层叠上下文的层叠等级
  • 当两个元素层叠等级相同、层叠顺序相同时,则后来居上

如果仅仅看这个套路,有些地方可能还是搞不清楚,稍微补充一下:

  • 关于display: flex:父元素设置该属性后,其子元素会变为层叠上下文元素

    参考张鑫旭的博客,父元素有两个孩子(设置了background的div、img),在父元素设置该属性之前,div属于普通块元素,它的层叠顺序高于z-index为负值的元素;设置该属性之后,div变为层叠上下文元素,因为它同时满足”层叠上下文元素”和”background/border”两个条件,层叠顺序降为最低一级

  • position: relative/absolute自身并不创建层叠上下文,只有其z-index属性为数值,不为auto的时候,才会创建层叠上下文

  • z-index只作用于层叠上下文元素,不只是设置了定位的元素,例子如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <style>
    #box {
    display: flex;
    }
    .child1 {
    width: 200px;
    height: 100px;
    background-color: #bfa;
    position: absolute;
    z-index: 2;
    }
    .child2 {
    width: 100px;
    height: 200px;
    background-color: orange;
    z-index: 3;
    /* position: absolute; */
    }
    </style>
    </head>
    <body>
    <div id="box">
    <div class="child1">
    child 1
    </div>
    <div class="child2">
    child 2
    </div>
    </div>
    </body>
    </html>

    效果是.child2挡住.child1。如果.child2不设置z-index,则它会被.child1遮挡,而.child2未开启定位,说明z-index作用的条件不是开启定位,而是层叠上下文元素

JavaScript

执行上下文

理解 JavaScript 中的执行上下文和执行栈

以下是阅读博客后整理的思维导图:

mixin

mixin是代码复用的一种方式 (Mixin 模式

mixin 提供了实现特定行为的方法,但是我们不单独使用它,而是使用它来将这些行为添加到其他类中

如何使用mixin:

  • 创建一个普通对象mixin,该对象内封装一些实现特定行为的方法
  • 有一些类,需要使用mixin对象中的方法,则使用Object.assign()方法,将前者(一般使用原型)合并到后者,这样无需继承,就可以在这些类中使用mixin对象的方法

继承

es5

根据我以前的博客,整理出一篇思维导图:

贴张图以便更好地理解寄生组合继承:

es6 class

【图解】ES6 Class类继承原理及其与ES5继承方式的区别

[ES6]ES6语法中的class、extends与super的原理

类继承

这部分并没有完全搞懂,比如不清楚_possibleConstructorReturn为什么可以判断没有调用super(),以及super()如何调用父类的构造函数…以后看看Bable编译的ES5代码,再好好理解一番

class 类的实现

  • 添加_classCallCheck方法,该方法要求构造函数必须以new的方式调用
  • 将class转换为一个函数,在函数内执行_classCallCheck方法,将class内部的变量函数赋值给this,最后执行 constructor 内部的逻辑

继承的实现

  • 自动开启严格模式

  • _inherits函数的实现:本质上是寄生组合继承(使得子类原型的__proto__指向父类原型)

    另外添加了类型检验与设置子类构造函数的__proto__指向父类构造函数(第二条继承链)

    两条继承链的好处:继承对于常规的和静态的方法都生效

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    function _inherits(subClass, superClass) {
    // (1) 校验父构造函数
    if (typeof superClass !== 'function' && superClass !== null) {
    throw new TypeError(
    'Super expression must either be null or a function, not ' +
    typeof superClass
    )
    }
    // (2) 利用寄生继承的方法继承父类原型
    subClass.prototype = Object.create(superClass && superClass.prototype, {
    // 创建一个对象,设置其constructor属性,作为子类的原型对象
    // 即:将原型的constructor属性指向构造函数
    constructor: {
    value: subClass, // 为构造函数赋值
    enumerable: false, // 规定方法不可通过 for...in 枚举
    writable: true,
    configurable: true,
    },
    })
    // (3) 将子构造函数的_proto_指向父构造函数
    if (superClass)
    Object.setPrototypeOf
    ? Object.setPrototypeOf(subClass, superClass)
    : (subClass.__proto__ = superClass)
    }
  • _possibleConstructorReturn函数的实现:

    self是子类实例对象,call是子类的原型对象(由寄生组合继承图示,可以认为它是F的实例对象,它同时也是子类的原型对象)

  • 将子类class转换为函数Child,该函数内使用闭包,在其内部再定义一个Child函数并将其return

    执行_inherits方法,实现继承

    在内部函数体中:

    • 总体和上边class类的实现类似
    • 多了一行对 _possibleConstructorReturn 的执行,这样便获取到了子类的原型对象
    • 对子类原型对象设置属性,添加方法,执行 constructor 内部的逻辑

super 与 [[HomeObject]]

这部分内容参考后两篇博客,要点归纳如下:

  • 在子类constructor内部,super()必须在使用this前调用

  • super 解析父类方法,如super.method()调用父类方法:

    • 如果类的方法是以method(){}定义的,而非method: function(){},那么该方法拥有 [[HomeObject]]属性,指向类自身;否则使用super会报错
    • 如果直接使用this,会出现递归调用自身而栈溢出的情况。借由[[HomeObject]]属性,super的功能得以正常实现

Event Loop

Tasks, microtasks, queues and schedules

当 Event Loop 遇上事件冒泡

JavaScript 运行机制详解:再谈Event Loop

JavaScript中的Event Loop(事件循环)机制(推荐)

正确理解 Node.js 的 Event loop(推荐)

nodejs中事件循环机制与面试题详解

浏览器

概述

  • Event Loop 主要分为三部分:事件队列(分为 宏任务队列/微任务队列)、执行栈

    事件队列:遇到异步事件不会等待它返回结果,而是将这个事件挂起,继续执行执行栈中的其他任务。当异步事件返回结果,将它放到事件队列中。被放入事件队列不会立刻执行起回调,而是等待当前执行栈中所有任务都执行完毕,主线程处于空闲状态时,会去查找事件队列中是否有任务,如果有,则取出排在第一位的事件,并把这个事件对应的回调放到执行栈中,然后执行其中的同步代码

  • 执行顺序:

    在当前执行栈为空时,主线程会查看微任务队列是否有事件存在:

    • 存在,从队列取出微任务回调并加入执行栈,直到微任务队列为空,然后从队列取出宏任务回调并加入执行栈
    • 不存在,则直接找宏任务队列

    当前执行栈(同步代码)执行完毕后时会立刻处理所有微任务队列中的事件,然后再去宏任务队列中取出一个事件。同一次事件循环中,微任务永远在宏任务之前执行

    执行顺序总结:执行宏任务,然后执行该宏任务产生的微任务,若微任务在执行过程中产生了新的微任务,则继续执行微任务,微任务执行完毕后,再回到宏任务中进行下一轮循环

    备注:Run script,即执行同步代码(或称作代码块),也属于宏任务

  • 微任务队列中,监视某元素的 observer 只能存在一个,不能有两个observer同时监视一个元素,因此后边的 observer 不会被加入队列

事件冒泡

点击触发事件与.click触发事件不同:

  • 前者:将Dispatch click任务加入宏任务队列,执行栈在执行完一次Onclick后即为空,然后执行微任务,接着再执行宏任务Dispatch click,再来一次Onclick

  • 后者:以Run script的形式执行inner.onclick(),先执行一次Onclick,执行完后进行冒泡,再执行一次Onclick,此时执行栈才为空,然后执行微任务和宏任务

  • 原因:前者的冒泡异步,后者的冒泡同步:

    Previously, this meant that microtasks ran between listener callbacks, but .click() causes the event to dispatch synchronously, so the script that calls .click() is still in the stack between callbacks. The above rule ensures microtasks don’t interrupt JavaScript that’s mid-execution. This means we don’t process the microtask queue between listener callbacks, they’re processed after both listeners.

    由此可见,微任务不会中断正在执行的JavaScript

    也可以这样理解:

    在一般情况下,微任务的优先级更高,优先于事件冒泡,但手动 .click() 会使得在 script 代码块还没弹出执行栈的时候,冒泡事件执行,从而推迟了微任务

node

概述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
┌───────────────────────┐
┌─>│ timers │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ I/O callbacks │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<──connections─── │
│ └──────────┬────────────┘ │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
│ │ check │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└───────────────────────┘

Event loop 包含一系列阶段 ,每个阶段都是只执行属于自己的的任务和微任务 ,这些阶段依次为:

  • timers:执行 setTimeout()setInterval() 回调函数
  • I/O callbacks :执行延迟到下一个循环迭代的 I/O 回调
  • idle, prepare:仅系统内部使用
  • poll(轮询):基本上涵盖了剩下的所有的情况,大部分回调,如果不是上面两种(并且除了微任务),基本上就是在 poll 阶段执行的
  • check:执行setImmediate() 回调函数
  • close callbacks:一些关闭的回调函数,如:socket.on('close', ...)
1
2
宏任务:script(代码块)、setImmediate、setTimeout、setInterval、I/O 操作
微任务:process.nextTick、Promise

分析方法

  • 判断起点阶段,如果被异步操作包裹,则从poll开始分析;否则从timers开始分析
  • 执行同步代码,将定时器、nextTick、promise等回调函数加入相应事件队列
  • 从起点阶段开始往后推,比如poll => check,则此时执行setImmediate的回调
  • 如果本轮事件循环结束,则清空微任务队列(先清空nextTick,再清空promise),然后从下一次循环的timers阶段开始

setTimeout 和 setImmediate

  • 如果两者都在主模块中调用,那么执行先后取决于进程性能(随机)

  • 如果两者都不在主模块调用(被一个异步操作包裹),那么setImmediate的回调永远先执行

    原因:如果被异步操作包裹,则起点阶段是poll,poll先到check,然后才到下一个循环的timers

自检

浏览器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 1
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0)
async1();
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
console.log('script end');

// 2
console.log('start');
setTimeout(() => {
console.log('children2');
Promise.resolve().then(() => {
console.log('children3');
})
}, 0);

new Promise(function(resolve, reject) {
console.log('children4');
setTimeout(function() {
console.log('children5');
resolve('children6')
}, 0)
}).then((res) => {
console.log('children7');
setTimeout(() => {
console.log(res);
}, 0)
})

结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 1
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
// 2
start
children4
children2
children3
children5
children7
children6

nodejs:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
const hostname = '127.0.0.1';
const port = 3000;
const server = http.createServer((req, res) => {
testEventLoop()
});
server.listen(port, hostname, () => {
console.log(`Server running at http://${hostname}:${port}/`);
});

function testEventLoop() {
console.log('=============')

// Timer
setTimeout(() => {
console.log('Timer phase')
process.nextTick(() => {
console.log('Timer phase - nextTick')
})
Promise.resolve().then(() => {
console.log('Timer phase - promise')
})
});

// Check
setImmediate(() => {
console.log('Check phase')
process.nextTick(() => {
console.log('Check phase - nextTick')
})
Promise.resolve().then(() => {
console.log('Check phase - promise')
})
})

// Poll
console.log('Poll phase');
process.nextTick(() => {
console.log('Poll phase - nextTick')
})
Promise.resolve().then(() => {
console.log('Poll phase - promise')
})
}

结果:

1
2
3
4
5
6
7
8
9
10
=============
Poll phase
Poll phase - nextTick
Poll phase - promise
Check phase
Check phase - nextTick
Check phase - promise
Timer phase
Timer phase - nextTick
Timer phase - promise