本文共 8365 字,大约阅读时间需要 27 分钟。
近日,GitHub上一位名为木易杨(yygmind)的开发者,在 GitHub 中建了一个名为Advanced-Frontend/Daily-Interview-Question项目,该项目每天会更新一道前端大厂面试题,并邀请开发者在issue区中作答,以下是我们从该项目中挑选的9道题和答案,希望能给大家一些帮助。
GitHub链接:
key的作用是为了在diff算法执行时更快的找到对应的节点,提高diff速度。
vue和react都是采用diff算法来对比新旧虚拟节点,从而更新节点。在vue的diff函数中。可以先了解一下diff算法。
在交叉对比的时候,当新节点跟旧节点头尾交叉对比
没有结果的时候,会根据新节点的key去对比旧节点数组中的key,从而找到相应旧节点(这里对应的是一个key =\u0026gt; index 的map映射)。如果没找到就认为是一个新增节点。而如果没有key,那么就会采用一种遍历查找的方式去找到对应的旧节点。一种一个map映射,另一种是遍历查找。相比而言。map映射的速度更快。
vue部分源码如下:
// vue项目 src/core/vdom/patch.js -488行// oldCh 是一个旧虚拟节点数组, if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
创建map函数:
function createKeyToOldIdx (children, beginIdx, endIdx) { let i, key const map = {} for (i = beginIdx; i \u0026lt;= endIdx; ++i) { key = children[i].key if (isDef(key)) map[key] = i } return map}
遍历寻找:
// sameVnode 是对比新旧节点是否相同的函数 function findIdxInOld (node, oldCh, start, end) { for (let i = start; i \u0026lt; end; i++) { const c = oldCh[i] if (isDef(c) \u0026amp;\u0026amp; sameVnode(node, c)) return i } }
本题链接:
第一眼看到这个题目的时候,脑海跳出的答案是 [1, 2, 3],但是真正的答案是[1, NaN, NaN]。
var new_array = arr.map(function callback(currentValue[, index[, array]]) { // Return element for new_array }[, thisArg])
这个callback一共可以接收三个参数,其中第一个参数代表当前被处理的元素,而第二个参数代表该元素的索引。
parseInt(string, radix)
接收两个参数,第一个表示被处理的值(字符串),第二个表示为解析时的基数。
parseInt(‘1’, 0) //radix为0时,且string参数不以“0x”和“0”开头时,按照10为基数处理。这个时候返回1;
parseInt(‘2’, 1) //基数为1(1进制)表示的数中,最大值小于2,所以无法解析,返回NaN;
parseInt(‘3’, 2) //基数为2(2进制)表示的数中,最大值小于3,所以无法解析,返回NaN。
map函数返回的是一个数组,所以最后结果为[1, NaN, NaN]。
最后附上MDN上对于这两个函数的链接,具体参数大家可以到里面看:
本题链接:
触发高频事件后n秒内函数只会执行一次,如果n秒内高频事件再次被触发,则重新计算时间;
每次触发事件时都取消之前的延时调用方法:
function debounce(fn) { let timeout = null; // 创建一个标记用来存放定时器的返回值 return function () { clearTimeout(timeout); // 每当用户输入的时候把前一个 setTimeout clear 掉 timeout = setTimeout(() =\u0026gt; { // 然后又创建一个新的 setTimeout, 这样就能保证输入字符后的 interval 间隔内如果还有字符输入的话,就不会执行 fn 函数 fn.apply(this, arguments); }, 500); }; } function sayHi() { console.log('防抖成功'); } var inp = document.getElementById('inp'); inp.addEventListener('input', debounce(sayHi)); // 防抖
2.节流
高频事件触发,但在n秒内只会执行一次,所以节流会稀释函数的执行频率。
每次触发事件时都判断当前是否有等待执行的延时函数。
function throttle(fn) { let canRun = true; // 通过闭包保存一个标记 return function () { if (!canRun) return; // 在函数开头判断标记是否为true,不为true则return canRun = false; // 立即设置为false setTimeout(() =\u0026gt; { // 将外部传入的函数的执行放在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));
本题链接:
Set
成员唯一、无序且不重复;
[value, value],键值与键名是一致的(或者说只有键值,没有键名);
可以遍历,方法有:add、delete、has。
WeakSet
成员都是对象;
成员都是弱引用,可以被垃圾回收机制回收,可以用来保存DOM节点,不容易造成内存泄漏;
不能遍历,方法有add、delete、has。
Map
本质上是键值对的集合,类似集合;
可以遍历,方法很多,可以跟各种数据格式转换。
WeakMap
只接受对象最为键名(null除外),不接受其他类型的值作为键名;
键名是弱引用,键值可以是任意的,键名所指向的对象可以被垃圾回收,此时键名是无效的;
不能遍历,方法有get、set、has、delete。
本题链接:
深度优先遍历(DFS)
深度优先遍历(Depth-First-Search),是搜索算法的一种,它沿着树的深度遍历树的节点,尽可能深地搜索树的分支。当节点v的所有边都已被探寻过,将回溯到发现节点v的那条边的起始节点。这一过程一直进行到已探寻源节点到其他所有节点为止,如果还有未被发现的节点,则选择其中一个未被发现的节点为源节点并重复以上操作,直到所有节点都被探寻完成。
简单的说,DFS就是从图中的一个节点开始追溯,直到最后一个节点,然后回溯,继续追溯下一条路径,直到到达所有的节点,如此往复,直到没有路径为止。
DFS 可以产生相应图的拓扑排序表,利用拓扑排序表可以解决很多问题,例如最大路径问题。一般用堆数据结构来辅助实现DFS算法。
注意:深度DFS属于盲目搜索,无法保证搜索到的路径为最短路径,也不是在搜索特定的路径,而是通过搜索来查看图中有哪些路径可以选择。
步骤:
访问顶点v;
依次从v的未被访问的邻接点出发,对图进行深度优先遍历;直至图中和v有路径相通的顶点都被访问;
若此时途中尚有顶点未被访问,则从一个未被访问的顶点出发,重新进行深度优先遍历,直到所有顶点均被访问过为止。
实现:
Graph.prototype.dfs = function() { var marked = [] for (var i=0; i\u0026lt;this.vertices.length; i++) { if (!marked[this.vertices[i]]) { dfsVisit(this.vertices[i]) } } function dfsVisit(u) { let edges = this.edges marked[u] = true console.log(u) var neighbors = edges.get(u) for (var i=0; i\u0026lt;neighbors.length; i++) { var w = neighbors[i] if (!marked[w]) { dfsVisit(w) } } }}
测试:
graph.dfs()// 1// 4// 3// 2// 5
测试成功。
广度优先遍历(BFS)
广度优先遍历(Breadth-First-Search)是从根节点开始,沿着图的宽度遍历节点,如果所有节点均被访问过,则算法终止,BFS 同样属于盲目搜索,一般用队列数据结构来辅助实现BFS。
BFS从一个节点开始,尝试访问尽可能靠近它的目标节点。本质上这种遍历在图上是逐层移动的,首先检查最靠近第一个节点的层,再逐渐向下移动到离起始节点最远的层。
步骤:
创建一个队列,并将开始节点放入队列中;
若队列非空,则从队列中取出第一个节点,并检测它是否为目标节点;
若队列为空,表示图中并没有目标节点,则结束遍历。
实现:
Graph.prototype.bfs = function(v) { var queue = [], marked = [] marked[v] = true queue.push(v) // 添加到队尾 while(queue.length \u0026gt; 0) { var s = queue.shift() // 从队首移除 if (this.edges.has(s)) { console.log('visited vertex: ', s) } let neighbors = this.edges.get(s) for(let i=0;i\u0026lt;neighbors.length;i++) { var w = neighbors[i] if (!marked[w]) { marked[w] = true queue.push(w) } } }}
测试:
graph.bfs(1)// visited vertex: 1// visited vertex: 4// visited vertex: 3// visited vertex: 2// visited vertex: 5
测试成功。
本题链接:
请写出下面代码的运行结果:
// 今日头条面试题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')})async1()new Promise(function (resolve) { console.log('promise1') resolve()}).then(function () { console.log('promise2')})console.log('script end')
题目的本质,就是考察setTimeout
、promise
、async await
的实现及执行顺序,以及JS的事件循环的相关问题。
答案:
script startasync1 startasync2promise1script endasync1 endpromise2settimeout
过程详解链接:
Array.from(new Set(arr.flat(Infinity))).sort((a,b)=\u0026gt;{ return a-b})
本题链接:
1. 回调函数(callback)
setTimeout(() =\u0026gt; { // callback 函数体}, 1000)
缺点:回调地狱,不能用 try catch 捕获错误,不能 return
回调地狱的根本问题在于:
缺乏顺序性: 回调地狱导致的调试困难,和大脑的思维方式不符;
嵌套函数存在耦合性,一旦有所改动,就会牵一发而动全身,即(控制反转);
嵌套函数过多的多话,很难处理错误。
ajax('XXX1', () =\u0026gt; { // callback 函数体 ajax('XXX2', () =\u0026gt; { // callback 函数体 ajax('XXX3', () =\u0026gt; { // callback 函数体 }) })})
优点:解决了同步的问题(只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行)。
2. Promise
Promise就是为了解决callback的问题而产生的。
Promise 实现了链式调用,也就是说每次 then 后返回的都是一个全新 Promise,如果我们在 then 中 return ,return 的结果会被 Promise.resolve() 包装。
优点:解决了回调地狱的问题。
ajax('XXX1') .then(res =\u0026gt; { // 操作逻辑 return ajax('XXX2') }).then(res =\u0026gt; { // 操作逻辑 return ajax('XXX3') }).then(res =\u0026gt; { // 操作逻辑 })
缺点:无法取消 Promise ,错误需要通过回调函数来捕获。
3. Generator
特点:可以控制函数的执行,可以配合 co 函数库使用。
function *fetch() { yield ajax('XXX1', () =\u0026gt; {}) yield ajax('XXX2', () =\u0026gt; {}) yield ajax('XXX3', () =\u0026gt; {})}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 = 0let b = async () =\u0026gt; { a = a + await 10 console.log('2', a) // -\u0026gt; '2' 10}b()a++console.log('1', a) // -\u0026gt; '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 的话,其实自己就可以实现这样的语法糖。
本题链接:
本题链接:
更多内容,请关注前端之巅。
会议推荐2019年6月,GMTC全球大前端技术大会2019即将到来。小程序、Flutter、移动AI、工程化、性能优化…大前端的下一站在哪里?点击下图了解更多详情。
转载地址:http://omsxx.baihongyu.com/