乐趣区

关于javascript:校招前端面试题集锦

JavaScript 类数组对象的定义?

一个领有 length 属性和若干索引属性的对象就能够被称为类数组对象,类数组对象和数组相似,然而不能调用数组的办法。常见的类数组对象有 arguments 和 DOM 办法的返回后果,还有一个函数也能够被看作是类数组对象,因为它含有 length 属性值,代表可接管的参数个数。

常见的类数组转换为数组的办法有这样几种:

(1)通过 call 调用数组的 slice 办法来实现转换

Array.prototype.slice.call(arrayLike);

(2)通过 call 调用数组的 splice 办法来实现转换

Array.prototype.splice.call(arrayLike, 0);

(3)通过 apply 调用数组的 concat 办法来实现转换

Array.prototype.concat.apply([], arrayLike);

(4)通过 Array.from 办法来实现转换

Array.from(arrayLike);

对节流与防抖的了解

  • 函数防抖是指在事件被触发 n 秒后再执行回调,如果在这 n 秒内事件又被触发,则从新计时。这能够应用在一些点击申请的事件上,防止因为用户的屡次点击向后端发送屡次申请。
  • 函数节流是指规定一个单位工夫,在这个单位工夫内,只能有一次触发事件的回调函数执行,如果在同一个单位工夫内某事件被触发屡次,只有一次能失效。节流能够应用在 scroll 函数的事件监听上,通过事件节流来升高事件调用的频率。

防抖函数的利用场景:

  • 按钮提交场景:防⽌屡次提交按钮,只执⾏最初提交的⼀次
  • 服务端验证场景:表单验证须要服务端配合,只执⾏⼀段间断的输⼊事件的最初⼀次,还有搜寻联想词性能相似⽣存环境请⽤ lodash.debounce

节流函数的适⽤场景:

  • 拖拽场景:固定工夫内只执⾏⼀次,防⽌超⾼频次触发地位变动
  • 缩放场景:监控浏览器 resize
  • 动画场景:防止短时间内屡次触发动画引起性能问题

对浏览器的缓存机制的了解

浏览器缓存的全过程:

  • 浏览器第一次加载资源,服务器返回 200,浏览器从服务器下载资源文件,并缓存资源文件与 response header,以供下次加载时比照应用;
  • 下一次加载资源时,因为强制缓存优先级较高,先比拟以后工夫与上一次返回 200 时的时间差,如果没有超过 cache-control 设置的 max-age,则没有过期,并命中强缓存,间接从本地读取资源。如果浏览器不反对 HTTP1.1,则应用 expires 头判断是否过期;
  • 如果资源已过期,则表明强制缓存没有被命中,则开始协商缓存,向服务器发送带有 If-None-Match 和 If-Modified-Since 的申请;
  • 服务器收到申请后,优先依据 Etag 的值判断被申请的文件有没有做批改,Etag 值统一则没有批改,命中协商缓存,返回 304;如果不统一则有改变,间接返回新的资源文件带上新的 Etag 值并返回 200;
  • 如果服务器收到的申请没有 Etag 值,则将 If-Modified-Since 和被申请文件的最初批改工夫做比对,统一则命中协商缓存,返回 304;不统一则返回新的 last-modified 和文件并返回 200;

    很多网站的资源前面都加了版本号,这样做的目标是:每次降级了 JS 或 CSS 文件后,为了避免浏览器进行缓存,强制扭转版本号,客户端浏览器就会从新下载新的 JS 或 CSS 文件,以保障用户可能及时取得网站的最新更新。

HTML5 的离线贮存怎么应用,它的工作原理是什么

离线存储指的是:在用户没有与因特网连贯时,能够失常拜访站点或利用,在用户与因特网连贯时,更新用户机器上的缓存文件。

原理:HTML5 的离线存储是基于一个新建的 .appcache 文件的缓存机制(不是存储技术),通过这个文件上的解析清单离线存储资源,这些资源就会像 cookie 一样被存储了下来。之后当网络在处于离线状态下时,浏览器会通过被离线存储的数据进行页面展现

应用办法:(1)创立一个和 html 同名的 manifest 文件,而后在页面头部退出 manifest 属性:

<html lang="en" manifest="index.manifest">

(2)在 cache.manifest 文件中编写须要离线存储的资源:

CACHE MANIFEST
    #v0.11
    CACHE:
    js/app.js
    css/style.css
    NETWORK:
    resourse/logo.png
    FALLBACK:
    / /offline.html
  • CACHE: 示意须要离线存储的资源列表,因为蕴含 manifest 文件的页面将被主动离线存储,所以不须要把页面本身也列出来。
  • NETWORK: 示意在它上面列出来的资源只有在在线的状况下能力拜访,他们不会被离线存储,所以在离线状况下无奈应用这些资源。不过,如果在 CACHE 和 NETWORK 中有一个雷同的资源,那么这个资源还是会被离线存储,也就是说 CACHE 的优先级更高。
  • FALLBACK: 示意如果拜访第一个资源失败,那么就应用第二个资源来替换他,比方下面这个文件示意的就是如果拜访根目录下任何一个资源失败了,那么就去拜访 offline.html。

(3)在离线状态时,操作 window.applicationCache 进行离线缓存的操作。

如何更新缓存:

(1)更新 manifest 文件

(2)通过 javascript 操作

(3)革除浏览器缓存

注意事项:

(1)浏览器对缓存数据的容量限度可能不太一样(某些浏览器设置的限度是每个站点 5MB)。

(2)如果 manifest 文件,或者外部列举的某一个文件不能失常下载,整个更新过程都将失败,浏览器持续全副应用老的缓存。

(3)援用 manifest 的 html 必须与 manifest 文件同源,在同一个域下。

(4)FALLBACK 中的资源必须和 manifest 文件同源。

(5)当一个资源被缓存后,该浏览器间接申请这个绝对路径也会拜访缓存中的资源。

(6)站点中的其余页面即便没有设置 manifest 属性,申请的资源如果在缓存中也从缓存中拜访。

(7)当 manifest 文件产生扭转时,资源申请自身也会触发更新。

对事件委托的了解

(1)事件委托的概念

事件委托实质上是利用了 浏览器事件冒泡 的机制。因为事件在冒泡过程中会上传到父节点,父节点能够通过事件对象获取到指标节点,因而能够把子节点的监听函数定义在父节点上,由父节点的监听函数对立解决多个子元素的事件,这种形式称为事件委托(事件代理)。

应用事件委托能够不必要为每一个子元素都绑定一个监听事件,这样缩小了内存上的耗费。并且应用事件代理还能够实现事件的动静绑定,比如说新增了一个子节点,并不需要独自地为它增加一个监听事件,它绑定的事件会交给父元素中的监听函数来解决。

(2)事件委托的特点

  • 缩小内存耗费

如果有一个列表,列表之中有大量的列表项,须要在点击列表项的时候响应一个事件:

<ul id="list">
  <li>item 1</li>
  <li>item 2</li>
  <li>item 3</li>
  ......
  <li>item n</li>
</ul>

如果给每个列表项一一都绑定一个函数,那对于内存耗费是十分大的,效率上须要耗费很多性能。因而,比拟好的办法就是把这个点击事件绑定到他的父层,也就是 ul 上,而后在执行事件时再去匹配判断指标元素,所以事件委托能够缩小大量的内存耗费,节约效率。

  • 动静绑定事件

给上述的例子中每个列表项都绑定事件,在很多时候,须要通过 AJAX 或者用户操作动静的减少或者去除列表项元素,那么在每一次扭转的时候都须要从新给新增的元素绑定事件,给行将删去的元素解绑事件;如果用了事件委托就没有这种麻烦了,因为事件是绑定在父层的,和指标元素的增减是没有关系的,执行到指标元素是在真正响应执行事件函数的过程中去匹配的,所以应用事件在动静绑定事件的状况下是能够缩小很多反复工作的。

// 来实现把 #list 下的 li 元素的事件代理委托到它的父层元素也就是 #list 上:// 给父层元素绑定事件
document.getElementById('list').addEventListener('click', function (e) {
  // 兼容性解决
  var event = e || window.event;
  var target = event.target || event.srcElement;
  // 判断是否匹配指标元素
  if (target.nodeName.toLocaleLowerCase === 'li') {console.log('the content is:', target.innerHTML);
  }
});

在上述代码中,target 元素则是在 #list 元素之下具体被点击的元素,而后通过判断 target 的一些属性(比方:nodeName,id 等等)能够更准确地匹配到某一类 #list li 元素之上;

(3)局限性

当然,事件委托也是有局限的。比方 focus、blur 之类的事件没有事件冒泡机制,所以无奈实现事件委托;mousemove、mouseout 这样的事件,尽管有事件冒泡,然而只能一直通过地位去计算定位,对性能耗费高,因而也是不适宜于事件委托的。

当然事件委托不是只有长处,它也是有 毛病 的,事件委托会影响页面性能,次要影响因素有:

  • 元素中,绑定事件委托的次数;
  • 点击的最底层元素,到绑定事件元素之间的 DOM 层数;

在必须应用事件委托的中央,能够进行如下的解决:

  • 只在必须的中央,应用事件委托,比方:ajax的部分刷新区域
  • 尽量的缩小绑定的层级,不在 body 元素上,进行绑定
  • 缩小绑定的次数,如果能够,那么把多个事件的绑定,合并到一次事件委托中去,由这个事件委托的回调,来进行散发。

归并排序 – 工夫复杂度 nlog(n)

题目形容: 实现一个工夫复杂度为 nlog(n)的排序算法

实现代码如下:

function merge(left, right) {let res = [];
  let i = 0;
  let j = 0;
  while (i < left.length && j < right.length) {if (left[i] < right[j]) {res.push(left[i]);
      i++;
    } else {res.push(right[j]);
      j++;
    }
  }
  if (i < left.length) {res.push(...left.slice(i));
  } else {res.push(...right.slice(j));
  }
  return res;
}

function mergeSort(arr) {if (arr.length < 2) {return arr;}
  const mid = Math.floor(arr.length / 2);

  const left = mergeSort(arr.slice(0, mid));
  const right = mergeSort(arr.slice(mid));
  return merge(left, right);
}
// console.log(mergeSort([3, 6, 2, 4, 1]));

参考 前端进阶面试题具体解答

死锁产生的起因?如果解决死锁的问题?

所谓死锁,是指多个过程在运行过程中因抢夺资源而造成的一种僵局,当过程处于这种僵持状态时,若无外力作用,它们都将无奈再向前推动。

零碎中的资源能够分为两类:

  • 可剥夺资源,是指某过程在取得这类资源后,该资源能够再被其余过程或零碎剥夺,CPU 和主存均属于可剥夺性资源;
  • 不可剥夺资源,当零碎把这类资源分配给某过程后,再不能强行发出,只能在过程用完后自行开释,如磁带机、打印机等。

产生死锁的起因:

(1)竞争资源

  • 产生死锁中的竞争资源之一指的是 竞争不可剥夺资源(例如:零碎中只有一台打印机,可供过程 P1 应用,假设 P1 已占用了打印机,若 P2 持续要求打印机打印将阻塞)
  • 产生死锁中的竞争资源另外一种资源指的是 竞争长期资源(长期资源包含硬件中断、信号、音讯、缓冲区内的音讯等),通常音讯通信程序进行不当,则会产生死锁

(2)过程间推动程序非法

若 P1 放弃了资源 R1,P2 放弃了资源 R2,零碎处于不平安状态,因为这两个过程再向前推动,便可能产生死锁。例如,当 P1 运行到 P1:Request(R2)时,将因 R2 已被 P2 占用而阻塞;当 P2 运行到 P2:Request(R1)时,也将因 R1 已被 P1 占用而阻塞,于是产生过程死锁

产生死锁的必要条件:

  • 互斥条件:过程要求对所调配的资源进行排它性管制,即在一段时间内某资源仅为一过程所占用。
  • 申请和放弃条件:当过程因申请资源而阻塞时,对已取得的资源放弃不放。
  • 不剥夺条件:过程已取得的资源在未应用完之前,不能剥夺,只能在应用完时由本人开释。
  • 环路期待条件:在产生死锁时,必然存在一个过程——资源的环形链。

预防死锁的办法:

  • 资源一次性调配:一次性调配所有资源,这样就不会再有申请了(毁坏申请条件)
  • 只有有一个资源得不到调配,也不给这个过程调配其余的资源(毁坏请放弃条件)
  • 可剥夺资源:即当某过程取得了局部资源,但得不到其它资源,则开释已占有的资源(毁坏不可剥夺条件)
  • 资源有序调配法:零碎给每类资源赋予一个编号,每一个过程按编号递增的程序申请资源,开释则相同(毁坏环路期待条件)

AJAX

const getJSON = function(url) {return new Promise((resolve, reject) => {const xhr = XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP');
        xhr.open('GET', url, false);
        xhr.setRequestHeader('Accept', 'application/json');
        xhr.onreadystatechange = function() {if (xhr.readyState !== 4) return;
            if (xhr.status === 200 || xhr.status === 304) {resolve(xhr.responseText);
            } else {reject(new Error(xhr.responseText));
            }
        }
        xhr.send();})
}

实现数组原型办法

forEach

Array.prototype.forEach2 = function(callback, thisArg) {if (this == null) {throw new TypeError('this is null or not defined')
    }
    if (typeof callback !== "function") {throw new TypeError(callback + 'is not a function')
    }
    const O = Object(this)  // this 就是以后的数组
    const len = O.length >>> 0  // 前面有解释
    let k = 0
    while (k < len) {if (k in O) {callback.call(thisArg, O[k], k, O);
        }
        k++;
    }
}

O.length >>> 0 是什么操作?就是无符号右移 0 位,那有什么意义嘛?就是为了保障转换后的值为正整数。其实底层做了 2 层转换,第一是非 number 转成 number 类型,第二是将 number 转成 Uint32 类型

map

基于 forEach 的实现可能很容易写出 map 的实现:

- Array.prototype.forEach2 = function(callback, thisArg) {+ Array.prototype.map2 = function(callback, thisArg) {if (this == null) {throw new TypeError('this is null or not defined')
    }
    if (typeof callback !== "function") {throw new TypeError(callback + 'is not a function')
    }
    const O = Object(this)
    const len = O.length >>> 0
-   let k = 0
+   let k = 0, res = []
    while (k < len) {if (k in O) {-           callback.call(thisArg, O[k], k, O);
+           res[k] = callback.call(thisArg, O[k], k, O);
        }
        k++;
    }
+   return res
}

filter

同样,基于 forEach 的实现可能很容易写出 filter 的实现:

- Array.prototype.forEach2 = function(callback, thisArg) {+ Array.prototype.filter2 = function(callback, thisArg) {if (this == null) {throw new TypeError('this is null or not defined')
    }
    if (typeof callback !== "function") {throw new TypeError(callback + 'is not a function')
    }
    const O = Object(this)
    const len = O.length >>> 0
-   let k = 0
+   let k = 0, res = []
    while (k < len) {if (k in O) {-           callback.call(thisArg, O[k], k, O);
+           if (callback.call(thisArg, O[k], k, O)) {+               res.push(O[k])                
+           }
        }
        k++;
    }
+   return res
}

some

同样,基于 forEach 的实现可能很容易写出 some 的实现:

- Array.prototype.forEach2 = function(callback, thisArg) {+ Array.prototype.some2 = function(callback, thisArg) {if (this == null) {throw new TypeError('this is null or not defined')
    }
    if (typeof callback !== "function") {throw new TypeError(callback + 'is not a function')
    }
    const O = Object(this)
    const len = O.length >>> 0
    let k = 0
    while (k < len) {if (k in O) {-           callback.call(thisArg, O[k], k, O);
+           if (callback.call(thisArg, O[k], k, O)) {
+               return true
+           }
        }
        k++;
    }
+   return false
}

reduce

Array.prototype.reduce2 = function(callback, initialValue) {if (this == null) {throw new TypeError('this is null or not defined')
    }
    if (typeof callback !== "function") {throw new TypeError(callback + 'is not a function')
    }
    const O = Object(this)
    const len = O.length >>> 0
    let k = 0, acc

    if (arguments.length > 1) {acc = initialValue} else {
        // 没传入初始值的时候,取数组中第一个非 empty 的值为初始值
        while (k < len && !(k in O)) {k++}
        if (k > len) {throw new TypeError( 'Reduce of empty array with no initial value');
        }
        acc = O[k++]
    }
    while (k < len) {if (k in O) {acc = callback(acc, O[k], k, O)
        }
        k++
    }
    return acc
}

数据类型判断

核心思想 typeof 能够判断 Undefined、String、Number、Boolean、Symbol、Function 类型的数据,但对其余的都会认为是 Object,比方Null、Array 等。所以通过 typeof 来判断数据类型会不精确。

解决办法 :能够通过Object.prototype.toString 解决。

实现

function mytypeof(obj) {return Object.prototype.toString.call(obj).slice(8, -1).toLowerCase();}
  1. 应用call 是为了绑定 thisobj
  2. 应用 slice 是因为这后面返回的后果是相似[Object xxx] 这样的, xxx 是依据 obj 的类型变动的
  3. 应用 toLowerCase 是因为原生typeof 的返回后果的第一个字母是小写字母。

实现节流函数和防抖函数

函数防抖的实现:

function debounce(fn, wait) {
  var timer = null;

  return function() {
    var context = this,
      args = [...arguments];

    // 如果此时存在定时器的话,则勾销之前的定时器从新记时
    if (timer) {clearTimeout(timer);
      timer = null;
    }

    // 设置定时器,使事件间隔指定事件后执行
    timer = setTimeout(() => {fn.apply(context, args);
    }, wait);
  };
}

函数节流的实现:

// 工夫戳版
function throttle(fn, delay) {var preTime = Date.now();

  return function() {
    var context = this,
      args = [...arguments],
      nowTime = Date.now();

    // 如果两次工夫距离超过了指定工夫,则执行函数。if (nowTime - preTime >= delay) {preTime = Date.now();
      return fn.apply(context, args);
    }
  };
}

// 定时器版
function throttle (fun, wait){
  let timeout = null
  return function(){
    let context = this
    let args = [...arguments]
    if(!timeout){timeout = setTimeout(() => {fun.apply(context, args)
        timeout = null 
      }, wait)
    }
  }
}

变量晋升

函数在运行的时候,会首先创立执行上下文,而后将执行上下文入栈,而后当此执行上下文处于栈顶时,开始运行执行上下文。

在创立执行上下文的过程中会做三件事:创立变量对象,创立作用域链,确定 this 指向,其中创立变量对象的过程中,首先会为 arguments 创立一个属性,值为 arguments,而后会扫码 function 函数申明,创立一个同名属性,值为函数的援用,接着会扫码 var 变量申明,创立一个同名属性,值为 undefined,这就是变量晋升。

请实现 DOM2JSON 一个函数,能够把一个 DOM 节点输入 JSON 的格局

题目形容:

<div>
  <span>
    <a></a>
  </span>
  <span>
    <a></a>
    <a></a>
  </span>
</div>

把上诉 dom 构造转成上面的 JSON 格局

{
  tag: 'DIV',
  children: [
    {
      tag: 'SPAN',
      children: [{ tag: 'A', children: [] }
      ]
    },
    {
      tag: 'SPAN',
      children: [{ tag: 'A', children: [] },
        {tag: 'A', children: [] }
      ]
    }
  ]
}

实现代码如下:

function dom2Json(domtree) {let obj = {};
  obj.name = domtree.tagName;
  obj.children = [];
  domtree.childNodes.forEach((child) => obj.children.push(dom2Json(child)));
  return obj;
}

扩大思考: 如果给定的不是一个 Dom 树结构 而是一段 html 字符串 该如何解析?

那么这个问题就相似 Vue 的模板编译原理 咱们能够利用正则 匹配 html 字符串 遇到开始标签 完结标签和文本 解析结束之后生成对应的 ast 并建设相应的父子关联 一直的 advance 截取残余的字符串 直到 html 全副解析结束

懒加载的特点

  • 缩小无用资源的加载:应用懒加载显著缩小了服务器的压力和流量,同时也减小了浏览器的累赘。
  • 晋升用户体验: 如果同时加载较多图片,可能须要期待的工夫较长,这样影响了用户体验,而应用懒加载就能大大的进步用户体验。
  • 避免加载过多图片而影响其余资源文件的加载:会影响网站利用的失常应用。

公布订阅模式

题目形容: 实现一个公布订阅模式领有 on emit once off 办法

实现代码如下:

class EventEmitter {constructor() {this.events = {};
  }
  // 实现订阅
  on(type, callBack) {if (!this.events[type]) {this.events[type] = [callBack];
    } else {this.events[type].push(callBack);
    }
  }
  // 删除订阅
  off(type, callBack) {if (!this.events[type]) return;
    this.events[type] = this.events[type].filter((item) => {return item !== callBack;});
  }
  // 只执行一次订阅事件
  once(type, callBack) {function fn() {callBack();
      this.off(type, fn);
    }
    this.on(type, fn);
  }
  // 触发事件
  emit(type, ...rest) {this.events[type] &&
      this.events[type].forEach((fn) => fn.apply(this, rest));
  }
}
// 应用如下
// const event = new EventEmitter();

// const handle = (...rest) => {//   console.log(rest);
// };

// event.on("click", handle);

// event.emit("click", 1, 2, 3, 4);

// event.off("click", handle);

// event.emit("click", 1, 2);

// event.once("dbClick", () => {//   console.log(123456);
// });
// event.emit("dbClick");
// event.emit("dbClick");

个别如何产生闭包

  • 返回函数
  • 函数当做参数传递

代码输入问题

window.number = 2;
var obj = {
 number: 3,
 db1: (function(){console.log(this);
   this.number *= 4;
   return function(){console.log(this);
     this.number *= 5;
   }
 })()}
var db1 = obj.db1;
db1();
obj.db1();
console.log(obj.number);     // 15
console.log(window.number);  // 40

这道题目看清起来有点乱,然而实际上是考查 this 指向的:

  1. 执行 db1()时,this 指向全局作用域,所以 window.number 4 = 8,而后执行匿名函数,所以 window.number 5 = 40;
  2. 执行 obj.db1(); 时,this 指向 obj 对象,执行匿名函数,所以 obj.numer * 5 = 15。

过程和线程的区别

  • 过程能够看做独立利用,线程不能
  • 资源:过程是 cpu 资源分配的最小单位(是能领有资源和独立运行的最小单位);线程是 cpu 调度的最小单位(线程是建设在过程的根底上的一次程序运行单位,一个过程中能够有多个线程)。
  • 通信方面:线程间能够通过间接共享同一过程中的资源,而过程通信须要借助 过程间通信。
  • 调度:过程切换比线程切换的开销要大。线程是 CPU 调度的根本单位,线程的切换不会引起过程切换,但某个过程中的线程切换到另一个过程中的线程时,会引起过程切换。
  • 零碎开销:因为创立或撤销过程时,零碎都要为之调配或回收资源,如内存、I/O 等,其开销远大于创立或撤销线程时的开销。同理,在进行过程切换时,波及以后执行过程 CPU 环境还有各种各样状态的保留及新调度过程状态的设置,而线程切换时只需保留和设置大量寄存器内容,开销较小。

怎么解决白屏问题

1、加 loading
2、骨架屏

代码输入后果

Promise.resolve('1')
  .then(res => {console.log(res)
  })
  .finally(() => {console.log('finally')
  })
Promise.resolve('2')
  .finally(() => {console.log('finally2')
      return '我是 finally2 返回的值'
  })
  .then(res => {console.log('finally2 前面的 then 函数', res)
  })

输入后果如下:

1
finally2
finally
finally2 前面的 then 函数 2

.finally()个别用的很少,只有记住以下几点就能够了:

  • .finally()办法不论 Promise 对象最初的状态如何都会执行
  • .finally()办法的回调函数不承受任何的参数,也就是说你在 .finally() 函数中是无奈晓得 Promise 最终的状态是 resolved 还是 rejected
  • 它最终返回的默认会是一个上一次的 Promise 对象值,不过如果抛出的是一个异样则返回异样的 Promise 对象。
  • finally 实质上是 then 办法的特例

.finally()的谬误捕捉:

Promise.resolve('1')
  .finally(() => {console.log('finally1')
    throw new Error('我是 finally 中抛出的异样')
  })
  .then(res => {console.log('finally 前面的 then 函数', res)
  })
  .catch(err => {console.log('捕捉谬误', err)
  })

输入后果为:

'finally1'
'捕捉谬误' Error: 我是 finally 中抛出的异样

代码输入后果

function runAsync (x) {const p = new Promise(r => setTimeout(() => r(x, console.log(x)), 1000))
  return p
}
Promise.race([runAsync(1), runAsync(2), runAsync(3)])
  .then(res => console.log('result:', res))
  .catch(err => console.log(err))

输入后果如下:

1
'result:' 1
2
3

then 只会捕捉第一个胜利的办法,其余的函数尽管还会继续执行,然而不是被 then 捕捉了。

退出移动版