大厂面试题第一部分¶
https://github.com/Advanced-Frontend/Daily-Interview-Question/blob/master/datum/summary.md
B 第 3 题:什么是防抖和节流?有什么区别?如何实现?¶
解析:第 3 题
- 防抖
触发高频事件后n秒内函数只会执行一次,如果n秒内高频事件再次被触发,则重新计算时间
- 思路:
每次触发事件时都取消之前的延时调用方法
function debounce(fn) {
let timeout = null; // 创建一个标记用来存放定时器的返回值
return function () {
clearTimeout(timeout); // 每当用户输入的时候把前一个 setTimeout clear 掉
timeout = setTimeout(() => { // 然后又创建一个新的 setTimeout, 这样就能保证输入字符后的 interval 间隔内如果还有字符输入的话,就不会执行 fn 函数
fn.apply(this, arguments);
}, 500);
};
}
function sayHi() {
console.log('防抖成功');
}
var inp = document.getElementById('inp');
inp.addEventListener('input', debounce(sayHi)); // 防抖
- 节流
高频事件触发,但在n秒内只会执行一次,所以节流会稀释函数的执行频率
- 思路:
每次触发事件时都判断当前是否有等待执行的延时函数
function throttle(fn) {
let canRun = true; // 通过闭包保存一个标记
return function () {
if (!canRun) return; // 在函数开头判断标记是否为true,不为true则return
canRun = false; // 立即设置为false
setTimeout(() => { // 将外部传入的函数的执行放在setTimeout中
fn.apply(this, arguments);
// 最后在setTimeout执行完毕后再把标记设置为true(关键)表示可以执行下一次循环了。当定时器没有执行的时候标记永远是false,在开头被return掉
canRun = true;
}, 500);
};
}
function sayHi(e) {
console.log(e.target.innerWidth, e.target.innerHeight);
}
window.addEventListener('resize', throttle(sayHi));
C 第 4 题:介绍下 Set、Map、WeakSet 和 WeakMap 的区别?¶
解析:第 4 题
https://es6.ruanyifeng.com/#docs/set-map
Set¶
- 成员不能重复
- 只有健值,没有健名,有点类似数组。
- 可以遍历,方法有add, delete,has
weakSet¶
- 成员都是对象
- 成员都是弱引用,随时可以消失。 可以用来保存DOM节点,不容易造成内存泄漏
- 不能遍历,方法有add, delete,has
Map¶
- 本质上是健值对的集合,类似集合
- 可以遍历,方法很多,可以干跟各种数据格式转换
weakMap¶
- 直接受对象作为健名(null除外),不接受其他类型的值作为健名
- 健名所指向的对象,不计入垃圾回收机制
- 不能遍历,方法同get,set,has,delete
B 第 5 题:介绍下深度优先遍历和广度优先遍历,如何实现?¶
解析:第 5 题
第五题问的是深度优先遍历和广度优先遍历,我是从dom节点的遍历来理解这个问题的
html代码
我将用深度优先遍历和广度优先遍历对这个dom树进行查找
深度优先遍历
深度优先遍历DFS 与树的先序遍历比较类似。 假设初始状态是图中所有顶点均未被访问,则从某个顶点v出发,首先访问该顶点然后依次从它的各个未被访问的邻接点出发深度优先搜索遍历图,直至图中所有和v有路径相通的顶点都被访问到。若此时尚有其他顶点未被访问到,则另选一个未被访问的顶点作起始点,重复上述过程,直至图中所有顶点都被访问到为止。
/*深度优先遍历三种方式*/
let deepTraversal1 = (node, nodeList = []) => {
if (node !== null) {
nodeList.push(node)
let children = node.children
for (let i = 0; i < children.length; i++) {
deepTraversal1(children[i], nodeList)
}
}
return nodeList
}
let deepTraversal2 = (node) => {
let nodes = []
if (node !== null) {
nodes.push(node)
let children = node.children
for (let i = 0; i < children.length; i++) {
nodes = nodes.concat(deepTraversal2(children[i]))
}
}
return nodes
}
}
// 非递归
let deepTraversal3 = (node) => {
let stack = []
let nodes = []
if (node) {
// 推入当前处理的node
stack.push(node)
while (stack.length) {
let item = stack.pop()
let children = item.children
nodes.push(item)
// node = [] stack = [parent]
// node = [parent] stack = [child3,child2,child1]
// node = [parent, child1] stack = [child3,child2,child1-2,child1-1]
// node = [parent, child1-1] stack = [child3,child2,child1-2]
for (let i = children.length - 1; i >= 0; i--) {
stack.push(children[i])
}
}
}
return nodes
}
广度优先遍历 BFS 从图中某顶点v出发,在访问了v之后依次访问v的各个未曾访问过的邻接点,然后分别从这些邻接点出发依次访问它们的邻接点,并使得“先被访问的顶点的邻接点先于后被访问的顶点的邻接点被访问,直至图中所有已被访问的顶点的邻接点都被访问到。 如果此时图中尚有顶点未被访问,则需要另选一个未曾被访问过的顶点作为新的起始点,重复上述过程,直至图中所有顶点都被访问到为止。
let widthTraversal2 = (node) => {
let nodes = []
let stack = []
if (node) {
stack.push(node)
while (stack.length) {
let item = stack.shift()
let children = item.children
nodes.push(item)
// 队列,先进先出
// nodes = [] stack = [parent]
// nodes = [parent] stack = [child1,child2,child3]
// nodes = [parent, child1] stack = [child2,child3,child1-1,child1-2]
// nodes = [parent,child1,child2]
for (let i = 0; i < children.length; i++) {
stack.push(children[i])
}
}
}
return nodes
}
第 6 题:请分别用深度优先思想和广度优先思想实现一个拷贝函数?¶
解析:第 6 题
话不多说直接放代码 发现了比较多的错误,但由于最近工作有点忙,一直没来得及纠正
更改(0226)
// 工具函数
let _toString = Object.prototype.toString
let map = {
array: 'Array',
object: 'Object',
function: 'Function',
string: 'String',
null: 'Null',
undefined: 'Undefined',
boolean: 'Boolean',
number: 'Number'
}
let getType = (item) => {
return _toString.call(item).slice(8, -1)
}
let isTypeOf = (item, type) => {
return map[type] && map[type] === getType(item)
}
深复制 深度优先遍历
let DFSdeepClone = (obj, visitedArr = []) => {
let _obj = {}
if (isTypeOf(obj, 'array') || isTypeOf(obj, 'object')) {
let index = visitedArr.indexOf(obj)
_obj = isTypeOf(obj, 'array') ? [] : {}
if (~index) { // 判断环状数据
_obj = visitedArr[index]
} else {
visitedArr.push(obj)
for (let item in obj) {
_obj[item] = DFSdeepClone(obj[item], visitedArr)
}
}
} else if (isTypeOf(obj, 'function')) {
_obj = eval('(' + obj.toString() + ')');
} else {
_obj = obj
}
return _obj
}
广度优先遍历
let BFSdeepClone = (obj) => {
let origin = [obj],
copyObj = {},
copy = [copyObj]
// 去除环状数据
let visitedQueue = [],
visitedCopyQueue = []
while (origin.length > 0) {
let items = origin.shift(),
_obj = copy.shift()
visitedQueue.push(items)
if (isTypeOf(items, 'object') || isTypeOf(items, 'array')) {
for (let item in items) {
let val = items[item]
if (isTypeOf(val, 'object')) {
let index = visitedQueue.indexOf(val)
if (!~index) {
_obj[item] = {}
//下次while循环使用给空对象提供数据
origin.push(val)
// 推入引用对象
copy.push(_obj[item])
} else {
_obj[item] = visitedCopyQueue[index]
visitedQueue.push(_obj)
}
} else if (isTypeOf(val, 'array')) {
// 数组类型在这里创建了一个空数组
_obj[item] = []
origin.push(val)
copy.push(_obj[item])
} else if (isTypeOf(val, 'function')) {
_obj[item] = eval('(' + val.toString() + ')');
} else {
_obj[item] = val
}
}
// 将已经处理过的对象数据推入数组 给环状数据使用
visitedCopyQueue.push(_obj)
} else if (isTypeOf(items, 'function')) {
copyObj = eval('(' + items.toString() + ')');
} else {
copyObj = obj
}
}
return copyObj
}
测试
/**测试数据 */
// 输入 字符串String
// 预期输出String
let str = 'String'
var strCopy = DFSdeepClone(str)
var strCopy1 = BFSdeepClone(str)
console.log(strCopy, strCopy1) // String String 测试通过
// 输入 数字 -1980
// 预期输出数字 -1980
let num = -1980
var numCopy = DFSdeepClone(num)
var numCopy1 = BFSdeepClone(num)
console.log(numCopy, numCopy1) // -1980 -1980 测试通过
// 输入bool类型
// 预期输出bool类型
let bool = false
var boolCopy = DFSdeepClone(bool)
var boolCopy1 = BFSdeepClone(bool)
console.log(boolCopy, boolCopy1) //false false 测试通过
// 输入 null
// 预期输出 null
let nul = null
var nulCopy = DFSdeepClone(nul)
var nulCopy1 = BFSdeepClone(nul)
console.log(nulCopy, nulCopy1) //null null 测试通过
// 输入undefined
// 预期输出undefined
let und = undefined
var undCopy = DFSdeepClone(und)
var undCopy1 = BFSdeepClone(und)
console.log(undCopy, undCopy1) //undefined undefined 测试通过
//输入引用类型obj
let obj = {
a: 1,
b: () => console.log(1),
c: {
d: 3,
e: 4
},
f: [1, 2],
und: undefined,
nul: null
}
var objCopy = DFSdeepClone(obj)
var objCopy1 = BFSdeepClone(obj)
console.log(objCopy === objCopy1) // 对象类型判断 false 测试通过
console.log(obj.c === objCopy.c) // 对象类型判断 false 测试通过
console.log(obj.c === objCopy1.c) // 对象类型判断 false 测试通过
console.log(obj.b === objCopy1.b) // 函数类型判断 false 测试通过
console.log(obj.b === objCopy.b) // 函数类型判断 false 测试通过
console.log(obj.f === objCopy.f) // 数组类型判断 false 测试通过
console.log(obj.f === objCopy1.f) // 数组类型判断 false 测试通过
console.log(obj.nul, obj.und) // 输出null,undefined 测试通过
// 输入环状数据
// 预期不爆栈且深度复制
let circleObj = {
foo: {
name: function() {
console.log(1)
},
bar: {
name: 'bar',
baz: {
name: 'baz',
aChild: null //待会让它指向obj.foo
}
}
}
}
circleObj.foo.bar.baz.aChild = circleObj.foo
var circleObjCopy = DFSdeepClone(circleObj)
var circleObjCopy1 = BFSdeepClone(circleObj)
console.log(circleObjCopy, circleObjCopy1) // 测试通过?
ps
关于深浅拷贝的问题博主已经在他的git上有文章说过了,我就不做多的叙述 这两个方法我认为主要区别在于对于深层次以及环状数据,用深度优先遍历递归去做容易爆栈,广度优先遍历我对环状数据进行了处理,已经存在过的对象会存在数组中,下次直接赋值即可,无需继续遍历
C 第 7 题:ES5/ES6 的继承除了写法以外还有什么区别?¶
解析:第 7 题
class
声明会提升,但不会初始化赋值。Foo
进入暂时性死区,类似于let
、const
声明变量。
const bar = new Bar(); // it's ok
function Bar() {
this.bar = 42;
}
const foo = new Foo(); // ReferenceError: Foo is not defined
class Foo {
constructor() {
this.foo = 42;
}
}
class
声明内部会启用严格模式。
// 引用一个未声明的变量
function Bar() {
baz = 42; // it's ok
}
const bar = new Bar();
class Foo {
constructor() {
fol = 42; // ReferenceError: fol is not defined
}
}
const foo = new Foo();
class
的所有方法(包括静态方法和实例方法)都是不可枚举的。
// 引用一个未声明的变量
function Bar() {
this.bar = 42;
}
Bar.answer = function() {
return 42;
};
Bar.prototype.print = function() {
console.log(this.bar);
};
const barKeys = Object.keys(Bar); // ['answer']
const barProtoKeys = Object.keys(Bar.prototype); // ['print']
class Foo {
constructor() {
this.foo = 42;
}
static answer() {
return 42;
}
print() {
console.log(this.foo);
}
}
const fooKeys = Object.keys(Foo); // []
const fooProtoKeys = Object.keys(Foo.prototype); // []
class
的所有方法(包括静态方法和实例方法)都没有原型对象 prototype,所以也没有[[construct]]
,不能使用new
来调用。
function Bar() {
this.bar = 42;
}
Bar.prototype.print = function() {
console.log(this.bar);
};
const bar = new Bar();
const barPrint = new bar.print(); // it's ok
class Foo {
constructor() {
this.foo = 42;
}
print() {
console.log(this.foo);
}
}
const foo = new Foo();
const fooPrint = new foo.print(); // TypeError: foo.print is not a constructor
- 必须使用
new
调用class
。
function Bar() {
this.bar = 42;
}
const bar = Bar(); // it's ok
class Foo {
constructor() {
this.foo = 42;
}
}
const foo = Foo(); // TypeError: Class constructor Foo cannot be invoked without 'new'
class
内部无法重写类名。
function Bar() {
Bar = 'Baz'; // it's ok
this.bar = 42;
}
const bar = new Bar();
// Bar: 'Baz'
// bar: Bar {bar: 42}
class Foo {
constructor() {
this.foo = 42;
Foo = 'Fol'; // TypeError: Assignment to constant variable
}
}
const foo = new Foo();
Foo = 'Fol'; // it's ok
第 8 题:setTimeout、Promise、Async/Await 的区别¶
解析:第 8 题
这题怎么没人答,我说下我粗浅的认识,抛砖引玉,欢迎指正和补充。 我觉得这题主要是考察这三者在事件循环中的区别,事件循环中分为宏任务队列和微任务队列。 其中settimeout的回调函数放到宏任务队列里,等到执行栈清空以后执行; promise.then里的回调函数会放到相应宏任务的微任务队列里,等宏任务里面的同步代码执行完再执行;async函数表示函数里面可能会有异步方法,await后面跟一个表达式,async方法执行时,遇到await会立即执行表达式,然后把表达式后面的代码放到微任务队列里,让出执行栈让同步代码先执行。
1. setTimeout¶
console.log('script start') //1. 打印 script start
setTimeout(function(){
console.log('settimeout') // 4. 打印 settimeout
}) // 2. 调用 setTimeout 函数,并定义其完成后执行的回调函数
console.log('script end') //3. 打印 script start
// 输出顺序:script start->script end->settimeout
2. Promise¶
Promise本身是**同步的立即执行函数**, 当在executor中执行resolve或者reject的时候, 此时是异步操作, 会先执行then/catch等,当主栈完成后,才会去调用resolve/reject中存放的方法执行,打印p的时候,是打印的返回结果,一个Promise实例。
console.log('script start')
let promise1 = new Promise(function (resolve) {
console.log('promise1')
resolve()
console.log('promise1 end')
}).then(function () {
console.log('promise2')
})
setTimeout(function(){
console.log('settimeout')
})
console.log('script end')
// 输出顺序: script start->promise1->promise1 end->script end->promise2->settimeout
当JS主线程执行到Promise对象时,
- promise1.then() 的回调就是一个 task
- promise1 是 resolved或rejected: 那这个 task 就会放入当前事件循环回合的 microtask queue
- promise1 是 pending: 这个 task 就会放入 事件循环的未来的某个(可能下一个)回合的 microtask queue 中
- setTimeout 的回调也是个 task ,它会被放入 macrotask queue 即使是 0ms 的情况
3. async/await¶
async function async1(){
console.log('async1 start');
await async2();
console.log('async1 end')
}
async function async2(){
console.log('async2')
}
console.log('script start');
async1();
console.log('script end')
// 输出顺序:script start->async1 start->async2->script end->async1 end
async 函数返回一个 Promise 对象,当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再执行函数体内后面的语句。可以理解为,是让出了线程,跳出了 async 函数体。
举个例子:
async function func1() {
return 1
}
console.log(func1())
很显然,func1的运行结果其实就是一个Promise对象。因此我们也可以使用then来处理后续逻辑。
func1().then(res => {
console.log(res); // 30
})
await的含义为等待,也就是 async 函数需要等待await后的函数执行完成并且有了返回结果(Promise对象)之后,才能继续执行下面的代码。await通过返回一个Promise对象来实现同步的效果。
更多可见setTimeout、Promise、Async/Await
第 9 题:(头条、微医)Async/Await 如何通过同步的方式实现异步¶
解析:第 9 题
本质是单向链表吧
Async/Await 如何通过同步的方式实现异步
作为前端人员要回答这个问题,需要了解这三个知识点:
- 同步
- 异步
- Async/Await
首先,js 是单线程的(重复三遍),所谓单线程,
通俗的讲就是,一根筋(比喻有点过分,哈哈)执行代码是一行一行的往下走(即所谓的**同步**),
如果上面的没执行完,就痴痴的等着(是不是很像恋爱中在路边等她/他的你,假装 new 了个对象,啊哈哈哈,调皮一下很开心),
还是举个 🌰 吧:
// chrome 75
function test() {
let d = Date.now();
for (let i = 0; i < 1e8; i++) {}
console.log(Date.now() - d); // 62ms左右
}
function test1() {
let d = Date.now();
console.log(Date.now() - d); // 0
}
test();
test1();
上面仅仅是一个 for 循环,而在实际应用中,会有大量的网络请求,它的响应时间是不确定的,这种情况下也要痴痴的等么?显然是不行的,因而 js 设计了异步,即 发起网络请求(诸如 IO 操作,定时器),由于需要等服务器响应,就先不理会,而是去做其他的事儿,等请求返回了结果的时候再说(即**异步**)。
那么如何实现异步呢?其实我们平时已经在大量使用了,那就是 callback
,例如:
$.ajax({
url: 'http://xxx',
success: function(res) {
console.log(res);
},
});
success 作为函数传递过去并不会立即执行,而是等请求成功了才执行,即**回调函数**(callback)
const fs = require('fs');
fs.rename('旧文件.txt', '新文件.txt', err => {
if (err) throw err;
console.log('重命名完成');
});
和网络请求类似,等到 IO 操作有了结果(无论成功与否)才会执行第三个参数:(err)=>{}
从上面我们就可以看出,实现异步的核心就是回调钩子,将 cb 作为参数传递给异步执行函数,当有了结果后在触发 cb。想了解更多,去看看 event-loop
机制吧。
至于 async/await 是如何出现的呢,在 es6 之前,大多 js 数项目中会有类似这样的代码:
ajax1(url, () => {
ajax2(url, () => {
ajax3(url, () => {
// do something
});
});
});
这种函数嵌套,大量的回调函数,使代码阅读起来晦涩难懂,不直观,形象的称之为**回调地狱(callback hell)**,所以为了在写法上能更通俗一点,es6+陆续出现了 Promise
、Generator
、Async/await
,力求在写法上简洁明了,可读性强。
=========================我是分割线==========================
以上只是铺垫,下面在进入正题 👇,开始说道说道主角:async/await
=========================我是分割线==========================
async/await
是参照 Generator
封装的一套异步处理方案,可以理解为 Generator
的语法糖,
所以了解 async/await
就不得不讲一讲 Generator
,
而 Generator
又依赖于迭代器Iterator
,
所以就得先讲一讲 Iterator
,
而 Iterator
的思想呢又来源于单向链表,
终于找到源头了:单向链表
- 单向链表
wiki:链表(Linked list)是一种常见的基础数据结构,是一种线性表,但是并不会按线性的顺序储存数据,而是在每一个节点里存到下一个节点的指针(Pointer)。由于不必须按顺序储存,链表在插入的时候可以达到 o(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要 o(n)的时间,而顺序表响应的时间复杂度分别是 o(logn)和 o(1)。
总结一下链表优点:
- 无需预先分配内存
- 插入/删除节点不影响其他节点,效率高(典型的例子:git commit、
dom 操作)
单向链表:是链表中最简单的一种,它包含两个域,一个信息域和一个指针域。这个链接指向列表中的下一个节点,而最后一个节点则指向一个空值。
一个单向链表包含两个值: 当前节点的值和一个指向下一个节点的链接
单链特点:节点的链接方向是单向的;相对于数组来说,单链表的的随机访问速度较慢,但是单链表删除/添加数据的效率很高。
理解 js 原型链/作用域链的话,理解这个很容易,他们是相通的。编程语言中,数组的长度时固定的,所以数组中的增加和删除比较麻烦,需要频繁的移动数组中的其他元素,而 js 作为一门动态语言,数组本质是一个类似数组的对象,是动态的,不需要预先分配内存
那么如何设计一个单向链表呢?这个取决于我们需要哪些操作,通常有:
- append(element):追加节点
- insert(element,index):在索引位置插入节点
- remove(element):删除第一个匹配到的节点
- removeAt(index):删除指定索引节点
- removeAll(element):删除所有匹配的节点
- get(index):获取指定索引的节点信息
- set(element,index):修改指定索引的节点值
- indexOf(element):获取某节点的索引位置
- clear():清除所有节点
- length():返回节点长度
- printf():打印节点信息
看到这些方法是不是有些许熟悉,当你用原生 js 或 jq 时常会用上面类似的方法,现在根据上面列出的方法进行实现一个单向链:
// 节点模型
class LinkNode {
constructor(element, next) {
this.element = element;
this.next = next;
}
}
class LinkedList {
constructor() {
this._head = null;
this._size = 0;
this._errorBoundary = this._errorBoundary.bind(this);
this._getNodeByIndex = this._getNodeByIndex.bind(this);
this.append = this.append.bind(this);
this.insert = this.insert.bind(this);
this.remove = this.remove.bind(this);
this.removeAt = this.removeAt.bind(this);
this.removeAll = this.removeAll.bind(this);
this.getElement = this.getElement.bind(this);
this.setIndex = this.setIndex.bind(this);
this.indexOf = this.indexOf.bind(this);
this.clear = this.clear.bind(this);
this.length = this.length.bind(this);
this.printf = this.printf.bind(this);
}
// 边界检验
_errorBoundary(index) {
if (index < 0 || index >= this._size) {
throw `超出边界(${0}~${this._size}),目标位置${index}不存在!`;
}
}
// 根据索引获取目标对象
_getNodeByIndex(index) {
this._errorBoundary(index);
let obj = this._head;
for (let i = 0; i < index; i++) {
obj = obj.next;
}
return obj;
}
// 追加节点
append(element) {
if (this._size === 0) {
this._head = new LinkNode(element, null);
} else {
let obj = this._getNodeByIndex(this._size - 1);
obj.next = new LinkNode(element, null);
}
this._size++;
}
// 在索引位置插入节点
insert(element, index) {
if (index === 0) {
this._head = new LinkNode(element, this._head);
} else {
let obj = this._getNodeByIndex(index - 1);
obj.next = new LinkNode(element, obj.next);
}
this._size++;
}
// 删除第一个匹配到的节点
remove(element) {
if (this._size < 1) return null;
if (this._head.element == element) {
this._head.element = this._head.next;
this._size--;
return element;
} else {
let temp = this._head;
while (temp.next) {
if (temp.next.element == element) {
temp.next = temp.next.next;
this._size--;
return element;
} else {
temp = temp.next;
}
}
}
return null;
}
// 删除指定索引节点
removeAt(index) {
this._errorBoundary(index);
let element = null;
if (index === 0) {
element = this._head.element;
this._head = this._head.next;
} else {
let prev = this._getNodeByIndex(index - 1);
element = prev.next.element;
prev.next = prev.next.next;
}
this._size--;
return element;
}
// 删除所有匹配的节点
removeAll(element) {
// 创建虚拟头节点,
let v_head = new LinkNode(null, this._head);
let tempNode = v_head;
// let tempEle = null;
while (tempNode.next) {
if (tempNode.next.element == element) {
tempNode.next = tempNode.next.next;
this._size--;
// tempEle = element;
} else {
tempNode = tempNode.next;
}
}
this._head = v_head.next;
}
// 获取指定索引的节点信息
getElement(index) {
return this._getNodeByIndex(index).element;
}
// 修改指定索引的节点值
setIndex(element, index) {
this._errorBoundary(index);
let obj = this._getNodeByIndex(index);
obj.element = element;
}
// 获取某节点的索引位置
indexOf(element) {
let obj = this._head;
let index = -1;
for (let i = 0; i < this._size; i++) {
if (obj.element == element) {
index = i;
break;
}
obj = obj.next;
}
return index;
}
// 清除所有节点
clear() {
this._head = null;
this._size = 0;
}
// 返回节点长度
length() {
return this._size;
}
// 打印节点信息
printf() {
let obj = this._head;
const arr = [];
while (obj != null) {
arr.push(obj.element);
obj = obj.next;
}
const str = arr.join('->');
return str || null;
}
}
const obj = new LinkedList();
obj.append(0);
obj.append(1);
obj.append(2);
obj.printf();
// "0->1->2"
obj.insert(3, 3);
obj.printf();
// "0->1->2->3"
obj.remove(3);
obj.printf();
// "0->1->2"
obj.removeAt(0);
obj.printf();
// "1->2"
obj.setIndex(0, 0);
obj.printf();
// "0->2"
obj.indexOf(2);
// 1
obj.length();
// 2
obj.clear();
obj.printf();
// null
通过以上,我假装你明白什么是单向链表,并且能够用代码实现一个单向链表了,下一步开始说一说**迭代器** Iterator
2. Iterator¶
Iterator
翻译过来就是**迭代器(遍历器)**让我们先来看看它的遍历过程(类似于单向链表):
- 创建一个**指针对象**,指向当前数据结构的起始位置
- 第一次调用指针对象的
next
方法,将指针指向数据结构的第一个成员 - 第二次调用指针对象的
next
方法,将指针指向数据结构的第二个成员 - 不断的调用指针对象的
next
方法,直到它指向数据结构的结束位置
一个对象要变成可迭代的,必须实现 @@iterator
方法,即对象(或它原型链上的某个对象)必须有一个名字是 Symbol.iterator
的属性(原生具有该属性的有:字符串、数组、类数组的对象、Set 和 Map):
属性 | 值 |
---|---|
[Symbol.iterator]: | 返回一个对象的无参函数,被返回对象符合迭代器协议 |
当一个对象需要被迭代的时候(比如开始用于一个 for..of
循环中),它的 @@iterator
方法被调用并且无参数,然后返回一个用于在迭代中获得值的迭代器
迭代器协议:产生一个有限或无限序列的值,并且当所有的值都已经被迭代后,就会有一个默认的返回值
当一个对象只有满足下述条件才会被认为是一个迭代器:
它实现了一个 next()
的方法,该方法**必须返回一个对象**,对象有两个必要的属性:
done
(bool)
- true:迭代器已经超过了可迭代次数。这种情况下,value 的值可以被省略
-
如果迭代器可以产生序列中的下一个值,则为 false。这等效于没有指定 done 这个属性
-
value
迭代器返回的任何 JavaScript 值。done 为 true 时可省略
根据上面的规则,咱们来自定义一个简单的迭代器:
const makeIterator = arr => {
let nextIndex = 0;
return {
next: () =>
nextIndex < arr.length
? { value: arr[nextIndex++], done: false }
: { value: undefined, done: true },
};
};
const it = makeIterator(['人月', '神话']);
console.log(it.next()); // { value: "人月", done: false }
console.log(it.next()); // { value: "神话", done: false }
console.log(it.next()); // {value: undefined, done: true }
我们还可以自定义一个可迭代对象:
const myIterable = {};
myIterable[Symbol.iterator] = function*() {
yield 1;
yield 2;
yield 3;
};
for (let value of myIterable) {
console.log(value);
}
// 1
// 2
// 3
//or
console.log([...myIterable]); // [1, 2, 3]
了解了迭代器,下面可以进一步了解生成器了
3. Generator¶
Generator
:生成器对象是生成器函数(GeneratorFunction)返回的,它符合**可迭代协议**和**迭代器协议**,既是迭代器也是可迭代对象,可以调用 next
方法,但它不是函数,更不是构造函数
生成器函数(GeneratorFunction):
function* name([param[, param[, ... param]]]) { statements }
- name:函数名
- param:参数
- statements:js 语句
调用一个生成器函数并不会马上执行它里面的语句,而是返回一个这个生成器的迭代器对象,当这个迭代器的 next()
方法被首次(后续)调用时,其内的语句会执行到第一个(后续)出现 yield
的位置为止(让执行处于**暂停状**),yield
后紧跟迭代器要返回的值。或者如果用的是 yield*
(多了个星号),则表示将执行权移交给另一个生成器函数(当前生成器**暂停执行**),调用 next()
(再启动)方法时,如果传入了参数,那么这个参数会作为**上一条执行的 yield
语句的返回值**,例如:
function* another() {
yield '人月神话';
}
function* gen() {
yield* another(); // 移交执行权
const a = yield 'hello';
const b = yield a; // a='world' 是 next('world') 传参赋值给了上一个 yidle 'hello' 的左值
yield b; // b=! 是 next('!') 传参赋值给了上一个 yidle a 的左值
}
const g = gen();
g.next(); // {value: "人月神话", done: false}
g.next(); // {value: "hello", done: false}
g.next('world'); // {value: "world", done: false} 将 'world' 赋给上一条 yield 'hello' 的左值,即执行 a='world',
g.next('!'); // {value: "!", done: false} 将 '!' 赋给上一条 yield a 的左值,即执行 b='!',返回 b
g.next(); // {value: undefined, done: false}
看到这里,你可能会问,Generator
和 callback
有啥关系,如何处理异步呢?其实二者没有任何关系,我们只是通过一些方式强行的它们产生了关系,才会有 Generator
处理异步
我们来总结一下 Generator
的本质,暂停,它会让程序执行到指定位置先暂停(yield
),然后再启动(next
),再暂停(yield
),再启动(next
),而这个暂停就很容易让它和异步操作产生联系,因为我们在处理异步时:开始异步处理(网络求情、IO 操作),然后暂停一下,等处理完了,再该干嘛干嘛。不过值得注意的是,js 是单线程的(又重复了三遍),异步还是异步,callback 还是 callback,不会因为 Generator
而有任何改变
下面来看看,用 Generator
实现异步:
const promisify = require('util').promisify;
const path = require('path');
const fs = require('fs');
const readFile = promisify(fs.readFile);
const gen = function*() {
const res1 = yield readFile(path.resolve(__dirname, '../data/a.json'), { encoding: 'utf8' });
console.log(res1);
const res2 = yield readFile(path.resolve(__dirname, '../data/b.json'), { encoding: 'utf8' });
console.log(res2);
};
const g = gen();
const g1 = g.next();
console.log('g1:', g1);
g1.value
.then(res1 => {
console.log('res1:', res1);
const g2 = g.next(res1);
console.log('g2:', g2);
g2.value
.then(res2 => {
console.log('res2:', res2);
g.next(res2);
})
.catch(err2 => {
console.log(err2);
});
})
.catch(err1 => {
console.log(err1);
});
// g1: { value: Promise { <pending> }, done: false }
// res1: {
// "a": 1
// }
// {
// "a": 1
// }
// g2: { value: Promise { <pending> }, done: false }
// res2: {
// "b": 2
// }
// {
// "b": 2
// }
以上代码是 Generator
和 callback
结合实现的异步,可以看到,仍然需要手动执行 .then
层层添加回调,但由于 next()
方法返回对象 {value: xxx,done: true/false}
所以我们可以简化它,写一个自动执行器:
const promisify = require('util').promisify;
const path = require('path');
const fs = require('fs');
const readFile = promisify(fs.readFile);
function run(gen) {
const g = gen();
function next(data) {
const res = g.next(data);
// 深度递归,只要 `Generator` 函数还没执行到最后一步,`next` 函数就调用自身
if (res.done) return res.value;
res.value.then(function(data) {
next(data);
});
}
next();
}
run(function*() {
const res1 = yield readFile(path.resolve(__dirname, '../data/a.json'), { encoding: 'utf8' });
console.log(res1);
// {
// "a": 1
// }
const res2 = yield readFile(path.resolve(__dirname, '../data/b.json'), { encoding: 'utf8' });
console.log(res2);
// {
// "b": 2
// }
});
说了这么多,怎么还没有到 async/await
,客官别急,马上来了(其实我已经漏了一些内容没说:Promise 和 callback 的关系,thunk 函数,co 库,感兴趣的可以去 google 一下,ruanyifeng 老师讲的es6 入门非常棒,我时不时的都会去看一看)
4. Async/Await¶
首先,async/await
是 Generator
的语法糖,上面*我是分割线*下的第一句已经讲过,先来看一下二者的对比:
// Generator
run(function*() {
const res1 = yield readFile(path.resolve(__dirname, '../data/a.json'), { encoding: 'utf8' });
console.log(res1);
const res2 = yield readFile(path.resolve(__dirname, '../data/b.json'), { encoding: 'utf8' });
console.log(res2);
});
// async/await
const readFile = async ()=>{
const res1 = await readFile(path.resolve(__dirname, '../data/a.json'), { encoding: 'utf8' });
console.log(res1);
const res2 = await readFile(path.resolve(__dirname, '../data/b.json'), { encoding: 'utf8' });
console.log(res2);
return 'done';
}
const res = readFile();
可以看到,async function
代替了 function*
,await
代替了 yield
,同时也无需自己手写一个自动执行器 run
了
现在再来看看async/await
的特点:
- 当
await
后面跟的是 Promise 对象时,才会异步执行,其它类型的数据会同步执行 - 执行
const res = readFile();
返回的仍然是个 Promise 对象,上面代码中的return 'done';
会直接被下面then
函数接收到
res.then(data => {
console.log(data); // done
});
啊,终于完了,一个 async-await
连带出来这么多知识点,以后面试被问到它的原理时,希望能够帮助到你
【参考】:
- https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Iteration_protocols#%E5%8F%AF%E8%BF%AD%E4%BB%A3%E5%8D%8F%E8%AE%AE
- http://es6.ruanyifeng.com/#docs/iterator
- http://es6.ruanyifeng.com/#docs/async
原文地址:https://juejin.im/post/5d2c814c6fb9a07ecd3d8e43
第 10 题:(头条)异步笔试题¶
请写出下面代码的运行结果
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0)
async1();
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
console.log('script end');
解析:第 10 题
从一道题浅说 JavaScript 的事件循环
注:本篇文章运行环境为当前最新版本的谷歌浏览器(72.0.3626.109)
最近看到这样一道有关事件循环的前端面试题:
//请写出输出内容
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0)
async1();
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
console.log('script end');
/*
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
*/
这道题主要考察的是事件循环中函数执行顺序的问题,其中包括async
,await
,setTimeout
,Promise
函数。下面来说一下本题中涉及到的知识点。
任务队列
首先我们需要明白以下几件事情:
- JS分为同步任务和异步任务
- 同步任务都在主线程上执行,形成一个执行栈
- 主线程之外,事件触发线程管理着一个任务队列,只要异步任务有了运行结果,就在任务队列之中放置一个事件。
- 一旦执行栈中的所有同步任务执行完毕(此时JS引擎空闲),系统就会读取任务队列,将可运行的异步任务添加到可执行栈中,开始执行。
根据规范,事件循环是通过任务队列的机制来进行协调的。一个 Event Loop 中,可以有一个或者多个任务队列(task queue),一个任务队列便是一系列有序任务(task)的集合;每个任务都有一个任务源(task source),源自同一个任务源的 task 必须放到同一个任务队列,从不同源来的则被添加到不同队列。 setTimeout/Promise 等API便是任务源,而进入任务队列的是他们指定的具体执行任务。
宏任务
(macro)task(又称之为宏任务),可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)。
浏览器为了能够使得JS内部(macro)task与DOM任务能够有序的执行,会在一个(macro)task执行结束后,在下一个(macro)task 执行开始前,对页面进行重新渲染,流程如下:
(macro)task->渲染->(macro)task->...
(macro)task主要包含:script(整体代码)、setTimeout、setInterval、I/O、UI交互事件、postMessage、MessageChannel、setImmediate(Node.js 环境)
微任务
microtask(又称为微任务),可以理解是在当前 task 执行结束后立即执行的任务。也就是说,在当前task任务后,下一个task之前,在渲染之前。
所以它的响应速度相比setTimeout(setTimeout是task)会更快,因为无需等渲染。也就是说,在某一个macrotask执行完后,就会将在它执行期间产生的所有microtask都执行完毕(在渲染前)。
microtask主要包含:Promise.then、MutaionObserver、process.nextTick(Node.js 环境)
运行机制
在事件循环中,每进行一次循环操作称为 tick,每一次 tick 的任务处理模型是比较复杂的,但关键步骤如下:
- 执行一个宏任务(栈中没有就从事件队列中获取)
- 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
- 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
- 当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染
- 渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)
流程图如下:
Promise和async中的立即执行
我们知道Promise中的异步体现在then
和catch
中,所以写在Promise中的代码是被当做同步任务立即执行的。而在async/await中,在出现await出现之前,其中的代码也是立即执行的。那么出现了await时候发生了什么呢?
await做了什么
从字面意思上看await就是等待,await 等待的是一个表达式,这个表达式的返回值可以是一个promise对象也可以是其他值。
很多人以为await会一直等待之后的表达式执行完之后才会继续执行后面的代码,实际上await是一个让出线程的标志。await后面的表达式会先执行一遍,将await后面的代码加入到microtask中,然后就会跳出整个async函数来执行后面的代码。
这里感谢@chenjigeng的纠正:
由于因为async await 本身就是promise+generator的语法糖。所以await后面的代码是microtask。所以对于本题中的
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
等价于
async function async1() {
console.log('async1 start');
Promise.resolve(async2()).then(() => {
console.log('async1 end');
})
}
回到本题
以上就本道题涉及到的所有相关知识点了,下面我们再回到这道题来一步一步看看怎么回事儿。
- 首先,事件循环从宏任务(macrotask)队列开始,这个时候,宏任务队列中,只有一个script(整体代码)任务;当遇到任务源(task source)时,则会先分发任务到对应的任务队列中去。所以,上面例子的第一步执行如下图所示:
- 然后我们看到首先定义了两个async函数,接着往下看,然后遇到了
console
语句,直接输出script start
。输出之后,script 任务继续往下执行,遇到setTimeout
,其作为一个宏任务源,则会先将其任务分发到对应的队列中:
- script 任务继续往下执行,执行了async1()函数,前面讲过async函数中在await之前的代码是立即执行的,所以会立即输出
async1 start
。
遇到了await时,会将await后面的表达式执行一遍,所以就紧接着输出async2
,然后将await后面的代码也就是console.log('async1 end')
加入到microtask中的Promise队列中,接着跳出async1函数来执行后面的代码。
- script任务继续往下执行,遇到Promise实例。由于Promise中的函数是立即执行的,而后续的
.then
则会被分发到 microtask 的Promise
队列中去。所以会先输出promise1
,然后执行resolve
,将promise2
分配到对应队列。
- script任务继续往下执行,最后只有一句输出了
script end
,至此,全局任务就执行完毕了。
根据上述,每次执行完一个宏任务之后,会去检查是否存在 Microtasks;如果有,则执行 Microtasks 直至清空 Microtask Queue。
因而在script任务执行完毕之后,开始查找清空微任务队列。此时,微任务中, Promise
队列有的两个任务async1 end
和promise2
,因此按先后顺序输出 async1 end,promise2
。当所有的 Microtasks 执行完毕之后,表示第一轮的循环就结束了。
-
第二轮循环开始,这个时候就会跳回async1函数中执行后面的代码,然后遇到了同步任务console
语句,直接输出async1 end
。这样第二轮的循环就结束了。(也可以理解为被加入到script任务队列中,所以会先与setTimeout队列执行) -
第二轮循环依旧从宏任务队列开始。此时宏任务中只有一个
setTimeout
,取出直接输出即可,至此整个流程结束。
下面我会改变一下代码来加深印象。
变式一
在第一个变式中我将async2中的函数也变成了Promise函数,代码如下:
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
//async2做出如下更改:
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
}
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0)
async1();
new Promise(function(resolve) {
console.log('promise3');
resolve();
}).then(function() {
console.log('promise4');
});
console.log('script end');
可以先自己看看输出顺序会是什么,下面来公布结果:
script start
async1 start
promise1
promise3
script end
promise2
async1 end
promise4
setTimeout
在第一次macrotask执行完之后,也就是输出script end
之后,会去清理所有microtask。所以会相继输出promise2
,async1 end
,promise4
,其余不再多说。
变式二
在第二个变式中,我将async1中await后面的代码和async2的代码都改为异步的,代码如下:
async function async1() {
console.log('async1 start');
await async2();
//更改如下:
setTimeout(function() {
console.log('setTimeout1')
},0)
}
async function async2() {
//更改如下:
setTimeout(function() {
console.log('setTimeout2')
},0)
}
console.log('script start');
setTimeout(function() {
console.log('setTimeout3');
}, 0)
async1();
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
console.log('script end');
可以先自己看看输出顺序会是什么,下面来公布结果:
script start
async1 start
promise1
script end
promise2
setTimeout3
setTimeout2
setTimeout1
在输出为promise2
之后,接下来会按照加入setTimeout队列的顺序来依次输出,通过代码我们可以看到加入顺序为3 2 1
,所以会按3,2,1的顺序来输出。
变式三
变式三是我在一篇面经中看到的原题,整体来说大同小异,代码如下:
async function a1 () {
console.log('a1 start')
await a2()
console.log('a1 end')
}
async function a2 () {
console.log('a2')
}
console.log('script start')
setTimeout(() => {
console.log('setTimeout')
}, 0)
Promise.resolve().then(() => {
console.log('promise1')
})
a1()
let promise2 = new Promise((resolve) => {
resolve('promise2.then')
console.log('promise2')
})
promise2.then((res) => {
console.log(res)
Promise.resolve().then(() => {
console.log('promise3')
})
})
console.log('script end')
无非是在微任务那块儿做点文章,前面的内容如果你都看懂了的话这道题一定没问题的,结果如下:
script start
a1 start
a2
promise2
script end
promise1
a1 end
promise2.then
promise3
setTimeout
参考文章
第 11 题:(携程)算法手写题¶
已知如下数组:
var arr = [[1, 2, 2], [3, 4, 5, 5], [6, 7, 8, 9, [11, 12, [12, 13, [14] ] ] ], 10];
编写一个程序将数组扁平化去并除其中重复部分数据,最终得到一个升序且不重复的数组
Array.from(new Set(arr.flat(Infinity))).sort((a,b)=>{ return a-b})
第 12 题:(滴滴、挖财、微医、海康)JS 异步解决方案的发展历程以及优缺点。¶
解析:第 12 题JS 异步已经告一段落了,这里来一波小总结
1. 回调函数(callback)¶
setTimeout(() => {
// callback 函数体
}, 1000)
缺点:回调地狱,不能用 try catch 捕获错误,不能 return
回调地狱的根本问题在于:
- 缺乏顺序性: 回调地狱导致的调试困难,和大脑的思维方式不符
- 嵌套函数存在耦合性,一旦有所改动,就会牵一发而动全身,即(控制反转)
- 嵌套函数过多的多话,很难处理错误
ajax('XXX1', () => {
// callback 函数体
ajax('XXX2', () => {
// callback 函数体
ajax('XXX3', () => {
// callback 函数体
})
})
})
优点:解决了同步的问题(只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。)
2. Promise¶
Promise就是为了解决callback的问题而产生的。
Promise 实现了链式调用,也就是说每次 then 后返回的都是一个全新 Promise,如果我们在 then 中 return ,return 的结果会被 Promise.resolve() 包装
优点:解决了回调地狱的问题
ajax('XXX1')
.then(res => {
// 操作逻辑
return ajax('XXX2')
}).then(res => {
// 操作逻辑
return ajax('XXX3')
}).then(res => {
// 操作逻辑
})
缺点:无法取消 Promise ,错误需要通过回调函数来捕获
3. Generator¶
特点:可以控制函数的执行,可以配合 co 函数库使用
function *fetch() {
yield ajax('XXX1', () => {})
yield ajax('XXX2', () => {})
yield ajax('XXX3', () => {})
}
let it = fetch()
let result1 = it.next()
let result2 = it.next()
let result3 = it.next()
4. Async/await¶
async、await 是异步的终极解决方案
优点是:代码清晰,不用像 Promise 写一大堆 then 链,处理了回调地狱的问题
缺点:await 将异步代码改造成同步代码,如果多个异步操作没有依赖性而使用 await 会导致性能上的降低。
async function test() {
// 以下代码没有依赖性的话,完全可以使用 Promise.all 的方式
// 如果有依赖性的话,其实就是解决回调地狱的例子了
await fetch('XXX1')
await fetch('XXX2')
await fetch('XXX3')
}
下面来看一个使用 await
的例子:
let a = 0
let b = async () => {
a = a + await 10
console.log('2', a) // -> '2' 10
}
b()
a++
console.log('1', a) // -> '1' 1
对于以上代码你可能会有疑惑,让我来解释下原因
- 首先函数
b
先执行,在执行到await 10
之前变量a
还是 0,因为await
内部实现了generator
,generator
会保留堆栈中东西,所以这时候a = 0
被保存了下来 - 因为
await
是异步操作,后来的表达式不返回Promise
的话,就会包装成Promise.reslove(返回值)
,然后会去执行函数外的同步代码 - 同步代码执行完毕后开始执行异步代码,将保存下来的值拿出来使用,这时候
a = 0 + 10
上述解释中提到了 await
内部实现了 generator
,其实 await
就是 generator
加上 Promise
的语法糖,且内部实现了自动执行 generator
。如果你熟悉 co 的话,其实自己就可以实现这样的语法糖。
本文首发于我的博客:JS异步解决方案的发展历程以及优缺点
http://www.ruanyifeng.com/blog/2012/12/asynchronous%EF%BC%BFjavascript.html
第 13 题:(微医)Promise 构造函数是同步执行还是异步执行,那么 then 方法呢?¶
解析:第 13 题
const promise = new Promise((resolve, reject) => {
console.log(1)
resolve()
console.log(2)
})
promise.then(() => {
console.log(3)
})
console.log(4)
执行结果是:1243 promise构造函数是同步执行的,then方法是异步执行的
第 14 题:(兑吧)情人节福利题,如何实现一个 new¶
解析:第 14 题
这样写是不是简单点啊
function _new(fn, ...arg) {
const obj = Object.create(fn.prototype);
const ret = fn.apply(obj, arg);
return ret instanceof Object ? ret : obj;
}
第 15 题:(网易)简单讲解一下http2的多路复用¶
解析:第 15 题
HTTP2采用二进制格式传输,取代了HTTP1.x的文本格式,二进制格式解析更高效。 多路复用代替了HTTP1.x的序列和阻塞机制,所有的相同域名请求都通过同一个TCP连接并发完成。在HTTP1.x中,并发多个请求需要多个TCP连接,浏览器为了控制资源会有6-8个TCP连接都限制。 HTTP2中
- 同域名下所有通信都在单个连接上完成,消除了因多个 TCP 连接而带来的延时和内存消耗。
- 单个连接上可以并行交错的请求和响应,之间互不干扰
在 HTTP/1 中,每次请求都会建立一次HTTP连接,也就是我们常说的3次握手4次挥手,这个过程在一次请求过程中占用了相当长的时间,即使开启了 Keep-Alive ,解决了多次连接的问题,但是依然有两个效率上的问题:
- 第一个:串行的文件传输。当请求a文件时,b文件只能等待,等待a连接到服务器、服务器处理文件、服务器返回文件,这三个步骤。我们假设这三步用时都是1秒,那么a文件用时为3秒,b文件传输完成用时为6秒,依此类推。(注:此项计算有一个前提条件,就是浏览器和服务器是单通道传输)
- 第二个:连接数过多。我们假设Apache设置了最大并发数为300,因为浏览器限制,浏览器发起的最大请求数为6,也就是服务器能承载的最高并发为50,当第51个人访问时,就需要等待前面某个请求处理完成。
HTTP/2的多路复用就是为了解决上述的两个性能问题。 在 HTTP/2 中,有两个非常重要的概念,分别是帧(frame)和流(stream)。 帧代表着最小的数据单位,每个帧会标识出该帧属于哪个流,流也就是多个帧组成的数据流。 多路复用,就是在一个 TCP 连接中可以存在多条流。换句话说,也就是可以发送多个请求,对端可以通过帧中的标识知道属于哪个请求。通过这个技术,可以避免 HTTP 旧版本中的队头阻塞问题,极大的提高传输性能。
第 16 题:谈谈你对TCP三次握手和四次挥手的理解¶
解析:第 16 题
TCP的内容
https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/15
第 17 题:A、B 机器正常连接后,B 机器突然重启,问 A 此时处于 TCP 什么状态
如果A 与 B 建立了正常连接后,从未相互发过数据,这个时候 B 突然机器重启,问 A 此时处于 TCP 什么状态?如何消除服务器程序中的这个状态?(超纲题,了解即可)
解析:第 17 题
第 18 题:(微医)React 中 setState 什么时候是同步的,什么时候是异步的?¶
解析:第 18 题
在React中,如果是由React引发的事件处理(比如通过onClick引发的事件处理),调用setState不会同步更新this.state,除此之外的setState调用会同步执行this.state 。所谓“除此之外”,指的是绕过React通过addEventListener直接添加的事件处理函数,还有通过setTimeout/setInterval产生的异步调用。
原因: 在React的setState函数实现中,会根据一个变量isBatchingUpdates判断是直接更新this.state还是放到队列中回头再说,而isBatchingUpdates默认是false,也就表示setState会同步更新this.state,但是,有一个函数batchedUpdates,这个函数会把isBatchingUpdates修改为true,而当React在调用事件处理函数之前就会调用这个batchedUpdates,造成的后果,就是由React控制的事件处理过程setState不会同步更新this.state。
注意: setState的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形式了所谓的“异步”,当然可以通过第二个参数 setState(partialState, callback) 中的callback拿到更新后的结果。
详细请看 深入 setState 机制
这里所说的同步异步, 并不是真正的同步异步, 它还是同步执行的。
这里的异步指的是多个state会合成到一起进行批量更新。
希望初学者不要被误导
第 19 题:React setState 笔试题,下面的代码输出什么?¶
class Example extends React.Component {
constructor() {
super();
this.state = {
val: 0
};
}
componentDidMount() {
this.setState({val: this.state.val + 1});
console.log(this.state.val); // 第 1 次 log
this.setState({val: this.state.val + 1});
console.log(this.state.val); // 第 2 次 log
setTimeout(() => {
this.setState({val: this.state.val + 1});
console.log(this.state.val); // 第 3 次 log
this.setState({val: this.state.val + 1});
console.log(this.state.val); // 第 4 次 log
}, 0);
}
render() {
return null;
}
};
解析:第 19 题
1、第一次和第二次都是在 react 自身生命周期内,触发时 isBatchingUpdates 为 true,所以并不会直接执行更新 state,而是加入了 dirtyComponents,所以打印时获取的都是更新前的状态 0。
2、两次 setState 时,获取到 this.state.val 都是 0,所以执行时都是将 0 设置成 1,在 react 内部会被合并掉,只执行一次。设置完成后 state.val 值为 1。
3、setTimeout 中的代码,触发时 isBatchingUpdates 为 false,所以能够直接进行更新,所以连着输出 2,3。
输出: 0 0 2 3
第 20 题:介绍下 npm 模块安装机制,为什么输入 npm install 就可以自动安装对应的模块?¶
解析:第 20 题
1. npm 模块安装机制:¶
- 发出
npm install
命令 - 查询node_modules目录之中是否已经存在指定模块
- 若存在,不再重新安装
- 若不存在
- npm 向 registry 查询模块压缩包的网址
- 下载压缩包,存放在根目录下的
.npm
目录里 - 解压压缩包到当前项目的
node_modules
目录
2. npm 实现原理¶
输入 npm install 命令并敲下回车后,会经历如下几个阶段(以 npm 5.5.1 为例):
- 执行工程自身 preinstall
当前 npm 工程如果定义了 preinstall 钩子此时会被执行。
- 确定首层依赖模块
首先需要做的是确定工程中的首层依赖,也就是 dependencies 和 devDependencies 属性中直接指定的模块(假设此时没有添加 npm install 参数)。
工程本身是整棵依赖树的根节点,每个首层依赖模块都是根节点下面的一棵子树,npm 会开启多进程从每个首层依赖模块开始逐步寻找更深层级的节点。
- 获取模块
获取模块是一个递归的过程,分为以下几步:
- 获取模块信息。在下载一个模块之前,首先要确定其版本,这是因为 package.json 中往往是 semantic version(semver,语义化版本)。此时如果版本描述文件(npm-shrinkwrap.json 或 package-lock.json)中有该模块信息直接拿即可,如果没有则从仓库获取。如 packaeg.json 中某个包的版本是 ^1.1.0,npm 就会去仓库中获取符合 1.x.x 形式的最新版本。
- 获取模块实体。上一步会获取到模块的压缩包地址(resolved 字段),npm 会用此地址检查本地缓存,缓存中有就直接拿,如果没有则从仓库下载。
-
查找该模块依赖,如果有依赖则回到第1步,如果没有则停止。
-
模块扁平化(dedupe)
上一步获取到的是一棵完整的依赖树,其中可能包含大量重复模块。比如 A 模块依赖于 loadsh,B 模块同样依赖于 lodash。在 npm3 以前会严格按照依赖树的结构进行安装,因此会造成模块冗余。
从 npm3 开始默认加入了一个 dedupe 的过程。它会遍历所有节点,逐个将模块放在根节点下面,也就是 node-modules 的第一层。当发现有**重复模块**时,则将其丢弃。
这里需要对**重复模块**进行一个定义,它指的是**模块名相同**且 **semver 兼容。**每个 semver 都对应一段版本允许范围,如果两个模块的版本允许范围存在交集,那么就可以得到一个**兼容**版本,而不必版本号完全一致,这可以使更多冗余模块在 dedupe 过程中被去掉。
比如 node-modules 下 foo 模块依赖 lodash@^1.0.0,bar 模块依赖 lodash@^1.1.0,则 ^1.1.0 为兼容版本。
而当 foo 依赖 lodash@^2.0.0,bar 依赖 lodash@^1.1.0,则依据 semver 的规则,二者不存在兼容版本。会将一个版本放在 node_modules 中,另一个仍保留在依赖树里。
举个例子,假设一个依赖树原本是这样:
node_modules -- foo ---- lodash@version1
-- bar ---- lodash@version2
假设 version1 和 version2 是兼容版本,则经过 dedupe 会成为下面的形式:
node_modules -- foo
-- bar
-- lodash(保留的版本为兼容版本)
假设 version1 和 version2 为非兼容版本,则后面的版本保留在依赖树中:
node_modules -- foo -- lodash@version1
-- bar ---- lodash@version2
- 安装模块
这一步将会更新工程中的 node_modules,并执行模块中的生命周期函数(按照 preinstall、install、postinstall 的顺序)。
- 执行工程自身生命周期
当前 npm 工程如果定义了钩子此时会被执行(按照 install、postinstall、prepublish、prepare 的顺序)。
最后一步是生成或更新版本描述文件,npm install 过程完成。
第 21 - 30 题¶
第 21 题:有以下 3 个判断数组的方法,请分别介绍它们之间的区别和优劣¶
Object.prototype.toString.call() 、 instanceof 以及 Array.isArray()
1. Object.prototype.toString.call()¶
每一个继承 Object 的对象都有 toString
方法,如果 toString
方法没有重写的话,会返回 [Object type]
,其中 type 为对象的类型。但当除了 Object 类型的对象外,其他类型直接使用 toString
方法时,会直接返回都是内容的字符串,所以我们需要使用call或者apply方法来改变toString方法的执行上下文。
const an = ['Hello','An'];
an.toString(); // "Hello,An"
Object.prototype.toString.call(an); // "[object Array]"
这种方法对于所有基本的数据类型都能进行判断,即使是 null 和 undefined 。
Object.prototype.toString.call('An') // "[object String]"
Object.prototype.toString.call(1) // "[object Number]"
Object.prototype.toString.call(Symbol(1)) // "[object Symbol]"
Object.prototype.toString.call(null) // "[object Null]"
Object.prototype.toString.call(undefined) // "[object Undefined]"
Object.prototype.toString.call(function(){}) // "[object Function]"
Object.prototype.toString.call({name: 'An'}) // "[object Object]"
Object.prototype.toString.call()
常用于判断浏览器内置对象时。
更多实现可见 谈谈 Object.prototype.toString
2. instanceof¶
instanceof
的内部机制是通过判断对象的原型链中是不是能找到类型的 prototype
。
使用 instanceof
判断一个对象是否为数组,instanceof
会判断这个对象的原型链上是否会找到对应的 Array
的原型,找到返回 true
,否则返回 false
。
[] instanceof Array; // true
但 instanceof
只能用来判断对象类型,原始类型不可以。并且所有对象类型 instanceof Object 都是 true。
[] instanceof Object; // true
3. Array.isArray()¶
-
功能:用来判断对象是否为数组
-
instanceof 与 isArray
当检测Array实例时,Array.isArray
优于 instanceof
,因为 Array.isArray
可以检测出 iframes
var iframe = document.createElement('iframe');
document.body.appendChild(iframe);
xArray = window.frames[window.frames.length-1].Array;
var arr = new xArray(1,2,3); // [1,2,3]
// Correctly checking for Array
Array.isArray(arr); // true
Object.prototype.toString.call(arr); // true
// Considered harmful, because doesn't work though iframes
arr instanceof Array; // false
Array.isArray()
与Object.prototype.toString.call()
Array.isArray()
是ES5新增的方法,当不存在 Array.isArray()
,可以用 Object.prototype.toString.call()
实现。
if (!Array.isArray) {
Array.isArray = function(arg) {
return Object.prototype.toString.call(arg) === '[object Array]';
};
}
第 22 题:介绍下重绘和回流(Repaint & Reflow),以及如何进行优化¶
解析:第 22 题
1. 浏览器渲染机制¶
- 浏览器采用流式布局模型(
Flow Based Layout
) - 浏览器会把
HTML
解析成DOM
,把CSS
解析成CSSOM
,DOM
和CSSOM
合并就产生了渲染树(Render Tree
)。 - 有了
RenderTree
,我们就知道了所有节点的样式,然后计算他们在页面上的大小和位置,最后把节点绘制到页面上。 - 由于浏览器使用流式布局,对
Render Tree
的计算通常只需要遍历一次就可以完成,但table
及其内部元素除外,他们可能需要多次计算,通常要花3倍于同等元素的时间,这也是为什么要避免使用table
布局的原因之一。
2. 重绘¶
由于节点的几何属性发生改变或者由于样式发生改变而不会影响布局的,称为重绘,例如outline
, visibility
, color
、background-color
等,重绘的代价是高昂的,因为浏览器必须验证DOM树上其他节点元素的可见性。
3. 回流¶
回流是布局或者几何属性需要改变就称为回流。回流是影响浏览器性能的关键因素,因为其变化涉及到部分页面(或是整个页面)的布局更新。一个元素的回流可能会导致了其所有子元素以及DOM中紧随其后的节点、祖先节点元素的随后的回流。
<body>
<div class="error">
<h4>我的组件</h4>
<p><strong>错误:</strong>错误的描述…</p>
<h5>错误纠正</h5>
<ol>
<li>第一步</li>
<li>第二步</li>
</ol>
</div>
</body>
在上面的HTML片段中,对该段落(<p>
标签)回流将会引发强烈的回流,因为它是一个子节点。这也导致了祖先的回流(div.error
和body
– 视浏览器而定)。此外,<h5>
和<ol>
也会有简单的回流,因为其在DOM中在回流元素之后。大部分的回流将导致页面的重新渲染。
回流必定会发生重绘,重绘不一定会引发回流。
4. 浏览器优化¶
现代浏览器大多都是通过队列机制来批量更新布局,浏览器会把修改操作放在队列中,至少一个浏览器刷新(即16.6ms)才会清空队列,但当你**获取布局信息的时候,队列中可能有会影响这些属性或方法返回值的操作,即使没有,浏览器也会强制清空队列,触发回流与重绘来确保返回正确的值**。
主要包括以下属性或方法:
offsetTop
、offsetLeft
、offsetWidth
、offsetHeight
scrollTop
、scrollLeft
、scrollWidth
、scrollHeight
clientTop
、clientLeft
、clientWidth
、clientHeight
width
、height
getComputedStyle()
getBoundingClientRect()
所以,我们应该避免频繁的使用上述的属性,他们都会强制渲染刷新队列。
5. 减少重绘与回流¶
-
CSS
-
使用
transform
替代top
-
使用
visibility
替换display: none
,因为前者只会引起重绘,后者会引发回流(改变了布局 -
避免使用
table
布局,可能很小的一个小改动会造成整个table
的重新布局。 -
尽可能在
DOM
树的最末端改变class
,回流是不可避免的,但可以减少其影响。尽可能在DOM树的最末端改变class,可以限制了回流的范围,使其影响尽可能少的节点。 -
避免设置多层内联样式,CSS 选择符**从右往左**匹配查找,避免节点层级过多。
<div> <a> <span></span> </a> </div> <style> span { color: red; } div > a > span { color: red; } </style>
对于第一种设置样式的方式来说,浏览器只需要找到页面中所有的
span
标签然后设置颜色,但是对于第二种设置样式的方式来说,浏览器首先需要找到所有的span
标签,然后找到span
标签上的a
标签,最后再去找到div
标签,然后给符合这种条件的span
标签设置颜色,这样的递归过程就很复杂。所以我们应该尽可能的避免写**过于具体**的 CSS 选择器,然后对于 HTML 来说也尽量少的添加无意义标签,保证**层级扁平**。 -
将动画效果应用到
position
属性为absolute
或fixed
的元素上,避免影响其他元素的布局,这样只是一个重绘,而不是回流,同时,控制动画速度可以选择requestAnimationFrame
,详见探讨 requestAnimationFrame。 -
避免使用
CSS
表达式,可能会引发回流。 -
将频繁重绘或者回流的节点设置为图层,图层能够阻止该节点的渲染行为影响别的节点,例如
will-change
、video
、iframe
等标签,浏览器会自动将该节点变为图层。 -
CSS3 硬件加速(GPU加速),使用css3硬件加速,可以让
transform
、opacity
、filters
这些动画不会引起回流重绘 。但是对于动画的其它属性,比如background-color
这些,还是会引起回流重绘的,不过它还是可以提升这些动画的性能。 -
JavaScript
-
避免频繁操作样式,最好一次性重写
style
属性,或者将样式列表定义为class
并一次性更改class
属性。 - 避免频繁操作
DOM
,创建一个documentFragment
,在它上面应用所有DOM操作
,最后再把它添加到文档中。 - 避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来。
- 对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流。
第 23 题:介绍下观察者模式和订阅-发布模式的区别,各自适用于什么场景¶
解析:第 23 题
可不可以理解 为 观察者模式没中间商赚差价 发布订阅模式 有中间商赚差价
- 发布-订阅模式就好像报社, 邮局和个人的关系,报纸的订阅和分发是由邮局来完成的。报社只负责将报纸发送给邮局。
- 观察者模式就好像 个体奶农和个人的关系。奶农负责统计有多少人订了产品,所以个人都会有一个相同拿牛奶的方法。奶农有新奶了就负责调用这个方法。
https://juejin.cn/post/6844903513009422343
第 24 题:聊聊 Redux 和 Vuex 的设计思想¶
解析:第 24 题
知乎上看到的一篇文章:Vuex、Flux、Redux、Redux-saga、Dva、MobX(https://zhuanlan.zhihu.com/p/53599723)
新的连接: https://zhuanlan.zhihu.com/p/53599723
第 25 题:说说浏览器和 Node 事件循环的区别¶
解析:第 25 题
为楼上补充一个例子
原文出自liubasara的个人博客
function test () {
console.log('start')
setTimeout(() => {
console.log('children2')
Promise.resolve().then(() => {console.log('children2-1')})
}, 0)
setTimeout(() => {
console.log('children3')
Promise.resolve().then(() => {console.log('children3-1')})
}, 0)
Promise.resolve().then(() => {console.log('children1')})
console.log('end')
}
test()
// 以上代码在node11以下版本的执行结果(先执行所有的宏任务,再执行微任务)
// start
// end
// children1
// children2
// children3
// children2-1
// children3-1
// 以上代码在node11及浏览器的执行结果(顺序执行宏任务和微任务)
// start
// end
// children1
// children2
// children2-1
// children3
// children3-1
题目应该是:浏览器和node的事件循环的区别吧,
先上链接:
第一个链接里面大佬讲的已经非常透彻了我来总结一下。
浏览器¶
关于微任务和宏任务在浏览器的执行顺序是这样的:
- 执行一只task(宏任务)
- 执行完micro-task队列 (微任务)
如此循环往复下去
浏览器的task(宏任务)执行顺序在 html#event-loops 里面有讲就不翻译了 常见的 task(宏任务) 比如:setTimeout、setInterval、script(整体代码)、 I/O 操作、UI 渲染等。 常见的 micro-task 比如: new Promise().then(回调)、MutationObserver(html5新特性) 等。
Node¶
Node的事件循环是libuv实现的,引用一张官网的图:
大体的task(宏任务)执行顺序是这样的:
- timers定时器:本阶段执行已经安排的 setTimeout() 和 setInterval() 的回调函数。
- pending callbacks待定回调:执行延迟到下一个循环迭代的 I/O 回调。
- idle, prepare:仅系统内部使用。
- poll 轮询:检索新的 I/O 事件;执行与 I/O 相关的回调(几乎所有情况下,除了关闭的回调函数,它们由计时器和 setImmediate() 排定的之外),其余情况 node 将在此处阻塞。
- check 检测:setImmediate() 回调函数在这里执行。
- close callbacks 关闭的回调函数:一些准备关闭的回调函数,如:socket.on('close', ...)。
微任务和宏任务在Node的执行顺序
Node 10以前:
- 执行完一个阶段的所有任务
- 执行完nextTick队列里面的内容
- 然后执行完微任务队列的内容
Node 11以后: 和浏览器的行为统一了,都是每执行一个宏任务就执行完微任务队列。
第 26 题:介绍模块化发展历程¶
可从IIFE、AMD、CMD、CommonJS、UMD、webpack(require.ensure)、ES Module、