前言
在面试中场景题是经常会考到的,本文将介绍几个常考的场景题,分别是数组、对象的扁平化,自动重新发送请求,事件循环,虚拟DOM渲染。
正文
场景一:数组的扁平化
问题描述: 给定一个可能包含多层嵌套数组的数组,将其转换为一个单一维度的数组。例如,将数组 [1, [2, 3, [4]]]
转换为 [1, 2, 3, 4]
。
示例代码:
const arr = [1, [2, 3, [4]]];
// 方法一:使用ES6的flat方法
const arr1 = arr.flat(Infinity);
console.log(arr1); // 输出: [1, 2, 3, 4]
// 方法二:手写一个flatArr方法
function flatArr(arr) {
let res = [];
for (let item of arr) {
if (Array.isArray(item)) {
// 如果当前项还是数组,则递归调用flatArr
res = res.concat(flatArr(item));
} else {
// 否则直接将元素加入结果数组
res.push(item);
}
}
return res;
}
console.log(flatArr(arr)); // 输出: [1, 2, 3, 4]
// 方法三:使用字符串分割和转换
function flatArrWithString(arr) {
let res = arr.toString();
return res.split(',').map(item => Number(item));
}
console.log(flatArrWithString(arr)); // 输出: [1, 2, 3, 4]
解释:
-
使用ES6的flat方法:
arr.flat(Infinity)
是ES6提供的方法,Infinity可以无限次地展开数组,也可以自己写。- 这种方法简单易用,但在旧版浏览器中可能不支持。
-
手写的flatArr方法:
flatArr
函数通过递归方式检查数组中的每一项,如果是数组,则继续展开,通过concat
方法拼接;如果不是,则直接添加到结果数组中。- 这种方法适用于任何版本的JavaScript,并且易于理解和实现。
-
使用字符串分割和转换:
- 将数组转换为字符串,然后通过逗号分割,最后再转换回数字。
场景二:对象的扁平化
问题描述: 给定一个可能包含多层嵌套的对象,将其转换为扁平化的形式。例子在为下面
示例代码:
const obj={
a:1,
b:[1,2,{c:true},[3]],
d:{e:2,f:3},
g:null
}
// const obj2={
// a:1,
// 'b[0]':1,
// 'b[1]':1,
// 'b[2].c':true,
// 'b[3][0]':3,
// 'd.e':2,
// 'd.f':3,
// // g:null
// }
// 把obj转化为obj2
const flattenRes=flattenObj(obj)
console.log(flattenRes);
function flattenObj(obj) {
let res = {}; // 初始化一个空对象用来存储结果
const help = (target, oldKey) => { // 定义一个辅助函数
for (let key in target) { // 遍历传入对象的所有属性
let newKey;
if (oldKey) { // 如果存在父级键名
if (Array.isArray(target)) {
newKey = `${oldKey}[${key}]`; // 数组的情况下,键名以方括号形式表示层级
} else {
newKey = `${oldKey}.${key}`; // 对象的情况下,键名以点的形式表示层级
}
} else {
if (Array.isArray(target)) {
newKey = `[${key}]`; // 如果顶级元素是数组,则直接使用方括号表示
} else {
newKey = key; // 否则使用属性名本身作为键名
}
}
if (Object.prototype.toString.call(target[key]) === '[object Object]' || Array.isArray(target[key])) {
help(target[key], newKey); // 如果值还是对象或者数组,则递归调用
} else if (target[key] !== null && target[key] !== undefined) {
res[newKey] = target[key]; // 如果值不是 null 或者 undefined,则将其存入结果对象
}
}
};
help(obj, ''); // 调用辅助函数开始扁平化过程
return res; // 返回扁平化后的对象
}
解释:
flattenObj
函数使用递归方式遍历对象的所有属性。- 如果遇到嵌套的对象或数组,则继续调用自身进行扁平化。
- 最终结果是一个新的对象,其中的键名表示了原对象中属性的层级关系。
场景三:实现自动重新发送请求
问题描述: 实现一个函数,用于发送请求。如果请求失败,则自动重新发送请求,直到请求成功或者达到规定的最大尝试次数为止。
示例代码:
function getData(){
console.log('发送请求')
const n=Math.random()
return new Promise((resolve,reject)=>{
setTimeout(()=>{
if(n>0.9){
resolve(n)
}else{
reject(n)
}
},1000)
})
}
function again(promiseFn, times = 5) {
let err = null
return new Promise(async (resolve, reject) => {
while (times) {
try {
let ret = await promiseFn()
resolve(ret)
break;
} catch (error) {
times -= 1
err = error
}
}
if (!times) {
reject(err)
}
})
}
again(getData).then(res=>{
console.log(`发送成功:${res}`)
})
.catch(error=>{
console.log(`发送失败:${error}`);
})
效果:
请求成功
的效果:
请求失败
的效果:
解释:
getData:
人为模拟了一个发送请求的过程,并且我们在这里用随机生成的数字模拟了请求可能失败。again
函数负责处理请求的重试逻辑,如果请求成功就把结果resolve
出来,停止循环,如果请求失败并且还有剩余次数,则再次调用promiseFn
即上面的getData
。- 最后,我们通过
.then
和.catch
方法来处理请求的结果或错误。
场景四:事件循环的理解
问题描述: 分析以下代码的执行顺序。
示例代码:
setImmediate(() => {
console.log(1);
},0)
setTimeout(() => {
console.log(2);
}, 0)
new Promise((resolve, reject) => {
console.log(3);
resolve()
console.log(4);
}).then(() => {
console.log(5);
})
async function test() {
const a = await 9
console.log(a);
const b = await new Promise((resolve) => {
resolve(10);
})
console.log(b);
}
test()
console.log(6);
process.nextTick(() => {
console.log(7);
})
console.log(8);
//打印结果:
//3
//4
//6
//8
//7
//5
//9
//10
//2
//1
解释:
v8从上到下执行过程
setImmediate(() => { console.log(1); });
异步代码,放入宏任务队列中,在当前事件循环结束时执行,(当前事件循环最后一个执行)。setTimeout(() => { console.log(2); }, 0);
异步代码,放入宏任务队列中。console.log(3);
和console.log(4);
在Promise构造函数中,是同步代码立即执行,打印3,4。then(() => { console.log(5); });
异步代码,放入微任务队列中。test()
的调用带来了const a = await 9;
,有await后面代码不用管,放入微任务队列中。console.log(6);
同步代码立即执行,打印6。process.nextTick(() => { console.log(7); });
异步代码,放入微任务队列中(会在当前同步代码执行完之后立即执行,可以理解为现实生活中大家都排队,但是它上面有关系,我上面有人,第一个执行)。console.log(8);
同步代码立即执行,打印8。
同步代码执行完,执行异步代码
process.nextTick(() => { console.log(7); });
执行打印7。then(() => { console.log(5); });
执行打印5。console.log(a)
执行打印9。const b = await new Promise
有await后面代码不用管,放入当前事件循环微任务队列的末尾。- 这时只有
console.log(b)
一个微任务,执行打印10;
微任务代码执行完,执行宏任务代码
setTimeout(() => { console.log(2); }, 0);
执行打印2。setImmediate(() => { console.log(1); });
执行打印1。
场景五:虚拟DOM渲染
问题描述: 给定一个虚拟DOM节点,将其渲染到页面上。
示例代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="root"></div>
<script>
//给定的dom代码
const vnode = {
tag: 'div',
attrs: {
id: 'app',
className: 'box',
style: {width: '100px', height: '100px',background: 'red'}
},
children: [
{
tag: 'span',
children: [{
tag: 'a',
children: [],
}],
},
{
tag: 'span',
children: [{
tag: 'a',
children: [],
}]
}
]
}
render(vnode, document.getElementById('root'));
function render(vnode, container) { // 定义 render 函数,接受虚拟 DOM 和容器作为参数
const newDom = createDom(vnode); // 调用 createDom 函数创建 DOM 节点
container.appendChild(newDom); // 将创建的 DOM 节点添加到容器中
}
function createDom(vnode) { // 定义 createDom 函数,用于创建 DOM 节点
const { tag, attrs, children } = vnode; // 解构 vnode 的属性
const dom = document.createElement(tag); // 创建 DOM 节点
if (typeof attrs === 'object' && attrs !== null) { // 如果 attrs 是一个对象且不为空
updateProps(dom, {}, attrs); // 更新 DOM 节点的属性
}
if (children.length > 0) { // 如果有子节点
reconcileChildren(children, dom); // 递归渲染子节点
}
return dom; // 返回创建的 DOM 节点
}
function updateProps(dom, oldProps = {}, newProps = {}) { // 更新 DOM 节点的属性
for (const key in newProps) { // 遍历新的属性对象
if (key === 'style') { // 如果是样式属性
let styleObj = newProps[key]; // 获取样式对象
for (let attr in styleObj) { // 遍历样式对象
dom.style[attr] = styleObj[attr]; // 设置 DOM 节点的样式属性
}
} else { // 其他属性
dom[key] = newProps[key]; // 设置 DOM 节点的其他属性
}
}
}
function reconcileChildren(children, dom) { // 递归渲染子节点
for (let child of children) { // 遍历子节点
render(child, dom); // 递归调用 render 函数渲染子节点
}
}
</script>
</body>
</html>
效果:
解释:
vnode
定义了一个虚拟DOM树。render
函数负责将虚拟DOM树渲染到页面上的指定容器中。createDom
函数创建真实的DOM节点,并设置其属性。updateProps
函数更新DOM节点的属性。reconcileChildren
函数递归地处理子节点,确保所有子节点都被正确地渲染到DOM树中。
总结
本文到此就结束了,希望这五个场景题能对你有所帮助,感谢你的阅读!