乐趣区

关于javascript:高级前端一面经典手写面试题汇总

图片懒加载

能够给 img 标签对立自定义属性 data-src='default.png',当检测到图片呈现在窗口之后再补充src 属性,此时才会进行图片资源加载。

function lazyload() {const imgs = document.getElementsByTagName('img');
  const len = imgs.length;
  // 视口的高度
  const viewHeight = document.documentElement.clientHeight;
  // 滚动条高度
  const scrollHeight = document.documentElement.scrollTop || document.body.scrollTop;
  for (let i = 0; i < len; i++) {const offsetHeight = imgs[i].offsetTop;
    if (offsetHeight < viewHeight + scrollHeight) {const src = imgs[i].dataset.src;
      imgs[i].src = src;
    }
  }
}

// 能够应用节流优化一下
window.addEventListener('scroll', lazyload);

请实现一个 add 函数,满足以下性能

add(1);             // 1
add(1)(2);      // 3
add(1)(2)(3);// 6
add(1)(2, 3); // 6
add(1, 2)(3); // 6
add(1, 2, 3); // 6
function add(...args) {
  // 在外部申明一个函数,利用闭包的个性保留并收集所有的参数值
  let fn = function(...newArgs) {return add.apply(null, args.concat(newArgs))
  }

  // 利用 toString 隐式转换的个性,当最初执行时隐式转换,并计算最终的值返回
  fn.toString = function() {return args.reduce((total,curr)=> total + curr)
  }

  return fn
}

考点:

  • 应用闭包,同时要对 JavaScript 的作用域链(原型链)有深刻的了解
  • 重写函数的 toSting()办法
// 测试,调用 toString 办法触发求值

add(1).toString();             // 1
add(1)(2).toString();      // 3
add(1)(2)(3).toString();// 6
add(1)(2, 3).toString(); // 6
add(1, 2)(3).toString(); // 6
add(1, 2, 3).toString(); // 6

实现数组的 push 办法

let arr = [];
Array.prototype.push = function() {for( let i = 0 ; i < arguments.length ; i++){this[this.length] = arguments[i] ;
    }
    return this.length;
}

解析 URL Params 为对象

let url = 'http://www.domain.com/?user=anonymous&id=123&id=456&city=%E5%8C%97%E4%BA%AC&enabled';
parseParam(url)
/* 后果{user: 'anonymous',  id: [ 123, 456], // 反复呈现的 key 要组装成数组,能被转成数字的就转成数字类型  city: '北京', // 中文需解码  enabled: true, // 未指定值得 key 约定为 true}*/
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;
}

手写深度比拟 isEqual

思路:深度比拟两个对象,就是要深度比拟对象的每一个元素。=> 递归

  • 递归退出条件:

    • 被比拟的是两个值类型变量,间接用“===”判断
    • 被比拟的两个变量之一为null,直接判断另一个元素是否也为null
  • 提前结束递推:

    • 两个变量 keys 数量不同
    • 传入的两个参数是同一个变量
  • 递推工作:深度比拟每一个key
function isEqual(obj1, obj2){
    // 其中一个为值类型或 null
    if(!isObject(obj1) || !isObject(obj2)){return obj1 === obj2;}

    // 判断是否两个参数是同一个变量
    if(obj1 === obj2){return true;}

    // 判断 keys 数是否相等
    const obj1Keys = Object.keys(obj1);
    const obj2Keys = Object.keys(obj2);
    if(obj1Keys.length !== obj2Keys.length){return false;}

    // 深度比拟每一个 key
    for(let key in obj1){if(!isEqual(obj1[key], obj2[key])){return false;}
    }

    return true;
}

实现千位分隔符

// 保留三位小数
parseToMoney(1234.56); // return '1,234.56'
parseToMoney(123456789); // return '123,456,789'
parseToMoney(1087654.321); // return '1,087,654.321'

function parseToMoney(num) {num = parseFloat(num.toFixed(3));
  let [integer, decimal] = String.prototype.split.call(num, '.');
  integer = integer.replace(/\d(?=(\d{3})+$)/g, '$&,');
  return integer + '.' + (decimal ? decimal : '');
}

正则表达式(使用了正则的前向申明和反前向申明):

function parseToMoney(str){
    // 仅仅对地位进行匹配
    let re = /(?=(?!\b)(\d{3})+$)/g; 
   return str.replace(re,','); 
}

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

手写 Object.create

思路:将传入的对象作为原型

function create(obj) {function F() {}
  F.prototype = obj
  return new F()}

实现一个拖拽

<style>
  html, body {
    margin: 0;
    height: 100%;
  }
  #box {
    width: 100px;
    height: 100px;
    background-color: red;
    position: absolute;
    top: 100px;
    left: 100px;
  }
</style>
<div id="box"></div>
window.onload = function () {var box = document.getElementById('box');
  box.onmousedown = function (ev) {
    var oEvent = ev || window.event; // 兼容火狐, 火狐下没有 window.event
    var distanceX = oEvent.clientX - box.offsetLeft; // 鼠标到可视区右边的间隔 - box 到页面右边的间隔
    var distanceY = oEvent.clientY - box.offsetTop;
    document.onmousemove = function (ev) {
      var oEvent = ev || window.event;
      var left = oEvent.clientX - distanceX;
      var top = oEvent.clientY - distanceY;
      if (left <= 0) {left = 0;} else if (left >= document.documentElement.clientWidth - box.offsetWidth) {left = document.documentElement.clientWidth - box.offsetWidth;}
      if (top <= 0) {top = 0;} else if (top >= document.documentElement.clientHeight - box.offsetHeight) {top = document.documentElement.clientHeight - box.offsetHeight;}
      box.style.left = left + 'px';
      box.style.top = top + 'px';
    }
    box.onmouseup = function () {
      document.onmousemove = null;
      box.onmouseup = null;
    }
  }
}

实现数组的 flat 办法

function _flat(arr, depth) {if(!Array.isArray(arr) || depth <= 0) {return arr;}
  return arr.reduce((prev, cur) => {if (Array.isArray(cur)) {return prev.concat(_flat(cur, depth - 1))
    } else {return prev.concat(cur);
    }
  }, []);
}

将 js 对象转化为树形构造

// 转换前:source = [{
            id: 1,
            pid: 0,
            name: 'body'
          }, {
            id: 2,
            pid: 1,
            name: 'title'
          }, {
            id: 3,
            pid: 2,
            name: 'div'
          }]
// 转换为: 
tree = [{
          id: 1,
          pid: 0,
          name: 'body',
          children: [{
            id: 2,
            pid: 1,
            name: 'title',
            children: [{
              id: 3,
              pid: 1,
              name: 'div'
            }]
          }
        }]

代码实现:

function jsonToTree(data) {
  // 初始化后果数组,并判断输出数据的格局
  let result = []
  if(!Array.isArray(data)) {return result}
  // 应用 map,将以后对象的 id 与以后对象对应存储起来
  let map = {};
  data.forEach(item => {map[item.id] = item;
  });
  // 
  data.forEach(item => {let parent = map[item.pid];
    if(parent) {(parent.children || (parent.children = [])).push(item);
    } else {result.push(item);
    }
  });
  return result;
}

手写 call 函数

call 函数的实现步骤:

  1. 判断调用对象是否为函数,即便咱们是定义在函数的原型上的,然而可能呈现应用 call 等形式调用的状况。
  2. 判断传入上下文对象是否存在,如果不存在,则设置为 window。
  3. 解决传入的参数,截取第一个参数后的所有参数。
  4. 将函数作为上下文对象的一个属性。
  5. 应用上下文对象来调用这个办法,并保留返回后果。
  6. 删除方才新增的属性。
  7. 返回后果。
// call 函数实现
Function.prototype.myCall = function(context) {
  // 判断调用对象
  if (typeof this !== "function") {console.error("type error");
  }
  // 获取参数
  let args = [...arguments].slice(1),
      result = null;
  // 判断 context 是否传入,如果未传入则设置为 window
  context = context || window;
  // 将调用函数设为对象的办法
  context.fn = this;
  // 调用函数
  result = context.fn(...args);
  // 将属性删除
  delete context.fn;
  return result;
};

实现 JSON.parse

var json = '{"name":"cxk","age":25}';
var obj = eval("(" + json + ")");

此办法属于黑魔法,极易容易被 xss 攻打,还有一种 new Function 大同小异。

Function.prototype.call

call 惟一不同的是,call()办法承受的是一个参数列表

Function.prototype.call = function(context = window, ...args) {if (typeof this !== 'function') {throw new TypeError('Type Error');
  }
  const fn = Symbol('fn');
  context[fn] = this;

  const res = context[fn](...args);
  delete context[fn];
  return res;
}

函数珂里化

指的是将一个承受多个参数的函数 变为 承受一个参数返回一个函数的固定模式,这样便于再次调用,例如 f(1)(2)

经典面试题:实现add(1)(2)(3)(4)=10;add(1)(1,2,3)(2)=9;

function add() {const _args = [...arguments];
  function fn() {_args.push(...arguments);
    return fn;
  }
  fn.toString = function() {return _args.reduce((sum, cur) => sum + cur);
  }
  return fn;
}

Object.is

Object.is解决的次要是这两个问题:

+0 === -0  // true
NaN === NaN // false
const is= (x, y) => {if (x === y) {
    // + 0 和 - 0 应该不相等
    return x !== 0 || y !== 0 || 1/x === 1/y;
  } else {return x !== x && y !== y;}
}

实现 getValue/setValue 函数来获取 path 对应的值

// 示例
var object = {a: [{ b: { c: 3} }] }; // path: 'a[0].b.c'
var array = [{a: { b: [1] } }]; // path: '[0].a.b[0]'

function getValue(target, valuePath, defaultValue) {}

console.log(getValue(object, "a[0].b.c", 0)); // 输入 3
console.log(getValue(array, "[0].a.b[0]", 12)); // 输入 1
console.log(getValue(array, "[0].a.b[0].c", 12)); // 输入 12

实现

/**
 * 测试属性是否匹配
 */
export function testPropTypes(value, type, dev) {const sEnums = ['number', 'string', 'boolean', 'undefined', 'function']; // NaN
  const oEnums = ['Null', 'Object', 'Array', 'Date', 'RegExp', 'Error'];
  const nEnums = ['[object Number]',
    '[object String]',
    '[object Boolean]',
    '[object Undefined]',
    '[object Function]',
    '[object Null]',
    '[object Object]',
    '[object Array]',
    '[object Date]',
    '[object RegExp]',
    '[object Error]',
  ];
  const reg = new RegExp('\\[object (.*?)\\]');

  // 齐全匹配模式,type 应该传递相似格局[object Window] [object HTMLDocument] ...
  if (reg.test(type)) {
    // 排除 nEnums 的 12 种
    if (~nEnums.indexOf(type)) {if (dev === true) {console.warn(value, 'The parameter type belongs to one of 12 types:number string boolean undefined Null Object Array Date RegExp function Error NaN');
      }
    }

    if (Object.prototype.toString.call(value) === type) {return true;}

    return false;
  }
}
const syncVarIterator = {getter: function (obj, key, defaultValue) {
    // 后果变量
    const defaultResult = defaultValue === undefined ? undefined : defaultValue;

    if (testPropTypes(obj, 'Object') === false && testPropTypes(obj, 'Array') === false) {return defaultResult;}

    // 后果变量,临时指向 obj 持有的援用,后续将可能被一直的批改
    let result = obj;

    // 失去晓得值
    try {
      // 解析属性档次序列
      const keyArr = key.split('.');

      // 迭代 obj 对象属性
      for (let i = 0; i < keyArr.length; i++) {
        // 如果第 i 层属性存在对应的值则迭代该属性值
        if (result[keyArr[i]] !== undefined) {result = result[keyArr[i]];

          // 如果不存在则返回未定义
        } else {return defaultResult;}
      }
    } catch (e) {return defaultResult;}

    // 返回获取的后果
    return result;
  },
  setter: function (obj, key, val) {
    // 如果不存在 obj 则返回未定义
    if (testPropTypes(obj, 'Object') === false) {return false;}

    // 后果变量,临时指向 obj 持有的援用,后续将可能被一直的批改
    let result = obj;

    try {
      // 解析属性档次序列
      const keyArr = key.split('.');

      let i = 0;

      // 迭代 obj 对象属性
      for (; i < keyArr.length - 1; i++) {
        // 如果第 i 层属性对应的值不存在,则定义为对象
        if (result[keyArr[i]] === undefined) {result[keyArr[i]] = {};}

        // 如果第 i 层属性对应的值不是对象(Object)的一个实例,则抛出谬误
        if (!(result[keyArr[i]] instanceof Object)) {throw new Error('obj.' + keyArr.splice(0, i + 1).join('.') + 'is not Object');
        }

        // 迭代该层属性值
        result = result[keyArr[i]];
      }

      // 设置属性值
      result[keyArr[i]] = val;

      return true;
    } catch (e) {return false;}
  },
};

应用 promise 来实现

创立 enhancedObject 函数

const enhancedObject = (target) =>
  new Proxy(target, {get(target, property) {if (property in target) {return target[property];
      } else {return searchFor(property, target); // 理论应用时要对 value 值进行复位
      }
    },
  });

let value = null;
function searchFor(property, target) {for (const key of Object.keys(target)) {if (typeof target[key] === "object") {searchFor(property, target[key]);
    } else if (typeof target[property] !== "undefined") {value = target[property];
      break;
    }
  }
  return value;
}

应用 enhancedObject 函数

const data = enhancedObject({
  user: {
    name: "test",
    settings: {theme: "dark",},
  },
});

console.log(data.user.settings.theme); // dark
console.log(data.theme); // dark

以上代码运行后,控制台会输入以下代码:

dark
dark

通过观察以上的输入后果可知,应用 enhancedObject 函数解决过的对象,咱们就能够不便地拜访一般对象外部的深层属性。

reduce 用法汇总

语法

array.reduce(function(total, currentValue, currentIndex, arr), initialValue);
/*
  total: 必须。初始值, 或者计算完结后的返回值。currentValue:必须。以后元素。currentIndex:可选。以后元素的索引;arr:可选。以后元素所属的数组对象。initialValue: 可选。传递给函数的初始值,相当于 total 的初始值。*/

reduceRight() 该办法用法与 reduce() 其实是雷同的,只是遍历的程序相同,它是从数组的最初一项开始,向前遍历到第一项

1. 数组求和

const arr = [12, 34, 23];
const sum = arr.reduce((total, num) => total + num);

// 设定初始值求和
const arr = [12, 34, 23];
const sum = arr.reduce((total, num) => total + num, 10);  // 以 10 为初始值求和


// 对象数组求和
var result = [{ subject: 'math', score: 88},
  {subject: 'chinese', score: 95},
  {subject: 'english', score: 80}
];
const sum = result.reduce((accumulator, cur) => accumulator + cur.score, 0); 
const sum = result.reduce((accumulator, cur) => accumulator + cur.score, -10);  // 总分扣除 10 分

2. 数组最大值

const a = [23,123,342,12];
const max = a.reduce((pre,next)=>pre>cur?pre:cur,0); // 342

3. 数组转对象

var streams = [{name: '技术', id: 1}, {name: '设计', id: 2}];
var obj = streams.reduce((accumulator, cur) => {accumulator[cur.id] = cur; return accumulator;}, {});

4. 扁平一个二维数组

var arr = [[1, 2, 8], [3, 4, 9], [5, 6, 10]];
var res = arr.reduce((x, y) => x.concat(y), []);

5. 数组去重

实现的基本原理如下:① 初始化一个空数组
② 将须要去重解决的数组中的第 1 项在初始化数组中查找,如果找不到(空数组中必定找不到),就将该项增加到初始化数组中
③ 将须要去重解决的数组中的第 2 项在初始化数组中查找,如果找不到,就将该项持续增加到初始化数组中
④ ……
⑤ 将须要去重解决的数组中的第 n 项在初始化数组中查找,如果找不到,就将该项持续增加到初始化数组中
⑥ 将这个初始化数组返回
var newArr = arr.reduce(function (prev, cur) {prev.indexOf(cur) === -1 && prev.push(cur);
    return prev;
},[]);

6. 对象数组去重

const dedup = (data, getKey = () => {}) => {const dateMap = data.reduce((pre, cur) => {const key = getKey(cur)
        if (!pre[key]) {pre[key] = cur
        }
        return pre
    }, {})
    return Object.values(dateMap)
}

7. 求字符串中字母呈现的次数

const str = 'sfhjasfjgfasjuwqrqadqeiqsajsdaiwqdaklldflas-cmxzmnha';

const res = str.split('').reduce((pre,next)=>{pre[next] ? pre[next]++ : pre[next] = 1
 return pre 
},{})
// 后果
-: 1
a: 8
c: 1
d: 4
e: 1
f: 4
g: 1
h: 2
i: 2
j: 4
k: 1
l: 3
m: 2
n: 1
q: 5
r: 1
s: 6
u: 1
w: 2
x: 1
z: 1

8. compose 函数

redux compose 源码实现

function compose(...funs) {if (funs.length === 0) {return arg => arg;}
    if (funs.length === 1) {return funs[0];
    }
    return funs.reduce((a, b) => (...arg) => a(b(...arg)))
}

实现 bind 办法

bind 的实现比照其余两个函数稍微地简单了一点,波及到参数合并(相似函数柯里化),因为 bind 须要返回一个函数,须要判断一些边界问题,以下是 bind 的实现

  • bind 返回了一个函数,对于函数来说有两种形式调用,一种是间接调用,一种是通过 new 的形式,咱们先来说间接调用的形式
  • 对于间接调用来说,这里抉择了 apply 的形式实现,然而对于参数须要留神以下状况:因为 bind 能够实现相似这样的代码 f.bind(obj, 1)(2),所以咱们须要将两边的参数拼接起来
  • 最初来说通过 new 的形式,对于 new 的状况来说,不会被任何形式扭转 this,所以对于这种状况咱们须要疏忽传入的 this

简洁版本

  • 对于一般函数,绑定 this 指向
  • 对于构造函数,要保障原函数的原型对象上的属性不能失落
Function.prototype.myBind = function(context = window, ...args) {
  // this 示意调用 bind 的函数
  let self = this;

  // 返回了一个函数,...innerArgs 为理论调用时传入的参数
  let fBound = function(...innerArgs) {//this instanceof fBound 为 true 示意构造函数的状况。如 new func.bind(obj)
      // 当作为构造函数时,this 指向实例,此时 this instanceof fBound 后果为 true,能够让实例取得来自绑定函数的值
      // 当作为一般函数时,this 指向 window,此时后果为 false,将绑定函数的 this 指向 context
      return self.apply(
        this instanceof fBound ? this : context, 
        args.concat(innerArgs)
      );
  }

  // 如果绑定的是构造函数,那么须要继承构造函数原型属性和办法:保障原函数的原型对象上的属性不失落
  // 实现继承的形式: 应用 Object.create
  fBound.prototype = Object.create(this.prototype);
  return fBound;
}
// 测试用例

function Person(name, age) {console.log('Person name:', name);
  console.log('Person age:', age);
  console.log('Person this:', this); // 构造函数 this 指向实例对象
}

// 构造函数原型的办法
Person.prototype.say = function() {console.log('person say');
}

// 一般函数
function normalFun(name, age) {console.log('一般函数 name:', name); 
  console.log('一般函数 age:', age); 
  console.log('一般函数 this:', this);  // 一般函数 this 指向绑定 bind 的第一个参数 也就是例子中的 obj
}


var obj = {
  name: 'poetries',
  age: 18
}

// 先测试作为结构函数调用
var bindFun = Person.myBind(obj, 'poetry1') // undefined
var a = new bindFun(10) // Person name: poetry1、Person age: 10、Person this: fBound {}
a.say() // person say

// 再测试作为一般函数调用
var bindNormalFun = normalFun.myBind(obj, 'poetry2') // undefined
bindNormalFun(12) // 一般函数 name: poetry2 一般函数 age: 12 一般函数 this: {name: 'poetries', age: 18}

留神:bind之后不能再次批改 this 的指向,bind屡次后执行,函数 this 还是指向第一次 bind 的对象

实现一个 JSON.parse

JSON.parse(text[, reviver])

用来解析 JSON 字符串,结构由字符串形容的 JavaScript 值或对象。提供可选的 reviver 函数用以在返回之前对所失去的对象执行变换(操作)

第一种:间接调用 eval

function jsonParse(opt) {return eval('(' + opt + ')');
}
jsonParse(jsonStringify({x : 5}))
// Object {x: 5}
jsonParse(jsonStringify([1, "false", false]))
// [1, "false", falsr]
jsonParse(jsonStringify({b: undefined}))
// Object {b: "undefined"}

防止在不必要的状况下应用 evaleval() 是一个危险的函数,他执行的代码领有着执行者的权力。如果你用 eval() 运行的字符串代码被歹意方(不怀好意的人)操控批改,您最终可能会在您的网页 / 扩大程序的权限下,在用户计算机上运行恶意代码。它会执行 JS 代码,有 XSS 破绽。

如果你只想记这个办法,就得对参数 json 做校验。

var rx_one = /^[\],:{}\s]*$/;
var rx_two = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g;
var rx_three = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g;
var rx_four = /(?:^|:|,)(?:\s*\[)+/g;
if (
    rx_one.test(
        json
            .replace(rx_two, "@")
            .replace(rx_three, "]")
            .replace(rx_four, "")
    )
) {var obj = eval("(" +json + ")");
}

第二种:Function

外围:Function 与 eval 有雷同的字符串参数个性

var func = new Function(arg1, arg2, ..., functionBody);

在转换 JSON 的理论利用中,只须要这么做

var jsonStr = '{"age": 20,"name":"jack"}'
var json = (new Function('return' + jsonStr))();

evalFunction都有着动静编译 js 代码的作用,然而在理论的编程中并不举荐应用

手写 Promise.then

then 办法返回一个新的 promise 实例,为了在 promise 状态发生变化时(resolve / reject 被调用时)再执行 then 里的函数,咱们应用一个 callbacks 数组先把传给 then 的函数暂存起来,等状态扭转时再调用。

那么,怎么保障后一个 **then** 里的办法在前一个 **then**(可能是异步)完结之后再执行呢? 咱们能够将传给 then 的函数和新 promiseresolve 一起 push 到前一个 promisecallbacks 数组中,达到承前启后的成果:

  • 承前:以后一个 promise 实现后,调用其 resolve 变更状态,在这个 resolve 里会顺次调用 callbacks 里的回调,这样就执行了 then 里的办法了
  • 启后:上一步中,当 then 里的办法执行实现后,返回一个后果,如果这个后果是个简略的值,就间接调用新 promiseresolve,让其状态变更,这又会顺次调用新 promisecallbacks 数组里的办法,周而复始。。如果返回的后果是个 promise,则须要等它实现之后再触发新 promiseresolve,所以能够在其后果的 then 里调用新 promiseresolve
then(onFulfilled, onReject){
    // 保留前一个 promise 的 this
    const self = this; 
    return new MyPromise((resolve, reject) => {
      // 封装前一个 promise 胜利时执行的函数
      let fulfilled = () => {
        try{const result = onFulfilled(self.value); // 承前
          return result instanceof MyPromise? result.then(resolve, reject) : resolve(result); // 启后
        }catch(err){reject(err)
        }
      }
      // 封装前一个 promise 失败时执行的函数
      let rejected = () => {
        try{const result = onReject(self.reason);
          return result instanceof MyPromise? result.then(resolve, reject) : reject(result);
        }catch(err){reject(err)
        }
      }
      switch(self.status){
        case PENDING: 
          self.onFulfilledCallbacks.push(fulfilled);
          self.onRejectedCallbacks.push(rejected);
          break;
        case FULFILLED:
          fulfilled();
          break;
        case REJECT:
          rejected();
          break;
      }
    })
   }

留神:

  • 间断多个 then 里的回调办法是同步注册的,但注册到了不同的 callbacks 数组中,因为每次 then 都返回新的 promise 实例(参考下面的例子和图)
  • 注册实现后开始执行构造函数中的异步事件,异步实现之后顺次调用 callbacks 数组中提前注册的回调
退出移动版