前端面试总结

HTML/CSS

1. 盒模型

  1. 标准盒模型
    • width 和 height 是内容区域即 content 的 width 和 height。
    • 盒子总宽度= width + margin(左右) + padding(左右) + border(左右)
  2. IE 盒模型或怪异盒模型
    • width 和 height 除了 content 区域外,还包含 padding 和 border
    • 盒子宽度 = width + margin(左右)
  3. 通过 box-sizing切换 border-boxcontent-box

2. 隐藏一个元素的方式

  1. display: none
  2. visibility: hidden
  3. opacity: 0
  4. 高度为 0
  5. 定位 position: absolute; left: 100%;| top: -100%
  6. text-indent 设置一个足够大的负值

3. display: none 和 visibility: hidden 的区别

  1. display: none 不占位
  2. visibility: hidden 占位 (内部盒子也不会显示)

4. BFC 模式

5. flex 1 代表什么

6. align-self 属性

align-self 属性允许单个项目有与其他项目不一样的对齐方式,可覆盖 align-items 属性。默认值为 auto,表示继承父元素的 align-items 属性,如果没有父元素,则等同于 stretch。

JavaScript

1. 数据类型

Number String Boolean Undefined Null Symbol Object BigInt

2. var let const 区别

  1. var 变量声明提升
  2. let const 区别
    1. let 变量
    2. const 常量
    3. let 可以直接修改值(或者引用)
    4. const 只可以改变属性值
  3. let const 暂时性死区

3. 箭头函数

4. 原型、原型链

5. 作用域、作用域链

6. 为什么 0.1+0.2 不等于 0.3

01. + 0.2 = 0.3 // false

7. 小数相加、整数相加

8. 实现 new 操作符

function Person(name, age) {
  this.name = name
  this.age = age
}

方式一:

function _new(func, ...args) {
  if (typeof func !== 'function') throw new Error('func is not a function')

  const context = Object.create(func.prototype)
  const rest = func.apply(context, args)

  return typeof rest === 'object' && rest !== null ? rest : context
}

const p = _new(Person, 'mike', 12)

方式二:借助 Reflect.construct

Reflect.construct 相当于 new target()

const p = Reflect.construct(Person, ['jack', 11])

9. 实现 instanceof

function instanceofX(obj, constructor) {
  const proto = Object.getPrototypeOf(obj)

  if (proto === null) return false
  
  if (proto  === constructor.prototype) return true

  return instanceofX(proto , constructor)
}

10. splice 的使用及返回值

const arr = [1, 2, 3, 4, 5]
  1. 删除:返回所删除的元素组成的数组
arr.splice(1, 2) // 返回 [2, 3]
  1. 增加:
arr.splice(1, 0, 9, 9) // 返回空数组 []
  1. 替换:返回所有替换后的元素组成的数组
arr.splice(1, 2, 7, 8) // [2, 3]

11. 类型转换

123 instanceof Number // false
new Number(123) instanceof Number // true
Number(123) instanceof Number // false

Number === 123
// Number 就是强转换

12. arguments 参数

function sidEffecting(ary) {
  ary[0] = ary[2]
}

function bar(a, b, c) {
  c = 10
  sidEffecting(arguments)
  return a + b + c
}

bar(1, 1, 1) // 21

说明:考察 arguments 参数。arguments 内部属性及其函数形参创建 getter 和 setter 方法,因此改变形参的值会影响到函数 arguments 的值,反过来也一样。

13. 深拷贝(只考虑数组和对象)

  1. 方式一
function deepClone(value) {
  if (Array.isArray(value)) {
    return value.map(deepClone)
  } else if (value && typeof value === 'object' && value !== null) {
    const res = {}

    for (const key in value) {
      if (value.hasOwnProperty(key)) {
          res[key] = deepClone(value[key])
      }
    }

    return res
  }

  return value
}

方式二: 借助reduce

function deepClone(value) {
    if (Array.isArray(value)) {
      return value.map(deepClone)
    } else if (typeof value === 'object' && value !== null) {
      return Object.keys(value).reduce((pre, key) => {
        return {
          ...pre,
          [key]: deepClone(value[key]),
        }
      }, {})
    }

    return value
  }

以上两种方式的弊端是只能克隆属性为对象自身的、可枚举、且不为Symbol类型的属性。

方式三: JSON.stringify()

JSON.parse(JSON.stringify(value))

缺点:

  • 会忽略值为undefined、函数类型的属性
  • 忽略值为Symbol类型的属性
  • 日期对象的值会通过toJSON()转成字符串
  • 存在循环引用会报错

比如:

const obj = {
    a: [1, 2, 3],
    b: { c: 1 }
  }

  obj.a.push(obj.b)
  obj.b.j = obj.a
  // JSON.stringify(obj)
  1. 实现值为undefined,key为symbol类型的情况
function clone(obj) {
  if (typeof obj === 'object' && obj !== null) {
    let map = new WeakMap()

    function deep(data) {
      const result = {}
      // const keys = [...Object.getOwnPropertyNames(data), ...Object.getOwnPropertySymbols(data)]
      // Or
      const keys = Reflect.ownKeys(data)

      if (!keys.length) return result

      const exist = map.get(data)

      if (exist) return exist

      map.set(data, result)
      keys.forEach(key => {
        let item = data[key]
        if (typeof item === 'object' && item) {
          result[key] = deep(item)
        } else {
          result[key] = item
        }
      })
      return result
    }
    return deep(obj)
  }
  return obj
}

14. 克隆数组

1. 一维数组

  1. 扩展运算 (Shallow copy)
;[...arr]
  1. for 循环 (Shallow copy)
const newArr = []

for (let i = 0; i < arr.length; i++) {
  newArr[i] = arr[i]
}
  1. while 循环 (Shallow copy)
let i = -1
const newArr = []

while (++i < arr.length) {
  newArr[i] = arr[i]
}
  1. Array.map (Shallow copy)
const newArr = arr.map(item => item)
  1. Array.filter (Shallow copy)
const newArr = arr.filter(Boolean)

不足之处就是:数组中有选项是 0 ,就会丢失0,最终结果就是错的。因为Boolean(0) --> false

  1. Array.reduce (Shallow copy)
arr.reduce((newArr, item) => newArr.concat(item), [])
  1. Array.slice (Shallow copy)
const newArr = arr.slice()
  1. Array.concat (Shallow copy)
const newArr = arr.concat()
// Or
const newArr = arr.concat([])
  1. Array.from (Shallow copy)
const newArr = Array.from(arr)
  1. JSON.stringify & JSON.parse (Deep copy)
const newArr = JSON.parse(JSON.stringify(arr))

2. 多维数组

  1. 借助 Array.from()
const deepCloneArr = value =>
  Array.isArray(value) ? Array.from(value, deepCloneArr) : value
  1. 借助 Array.prototype.map()
const deepCloneArr = value =>
  Array.isArray(value) ? value.map(deepCloneArr) : value

15. 平铺数组

ES6 方法

参数默认是 1,多维用 Infinity 参数

Array.prototype.flat()

js 实现

  1. 二维数组
const arr = [1, 2, [3, 4], [5, 6]]
const result = [].concat(...arr)
  1. 多维数组
function spread(arr) {
  const result = [].concat(...arr)

  return result.some(item => Array.isArray(item)) ? spread(result) : result
}
  1. 可以定义平铺的层级数
function spread(arr, count = 1) {
    const result = [].concat(...arr)

    if (1 < count) {
      return result.some(item => Array.isArray(item))
        ? spread(result, --count)
        : result
    }
    return result
  }

16. 实现 Promise.all

Promise.allX = function (promises) {
  return new Promise((resolve, reject) => {
    promises = [...promises]
    const result = []
    const length = promises.length

    promises.forEach((promise, index) => {
      Promise.resolve(promise)
        .then(res => {
          result[index] = res

          if (--length === 0) resolve(result)
        })
        .catch(err => {
          reject(err)
        })
    })
  })
}

17. 实现 Promise.allSettled

Promise.allSettled 接收一组 Promise 实例作为参数,返回新的 Promise 实例,只有等到所有 Promise 实例的状态都改变之后才 resolve
所以 Promise.allSettled 的状态总是 fulfilled。其返回值为每个 Promise 实例的 statusvalue/reason

Promise.allSettled = function (promises) {
    return new Promise(resolve => {
      promises = [...promises]
      let time = 0
      const length = promises.length
      const result = []

      promises.forEach((promise, index) => {
        Promise.resolve(promise)
          .then(value => {
            result[index] = {
              status: 'fulfilled',
              value,
            }

            if (++time === length) resolve(result)
          })
          .catch(reason => {
            result[index] = {
              status: 'rejected',
              reason,
            }
            if (++time === length) resolve(result)
          })
      })
    })
  }

18. 实现 Promise.any

接收一组 Promise 实例作为参数,返回 Promise 实例,其中只要有一个 Promise 的状态变成 fulfilled,则返回的 Promise 的状态就为 fulfilled,只有所有的 Promise 都为 rejected状态则为 rejected

Promise.any = function (promises) {
    return new Promise((resolve, reject) => {
      promises = [...promises]
      let time = 0
      const length = promises.length
      const result = []

      promises.forEach((promise, index) => {
        Promise.resolve(promise)
        .then(res => {
          resolve(res)
        })
        .catch(err => {
          result[index] = err

          if(++time === length) reject(result)
        })
      })
    })
  }

实现Promise.race()

function race(promises) {
    return new Promise((resolve, reject) => {
      promises = [...promises]

      promises.forEach(p => {
        Promise.resolve(p).then(
          res => {
            resolve(res)
          },
          err => {
            reject(err)
          }
        )
      })
    })
  }

19. 防抖、节流实现方式

20. 多种方式实现 call/apply/bind

21. js 借助 async/await 实现休眠效果

function sleep(interval) {
  return new Promise(resolve => {
    setTimeout(resolve, interval)
  })
}

// 用法
async function one2FiveInAsync() {
  for (let i = 1; i <= 5; i++) {
    console.log(i)
    await sleep(1000)
  }
}
one2FiveInAsync()

22. 实现多次重复尝试,比如多次请求一个接口,最多请求 3 次

const COUNT = 3

async function request(url) {
  for (let i = 0; i < COUNT; ++i) {
    try {
      await fetch(url)
      break // 跳出循环
    } catch {}
  }
}

23. 实现 pipe

pipe 执行顺序从左向右

  1. 单个参数
const pipe = (...fns) => val => fns.reduce((pre, cur) => cur(pre), val)
  1. 多个参数
const pipes = (...fns) =>
  fns.reduce((pre, cur) => (...args) => cur(pre(...args)))

24. 实现 compose

执行顺序从右向左 第一个执行函数可以接收多个参数,后面的执行函数参数都是单个的。所有函数的执行都是同步的。

  1. 单个参数
const compose = (...fns) => val => fns.reduceRight((pre, cur) => cur(pre), val)
  1. 多个参数
const composes = (...fns) =>
  fns.reduceRight((pre, cur) => (...args) => cur(pre(...args)))

// 或
const composesX = (...fns) =>
  fns.reduce((pre, cur) => (...args) => pre(cur(...args)))

实现无限柯里化

// sum(1)(2)(3)(4)(5)(6)...()
function sum(a) {
    return function (b) {
      if (b != null) {
        return sum(a + b)
      }

      return a
    }
  }

25. 实现 trim()

function trim(str) {
  return str.replace(/^\s+|\s+$/g, '')
  // 或
  return str.replace(/^\s*/, '').replace(/\s*$/, '')
}

26. requestAnimationFrame

27. 本地存储

  1. localStorage
    1. 持久型
    2. 存储容量大 大约 5M
  2. sessionStorage 会话
    1. 会话型
    2. 存储容量大
  3. cookie
    1. 同域下往返于客户端和服务端
    2. 存储容量小 大概 4k

28. 交换两个值为 number 类型的变量

let a = 3
let b = 5
  1. 解构
;[a, b] = [b, a]
  1. 加减法
a = a + b
b = a - b
a = a - b
  1. 交换变量
let c = a
a = b
b = c
  1. 对象
a = { a, b }
b = a.a
a = a.b
  1. 托梦做出来的吧?
a = [b, (b = a)][0]
  1. 位运算
a = a ^ b
b = b ^ a
a = a ^ b

29. isNaN 和 Number.isNaN 的区别?

  1. isNaN 在调用时会将传入的参数转换为数字类型,所以非数字传入也有可能返回 true
  2. Number.isNaN 首先会判断传入的参数是否为数字,如果为非数字,直接返回 false

30. js 垃圾回收机制

  1. 极客时间-垃圾是如何自动回收的
  2. 垃圾回收

31. 实现图片懒加载

<div class="img-area">
  <img class="my-pic" alt="loading" data-src="./img/img1.png" />
</div>
//图片什么时候出现在可视区域内
function isInSight(el) {
  const rect = el.getBoundingClientRect()
  //这里我们只考虑向下滚动
  const clientHeight = window.innerHeight
  // 这里加50为了让其提前加载
  return rect.top <= clientHeight + 50
}
//data-src的属性值代替src属性值
function loadImg(el) {
  if (!el.src) {
    const source = el.dataset.src
    el.src = source
  }
}
let index = 0
function checkImgs() {
  const imgs = document.querySelectorAll('.my-pic')
  for (let i = index; i < imgs.length; i++) {
    if (isInSight(imgs[i])) {
      loadImg(imgs[i])
      index = i
    }
  }
}

Element.getBoundingClientRect

Tips:Element.getBoundingClientRect 方法返回一个 rect 对象,提供当前元素节点的大小、位置等信息,基本上就是 CSS 盒状模型的所有信息,如下:

  • x:元素左上角相对于视口的横坐标
  • y:元素左上角相对于视口的纵坐标
  • height:元素高度
  • width:元素宽度
  • left:元素左上角相对于视口的横坐标,与 x 属性相等
  • right:元素右边界相对于视口的横坐标(等于 x + width)
  • top:元素顶部相对于视口的纵坐标,与 y 属性相等
  • bottom:元素底部相对于视口的纵坐标(等于 y + height)

32. 实现 thunk

将数组(array)拆分成多个 size 长度的区块,并将这些区块组成一个新数组。 如果 array 无法被分割成全部等长的区块,那么最后剩余的元素将组成一个区块。

例如:

chunk(['a', 'b', 'c', 'd'], 2)
// => [['a', 'b'], ['c', 'd']]

chunk(['a', 'b', 'c', 'd'], 3)
// => [['a', 'b', 'c'], ['d']]

方法1

function chunk(array, size = 1) {
  const length = array == null ? 0 : array.length
  let start = 0

  if (!length || size < 1) return []

  const result = []

  while (start < length) {
    result.push(array.slice(start, (start += size)))
  }

  return result
}

方法2

function chunk(arr, limit) {
  if (!arr.length || limit < 1) return []

  return arr.reduce(
    (pre, cur) => {
      const temp = pre[pre.length - 1]
      if (temp.length < limit) {
        temp.push(cur)
      } else {
        pre.push([cur])
      }
      return pre
    },
    [[]]
  )
}

33. 实现render函数 将VNode转成真是VNode,并插入文档

VNode

const VNode = {
    tagName: 'ul',
    props: {
      class: 'list'
    },
    children: [
      {
        tagName: 'li',
        props: {
          class: 'item'
        },
        children: ['item1']
      },
      {
        tagName: 'li',
        props: {
          style: 'color: red'
        },
        children: ['item2']
      },
      {
        tagName: 'li',
        children: ['item3'],
        props: {
          style: 'cursor: pointer'
        },
        on: {
          click: () => {
            console.log('click item3')
          }
        }
      }
    ]
  }

实现如下:

 <div id="app"></div>

  // 元素节点
  function createEle(tag, props = {}, on = {}) {
    const ele = document.createElement(tag)
    const keys = Object.keys(props)
    const events = Object.keys(on)

    keys.length && keys.forEach(key => {
      ele.setAttribute(key, props[key])
    })

    events.length && events.forEach(event => {
      ele.addEventListener(event, on[event])
    })

    return ele
  }

  // 文本节点
  function createTextNode(text) {
    return document.createTextNode(text)
  }

  function render(VNode) {
    const { tagName, props, children, on } = VNode
    const ele = createEle(tagName, props, on)

    if (children.length) {
      const fragment = document.createDocumentFragment()

      children.forEach(child => {
        if (typeof child === 'object' && child !== null) {
          fragment.appendChild(render(child))
        } else {
          fragment.appendChild(createTextNode(child))
        }
      })
      ele.appendChild(fragment)
    }

    return ele
  }
  document.querySelector('#app').appendChild(render(VNode))

34. (链式调用)要求设计 LazyMan 类,实现以下功能

new LazyMan('Tony')
// Hi I am Tony

new LazyMan('Tony').sleep(10).eat('lunch')
// Hi I am Tony
// 等待了10秒...
// I am eating lunch

new LazyMan('Tony').eat('lunch').sleep(10).eat('dinner')
// Hi I am Tony
// I am eating lunch
// 等待了10秒...
// I am eating diner

new LazyMan('Tony').eat('lunch').eat('dinner').sleepFirst(5).sleep(10).eat('junk food')
// Hi I am Tony
// 等待了5秒...
// I am eating lunch
// I am eating dinner
// 等待了10秒...
// I am eating junk food

35. 实现 Object.is()

比较两个值是否相等。

在这里NaN等于NaN,+0不等于-0。

+0 === -0 //true
NaN === NaN // false

Object.is(+0, -0) // false
Object.is(NaN, NaN) // true

实现思路
如果两个值相等,就要进一步排除0的情况;如果不相等就需要排除NaN的情况。

Object.defineProperty(Object, 'is', {
  value(x, y) {
    if (x === y) {
      // 针对+0 不等于 -0的情况
      return x !== 0 || 1 / x === 1 / y
    }
    // 针对NaN的情况
    return x !== x && y !== y
  },
  configurable: true,
  enumerable: false,
  writable: true
})

36. a 在什么情况下会打印 1?

 const a = {
    i: 1,
    toString() {
      return a.i++
    }
  }
  
  // 进行隐式转换 Object == Primitive,需要Object转为Primitive(具体通过valueOf和toString方法)
  if (a == 1 && a == 2 && a == 3) {
    console.log(1)
  }

  const a = [1, 2, 3]
  // 移除第一个元素,返回值为移除后的元素
  a.toString = a.shift 
 // 对象和数字比较,会调用对象的toString方法
  if (a == 1 && a == 2 && a == 3) {
    console.log(1)
  }

Vue

1. vue

1. vue响应式和依赖收集

2. nextTick 原理


let timerFunc
const noop = () => {}

// 有、且是原生Promise
if (typeof Promise !== 'undefined' && isNative(Promise)) { // Promise
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterData: true
  })
  timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}


// 传入回调和返回Promise实例只能二选一
export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve

  callbacks.push(() => {
    // 如果传入了回调,则执行回调。反之执行Promise的resolve方法
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx) 
    }
  })
  if (!pending) {
    pending = true
    timerFunc() // 该函数内部的flushCallbacks是异步的,所以不会阻塞下面的流程
  }
  // $flow-disable-line
  // 如果没有传入回调,并且支持Promise的话,返回一个Promise的实例
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

3. keep-alive 原理

4. key 的作用

  1. key的作用主要是为了更高效的更新虚拟DOM。
  2. vue在patch过程中判断两个节点是否是相同节点的一个必要条件就是key。在渲染一组列表时,key往往是唯一标识,所以如果不定义key的话,vue只能认为比较的两个节点是同一个,哪怕它们实际上不是,这导致了频繁更新元素,使得整个patch过程比较低效,影响性能。
  3. 实际使用中在渲染一组列表时key必须设置,而且必须是唯一标识,应该避免使用数组索引作为key,这可能导致一些隐蔽的bug;
  4. vue中在使用相同标签元素过渡切换时,也会使用key属性,其目的也是为了让vue可以区分它们,否则vue只会替换其内部属性而不会触发过渡效果。

从源码中可以知道,vue判断两个节点是否相同时主要判断两者的key和元素类型等,因此如果不设置key,它的值就是undefined,则可能永远认为这是两个相同节点,只能去做更新操作,这造成了大量的dom更新操作,明显是不可取的。

5. VNode有哪些好处?

  1. 优化性能 。DOM 操作是比较耗时的,对于大量、频繁的 DOM 操作,如果先在 JavaScript 中模拟进行,然后再通过计算比对,找到真正需要更新的节点,这样就有可能减少不必要的 DOM 操作,从而提升渲染性能。
  2. 跨平台 。由于虚拟 DOM 以 JavaScript 对象为基础,所以可根据不同的运行环境进行代码转换(比如浏览器、服务端、原生应用等),这使得它具有了跨平台的能力。

一定比真实DOM的性能高吗?
并不是所有的 DOM 操作都能通过虚拟 DOM 提升性能,比如单次删除某个节点,直接操作 DOM 肯定比虚拟 DOM 计算比对之后再删除要快。总体而言, 虚拟 DOM 提升了 DOM 操作的性能下限,降低了 DOM 操作的性能上限。 所以会看到一些对渲染性能要求比较高的场景,比如在线文档、表格编辑,还是会使用原生 DOM 操作。

6. 组件通信

  1. 父传子 props
  2. 子传父 emit/on
  3. 事件总线 eventBus
  4. 跨组件之间 provide/inject 常见于组件库的封装,实际项目很少会用到
  5. parent获取父组件实例children/ref获取子组件实例
  6. attrs/listeners
  7. vuex

7. 生命周期

8. diff 算法

9. vue 中优化策略

  1. 路由懒加载
  2. keep-alive
  3. 频繁切换显隐式的使用v-show
  4. 静态列表通过Object.freeze()不开启响应式
  5. 无状态组件使用函数式组件
  6. 长列表使用虚拟列表
  7. 事件代理
  8. 图片懒加载
  9. 三方组件按需引入
  10. SSR

vue-interview

2. vuex

1. actions 和 mutations 区别

  1. 必须通过提交 mutation 的方式修改 state
  2. mutation 中只可以做同步操作
  3. action 中可同步可异步

2. vuex 原理

Vuex框架原理与源码分析

3. vuex 如何实现数据的响应式

new Vue({
  data: state
})

4. mapState 实现方式

3. vue-router

1. 导航守卫

  1. 全局守卫

    1. beforeEach 全局前置守卫
    2. beforeResolve 全局解析守卫
    3. afterEach 全局后置
  2. 路由独享守卫

    1. beforeEnter
  3. 组件守卫

    1. beforeRouteEnter
    2. beforeRouteUpdate
    3. beforeRouteLeave

2. 模式

  1. hash
  2. history

3. 原理

4. vue3

1、vue2 和 vue3 响应式原理对比,及具体实现思路

vue2响应式的缺陷

  1. 初始化时需要遍历对象所有key,如果对象层级较深,性能不好
  2. 不能监听数组的变化,需要重写数组的变异方法
  3. 通知更新过程需要维护大量dep实例和watcher实例,额外占用内存较多
  4. 动态新增、删除对象属性无法拦截,只能用特定set/delete api代替
  5. 不支持新的Map、Set等数据结构

vue3使用Proxy

  1. 可以同时支持object和array
  2. 动态属性增、删都可以拦截,新增数据结构均支持
  3. 对象嵌套属性运行时递归,也就是说只有用到时才代理,也不需要维护特别多的依赖关系,极大的降低性能
    缺点就是兼容性问题,因为proxy是js引擎提供的API,所以无法完全polyfill。

2、vue3 做了哪些优化

  1. proxy
  2. 静态节点标记
  3. VNode 重构

3、composition API 有哪些特性

setup()

React

1. React

  1. React 如何区分 class 和 functional?
  2. 为什么要写 super(props)?
  3. 为什么在编写组件时没有用到 React 还要引入 React?
  4. JSX 原理
  5. setState() 是同步还是异步的?
  6. 高阶组件 HOC
    1. 解决了什么问题
  7. render props
  8. HOOKS
    1. useEffect 如何实现 class 组件的 componentDidMounted 和 componentDidUnMounted
    2. useCallback() 和 useMemo() 的区别

2. Redux

  1. 实现简易版 redux
function createStore(reducer) {
  let state = reducer(undefined, {})
  let callback = []

  return {
    getState() {
      return state
    },
    dispatch(action) {
      state = reducer(state, action)
      callback.forEach(fn => fn())
    },
    subscribe(handler) {
      callback.push(handler)
    },
    unsubscribe(l) {
      callback = callback.filter(fn => fn !== l)
    }
  }
}
  1. 实现 bindActionCreators
/**
 * @param {Object | Function} actionCreators (Function: action creator,Object:value为action creator的对象)
 * @param {Function} dispatch 由store提供的dispatch函数
 * @returns {Object | Function}
 */

const bindActionCreators = (actionCreators, dispatch) => {
  if (typeof actionCreators === 'function') {
    return (...args) => {
      dispatch(actionHandle(...args))
    }
  }
  return Object.keys(actionCreators).reduce((key, creators) => {
    creators[key] = (...args) => dispatch(creators[key](...args))
    return handle
  }, {})
}
  1. 实现 combineReducers
const combineReducers = reducers => {
  return (state = {}, action) => {
    return Object.keys(reducers).reduce((nextState, key) => {
      nextState[key] = reducers[key](state[key], action)
      return nextState
    }, {})
  }
}
  1. redux 中间件实现原理

中间件本质上就是一个函数。 因为 reducer 为纯函数,且派发出 dispatch 之后,redux 内部会立即调用 reducer,所以只能在发出 action 和执行 reducer 之间做操作。

// 保存store原本的dispatch方法
const dispatch = store.dispatch

// 重写store.dispatch
store.dispatch = action => {
  setTimeout(() => {
    console.log('异步执行完成,派发action')
    dispatch(action)
  }, 2000)
}

store.dispatch({ type: 'ADD' })
  1. Redux从设计到源码

Node

1. Node

  1. http
  2. fs
  3. path.join() 和 path.resolve()区别?
  4. 启动一个 http 服务

2. Express

3. Koa2

Koa 总结

浏览器

1. 输入 URL 到页面展示做了什么

  1. 用户输入关键词,地址栏判断是搜索内容还是 url 地址。
    如果是搜索内容,会使用浏览器默认搜索引擎加上搜索内容合成 url;
    如果是域名会加上协议(如 https)合成完整的 url。

  2. 然后按下回车。浏览器进程通过 IPC(进程间通信)把 url 传给网络进程(网络进程接收到 url 才发起真正的网络请求)。

  3. 网络进程接收到 url 后,先查找有没有缓存。
    有缓存并且缓存还在生存期内,直接返回缓存的资源。
    缓存过期或没有缓存。(进入真正的网络请求)。如果缓存过期了,浏览器则会继续发起网络请求,并且在 HTTP 请求头中带上If-None-Match: "XXX"

首先获取域名的 IP,系统会首先自动从 hosts 文件(DNS 缓存)中寻找域名对应的 IP 地址,一旦找到,和服务器建立 TCP 连接;如果没有找到,则系统会将网址提交 DNS 域名解析服务器进行 IP 地址的解析。

  1. 利用 IP 地址和服务器建立 TCP 连接(3 次握手)。

  2. 建立连接后,浏览器网络进程构建数据包(包含请求行,请求头,请求正文,并把该域名相关 Cookie 等数据附加到请求头),然后向服务器发送请求消息。

  3. 服务器接收到消息后根据请求信息构建响应数据(包括响应行,响应头,响应正文),然后发送回网络进程。

  4. 网络进程接收到响应数据后进行解析。

如果发现响应行的返回的状态码为 301,302,说明服务器需要浏览器重定向到其他 URL,这时网络进程会找响应头中的 Location 字段的 URL,重新发起 HTTP 或者 HTTPS 请求。
如果返回的状态码为 200,说明服务器返回了数据。
如果状态码是 304,则说明缓存内容在服务端没有更新,服务器不会再返回内容,让浏览器从缓存中读取。

  1. 网络进程将响应头和响应行传给浏览器进程,浏览器进程根据响应头中的 Content-Type 告诉浏览器服务器返回的数据类型。如果返回的类型是 text/html,则告诉浏览器返回的是 HTML,如果是 application/octet-stream 则为下载类型,那么请求会交给浏览器的下载管理器,同时 URL 的导航到此结束,如果是 HTML,那么浏览器会继续进行导航流程。(导航:用户发出 URL 请求到页面开始解析的这个过程,就叫做导航)

  2. 浏览器进程接收到网络进程的响应头数据后,向渲染进程发出“提交导航”(CommitNavigation)的消息(带着响应头等消息)。

  3. 渲染进程接收到提交导航的消息后,准备渲染进程。创建新的渲染进程还是复用,规则如下:
    如果是新打开的页面,会单独使用一个渲染进程;
    如果是A页面打开“同一站点”的B页面,则会复用A页面的渲染进程;
    如果不是“同一站点”,则单独创建新的渲染进程;

  4. 渲染进程准备好后便和网络进程建立数据传输的“管道”。

  5. 等文档数据传输完成后,渲染进程会返回“确认提交”的消息给浏览器进程。意思是告诉浏览器进程我已经准备好接收网络进程的数据和解析页面数据了。

  6. 浏览器进程接收到“确认提交”消息后移除旧的文档,然后更新浏览器界面,包括 web 页面(空白页)、导航按钮、URL 地址栏、网络安全状态。浏览器的加载按钮还是加载中状态。至此,导航流程就完成了。接下来就是解析渲染阶段了。

    1. 构建 DOM
      1. 输入 html --> 解析器 ---> DOM Tree
    2. 样式计算
      1. css 来源:行内、style、外部(link 引入)
      2. css ---> styleSheets
      3. css 转成 styleSheets
      4. 属性标准化
      5. 计算 DOM 树中每个节点的具体样式 (继承、层叠)
    3. 布局 layout DOM 树&styleSheets ---> 布局树
      1. 创建布局树(只包含可见元素)
      2. 布局计算
    4. 分层 特定节点生成专用图层,并生成图层树

    图层生成的条件:

    1. 拥有层叠上下文属性的元素
    2. 需要剪裁的元素
    1. 在主线程上生成图层的绘制列表,将绘制列表提交给合成线程

    2. 栅格化

      1. 主线程将绘制列表提交给合成线程,合成线程将图层分块,生成图块(图块是栅格化的最小单位)
      2. 光栅化线程池和GPU生成位图
      3. 图块被光栅化完成后,合成线程就会生成一个绘制图块的命令——“DrawQuad”,然后将该命令提交给浏览器进程。
    3. 合成

      1. 合成线程提交命令 ---> 浏览器进程 ---> viz 组件接收命令 ---> 合成图片 ---> 显示
      2. 合成的图层会被提交给浏览器进程,浏览器进程里会执行显示合成(Display Compositor),也就是将所有的图层合成为可以显示的页面图片。 最终显示器显示的就是浏览器进程中合成的页面图片。
    4. 渲染进程开始页面解析和加载子资源(边下载边解析),一旦资源加载、渲染完毕,渲染进程会发送一个消息给浏览器进程,浏览器接收到这个消息后会停止标签图标的加载动画。

至此,一个完整的页面形成了。

2. 重绘和重排

为什么 DOM 操作耗费性能和时间?

  1. 渲染引擎和 js 引擎切换,(俗称上下文切换)耗费性能。
  2. 重绘/重排(元素及样式变化引起的再次渲染),而且重排耗时明显高于重绘
  3. 重排一定引起重绘,而重绘不一定引起重排

引起重绘的操作:

  1. 修改元素样式,比如颜色、bgc、border-color

引起重排的操作:

  1. 内容改变
  2. 增、删 DOM
  3. DOM 几何属性变化,如:宽高、位置、边框、padding、margin
  4. DOM 树结构发生改变
  5. 浏览器窗口尺寸改变
  6. 页面一开始渲染(不可避免的)
  7. 获取某些布局属性时。当获取一些属性值时,浏览器为了保证获取到正确的值也会引起重排。如:
    1. offsetTop,offsetLeft,offsetHeight,offsetWidth
    2. scrollTop,scrollWidth,scrollLeft,scrollHeight
    3. clientWidth,clientHeight,clientLeft,clientTop
    4. getComputedStyle()
    5. getBoundingClientRect()
    6. 更多参考这里

如何减少重排和重绘?

  1. 批量操作 DOM
    1. 在循环外操作元素
    2. 拼接字符串 --> innerHTML
    3. DocumentFragment 文档片段
    4. 缓存元素集合
  2. 复杂动画,使用绝对定位让其脱离文档流
  3. CSS3 硬件加速(GPU 加速)
    1. transform
    2. opacity
    3. filters
    4. will-change

硬件加速

  • 使用硬件加速可以让 transform、opacity、filters 不会引起重绘和回流,但是对于 bgc 这些属性还是会引起重绘
  • 硬件加速不可滥用,因为会占用内存较大,会影响性能

CSS 加载阻塞情况

  1. CSS 加载不会阻塞 DOM 树生成
  2. CSS 加载会阻塞 DOM 树的渲染。主要是浏览器出于性能考虑,避免渲染完成后又有样式变动,造成回流和重绘
  3. CSS 加载会阻塞后面 js 的执行

缩短白屏时间,尽可能加快 CSS 加载速度

  1. 使用 CDN
  2. 压缩 CSS
  3. 合理使用缓存
  4. 减少 HTTP 请求数(合并 CSS)

原理:

  1. HTML 解析和 CSS 解析是并行的过程,所以 CSS 加载不会阻塞 DOM 树生成
  2. render Tree 的形成依赖于 DOM Tree 和 CSSOM Tree,所以必须等到 CSS 加载完成才渲染
  3. 因为 js 可能会操作 DOM 节点和 CSS 样式,因此样式表会在加载完毕后执行后面的 js

跨域

原因:
浏览器同源策略的限制。

解决方案:

  1. jsonp
  2. cors
  3. Iframe
  4. postMessage
  5. node middleware
  6. nginx
  7. webSocket

网络

应用层

1. HTTP

1.1 状态码

  1. 1xx 收到请求,需要请求继续执行操作
  2. 2xx 成功
    1. 200 ok
    2. 204 没有资源可返回
    3. 206 返回部分资源,(请求范围资源)比如:音视频文件
  3. 3xx 重定向、浏览器需要执行某些特殊处理以完成正常请求
    1. 301 永久重定向 表示旧地址资源被永久地移除了(这个资源不可访问了),搜索引擎在抓取新内容的同时也将旧的网址交换为重定向之后的网址
    2. 302 临时重定向 表示旧地址 A 的资源还在(仍然可以访问),这个重定向只是临时地从旧地址 A 跳转到地址 B,搜索引擎会抓取新的内容而保存旧的网址
    3. 304 协商缓存
  4. 4xx 客户度错误
    1. 400 请求报文中存在语法错误
    2. 401 表示发送的请求需要有通过 HTTP 认证的认证信息
    3. 403 Forbidden(禁止) 请求被服务器拒绝了
    4. 404 Not Found
    5. 405 Method Not Allowed(不允许使用的方法)
    6. 406 请求的 content-Type 和相应的 content-type 不一致。说白了就是后台返回的资源前台无法解析
    7. 416 所请求的范围无法满足(读取文件时设置的 Range 有误造成的)
  5. 5xx 服务端错误
    1. 500 表示服务器在执行请求时发生了错误
    2. 503 表示服务器暂时处于超负载或正在进行停机维护状态,现在无法处理请求

1.2 request

  1. 请求行 method、URI、HTTP 版本
  2. 请求头
  3. 请求体

1.3 response

  1. 状态行 状态码、原因短语、服务器HTTP版本
  2. 响应头
  3. 响应体

1.4 http 头

connection: keep-alive 长连接
google chrome 默认同时最多可以建立6个TCP连接,TCP上http传输是串行的。(HTTP1.1)
HTTP2 只建立一个TCP,HTTP并行,理论上没有数量限制。

http 缓存

http缓存分为强缓存和协商缓存。详细内容查看 HTTP 缓存

2. FTP

文件传输协议

3. DNS

域名解析系统。域名和 IP 的映射,域名查 IP,IP 反查域名。

2. 传输层

1. TCP

面向连接的、可靠的、字节流服务。

  1. 面向连接 ---> TCP 连接
  2. 可靠的
    1. 具有重发机制,不会丢包
    2. TCP 三次握手
    3. 数据包上标记序号,到达接收方可以重组
  3. 字节流服务 --> 大数据包切割成报文段的小数据包

缺陷:
传输速度慢,不如 UDP

2. UDP

  1. 无连接的
  2. 易丢包
  3. 无序无法重组
  4. 传输速度快
  5. 场景:直播、视频会议等

3. 网络层

IP 协议 负责传输

4. 链路层

操作系统、显卡等物理器件

HTTPS

HTTP 缺点:

1. 明文传输,不加密,内容容易被窃听

解决:

  1. 内容(报文)加密,仍不可靠,容易被篡改
  2. SSL 通信加密 建立安全的通信线路,http 在该线路上传输

2. 无法验证身份,容易被伪装(通信双方无法确认对方身份)

解决:

SSL/TLS 证书认证

3. 无法保证内容的完整性,容易被篡改(明文传输,无法验证身份,导致内容容易被篡改)

解决:

SSL/TLS

HTTPS

HTTP + 通信加密 + 身份认证 + 内容完整性保护 = HTTPS

SSL/TLS 不仅提供了通信加密,还提供了证书认证,及内容完整性保护。

缺点:

使用 SSL/TLS 处理速度变慢

  1. SSL/TLS 建立连接耗费时间
  2. 通信慢: SSL/TLS 通信部分消耗网络资源
  3. 通信双方进行加解密处理,消耗大量的 CPU 和内存等资源

HTTP1.1

HTTP/1.1 为网络效率做了大量的优化,最核心的有如下三种方式:

  1. 增加了持久连接
  2. 浏览器为每个域名最多同时维护 6 个 TCP 持久连接
  3. 使用 CDN 的实现域名分片机制

HTTP/1.1 的主要问题

对带宽的利用率不理想

原因:

  1. TCP 的慢启动
  2. 同时开启了多条 TCP 连接,那么这些连接会竞争固定的带宽
  3. HTTP/1.1 队头阻塞的问题(一个TCP同时可以进行一个HTTP传输)

慢启动和 TCP 连接之间相互竞争带宽是由于 TCP 本身的机制导致的,而队头阻塞是由于 HTTP/1.1 的机制导致的。

HTTP2 特性

  1. 多路复用机制:
  • 一个域名建立一个 TCP 连接
  • 一个TCP可以同时进行多个HTTP的请求和响应
  1. 可以设置请求的优先级:在发送请求时,可以标上该请求的优先级
  2. 服务器推送:提前将静态资源推送到客户端
  3. 头部压缩

算法

1. 排序

1.1 冒泡排序

思路:

双层遍历,第二层遍历,当前值和下一个值比较,根据大小交换位置 修改的是原数组
时间复杂度 O(n^2) 空间复杂度 O(1)

function sort(array) {
  for (let i = 0; i < array.length; i++) {
    for (let j = 0; j < array.length - i; j++) {
      const temp = array[j + 1]

      if (array[j] > array[j + 1]) {
        array[j + 1] = array[j]
        array[j] = temp
      }
    }
  }
  return array
}

1.2 快速排序

思路:
选取数组第一项作为基准值,从数组第二项开始比较,小的放在左边数组,大的放在右边数组。
最终结束时机是当数组长度为 1 时。最终就是 arr.length 个长度为 1 的数组的合并。
返回的是新数组。
时间复杂度: O(nlogn)

function quickSort(arr) {
  if (arr.length <= 1) return arr

  const base = arr[0]
  const left = []
  const right = []

  for (let i = 1; i < arr.length; i++) {
    const temp = arr[i]

    if (base >= temp) {
      left.push(temp)
    } else {
      right.push(temp)
    }
  }

  return [...quickSort(left), base, ...quickSort(right)]
}

2. 反转数组

直接修改原数组

1. 遍历

function reverse(arr) {
   const mid = arr.length / 2

    for (let i = 0; i < mid; i++) {
      ;[arr[arr.length - 1 - i], arr[i]] = [arr[i], arr[arr.length - 1 - i]]
    }

    return arr
}

2. 借助 reduce

function reverseReduce(arr) {
  return arr.reduce((pre, cur) => [cur, ...pre], [])
}

3. 借助 reduceRight

function reverseReduceRight(arr) {
  return arr.reduceRight((pre, cur) => [...pre, cur], [])
}

3. 二分查找

前提条件:一定是有序数组。

查找有序数组中的某一项,以下实现方式假设数组是递增的。

  1. 非递归方式
function binarySearch(arr, target) {
  let start = 0
  let end = arr.length - 1
 
  while (start <= end) {
    let mid = Math.floor((start + end) / 2)
    const midVal = arr[mid]

    if (target === midVal) return midVal 
    if (target > midVal) {
      start = mid + 1
    } else {
      end = mid - 1
    }
  }
  return false
 }
}
  1. 递归方式
function binary(arr, target, start = 0, end = arr.length - 1) {
  if (start > end) return false
  const mid = Math.floor((start + end) / 2)
  const val = arr[mid]

  if (val === target) return mid
  if (val > target) return binary(arr, target, start, mid - 1)
  return binary(arr, target, mid + 1, end)
}

4. 斐波那契数列

写一个函数,输入 n ,求斐波那契(Fibonacci)数列的第 n 项。斐波那契数列的定义如下:

F(0) = 0, F(1) = 1
F(N) = F(N - 1) + F(N - 2), 其中 N > 1.
斐波那契数列由 0 和 1 开始,之后的斐波那契数就是由之前的两数相加而得出。

答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。

思路:

斐波那契数是由前两数相加而得,需对初始值 0 和 1 做单独判断。
之后只需做一次 for 循环遍历,需要注意得是,为避免时间复杂度的消耗,需要缓存前两数的结果,最后按题要求,所得的数需要经过取模运算。

var fib = function (n) {
  if (n <= 1) return n

  // n-1
  let prev1 = 1
  // n-2
  let prev2 = 0

  let sum = 0

  for (let i = 2; i <= n; i++) {
    sum = (prev1 + prev2) % 1000000007
    prev2 = prev1
    prev1 = sum
  }
  return sum
}

5. 青蛙跳台阶

一只青蛙一次可以跳上 1 级台阶,也可以跳上 2 级台阶。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。

答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。

示例 1:

输入:n = 2
输出:2
示例 2:

输入:n = 7
输出:21
示例 3:

输入:n = 0
输出:1

思路:

和斐波那契一样的思路,区别是斐波那契的 0 项为 0,而爬楼梯的 0 项是 1

空间复杂度: O(1) 时间复杂度: O(n)

function numWays(n) {
  if (n <= 1) return 1

  let a = 1
  let b = 2
  let sum

  for (let i = 3; i <= n; i++) {
    sum = (a + b) % 1000000007

    a = b
    b = sum
  }
  return b
}

递归实现方式

function bar(n) {
  if (n <= 1) return 1
  if (n < 3) return n
  return f(n - 1) + f(n - 2)
}

6. 有序数组合并 排序

假设是升序的

function concatSort(a, b) {
let i = 0
    let j = 0
    const arr = []

    while (i < a.length && j < b.length) {
      if (a[i] > b[j]) {
        arr.push(b[j++])
      } else {
        arr.push(a[i++])
      }
    }

    while (i < a.length) {
      arr.push(a[i++])
    }

    while (j < b.length) {
      arr.push(b[j++])
    }

    return arr
}

7. 删除数组中的重复项

给定一个排序数组,需要在 原地 删除重复出现的元素,使得每个元素只出现一次,返回移除后数组的新长度。
不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。

方法一:双指针法(慢快指针)

思路:
数组完成排序后,我们可以放置两个指针 i 和 j,其中 i 是慢指针,而 j 是快指针。慢指针初始值为 0,快指针初始值为 1.
如果 arr[i] === arr[j],跳过,快指针加 1。反之,慢指针加 1,快指针的值赋值给慢指针。

function removeDuplicates(arr) {
  let i = 0

  for (let j = 1; j < arr.length; j++) {
    if (arr[i] !== arr[j]) {
      arr[++i] = arr[j]
    }
  }

  return i + 1
}

方法二: 计数排序思想

思路:

因为题目是要求返回去重后数组的长度,并不关心去重后的数组,所以找出重复出现的个数即可。

由于数组是有序排列的,定义一个变量 count = 0; 从位置 1 开始遍历,判断当前元素是否和上一个元素相等,如果相等 count+1,反之跳过。

最后 count 就是所有重复元素出现的个数。那么不重复元素组成的数组就是 数组长度 - count

function removeItem(arr) {
  let count = 0

  for (let i = 1; i < arr.length; i++) {
    if (arr[i] === arr[i - 1]) ++count
  }

  return arr.length - count
}

8. 组中的第 K 个最大元素

function findKthLargest(nums, k) {
  // 快速排序
  function sort(nums) {
    if (nums.length <= 1) return nums

    let base = nums[0]
    let left = []
    let right = []

    for (let i = 1; i < nums.length; i++) {
      if (nums[i] > base) {
        right.push(nums[i])
      } else {
        left.push(nums[i])
      }
    }
    return [...sort(left), base, ...sort(right)]
  }
  nums = sort(nums)
  const len = nums.length

  return nums[len - k]
}

9. 两数之和

找出数组中 两个元素加起来等于 target 的元素的索引

1. 双层循环

适用情况:对数组的排序无要求,最后返回元素的索引

function twoSum(arr, target) {
   for (let i = 0; i < arr.length; i++) {
    for (let j = i + 1; j < arr.length; j++) {
      if (arr[i] + arr[j] === target) {
        return [i, j]
      }
    }
  }
  return []
}

2. 双指针对撞

适用情况: 数组必须是有序的。

时间复杂度 O(n)
空间复杂度 O(1)

假设有序递增数组

function search(arr, target) {
  let left = 0
  let right = arr.length - 1

  while (left < right) {
    const i = arr[left]
    const j = arr[right]
    if (i + j === target) return [i, j]
    if (i + j > target) {
      right--
    } else {
      left++
    }
  }

  return []
}

10. 二维数组的查找

在一个 n * m 的二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。

示例:

现有矩阵 matrix 如下:

;[
  [1, 4, 7, 11, 15],
  [2, 5, 8, 12, 19],
  [3, 6, 9, 16, 22],
  [10, 13, 14, 17, 24],
  [18, 21, 23, 26, 30]
]

给定 target = 5,返回 true。

给定 target = 20,返回 false。

时间复杂度 O(n + m)
空间复杂度 O(1)

function findNumberIn2DArray(matrix, target) {
  if (matrix.length < 1 || matrix[0].length < 1) return false

  let row = 0
  let col = matrix[0].length - 1

  while (row < matrix.length && col >= 0) {
    const current = matrix[row][col]

    if (current === target) return true
    if (current > target) {
      col--
    } else {
      row++
    }
  }

  return false
}

11. 第一个只出现一次的字符

在字符串 s 中找出第一个只出现一次的字符。如果没有,返回一个单空格。 s 只包含小写字母。

示例:

s = "abaccdeff"
返回 "b"

s = ""
返回 " "

1. Set + 正则

function firstUniqChar(s) {
  for (let char of new Set(s)) {
    // 正则匹配变量
    if (s.match(new RegExp(char, 'g')).length === 1) {
      return char
    }
  }
  return ' '
}

2. Map 的 keys 可以保证顺序

function search(s) {
  const map = s.split('').reduce((pre, cur) => {
    const temp = pre.get(cur)

    pre.set(cur, temp + 1 || 1)

    return pre
  }, new Map())

  for (let key of map.keys()) {
    if (map.get(key) === 1) return key
  }
  return ' '
}

12. 打印出从 1 到最大的 n 位数

输入数字 n,按顺序打印出从 1 到最大的 n 位十进制数。比如输入 3,则打印出 1、2、3 一直到最大的 3 位数 999。

示例 1:

输入: n = 1
输出: [1,2,3,4,5,6,7,8,9]

function printNumbers(n) {
  const length = Math.pow(10, n) - 1

  return Array.from({ length }, (_, index) => index + 1)
}

13. 数组中重复的数字

找出数组中重复的数字。

在一个长度为 n 的数组 nums 里的所有数字都在 0 ~ n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。

示例 1:

输入:
[2, 3, 1, 0, 2, 5, 3]
输出:2 或 3

function findRepeatNumber(arr) {
  const set = new Set()

  for (let item of arr) {
    if (set.has(item)) return item
    set.add(item)
  }
}

14. 查找出数组中出现次数最多的元素

const arr = [1, 2, 3, 2, 2, 1, 3, 3, 3, 3, 4, 4332, 21, 12, 2, 2, 2, 2, 2]

function maxTime(arr) {
 if (!arr.length) return
  if (arr.length === 1) return arr[0]

  let maxEle = arr[0]
  let maxCount = 0

  arr.reduce((pre, cur) => {
    pre = {
      ...pre,
      [cur]: pre[cur] + 1 || 1,
    }

    if (pre[cur] > maxCount) {
      maxCount = pre[cur]
      maxEle = cur
    }

    return pre
  }, {})

  return maxEle
}
maxTime(arr) // 2

15. 输出以奇数结尾的字符串拼接

题:[任意字符][一位数字]_,拼接的字符串,输出以奇数结尾的字符串拼接。
输入:str1_key2_val3_d4_e5
输出:str1val3e5

let str = 'str1_key2_val3_d4_e5'
  1. charAt()
let arr = str.split('_')
const result = arr.reduce((pre, cur) => {
  if (cur.charAt(cur.length - 1) % 2 !== 0) {
    return pre.concat(cur)
  } else {
    return pre
  }
}, '')
  1. slice()
const result = str.split('_').reduce((pre, cur) => {
  if (cur.slice(-1) % 2) {
    return (pre += cur)
  }
  return pre
}, '')

16. 二维数组的排列组合

设计模式

1. 单例模式

class Person {
  static getInstance() {
    if (!Person.instance) {
      console.log('只创建一次')
      Person.instance = new Person()
    }
    return Person.instance
  }
  constructor(name, age) {}

  // 原型方法
  getSkill() {
    console.log('吃饭')
  }

  // 静态方法
  static myWork() {
    console.log('睡觉')
  }
}

// 单例模式。实例只创建一次
const p1 = Person.getInstance()
const p2 = Person.getInstance()

2. 发布订阅模式

class EventEmitter {
    constructor() {
      this.events = {}
    }
    emit(eventName, ...args) {
      const events = this.events[eventName]

      if (events) {
        events.forEach(event => {
          event.apply(null, args)
        })
      }
    }
    on(eventName, fn) {
      if (this.events[eventName]) {
        this.events[eventName].push(fn)
      } else {
        this.events[eventName] = [fn]
      }
    }
  }

3. 观察者模式

性能优化

1. webpack 构建优化

首先造成 webpack 构建速度慢的因素就是重复的编译、代码压缩

方案

  1. 缓存
    1. 大部分 loader 提供了缓存选项
    2. 或者使用 cache-loader;需要定义在所有 loader 的最前面
  2. js 压缩
    1. 开启缓存
    2. 开启 parallel(并行编译)
  3. happypack 多核编译 (多线程)
    1. MiniCssExtractPlugin 无法与 happypack 共存
    2. MiniCssExtractPlugin 必须置于 cache-loader执行之后,否则无法生效
  4. 通过 DllPlugin 抽离静态依赖包,避免反复编译,比如 lodash 等,或者通过 externals CDN 引入。
  5. Tree shaking
  6. Scope hoisting 构建后的代码会存在大量闭包,造成体积增大,运行代码时创建的函数作用域变多,内存开销变大。Scope hoisting 将所有模块的代码按照引用顺序放在一个函数作用域里,然后适当的重命名一些变量以防止变量名冲突

提升构建体验

  1. progress-bar-webpack-plugin 构建进度提示
  2. webpack-build-notifier 构建完成后提示,还有提示音
  3. webpack-dashboard 构建界面

2. 资源优化

图片

  1. 减小 favicon.ico 的体积 设置强缓存,过期时间设置几个月
  2. 压缩图片
    1. webpack
    2. 在线压缩工具
  3. 雪碧图
    1. 横向排列会更小
  4. 禁止在 HTML 中缩放图片
  5. PNG logo
  6. WebP 格式 注意降级处理
    1. 多后缀方式兼容
    2. 指定 Accept 头支持 WebP 格式
  7. 小图标 base64 直接嵌入 HTML 文档
  8. 懒加载
  9. 图片渐进显示

CSS

  1. css 放在顶部
  2. 使用 link 而不是@import 加载样式
  3. css 通过文件的方式引入,不要写在 html 文档,以减小文档的大小,此外 css 文件可以被缓存
  4. 压缩 css
  5. 硬件加速
    1. transform: translateZ(0); // 仅开启硬件加速
    2. transform: translate3d(0, 0, 0); // 3d 变换开启硬件加速
    3. perspective: 1000

注意:使用 3D 硬件加速提升动画性能时,最好给元素增加一个 z-index 属性,人为干扰复合层的排序,可以有效减少 chrome 创建不必要的复合层,提升渲染性能,移动端优化效果尤为明显。
硬件加速最好只用在 animation 或者 transform 上。不要滥用硬件加速,因为这样会增加性能的消耗(内存的使用),如果滥用反而会使动画变得更加卡,就得不偿失了。

JavaScript

  1. 脚本放在底部
  2. js 和 css 通过文件的方式引入,不要写在 html 中,原因:
    1. 减小了 HTML 文档的大小
    2. js 和 css 会被缓存
    3. 压缩 js
  3. 删除重复的脚本
  4. 减少 DOM 的访问
    1. 缓存频繁访问的 DOM
    2. 在 DOM 树外更新节点,然后添加到 DOM 树,比如 DocumentFragment
    3. 动画能用 CSS 实现的不用 js 实现
  5. 防抖/节流
  6. 减少重绘重排

3. cookie 优化

  1. 消除不必要的 cookie
  2. 尽可能减小 cookie 的大小
  3. 注意设置 cookie 到合适的域名级别,则其它子域名不会被影响
  4. 正确设置 Expires 日期
  5. 静态资源请求没有 cookie,比如将静态资源放在全新的域下

4. HTTP 优化

  1. 最小化请求数
  2. 预加载资源
  3. 缓存策略

5. performance API 的使用

window.performance

const timingInfo = window.performance.timing

// TCP连接耗时
timingInfo.connectEnd - timingInfo.connectStart

// DNS查询耗时
timingInfo.domainLookupEnd - timingInfo.domainLookupStart

// 获得首字节耗费时间,也叫TTFB
timingInfo.responseStart - timingInfo.navigationStart

// domReady时间(与前面提到的DomContentLoad事件对应)
timingInfo.domContentLoadedEventStart - timingInfo.navigationStart

// DOM资源下载
timingInfo.responseEnd - timingInfo.responseStart

web 安全

XSS(跨站脚本攻击)

跨站脚本攻击(XSS):为什么 Cookie 中有 HttpOnly 属性?

CSRF(跨站伪造攻击)

CSRF 攻击:陌生链接不要随便点

错误监控及上报

一、类型及解决方式

1. 运行时错误

  1. try...catch
  2. window.onerror
  3. window.addEventListener('error')

2. 资源加载错误(图片)

  1. img.onerror

3. script Error(跨域代码)

原因:
跨域访问的 js 内部报错,浏览器处于安全考虑,不会报告具体的错误堆栈和行号,只抛出 script error 错误

解决:

  1. script 添加 crossorigin
  2. 服务端设置 Access-Control-allow-origin 为 * 或 访问域

二、错误上报

  1. ajax
  2. image 的 src((new Image()).src = '错误上报的请求地址') 使用图片发送 get 请求,上报信息,由于浏览器对图片有缓存,同样的请求,图片只会发送一次,避免重复上报

Webpack

loader 和 plugin 区别及原理

Git

git pull 和 git fetch 的区别?

  1. fetch:相当于是从远程获取最新版本到本地,不会自动 merge
  2. git pull:相当于是从远程获取最新版本并 merge 到本地

2. git rebase

你真的懂 git rebase 吗?

场景题

1. 音乐播放器

2. 虚拟列表

3. 数据预加载

4. 几十万条数据渲染

<ul id="list"></ul>
// 总数据
  const total = 100000

  // 每帧渲染的数量
  const once = 20

  // 总共渲染的次数
  const count = Math.ceil(total / once)

  // 父容器
  const ul = document.querySelector('#list')

  let loopTime = 1

  function render() {
    const fragment = document.createDocumentFragment()

    for (let i = 0; i < once; i++) {
      const li = document.createElement('li')

      li.innerText = Math.random() * total

      fragment.appendChild(li)
    }
    ul.appendChild(fragment)

    loop()
  }

  function loop() {
    if (loopTime++ < count) {
      window.requestAnimationFrame(render)
    }
  }

loop()

未完待续...

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269