前端面试系列二

127 阅读5分钟

1、【‘1’,‘2’,‘3’】.map(parseInt)

image.png

image.png

image.png

image.png

image.png

2、函数修改形参,能否影响实参 ?

image.png

image.png image.png

3、手写 convert 函数,将数组转为树

image.png

image.png

image.png

image.png

代码演示

/**
 * @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)

连环问:手写 convert 函数,将树转为tree

image.png

image.png

image.png

image.png

代码演示

/**
 * @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)

4、 代码输出

image.png

image.png

image.png

5、promise-then 的 执行顺序

image.png

image.png

image.png

image.png

Promise.resolve().then(() => {
    console.log(1)
}).then(() => {
    console.log(2)
}).then(() => {
    console.log(3)
}).then(() => {
    console.log(4)
}).then(() => {
    console.log(5)
})

Promise.resolve().then(() => {
    console.log(10)
}).then(() => {
    console.log(20)
}).then(() => {
    console.log(30)
}).then(() => {
    console.log(40)
}).then(() => {
    console.log(50)
})

Promise.resolve().then(() => {
    console.log(100)
}).then(() => {
    console.log(200)
}).then(() => {
    console.log(300)
}).then(() => {
    console.log(400)
}).then(() => {
    console.log(500)
})

image.png

image.png

image.png

image.png

image.png

6、React state 经典面试题

image.png

image.png

image.png

image.png

image.png

image.png

image.png

连环问: react setState 是微任务还是宏任务 ?

image.png

image.png

image.png

7、读代码- 对象和属性的连续赋值 ?

image.png

image.png

image.png

image.png

image.png

8、读代码 - 对象属性类型问题 ?

image.png

image.png

image.png

image.png

image.png

image.png

image.png

image.png

9、new一个对象的过程是什么,手写代码表示

image.png

export function customNew<T>(constructor: Function, ...args: any[]): T {
    // 1. 创建一个空对象,继承 constructor 的原型
    const obj = Object.create(constructor.prototype)
    // 2. 将 obj 作为 this ,执行 constructor ,传入参数
    constructor.apply(obj, args)
    // 3. 返回 obj
    return obj
}

class Foo {
    // 属性
    name: string
    city: string
    n: number

    constructor(name: string, n: number) {
        this.name = name
        this.city = '北京'
        this.n = n
    }

    getName() {
        return this.name
    }
}

// const f = new Foo('双越', 100)
const f = customNew<Foo>(Foo, '双越', 100)
console.info(f)
console.info(f.getName())

10、深度优先遍历一个DOM树

image.png

image.png

/**
 * 访问节点
 * @param n node
 */
function visitNode(n: Node) {
    if (n instanceof Comment) {
        // 注释
        console.info('Comment node ---', n.textContent)
    }
    if (n instanceof Text) {
        // 文本
        const t = n.textContent?.trim()
        if (t) {
            console.info('Text node ---', t)
        }
    }
    if (n instanceof HTMLElement) {
        // element
        console.info('Element node ---', `<${n.tagName.toLowerCase()}>`)
    }
}

/**
 * 深度优先遍历
 * @param root dom node
 */
function depthFirstTraverse1(root: Node) {
    visitNode(root)

    const childNodes = root.childNodes // .childNodes 和 .children 不一样
    if (childNodes.length) {
        childNodes.forEach(child => {
            depthFirstTraverse1(child) // 递归
        })
    }
}

/**
 * 深度优先遍历
 * @param root dom node
 */
 function depthFirstTraverse2(root: Node) {
     const stack: Node[] = []

     // 根节点压栈
     stack.push(root)

     while (stack.length > 0) {
         const curNode = stack.pop() // 出栈
         if (curNode == null) break

         visitNode(curNode)

         // 子节点压栈
         const childNodes = curNode.childNodes
         if (childNodes.length > 0) {
             // reverse 反顺序压栈
             Array.from(childNodes).reverse().forEach(child => stack.push(child))
         }
     }
 }

11、广度优先遍历一个DOM树

/**
 * 访问节点
 * @param n node
 */
function visitNode(n: Node) {
    if (n instanceof Comment) {
        // 注释
        console.info('Comment node ---', n.textContent)
    }
    if (n instanceof Text) {
        // 文本
        const t = n.textContent?.trim()
        if (t) {
            console.info('Text node ---', t)
        }
    }
    if (n instanceof HTMLElement) {
        // element
        console.info('Element node ---', `<${n.tagName.toLowerCase()}>`)
    }
}

/**
 * 广度优先遍历
 * @param root dom node
 */
function breadthFirstTraverse(root: Node) {
    const queue: Node[] = [] // 数组 vs 链表

    // 根节点入队列
    queue.unshift(root)

    while (queue.length > 0) {
        const curNode = queue.pop()
        if (curNode == null) break

        visitNode(curNode)

        // 子节点入队
        const childNodes = curNode.childNodes
        if (childNodes.length) {
            childNodes.forEach(child => queue.unshift(child))
        }
    }
}

【连环问】深度优先遍历可以不用递归吗

12、instanceof原理是什么,请写代码表示

image.png

image.png

/**
 * @description 手写 instanceof
 */

/**
 * 自定义 instanceof
 * @param instance instance
 * @param origin class or function
 */
export function myInstanceof(instance: any, origin: any): boolean {
    if (instance == null) return false // null undefined

    const type = typeof instance
    if (type !== 'object' && type !== 'function') {
        // 值类型
        return false
    }

    let tempInstance = instance // 为了防止修改 instance
    while (tempInstance) {
        if (tempInstance.__proto__ === origin.prototype) {
            return true // 配上了
        }
        // 未匹配
        tempInstance = tempInstance.__proto__ // 顺着原型链,往上找
    }

    return false
}

// // 功能测试
console.info(myInstanceof({}, Object)) // true
console.info(myInstanceof([], Object)) // true
console.info(myInstanceof([], Array))  // true
console.info(myInstanceof({}, Array))  // false
console.info(myInstanceof('abc', String)) // false

13、手写函数bind功能

image.png

image.png

image.png

/**
 * @description 手写 bind
 */

// @ts-ignore
Function.prototype.customBind = function (context: any, ...bindArgs: any[]) {
    // context 是 bind 传入的 this
    // bindArgs 是 bind 传入的各个参数

    const self = this // 当前的函数本身

    return function (...args: any[]) {
        // 拼接参数
        const newArgs = bindArgs.concat(args)
        return self.apply(context, newArgs)
    }
}

// // 功能测试
// function fn(this: any, a: any, b: any, c: any) {
//     console.info(this, a, b, c)
// }
// // @ts-ignore
// const fn1 = fn.customBind({x: 100}, 10)
// fn1(20, 30)

【连环问】手写函数call和apply功能

image.png

image.png

image.png

/**
 * @description 自定义 call apply
 */

// @ts-ignore
Function.prototype.customCall = function (context: any, ...args: any[]) {
    if (context == null) context = globalThis
    if (typeof context !== 'object') context = new Object(context) // 值类型,变为对象

    const fnKey = Symbol() // 不会出现属性名称的覆盖
    context[fnKey] = this // this 就是当前的函数

    const res = context[fnKey](...args) // 绑定了 this

    delete context[fnKey] // 清理掉 fn ,防止污染

    return res
}

// @ts-ignore
Function.prototype.customApply = function (context: any, args: any[] = []) {
    if (context == null) context = globalThis
    if (typeof context !== 'object') context = new Object(context) // 值类型,变为对象

    const fnKey = Symbol() // 不会出现属性名称的覆盖
    context[fnKey] = this // this 就是当前的函数

    const res = context[fnKey](...args) // 绑定了 this

    delete context[fnKey] // 清理掉 fn ,防止污染

    return res
}

function fn(this: any, a: any, b: any, c: any) {
    console.info(this, a, b, c)
}
// // @ts-ignore
// fn.customCall({x: 100}, 10, 20, 30)
// @ts-ignore
// fn.customApply({x: 200}, [100, 200, 300])

14、手写一个JS函数,实现数组扁平化Array Flatten

/**
 * @description 数组扁平化
 */

/**
 * 数组扁平化,使用 push
 * @param arr arr
 */
export function flatten1(arr: any[]): any[] {
    const res: any[] = []

    arr.forEach(item => {
        if (Array.isArray(item)) {
            item.forEach(n => res.push(n))
        } else {
            res.push(item)
        }
    })

    return res
}

const arr = [1, 2, 3, 4, 5, [6, 7, [8, 9]]]
console.log(flatten1(arr)) //[1,2,3,4,5,6,7,[8,9]]


/**
 * 数组扁平化,使用 concat
 * @param arr arr
 */
export function flatten2(arr: any[]): any[] {
    let res: any[] = []

    arr.forEach(item => {
        res = res.concat(item)
    })

    return res
}

// // 功能测试
const arr2 = [1, 2, 3, 4, 5, [6, 7, [8, 9]]]
console.log(flatten1(arr2)) //[1,2,3,4,5,6,7,[8,9]]

【连环问】手写一个JS函数,实现数组深度扁平化

/**
 * @description 数组深度扁平化
 */

/**
 * 数组深度扁平化,使用 push
 * @param arr arr
 */
export function flattenDeep1(arr: any[]): any[] {
    const res: any[] = []

    arr.forEach(item => {
        if (Array.isArray(item)) {
            const flatItem = flattenDeep1(item) // 递归
            flatItem.forEach(n => res.push(n))
        } else {
            res.push(item)
        }
    })

    return res
}

const arrs = [1, 2, 3, 4, [5, 6, [7, 8]]]
console.log(flatternDeep1(arrs)) // [1,2,3,4,5,6,7,8]


/**
 * 数组深度扁平化,使用 concat
 * @param arr arr
 */
export function flattenDeep2(arr: any[]): any[] {
    let res: any[] = []

    arr.forEach(item => {
        if (Array.isArray(item)) {
            const flatItem = flattenDeep2(item) // 递归
            res = res.concat(flatItem)
        } else {
            res = res.concat(item)
        }
    })

    return res
}


// // 功能测试
// const arr = [1, [2, [3, ['a', [true], 'b'], 4], 5], 6]
// console.info( flattenDeep2(arr) ) // [1, 2, 3, 'a', true, 'b', 4, 5, 6]

15、 手写curry函数,实现函数柯里化(常考)

image.png

/**
 * @description curry add
 */

export function curry(fn: Function) {
    const fnArgsLength = fn.length // 传入函数的参数长度
    let args: any[] = []

    // ts 中,独立的函数,this 需要声明类型
    function calc(this: any, ...newArgs: any[]) {
        // 积累参数
        args = [
            ...args,
            ...newArgs
        ]
        if (args.length < fnArgsLength) {
            // 参数不够,返回函数
            return calc
        } else {
            // 参数够了,返回执行结果
            return fn.apply(this, args.slice(0, fnArgsLength))
        }
    }

    return calc
}

// function add(a: number, b: number, c: number): number {
//     return a + b + c
// }
// // add(10, 20, 30) // 60

// const curryAdd = curry(add)
// const res = curryAdd(10)(20)(30) // 60
// console.info(res)

16、手写JS深拷贝-考虑各种数据类型和循环引用