一些有意思的知识

458 阅读13分钟

说在最前。本文内容并非全部原创,只是个人在面试过程中在查阅了众多文章之后的总结,如有记得出处会标明。其一是提供给有需要的同学做复习,其二是自己保存一份以便查阅。希望各位文明观看。

js基础知识

判断数据类型的方式

1typeof
typeof [] // object, 数组也可单独使用Array.isArray()
typeof {}; // object
typeof new Function(); // function

2instanceof
[] instanceof Array; // true

3、constructor
[].__proto__.constructor === Array // true, 并不准确,如果继承时没有显示指定constructor会判断出错

4、toString
Object.prototype.toString.call('a') // "[object, string]"
Object.prototype.toString.call([5]) // "[object, Array]"

原型问题

请查阅 这篇文章,讲的很清晰了。原型问题是高频题,一定要去理解,而不是死记硬背

箭头函数和普通函数的区别

juejin.cn/post/684490…

async/await实现

juejin.cn/post/684490…

0.1 + 0.2 === 0.3 ?

github.com/mqyqingfeng…

宏任务和微任务

jakearchibald.com/2015/tasks-…

实现new操作符

function myNew (fn) {
    var obj = Object.create(fn.prototype)
    var args = Array.prototype.slice.call(arguments, 1)
    var res = fn.call(obj, ...args)
    if (res && typeof res === 'object' || typeof res === 'function') {
        return res
    }
    return obj
}

实现instanceof

function myInstanceOf (obj, fn) {
  if (typeof obj !== 'object' || obj === null) return false
  const proto = obj.__proto__
  if (proto === null) return false
  if (proto !== fn.prototype) {
    return myInstanceOf(proto, fn)
  } else {
    return true
  }
}

手写call

Function.prototype.myCall = function (target, ...args) {
    if (typeof this !== 'function') {
        throw new Error('not a function')
    }
    target = target || window
    var fn = Symbol()
    target[fn] = this

    var res = target[fn](...args)
    delete target[fn]
    return res
}

手写apply

Function.prototype.myApply = function (target, args) {
    if (typeof this !== 'function') {
        throw new Error('not a function')
    }
    target = target || window
    var fn = Symbol()
    target[fn] = this
    
    var res = target[fn](args)
    delete target[fn]
    return res
}

手写bind

Function.prototype.myBind = function (context, ...args) {
    if (typeof this !== 'function') {
        throw TypeError("Bind must be called on a function");
    }
    context = context || window
    var _this = this
    var fn = Symbol()
    context[fn] = this
    var result =  function () {
        var _args = Array.from(arguments)
        if (this instanceof _this) {
            this[fn] = _this
            this[fn](...[...args, ..._args])
            delete this[fn]
        } else {
            context[fn](...[...args, ..._args]);
            delete context[fn];
        }
    }
    // 实现继承,当result作为构造函数使用时, this.__proto__ === result.prototype
    //  this.__proto__.__proto__ === result.prototype.__proto__ === _this.prototype; //this instanceof _this =>true
    result.prototype = Object.create(this.prototype)
    return result
}

实现reduce

Array.prototype.myReduce = function(cb, initValue) {
    if (!Array.isArray(this)) {
      throw new TypeError("not a array")
    }
    // 数组为空,并且有初始值,报错
    if (this.length === 0 && arguments.length < 2) {
      throw new TypeError('Reduce of empty array with no initial value')
    }
    var res = null
    var arr = this
    if (initValue) {
        res = initValue
    } else {
        res = arr.splice(0, 1)[0]
    }
    arr.forEach((item, index) => {
        res = cb(res, item, index, arr)
    })
    return res
};

函数柯里化(参数长度固定)

function curry (fn) {
  const res = function (...args) {
    if (args.length === fn.length) {
      return fn(...args)
    } else {
      return function (...newArgs) {
        return res(...args, ...newArgs)
      }
    }
  }
}

函数柯里化(参数长度不固定)

function curry (fn) {
  let args = []
  return function temp (...newArgs) {    
    if (newArgs.length) {
      args = [...args, ...newArgs]
      return temp
    } else {
      let val = fn.apply(this, args)
      args = []
      return val
    }
  }
}

防抖

function debounce (fn ,delay) {
    let timer
    return function () {
      var args = Array.from(arguments)
      if (timer) {
        clearTimeout(timer)
      }
      timer = setTimeout(() => {
        fn.apply(this, args)
      }, delay)
    }
  }

防抖立即触发

function debounceImm (fn ,delay, immediate) {
    let timeout
    return function () {
      const context = this
      const args = Array.from(arguments)
      if (timeout) clearTimeout(timeout)
      if (immediate) {
        const callNow = !timeout
        setTimeout(() => {
          timeout = null
        }, delay)
        if (callNow) fn.apply(context, args)
      } else {
        timeout = setTimeout(() => {
          fn.apply(context, args)
        }, delay)
      }
    }
}

节流

function throttle (fn, delay) {
    let flag = true
    return function () {
      var args = Array.from(arguments)
      if (!flag) {
        return
      }
      flag = false
      setTimeout(() => {
        fn.apply(this, args)
        flag = true
      })
    }
}

防抖和节流有各自的应用场景,比如列表滑动使用节流,表单提交使用防抖

用proxy实现数组的负索引, 如arr[-1]就返回arr[arr.length - 1]

var negativeArray = (arr) => {
  return new Proxy(arr, {
    get: (target, prop, receiver) => {
      const key =  +prop > 0 ? prop : (target.length + (+prop))
      console.log('key', key)
      return Reflect.get(target, key, receiver)
    }
  })
}
var unicon = negativeArray([1,2,3,4])
console.log(unicon[-2])

模拟lodash的_.get()方法

function get (target, path, defaultValue = undefined) {
  const paths = path.replace(/\[(\d+)\]/g, '.$1').split('.')
  let result = target
  for (const p of paths) {
    result = Object(result)[p]
    if (result === undefined) {
      return defaultValue
    }
  }
  return result
}
console.log(get({ a: null }, "a.b.c", 3)); // output: 3
console.log(get({ a: [{ b: 1 }] }, "a[0].b", 3)); // output: 1

深拷贝

function isObject (val) {
  return typeof val === 'object' && val !== null
}

function deepClone (obj, map = new WeekMap()) {
  if (!isObject(obj)) return obj
  // 防止循环引用
  if (map.has(obj)) {
    return map.get(obj)
  }
  let target = Array.isArray(obj) ? [] : {}
  map.set(obj, target)
  Object.keys(obj).forEach(item => {
    if (isObject(item)) {
      target[item] = deepClone(obj[item], map)
    } else {
      target[item] = obj[item]
    }
  })
  return target
}

更细致的处理方式请查看github.com/jsmini/clon…

编程题

大数相加

function add(a ,b) {
  //取两个数字的最大长度
  let maxLength = Math.max(a.length, b.length)
  //用0去补齐长度
  a = a.padStart(maxLength, 0)//'0000007123434532341'
  b = b.padStart(maxLength, 0)//'1234567800000009999'
  //定义加法过程中需要用到的变量
  let everySum = 0
  let carry = 0   //进位
  let res = ''
  for(let i = maxLength - 1 ; i >= 0 ;i--){
     everySum = parseInt(a[i]) + parseInt(b[i]) + carry
     carry = Math.floor(everySum / 10)
     res = everySum % 10 + res
  }
  if(carry !== 0){
     res = '' + carry + res
  }
  return res
}

给定一个正整数,返回小于该数的最大2次幂。如输入9,返回8

function getMaxSecondPower (target) {
    // 观察一下2的幂数字的二进制规律,2->10, 4 -> 100, 8 -> 1000, 所以 2^n < target < 2^n-1, 那么target转二进制后必定与2^n位数相同
    const toBinary = Number(target).toString(2)
    return parseInt('1'.padEnd(toBinary.length, 0), 2)
  }

实现其他格式转驼峰

如: first_name, FIRST_NAME, FirstName, first_Name 转为 firstName

function smallCamel(value) {
    return value.replace(/(_?)([A-Z][a-z]+|[a-z]+|[A-Z]+)/g, function (value, _ ,word, i) {
      if (i === 0) {
        return word.toLowerCase()
      } else {
        return word.slice(0, 1).toUpperCase() + word.slice(1).toLowerCase()
      }
    })
}

虚拟dom转为真实dom

function _render (vnode) {
    if (typeof vnode === 'number') {
      vnode = String(vnode)
    }
    if (typeof vnode === 'string') {
      return document.createTextNode(vnode)
    }
    var dom = document.createElement(vnode.tag)
    if (vnode.attrs) {
      Object.keys(vnode.attrs).forEach(key => {
        dom.setAttribute(key, vnode.attrs[key])
      })
    }
    vnode.children.forEach(child => {
      return dom.appendChild(_render(child))
    })
    return dom
  }

  var vnode = {
    tag: 'DIV',
    attrs:{
      id:'app'
    },
    children: [
      {
        tag: 'SPAN',
        children: [
          { tag: 'A', children: [] }
        ]
      },
      {
        tag: 'SPAN',
        children: [
          { tag: 'A', children: [] },
          { tag: 'A', children: [] }
        ]
      }
    ]
}
console.log(_render(vnode))

将对象铺平

function flattenObj (obj) {
    if (!isObject(obj)) {
      return
    }
    let res = {}
    const dfs = (cur, prefix) => {
      if (isObject(cur)) {
        if (Array.isArray(cur)) {
          cur.forEach((item, index) => {
            dfs(item, `${prefix}[${index}]`)
          })
        } else {
          for (var k in cur) {
            dfs(cur[k], `${prefix}${prefix ? '.' : ''}${k}`)
          }
        }
      } else {
        res[prefix] = cur
      }
    }
    dfs(obj, '')
    console.log('flattenObj: ', res)
    return res
}

function isObject (val) {
    return typeof val === 'object' && val !== null
}

const fobj = {
    a: {
           b: 1,
           c: 2,
           d: {e: 5}
       },
    b: [1, 3, {a: 2, b: 3}],
    c: 3
}

flattenObj(fobj)

上两题主要考察递归问题

实现compose函数

function compose (...fn) {
  if (!fn.length) return (v) => v
  if (fn.length === 1) return fn[0]
  return fn.reduce((pre, cur) => {
    // 相当于一个个入栈,再出栈,从右往左执行
    return (...args) => {
      return pre(cur(...args))
    }
  })
}

function fn1(x) {
  return x + 1;
}
function fn2(x) {
  return x + 2;
}
function fn3(x) {
  return x + 3;
}
function fn4(x) {
  return x + 4;
}
const a_test = compose(fn1, fn2, fn3, fn4);
console.log(a_test(1)); // 1+4+3+2+1=11

版本号排序

function sortVersion () {
  var list = ['0.1.1', '2.3.3', '0.302.1', '4.2', '4.3.5', '4.3.4.5']
  list.sort((a, b) => {
    let i = 0
    let arr1 = a.split('.')
    let arr2 = b.split('.')
    while (true) {
      const s1 = arr1[i]
      const s2 = arr2[i]
      i++
      if (s1 === undefined || s2 === undefined) {
        return arr2.length - arr1.length
      }
      if (s1 === s2) continue

      return s2 - s1
    }
  })
  console.log(list)
}

订阅发布模式

class EventEmitter {
    constructor () {
        this.events = {}
    }

    on (type, callback) {
        if (!this.events[type]) {
            this.events[type] = [callback]
        } else {
            this.events[type].push(callback)
        }
    }

    off (type, callback) {
        if (!this.events[type]) {
            return
        }
        this.events[type] = this.events[type].filter(item => {
            return item !== callback
        })
        console.log('off', this.events)
    }

    once (type, callback) {
        // callback需要使用具名函数,绑定一次,off一次
        function fn () {
            callback()
            this.off(type, fn)
        }
        this.on(type, fn)
    }

    emit (type, ...rest) {
        console.log('emit', this.events)
        this.events[type] && this.events[type].forEach(fn => {
            fn.apply(this, rest)
        })
    }
}

const event = new EventEmitter()

更详细的设计模式请参考 设计模式

实现Promise及其方法

const PEDDING = 'pedding'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'

function MyPromise (exector) {
  this.status = PEDDING
  this.value = undefined
  this.reason = undefined
  this.onFulfilledCb = []
  this.onRejectedCb = []

  function resolve (value) {
    if (this.status === PEDDING) {
      this.status = FULFILLED
      this.value = value
      // 调用then函数的回调函数,因为有可能.then().then()所以回调用数组保存
      this.onFulfilledCb.forEach(fn => fn())
    }
  }
  function reject (reason) {
    if (this.status === PEDDING) {
      this.status = REJECTED
      this.reason = reason
      this.onRejectedCb.forEach(fn => fn())
    }
  }
  
  try {
    exector(resolve, reject)
  } catch (error) {
    reject(error)
  }
}

MyPromise.prototype.then = function (onFulfilled, onRejected) {
  onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value
  onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err } 
  const promise2 = new MyPromise((resolve, reject) => {

    if (this.status === FULFILLED) {
      // A+规范onFulfilled函数必须被异步调用
      setTimeout(() => {
        let x = onFulfilled(this.value)
        // 1、首先,要看x是不是promise。
        // 2、如果是promise,则取它的结果,作为新的promise2成功的结果
        // 3、如果是普通值,直接作为promise2成功的结果
        resolvePromise(promise2, x, resolve, reject)
      }, 0)
    }

    if (this.status === REJECTED) {
      setTimeout(() => {
        let x = onRejected(this.reason)
        resolvePromise(promise2, x, resolve, reject)
      }, 0)
    }

    if (this.status === PEDDING) {
      // 此时new Promise的异步回调还未结束,所以需要暂时保存then的回调
      this.onFulfilledCb.push(()=> {
        setTimeout(() => {
          try {
            let x = onFulfilled(this.value)
            resolvePromise(promise2, x, resolve, reject)
          } catch (error) {
            reject(error) 
          }
        }, 0)
      })

      this.onRejectedCb.push(() => {
        try {
          let x = onRejected(this.reason)
          resolvePromise(promise2, x, resolve, reject)
        } catch (error) {
          reject(error) 
        }
      })
    }
  })
  return promise2
}

MyPromise.prototype.catch = function (onRejected) {
  return this.then(null, onRejected)
}

MyPromise.prototype.resolve = function (value) {
  if (value instanceof MyPromise) {
    return value
  }
  if (typeof value === 'object' || typeof value === 'function') {
    try {
      const then = value.then
      if (typeof then === 'function') {
        // 如果参数是一个promise,那么应该做的是把参数promise的结果(通过promise.then(resolve,reject)获取结果)传递给返回的promise的then
        // return new Promise(then.bind(value)) // 跟下面的写法一个意思
        return new Promise((resolve, reject) => {
          // 这样,参数value在status发生变化的时候,返回的promise实例status也会同时发生变化,并且会将参数的this.value传递下去
          then.call(value, resolve, reject)
        })
      }
    } catch (error) {
      return new MyPromise((resolve, reject) => {
        reject(error)
      })
    }
  } else {
    return new MyPromise((resolve, reject) => {
      resolve(value)
    })
  }
}

MyPromise.prototype.reject = function (error) {
  return new MyPromise((resolve, reject) => {
    reject(error)
  })
}

MyPromise.prototype.all = function (promises) {
  return new Promise ((resolve, reject) => {
    let i = 0
    let res = []
    if (promises.length === 0) {
      return resolve(res)
    }
    promises.forEach((promise, index) => {
      promise.then(data => {
        res[index] = data
        i++
        if (i === promises.length) {
          resolve(res)
        }
      }, reject)
    })
  })
}

MyPromise.prototype.race = function (promises) {
  return new MyPromise((resolve, reject) => {
    if (promises.length === 0) {
      return resolve()
    }
    promises.forEach((promise) => promise.then(resolve, reject))
  })
}

MyPromise.prototype.finally = function (fn) {
  // 与catch类似,都是this.then的变式, finally要求返回一个promise,并且调用方的value传递下去
  // 即 A.finally().then(val => val) 需要将A.value传递下去
  return this.then( value => {
    // Promise.resolve(fn())可以等待fn执行完成之后再返回结果
    return MyPromise.resolve(fn()).then(() => {
      // value就是 new Promise(...).finally() 过程中,finally方法的调用方的值
      return value
    })
  }, error => {
    return MyPromise.resolve(fn()).then(() => {
      throw error
    })
  })
}

function resolvePromise (promise, x, resolve, reject) {
  if (promise === x) {
    reject(new TypeError('Chaining cycle detected for promise'))
  }
  let called
  if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
    try {  
      let then = x.then
      if (then && typeof then === 'function') {
        then.call(x, y => {
          if (called) return
          called = true
          resolvePromise(promise, y, resolve, reject)
        }, err => {
          if (called) return
          called = true
          reject(err)
        })
      } else {
        resolve(x)
      }
    } catch (error) {
      if (called) return
      called = true
      reject(error)
    }
  } else {
    resolve(x)
  }
}

并发问题1

最大同时发送n个,有一个返回才能继续发送下一个

const promises = Array(21).fill(0).map((v, i) => {
  return () => {
    return new Promise((resolve) => {
      setTimeout(() => {
        console.log(`完成第 ${i} 个请求`)
        resolve(i)
      }, 2000)
    })
  }
})

 function conurlRequest (urls, maxNum) {
  return new Promise((resolve, reject) => {
    if (urls.length === 0) {
      resolve([])
      return
    }
    const result = []
    const len = urls.length
    let index = 0
    let completedCount = 0 
    async function request () {
      // 存储这个请求在原数组中的下标
      const i = index
      const url = urls[index]
      index++
      try {
        // const resp = await fetch(url)
        console.log(`执行第 ${i} 个请求`)
        const resp = await url()
        result[i] = resp
      } catch (error) {
        result[i] = error
      } finally {
        completedCount++
        if (completedCount === len) {
          resolve(result)
        }
        if (index < len) {
          request()
        }
      }
    }
    // maxNum可能大于len,所以不能直接用len
    for (let i = 0; i < Math.min(len, maxNum); i++) {
      request()
    }
  })
}

conurlRequest(promises, 3)

并发问题2

let urls = ['http://dcdapp.com'];
/*
*实现一个方法,比如每次并发的执行三个请求,如果超时(timeout)就输入null,直到全部请求完
*batchGet(urls, batchnum=3, timeout=3000);
*urls是一个请求的数组,每一项是一个url
*最后按照输入的顺序返回结果数组[]
*/
function batchGet (urls, batchnum, timeout = 3000) {
    var ret = []
    while (urls.length > 0) {
        var pre = urls.splice(0, batchnum)
        var requestList = pre.map(url => request(url, timeout))
        var result = await Promise.allSettled(requestList)
        ret.concat(result.map(item => {
            if (item.status === 'rejected') {
                return null
            } else {
                return item.value
            }
        }))
    }
    return ret
}
function request (url ,timeout) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            reject()
        }, timeout)
        fetch(url).then(res => {
            resolve(res)
        })
    })
}

算法(常见问题)

排序篇

常见排序算法可以看 这篇文章

链表篇(链表问题最好还是去Leecode上刷)

反转单链表

/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} head
 * @return {ListNode}
 */
 // 递归法
var reverseList2 = function(head) {
    if (head === null) return null
    if (head.next === null) { // head.next === null时,说明是翻转前的最后一个节点,也就是反转后的头节点,所以直接return head
        return head
    }
    var last = reverseList(head.next)
    head.next.next = head
    head.next = null // 反转完之后,之前的头节点就是最后一个节点,所以指向null
    return last
};

// 迭代法
var reverseList = function  (head) {
    if (head === null) return null
    var pre = null, cur = head, nxt = head
    while (cur !== null) {
        nxt = cur.next
        cur.next = pre
        pre = cur
        cur = nxt
    }
    return pre
}

合并两个有序链表

/**
 * @param {ListNode} l1
 * @param {ListNode} l2
 * @return {ListNode}
 */
var mergeTwoLists = function(l1, l2) {
    // 这道题可以使用递归实现,新链表也不需要构造新节点,我们下面列举递归三个要素
    // 终止条件:两条链表分别名为 l1 和 l2,当 l1 为空或 l2 为空时结束
    // 返回值:每一层调用都返回排序好的链表头
    // 本级递归内容:如果 l1 的 val 值更小,则将 l1.next 与排序好的链表头相接,l2 同理
    // O(m+n)O(m+n),mm 为 l1的长度,nn 为 l2 的长度
    if (l1 === null)  return l2
    if (l2 === null) return l1
    if (l1.val < l2.val) {
        l1.next = mergeTwoLists(l1.next, l2)
        return l1
    } else {
        l2.next = mergeTwoLists(l2.next, l1)
        return l2
    }
};

var mergeTwoLists2 = function(l1, l2) {
    // 迭代方式
    const prehead = new ListNode(-1);

    let prev = prehead;
    while (l1 != null && l2 != null) {
        if (l1.val <= l2.val) {
            prev.next = l1;
            l1 = l1.next;
        } else {
            prev.next = l2;
            l2 = l2.next;
        }
        prev = prev.next;
    }

    // 合并后 l1 和 l2 最多只有一个还未被合并完,我们直接将链表末尾指向未合并完的链表即可
    prev.next = l1 === null ? l2 : l1;

    return prehead.next;
};

两个链表相加

/**
 * @param {ListNode} l1
 * @param {ListNode} l2
 * @return {ListNode}
 */
var addTwoNumbers = function(l1, l2) {
    var head = null, tail = null, carry = 0  // tail存放head的下一个节点
    while (l1 || l2) {
        var v1 = l1 ? l1.val : 0
        var v2 = l2 ? l2.val : 0
        var val = v1 + v2 + carry
        if (!head) { // 没有头节点时
            head = tail = new ListNode(val % 10)
        } else {
            tail.next = new ListNode(val % 10)
            tail = tail.next
        }
        carry = Math.floor(val / 10)
        if (l1) {
            l1 = l1.next
        }
        if (l2) {
            l2 = l2.next
        }
    }
    if (carry) {
        tail.next = new ListNode(carry)
    }
    return head
};

leecode地址 两数相加

相交链表

/**
 * @param {ListNode} headA
 * @param {ListNode} headB
 * @return {ListNode}
 */
var getIntersectionNode = function(headA, headB) {
    if (headA === null || headB === null) {
        return null
    }
    var p1 = headA, p2 = headB
    // 两个链表相交后,后续的长度是一样的。假如链表长度分别为m,n 并且 m > n, 那么在相交点之前,一定是有m - n个节点。
    // 当n走到尽头时,m还有 m - n个节点没走。此时n从headA开始走,当m走到终点是,n也恰好继续再走m - n步,即为相交点。
    while (p1 !== p2) {
        p1 = p1 === null ? headB : p1.next
        p2 = p2 === null ? headA : p2.next
    }
    return p1
};

判断链表中有环

/**
 * @param {ListNode} head
 * @return {boolean}
 */
var hasCycle = function(head) {
    if (!head || head.next === null) {
        return false
    }
    var slow = head, fast = head
    while (fast !== null && fast.next !== null) {
        slow = slow.next
        fast = fast.next.next
        if (slow === fast) {
            return true
        }
    }
    return false
};

删除链表的倒数第 N 个结点

/**
 * @param {ListNode} head
 * @param {number} n
 * @return {ListNode}
 */
var removeNthFromEnd = function(head, n) {
    if (head === null) { return null }
    var slow = head, fast = head
    while (n-- > 0) {
        fast = fast.next
    }
    if (fast === null) {
        // 走了n步走到头了,说明倒数第n个就是第一个
        return head.next
    }
    while (fast !== null && fast.next !== null) {
        slow = slow.next
        fast = fast.next
    }
    // fast始终快slow n步, n步即可以向前走n个节点,当fast.next === null时,说明fast就是最后一个,此时slow后面还有n个节点。那倒数第n个就是slow.next
    slow.next = slow.next.next
    return head
};

根据数组构建树,并输出广度遍历结果

var arr = [
    { node: 2, children: [3, 9, 4] },
    { node: 7, children: [2] },
    { node: 3, children: [6] },
    { node: 4, children: [5] },
    { node: 5, children: [8] },
    { node: 10, children: [11] },
  ];
//    7           10
//    |            |
//    2           11
//  / | \
// 3  9  4
// |     |
// 6     5
//       |
//       8 
//[7, 10, 2, 11, 3, 9,  4, 6,  5, 8]
  
class Node {
    constructor(key, children) {
        this.key = key;
        this.children = children || [];
    }
}

const nodeMap = {};
const childrenMap = {};

arr.forEach((item) => {
    let { node: key, children } = item;
    children = children.map((key) => {
        childrenMap[key] = 1;
        if (nodeMap[key]) {
            return nodeMap[key];
        }
        const child = new Node(key);
        nodeMap[key] = child;
        return child;
    });
    if (nodeMap[key]) {
        nodeMap[key].children = children;
    } else {
        const node = new Node(key, children);
        nodeMap[key] = node;
    }
});

console.log(nodeMap);
let roots = [];
for (const key in nodeMap) {
    if (!childrenMap[key]) {
        roots.push(nodeMap[key]);
    }
}
console.log(roots);
const res = []
while (roots.length) {
    var len = roots.length
    while (len--) {
        const node = roots.shift()
        res.push(node.key)
        if (node.children.length) {
            roots = roots.concat(node.children)
            console.log(roots)
        }
    }
}
console.log(res)

其余算法

出现次数最少的字符

const deleteMinNumStr = (str) => {
    const strNumHash = new Map()
    let minNum = str.length
    let minNumKey = []
    let newStr = ''
    for (let i = 0; i < str.length; i++) {
      strNumHash.set(str[i], strNumHash.get(str[i]) + 1 || 1)
    }
    for (let [key, value] of strNumHash) {
      if (value < minNum) {
        minNum = value
        minNumKey = [key]
      } else if (value === minNum) {
        minNumKey.push(key)
      }
    }
    let fast = 0
    // 这里可以用replace替换
    while (fast < str.length) {
      while (minNumKey.includes(str[fast])) {
        fast++
      }
      newStr += str[fast]
      fast++
    }
    return newStr
  }
  
deleteMinNumStr('aafbbbbbaffcccc')

两数之和

var twoSum = function(nums, target) {
    var map = {}
    for (var i = 0; i < nums.length; i++) {
        if (map[target - nums[i]] !== undefined) {
            return [map[target - nums[i]], i]
        }
        map[nums[i]] = i
    }
    return []
}

最大子序和

输入: nums = [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6 。
var maxSubArray = function(nums) {
    var max_end_here = nums[0] // 该字段表示,以 i 节点为结束节点的所有子序列的最大和
    var max_so_far = nums[0] // 该字段表示目前所有子序列的最大和
    for (var i = 1; i < nums.length; i++) {
        max_end_here = Math.max(max_end_here + nums[i], nums[i])
        max_so_far = Math.max(max_end_here, max_so_far)
    }
    return max_so_far
};

无重复字符的最长字串

/** 字串问题就使用滑动窗口
 * @param {string} s
 * @return {number}
 */
var lengthOfLongestSubstring = function(s) {
    var window = new Map()
    // 构建窗口和字典
    // needs = { a: 1, b: 2, c: 3}, 存储有哪些字符和他们出现的次数
    for (var i = 0; i < s.length; i++) {
        var item = s[i]
        if (!window.get(item)) {
            window.set(item, 0)
        }
    }
    var len = 0
    var left = 0, right = 0, valid = 0
    while (right < s.length) {
        var c = s[right]
        right++
        window.set(c, window.get(c) + 1)
        while (window.get(c) > 1) {
            var d = s[left]
            left++
            window.set(d, window.get(d) - 1)
        }
        // 每次都计算最大不重复的长度,当出现重复时,之前不重复的长度已经被记录下来了
        len = Math.max(len, right - left)
    }
    return len
};

最小覆盖字串(还是子串问题,解决方式还是滑动窗口)

/**
 * @param {string} s
 * @param {string} t
 * @return {string}
 */
var minWindow = function(s, t) {
    var window = new Map(), needs = new Map()
    // 构建窗口和字典
    // needs = { a: 1, b: 2, c: 3}, 存储有哪些字符和他们出现的次数
    for (var i = 0; i < t.length; i++) {
        var item = t[i]
        if (!needs.get(item)) {
            needs.set(item, 1)
        } else {
            needs.set(item, needs.get(item) + 1)
        }
        if (!window.get(item)) {
            window.set(item, 0)
        }
    }
    var valid = 0
    var len = Number.MAX_VALUE, start = 0 // start和len用于最后拿字串
    var left = right = 0
    while (right < s.length) {
        // c是移入窗口的字符
        var c = s[right]
        right++
        // 进行窗口内数据的更新操作
        if (needs.get(c)) {
            // 每出现一次t中包含的字符,就将窗口内该字符的出现次数+1
            window.set(c, window.get(c) + 1)
            if (window.get(c) === needs.get(c)) { // 如果窗口内该字符出现的次数和t中该字符的次数相同,则已经找到的字符数+1
                valid++
            }
        }
        while (valid === needs.size) { // 如果已经找到的字符和needs字典中的字符数相等(即needs中包含的字符种类),那么就算完成匹配
            if (right - left < len) {
                // 存储匹配成功的结果,然后与前一次对比,如果是更短的串则进行替换
                start = left
                len = right - left
            }
            var d = s[left]
            left++
            if (needs.get(d)) {
                if (window.get(d) === needs.get(d)) {
                    valid--
                }
                window.set(d, window.get(d) - 1)
            }
        }
    }
    return len === Number.MAX_VALUE ? '' : s.substr(start, len)
};

斐波那契数列

// 1、迭代方式
function fib (n) {
  if (n < 0) throw new Error('输入的数字不能小于0')
  if (n < 2) {
    return n
  }
  var list = []
  list[0] = 0
  list[1] = 1
  for (var i = 0; i < n; i++) {
    list[i + 1] = list[i] + list[i - 1]
  }
  return list[i]
}

// 2、记忆化递归
function fibMem (n, mem) {
  if (n < 2) {
    return n
  }
  if (mem[n]) return mem[n]
  mem[n] = fibMem(n - 1, mem) + fibMem(n - 2, mem)
  return mem[n] 
}

// 3、求斐波那契前n项和
function Fibonacci (n, acc1, acc2, sum) {
    if (n === 0) return sum
    return Fibonacci(n - 1, acc2, acc1 + acc2, acc2 + sum)
}

实现一个数组方法,获取数组的最大层级

Array.prototype.getLevel = function () {
    let level = 0
    let res = 1
    const dfs = (arr, level) => {
      if (Array.isArray(arr)) {
        level++
        arr.forEach(item => {
          dfs(item, level)
          res = Math.max(res, level)
        })
      } else {
        return
      }
    }
    dfs(this, level)
    return res
  }
var arr = [[1, [2]], [3], [[[5, [6]]]]]
arr.getLevel()

获取父节点id

// 给定一个节点列表,实现一个方法,输入一个childId, 返回它的所有父节点的id
  var arr = [
    {
      id: 1,
      child: [{
          id: 2,
          child: [{
              id: 3,
              child: null
            }]
        }]
    },
    {
      id: 5,
      child: [{
          id: 6,
          child: null
        }]
    }
  ]
  
  function getParent (arr, childId) {
    for (let i = 0; i < arr.length; i++) {
      var res = []
      const flag = dfs(arr[i], childId, res)
      if (flag) {
        return res
      }
    }
    return res
  }
  function dfs (node, childId, res) {
    if (node.id === childId) {
      return true
    }
    if (node.child) {
      res.push(node.id)
      for (let j = 0; j < node.child.length; j++) {
        const child = node.child[j]
        dfs(child, childId, res)
      }
    } else {
      res.pop()
      return false
    }
  }
  console.log('getParent', getParent(arr, 3))

浏览器

理解前端缓存

juejin.cn/post/694793…

前端安全问题(xss, csrf)

juejin.cn/post/689332…

输入url之后到看到页面发生的事情

juejin.cn/post/684490…

浏览器渲染机制

juejin.cn/post/684490…

js垃圾回收机制

文章很硬核,看完这个问题面试过程中就没困难了

http/https

https加密过程

juejin.cn/post/684490…

http2.0

blog.csdn.net/m0_60360320…

http的知识也属于常见问题,大厂很喜欢问

webpack

webpack做过什么优化

github.com/lgwebdream/…

webpack打包原理

juejin.cn/post/685457…

vue

vue项目优化

juejin.cn/post/684490…

虚拟dom

juejin.cn/post/684490…

$nextTick实现

juejin.cn/post/684490…

错误监控

性能监控

性能优化