HTTP1.0 中默认是在每次请求/应答,客户端和服务器都要新建一个连接,完成之后立即断开连接,这就是短连接。当使用Keep-Alive模式时,Keep-Alive功能使客户端到服务器端的连接持续有效,当出现对服务器的后继请求时,Keep-Alive功能避免了建立或者重新建立连接,这就是长连接。其使用方法如下:
Connection: keep-alive
字段。若想断开keep-alive连接,需发送Connection:close
字段;Connection:close
首部字段。Keep-Alive的建立过程:
服务端自动断开过程(也就是没有keep-alive):
客户端请求断开连接过程:
开启Keep-Alive的优点:
开启Keep-Alive的缺点:
为 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;
}
众所周知 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 顺序是这样的
通过上述的 Event loop 顺序可知,如果宏任务中的异步代码有大量的计算并且需要操作 DOM 的话,为了更快的 界面响应,我们可以把操作 DOM 放入微任务中。
Node 中的 Event loop 和浏览器中的不相同。
Node 的 Event loop 分为6个阶段,它们会按照顺序反复运行
┌───────────────────────┐
┌─>│ timers │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ I/O callbacks │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<──connections─── │
│ └──────────┬────────────┘ │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
│ │ check │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└───────────────────────┘
timers 阶段会执行 setTimeout
和 setInterval
一个 timer
指定的时间并不是准确时间,而是在达到这个时间后尽快执行回调,可能会因为系统正在执行别的事务而延迟。
下限的时间有一个范围:[1, 2147483647]
,如果设定的时间不在这个范围,将被设置为1。
I/O 阶段会执行除了 close 事件,定时器和 setImmediate
的回调
idle, prepare 阶段内部实现
poll 阶段很重要,这一阶段中,系统会做两件事情
并且当 poll 中没有定时器的情况下,会发现以下两件事情
setImmediate
需要执行,poll 阶段会停止并且进入到 check 阶段执行 setImmediate
setImmediate
需要执行,会等待回调被加入到队列中并立即执行回调如果有别的定时器需要被执行,会回到 timer 阶段执行回调。
check 阶段执行 setImmediate
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 ,还有说是基于火狐内核。
作用:判断对象的具体类型。可以区别 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__;
}
}
实现代码如下:
// 一个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))
输出结果如下:
async2
Uncaught (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 start
error!!!
async1
async1 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。
值得注意的是,和⼤多数浏览器不同,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 start
async2
start
async1 end
代码的执行过程如下:
async1 start
,之后遇到了await
,它会阻塞async1
后面代码的执行,因此会先去执行async2
中的同步代码async2
,然后跳出async1
;async1
函数后,执行同步代码start
;await
后面的内容async1 end
。这里可以理解为await后面的语句相当于放到了new Promise中,下一行及之后的语句相当于放在Promise.then中。
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;
}
function flat(arr) {
return arr.reduce(
(pre, cur) => pre.concat(Array.isArray(cur) ? flat(cur) : cur), []
);
}
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
分割还原为数组。由于 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]
let arr = Array.from(arrayLike);
let arr = Array.prototype.slice.call(arrayLike);
let arr = Array.apply(null, arrayLike);
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
解析:
=>
定义,箭头函数不应用普通函数 this 绑定的四种规则,而是根据外层(函数或全局)的作用域来决定 this,且箭头函数的绑定无法被修改(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);
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。