关于javascript:腾讯前端高频面试题合集

45次阅读

共计 18284 个字符,预计需要花费 46 分钟才能阅读完成。

实现 JSONP 跨域

JSONP 外围原理script 标签不受同源策略束缚,所以能够用来进行跨域申请,长处是兼容性好,然而只能用于 GET 申请;

实现

const jsonp = (url, params, callbackName) => {const generateUrl = () => {
        let dataSrc = "";
        for(let key in params) {if(params.hasOwnProperty(key)) {dataSrc += `${key}=${params[key]}&`
            }
        }
        dataSrc += `callback=${callbackName}`;
        return `${url}?${dataSrc}`;
    }
    return new Promise((resolve, reject) => {const scriptEle = document.createElement('script');
        scriptEle.src = generateUrl();
        document.body.appendChild(scriptEle);
        window[callbackName] = data => {resolve(data);
            document.removeChild(scriptEle);
        }
    });
}

Unicode、UTF-8、UTF-16、UTF-32 的区别?

(1)Unicode

在说 Unicode 之前须要先理解一下 ASCII 码:ASCII 码(American Standard Code for Information Interchange)称为美国规范信息替换码。

  • 它是基于拉丁字母的一套电脑编码零碎。
  • 它定义了一个用于代表常见字符的字典。
  • 它蕴含了 ”A-Z”(蕴含大小写),数据 ”0-9″ 以及一些常见的符号。
  • 它是专门为英语而设计的,有 128 个编码,对其余语言无能为力

ASCII码能够示意的编码无限,要想示意其余语言的编码,还是要应用 Unicode 来示意,能够说 UnicodeASCII 的超集。

Unicode全称 Unicode Translation Format,又叫做对立码、万国码、繁多码。Unicode 是为了解决传统的字符编码方案的局限而产生的,它为每种语言中的每个字符设定了对立并且惟一的二进制编码,以满足跨语言、跨平台进行文本转换、解决的要求。

Unicode的实现形式(也就是编码方式)有很多种,常见的是 UTF-8UTF-16UTF-32USC-2

(2)UTF-8

UTF-8是应用最宽泛的 Unicode 编码方式,它是一种可变长的编码方式,能够是 1—4 个字节不等,它能够齐全兼容 ASCII 码的 128 个字符。

留神: UTF-8 是一种编码方式,Unicode是一个字符汇合。

UTF-8的编码规定:

  • 对于 单字节 的符号,字节的第一位为 0,前面的 7 位为这个字符的 Unicode 编码,因而对于英文字母,它的 Unicode 编码和 ACSII 编码一样。
  • 对于 n 字节 的符号,第一个字节的前 n 位都是 1,第 n + 1 位设为 0,前面字节的前两位一律设为 10,剩下的没有提及的二进制位,全副为这个符号的 Unicode 码。

来看一下具体的 Unicode 编号范畴与对应的 UTF-8 二进制格局:

编码范畴(编号对应的十进制数) 二进制格局
0x00—0x7F(0-127) 0xxxxxxx
0x80—0x7FF(128-2047) 110xxxxx 10xxxxxx
0x800—0xFFFF(2048-65535) 1110xxxx 10xxxxxx 10xxxxxx
0x10000—0x10FFFF(65536 以上) 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

那该如何通过具体的 Unicode 编码,进行具体的 UTF-8 编码呢?步骤如下:

  • 找到该 Unicode 编码的所在的编号范畴,进而找到与之对应的二进制格局
  • Unicode 编码转换为二进制数(去掉最高位的 0)
  • 将二进制数从右往左一次填入二进制格局的 X 中,如果有 X 未填,就设为 0

来看一个理论的例子:
”字的Unicode 编码是:0x9A6C,整数编号是39532(1)首选确定了该字符在第三个范畴内,它的格局是 1110xxxx 10xxxxxx 10xxxxxx(2)39532 对应的二进制数为1001 1010 0110 1100(3)将二进制数填入 X 中,后果是:11101001 10101001 10101100

(3)UTF-16

1. 立体的概念

在理解 UTF-16 之前,先看一下 立体 的概念:Unicode编码中有很多很多的字符,它并不是一次性定义的,而是分区进行定义的,每个区寄存 65536(216)个字符,这称为一个 立体,目前总共有 17 个立体。

最后面的一个立体称为 根本立体 ,它的码点从0 — 216-1,写成 16 进制就是U+0000 — U+FFFF,那剩下的 16 个立体就是 辅助立体,码点范畴是 U+10000—U+10FFFF

2. UTF-16 概念:

UTF-16也是 Unicode 编码集的一种编码模式,把 Unicode 字符集的形象码位映射为 16 位长的整数(即码元)的序列,用于数据存储或传递。Unicode字符的码位须要 1 个或者 2 个 16 位长的码元来示意,因而 UTF-16 也是用变长字节示意的。

3. UTF-16 编码规定:

  • 编号在 U+0000—U+FFFF 的字符(罕用字符集),间接用两个字节示意。
  • 编号在 U+10000—U+10FFFF 之间的字符,须要用四个字节示意。

4. 编码辨认

那么问题来了,当遇到两个字节时,怎么晓得是把它当做一个字符还是和前面的两个字节一起当做一个字符呢?

UTF-16 编码必定也思考到了这个问题,在根本立体内,从 U+D800 — U+DFFF 是一个空段,也就是说这个区间的码点不对应任何的字符,因而这些空段就能够用来映射辅助立体的字符。

辅助立体共有 220 个字符位,因而示意这些字符至多须要 20 个二进制位。UTF-16 将这 20 个二进制位分成两半,前 10 位映射在 U+D800 — U+DBFF,称为 高位 (H),后 10 位映射在 U+DC00 — U+DFFF,称为 低位(L)。这就相当于,将一个辅助立体的字符拆成了两个根本立体的字符来示意。

因而,当遇到两个字节时,发现它的码点在 U+D800 —U+DBFF之间,就能够晓得,它前面的两个字节的码点应该在 U+DC00 — U+DFFF 之间,这四个字节必须放在一起进行解读。

5. 举例说明

以 “𡠀” 字为例,它的 Unicode 码点为 0x21800,该码点超出了根本立体的范畴,因而须要用四个字节来示意,步骤如下:

  • 首先计算超出局部的后果:0x21800 - 0x10000
  • 将下面的计算结果转为 20 位的二进制数,有余 20 位就在后面补 0,后果为:0001000110 0000000000
  • 将失去的两个 10 位二进制数别离对应到两个区间中
  • U+D800 对应的二进制数为 1101100000000000,将 0001000110 填充在它的后 10 个二进制位,失去 1101100001000110,转成 16 进制数为 0xD846。同理,低位为 0xDC00,所以这个字的UTF-16 编码为 0xD846 0xDC00

(4)UTF-32

UTF-32 就是字符所对应编号的整数二进制模式,每个字符占四个字节,这个是间接进行转换的。该编码方式占用的贮存空间较多,所以应用较少。

比方“”字的 Unicode 编号是:U+9A6C,整数编号是39532,间接转化为二进制:1001 1010 0110 1100,这就是它的 UTF-32 编码。

(5)总结

Unicode、UTF-8、UTF-16、UTF-32 有什么区别?

  • Unicode 是编码字符集(字符集),而 UTF-8UTF-16UTF-32 是字符集编码(编码规定);
  • UTF-16 应用变长码元序列的编码方式,相较于定长码元序列的 UTF-32 算法更简单,甚至比同样是变长码元序列的 UTF-8 也更为简单,因为其引入了独特的 代理对 这样的代理机制;
  • UTF-8须要判断每个字节中的结尾标记信息,所以如果某个字节在传送过程中出错了,就会导致前面的字节也会解析出错;而 UTF-16 不会判断结尾标记,即便错也只会错一个字符,所以容错能力教强;
  • 如果字符内容全副英文或英文与其余文字混合,但英文占绝大部分,那么用 UTF-8 就比 UTF-16 节俭了很多空间;而如果字符内容全副是中文这样相似的字符或者混合字符中中文占绝大多数,那么 UTF-16 就占优势了,能够节俭很多空间;

script 标签中 defer 和 async 的区别

如果没有 defer 或 async 属性,浏览器会立刻加载并执行相应的脚本。它不会期待后续加载的文档元素,读取到就会开始加载和执行,这样就阻塞了后续文档的加载。

defer 和 async 属性都是去异步加载内部的 JS 脚本文件,它们都不会阻塞页面的解析,其区别如下:

  • 执行程序: 多个带 async 属性的标签,不能保障加载的程序;多个带 defer 属性的标签,依照加载程序执行;
  • 脚本是否并行执行:async 属性,示意 后续文档的加载和执行与 js 脚本的加载和执行是并行进行的 ,即异步执行;defer 属性,加载后续文档的过程和 js 脚本的加载(此时仅加载不执行) 是并行进行的(异步),js 脚本须要等到文档所有元素解析实现之后才执行,DOMContentLoaded 事件触发执行之前。

Promise 以及相干办法的实现

题目形容: 手写 Promise 以及 Promise.all Promise.race 的实现

实现代码如下:

class Mypromise {constructor(fn) {
    // 示意状态
    this.state = "pending";
    // 示意 then 注册的胜利函数
    this.successFun = [];
    // 示意 then 注册的失败函数
    this.failFun = [];

    let resolve = (val) => {
      // 放弃状态扭转不可变(resolve 和 reject 只准触发一种)if (this.state !== "pending") return;

      // 胜利触发机会  扭转状态 同时执行在 then 注册的回调事件
      this.state = "success";
      // 为了保障 then 事件先注册(次要是思考在 promise 外面写同步代码)promise 标准 这里为模仿异步
      setTimeout(() => {
        // 执行以后事件外面所有的注册函数
        this.successFun.forEach((item) => item.call(this, val));
      });
    };

    let reject = (err) => {if (this.state !== "pending") return;
      // 失败触发机会  扭转状态 同时执行在 then 注册的回调事件
      this.state = "fail";
      // 为了保障 then 事件先注册(次要是思考在 promise 外面写同步代码)promise 标准 这里模仿异步
      setTimeout(() => {this.failFun.forEach((item) => item.call(this, err));
      });
    };
    // 调用函数
    try {fn(resolve, reject);
    } catch (error) {reject(error);
    }
  }

  // 实例办法 then

  then(resolveCallback, rejectCallback) {
    // 判断回调是否是函数
    resolveCallback =
      typeof resolveCallback !== "function" ? (v) => v : resolveCallback;
    rejectCallback =
      typeof rejectCallback !== "function"
        ? (err) => {throw err;}
        : rejectCallback;
    // 为了放弃链式调用  持续返回 promise
    return new Mypromise((resolve, reject) => {
      // 将回调注册到 successFun 事件汇合外面去
      this.successFun.push((val) => {
        try {
          //    执行回调函数
          let x = resolveCallback(val);
          //(最难的一点)// 如果回调函数后果是一般值 那么就 resolve 进来给下一个 then 链式调用  如果是一个 promise 对象(代表又是一个异步)那么调用 x 的 then 办法 将 resolve 和 reject 传进去 等到 x 外部的异步 执行结束的时候(状态实现)就会主动执行传入的 resolve 这样就管制了链式调用的程序
          x instanceof Mypromise ? x.then(resolve, reject) : resolve(x);
        } catch (error) {reject(error);
        }
      });

      this.failFun.push((val) => {
        try {
          //    执行回调函数
          let x = rejectCallback(val);
          x instanceof Mypromise ? x.then(resolve, reject) : reject(x);
        } catch (error) {reject(error);
        }
      });
    });
  }
  // 静态方法
  static all(promiseArr) {let result = [];
    // 申明一个计数器 每一个 promise 返回就加一
    let count = 0;
    return new Mypromise((resolve, reject) => {for (let i = 0; i < promiseArr.length; i++) {
      // 这里用 Promise.resolve 包装一下 避免不是 Promise 类型传进来
        Promise.resolve(promiseArr[i]).then((res) => {// 这里不能间接 push 数组  因为要管制程序一一对应(感激评论区斧正)
            result[i] = res;
            count++;
            // 只有全副的 promise 执行胜利之后才 resolve 进来
            if (count === promiseArr.length) {resolve(result);
            }
          },
          (err) => {reject(err);
          }
        );
      }
    });
  }
  // 静态方法
  static race(promiseArr) {return new Mypromise((resolve, reject) => {for (let i = 0; i < promiseArr.length; i++) {Promise.resolve(promiseArr[i]).then((res) => {
            //promise 数组只有有任何一个 promise 状态变更  就能够返回
            resolve(res);
          },
          (err) => {reject(err);
          }
        );
      }
    });
  }
}

// 应用
// let promise1 = new Mypromise((resolve, reject) => {//   setTimeout(() => {//     resolve(123);
//   }, 2000);
// });
// let promise2 = new Mypromise((resolve, reject) => {//   setTimeout(() => {//     resolve(1234);
//   }, 1000);
// });

// Mypromise.all([promise1,promise2]).then(res=>{//   console.log(res);
// })

// Mypromise.race([promise1, promise2]).then(res => {//   console.log(res);
// });

// promise1
//   .then(
//     res => {//       console.log(res); // 过两秒输入 123
//       return new Mypromise((resolve, reject) => {//         setTimeout(() => {//           resolve("success");
//         }, 1000);
//       });
//     },
//     err => {//       console.log(err);
//     }
//   )
//   .then(
//     res => {//       console.log(res); // 再过一秒输入 success
//     },
//     err => {//       console.log(err);
//     }
//   );

扩大思考: 如何勾销 promise

Promise.race()办法能够用来竞争 Promise
能够借助这个个性 本人包装一个 空的 Promise 与要发动的 Promise 来实现

function wrap(pro) {let obj = {};
  // 结构一个新的 promise 用来竞争
  let p1 = new Promise((resolve, reject) => {
    obj.resolve = resolve;
    obj.reject = reject;
  });

  obj.promise = Promise.race([p1, pro]);
  return obj;
}

let testPro = new Promise((resolve, reject) => {setTimeout(() => {resolve(123);
  }, 1000);
});

let wrapPro = wrap(testPro);
wrapPro.promise.then((res) => {console.log(res);
});
wrapPro.resolve("被拦挡了");

树形构造转成列表

题目形容:

[
    {
        id: 1,
        text: '节点 1',
        parentId: 0,
        children: [
            {
                id:2,
                text: '节点 1_1',
                parentId:1
            }
        ]
    }
]
转成
[
    {
        id: 1,
        text: '节点 1',
        parentId: 0 // 这里用 0 示意为顶级节点
    },
    {
        id: 2,
        text: '节点 1_1',
        parentId: 1 // 通过这个字段来确定子父级
    }
    ...
]

实现代码如下:

function treeToList(data) {let res = [];
  const dfs = (tree) => {tree.forEach((item) => {if (item.children) {dfs(item.children);
        delete item.children;
      }
      res.push(item);
    });
  };
  dfs(data);
  return res;
}

Object.is 实现

题目形容:

Object.is 不会转换被比拟的两个值的类型,这点和 === 更为类似,他们之间也存在一些区别。1. NaN 在 === 中是不相等的,而在 Object.is 中是相等的
    2. + 0 和 - 0 在 === 中是相等的,而在 Object.is 中是不相等的

实现代码如下:

Object.is = function (x, y) {if (x === y) {
    // 当前情况下,只有一种状况是非凡的,即 +0 -0
    // 如果 x !== 0,则返回 true
    // 如果 x === 0,则须要判断 + 0 和 -0,则能够间接应用 1/+0 === Infinity 和 1/-0 === -Infinity 来进行判断
    return x !== 0 || 1 / x === 1 / y;
  }

  // x !== y 的状况下,只须要判断是否为 NaN,如果 x!==x,则阐明 x 是 NaN,同理 y 也一样
  // x 和 y 同时为 NaN 时,返回 true
  return x !== x && y !== y;
};

instanceof

作用:判断对象的具体类型。能够区别 arrayobjectnullobject 等。

语法A instanceof B

如何判断的?: 如果 B 函数的显式原型对象在 A 对象的原型链上,返回true,否则返回false

留神:如果检测原始值,则始终返回 false

实现:

function myinstanceof(left, right) {
    // 根本数据类型都返回 false,留神 typeof 函数 返回 "function"
    if((typeof left !== "object" && typeof left !== "function") || left === null) return false;
    let leftPro = left.__proto__;  // 取右边的(隐式)原型 __proto__
    // left.__proto__ 等价于 Object.getPrototypeOf(left)
    while(true) {
        // 判断是否到原型链顶端
        if(leftPro === null) return false;
        // 判断左边的显式原型 prototype 对象是否在右边的原型链上
        if(leftPro === right.prototype) return true;
        // 原型链查找
        leftPro = leftPro.__proto__;
    }
}

偏函数

什么是偏函数?偏函数就是将一个 n 参的函数转换成固定 x 参的函数,残余参数(n – x)将在下次调用全副传入。举个例子:

function add(a, b, c) {return a + b + c}
let partialAdd = partial(add, 1)
partialAdd(2, 3)

发现没有,其实偏函数和函数柯里化有点像,所以依据函数柯里化的实现,可能能很快写出偏函数的实现:

function partial(fn, ...args) {return (...arg) => {return fn(...args, ...arg)
    }
}

如上这个性能比较简单,当初咱们心愿偏函数能和柯里化一样能实现占位性能,比方:

function clg(a, b, c) {console.log(a, b, c)
}
let partialClg = partial(clg, '_', 2)
partialClg(1, 3)  // 顺次打印:1, 2, 3

_ 占的位其实就是 1 的地位。相当于:partial(clg, 1, 2),而后 partialClg(3)。明确了原理,咱们就来写实现:

function partial(fn, ...args) {return (...arg) => {args[index] = 
        return fn(...args, ...arg)
    }
}

LRU 算法

实现代码如下:

//  一个 Map 对象在迭代时会依据对象中元素的插入程序来进行
// 新增加的元素会被插入到 map 的开端,整个栈倒序查看
class LRUCache {constructor(capacity) {this.secretKey = new Map();
    this.capacity = capacity;
  }
  get(key) {if (this.secretKey.has(key)) {let tempValue = this.secretKey.get(key);
      this.secretKey.delete(key);
      this.secretKey.set(key, tempValue);
      return tempValue;
    } else return -1;
  }
  put(key, value) {
    // key 存在,仅批改值
    if (this.secretKey.has(key)) {this.secretKey.delete(key);
      this.secretKey.set(key, value);
    }
    // key 不存在,cache 未满
    else if (this.secretKey.size < this.capacity) {this.secretKey.set(key, value);
    }
    // 增加新 key,删除旧 key
    else {this.secretKey.set(key, value);
      // 删除 map 的第一个元素,即为最长未应用的
      this.secretKey.delete(this.secretKey.keys().next().value);
    }
  }
}
// let cache = new LRUCache(2);
// cache.put(1, 1);
// cache.put(2, 2);
// console.log("cache.get(1)", cache.get(1))// 返回  1
// cache.put(3, 3);// 该操作会使得密钥 2 作废
// console.log("cache.get(2)", cache.get(2))// 返回 -1 (未找到)
// cache.put(4, 4);// 该操作会使得密钥 1 作废
// console.log("cache.get(1)", cache.get(1))// 返回 -1 (未找到)
// console.log("cache.get(3)", cache.get(3))// 返回  3
// console.log("cache.get(4)", cache.get(4))// 返回  4

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

图片懒加载

与一般的图片懒加载不同,如下这个多做了 2 个精心解决:

  • 图片全副加载实现后移除事件监听;
  • 加载完的图片,从 imgList 移除;
let imgList = [...document.querySelectorAll('img')]
let length = imgList.length

// 修改谬误,须要加上自执行
- const imgLazyLoad = function() {+ const imgLazyLoad = (function() {
    let count = 0

   return function() {let deleteIndexList = []
        imgList.forEach((img, index) => {let rect = img.getBoundingClientRect()
            if (rect.top < window.innerHeight) {
                img.src = img.dataset.src
                deleteIndexList.push(index)
                count++
                if (count === length) {document.removeEventListener('scroll', imgLazyLoad)
                }
            }
        })
        imgList = imgList.filter((img, index) => !deleteIndexList.includes(index))
   }
- }
+ })()

// 这里最好加上防抖解决
document.addEventListener('scroll', imgLazyLoad)

实现有并行限度的 Promise 调度器

题目形容:JS 实现一个带并发限度的异步调度器 Scheduler,保障同时运行的工作最多有两个

 addTask(1000,"1");
 addTask(500,"2");
 addTask(300,"3");
 addTask(400,"4");
 的输入程序是:2 3 1 4

 整个的残缺执行流程:一开始 1、2 两个工作开始执行
500ms 时,2 工作执行结束,输入 2,工作 3 开始执行
800ms 时,3 工作执行结束,输入 3,工作 4 开始执行
1000ms 时,1 工作执行结束,输入 1,此时只剩下 4 工作在执行
1200ms 时,4 工作执行结束,输入 4

实现代码如下:

class Scheduler {constructor(limit) {this.queue = [];
    this.maxCount = limit;
    this.runCounts = 0;
  }
  add(time, order) {const promiseCreator = () => {return new Promise((resolve, reject) => {setTimeout(() => {console.log(order);
          resolve();}, time);
      });
    };
    this.queue.push(promiseCreator);
  }
  taskStart() {for (let i = 0; i < this.maxCount; i++) {this.request();
    }
  }
  request() {if (!this.queue || !this.queue.length || this.runCounts >= this.maxCount) {return;}
    this.runCounts++;
    this.queue
      .shift()()
      .then(() => {
        this.runCounts--;
        this.request();});
  }
}
const scheduler = new Scheduler(2);
const addTask = (time, order) => {scheduler.add(time, order);
};
addTask(1000, "1");
addTask(500, "2");
addTask(300, "3");
addTask(400, "4");
scheduler.taskStart();

具体阐明 Event loop

家喻户晓 JS 是门非阻塞单线程语言,因为在最后 JS 就是为了和浏览器交互而诞生的。如果 JS 是门多线程的语言话,咱们在多个线程中解决 DOM 就可能会产生问题(一个线程中新加节点,另一个线程中删除节点),当然能够引入读写锁解决这个问题。

JS 在执行的过程中会产生执行环境,这些执行环境会被程序的退出到执行栈中。如果遇到异步的代码,会被挂起并退出到 Task(有多种 task)队列中。一旦执行栈为空,Event Loop 就会从 Task 队列中拿出须要执行的代码并放入执行栈中执行,所以实质上来说 JS 中的异步还是同步行为。

console.log('script start');

setTimeout(function() {console.log('setTimeout');
}, 0);

console.log('script end');

以上代码尽管 setTimeout 延时为 0,其实还是异步。这是因为 HTML5 标准规定这个函数第二个参数不得小于 4 毫秒,有余会主动减少。所以 setTimeout 还是会在 script end 之后打印。

不同的工作源会被调配到不同的 Task 队列中,工作源能够分为 微工作(microtask)和 宏工作(macrotask)。在 ES6 标准中,microtask 称为 jobs,macrotask 称为 task

console.log('script start');

setTimeout(function() {console.log('setTimeout');
}, 0);

new Promise((resolve) => {console.log('Promise')
    resolve()}).then(function() {console.log('promise1');
}).then(function() {console.log('promise2');
});

console.log('script end');
// script start => Promise => script end => promise1 => promise2 => setTimeout

以上代码尽管 setTimeout 写在 Promise 之前,然而因为 Promise 属于微工作而 setTimeout 属于宏工作,所以会有以上的打印。

微工作包含 process.nextTickpromiseObject.observeMutationObserver

宏工作包含 scriptsetTimeoutsetIntervalsetImmediateI/OUI rendering

很多人有个误区,认为微工作快于宏工作,其实是谬误的。因为宏工作中包含了 script,浏览器会先执行一个宏工作,接下来有异步代码的话就先执行微工作。

所以正确的一次 Event loop 程序是这样的

  1. 执行同步代码,这属于宏工作
  2. 执行栈为空,查问是否有微工作须要执行
  3. 执行所有微工作
  4. 必要的话渲染 UI
  5. 而后开始下一轮 Event loop,执行宏工作中的异步代码

通过上述的 Event loop 程序可知,如果宏工作中的异步代码有大量的计算并且须要操作 DOM 的话,为了更快的 界面响应,咱们能够把操作 DOM 放入微工作中。

Node 中的 Event loop

Node 中的 Event loop 和浏览器中的不雷同。

Node 的 Event loop 分为 6 个阶段,它们会依照程序重复运行

┌───────────────────────┐
┌─>│        timers         │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     I/O callbacks     │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     idle, prepare     │
│  └──────────┬────────────┘      ┌───────────────┐
│  ┌──────────┴────────────┐      │   incoming:   │
│  │         poll          │<──connections───     │
│  └──────────┬────────────┘      │   data, etc.  │
│  ┌──────────┴────────────┐      └───────────────┘
│  │        check          │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
└──┤    close callbacks    │
   └───────────────────────┘
timer

timers 阶段会执行 setTimeoutsetInterval

一个 timer 指定的工夫并不是精确工夫,而是在达到这个工夫后尽快执行回调,可能会因为零碎正在执行别的事务而提早。

上限的工夫有一个范畴:[1, 2147483647],如果设定的工夫不在这个范畴,将被设置为 1。

I/O

I/O 阶段会执行除了 close 事件,定时器和 setImmediate 的回调

idle, prepare

idle, prepare 阶段外部实现

poll

poll 阶段很重要,这一阶段中,零碎会做两件事件

  1. 执行到点的定时器
  2. 执行 poll 队列中的事件

并且当 poll 中没有定时器的状况下,会发现以下两件事件

  • 如果 poll 队列不为空,会遍历回调队列并同步执行,直到队列为空或者零碎限度
  • 如果 poll 队列为空,会有两件事产生

    • 如果有 setImmediate 须要执行,poll 阶段会进行并且进入到 check 阶段执行 setImmediate
    • 如果没有 setImmediate 须要执行,会期待回调被退出到队列中并立刻执行回调

如果有别的定时器须要被执行,会回到 timer 阶段执行回调。

check

check 阶段执行 setImmediate

close callbacks

close callbacks 阶段执行 close 事件

并且在 Node 中,有些状况下的定时器执行程序是随机的

setTimeout(() => {console.log('setTimeout');
}, 0);
setImmediate(() => {console.log('setImmediate');
})
// 这里可能会输入 setTimeout,setImmediate
// 可能也会相同的输入,这取决于性能
// 因为可能进入 event loop 用了不到 1 毫秒,这时候会执行 setImmediate
// 否则会执行 setTimeout

当然在这种状况下,执行程序是雷同的

var fs = require('fs')

fs.readFile(__filename, () => {setTimeout(() => {console.log('timeout');
    }, 0);
    setImmediate(() => {console.log('immediate');
    });
});
// 因为 readFile 的回调在 poll 中执行
// 发现有 setImmediate,所以会立刻跳到 check 阶段执行回调
// 再去 timer 阶段执行 setTimeout
// 所以以上输入肯定是 setImmediate,setTimeout

下面介绍的都是 macrotask 的执行状况,microtask 会在以上每个阶段实现后立刻执行。

setTimeout(()=>{console.log('timer1')

    Promise.resolve().then(function() {console.log('promise1')
    })
}, 0)

setTimeout(()=>{console.log('timer2')

    Promise.resolve().then(function() {console.log('promise2')
    })
}, 0)

// 以上代码在浏览器和 node 中打印状况是不同的
// 浏览器中打印 timer1, promise1, timer2, promise2
// node 中打印 timer1, timer2, promise1, promise2

Node 中的 process.nextTick 会先于其余 microtask 执行。

setTimeout(() => {console.log("timer1");

  Promise.resolve().then(function() {console.log("promise1");
  });
}, 0);

process.nextTick(() => {console.log("nextTick");
});
// nextTick, timer1, promise1

字符串模板

function render(template, data) {const reg = /\{\{(\w+)\}\}/; // 模板字符串正则
    if (reg.test(template)) { // 判断模板里是否有模板字符串
        const name = reg.exec(template)[1]; // 查找以后模板里第一个模板字符串的字段
        template = template.replace(reg, data[name]); // 将第一个模板字符串渲染
        return render(template, data); // 递归的渲染并返回渲染后的构造
    }
    return template; // 如果模板没有模板字符串间接返回
}

测试:

let template = '我是{{name}},年龄{{age}},性别{{sex}}';
let person = {
    name: '布兰',
    age: 12
}
render(template, person); // 我是布兰,年龄 12,性别 undefined

数组扁平化

题目形容: 实现一个办法使多维数组变成一维数组

最常见的递归版本如下:

function flatter(arr) {if (!arr.length) return;
  return arr.reduce((pre, cur) =>
      Array.isArray(cur) ? [...pre, ...flatter(cur)] : [...pre, cur],
    []);
}
// console.log(flatter([1, 2, [1, [2, 3, [4, 5, [6]]]]]));

扩大思考:能用迭代的思路去实现吗?

实现代码如下:

function flatter(arr) {if (!arr.length) return;
  while (arr.some((item) => Array.isArray(item))) {arr = [].concat(...arr);
  }
  return arr;
}
// console.log(flatter([1, 2, [1, [2, 3, [4, 5, [6]]]]]));

事件总线(公布订阅模式)

class EventEmitter {constructor() {this.cache = {}
    }
    on(name, fn) {if (this.cache[name]) {this.cache[name].push(fn)
        } else {this.cache[name] = [fn]
        }
    }
    off(name, fn) {let tasks = this.cache[name]
        if (tasks) {const index = tasks.findIndex(f => f === fn || f.callback === fn)
            if (index >= 0) {tasks.splice(index, 1)
            }
        }
    }
    emit(name, once = false, ...args) {if (this.cache[name]) {
            // 创立正本,如果回调函数内持续注册雷同事件,会造成死循环
            let tasks = this.cache[name].slice()
            for (let fn of tasks) {fn(...args)
            }
            if (once) {delete this.cache[name]
            }
        }
    }
}

// 测试
let eventBus = new EventEmitter()
let fn1 = function(name, age) {console.log(`${name} ${age}`)
}
let fn2 = function(name, age) {console.log(`hello, ${name} ${age}`)
}
eventBus.on('aaa', fn1)
eventBus.on('aaa', fn2)
eventBus.emit('aaa', false, '布兰', 12)
// '布兰 12'
// 'hello, 布兰 12'

vue-router

vue-router 是 vuex.js 官网的路由管理器,它和 vue.js 的外围深度集成,让构建但页面利用变得大海捞针

<router-link> 组件反对用户在具备路由性能的利用中 (点击) 导航。通过 to 属性指定指标地址

<router-view> 组件是一个 functional 组件,渲染门路匹配到的视图组件。<keep-alive> 组件是一个用来缓存组件

router.beforeEach

router.afterEach

to: Route: 行将要进入的指标 路由对象

from: Route: 以后导航正要来到的路由

next: Function: 肯定要调用该办法来 resolve 这个钩子。执行成果依赖 next 办法的调用参数。介绍了路由守卫及用法,在我的项目中路由守卫起到的作用等等

IE 兼容

  • attchEvent(‘on’ + type, handler)
  • detachEvent(‘on’ + type, handler)

对事件循环的了解

因为 js 是单线程运行的,在代码执行时,通过将不同函数的执行上下文压入执行栈中来保障代码的有序执行。在执行同步代码时,如果遇到异步事件,js 引擎并不会始终期待其返回后果,而是会将这个事件挂起,继续执行执行栈中的其余工作。当异步事件执行结束后,再将异步事件对应的回调退出到一个工作队列中期待执行。工作队列能够分为宏工作队列和微工作队列,当以后执行栈中的事件执行结束后,js 引擎首先会判断微工作队列中是否有工作能够执行,如果有就将微工作队首的事件压入栈中执行。当微工作队列中的工作都执行实现后再去执行宏工作队列中的工作。

Event Loop 执行程序如下所示:

  • 首先执行同步代码,这属于宏工作
  • 当执行完所有同步代码后,执行栈为空,查问是否有异步代码须要执行
  • 执行所有微工作
  • 当执行完所有微工作后,如有必要会渲染页面
  • 而后开始下一轮 Event Loop,执行宏工作中的异步代码

事件流

事件流是网页元素接管事件的程序,”DOM2 级事件 ” 规定的事件流包含三个阶段:事件捕捉阶段、处于指标阶段、事件冒泡阶段。
首先产生的事件捕捉,为截获事件提供机会。而后是理论的指标承受事件。最初一个阶段是工夫冒泡阶段,能够在这个阶段对事件做出响应。
尽管捕捉阶段在标准中规定不容许响应事件,然而实际上还是会执行,所以有两次机会获取到指标对象。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title> 事件冒泡 </title>
</head>
<body>
    <div>
        <p id="parEle"> 我是父元素    <span id="sonEle"> 我是子元素 </span></p>
    </div>
</body>
</html>
<script type="text/javascript">
var sonEle = document.getElementById('sonEle');
var parEle = document.getElementById('parEle');parEle.addEventListener('click', function () {alert('父级 冒泡');}, false);parEle.addEventListener('click', function () {alert('父级 捕捉');}, true);sonEle.addEventListener('click', function () {alert('子级冒泡');}, false);sonEle.addEventListener('click', function () {alert('子级捕捉');}, true);

</script>

当容器元素及嵌套元素,即在 捕捉阶段 又在 冒泡阶段 调用事件处理程序时:事件按 DOM 事件流的程序 执行事件处理程序:

  • 父级捕捉
  • 子级捕捉
  • 子级冒泡
  • 父级冒泡

且当事件处于指标阶段时,事件调用程序决定于绑定事件的 书写程序,按下面的例子为,先调用冒泡阶段的事件处理程序,再调用捕捉阶段的事件处理程序。顺次 alert 出“子集冒泡”,“子集捕捉”。

代码输入后果

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 捕捉了。

正文完
 0