[관련 추천: 프론트엔드 면접 질문(2020)]
1. React/Vue 프로젝트 작성 시 컴포넌트에 키를 작성해야 하는 이유와 그 기능은 무엇인가요?
키의 기능은 diff 알고리즘이 실행될 때 해당 노드를 더 빠르게 찾아 diff 속도를 향상시키는 것입니다.
vue와 React는 모두 diff 알고리즘을 사용하여 새 가상 노드와 이전 가상 노드를 비교하여 노드를 업데이트합니다. Vue의 diff 기능에서. 먼저 diff 알고리즘을 이해할 수 있습니다.
교차 비교 중에 새 노드와 이전 노드 간의 교차 비교 결과가 없으면 이전 노드 배열의 키를 새 노드의 키에 따라 비교하여 해당 이전 노드를 찾습니다(여기서는 키 => 인덱스 맵 매핑에 해당합니다). 찾을 수 없는 경우 새 노드로 간주됩니다. 키가 없으면 순회 검색 방법을 사용하여 해당 이전 노드를 찾습니다. 하나는 지도 매핑이고, 다른 하나는 순회 검색입니다. 비교하면. 지도 매핑이 더 빠릅니다.
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)
지도 기능 생성:
function createKeyToOldIdx (children, beginIdx, endIdx) { let i, key const map = {} for (i = beginIdx; i <= 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 < end; i++) { const c = oldCh[i] if (isDef(c) && sameVnode(node, c)) return i } }
2. 3'].map(parseInt)
처음 이 질문을 봤을 때 떠오른 답은 [1, 2, 3]이었는데 실제 답은 [1, NaN, NaN]이었습니다.
먼저 map 함수의 첫 번째 매개변수 콜백을 살펴보겠습니다.
var new_array = arr.map(function callback(currentValue[, index[, array]]) { // Return element for new_array }[, thisArg])
이 콜백은 총 3개의 매개변수를 받을 수 있으며, 첫 번째 매개변수는 현재 처리 중인 요소를 나타내고 두 번째 매개변수는 인덱스를 나타냅니다.
그리고parseInt는 문자열을 구문 분석하는 데 사용되어 문자열을 지정된 밑수를 가진 정수로 만듭니다.
parseInt(string, radix)는 두 개의 매개변수를 받습니다. 첫 번째 매개변수는 처리할 값(문자열)을 나타내고 두 번째 매개변수는 구문 분석 중 기수를 나타냅니다.
이 두 함수를 이해한 후 작업을 시뮬레이션할 수 있습니다.
parseInt('1', 0) // radix가 0이고 문자열 매개변수가 "0x" 및 "0"으로 시작하지 않는 경우 다음을 따르세요. 10 기본 번호에 대해 처리되었습니다. 이때 1이 반환됩니다;
parseInt('2', 1) // 1진수(base 1)로 표현되는 숫자 중 최대값이 2보다 작아서 파싱할 수 없고 NaN이 반환됩니다.
parseInt('3', 2) // 2진수(바이너리)로 표현되는 숫자 중 최대값이 3보다 작아서 파싱할 수 없으며 NaN이 반환됩니다.
map 함수는 배열을 반환하므로 최종 결과는 [1, NaN, NaN]입니다.
3. 흔들림 방지 및 스로틀링이란 무엇인가요? 차이점은 무엇입니까? 달성하는 방법?
1) 흔들림 방지
이 기능은 고주파 이벤트가 발생한 후 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)); // 防抖
2) Throttling
고빈도 이벤트가 발생하지만 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));
4. Set, Map, WeakSet 및 WeakMap의 차이점을 소개하세요.
1) Set
멤버는 고유하고 순서가 없으며 반복되지 않습니다.
[값, 값], 키 값과 키 이름이 일치합니다(또는 키 값만, 키 이름 없음).
순회할 수 있으며 메소드는 추가, 삭제, 보유입니다.2) WeakSet
멤버는 모두 객체입니다. 멤버는 모두 약한 참조이고 가비지 수집 메커니즘으로 재활용할 수 있으며 DOM 노드를 저장하는 데 사용할 수 있으며 메모리 누수가 발생하지 않습니다. 순회되며 메소드는 add,delete,has입니다.3) Map
은 본질적으로 컬렉션과 유사한 키-값 쌍의 컬렉션입니다.
탐색할 수 있고 다양한 메서드가 있으며 다양한 데이터 형식으로 변환할 수 있습니다.4) WeakMap
은 객체만 키 이름(null 제외)으로 허용하고 다른 유형의 값을 키 이름으로 허용하지 않습니다.
키 이름은 약한 참조이며 키 값은 무엇이든 가능합니다. , 키 이름으로 가리키는 개체 가비지 수집될 수 있으며 현재 키 이름은 유효하지 않습니다. 탐색할 수 없으며 메서드에는 get, set, has 및 delete가 포함됩니다. 5. 깊이 우선 순회와 너비 우선 순회를 도입하고 이를 구현하는 방법은 무엇인가요?DFS(깊이 우선 순회)
깊이 우선 검색은 트리의 깊이를 따라 트리의 노드를 순회하여 가능한 한 깊이까지 트리를 검색하는 검색 알고리즘입니다. 노드 v의 모든 모서리를 탐색한 후 노드 v가 발견된 모서리의 시작 노드로 역추적을 수행합니다. 이 과정은 소스 노드가 다른 모든 노드에 대해 탐색될 때까지 계속됩니다. 아직 탐색되지 않은 노드가 있는 경우 탐색되지 않은 노드 중 하나를 소스 노드로 선택하고 모든 노드가 탐색될 때까지 위의 작업을 반복합니다.
简单的说,DFS 就是从图中的一个节点开始追溯,直到最后一个节点,然后回溯,继续追溯下一条路径,直到到达所有的节点,如此往复,直到没有路径为止。
DFS 可以产生相应图的拓扑排序表,利用拓扑排序表可以解决很多问题,例如最大路径问题。一般用堆数据结构来辅助实现 DFS 算法。
注意:深度 DFS 属于盲目搜索,无法保证搜索到的路径为最短路径,也不是在搜索特定的路径,而是通过搜索来查看图中有哪些路径可以选择。
步骤:
访问顶点 v;
依次从 v 的未被访问的邻接点出发,对图进行深度优先遍历;直至图中和 v 有路径相通的顶点都被访问;
若此时途中尚有顶点未被访问,则从一个未被访问的顶点出发,重新进行深度优先遍历,直到所有顶点均被访问过为止。
实现:
Graph.prototype.dfs = function() { var marked = [] for (var i=0; i<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<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 > 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<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
测试成功。
6. 异步笔试题
请写出下面代码的运行结果:
// 今日头条面试题 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 start async1 start async2 promise1 script end async1 end promise2 settimeout
7. 将数组扁平化并去除其中重复数据,最终得到一个升序且不重复的数组
Array.from(new Set(arr.flat(Infinity))).sort((a,b)=>{ return a-b})
8.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가 내부적으로 생성기를 구현한다고 언급한 위의 설명은 실제로 wait는 생성기의 구문 설탕에 Promise를 더한 것이며 내부적으로 생성기의 자동 실행을 구현합니다. co에 익숙하다면 실제로 이러한 구문 설탕을 직접 구현할 수 있습니다.
9. TCP 3방향 핸드셰이크와 4방향 웨이브에 대한 이해에 대해 이야기해 주세요.