# 分析解决问题
TIP
分析解决问题
# 数组转树
# 题目
定义一个 convert 函数,将以下数组转换为树结构。
const arr = [
{ id: 1, name: '部门A', parentId: 0 }, // 0 代表顶级节点,无父节点
{ id: 2, name: '部门B', parentId: 1 },
{ id: 3, name: '部门C', parentId: 1 },
{ id: 4, name: '部门D', parentId: 2 },
{ id: 5, name: '部门E', parentId: 2 },
{ id: 6, name: '部门F', parentId: 3 },
]
2
3
4
5
6
7
8

# 分析
定义树节点的数据结构
interface ITreeNode {
id: number
name: string
children?: ITreeNode[]
}
2
3
4
5
遍历数组,针对每个元素
- 生成 tree node
- 找到 parentNode 并加入到它的
children
找 parentNode 时,需要根据 id 能尽快找到 tree node
需要一个 map ,这样时间复杂度是 O(1) 。否则就需要遍历查找,时间复杂度高。
# 实现
# arr-to-tree.ts
/**
* @description array to tree
* @author 双越老师
*/
interface IArrayItem {
id: number
name: string
parentId: number
}
interface ITreeNode {
id: number
name: string
children?: ITreeNode[]
}
function convert(arr: IArrayItem[]): ITreeNode | null {
// 用于 id 和 treeNode 的映射
const idToTreeNode: Map<number, ITreeNode> = new Map()
let root = null
arr.forEach(item => {
const { id, name, parentId } = item
// 定义 tree node 并加入 map
const treeNode: ITreeNode = { id, name }
idToTreeNode.set(id, treeNode)
// 找到 parentNode 并加入到它的 children
const parentNode = idToTreeNode.get(parentId)
if (parentNode) {
if (parentNode.children == null) parentNode.children = []
parentNode.children.push(treeNode)
}
// 找到根节点
if (parentId === 0) root = treeNode
})
return root
}
const arr = [
{ id: 1, name: '部门A', parentId: 0 }, // 0 代表顶级节点,无父节点
{ id: 2, name: '部门B', parentId: 1 },
{ id: 3, name: '部门C', parentId: 1 },
{ id: 4, name: '部门D', parentId: 2 },
{ id: 5, name: '部门E', parentId: 2 },
{ id: 6, name: '部门F', parentId: 3 },
]
const tree = convert(arr)
console.info(tree)
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
# tree-to-arr.ts
# 扩展
/**
* @description tree to arr
* @author 双越老师
*/
interface IArrayItem {
id: number
name: string
parentId: number
}
interface ITreeNode {
id: number
name: string
children?: ITreeNode[]
}
function convert1(root: ITreeNode): IArrayItem[] {
// Map
const nodeToParent: Map<ITreeNode, ITreeNode> = new Map()
const arr: IArrayItem[] = []
// 广度优先遍历,queue
const queue: ITreeNode[] = []
queue.unshift(root) // 根节点 入队
while (queue.length > 0) {
const curNode = queue.pop() // 出队
if (curNode == null) break
const { id, name, children = [] } = curNode
// 创建数组 item 并 push
const parentNode = nodeToParent.get(curNode)
const parentId = parentNode?.id || 0
const item = { id, name, parentId }
arr.push(item)
// 子节点入队
children.forEach(child => {
// 映射 parent
nodeToParent.set(child, curNode)
// 入队
queue.unshift(child)
})
}
return arr
}
const obj = {
id: 1,
name: '部门A',
children: [
{
id: 2,
name: '部门B',
children: [
{ id: 4, name: '部门D' },
{ id: 5, name: '部门E' }
]
},
{
id: 3,
name: '部门C',
children: [
{ id: 6, name: '部门F' }
]
}
]
}
const arr1 = convert1(obj)
console.info(arr1)
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
这两种数据结构很像 MySQL vs Mongodb ,一个关系型,一个文档型
# map parseInt
# 题目
['1', '2', '3'].map(parseInt) 输出什么?
# parseInt
parseInt(string, radix) 解析一个字符串并返回指定基数的十进制整数
string要解析的字符串radix可选参数,数字基数(即进制),范围为 2-36
示例
parseInt('11', 1) // NaN ,1 非法,不在 2-36 范围之内
parseInt('11', 2) // 3 = 1*2 + 1
parseInt('3', 2) // NaN ,2 进制中不存在 3
parseInt('11', 3) // 4 = 1*3 + 1
parseInt('11', 8) // 9 = 1*8 + 1
parseInt('9', 8) // NaN ,8 进制中不存在 9
parseInt('11', 10) // 11
parseInt('A', 16) // 10 ,超过 10 进制,个位数就是 1 2 3 4 5 6 7 8 9 A B C D ...
parseInt('F', 16) // 15
parseInt('G', 16) // NaN ,16 进制个位数最多是 F ,不存在 G
parseInt('1F', 16) // 31 = 1*16 + F
2
3
4
5
6
7
8
9
10
11
# radix == null 或者 radix === 0
- 如果
string以0x开头,则按照 16 进制处理,例如parseInt('0x1F')等同于parseInt('1F', 16) - 如果
string以0开头,则按照 8 进制处理 —— ES5 之后就取消了,改为按 10 进制处理,但不是所有浏览器都这样,一定注意!!! - 其他情况,按 10 进制处理
# 分析代码
题目代码可以拆解为
const arr = ['1', '2', '3']
const res = arr.map((s, index) => {
console.log(`s is ${s}, index is ${index}`)
return parseInt(s, index)
})
console.log(res)
2
3
4
5
6
分析执行过程
parseInt('1', 0) // 1 ,radix === 0 按 10 进制处理
parseInt('2', 1) // NaN ,radix === 1 非法(不在 2-36 之内)
parseInt('3', 2) // NaN ,2 进制中没有 3
2
3
# 答案
['1', '2', '3'].map(parseInt) // [1, NaN, NaN]
# 划重点
- 要知道
parseInt参数的定义 - 把代码拆解到最细粒度,再逐步分析
# 扩展
为何 eslint 建议 partInt 要指定 radix(第二个参数)?
因为 parseInt('011') 无法确定是 8 进制还是 10 进制,因此必须给出明确指示。
# 原型
# 题目
以下代码,执行会输出什么?
function Foo() {
Foo.a = function() { console.log(1) }
this.a = function() { console.log(2) }
}
Foo.prototype.a = function() { console.log(3) }
Foo.a = function() { console.log(4) }
Foo.a()
let obj = new Foo()
obj.a()
Foo.a()
2
3
4
5
6
7
8
9
10
11
# 分析
把自己想象成 JS 引擎,你不是在读代码,而是在执行代码 —— 定义的函数如果不执行,就不要去看它的内容 —— 这很重要!!!
按这个思路来“执行”代码
# 原型

# 答案
执行输出 4 2 1
# 重点
- 原型基础知识
- 你不是在读代码,而是在模拟执行代码
# promise
# 题目
以下代码,执行会输出什么
Promise.resolve().then(() => {
console.log(0)
return Promise.resolve(4)
}).then((res) => {
console.log(res)
})
Promise.resolve().then(() => {
console.log(1)
}).then(() => {
console.log(2)
}).then(() => {
console.log(3)
}).then(() => {
console.log(5)
}).then(() =>{
console.log(6)
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 这道题很难
网上有很多文章介绍这道题,都没有给出清晰的答案。
被称为“令人失眠的”题目
# 回顾
- 单线程和异步
- 事件循环
- 宏任务 微任务
# then 交替执行
如果有多个 fulfilled 状态的 promise 实例,同时执行 then 链式调用,then 会交替调用
这是编译器的优化,防止一个 promise 持续占据事件
Promise.resolve().then(() => {
console.log(1)
}).then(() => {
console.log(2)
}).then(() => {
console.log(3)
}).then(() => {
console.log(4)
})
Promise.resolve().then(() => {
console.log(10)
}).then(() => {
console.log(20)
}).then(() => {
console.log(30)
}).then(() => {
console.log(40)
})
Promise.resolve().then(() => {
console.log(100)
}).then(() => {
console.log(200)
}).then(() => {
console.log(300)
}).then(() => {
console.log(400)
})
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
# then 返回 promise 对象
当 then 返回 promise 对象时,可以认为是多出一个 promise 实例。
Promise.resolve().then(() => {
console.log(1)
return Promise.resolve(100) // 相当于多处一个 promise 实例,如下注释的代码
}).then(res => {
console.log(res)
}).then(() => {
console.log(200)
}).then(() => {
console.log(300)
}).then(() => {
console.log(300)
})
Promise.resolve().then(() => {
console.log(10)
}).then(() => {
console.log(20)
}).then(() => {
console.log(30)
}).then(() => {
console.log(40)
})
// // 相当于新增一个 promise 实例 —— 但这个执行结果不一样,后面解释
// Promise.resolve(100).then(res => {
// console.log(res)
// }).then(() => {
// console.log(200)
// }).then(() => {
// console.log(300)
// }).then(() => {
// console.log(400)
// })
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
# “慢两拍”
then 返回 promise 实例和直接执行 Promise.resolve() 不一样,它需要等待两个过程
- promise 状态由 pending 变为 fulfilled
- then 函数挂载到 microTaskQueue
所以,它变现的会“慢两拍”。可以理解为
Promise.resolve().then(() => {
console.log(1)
})
Promise.resolve().then(() => {
console.log(10)
}).then(() => {
console.log(20)
}).then(() => {
console.log(30)
}).then(() => {
console.log(40)
})
Promise.resolve().then(() => {
// 第一拍
const p = Promise.resolve(100)
Promise.resolve().then(() => {
// 第二拍
p.then(res => {
console.log(res)
}).then(() => {
console.log(200)
}).then(() => {
console.log(300)
}).then(() => {
console.log(400)
})
})
})
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
# 答案
题目代码输出的结果是 1 2 3 4 5 6
# 重点
- 熟悉基础知识:事件循环 宏任务 微任务
- then 交替执行
- then 返回 promise 对象时“慢两拍”
PS:这里一直在微任务环境下,如果加入宏任务就不一样了
# 对象属性赋值
# 题目
执行以下代码,会输出什么
// example1
let a = {}, b = '123', c = 123
a[b] = 'b'
a[c] = 'c'
console.log(a[b])
// example 2
let a = {}, b = Symbol('123'), c = Symbol('123')
a[b] = 'b'
a[c] = 'c'
console.log(a[b])
// example 3
let a = {}, b = { key:'123' }, c = { key:'456' }
a[b] = 'b'
a[c] = 'c'
console.log(a[b])
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 对象的 key
- 对象的键名只能是字符串和 Symbol 类型
- 其他类型的键名会被转换成字符串类型
- 对象转字符串默认会调用
toString方法
const obj = {}
obj[0] = 100
const x = { s: 'abc' }
obj[x] = 200
const y = Symbol()
obj[y] = 300
const z = true
obj[z] = 400
Object.keys(obj) // ['0', '[object Object]', 'true']
2
3
4
5
6
7
8
9
10
有些类数组的结构是 { 0: x, 1: y, 2: z, length: 3 } ,如 document.getElementsByTagName('div')
实际上它的 key 是 ['0', '1', '2', 'length']
# 答案
题目代码执行分别打印 'c' 'b' 'c'
# 扩展:Map 和 WeakMap
- Map 可以用任何类型值作为
key - WeakMap 只能使用引用类型作为
key,不能是值类型
# 函数参数
# 题目
运行以下代码,会输出什么
function changeArg(x) { x = 200 }
let num = 100
changeArg(num)
console.log('changeArg num', num)
let obj = { name: '双越' }
changeArg(obj)
console.log('changeArg obj', obj)
function changeArgProp(x) {
x.name = '张三'
}
changeArgProp(obj)
console.log('changeArgProp obj', obj)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 分析
调用函数,传递参数 —— 赋值传递
function fn(x, y) {
// 继续操作 x y
}
const num = 100
const obj = { name: '双越' }
fn(num, obj)
2
3
4
5
6
以上代码相当于
const num = 100
const obj = { name: '双越' }
let x = num
let y = obj
// 继续操作 x y
2
3
4
5
6
# 解题
执行题目代码分别输出 100 {name: '双越'} {name: '张三'}
# 扩展:
eslint 规则建议:函数参数当作一个 const 常量,不要修改函数参数 —— 这样代码更易读
# setState
# 题目
React 中以下代码会输出什么
class Example extends React.Component {
constructor() {
super()
this.state = { val: 0 }
}
componentDidMount() {
// this.state.val 初始值是 0
this.setState({val: this.state.val + 1})
console.log(this.state.val)
this.setState({val: this.state.val + 1})
console.log(this.state.val)
setTimeout(() => {
this.setState({val: this.state.val + 1})
console.log(this.state.val)
this.setState({val: this.state.val + 1})
console.log(this.state.val)
}, 0)
}
render() {
return <p>{this.state.val}</p>
}
}
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
# setState 默认异步更新
componentDidMount() {
this.setState({val: this.state.val + 1}, () => {
// 回调函数可以拿到最新值
console.log('callback', this.state.val)
})
console.log(this.state.val) // 拿不到最新值
}
2
3
4
5
6
7
# setState 默认会合并
多次执行,最后 render 结果还是 1
componentDidMount() {
this.setState({val: this.state.val + 1})
this.setState({val: this.state.val + 1})
this.setState({val: this.state.val + 1})
}
2
3
4
5
# setState 有时同步更新
根据 setState 的触发时机是否受 React 控制
如果触发时机在 React 所控制的范围之内,则异步更新
- 生命周期内触发
- React JSX 事件内触发
如果触发时机不在 React 所控制的范围之内,则同步更新
- setTimeout setInterval
- 自定义的 DOM 事件
- Promise then
- ajax 网络请求回调
# setState 有时不会合并
第一,同步更新,不会合并
第二,传入函数,不会合并 (对象可以 Object.assign,函数无法合并)
this.setState((prevState, props) => {
return { val: prevState.val + 1 }
})
2
3
# 答案
题目代码执行打印 0 0 2 3
# 重点
setState 是 React 最重要的 API ,三点:
- 使用不可变数据
- 合并 vs 不合并
- 异步更新 vs 同步更新
← 软技能 知识广度-从前端到全栈 →
