20张图助力前端面试

223 阅读19分钟

导读

今年的“金三银四”蒙上了一层厚厚的灰色。虽然大环境确实不好,面试也变得越来越卷,背的八股文突然也不够用了,我一直觉得光去背这些,等到面试的时候,稍微修改下问的方式,就还是无法应对,显得有些捉襟见肘。

为此,把那些八股文改成图片去记忆,效果会更好一些,所以整理了一些高频面试的知识点的导图形式,助力今年不那么好的“铜三铁四”。

更全面的知识体系,可以点击查看这里

画图工具

HTML

semantic.png

语义化的问题更多是考察对一些新的 HTML5 标签的运用,了解一些常见标签的用法,知道标签是用于什么语义情况,为什么使用语义化的标签,有什么好处等等问题。可以经常看到一些招聘需求上面写的“符合W3C规范的语义化的页面”这种要求。

上图列举了一些常用的语义化标签,也是参考了 MDN 文档中的语义化的解释,了解这些标签,不止说只有面试中用到,平时开发也可以用到这些,更有利于我们的开发

CSS

css_performance.png

CSS 中经常遇到的问题就是性能优化,也是整个前端的着重点之一,谈到性能优化,无非就是分为加载时运行时的优化。上图很多优化,大部分都是日常开发中都已经做过了,只是可能是框架层面已经帮你做过了,或者写 CSS 的时候,你已经有了这么一个概念了。做到了上面这些优化,CSS 差不多已经优化了 80% 了,所以平时的开发中不断总结是非常重要的。

JavaScript

关于 HTML 和 CSS,更多还是记忆层面的东西,只要你用过,都会留有印象。而在 JS 部分,更多则是看个人的理解,拓展以及深度了。当然对于面试来说,重点还是在于基础,如果你能说出更深层次的东西,那自然是一个加分项,加分项就是拉开差距的重要条件。

原型&原型链

proto.png

首先原型已经是被问烂的一个问题,但是如何回答才能变成一个加分项呢?

先解释下原型的概念,原型是什么。原型相关的一些属性,prototype、constructor以及非标准属性__proto__。有非标准获取原型的属性,肯定也有标准获取原型的属性,Object.getPrototypeOf 获取原型,Object.setPrototypeOf 设置原型。

原型怎么使用。原型用于如何查找属性,当前对象上如果没有该属性,则在其原型上查找,引出原型链的概念。一般一个对象的原型链的顶层就是 Object.prototype,再往上则是 null 了。利用这个特性,可以为构造函数生成的多个对象设置共享属性,共享其实就体现了面向对象中的继承特性,这也是 ES6 实现 extends 继承的原理之一。

再去拓展一下原型的知识,就上面说到的继承展开说说,JS 中的继承有几种方法,例如原型链继承、构造函数继承、组合继承、寄生继承等等。如果你看过“你不知道的JavaScript”一书,还可以谈谈 JS 中原型的本质,实质上就是委托,所有的对象都是由委托来生成属性的。类比原型的这种委托机制,可以想到相似的一个概念——作用域以及作用域链

作用域&作用域链

JS 的作用域是静态作用域,所谓静态,就是决定作用域的是代码书写的地方,而不是代码生成的地方,一个经典的面试题:

var global = 'global';
function foo() {
    var global = 'local';
    bar();
}
function bar() {
    console.log(global); // global
}

这道题就证明了上面说到的代码书写的地方这个短语的意思。由此可以引申出作用域最直观的解释:

作用域就是代码的作用范围

代码的作用范围就是代码定义的地方,那么,作用范围的“作用”又该如何解释呢?思考一下为什么会有作用域这个概念,就是为了规定了代码如何查找变量,也就是代码对于变量的访问权限。这就是作用域的作用。在 JS 中作用域分为了全局作用域和函数作用域,这是在letconst出现之前的划分,在这之后,又出现了块级作用域,为什么会出现块级作用域呢,一个新事物的出现,必然是为了弥补旧事物的缺陷。

执行上下文

这又说到 JS 中的代码是如何执行的,如何去保存这些作用域信息的。此时出现了一个概念,叫做执行上下文

excute_context_stack.png

上图就是执行右边的代码以后,生成的执行上下文。先不考虑let声明的变量,从这张图中,可以得到以下几个知识点。

首先就是代码执行时的相关信息是保存在栈结构中,栈底是全局上下文,包括var声明的变量,function定义的函数声明,还有词法环境以及 this。而 var 和 function 定义的变量就构成了变量对象,变量对象中声明的变量会被提升到代码的顶层,也就是第一行的位置,这样导致的结果就是,在定义这些变量之前访问这些对象,也是可以访问到的。这种现象就是变量提升

变量提升的好处就是提前把所有声明的变量放入变量对象中,等到用到的时候,再去变量对象中查找,非常简单明了,也易于实现。但同样,变量提升也存在问题:就是不符合人类的线形思维,对于初学者来说,很难理解为什么变量还没有定义就能访问了呢。另外则是容易出错,变量提升对于 var 来说,会赋为一个初值 undefined,这就会导致一些意想不到的问题出现。基于这些,才有了块级作用域的出现。

块级作用域更好理解,大括号之间的就是块级作用域,使用let或者const就会绑定词法环境,无法在声明之前使用变量:

{
    console.log(name);
    let name = 'Jack';
}

这种情况下就会报错,也就是临时性死区(TDZ)

当代码执行时访问一个变量,如果当前作用域中不存在该变量,就会去其父级作用域中查找,父级作用域中也不存在,则去父级作用域的父级去查找,以此类推,直到全局作用域。这种查找可以理解为一种链式的查找,也就是上图中的scope chain的指向构成了作用域链。

至此,上图中上下文环境中的三个就都已经说到了,就剩下一个 this。this 的指向你可能很清楚,但是为什么会出现 this,就很少有人能说清楚了。上面说了这么多,其实都在说一个概念,执行上下文,可以理解为代码的执行环境,那如何拿到当前执行环境中的变量呢?直接拿对应的变量名?如果是在全局中变量 OR 嵌套函数中的变量呢?

所以就出现了 this 的概念,用来指代这个上下文,这跟传统的静态语言中的 this 是完全不一样的。所以我们经常看到某些文章中提到 this 的指向的时候,会说“this 的指向根据其执行上下文来决定的”。OK,现在好像就豁然开朗了,不是吗。

内存模型&闭包

关于上图其实就说的差不多了,但是提到了作用域,又不得不提 JS 中著名的闭包

闭包就是指那些能够访问自由变量的函数,自由变量就是那些既不是函数参数也不是函数局部变量的变量。

这里强烈推荐冴羽大佬的博客,值得一看。

所以这么一看,闭包就是函数,而闭包形成的原因就是作用域链,或者也能理解闭包就是一种特殊的上下文环境。这里我们结合下面的内存模型图分析一下。

memory_model.png

我们都知道 JS 代码执行时,会把基础类型的变量存放在栈空间,而引用类型的变量存放在堆空间。上一小节提到的执行上下文栈也存放在栈空间。为什么要这么区分呢?首先从变量本身占据的内存大小来说,引用类型通常占用内存大,而基础类型占用的内存小。再从空间的用途来说,栈用来存放代码执行时的环境,当一个函数执行完毕以后,需要把当前函数出栈,如何表示当前函数出栈了呢?JS 使用了一个 ESP 的指针,来表示当前正在执行的上下文,执行完毕以后,指针下移,就完成了上下文的切换。如果换成堆结构,指针的移动则太过复杂,反而得不偿失。

回到闭包上来,看上图左边的代码。foo 返回了一个 bar 函数,执行 bar 函数,可以把这段代码放到 vscode 中打个断点一步一步调试一下,你会发现,当进入 bar 函数的时候,其上下文中已经包含了一个闭包对象closure(foo),此时 foo 已经执行完毕,其上下文已经被销毁了,但是在 bar 中还是可以访问到 foo 中的 a 变量。这种情况就好像,当 JS 引擎扫描到 foo 时,就已在堆内存中开辟了一块空间,复制了 foo 的上下文,并把这块空间关联到 bar 上,属于 bar 的一个专属的背包

JavaScript 编译原理

当然实际 JS 引擎是否真的复制了 foo 的上下文,什么时候复制的,还有待去研究。具体的引擎实现细节我们很难去深入,但是关于 JS 引擎是如何执行一段代码的,却是我们需要去了解的。

js_excute_process.png

上图展示了 JS 从源码到被执行的过程。学过编译原理的同学应该都知道,JS 的编译流程也差不多,从源代码进行词法分析语法分析,这个阶段会创建执行上下文,并且生成 AST,也就是抽象语法树。关键点来了,JS 的解释器会把 AST 转为字节码,众所周知,字节码的执行效率是比较低的,所以又提出了JIT即时编译的技术,其实就是把字节码中经常用到的代码,称为热点代码编译成机器码,也就是01的机器码。这么做其实就是解释与编译相结合,JS 也不再是纯解释执行的代码了,执行效率相比以前也更高了,类似 Java 等语言都是用了 JIT 技术。

那么 JS 代码执行的时候,同步代码都是按顺序一步一步执行,遇到异步的代码,诸如 setTimeoutPromise这样的代码,又按照什么顺序执行呢?其实这就是事件循环所做的事。

事件循环

事件循环分为浏览器环境和 Nodejs 环境,浏览器环境大部分前端接触的可能比较多一点,而 Nodejs 环境则相对较少,关于下面两张图就不再赘述,感兴趣可以去看看这篇文章

event_loop_browser.png

event_loop_nodejs.png

无论是在浏览器环境,还是在 Nodejs 中,JS 还是一个单线程的语言,这就意味着 JS 的异步并不是开启了一个新线程,当然这里没有涉及 worker 线程的概念,worker 线程的使用还是有很多限制,所以没有办法作为一个真正的线程来使用。JS 中的异步可以理解为消息队列的机制,代码执行时遇到异步任务,把其添加到队列中,等到合适的时机,再从消息队列中取出队首的任务执行。这种消息队列就是上图的任务队列的概念。

那任务队列为什么又分为宏任务微任务呢?宏任务就是上图列出的一些 API,用来表示未来某个时刻会执行的任务,微任务队列会在当前的宏任务之前执行,为什么会有这么一个机制呢?其实就是优先级的原因,需要优先于宏任务之前执行任务的机制。

JavaScript 垃圾回收机制

至此,整个 JS 从上下文,到执行机制,到编译原理都有了大概的理解。那么 JS 的代码执行完成后,如何去回收那些已经用不到的对象呢,这在 JS 中是由引擎自动为我们处理的,但是我们也还是需要去理解 JS 的垃圾回收机制的。

gc.png

从内存模型那张图,我们可以知道 JS 的代码产生的变量被存储在栈空间或者堆空间,垃圾回收的对象主要也就是这两者。对于栈空间,上文已经说过,就是通过 ESP 的指针,指向当前执行代码,而栈顶的上下文则会被系统给清除。

堆空间的垃圾回收则如上图所示,分为老生代和新生代的区域,老生代空间用来存放那些占据内存大,使用时间久的对象,新生代则存放那些使用时间短,占据内存小的对象。大部分的垃圾回收都发生在新生代区域。

新生代区域分为了两个部分,对象区域和空间区域,对象区域存储的对象占满空间时,会与空闲区域进行反转,对象区域中还存活的对象会被复制到空闲区域中,然后清空整个对象区域,这样对象区域就变成了空闲区域,而空闲区域则会变成对象区域,进行下一轮的垃圾回收。对象区域中存活的对象,经历过一次垃圾回收以后,会发生对象晋升,即把该对象放入老生代区域中。这种垃圾回收的机制就是Scavenge算法

老生代区域采用了耳熟能详的标记清除标记整理算法。由于垃圾回收会占用线程执行,所以一旦执行垃圾回收,JS 线程便会阻塞,这就是全停顿。为了应对这种情况,引擎采用了增量标记的方法,把垃圾回收任务拆分成一个个小任务,插入到 JS 的执行线程中,这样就不会产生长时间的阻塞卡顿,其实和 React 的时间切片有异曲同工之妙。

浏览器

浏览器部分是串联整个前端知识体系的重要部分。

broswer_process.png

如上图,浏览器更多是掌握其渲染原理,也是面试会被频繁问到的知识点。由于每个部分深入又是一个很大的点,所以不再赘述,只是对照这张图,罗列一下浏览器涉及到的知识点:

  1. DNS协议
  2. 浏览器缓存
  3. 网络协议
  4. HTML渲染

工程化

工程化这块,更多其实就在平时的工作中的积累。工程化就是一整套的高效的开发流程,任何提升我们开发效率的工具、框架等都是工程化的一部分。

工程化就是整个开发流程的高效运作

engineering.png

上图是整个前端开发过程中,都会用到的一些工具框架等,都是与我们息息相关。其中贯穿整个前端开发流程的工具就是 Babel、Webpack,这也是前端开发中必须要掌握的知识点,工程化的主要面试点就在于此。

Babel

babel.png

图片来源

如上所示是一个 Babel 的工作原理图,Babel 非常强大的地方就在于它的插件系统,Babel 本身,也就是 babel-core 是 Babel 的核心包,主要提供 AST、转换 AST 节点以及生成新的 AST 的功能,而插件系统则是根据 AST 节点解析规则,生成不同的 JS 语法。

而 Webpack 内部也是使用了 babel-core 的核心包来处理 AST。

Webpack

webpack_process.png 图片来源

Webpack 的运行流程如上图所示,就不再赘述,其中有几个流程中的概念,需要理解一下。

Compiler 对象代表了完整的 webpack 环境配置。这个对象在启动 webpack 时被一次性建立,并配置好所有可操作的设置,包括 options,loader 和 plugin。当在 webpack 环境中应用一个插件时,插件将收到此 compiler 对象的引用。可以使用它来访问 webpack 的主环境。

Compilation 对象代表了一次资源版本构建。当运行 webpack 开发环境中间件时,每当检测到一个文件变化,就会创建一个新的 compilation,从而生成一组新的编译资源。一个 compilation 对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。compilation 对象也提供了很多关键时机的回调,以供插件做自定义处理时选择使用。

而插件其实就是在这个运行流程的节点中触发注册的回调函数,类似于发布订阅模式的实现,依赖Tapable这个库来实现。

说完了 Plugin,继续来说说 Loader。

loaders.png 图片来源

很多同学都知道 Loader 是从右至左从下到上的顺序运行的,但很少知道 Loader 的运行顺序取决于 Loader 的类型,Webpack 的文档中告诉我们 Loader 有以下几种类型:

  • pre loader
  • post loader
  • inline loader
  • normal loader
  • pitch loader

具体每种 Loader 如何使用,可以去查看官方文档。Loader 的运行分为了两个阶段,一个是 Pitching 阶段,一个是 Normal 阶段。Pitching 阶段,Loader 的运行顺序是 post、inline、normal、pre。而 Normal 阶段则是按照 pre、normal、inline、post 的顺序运行。

注册 Pitching 阶段的 Loader 方法和普通 Loader 是一样的,都是作为一个普通的方法暴露出去,但是 Pitching Loader 是作为 Normal Loader 的 pitch 属性,如下所示。

// normal loader

module.exports = function loader(source) {
    return source;
};

// pitching loader
module.exports.pitch = function loader(source) {
    console.log('Pitching Loader');
    return;
}

Pitch Loader 的执行顺序如上图所示,和 Normal Loader 的执行顺序是相反的。并且当 Pitch Loader 存在返回值的时候,会发生熔断,即会中断后续所有的 Normal Loader 和 Pitch Loader,返回执行其前一个 Normal Loader。

最后说说 Webpack 的 HMR 的机制,也是面试中经常会被问到的。

hmr.png

上图展示了 HMR 的运行流程 abcd。

  1. 使用 webpack-dev-server 托管静态资源,往浏览器注入 HMR Runtime 代码
  2. 浏览器加载页面,与 webpack-dev-server 建立 websocket 连接
  3. webpack 监听到文件变化后,重新编译更新的模块,通过 websocket 发送 hash 事件
  4. 浏览器收到 hash 事件后,请求 manifest 资源文件,确认更新范围
  5. 浏览器加载发生更新的模块
  6. webpack 运行时触发更新模块的 module.hot.accept 的回调方法

框架 React 为例

框架方面,因为我是 React 为主,所以这里就谈到了 React 框架的一些问题,首先就是关于 React 的 diff 算法的实现。大家可以参考卡颂大佬的React技术揭秘,这里我是画了几张图,试图把 React 的 diff 过程展示出来。

Diff 算法

diff 主要分为两类,一类就是单节点 diff,一类就是多节点的 diff。另外就是 diff 中一个非常重要的属性Key,主要是用来暗示哪些元素在更新前后没有发生变化,可以复用,所以 key 属性用的好,确实可以提升 diff 的效率,否则就会起到反作用。

react_single_diff.png

react_diff_multi.png

Hooks 的实现

hooks.png

我们知道 class 组件需要实例化,而组件的 state 和 props 都存储在组件的实例上了。但是函数式组件没有实例,执行完以后就被销毁了,所以 hooks 应该如何存储呢。

如果看过一些 React 的源码分析就会知道 Fiber 的概念,可以简单理解为虚拟DOM,hook 其实就是挂载在了 Fiber 节点上了。hook 本身是一个对象,这个对象上有一个 next 指针,指向了下一个 hook,形成一个单向链表结构。其次 hook 中还包含了一个更新队列,里面存储了当前的状态,以及更新以后的下一个状态,它们被链接起来形成一个单向循环链表结构。

性能优化

performance.png

关于前端的性能优化,内容其实非常多,所以说自己熟悉的一些优化就可以了。优化和上面的 CSS 的优化是类似的,分为运行时的优化和加载时的优化,每一个点都需要你切实的去做过,或者深入研究过,才能说的透彻,否则反而变成了暴露了自己的劣势。

排序算法

sort.png

算法这块更多的是考察解决实际题目的能力,但有时也会问到一些基础排序算法的复杂度,是否稳定等等。这样的问题,就需要你对排序算法了然于心,可以借助上面这张图,比较一下各个排序算法之间的区别,从而更好的理解排序算法。

结语

聪明的你应该看出来了,每个模块,我尽量是从一个点切入,然后去关联到其他知识点上去,通俗来说,就是非常能“吹牛X”吗,但是内里其实就是前端的整个知识体系在起作用,它最直观的作用就是能让你面对面试官的时候,不至于冷场或者不知道说什么,这样的问题。但是如果你说,面试官打断了你,不想听你说这么多“没用”的,别怀疑,那就是面试官格局小了(狗头保命)。

最后,说了这么多,都是自己近些年的一些个人总结,可能说的不好,可能有错误,或者还是不完善,很多遗漏的地方,那么就欢迎你们来补充啦。