面经

动画效果

left/top/margin 之类的属性会影响到元素在文档中的布局,当对布局(layout)进行动画时,该元素的布局改变可能会影响到其他元素在文档中的位置,就导致了所有被影响到的元素都要进行重新布局,浏览器需要为整个层进行重绘并重新上传到 GPU,造成了极大的性能开销。

transform 属于合成属性(composite property),对合成属性进行 transition/animation 动画将会创建一个合成层(composite layer),这使得被动画元素在一个独立的层中进行动画。通常情况下,浏览器会将一个层的内容先绘制进一个位图中,然后再作为纹理(texture)上 传到 GPU,只要该层的内容不发生改变,就没必要进行重绘(repaint),浏览器会通过重新复合(recomposite)来形成一个新的帧

原型

要想让构造函数生成的所有实例对象都能够共享属性,那么我们就给构造函数加一个属性叫做 prototype,用来指向原型对象,我们把所有实例对象共享的属性和方法都放在这个构造函数的 prototype 属性指向的原型对象中,不需要共享的属性和方法放在构造函数中

js 错误类型 参考

  1. SyntaxError 语法错误
  2. ReferenceError 不存在
  3. TypeError 类型错误 比如 const a = 1; a()
  4. RangeError 范围错误(参数超范围) [].length = -5
  5. EvalError 非法调用 eval
  6. URIError url 不合法

从输入 URL 到页面加载的过程 参考

  1. 从浏览器接收 url 到开启网络请求线程(这一部分可以展开浏览器的机制以及进程与线程之间的关系)

    GUI 线程 JS 引擎线程 事件触发线程 定时器线程 网络请求线程

  2. 开启网络线程到发出一个完整的 http 请求(这一部分涉及到 dns 查询,tcp/ip 请求,五层因特网协议栈等知识)

    dns 查询 如果浏览器有缓存,直接使用浏览器缓存,否则使用本机缓存,再没有的话就是用 host 如果本地没有,就向 dns 域名服务器查询(当然,中间可能还会经过路由,也有缓存等),查询到对应的 IP dns 解析是很耗时的,因此如果解析域名过多,会让首屏加载变得过慢,可以考虑dns-prefetch优化

     1.应用层(dns,http) DNS解析成IP并发送http请求c
     2.传输层(tcp,udp) 建立tcp连接(三次握手)
     3.网络层(IP,ARP) IP寻址
     4.数据链路层(PPP) 封装成帧
     5.物理层(利用物理介质传输比特流) 物理传输(然后传输的时候通过双绞线,电磁波等各种介质)
    
  3. 从服务器接收到请求到对应后台接收到请求(这一部分可能涉及到负载均衡,安全拦截以及后台内部的处理等等) 跨域

  4. 后台和前台的 http 交互(这一部分包括 http 头部、响应码、报文结构、cookie 等知识,可以提下静态资源的 cookie 优化, 以及编码解码,如 gzip 压缩等)

  5. 单独拎出来的缓存问题,http 的缓存(这部分包括 http 缓存头部,etag,catch-control 等) 参考

  6. 浏览器接收到 http 数据包后的解析流程(解析 html-词法分析然后解析成 dom 树、解析 css 生成 css 规则树、合并成 render 树, 然后 layout、painting 渲染、复合图层的合成、GPU 绘制、外链资源的处理、loaded 和 domcontentloaded 等)

  7. CSS 的可视化格式模型(元素的渲染规则,如包含块,控制框,BFC,IFC 等概念)

  8. JS 引擎解析过程(JS 的解释阶段,预处理阶段,执行阶段生成执行上下文,VO,作用域链、回收机制等等)

  9. 其它(可以拓展不同的知识模块,如跨域,web 安全,hybrid 模式等等内容)

根据 DNS 优化首屏加载速度 -- DNS 预获取 dns-prefetch 提升页面载入速度 https://blog.csdn.net/langyu1021/article/details/78923009

1. HTTP 中的状态码

http2.0

http2.0 不是 https,它相当于是 http 的下一代规范(譬如 https 的请求可以是 http2.0 规范的) 然后简述下 http2.0 与 http1.1 的显著不同点:

http1.1中,每请求一个资源,都是需要开启一个tcp/ip连接的,所以对应的结果是,每一个资源对应一个tcp/ip请求,由于tcp/ip本身有并发数限制,所以当资源一多,速度就显著慢下来
http2.0中,一个tcp/ip请求可以请求多个资源,也就是说,只要一次tcp/ip请求,就可以请求若干个资源,分割成更小的帧请求,速度明显提升。

所以,如果 http2.0 全面应用,很多 http1.1 中的优化方案就无需用到了(譬如打包成精灵图,静态资源多域名拆分等) 然后简述下 http2.0 的一些特性:

多路复用(即一个tcp/ip连接可以请求多个资源)
首部压缩(http头部压缩,减少体积)
二进制分帧(在应用层跟传送层之间增加了一个二进制分帧层,改进传输性能,实现低延迟和高吞吐量)
服务器端推送(服务端可以对客户端的一个请求发出多个响应,可以主动通知客户端)
请求优先级(如果流被赋予了优先级,它就会基于这个优先级来处理,由服务器决定需要多少资源来处理该请求。)

什么是 HttpOnly?

如果cookie中设置了HttpOnly属性,那么通过js脚本将无法读取到cookie信息,这样能有效的防止XSS攻击,窃取cookie内容,这样就增加了cookie的安全性,即便是这样,也不要将重要信息存入cookie。XSS全称Cross SiteScript,跨站脚本攻击,是Web程序中常见的漏洞,XSS属于被动式且用于客户端的攻击方式,所以容易被忽略其危害性。其原理是攻击者向有XSS漏洞的网站中输入(传入)恶意的HTML代码,当其它用户浏览该网站时,这段HTML代码会自动执行,从而达到攻击的目的。如,盗取用户Cookie、破坏页面结构、重定向到其它网站等。

get 和 post 的区别

get 和 post 虽然本质都是 tcp/ip,但两者除了在 http 层面外,在 tcp/ip 层面也有区别。get 会产生一个 tcp 数据包,post 两个

  1. get 请求时,浏览器会把 headers 和 data 一起发送出去,服务器响应 200
  2. post 请求时,浏览器先发送 headers,服务器响应 100 continue, 浏览器再发送 data,服务器响应 200(返回数据)

说一下 http 报文主要包含哪些 参考

  1. 通用头部
  2. 请求/响应头部
  3. 请求/响应体

script defer 属性 参考

defer 是延迟执行,而 async 是异步执行

渲染进程 Renderer 的主要线程

GUI 渲染线程

  1. 负责渲染浏览器界面,解析 HTML,CSS,构建 DOM 树和 RenderObject 树,布局和绘制等

    1. 解析 html 代码(HTML 代码本质是字符串)转化为浏览器认识的节点,生成 DOM 树,也就是 DOM Tree
    2. 解析 css,生成 CSSOM(CSS 规则树)
    3. 把 DOM Tree 和 CSSOM 结合,生成 Rendering Tree(渲染树)
  2. 当我们修改了一些元素的颜色或者背景色,页面就会重绘(Repaint)

  3. 当我们修改元素的尺寸,页面就会回流(Reflow)

  4. 当页面需要 Repaing 和 Reflow 时 GUI 线程执行,绘制页面

  5. 回流(Reflow)比重绘(Repaint)的成本要高,我们要尽量避免 Reflow 和 Repaint

  6. GUI 渲染线程与 JS 引擎线程是互斥的

    1. 当 JS 引擎执行时 GUI 线程会被挂起(相当于被冻结了)
    2. GUI 更新会被保存在一个队列中等到 JS 引擎空闲时立即被执行

事件触发线程

  1. 当 js 执行碰到事件绑定和一些异步操作(如 setTimeOut,也可来自浏览器内核的其他线程,如鼠标点击、AJAX 异步请求等),会走事件触发线程将对应的事件添加到对应的线程中(比如定时器操作,便把定时器事件添加到定时器线程),等异步事件有了结果,便把他们的回调操作添加到事件队列,等待 js 引擎线程空闲时来处理。
  2. 当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待 JS 引擎的处理
  3. 因为 JS 是单线程,所以这些待处理队列中的事件都得排队等待 JS 引擎处理

事件循环 参考

  1. 首先,整体的 script(作为第一个宏任务)开始执行的时候,会把所有代码分为同步任务、异步任务两部分 同步任务会直接进入主线程依次执行 异步任务会再分为宏任务和微任务 宏任务进入到 Event Table 中,并在里面注册回调函数,每当指定的事件完成时,Event Table 会将这个函数移到 Event Queue 中 微任务也会进入到另一个 Event Table 中,并在里面注册回调函数,每当指定的事件完成时,Event Table 会将这个函数移到 Event Queue 中 当主线程内的任务执行完毕,主线程为空时,会检查微任务的 Event Queue,如果有任务,就全部执行,如果没有就执行下一个宏任务 上述过程会不断重复,这就是 Event Loop,比较完整的事件循环

说一下为什么 javascript 是单线程 参考

与它的用途有关。作为浏览器脚本语言,JavaScript 的主要用途是与用户互动,以及操作 DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定 JavaScript 同时有两个线程,一个线程在某个 DOM 节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准

宏任务与微任务 参考 参考

事件循环会不断地处理消息队列出队的任务,而宏任务指的就是入队到消息队列中的任务,每个宏任务都有一个微任务队列,宏任务在执行过程中,如果此时产生微任务,那么会将产生的微任务入队到当前的微任务队列中,在当前宏任务的主要任务完成后,会依次出队并执行微任务队列中的任务,直到当前微任务队列为空才会进行下一个宏任务

说说你对函数式编程的理解?函数柯里化的理解?平时的使用场景? 参考

  1. 对集合统一处理、统一操作,而命令式编程需要取出来每个单词单独处理,单独计数,而函数式只需要传入待处理对象集合、处理规则,我们不需要关注于具体细节
  2. 重点在于“相同的输入,永远会得到相同的输出”
  3. curry 概念 只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。
  4. bind 就是科里化的一个体现

闭包概念,最主要的还是问闭包的场景 参考

一个函数和对其周围状态(词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包

  1. 当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行的

场景

  1. 给对象设置私有变量并且利用特权方法去访问私有属性
  2. 封装相关功能集
  3. 保护变量
(function() {
    var name = '';
    //
    Person = function(value) {
        name = value;
    }
    Person.prototype.getName = function() {
        return name;
    }
    Person.prototype.setName = function(value) {
        name = value;
    }
})()
var person1 = new Person('xiaoming');
console.log(person1.getName()); // xiaoming
person1.setName('xiaohong');
console.log(person1.getName()); // xiaohong

var person2 = new Person('luckyStar');
console.log(person1.getName()); // luckyStar
console.log(person2.getName()); // luckyStar

vuex 参考

Vuex 的双向绑定通过调用 new Vue 实现,然后通过 Vue.mixin 注入到 Vue 组件的生命周期中,再通过劫持 state.get 将数据放入组件中

  1. vuex 的 store 是如何注入到组件中的?

    在调用 vuex 的 install 方法时,装载 vuex

    1. store 注入 vue 的实例组件的方式,是通过 vue 的 mixin 机制,借助 vue 组件的生命周期 钩子 beforeCreate 完成的。即 每个 vue 组件实例化过程中,会在 beforeCreate 钩子前调用 vuexInit 方法。下面,我们将焦点聚焦在 vuexInit 函数。
  2. vuex 的 state 和 getter 是如何映射到 各个组件实例中自动更新的?

    event bus

    1. 中央事件总线的解决方案!其核心设计思想是引入中央通信桥梁——中央事件总线,使各个组件只与其进行通信,达到数据同步的通信目的!如下图 组件 A 数据变更,通知中央事件总线,其他组件监听并接收变更的数据。
    2. vuex 的 state 是借助 vue 的响应式 data 实现的
    3. getter 的实现借助了 vue 的 computed 的特性而实现

nextTick 参考

  1. 异步渲染 dom, 放入任务队列, 把回掉放到最后,我们有可能会在同步任务中多次改变 DOM。那么在所有同步任务执行完毕之后,就说明数据修改已经结束了,改变 DOM 的函数我都执行过了,已经得到了最终要渲染的 DOM 数据,所以这个时候可放心更新 DOM 了。因此 nextTick 的回调函数都是在 microtask 中执行的。这样就可以尽量避免重复的修改渲染某个 DOM 元素,另一方面也能够将 DOM 操作聚集,减少渲染的次数,提升 DOM 渲染效率。等到所有的微任务都被执行完毕之后,就开始进行页面的渲染

tree shaking 的原理

  1. 本质是消除无用的 js 代码。无用代码消除在广泛存在于传统的编程语言编译器中,编译器可以判断出某些代码根本不影响输出,然后消除这些代码,这个称之为 DCE(dead code elimination)

    DCE => 消灭不可能执行的代码 Tree-shaking 关注消除没有用到的代码

    1. 代码不会被执行到
    2. 执行结果没有被用到
    3. 代码只会影响死变量(只写不读)
  2. Tree-shaking 只对 ES Module 起作用,对于 commonjs 无效,对于 umd 亦无效 因为 tree-shaking 是针对静态结构进行分析,只有 import 和 export 是静态的导入和导出。而 commonjs 有动态导入和导出的功能,无法进行静态分析

  3. 是 uglify 完成了 javascript 的 DCE

  4. tree-shaking rollup()阶段,分析源码,生成 ast tree,对 ast tree 上的每个节点进行遍历,判断出是否 include,是的话标记,然后生成 chunks,最后导出。 generate()或者 write()阶段根据 rollup()阶段做的标记,进行代码收集,最后生成真正用到的代码,这就是 tree shaking 的基本原理

es module 和 common.js 的区别

  1. CommonJS 模块输出是值的拷贝,ES6 模块输出是值的引用(引用时可能修改到模块的值)
  2. CommonJS 是运行时加载,ES6 模块是编译时加载 CommonJS 模块规范使用 require 语句导入模块,module.exports 导出模块,输出的是值的拷贝,模块导入的也是输出值的拷贝,也就是说,一旦输出这个值,这个值在模块内部的变化是监听不到的。

ES6 模块的规范是使用 import 语句导入模块,export 语句导出模块,输出的是对值的引用。ES6 模块的运行机制和 CommonJS 不一样,遇到模块加载命令 import 时不去执行这个模块,只会生成一个动态的只读引用,等真的需要用到这个值时,再到模块中取值,也就是说原始值变了,那输入值也会发生变化。

webpack hash 值一共有哪些

  1. hash 如果都使用 hash 的话,所有文件的 hash 都是一样的,而且每次修改任何一个文件,所有文件名的 hash 值都将改变。所以一旦修改了任何一个文件,整个项目的文件缓存都将失效

  2. chunkhash

    使只有被修改了的文件的文件名 hash 值修改

  3. contenthash contenthash 表示由文件内容产生的 hash 值,内容不同产生的 contenthash 值也不一样。在项目中,通常做法是把项目中 css 都抽离出对应的 css 文件来加以引用。 在这里我用 mini-css-extract-plugin 替代了 extract-text-webpack-plugin

splitchunks 是怎么实现 参考

重点: SplitChunksPlugin 的核心在于将每个模块(module)按照规则分配到各个缓存组中,形成一个缓存组的 map 结构 chunksInfoMap,每个缓存组会对应最终分割出来的新代码块。我们对 splitChunks 中的 cacheGroups 进行配置,其实就是控制 chunksInfoMap 中的每个缓存组

  1. compilation 会在生成 chunkGraph(包含代码块依赖关系的图结构)之后,触发 optimizeChunks 事件并传入 chunks,开始代码分割优化过程,所有优化都在 optimizeChunks 事件的回调函数中完成

  2. 准备阶段

    1. chunksInfoMap 存储着代码分割信息,每一项都是一个缓存组,对应于最终要分割出哪些额外代码块,会不断迭代,最终将代码分割结果加入 chunkGraph 中,而 chunkGraph 最终会生成我们见到的打包文件。当然,这些缓存组目前还附带一些额外信息,比如 cacheGroup,就是我们配置的 cacheGroup 代码分割规则,用于后续校验;再比如 sizes,记录了缓存组中模块的总体积,用于之后判断是否符合我们配置的 minSize 条件。
    2. addModuleToChunksInfoMap 就是向 chunksInfoMap 中添加新的代码分割信息,每次添加都会根据 key 值选择是创建新的缓存组还是在已有缓存组中添加模块,并更新缓存组信息。
  3. 模块分组阶段

    1. 准备完成后,遍历所有 module,将符合条件的 module 通过 addModuleToChunksInfoMap 方法存到 chunksInfoMap 中,进行分组,其实就是创建缓存组的过程
    2. 在分组阶段,会将 cacheGroup 的配置全部取出,顺便检查配置中的 minChunks 和 chunks 规则,只有符合条件的分组才会创建。本阶段只检查和数量有关的配置,其他配置在下个阶段进行校验。
  4. 排队检查阶段

    1. 上一阶段生成了缓存组信息 chunksInfoMap,本阶段按照用户的 cacheGroup 配置,一项一项检查 chunksInfoMap 中各个缓存组是否符合规则,去除不符合的,留下符合的加入 compilation 的 chunkGraph 中,直至把全部代码分割结果都更新到 chunkGraph 中。代码比较长,但都是按部就班,先进行规则校验,然后将符合条件的缓存组中的模块打包成新的 chunk
    2. 经过本阶段的筛选,chunksInfoMap 中符合配置规则的缓存组会被全部打包成新代码块,并且加入 compilation 的 chunkGraph 中,完成代码分割的工作,最终生成打包文件。不要害怕大量 if,else 分支,其实都只是按部就班检查各类配置是否满足,排除一些特殊特殊情况

说一下 Webpack 的热更新原理吧

  1. Webpack 的热更新又称热替换(Hot Module Replacement),缩写为 HMR。 这个机制可以做到不用刷新浏览器而将新变更的模块替换掉旧的模块

  2. HMR 的核心就是客户端从服务端拉去更新后的文件,准确的说是 chunk diff (chunk 需要更新的部分),实际上 WDS 与浏览器之间维护了一个 Websocket,当本地资源发生变化时,WDS 会向浏览器推送更新,并带上构建时的 hash,让客户端与上一次资源进行对比。客户端对比出差异后会向 WDS 发起 Ajax 请求来获取更改内容(文件列表、hash),这样客户端就可以再借助这些信息继续向 WDS 发起 jsonp 请求获取该 chunk 的增量更新

  3. 后续的部分(拿到增量更新之后如何处理?哪些状态该保留?哪些又需要更新?)由 HotModulePlugin 来完成,提供了相关 API 以供开发者针对自身场景进行处理,像 react-hot-loader 和 vue-loader 都是借助这些 API 实现 HMR

webpack 打包 参考

可以使用 enforce 强制执行 loader 的作用顺序,pre 代表在所有正常 loader 之前执行,post 是所有 loader 之后执行 Webpack 主要使用 Compiler 和 Compilation 两个类来控制 Webpack 的整个生命周期。他们都继承了 Tapabel 并且通过 Tapabel 来注册了生命周期中的每一个流程需要触发的事件

  1. 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数

  2. 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译

  3. 确定入口:根据配置中的 entry 找出所有的入口文件

  4. 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理

  5. 完成模块编译:在经过第 4 步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系

  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会

  7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统

image.png

babel 参考

  1. Babel 是一个 JavaScript 编译器。他把最新版的 javascript 编译成当下可以执行的版本

过程

  1. 解析步骤接收代码并输出 AST。 这个步骤分为两个阶段:词法分析 和 语法分析 Babel 中的解析器是 babylon

    词法分析阶段把字符串形式的代码转换为 令牌(tokens) 流。你可以把令牌看作是一个扁平的语法片段数组 语法分析阶段会把一个令牌流转换成 AST 的形式。 这个阶段会使用令牌中的信息把它们转换成一个 AST 的表述结构,这样更易于后续的操作 code(字符串形式代码) -> tokens(令牌流) -> AST(抽象语法树)

  2. 转换步骤接收 AST 并对其进行遍历,在此过程中对节点进行添加、更新及移除等操作。 这是 Babel 或是其他编译器中最复杂的过程。 Babel 提供了@babel/traverse(遍历)方法维护这 AST 树的整体状态,并且可完成对其的替换,删除或者增加节点,这个方法的参数为原始 AST 和自定义的转换规则,返回结果为转换后的 AST。

  3. 代码生成步骤把最终(经过一系列转换之后)的 AST 转换成字符串形式的代码,同时还会创建源码映射(source maps)代码生成其实很简单:深度优先遍历整个 AST,然后构建可以表示转换后代码的字符串。Babel 使用 @babel/generator 将修改后的 AST 转换成代码,生成过程可以对是否压缩以及是否删除注释等进行配置,并且支持 sourceMap。

  4. 按需导入 实际上在 转换的过程中 将 import { Button } from 'element' 转换成 import Button from "element-ui/lib/Button" 在生成代码

typeof 实现原理

实现一个检查 对象类型

instanceof 原理

为什么 typeof null === Object

proto 和 getPrototypeOf 区别

Object.prototype.toString.call()

什么是构造函数

构造函数和普通函数的区别

new 实现原理

class 和function 区别 this使用

什么是可迭代对象 和 可便利对象的区别

async await 实现原理

生成器 迭代器 装饰器

nuxt怎么实现ssr

nuxt 优缺点

vue 有什么bug

nuxt router 怎么扩展

asyncData 实现原理

线上发布流程

守护进程是啥

怎么多个项目同时打包

怎么修改ui库的某些组件 (patch) 对ui库打补丁

判断数据类型有哪些

js有哪些内置对象

变量交换

有哪些宏任务 和微任务

es6用到比较多的方法

vue router 实现原理

有哪些plugins loaders有什么用 用过哪些

beforeRouterEnter beforeRouteLeave

nuxt 啥时候会用到create

nuxt怎么实现客户端和服务端数据互通

Performance

mvvm理解

keepalive 原理

computed为啥不能有异步

template里面为啥不用写this

vue-loader干了啥

compaile 怎么编译tempalte倒语法树的

模版编译原理 解析:转成ast 优化:【是否是静态节点做标记】 生成:根据ast转成render函数 输入:视图模板, 输出:渲染函数

map 和object区别 map的应用场景

new vue干了啥 合并配置 初始化生命周期 初始化事件中心 初始化渲染 调用 beforeCreate 钩子函数 init injections and reactivity(这个阶段属性都已注入绑定,而且被 $watch 变成reactivity,但是 $el 还是没有生成,也就是DOM没有生成) 初始化state状态(初始化了data、props、computed、watcher) 调用created钩子函数。

vnode类型 参考

new 创建对象和 字面量创建的区别

object.keys 手动兼容低版本浏览器

怎么判断一个对象的属性是自身的还是原型的 //判断属性是否是存在于自己的实例中,如果是:返回true,如果仅仅存在自己的原型总,则返回false

扁平数组转树优化点

for in 和for of区别

面经的相似文章

凛冬将至什么水平,2022凛冬之时三年经验前端面经分析理解vue中的diff算法,Vue原理解析(八):一起搞明白令人头疼的diff算法分析Vue 的生命周期之间到底做了什么事清?(源码详解,带你从头梳理组件化流程)分析金三银四,我先面为敬了(腾讯、美团、商汤科技等七家大厂面试有感)分析浏览器的渲染原理及优化方式,浏览器层合成与页面渲染优化分析 连八股文都不懂还指望在前端混下去么分析【7k长文,一次到位】前端八股文再来一遍🧐(图解 + 总结)分析2021年前端各大公司都考了那些手写题(附带代码)分析vue中$router与$route的区别,可能比文档还详细--VueRouter完全指北分析【建议👍】再来40道this面试题酸爽继续(1.2w字用手整理)分析