# 分析解决问题

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 },
]
1
2
3
4
5
6
7
8

# 分析

定义树节点的数据结构

interface ITreeNode {
    id: number
    name: string
    children?: ITreeNode[]
}
1
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)
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

# 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)
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

这两种数据结构很像 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
1
2
3
4
5
6
7
8
9
10
11

# radix == null 或者 radix === 0

  • 如果 string0x 开头,则按照 16 进制处理,例如 parseInt('0x1F') 等同于 parseInt('1F', 16)
  • 如果 string0 开头,则按照 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)
1
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
1
2
3

# 答案

['1', '2', '3'].map(parseInt) // [1, NaN, NaN]
1

# 划重点

  • 要知道 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()
1
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)
})
1
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)
})
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

# 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)
// })
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

# “慢两拍”

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)
        })
    })
})
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

# 答案

题目代码输出的结果是 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])
1
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']
1
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)
1
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)
1
2
3
4
5
6

以上代码相当于

const num = 100
const obj = { name: '双越' }

let x = num
let y = obj
// 继续操作 x y
1
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>
    }
}
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

# setState 默认异步更新

componentDidMount() {
  this.setState({val: this.state.val + 1}, () => {
    // 回调函数可以拿到最新值
    console.log('callback', this.state.val)
  })
  console.log(this.state.val) // 拿不到最新值
}
1
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})
}
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 }
})
1
2
3

# 答案

题目代码执行打印 0 0 2 3

# 重点

setState 是 React 最重要的 API ,三点:

  • 使用不可变数据
  • 合并 vs 不合并
  • 异步更新 vs 同步更新
Last Updated: 8/5/2022, 9:18:58 AM
강남역 4번 출구
Plastic / Fallin` Dild