1. 1. 你在长列表优化中做了哪些具体的事情?
    1. 1.1. 1. 虚拟滚动
    2. 1.2. 2. 分页和无限滚动
      1. 1.2.1. 分页
      2. 1.2.2. 无限滚动
    3. 1.3. 3. 微观优化:必须渲染长列表时
    4. 1.4. 4. 其他辅助优化
  2. 2. 数组扁平化、去重、排序
  3. 3. 数组扁平化与去重
  4. 4. 数组扁平化(数组降维)
    1. 4.1. Array.prototype.flat() 方法
    2. 4.2. Array.prototype.flat() 特性总结
  5. 5. 手动实现扁平化方法
    1. 5.1. 方法一:使用 reduce 方法
      1. 5.1.1. 一次性扁平化所有 ***** (important)
      2. 5.1.2. 实现 flat 函数
    2. 5.2. 方法二:使用栈
      1. 5.2.1. 一次性降维所有
  6. 6. 数组去重
    1. 6.1. 方式一:Set(ES6)
    2. 6.2. 方式二:reduce
    3. 6.3. 方法三:filter
    4. 6.4. 方法四:indexOf
  7. 7. 数组原地去重方法
  8. 8. 方法一:排序去重
  9. 9. 方法二:优化版本
  10. 10. 两种方法的比较
  11. 11. 数组去重函数(支持对象和数组元素)
    1. 11.1. 需求说明
  12. 12. 基础解法:使用 JSON.stringify
  13. 13. 问题分析
  14. 14. 解决方案
    1. 14.1. 解决思路
    2. 14.2. 代码实现
  15. 15. 总结
    1. 15.1.
    2. 15.2. 手写aysnc和await
    3. 15.3. 合并两个有序数组
  16. 16. js判断数据类型
  17. 17. map实现reduce
  18. 18. Jsx和React.createElement()
  19. 19. AST转换
  20. 20. 预加载
    1. 20.1. 浅拷贝和深拷贝
    2. 20.2. JSON方法深拷贝的局限性
  21. 21. DFS,二叉树,前中后序,以及插入部分BFS的理论知识
    1. 21.1. 👉 DFS 先序 / 后序
    2. 21.2. 👉 二叉树前中后序
    3. 21.3. BFS应用
    4. 21.4. 面试官:DFS和BFS的区别?在前端有哪些场景应用?
    5. 21.5. 类数组转化为真实数组
    6. 21.6. 原地打乱数组
    7. 21.7. 判断对象是否存在循环引用
    8. 21.8. 反转字符串
    9. 21.9. 实现一个once函数,传入函数参数只执行一次
    10. 21.10. 实现sleep效果
    11. 21.11. 实现一个instanceof
    12. 21.12. 单例模式

2025年|前端面试练习题

你在长列表优化中做了哪些具体的事情?

核心思路:避免同时渲染大量的DOM节点,因为DOM节点的创建、渲染和内存占用都非常消耗性能。

1. 虚拟滚动

  • 计算可视区域:监听容器的滚动事件,获取 scrollTopscrollTop + clientHeight
  • 计算渲染区间:根据可视区域和每个列表项的高度,计算出需要渲染的起始索引 startIndex 和结束索引 endIndex。通常会上下多渲染 5-10 个项,防止滚动时出现白屏
  • 动态渲染和定位
    • 只渲染 [startIndex, endIndex] 区间的列表项
    • 容器高度设置为所有列表项的总高度,通过”撑开容器”实现
    • 对渲染的列表项使用 position: absolutetransform: translateY(...),设置 top 属性为 索引 × 列表项高度
  • 性能优化
    • 使用 requestAnimationFrame 优化动画或 Intersection Observer 优化滚动监听
    • 缓存已计算的位置,对于动态高度的列表需要在渲染后测量并缓存高度
  • 使用的库/技术
    • React: react-window, react-virtualized
    • Vue: vue-virtual-scroller
    • 也可根据业务需求使用 Intersection Observer API 自己实现简化版

2. 分页和无限滚动

分页

  • 实现方式:明确将数据分成多页,用户通过点击页码或”加载更多”按钮请求新数据
  • 优点:实现简单,SEO友好,内存压力小
  • 缺点:交互不连贯,需要用户主动操作

无限滚动

  • 实现方式:当用户滚动到底部附近时,自动加载下一页数据并追加到当前列表
  • 优点:用户体验流畅,适合内容流(社交媒体、商品列表)
  • 缺点:实现复杂,需要管理滚动位置和加载状态;性能随滚动逐渐下降;页脚可能无法到达
  • 优化点:可与虚拟滚动结合,即使加载很多数据,DOM节点数也保持恒定

3. 微观优化:必须渲染长列表时

  • 优化key属性

    • 使用唯一且稳定的key(如数据中的 id),而不是数组的index
    • 帮助框架准确识别哪些节点可被复用、移动或销毁
  • 避免内联对象和函数

    • 将内联样式对象和函数提取到组件外部,或使用 useCallback/useMemo 进行记忆化
    • 避免因引用不同导致的不必要重渲染
  • 简化子组件

    • 将列表项拆分成独立的轻量子组件
    • 使用 React.memo(函数组件)或 PureComponent(类组件)进行包裹
  • 不可变数据与结构共享

    • 使用不可变数据的方式更新列表数据(ES6 扩展运算符、Immer.js 等)
    • React.memo 的浅比较快速失效,精确重渲染发生变化的项
  • 控制更新粒度

    • 将频繁更新的状态(如点赞按钮)下放到每个列表项组件自身
    • 使用状态管理库避免触发整个列表的根组件更新

4. 其他辅助优化

  • 图片懒加载

    • 使用 loading="lazy" 属性或 Intersection Observer
    • 减少页面初始化时的 HTTP 请求数和带宽占用
  • 使用 CSS 属性 contain: content

    • 告诉浏览器元素的子树独立于页面其余部分
    • 减少重绘和重排时的计算量,提升滚动性能

数组扁平化、去重、排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var arr = [ [1, 2, 2], [3, 4, 5, 5], [6, 7, 8, 9, [11, 12, [12, 13, [14] ] ] ], 10];

// 扁平化
let flatArr = arr.flat(4);

// 去重
let disArr = Array.from(new Set(flatArr));

// 排序
let result = disArr.sort(function(a, b) {
return a - b;
});

console.log(result);

数组扁平化与去重

数组扁平化(数组降维)

Array.prototype.flat() 方法

MDN:flat() 方法会按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const test = ["a", ["b", "c"], ["d", ["e", ["f"]], "g"]]

// 不传参数时,默认扁平化一层
test.flat()
// ["a", "b", "c", "d", ["e", ["f"]], "g"]

// 传入一个整数参数,整数即扁平化的层数
test.flat(2)
// ["a", "b", "c", "d", "e", ["f"], "g"]

// Infinity 关键字作为参数时,无论多少层嵌套,都会转为一维数组
test.flat(Infinity)
// ["a", "b", "c", "d", "e", "f", "g"]

// 传入 <=0 的整数将返回原数组,不扁平化
test.flat(0)
test.flat(-10)
// ["a", ["b", "c"], ["d", ["e", ["f"]], "g"]]

// 如果原数组有空位,******flat()方法会跳过空位。
["a", "b", "c", "d",,].flat()
// ["a", "b", "c", "d"]

Array.prototype.flat() 特性总结

  • 用于将嵌套的数组扁平化,变成一维的数组
  • 该方法返回一个新数组,对原数据没有影响
  • 不传参数时,默认扁平化一层
  • 可以传入一个整数,表示想要扁平化的层数
  • 传入 <=0 的整数将返回原数组,不扁平化
  • Infinity 关键字作为参数时,无论多少层嵌套,都会转为一维数组
  • 如果原数组有空位,Array.prototype.flat() 会跳过空位

手动实现扁平化方法

方法一:使用 reduce 方法

一次性扁平化所有 ***** (important)

1
2
3
4
5
6
7
8
9
10
function flattenDeep(arr) { 
return Array.isArray(arr)
? arr.reduce( (acc, cur) => [...acc, ...flattenDeep(cur)] , [])
: [arr]
}

// 测试
var test = ["a", ["b", "c"], ["d", ["e", ["f"]], "g"]]
flattenDeep(test)
// ["a", "b", "c", "d", "e", "f", "g"]

实现 flat 函数

  1. 作业帮一面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
function flat(arr, depth = 1) {
return depth > 0
? arr.reduce((acc, cur) => {
if(Array.isArray(cur)) {
return [...acc, ...flat(cur, depth-1)]
}
return [...acc, cur]
} , [])
: arr
}

// 测试
var test = ["a", ["b", "c"], ["d", ["e", ["f"]], "g"]]

// 不传参数时,默认扁平化一层
flat(test)
// ["a", "b", "c", "d", ["e", ["f"]], "g"]

// 传入一个整数参数,整数即扁平化的层数
flat(test, 2)
// ["a", "b", "c", "d", "e", ["f"], "g"]

// Infinity 关键字作为参数时,无论多少层嵌套,都会转为一维数组
flat(test, Infinity)
// ["a", "b", "c", "d", "e", "f", "g"]

// 传入 <=0 的整数将返回原数组,不扁平化
flat(test, 0)
flat(test, -10)
// ["a", ["b", "c"], ["d", ["e", ["f"]], "g"]];

// 如果原数组有空位,flat()方法会跳过空位。
var arr = ["a", "b", "c", "d",,]
flat(arr)
// ["a", "b", "c", "d"]

方法二:使用栈

一次性降维所有

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function flattenDeep(arr) {
const result = []
// 将数组元素拷贝至栈,直接赋值会改变原数组
const stack = [...arr]
// 如果栈不为空,则循环遍历
while (stack.length !== 0) {
const val = stack.pop()
if (Array.isArray(val)) {
// 如果是数组再次入栈,并且展开了一层
stack.push(...val)
} else {
// 如果不是数组,就用头插法插入到结果数组中
result.unshift(val)
}
}
return result
}

// 测试
var test = ["a", ["b", "c"], ["d", ["e", ["f"]], "g"]]
flattenDeep(test)
// ["a", "b", "c", "d", "e", "f", "g"]

数组去重

方式一:Set(ES6)

1
2
3
4
5
6
7
8
9
function unique(arr) {
return Array.from(new Set(arr))
}
// 或者
var unique = arr => [...new Set(arr)]

// 测试
var arr = [1, 2, 2, 3]
unique(arr); // [1, 2, 3]

方式二:reduce

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function unique (arr) {
return arr.sort().reduce((acc, cur) => {
if (acc.length === 0 || acc[acc.length - 1] !== cur) {
acc.push(cur);
}
return acc
}, [])
}

cosnt uniqueArr = arr.reduce((acc, cur) => {
if(!acc.includes(cur)){
acc.push(cur)
}
return acc
}, [])

// 测试
var arr = [1, 2, 2, 3]
unique(arr); // [1, 2, 3]

方法三:filter

1
2
3
4
5
6
7
8
9
function unique(arr) { 
return arr.filter( (element, index, array) => {
return array.indexOf(element) === index
})
}

// 测试
var arr = [1, 2, 2, 3]
unique(arr); // [1, 2, 3]

方法四:indexOf

1
2
3
4
5
6
7
const arr = [1,23,44,44,3]
const uniqueArr = []
arr.forEach((value) => {
if(uniqueArr.indexOf(value) === -1){ // if(!uniqueArr.includes(value))
uniqueArr.push(value)
}
})

数组原地去重方法

方法一:排序去重

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const removeDuplicates = (nums) => {
// 原地排序
nums.sort()
// 去重
let len = 1
for (let i = 1; i < nums.length; i++)
if (nums[i] != nums[i-1]) nums[len++] = nums[i];
// 删除重复项
nums.splice(len)
return nums
}

// 测试
removeDuplicates([1, 2, 3, 1, 3])
// [1, 2, 3]

特点:

  • 使用 sort() 方法进行原地排序
  • 通过双指针技巧在排序后的数组中去除重复项
  • 最后使用 splice() 删除多余的重复元素

方法二:优化版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const removeDuplicates = (nums) => {
let len = nums.length - 1
for(let i = len; i>=0; i--) {
if(nums.indexOf(nums[i]) != i) {
nums[i] = nums[len --]
}
}
// 删除重复项
nums.splice(len+1)
return nums
}

// 测试
removeDuplicates([1, 2, 3, 1, 3])
// [1, 2, 3]

特点:

  • 从后向前遍历数组
  • 使用 indexOf 检查当前元素是否为重复项
  • 将重复元素与数组末尾元素交换,然后缩短数组长度
  • 最后使用 splice() 删除末尾的重复元素

两种方法的比较

方法 优点 缺点 是否保持原顺序
排序去重 效率较高 改变元素原始顺序
优化版本 保持元素原始顺序 效率相对较低

数组去重函数(支持对象和数组元素)

需求说明

实现一个数组去重函数,能够处理包含对象、数组等复杂类型的元素:

  • 输入:[123, "meili", "123", "mogu", 123]

  • 输出:[123, "meili", "123", "mogu"]

  • 输入:[123, [1, 2, 3], [1, "2", 3], [1, 2, 3], "meili"]

  • 输出:[123, [1, 2, 3], [1, "2", 3], "meili"]

  • 输入:[123, {a: 1}, {a: {b: 1}}, {a: "1"}, {a: {b: 1}}, "meili"]

  • 输出:[123, {a: 1}, {a: {b: 1}}, {a: "1"}, "meili"]

基础解法:使用 JSON.stringify

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const removeDuplicates = (arr) => {
let map = new Map()
arr.forEach(item => {
map.set(JSON.stringify(item), item)
})
return [...map.values()]
}

// 测试
removeDuplicates([123, "meili", "123", "mogu", 123])
// [123, "meili", "123", "mogu"]

removeDuplicates([123, [1, 2, 3], [1, "2", 3], [1, 2, 3], "meili"])
// [123, [1, 2, 3], [1, "2", 3], "meili"]

removeDuplicates([123, {a: 1}, {a: {b: 1}}, {a: "1"}, {a: {b: 1}}, "meili"])
// [123, {a: 1}, {a: {b: 1}}, {a: "1"}, "meili"]

问题分析

使用 JSON.stringify 的局限性:

1
2
3
4
5
6
7
8
9
10
11
let o1 = {a:1, b:2}
let o2 = {b:2, a:1}

JSON.stringify(o1)
// "{"a":1,"b":2}"

JSON.stringify(o2)
// "{"b":2,"a":1}"

JSON.stringify(o1) === JSON.stringify(o2)
// false

问题:对象键顺序不同会被认为是不同的元素。

解决方案

解决思路

一个数组(包含对象等类型元素)去重函数,需要在基础类型判断相等条件下满足以下条件:

  • 如果元素是数组类型,则需要数组中的每一项相等
  • 如果元素是对象类型,则需要对象中的每个键值对相等

去重本身就是遍历数组,然后比较数组中的每一项是否相等而已,所以关键步骤有两步:比较去重

比较

  1. 首先判断类型是否一致,类型不一致则认为两个数组元素是不同的
  2. 如果是数组类型,则递归比较数组中的每个元素是否相等
  3. 如果是对象类型,则递归比较对象中的每个键值对是否相等
  4. 否则,直接 === 比较

去重

  • 采用 reduce 去重,初始 accumulator[]
  • 采用 findIndex 找到 accumulator 是否包含相同元素
  • 如果不包含则加入,否则不加入
  • 返回最终的 accumulator,则为去重后的数组

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// 获取类型
const getType = (function() {
const class2type = {
'[object Boolean]': 'boolean',
'[object Number]': 'number',
'[object String]': 'string',
'[object Function]': 'function',
'[object Array]': 'array',
'[object Date]': 'date',
'[object RegExp]': 'regexp',
'[object Object]': 'object',
'[object Error]': 'error',
'[object Symbol]': 'symbol'
}

return function getType(obj) {
if (obj == null) {
return obj + ''
}
// javascript高级程序设计中提供了一种方法,可以通用的来判断原始数据类型和引用数据类型
const str = Object.prototype.toString.call(obj)
return typeof obj === 'object' || typeof obj === 'function' ? class2type[str] || 'object' : typeof obj
};
})();

/**
* 判断两个元素是否相等
* @param {any} o1 比较元素
* @param {any} o2 其他元素
* @returns {Boolean} 是否相等
*/
const isEqual = (o1, o2) => {
const t1 = getType(o1)
const t2 = getType(o2)

// 比较类型是否一致
if (t1 !== t2) return false

// 类型一致
if (t1 === 'array') {
// 首先判断数组包含元素个数是否相等
if (o1.length !== o2.length) return false
// 比较两个数组中的每个元素
return o1.every((item, i) => {
return isEqual(item, o2[i])
})
}

if (t1 === 'object') {
// object类型比较类似数组
const keysArr = Object.keys(o1)
if (keysArr.length !== Object.keys(o2).length) return false
// 比较每一个元素
return keysArr.every(k => {
return isEqual(o1[k], o2[k])
})
}

return o1 === o2
}

// 数组去重
const removeDuplicates = (arr) => {
return arr.reduce((accumulator, current) => {
const hasIndex = accumulator.findIndex(item => isEqual(current, item))
if (hasIndex === -1) {
accumulator.push(current)
}
return accumulator
}, [])
}

// 测试
removeDuplicates([123, {a: 1}, {a: {b: 1}}, {a: "1"}, {a: {b: 1}}, "meili", {a:1, b:2}, {b:2, a:1}])
// [123, {a: 1}, {a: {b: 1}}, {a: "1"}, "meili", {a: 1, b: 2}]

总结

这种方法解决了 JSON.stringify 在面对对象键顺序不同时的局限性,通过深度比较实现了真正的对象内容去重。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

// 模拟异步数据获取
async function* asyncNumberGenerator() {
yield await Promise.resolve(1);
yield await Promise.resolve(2);
yield await Promise.resolve(3);
}

// 使用 for await...of 遍历
async function processNumbers() {
for await (const num of asyncNumberGenerator()) {
console.log(num); // 依次输出: 1, 2, 3
}
}
processNumbers();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 模拟多个异步操作
const fetchUserData = (id, delay) =>
new Promise(resolve =>
setTimeout(() => resolve(`用户${id}的数据`), delay)
);

const promises = [
fetchUserData(1, 1000),
fetchUserData(2, 500),
fetchUserData(3, 1500)
];

async function processUsers() {
console.log('开始获取用户数据...');

for await (const userData of promises) {
console.log('收到:', userData);
// 按完成顺序输出,不是按数组顺序
}

console.log('所有数据获取完成');
}
processUsers();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Node.js 文件系统示例
import { createReadStream } from 'fs';
import { createInterface } from 'readline';

async function readLargeFile() {
const fileStream = createReadStream('large-file.txt');
const rl = createInterface({
input: fileStream,
crlfDelay: Infinity
});

let lineCount = 0;

for await (const line of rl) {
lineCount++;
console.log(`第${lineCount}行:`, line);

// 可以在这里处理每一行,不会阻塞其他操作
if (lineCount >= 10) {
console.log('只显示前10行');
break;
}
}

console.log(`总共处理了 ${lineCount} 行`);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

// 同步 vs 异步迭代对比
async function compareIteration() {
const syncArray = [1, 2, 3];
const asyncArray = [
Promise.resolve(1),
Promise.resolve(2),
Promise.resolve(3)
];

console.log('=== 同步迭代 ===');
for (const item of syncArray) {
console.log(item); // 立即输出 1, 2, 3
}

console.log('=== 异步迭代 ===');
for await (const item of asyncArray) {
console.log(item); // 等待 Promise 解决后输出 1, 2, 3
}
}
compareIteration();

手写aysnc和await

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

ffunction asyncToGenerator(genFunc) {
return function (...args) {
const gen = genFunc.apply(this, args);

return new Promise((resolve, reject) => {
function step(key, arg) {
let result;
try {
result = gen[key](arg);
} catch (err) {
return reject(err);
}

const { value, done } = result;

if (done) {
return resolve(value);
}

Promise.resolve(value).then(
val => step("next", val),
err => step("throw", err)
);
}

step("next");
});
};
}

// ----------------------------
// 测试代码
// ----------------------------
function* testGen() {
const a = yield Promise.resolve(1);
const b = yield Promise.resolve(a + 1);
return b + 1;
}

const test = asyncToGenerator(testGen);

test().then(res => {
console.log("结果: ", res); // 3
});


合并两个有序数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

let merge = function(nums1, m, nums2, n){
let len1 = m-1,
len2 = n-1,
len = m+n-1
while(len2 >=0){
if(len1<0){
nums1[len--] = nums2[len2--]
continue
}
nums1[len--] = nums1[len1] >= nums2[len2] ? nums1[len1--] : nums2[len2--]
}

}


js判断数据类型

typeof 判断
Array.isArray()
instanceof
Object.prototype.toString.call()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
typeof 123          // "number"
typeof "abc" // "string"
typeof true // "boolean"
typeof undefined // "undefined"
typeof {} // "object"
typeof [] // "object" ⚠️数组也返回 object
typeof null // "object" ⚠️历史 bug
typeof function(){} // "function"


Array.isArray([]) // true
Array.isArray({}) // false

[] instanceof Array // true
{} instanceof Object // true
new Date() instanceof Date // true

Object.prototype.toString.call(123) // "[object Number]"
Object.prototype.toString.call("abc") // "[object String]"
Object.prototype.toString.call(true) // "[object Boolean]"
Object.prototype.toString.call([]) // "[object Array]"
Object.prototype.toString.call({}) // "[object Object]"
Object.prototype.toString.call(null) // "[object Null]"
Object.prototype.toString.call(undefined) // "[object Undefined]"
Object.prototype.toString.call(new Date()) // "[object Date]"
Object.prototype.toString.call(/abc/) // "[object RegExp]"

map实现reduce

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
function myReduceMap(arr, callback, initialValue) {
let hasInitial = arguments.length > 2;
let accumulator = hasInitial ? initialValue : arr[0];

arr.forEach((item, index) => {
// 如果没有初始值,跳过第一个元素
if (!hasInitial && index === 0) return;
accumulator = callback(accumulator, item, index, arr);
});

return accumulator;
}

// 测试
const arr = [1, 2, 3, 4];

const sum = myReduceMap(arr, (acc, cur) => acc + cur, 0);
console.log(sum); // 10

const sumNoInit = myReduceMap(arr, (acc, cur) => acc + cur);
console.log(sumNoInit); // 10

const product = myReduceMap(arr, (acc, cur) => acc * cur, 1);
console.log(product); // 24
function myReduceMap(arr, callback, initialValue) {
let hasInitial = arguments.length > 2;
let accumulator = hasInitial ? initialValue : arr[0];

arr.forEach((item, index) => {
// 如果没有初始值,跳过第一个元素
if (!hasInitial && index === 0) return;
accumulator = callback(accumulator, item, index, arr);
});

return accumulator;
}

// 测试
const arr = [1, 2, 3, 4];

const sum = myReduceMap(arr, (acc, cur) => acc + cur, 0);
console.log(sum); // 10

const sumNoInit = myReduceMap(arr, (acc, cur) => acc + cur);
console.log(sumNoInit); // 10

const product = myReduceMap(arr, (acc, cur) => acc * cur, 1);
console.log(product); // 24

function myReduce(arr, callback, initialValue) {
let hasInitial = arguments.length > 2; // 判断是否传了初始值
let accumulator = hasInitial ? initialValue : arr[0];
let startIndex = hasInitial ? 0 : 1;

for (let i = startIndex; i < arr.length; i++) {
accumulator = callback(accumulator, arr[i], i, arr);
}

return accumulator;
}

// 测试
const arr = [1, 2, 3, 4];

const sum = myReduce(arr, (acc, cur) => acc + cur, 0);
console.log(sum); // 10

const sumNoInit = myReduce(arr, (acc, cur) => acc + cur);
console.log(sumNoInit); // 10

const product = myReduce(arr, (acc, cur) => acc * cur, 1);
console.log(product); // 24


function mapReduce(arr, callback, initialValue){
if(arr.length === 0) return initialValue;
const accArray = arr.map((item, index) => {
if(index === 0) {
return callback(initialValue, item, index, arr)
}

return callback(accArray[index-1], item, index, arr)
})

return accArray[accArray.length-1];
}

function functionalReduce(arr, callback, initialValue) {
if (arr.length === 0) return initialValue;

const accArray = arr.map((item, index) =>
index === 0
? callback(initialValue, item, index, arr) // 第一项累加初始值
: callback(accArray[index - 1], item, index, arr) // 后续项累加上前一步
);

return accArray[arr.length - 1];
}

// 测试
const arr = [1, 2, 3, 4];

const sum = functionalReduce(arr, (acc, cur) => acc + cur, 0);
console.log(sum); // 10

const product = functionalReduce(arr, (acc, cur) => acc * cur, 1);
console.log(product); // 24

Jsx和React.createElement()

1
2
3
4
5
6
7
8
9
10

const name = 'Alice';
const element = <h1>Hello, {name}</h1>;

const element = React.createElement(
'h1',
null,
`Hello, ${name}` // 注意这是 JS 表达式求值后的结果
)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
+------------------------------------------------------+
| JSON |
|------------------------------------------------------|
| { |
| "type": "BinaryExpression", |
| "operator": "+", |
| "left": { "type": "NumericLiteral", "value": 1 }, |
| "right": { "type": "NumericLiteral", "value": 2 } |
| } |
+------------------------------------------------------+
|
v
+------------------------------------------------------+
| AST |
|------------------------------------------------------|
| BinaryExpression |
| / \ |
| NumericLiteral(1) NumericLiteral(2) |
+------------------------------------------------------+
|
v
+------------------------------------------------------+
| Babel Parser |
+------------------------------------------------------+
|
v
+------------------------------------------------------+
| JavaScript |
|------------------------------------------------------|
| const a = 1 + 2; |
+------------------------------------------------------+

低代码平台的核心就是“配置转代码”和“代码回流配置”。
用户拖拽产生的是 JSON Schema,我会把这个 Schema 转成 AST,使用 Babel Types 生成 JSX,再用 Generator 输出 React/Vue 代码。
用户修改代码时,我反向用Babel Parser把代码解析成AST,再还原成JSON Schema,这样可视化和代码能双向同步。
事件表达式、安全校验、自动 import、属性补全、跨端转换(React → H5/Native)也都是基于AST的遍历和重写完成的。
所以AST是低代码的“编译器基础设施”.

AST转换

为什么前端AST转换用DFS
因为Babel、ESLint都是基于DFS preOrder + postOrder的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const ast = {
type: "BinaryExpression",
operator: "+",
left: {
type: "NumericLiteral",
value: 1
},
right: {
type: "BinaryExpression",
operator: "*",
left: { type: "NumericLiteral", value: 2 },
right: { type: "NumericLiteral", value: 3 }
}
}


function traverse(node, callback){
if(!node) return;
callback(node)
if(node.left) traverse(node.left, callback)
if(node.right) traverse(node.right, callback)
}

traverse(ast, (node) => {
console.log("visit:", node.type);
});

预加载

❓“preload 和 prefetch 区别是什么?”
完美回答:
preload 是为当前页面做的高优先级加载;
prefetch 是为未来页面做的低优先级加载。
preload 立即加载并参与渲染;
prefetch 在空闲时间加载,不阻塞当前渲染。
❓“preload 字体为什么要加 crossorigin?”
因为字体是跨域资源,浏览器需要使用 CORS 才能缓存。
否则会重复下载两次。
❓“什么时候用 preconnect?”
当你要请求第三方域名(CDN、API、字体)时,提前建好连接,可以减少 50~300ms 的延迟。

  1. 最佳实践(实际项目中这样写)
    🌟 推荐的 HTML 头部
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <!-- 提前解析 DNS -->
    <link rel="dns-prefetch" href="//cdn.example.com">

    <!-- 提前建立连接 -->
    <link rel="preconnect" href="https://cdn.example.com" crossorigin>

    <!-- 首屏关键 CSS / JS -->
    <link rel="preload" href="/static/main.css" as="style">
    <link rel="preload" href="/static/main.js" as="script">

    <!-- 字体 -->
    <link rel="preload" href="/fonts/myfont.woff2" as="font" crossorigin>

浅拷贝和深拷贝

1
2
3
4
5
6
7
8
//浅拷贝
const copy1 = {...obj};

const copy2 = Object.assign({}, obj)

const copy3 = arr.slice();

const copy4 = Array.from(arr)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//深拷贝
const deepCopy1 = JSON.parse(JSON.stringify(obj))

const deepClone(obj){
if(obj===null || typeof obj !== 'object') return obj;
if(obj instanceof Date) return new Date(obj);
if(obj instanceof Array) return obj.map(item => deepClone(item));

const cloned = {};
for(let key in obj){
if(obj.hasOwnProperty(key)){
cloned[key] = deepClone(obj(key))
}
}
return cloned
}

const deepCopy3 = _.cloneDeep(obj)

JSON方法深拷贝的局限性

1
2
3
4
5
6
7
8
9
10
11
12
13
const obj = {
date: new Date(), // 会被转为字符串
func: function() {}, // 会被丢失
undefined: undefined, // 会被丢失
infinity: Infinity, // 会被转为 null
regexp: /pattern/, // 会被转为空对象
symbol: Symbol('foo'), // 会被丢失
// 循环引用会导致错误
};

// 循环引用问题
obj.self = obj;
JSON.parse(JSON.stringify(obj)); // 报错:Converting circular structure to JSON

DFS,二叉树,前中后序,以及插入部分BFS的理论知识

DFS只有先序(pre-order)和后序(post-order),没有“中序”。
而“前序/中序/后序”是二叉树独有的概念。

👉 DFS 先序 / 后序

是泛树的概念,只有 “先访问自己 OR 后访问自己”。DFS = Depth First Search(深度优先搜索)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// 伪代码
// DFS先序遍历(pre-order)
// 当前节点-> 子节点们
// 特点:
// “一到节点就访问”。
// 用在:如构建路径、生成遍历顺序、表达式树生成前缀表达式等。
function dfs(node){
if(!node) return;
visit(node)
for(let child of node.children){
dfs(child)
}
}

// DFS后序遍历(post-order)
// 子节点们-> 当前节点
// “处理完所有孩子再处理自己”。
// 用在:如求文件夹大小(先算子文件夹大小,再汇总到父级)、表达式树计算、树DP。
function dfs(node){
if(!node) return;
for(let child of node.children){
dfs(child)
}
visit(node)
}

// 1. JSON / 配置树结构处理(超高频)
// 比如权限树、侧边栏菜单、路由表。
function dfsMenu(tree){
for(const node of tree){
console.log(node.title)
if(node.children){
dfsMenu(node.children)
}
}
}
// 2. 虚拟 DOM 遍历(React / Vue)
// Virtual DOM 的 diff 和 patch 都依赖 DFS。
// 对每个 vnode → 深度优先往下遍历子节点
// 一路走到叶子节点
// 再回溯更新
// 面试说法:
// Virtual DOM 的渲染和 diff 都是 DFS 先序遍历(pre-order),因为要先创建父节点再进入子节点。

// 3. AST(抽象语法树)遍历(编译、Babel、ESLint)
// 代码分析、source-map、webpack loader 都是 AST DFS 遍历。
// AST是树结构,编译器通过DFS后序遍历生成代码,ESLint 插件通过 DFS 访问和修改节点。
traverse(ast, {
Identifier(path){
// Dfs 访问节点
}
})

// 4. 寻找节点路径 / 查找特定节点(如查找面包屑导航)
function findPath(tree, target){
path.push(node);
if(node.id === target) return true;
for(child of node.children){
if(findPaht(child, target)) return true
}
path.pop();
}

// 5. 深克隆 JSON / 比较深层对象
// 例如 lodash 的 cloneDeep。

👉 二叉树前中后序

是 在 DFS 基础上 针对二叉树的 根、左、右 三者顺序组合出来的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 都是伪代码
// 前序:根排最前,根左右
// 拷贝树、序列化
function preorder(node){
if(!root) return;
console.log(root.val)
preorder(root.left)
preorder(root.right)
}
// 中序:根在中间,左根右
// 在BST(二叉搜索树)中,中序遍历能得到有序数组。(面试高频)
function inorder(node){
if(!root) return
preorder(root.left)
console.log(root.val)
preorder(root.right)
}
// 后序:根排最后,左右根
// 删除树、计算子节点值
function postorder(node){
if(!root) return;
preorder(root.left)
preorder(root.right)
console.log(root.valu)
}

BFS应用

1
2
3
4
5
6
7
8
9
10
11
12
// 伪代码
function bfs(start){
const queue = [start]
while(queue.length > 0){
const node = queue.shift();
visit(node)
for(const child of node.children){
queue.push(child)
}
}
}

  1. React Fiber 架构(核心点)

React Fiber 不再使用 DFS 递归,而是使用 可中断的 BFS 式遍历。
逐层遍历每个节点
并允许中断(时间切片)
做成可恢复的链表结构

面试说法:
Fiber 是一个基于链表的可中断的 BFS 遍历,解决了递归 DFS 不能中断导致的卡顿问题。

  1. 渲染树(DOM Tree)层级布局计算

浏览器 Layout(布局)阶段是 自上而下 BFS 派发。

  1. 聊天 / 评论 / 社交 App 的层级展开

例如查看评论时,要按“楼层”展示。

  1. 微前端路由调度、依赖图分析(拓扑排序)

BFS 用于:
构建依赖图
层序加载微应用
构建拓扑排序

面试官:DFS和BFS的区别?在前端有哪些场景应用?

你可以这么回答(非常强的那种👇):
DFS 是深度优先,一条路走到底;BFS 是广度优先,按层遍历。
两者的区别在于遍历策略、数据结构(DFS 用栈,BFS 用队列)、内存占用和是否适合求最短路径。
在前端工程中 DFS 常用于:
虚拟DOM深度遍历(Vdom Diff)
路由树、菜单树、权限树的处理
AST的遍历(Babel、ESLint、Webpack Loader)
查找节点路径、深克隆
BFS 常用于:
React Fiber的遍历机制
按层级渲染数据(评论、留言、树组件)
依赖图、拓扑排序
查找最近的节点(最近可见元素)

类数组转化为真实数组

1
2
3
4
5
6
7
8
// 1. Array.from
const nodeList = document.querySelectorAll('.elements')
const arrayFromNodeList = Arrary.from(nodeList)

// 2.
const arrayFromNodeList1 = Array.prototype.slice.call(nodeList)
// 3.
const arrayFromNodeList2 = [...nodeList]

原地打乱数组

1
2
3
4
5
6
7
function shuffleArray(arr) {
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1)); // ✅ 关键
[arr[i], arr[j]] = [arr[j], arr[i]];
}
return arr;
}

判断对象是否存在循环引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function hasCircularReference(obj, visited = new WeakSet()){
if(obj && typeof obj === 'object'){
if(visited.has(obj)){
return true;
}
visited.add(obj);
const keys = Reflect.ownKeys(obj);
for(let i=0; i<keys.length;i++){
if(hasCircularReference(obj[keys[i]], visited)){
return true
}
}
visited.delete(obj)
}
return false
}

反转字符串

1
2
3
4
const str = 'hello a';
const resultStr = Array.from(str).reduce((pre, cur) => {
return `${cur}${pre}`
}, '')

实现一个once函数,传入函数参数只执行一次

1
2
3
4
5
6
7
8
9
10
function once(fn){
let called = false;

return function (...args){
if(!called){
called = true;
return fn(...args)
}
}
}

实现sleep效果

1
2
3
4
5
6
7
async function sleep(time){
return new Promise(resolve => {
setTimeout(() => {
resolve()
}, time)
})
}

实现一个instanceof

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function myInstanceof(obj, constructor){
if(typeof constructor !== 'function'){
throw new TypeError('constructor of instanceof is not callable')
}
if(obj === null || (typeof obj !== 'object' && typeof obj !== 'function')){
return false
}

let proto = Object.getPrototypeOf(obj);

while(protp !== null){
console.log(proto, constructor.prototype)
if(proto == constructor.prototype){
return true
}
proto = Object.getPrototypeOf(proto)
}

return false
}

单例模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const Singleton = (function(){
let instance;

function createInstance(){
return new Object("singleton")
}

return {
getInstance: function(){
if(!instance){
instance = createInstance();
}
return instance;
}
}
})()

const instance1 = Singleton.getInstance();
const instance2 = Singleton.getInstance();

console.log(instance1 === instance2)