对 keep-alive 的理解
HTTP1.0 中默认是在每次请求/应答,客户端和服务器都要新建一个连接,完成之后立即断开连接,这就是短连接。当使用 Keep-Alive 模式时,Keep-Alive 功能使客户端到服务器端的连接持续有效,当出现对服务器的后继请求时,Keep-Alive 功能避免了建立或者重新建立连接,这就是长连接。其使用方法如下:
- HTTP1.0 版本是默认没有 Keep-alive 的(也就是默认会发送 keep-alive),所以要想连接得到保持,必须手动配置发送- Connection: keep-alive字段。若想断开 keep-alive 连接,需发送- Connection:close字段;
 
- HTTP1.1 规定了默认保持长连接,数据传输完成了保持 TCP 连接不断开,等待在同域名下继续用这个通道传输数据。如果需要关闭,需要客户端发送- Connection:close首部字段。
 
Keep-Alive 的建立过程:
服务端自动断开过程(也就是没有 keep-alive):
客户端请求断开连接过程:
开启 Keep-Alive 的优点:
开启 Keep-Alive 的缺点:
用过 TypeScript 吗?它的作用是什么?
为 JS 添加类型支持,以及提供最新版的 ES 语法的支持,是的利于团队协作和排错,开发大型项目
事件委托的使用场景
场景:给页面的所有的 a 标签添加 click 事件,代码如下:
 document.addEventListener("click", function(e) {    if (e.target.nodeName == "A")        console.log("a");}, false);
   复制代码
 
但是这些 a 标签可能包含一些像 span、img 等元素,如果点击到了这些 a 标签中的元素,就不会触发 click 事件,因为事件绑定上在 a 标签元素上,而触发这些内部的元素时,e.target 指向的是触发 click 事件的元素(span、img 等其他元素)。
这种情况下就可以使用事件委托来处理,将事件绑定在 a 标签的内部元素上,当点击它的时候,就会逐级向上查找,知道找到 a 标签为止,代码如下:
 document.addEventListener("click", function(e) {    var node = e.target;    while (node.parentNode.nodeName != "BODY") {        if (node.nodeName == "A") {            console.log("a");            break;        }        node = node.parentNode;    }}, false);
   复制代码
 异步任务调度器
描述:实现一个带并发限制的异步调度器 Scheduler,保证同时运行的任务最多有 limit 个。
实现:
 class Scheduler {    queue = [];  // 用队列保存正在执行的任务    runCount = 0;  // 计数正在执行的任务个数    constructor(limit) {        this.maxCount = limit;  // 允许并发的最大个数    }    add(time, data){        const promiseCreator = () => {            return new Promise((resolve, reject) => {                setTimeout(() => {                    console.log(data);                    resolve();                }, time);            });        }        this.queue.push(promiseCreator);        // 每次添加的时候都会尝试去执行任务        this.request();    }    request() {        // 队列中还有任务才会被执行        if(this.queue.length && this.runCount < this.maxCount) {            this.runCount++;            // 执行先加入队列的函数            this.queue.shift()().then(() => {                this.runCount--;                // 尝试进行下一次任务                this.request();            });        }    }}
// 测试const scheduler = new Scheduler(2);const addTask = (time, data) => {    scheduler.add(time, data);}
addTask(1000, '1');addTask(500, '2');addTask(300, '3');addTask(400, '4');// 输出结果 2 3 1 4
   复制代码
 
参考前端进阶面试题详细解答
树形结构转成列表
题目描述:
 [    {        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;}
   复制代码
 详细说明 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.nextTick ,promise ,Object.observe ,MutationObserver
宏任务包括 script , setTimeout ,setInterval ,setImmediate ,I/O ,UI rendering
很多人有个误区,认为微任务快于宏任务,其实是错误的。因为宏任务中包括了 script ,浏览器会先执行一个宏任务,接下来有异步代码的话就先执行微任务。
所以正确的一次 Event loop 顺序是这样的
- 执行同步代码,这属于宏任务 
- 执行栈为空,查询是否有微任务需要执行 
- 执行所有微任务 
- 必要的话渲染 UI 
- 然后开始下一轮 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 阶段会执行 setTimeout 和 setInterval
一个 timer 指定的时间并不是准确时间,而是在达到这个时间后尽快执行回调,可能会因为系统正在执行别的事务而延迟。
下限的时间有一个范围:[1, 2147483647] ,如果设定的时间不在这个范围,将被设置为 1。
I/O
I/O 阶段会执行除了 close 事件,定时器和 setImmediate 的回调
idle, prepare
idle, prepare 阶段内部实现
poll
poll 阶段很重要,这一阶段中,系统会做两件事情
- 执行到点的定时器 
- 执行 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
   复制代码
 常见浏览器所用内核
(1) IE 浏览器内核:Trident 内核,也是俗称的 IE 内核;
(2) Chrome 浏览器内核:统称为 Chromium 内核或 Chrome 内核,以前是 Webkit 内核,现在是 Blink 内核;
(3) Firefox 浏览器内核:Gecko 内核,俗称 Firefox 内核;
(4) Safari 浏览器内核:Webkit 内核;
(5) Opera 浏览器内核:最初是自己的 Presto 内核,后来加入谷歌大军,从 Webkit 又到了 Blink 内核;
(6) 360 浏览器、猎豹浏览器内核:IE + Chrome 双内核;
(7) 搜狗、遨游、QQ 浏览器内核:Trident(兼容模式)+ Webkit(高速模式);
(8) 百度浏览器、世界之窗内核:IE 内核;
(9) 2345 浏览器内核:好像以前是 IE 内核,现在也是 IE + Chrome 双内核了;
(10)UC 浏览器内核:这个众口不一,UC 说是他们自己研发的 U3 内核,但好像还是基于 Webkit 和 Trident ,还有说是基于火狐内核。
instanceof
作用:判断对象的具体类型。可以区别 array 和 object, null 和 object 等。
语法: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__;    }}
   复制代码
 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
   复制代码
 实现模板字符串解析功能
题目描述:
 let template = '我是{{name}},年龄{{age}},性别{{sex}}';let data = {  name: '姓名',  age: 18}render(template, data); // 我是姓名,年龄18,性别undefined
   复制代码
 
实现代码如下:
 function render(template, data) {  let computed = template.replace(/\{\{(\w+)\}\}/g, function (match, key) {    return data[key];  });  return computed;}
   复制代码
 代码输出结果
 async function async1 () {  await async2();  console.log('async1');  return 'async1 success'}async function async2 () {  return new Promise((resolve, reject) => {    console.log('async2')    reject('error')  })}async1().then(res => console.log(res))
   复制代码
 
输出结果如下:
 async2Uncaught (in promise) error
   复制代码
 
可以看到,如果 async 函数中抛出了错误,就会终止错误结果,不会继续向下执行。
如果想要让错误不足之处后面的代码执行,可以使用 catch 来捕获:
 async function async1 () {  await Promise.reject('error!!!').catch(e => console.log(e))  console.log('async1');  return Promise.resolve('async1 success')}async1().then(res => console.log(res))console.log('script start')
   复制代码
 
这样的输出结果就是:
 script starterror!!!async1async1 success
   复制代码
 深/浅拷贝
首先判断数据类型是否为对象,如果是对象(数组|对象),则递归(深/浅拷贝),否则直接拷贝。
 function isObject(obj) {    return typeof obj === "object" && obj !== null;}
   复制代码
 
这个函数只能判断 obj 是否是对象,无法判断其具体是数组还是对象。
防抖节流
题目描述:手写防抖节流
实现代码如下:
 // 防抖function debounce(fn, delay = 300) {  //默认300毫秒  let timer;  return function () {    const args = arguments;    if (timer) {      clearTimeout(timer);    }    timer = setTimeout(() => {      fn.apply(this, args); // 改变this指向为调用debounce所指的对象    }, delay);  };}
window.addEventListener(  "scroll",  debounce(() => {    console.log(111);  }, 1000));
// 节流// 设置一个标志function throttle(fn, delay) {  let flag = true;  return () => {    if (!flag) return;    flag = false;    timer = setTimeout(() => {      fn();      flag = true;    }, delay);  };}
window.addEventListener(  "scroll",  throttle(() => {    console.log(111);  }, 1000));
   复制代码
 代码输出结果
 Promise.resolve(1)  .then(2)  .then(Promise.resolve(3))  .then(console.log)
   复制代码
 
输出结果如下:
看到这个题目,好多的 then,实际上只需要记住一个原则:.then 或.catch 的参数期望是函数,传入非函数则会发生值透传。
第一个 then 和第二个 then 中传入的都不是函数,一个是数字,一个是对象,因此发生了透传,将resolve(1) 的值直接传到最后一个 then 里,直接打印出 1。
代码输出结果
 var a, b(function () {   console.log(a);   console.log(b);   var a = (b = 3);   console.log(a);   console.log(b);   })()console.log(a);console.log(b);
   复制代码
 
输出结果:
 undefined undefined 3 3 undefined 3
   复制代码
 
这个题目和上面题目考察的知识点类似,b 赋值为 3,b 此时是一个全局变量,而将 3 赋值给 a,a 是一个局部变量,所以最后打印的时候,a 仍旧是 undefined。
浏览器的主要组成部分
- ⽤户界⾯ 包括地址栏、前进/后退按钮、书签菜单等。除了浏览器主窗⼝显示的您请求的⻚⾯外,其他显示的各个部分都属于⽤户界⾯。 
- 浏览器引擎 在⽤户界⾯和呈现引擎之间传送指令。 
- 呈现引擎 负责显示请求的内容。如果请求的内容是 HTML,它就负责解析 HTML 和 CSS 内容,并将解析后的内容显示在屏幕上。 
- ⽹络 ⽤于⽹络调⽤,⽐如 HTTP 请求。其接⼝与平台⽆关,并为所有平台提供底层实现。 
- ⽤户界⾯后端 ⽤于绘制基本的窗⼝⼩部件,⽐如组合框和窗⼝。其公开了与平台⽆关的通⽤接⼝,⽽在底层使⽤操作系统的⽤户界⾯⽅法。 
- JavaScript 解释器。⽤于解析和执⾏ JavaScript 代码。 
- 数据存储 这是持久层。浏览器需要在硬盘上保存各种数据,例如 Cookie。新的 HTML 规范 (HTML5) 定义了“⽹络数据库”,这是⼀个完整(但是轻便)的浏览器内数据库。 
值得注意的是,和⼤多数浏览器不同,Chrome 浏览器的每个标签⻚都分别对应⼀个呈现引擎实例。每个标签⻚都是⼀个独⽴的进程。
代码输出结果
 async function async1() {  console.log("async1 start");  await async2();  console.log("async1 end");}async function async2() {  console.log("async2");}async1();console.log('start')
   复制代码
 
输出结果如下:
 async1 startasync2startasync1 end
   复制代码
 
代码的执行过程如下:
- 首先执行函数中的同步代码- async1 start,之后遇到了- await,它会阻塞- async1后面代码的执行,因此会先去执行- async2中的同步代码- async2,然后跳出- async1;
 
- 跳出- async1函数后,执行同步代码- start;
 
- 在一轮宏任务全部执行完之后,再来执行- await后面的内容- async1 end。
 
这里可以理解为 await 后面的语句相当于放到了 new Promise 中,下一行及之后的语句相当于放在 Promise.then 中。
数组扁平化
ES5 递归写法 —— isArray()、concat()
 function flat11(arr) {    var res = [];    for (var i = 0; i < arr.length; i++) {        if (Array.isArray(arr[i])) {            res = res.concat(flat11(arr[i]));        } else {            res.push(arr[i]);        }    }    return res;}
   复制代码
 
如果想实现第二个参数(指定“拉平”的层数),可以这样实现,后面的几种可以自己类似实现:
 function flat(arr, level = 1) {    var res = [];    for(var i = 0; i < arr.length; i++) {        if(Array.isArray(arr[i]) || level >= 1) {            res = res.concat(flat(arr[i]), level - 1);        }        else {            res.push(arr[i]);        }    }    return res;}
   复制代码
 ES6 递归写法 — reduce()、concat()、isArray()
 function flat(arr) {    return arr.reduce(        (pre, cur) => pre.concat(Array.isArray(cur) ? flat(cur) : cur), []    );}
   复制代码
 ES6 迭代写法 — 扩展运算符(...)、some()、concat()、isArray()
ES6 的扩展运算符(...) 只能扁平化一层
 function flat(arr) {    return [].concat(...arr);}
   复制代码
 
全部扁平化:遍历原数组,若arr中含有数组则使用一次扩展运算符,直至没有为止。
 function flat(arr) {    while(arr.some(item => Array.isArray(item))) {        arr = [].concat(...arr);    }    return arr;}
   复制代码
 toString/join & split
调用数组的 toString()/join() 方法(它会自动扁平化处理),将数组变为字符串然后再用 split 分割还原为数组。由于 split 分割后形成的数组的每一项值为字符串,所以需要用一个map方法遍历数组将其每一项转换为数值型。
 function flat(arr){    return arr.toString().split(',').map(item => Number(item));    // return arr.join().split(',').map(item => Number(item));}
   复制代码
 使用正则
JSON.stringify(arr).replace(/[|]/g, '') 会先将数组arr序列化为字符串,然后使用 replace() 方法将字符串中所有的[ 或 ] 替换成空字符,从而达到扁平化处理,此时的结果为 arr 不包含 [] 的字符串。最后通过JSON.parse() 解析字符串。
 function flat(arr) {    return JSON.parse("[" + JSON.stringify(arr).replace(/\[|\]/g,'') + "]");}
   复制代码
 类数组转化为数组
类数组是具有 length 属性,但不具有数组原型上的方法。常见的类数组有 arguments、DOM 操作方法返回的结果(如document.querySelectorAll('div'))等。
扩展运算符(...)
注意:扩展运算符只能作用于 iterable 对象,即拥有 Symbol(Symbol.iterator) 属性值。
Array.from()
 let arr = Array.from(arrayLike);
   复制代码
 Array.prototype.slice.call()
 let arr = Array.prototype.slice.call(arrayLike);
   复制代码
 Array.apply()
 let arr = Array.apply(null, arrayLike);
   复制代码
 concat + apply
 let arr = Array.prototype.concat.apply([], arrayLike);
   复制代码
 代码输出问题
 function A(){}function B(a){  this.a = a;}function C(a){  if(a){this.a = a;  }}A.prototype.a = 1;B.prototype.a = 1;C.prototype.a = 1;
console.log(new A().a);console.log(new B().a);console.log(new C(2).a);
   复制代码
 
输出结果:1 undefined 2
解析:
- console.log(new A().a),new A()为构造函数创建的对象,本身没有 a 属性,所以向它的原型去找,发现原型的 a 属性的属性值为 1,故该输出值为 1; 
- console.log(new B().a),ew B()为构造函数创建的对象,该构造函数有参数 a,但该对象没有传参,故该输出值为 undefined; 
- console.log(new C(2).a),new C()为构造函数创建的对象,该构造函数有参数 a,且传的实参为 2,执行函数内部,发现 if 为真,执行 this.a = 2,故属性 a 的值为 2。 
箭头函数和普通函数有啥区别?箭头函数能当构造函数吗?
- 普通函数通过 function 关键字定义, this 无法结合词法作用域使用,在运行时绑定,只取决于函数的调用方式,在哪里被调用,调用位置。(取决于调用者,和是否独立运行) 
- 箭头函数使用被称为 “胖箭头” 的操作 - =>定义,箭头函数不应用普通函数 this 绑定的四种规则,而是根据外层(函数或全局)的作用域来决定 this,且箭头函数的绑定无法被修改(new 也不行)。
 
- 箭头函数常用于回调函数中,包括事件处理器或定时器 
- 箭头函数和 var self = this,都试图取代传统的 this 运行机制,将 this 的绑定拉回到词法作用域 
- 没有原型、没有 this、没有 super,没有 arguments,没有 new.target 
- 不能通过 new 关键字调用 
- 一个函数内部有两个方法:[[Call]] 和 [[Construct]],在通过 new 进行函数调用时,会执行 [[construct]] 方法,创建一个实例对象,然后再执行这个函数体,将函数的 this 绑定在这个实例对象上 
- 当直接调用时,执行 [[Call]] 方法,直接执行函数体 
- 箭头函数没有 [[Construct]] 方法,不能被用作构造函数调用,当使用 new 进行函数调用时会报错。 
 function foo() {  return (a) => {    console.log(this.a);  }}
var obj1 = {  a: 2}
var obj2 = {  a: 3 }
var bar = foo.call(obj1);bar.call(obj2);
   复制代码
 
评论