2022秋招前端面试题(三)(附答案)

TCP粘包是怎么回事,如何处理?
默认情况下, TCP 连接会启?延迟传送算法 (Nagle 算法), 在数据发送之前缓存他们. 如果短时间有多个数据发送, 会缓冲到?起作?次发送 (缓冲??? socket.bufferSize ), 这样可以减少 IO 消耗提?性能.
如果是传输?件的话, 那么根本不?处理粘包的问题, 来?个包拼?个包就好了。但是如果是多条消息, 或者是别的?途的数据那么就需要处理粘包.
下面看?个例?, 连续调?两次 send 分别发送两段数据 data1 和 data2, 在接收端有以下?种常?的情况:
A. 先接收到 data1, 然后接收到 data2 .
B. 先接收到 data1 的部分数据, 然后接收到 data1 余下的部分以及 data2 的全部.
C. 先接收到了 data1 的全部数据和 data2 的部分数据, 然后接收到了 data2 的余下的数据.
D. ?次性接收到了 data1 和 data2 的全部数据.
其中的 BCD 就是我们常?的粘包的情况. ?对于处理粘包的问题, 常?的解决?案有:

  • 多次发送之前间隔?个等待时间:只需要等上?段时间再进?下?次 send 就好, 适?于交互频率特别低的场景. 缺点也很明显, 对于?较频繁的场景??传输效率实在太低,不过?乎不?做什么处理.
  • 关闭 Nagle 算法:关闭 Nagle 算法, 在 Node.js 中你可以通过 socket.setNoDelay() ?法来关闭 Nagle 算法, 让每?次 send 都不缓冲直接发送。该?法?较适?于每次发送的数据都?较? (但不是?件那么?), 并且频率不是特别?的场景。如果是每次发送的数据量?较?, 并且频率特别?的, 关闭 Nagle 纯属?废武功。另外, 该?法不适?于?络较差的情况, 因为 Nagle 算法是在服务端进?的包合并情况, 但是如果短时间内客户端的?络情况不好, 或者应?层由于某些原因不能及时将 TCP 的数据 recv, 就会造成多个包在客户端缓冲从?粘包的情况。 (如果是在稳定的机房内部通信那么这个概率是?较?可以选择忽略的)
  • 进?封包/拆包: 封包/拆包是?前业内常?的解决?案了。即给每个数据包在发送之前, 于其前/后放?些有特征的数据, 然后收到数据的时 候根据特征数据分割出来各个数据包。
::before 和 :after 的双冒号和单冒号有什么区别?
(1)冒号(:)用于CSS3伪类,双冒号(::)用于CSS3伪元素。
(2)::before就是以一个子元素的存在,定义在元素主体内容之前的一个伪元素。并不存在于dom之中,只存在在页面之中。
注意: :before:after 这两个伪元素,是在CSS2.1里新出现的。起初,伪元素的前缀使用的是单冒号语法,但随着Web的进化,在CSS3的规范里,伪元素的语法被修改成使用双冒号,成为::before::after
Vue的父子组件生命周期钩子函数执行顺序?
父created -> 父beforeMount -> 子beforeCreate -> 子created -> 子beforeMount -> 子mounted -> 父mounted -->子beforeUpdate -> 子updaed -> 父updated -->父updated -->子beforeDestroy -> 子destroyed ->父destroyed --> 复制代码

说一下你对盒模型的理解?
CSS3中的盒模型有以下两种:标准盒模型、IE盒模型 盒模型都是由四个部分组成的,分别是margin、border、padding和content 标准盒模型和IE盒模型的区别在于设置width和height时, 所对应的范围不同 1、标准盒模型的width和height属性的范围只包含了content 2、IE盒模型的width和height属性的范围包含了border、padding和content 可以通过修改元素的box-sizing属性来改变元素的盒模型; 1、box-sizing:content-box表示标准盒模型(默认值) 2、box-sizing:border-box表示IE盒模型(怪异盒模型) 复制代码

Vue的生命周期是什么 每个钩子里面具体做了什么事情
Vue 实例有?个完整的?命周期,也就是从开始创建、初始化数据、编译模版、挂载Dom -> 渲染、更新 -> 渲染、卸载 等?系列过程,称这是Vue的?命周期。 1、beforeCreate(创建前) :数据观测和初始化事件还未开始,此时 data 的响应式追踪、event/watcher 都还没有被设置,也就是说不能访问到data、computed、watch、methods上的方法和数据。 2、created(创建后) :实例创建完成,实例上配置的 options 包括 data、computed、watch、methods 等都配置完成,但是此时渲染得节点还未挂载到 DOM,所以不能访问到 `$el` 属性。 3、beforeMount(挂载前) :在挂载开始之前被调用,相关的render函数首次被调用。实例已完成以下的配置:编译模板,把data里面的数据和模板生成html。此时还没有挂载html到页面上。 4、mounted(挂载后) :在el被新创建的 vm.$el 替换,并挂载到实例上去之后调用。实例已完成以下的配置:用上面编译好的html内容替换el属性指向的DOM对象。完成模板中的html渲染到html 页面中。此过程中进行ajax交互。 5、beforeUpdate(更新前) :响应式数据更新时调用,此时虽然响应式数据更新了,但是对应的真实 DOM 还没有被渲染。 6、updated(更新后):在由于数据更改导致的虚拟DOM重新渲染和打补丁之后调用。此时 DOM 已经根据响应式数据的变化更新了。调用时,组件 DOM已经更新,所以可以执行依赖于DOM的操作。然而在大多数情况下,应该避免在此期间更改状态,因为这可能会导致更新无限循环。该钩子在服务器端渲染期间不被调用。 7、beforeDestroy(销毁前) :实例销毁之前调用。这一步,实例仍然完全可用,`this` 仍能获取到实例。 8、destroyed(销毁后) :实例销毁后调用,调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。该钩子在服务端渲染期间不被调用。 另外还有 `keep-alive` 独有的生命周期,分别为 `activated` 和 `deactivated` 。用 `keep-alive` 包裹的组件在切换时不会进行销毁,而是缓存到内存中并执行 `deactivated` 钩子函数,命中缓存渲染后会执行 `activated` 钩子函数。 复制代码

Loader和Plugin 有什么区别
Loader:直译为"加载器"。Webpack将一切文件视为模块,但是webpack原生是只能解析js文件,如果想将其他文件也打包的话,就会用到loader。 所以Loader的作用是让webpack拥有了加载和解析非JavaScript文件的能力。 Plugin:直译为"插件"。Plugin可以扩展webpack的功能,让webpack具有更多的灵活性。 在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。
iframe 有那些优点和缺点?
iframe 元素会创建包含另外一个文档的内联框架(即行内框架)。
优点:
  • 用来加载速度较慢的内容(如广告)
  • 可以使脚本可以并行下载
  • 可以实现跨子域通信
缺点:
  • iframe 会阻塞主页面的 onload 事件
  • 无法被一些搜索引擎索识别
  • 会产生很多页面,不容易管理
解析 URL 参数为对象
function parseParam(url) { const paramsStr = /.+\?(.+)$/.exec(url)[1]; // 将 ? 后面的字符串取出来 const paramsArr = paramsStr.split('&'); // 将字符串以 & 分割后存到数组中 let paramsObj = {}; // 将 params 存到对象中 paramsArr.forEach(param => { if (/=/.test(param)) { // 处理有 value 的参数 let [key, val] = param.split('='); // 分割 key 和 value val = decodeURIComponent(val); // 解码 val = /^\d+$/.test(val) ? parseFloat(val) : val; // 判断是否转为数字if (paramsObj.hasOwnProperty(key)) { // 如果对象有 key,则添加一个值 paramsObj[key] = [].concat(paramsObj[key], val); } else { // 如果对象没有这个 key,创建 key 并设置值 paramsObj[key] = val; } } else { // 处理没有 value 的参数 paramsObj[param] = true; } })return paramsObj; } 复制代码

什么是原型什么是原型链?
Document复制代码

NaN 是什么,用 typeof 会输出什么?
Not a Number,表示非数字,typeof NaN === 'number'
代码输出问题
function fun(n, o) { console.log(o) return { fun: function(m){ return fun(m, n); } }; } var a = fun(0); a.fun(1); a.fun(2); a.fun(3); var b = fun(0).fun(1).fun(2).fun(3); var c = fun(0).fun(1); c.fun(2); c.fun(3); 复制代码

输出结果:
undefined000 undefined012 undefined011 复制代码

这是一道关于闭包的题目,对于fun方法,调用之后返回的是一个对象。我们知道,当调用函数的时候传入的实参比函数声明时指定的形参个数要少,剩下的形参都将设置为undefined值。所以 console.log(o); 会输出undefined。而a就是是fun(0)返回的那个对象。也就是说,函数fun中参数 n 的值是0,而返回的那个对象中,需要一个参数n,而这个对象的作用域中没有n,它就继续沿着作用域向上一级的作用域中寻找n,最后在函数fun中找到了n,n的值是0。了解了这一点,其他运算就很简单了,以此类推。
手写发布订阅
class EventListener { listeners = {}; on(name, fn) { (this.listeners[name] || (this.listeners[name] = [])).push(fn) } once(name, fn) { let tem = (...args) => { this.removeListener(name, fn) fn(...args) } fn.fn = tem this.on(name, tem) } removeListener(name, fn) { if (this.listeners[name]) { this.listeners[name] = this.listeners[name].filter(listener => (listener != fn && listener != fn.fn)) } } removeAllListeners(name) { if (name && this.listeners[name]) delete this.listeners[name] this.listeners = {} } emit(name, ...args) { if (this.listeners[name]) { this.listeners[name].forEach(fn => fn.call(this, ...args)) } } }

同步和异步的区别
  • 同步指的是当一个进程在执行某个请求时,如果这个请求需要等待一段时间才能返回,那么这个进程会一直等待下去,直到消息返回为止再继续向下执行。
  • 异步指的是当一个进程在执行某个请求时,如果这个请求需要等待一段时间才能返回,这个时候进程会继续往下执行,不会阻塞等待消息的返回,当消息返回时系统再通知进程进行处理。
介绍下 promise 的特性、优缺点,内部是如何实现的,动手实现 Promise
1)Promise基本特性
  • 1、Promise有三种状态:pending(进行中)、fulfilled(已成功)、rejected(已失败)
  • 2、Promise对象接受一个回调函数作为参数, 该回调函数接受两个参数,分别是成功时的回调resolve和失败时的回调reject;另外resolve的参数除了正常值以外, 还可能是一个Promise对象的实例;reject的参数通常是一个Error对象的实例。
  • 3、then方法返回一个新的Promise实例,并接收两个参数onResolved(fulfilled状态的回调);onRejected(rejected状态的回调,该参数可选)
  • 4、catch方法返回一个新的Promise实例
  • 5、finally方法不管Promise状态如何都会执行,该方法的回调函数不接受任何参数
  • 6、Promise.all()方法将多个多个Promise实例,包装成一个新的Promise实例,该方法接受一个由Promise对象组成的数组作为参数(Promise.all()方法的参数可以不是数组,但必须具有Iterator接口,且返回的每个成员都是Promise实例),注意参数中只要有一个实例触发catch方法,都会触发Promise.all()方法返回的新的实例的catch方法,如果参数中的某个实例本身调用了catch方法,将不会触发Promise.all()方法返回的新实例的catch方法
  • 7、Promise.race()方法的参数与Promise.all方法一样,参数中的实例只要有一个率先改变状态就会将该实例的状态传给Promise.race()方法,并将返回值作为Promise.race()方法产生的Promise实例的返回值
  • 8、Promise.resolve()将现有对象转为Promise对象,如果该方法的参数为一个Promise对象,Promise.resolve()将不做任何处理;如果参数thenable对象(即具有then方法),Promise.resolve()将该对象转为Promise对象并立即执行then方法;如果参数是一个原始值,或者是一个不具有then方法的对象,则Promise.resolve方法返回一个新的Promise对象,状态为fulfilled,其参数将会作为then方法中onResolved回调函数的参数,如果Promise.resolve方法不带参数,会直接返回一个fulfilled状态的 Promise 对象。需要注意的是,立即resolve()的 Promise 对象,是在本轮“事件循环”(event loop)的结束时执行,而不是在下一轮“事件循环”的开始时。
  • 9、Promise.reject()同样返回一个新的Promise对象,状态为rejected,无论传入任何参数都将作为reject()的参数
2)Promise优点
  • ①统一异步 API
    • Promise 的一个重要优点是它将逐渐被用作浏览器的异步 API ,统一现在各种各样的 API ,以及不兼容的模式和手法。
  • ②Promise 与事件对比
    • 和事件相比较, Promise 更适合处理一次性的结果。在结果计算出来之前或之后注册回调函数都是可以的,都可以拿到正确的值。 Promise 的这个优点很自然。但是,不能使用 Promise 处理多次触发的事件。链式处理是 Promise 的又一优点,但是事件却不能这样链式处理。
  • ③Promise 与回调对比
    • 解决了回调地狱的问题,将异步操作以同步操作的流程表达出来。
  • ④Promise 带来的额外好处是包含了更好的错误处理方式(包含了异常处理),并且写起来很轻松(因为可以重用一些同步的工具,比如 Array.prototype.map() )。
3)Promise缺点
  • 1、无法取消Promise,一旦新建它就会立即执行,无法中途取消。
  • 2、如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。
  • 3、当处于Pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
  • 4、Promise 真正执行回调的时候,定义 Promise 那部分实际上已经走完了,所以 Promise 的报错堆栈上下文不太友好。
4)简单代码实现
最简单的Promise实现有7个主要属性, state(状态), value(成功返回值), reason(错误信息), resolve方法, reject方法, then方法
class Promise{ constructor(executor) { this.state = 'pending'; this.value = https://www.it610.com/article/undefined; this.reason = undefined; let resolve = value => { if (this.state === 'pending') { this.state = 'fulfilled'; this.value = https://www.it610.com/article/value; } }; let reject = reason => { if (this.state === 'pending') { this.state = 'rejected'; this.reason = reason; } }; try { // 立即执行函数 executor(resolve, reject); } catch (err) { reject(err); } } then(onFulfilled, onRejected) { if (this.state === 'fulfilled') { let x = onFulfilled(this.value); }; if (this.state === 'rejected') { let x = onRejected(this.reason); }; } }

5)面试够用版
function myPromise(constructor){ let self=this; self.status="pending" //定义状态改变前的初始状态 self.value=https://www.it610.com/article/undefined; //定义状态为resolved的时候的状态 self.reason=undefined; //定义状态为rejected的时候的状态 function resolve(value){ //两个==="pending",保证了了状态的改变是不不可逆的 if(self.status==="pending"){ self.value=https://www.it610.com/article/value; self.status="resolved"; } } function reject(reason){ //两个==="pending",保证了了状态的改变是不不可逆的 if(self.status==="pending"){ self.reason=reason; self.status="rejected"; } } //捕获构造异常 try{ constructor(resolve,reject); }catch(e){ reject(e); } } myPromise.prototype.then=function(onFullfilled,onRejected){ let self=this; switch(self.status){ case "resolved": onFullfilled(self.value); break; case "rejected": onRejected(self.reason); break; default: } }// 测试 var p=new myPromise(function(resolve,reject){resolve(1)}); p.then(function(x){console.log(x)}) //输出1

【2022秋招前端面试题(三)(附答案)】6)大厂专供版
const PENDING = "pending"; const FULFILLED = "fulfilled"; const REJECTED = "rejected"; const resolvePromise = (promise, x, resolve, reject) => { if (x === promise) { // If promise and x refer to the same object, reject promise with a TypeError as the reason. reject(new TypeError('循环引用')) } // if x is an object or function, if (x !== null && typeof x === 'object' || typeof x === 'function') { // If both resolvePromise and rejectPromise are called, or multiple calls to the same argument are made, the first call takes precedence, and any further calls are ignored. let called try { // If retrieving the property x.then results in a thrown exception e, reject promise with e as the reason. let then = x.then // Let then be x.then // If then is a function, call it with x as this if (typeof then === 'function') { // If/when resolvePromise is called with a value y, run [[Resolve]](promise, y) // If/when rejectPromise is called with a reason r, reject promise with r. then.call(x, y => { if (called) return called = true resolvePromise(promise, y, resolve, reject) }, r => { if (called) return called = true reject(r) }) } else { // If then is not a function, fulfill promise with x. resolve(x) } } catch (e) { if (called) return called = true reject(e) } } else { // If x is not an object or function, fulfill promise with x resolve(x) } } function Promise(excutor) { let that = this; // 缓存当前promise实例例对象 that.status = PENDING; // 初始状态 that.value = https://www.it610.com/article/undefined; // fulfilled状态时 返回的信息 that.reason = undefined; // rejected状态时 拒绝的原因 that.onFulfilledCallbacks = []; // 存储fulfilled状态对应的onFulfilled函数 that.onRejectedCallbacks = []; // 存储rejected状态对应的onRejected函数 function resolve(value) { // value成功态时接收的终值 if(value instanceof Promise) { return value.then(resolve, reject); } // 实践中要确保 onFulfilled 和 onRejected ?方法异步执?行行,且应该在 then ?方法被调?用的那?一轮事件循环之后的新执?行行栈中执?行行。 setTimeout(() => { // 调?用resolve 回调对应onFulfilled函数 if (that.status === PENDING) { // 只能由pending状态 => fulfilled状态 (避免调?用多次resolve reject) that.status = FULFILLED; that.value = https://www.it610.com/article/value; that.onFulfilledCallbacks.forEach(cb => cb(that.value)); } }); } function reject(reason) { // reason失败态时接收的拒因 setTimeout(() => { // 调?用reject 回调对应onRejected函数 if (that.status === PENDING) { // 只能由pending状态 => rejected状态 (避免调?用多次resolve reject) that.status = REJECTED; that.reason = reason; that.onRejectedCallbacks.forEach(cb => cb(that.reason)); } }); }// 捕获在excutor执?行行器器中抛出的异常 // new Promise((resolve, reject) => { //throw new Error('error in excutor') // }) try { excutor(resolve, reject); } catch (e) { reject(e); } } Promise.prototype.then = function(onFulfilled, onRejected) { const that = this; let newPromise; // 处理理参数默认值 保证参数后续能够继续执?行行 onFulfilled = typeof onFulfilled === "function" ? onFulfilled : value => value; onRejected = typeof onRejected === "function" ? onRejected : reason => { throw reason; }; if (that.status === FULFILLED) { // 成功态 return newPromise = new Promise((resolve, reject) => { setTimeout(() => { try{ let x = onFulfilled(that.value); resolvePromise(newPromise, x, resolve, reject); //新的promise resolve 上?一个onFulfilled的返回值 } catch(e) { reject(e); // 捕获前?面onFulfilled中抛出的异常then(onFulfilled, onRejected); } }); }) } if (that.status === REJECTED) { // 失败态 return newPromise = new Promise((resolve, reject) => { setTimeout(() => { try { let x = onRejected(that.reason); resolvePromise(newPromise, x, resolve, reject); } catch(e) { reject(e); } }); }); } if (that.status === PENDING) { // 等待态 // 当异步调?用resolve/rejected时 将onFulfilled/onRejected收集暂存到集合中 return newPromise = new Promise((resolve, reject) => { that.onFulfilledCallbacks.push((value) => { try { let x = onFulfilled(value); resolvePromise(newPromise, x, resolve, reject); } catch(e) { reject(e); } }); that.onRejectedCallbacks.push((reason) => { try { let x = onRejected(reason); resolvePromise(newPromise, x, resolve, reject); } catch(e) { reject(e); } }); }); } };

如何避免回流与重绘?
减少回流与重绘的措施:
  • 操作DOM时,尽量在低层级的DOM节点进行操作
  • 不要使用table布局, 一个小的改动可能会使整个table进行重新布局
  • 使用CSS的表达式
  • 不要频繁操作元素的样式,对于静态页面,可以修改类名,而不是样式。
  • 使用absolute或者fixed,使元素脱离文档流,这样他们发生变化就不会影响其他元素
  • 避免频繁操作DOM,可以创建一个文档片段documentFragment,在它上面应用所有DOM操作,最后再把它添加到文档中
  • 将元素先设置display: none,操作结束后再把它显示出来。因为在display属性为none的元素上进行的DOM操作不会引发回流和重绘。
  • 将DOM的多个读操作(或者写操作)放在一起,而不是读写操作穿插着写。这得益于浏览器的渲染队列机制。
浏览器针对页面的回流与重绘,进行了自身的优化——渲染队列
浏览器会将所有的回流、重绘的操作放在一个队列中,当队列中的操作到了一定的数量或者到了一定的时间间隔,浏览器就会对队列进行批处理。这样就会让多次的回流、重绘变成一次回流重绘。
上面,将多个读操作(或者写操作)放在一起,就会等所有的读操作进入队列之后执行,这样,原本应该是触发多次回流,变成了只触发一次回流。
常见的水平垂直方式有几种?
//利用绝对定位,先将元素的左上角通过 top:50%和 left:50%定位到页面的中心,然后再通过 translate 来调整元素的中心点到页面的中心。该方法需要考虑浏览器兼容问题。 .parent { position: relative; }.child { position: absolute; left: 50%; top: 50%; transform: translate(-50%,-50%); } //利用绝对定位,设置四个方向的值都为 0,并将 margin 设置为 auto,由于宽高固定,因此对应方向实现平分,可以实现水平和垂直方向上的居中。该方法适用于盒子有宽高的情况: .parent { position: relative; }.child { position: absolute; top: 0; bottom: 0; left: 0; right: 0; margin: auto; } //利用绝对定位,先将元素的左上角通过 top:50%和 left:50%定位到页面的中心,然后再通过 margin 负值来调整元素的中心点到页面的中心。该方法适用于盒子宽高已知的情况 .parent { position: relative; }.child { position: absolute; top: 50%; left: 50%; margin-top: -50px; /* 自身 height 的一半 */ margin-left: -50px; /* 自身 width 的一半 */ } //使用 flex 布局,通过 align-items:center 和 justify-content:center 设置容器的垂直和水平方向上为居中对齐,然后它的子元素也可以实现垂直和水平的居中。该方法要**考虑兼容的问题**,该方法在移动端用的较多: .parent { display: flex; justify-content:center; align-items:center; } //另外,如果父元素设置了flex布局,只需要给子元素加上`margin:auto; `就可以实现垂直居中布局 .parent{ display:flex; } .child{ margin: auto; } 复制代码

说一下HTTP和HTTPS协议的区别?
1、HTTPS协议需要CA证书,费用较高; 而HTTP协议不需要 2、HTTP协议是超文本传输协议,信息是明文传输的,HTTPS则是具有安全性的SSL加密传输协议; 3、使用不同的连接方式,端口也不同,HTTP协议端口是80,HTTPS协议端口是443; 4、HTTP协议连接很简单,是无状态的; HTTPS协议是具有SSL和HTTP协议构建的可进行加密传输、身份认证的网络协议,比HTTP更加安全 复制代码

对节流与防抖的理解
  • 函数防抖是指在事件被触发 n 秒后再执行回调,如果在这 n 秒内事件又被触发,则重新计时。这可以使用在一些点击请求的事件上,避免因为用户的多次点击向后端发送多次请求。
  • 函数节流是指规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内某事件被触发多次,只有一次能生效。节流可以使用在 scroll 函数的事件监听上,通过事件节流来降低事件调用的频率。
防抖函数的应用场景:
  • 按钮提交场景:防?多次提交按钮,只执?最后提交的?次
  • 服务端验证场景:表单验证需要服务端配合,只执??段连续的输?事件的最后?次,还有搜索联想词功能类似?存环境请?lodash.debounce
节流函数的适?场景:
  • 拖拽场景:固定时间内只执??次,防?超?频次触发位置变动
  • 缩放场景:监控浏览器resize
  • 动画场景:避免短时间内多次触发动画引起性能问题
点击刷新按钮或者按 F5、按 Ctrl+F5 (强制刷新)、地址栏回车有什么区别?
  • 点击刷新按钮或者按 F5: 浏览器直接对本地的缓存文件过期,但是会带上If-Modifed-Since,If-None-Match,这就意味着服务器会对文件检查新鲜度,返回结果可能是 304,也有可能是 200。
  • 用户按 Ctrl+F5(强制刷新): 浏览器不仅会对本地文件过期,而且不会带上 If-Modifed-Since,If-None-Match,相当于之前从来没有请求过,返回结果是 200。
  • 地址栏回车: 浏览器发起请求,按照正常流程,本地检查是否过期,然后服务器检查新鲜度,最后返回内容。
代码输出结果
Promise.resolve(1) .then(2) .then(Promise.resolve(3)) .then(console.log) 复制代码

输出结果如下:
1 复制代码

看到这个题目,好多的then,实际上只需要记住一个原则:.then.catch 的参数期望是函数,传入非函数则会发生值透传。
第一个then和第二个then中传入的都不是函数,一个是数字,一个是对象,因此发生了透传,将resolve(1) 的值直接传到最后一个then里,直接打印出1。

    推荐阅读