对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的建设过程:
- 客户端向服务器在发送申请报文同时在首部增加发送Connection字段
- 服务器收到申请并解决 Connection字段
- 服务器回送Connection:Keep-Alive字段给客户端
- 客户端接管到Connection字段
- Keep-Alive连贯建设胜利
服务端主动断开过程(也就是没有keep-alive):
- 客户端向服务器只是发送内容报文(不蕴含Connection字段)
- 服务器收到申请并解决
- 服务器返回客户端申请的资源并敞开连贯
- 客户端接管资源,发现没有Connection字段,断开连接
客户端申请断开连接过程:
- 客户端向服务器发送Connection:close字段
- 服务器收到申请并解决connection字段
- 服务器回送响应资源并断开连接
- 客户端接管资源并断开连接
开启Keep-Alive的长处:
- 较少的CPU和内存的使⽤(因为同时关上的连贯的缩小了);
- 容许申请和应答的HTTP管线化;
- 升高拥塞管制 (TCP连贯缩小了);
- 缩小了后续申请的提早(⽆需再进⾏握⼿);
- 报告谬误⽆需敞开TCP连;
开启Keep-Alive的毛病:
- 长时间的Tcp连贯容易导致系统资源有效占用,节约系统资源。
用过 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)
输入后果如下:
1
看到这个题目,好多的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)
属性值。
let arr = [...arrayLike]
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);