Giter Club home page Giter Club logo

keep-learning's Introduction

介绍

本仓库主要是对本人在日常开发过程中遇到的问题所做的小记录,以及文章的分享等等。当然,文章或记录当中难免会出现某些小问题,可以第一时间发邮件跟我说说的 😄。

文章的分享旨在跟大家一起好好学习、天天向上,也只有通过分享我才能更好滴知道自己在哪方面的不足,以此进行相应的改进。💪

仓库计划

目前,所有的博客我都已经迁移到了 vuepress 当中,有兴趣的童鞋可以直接访问这个地址的。http://www.ermei.top/

每一周我都会尽量分享至少一篇文章,对于文章的分类呢,我会按周对其进行分类整理下面,如果你只是想知道都哪些文章,可以直接观看下面的分类的~当然,如果你想看全部的内容以及更好的观看体验,那就直接访问网页地址即可啦。

JavaScript

HTML

CSS

Vue

React

Webpack

Typescript

Nodejs

Weex

Network

数据结构

日常杂记

疑问或建议

如对文章中有疑惑的地方或者建议,都可以发我邮件哈,又或者提个 issue,我有时间都会尽量回复。正所谓一起学习一起进步啦 💪。

keep-learning's People

Contributors

haoquan-yougola avatar leo-lin214 avatar znlinhq avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

keep-learning's Issues

手写实现那回事

好的文章可参考如下。
各种源码实现,你想要的这里都有

当下不管是在面试过程中还是在日常开发过程中,一些底层的实现变得越来越有效率性,那么当理解如何实现时,能否直接写出一份实现呢?

为此,我特意写了一些自己的理解总结,便于后期的回顾或分享。😄

Bind 函数实现

实现 Bind 函数的关键点在于两点。

  1. 箭头函数中的 this 指向函数定义时所在的作用域
  2. 使用 Bind 函数绑定返回的函数,在使用 new 时,this 的指向会指向新创建的对象,而不是原来已绑定的对象。
答案

  Function.prototype.myBind = function(obj) {
    // 先判断是否为函数
    if(typeof this !== 'function') {
      throw new Error('This is not function...')
    }
    const remainArgs = Array.prototype.slice.call(arguments, 1) // 获取剩余参数
    const TempFun = function() {} // 构造一个空函数
    const self = this // 缓存当前函数
    if(self.prototype) { // 保存当前函数的原型
      TempFun.prototype = self.prototype
    }
    const returnFun = function() {
      return self.apply(this instanceof TempFun ? this : obj, remainArgs)
    }
    returnFun.prototype = new TempFun()
    return returnFun
  }
  

Call 和 Apply 函数实现

实现 Call 函数和 Apply 函数,主要体现在两点。

  1. 将当前函数保存下来,并使用 Symbol 作为属性名保存在相应对象下面。
  2. 执行刚刚在相应对象下使用 Symbol 保存的函数,并把该结果进行返回。
答案

  Function.prototype.myCall = function(obj) {
    if(typeof this !== 'function') {
      throw new Error('This is not a function...')
    }
    const selfFun = this // 保存当前函数
    const remainArgs = [...arguments].slice(1) // 获取剩余参数
    const fn = Symbol('fn') // 避免对象函数名冲突
    obj[fn] = selfFun
    const result = obj[fn](...remainArgs) // 保存最终的对象调用函数结果
    delete obj[fn] // 删除相应的对象函数
    return result
  }
  

Reduce 函数实现

实现 Reduce 函数,主要体现在两点。

  1. 根据参数获取初始值。
  2. 在遍历数组的过程中,必须要注意[ empty ]情况。
答案

  // reduce方法实现
  Array.prototype.myReduce = function(callbackFun) {
    let returnSum = undefined // 叠加器最终变量
    let index = 0 // 数组遍历的索引
    const self = this // 获取当前数组
    if(arguments.length > 1) {
      returnSum = arguments[1]
    } else if(arguments.length === 1) {
      returnSum = self[index]
      index = 1
    }
    while(index < self.length) {
      let hasVal = self.hasOwnProperty(index)
      if(hasVal) {
        let currentVal = self[index]
        returnSum = callbackFun(returnSum, currentVal, index)
      }
      index++ 
    }
    return returnSum
  }
  

New 实现

new 内部机制主要经历以下四个步骤:

  1. 创建一个新的对象。
  2. 将构造函数的作用域赋给该对象。
  3. 执行构造函数中的代码。
  4. 返回该新的对象。

因此要实现一个 new 方法,那么必须抓住两点。

  1. 必须将新创建的对象的原型指针指向构造函数的原型对象。
  2. 执行完构造函数中的代码后,必须判断其返回值是否为对象,若是则 return,若不是则直接返回刚刚新创建的对象。
答案

  // new实现
  function myNew() {
    const obj = new Object() // 创建一个新的对象
    const argsFun = Array.prototype.shift.call(arguments) // 获取构造函数
    obj.__proto__ = argsFun.prototype // 将新对象的原型指针指向构造函数的原型对象
    const remainArgs = Array.prototype.slice.call(arguments, 0) // 获取剩余参数
    const result = argsFun.apply(obj, remainArgs) // 执行构造函数中的代码
    return typeof result === 'object' && result !== null ? result : obj // 返回新的对象
  }
  

需要注意的是,new 是属于关键字,不能被重写,因此只能使用函数来模拟实现 new 的功能。

async/await 实现

async/await 是 generator 语法糖,基于 promise 进行编写的。由于 async/await 都是属于关键字,不能被重写,只能通过使用函数来模拟实现。

要实现 async/await 函数,必须遵循下列几点要求。

  1. async 函数只能面向 generator 生成器函数,并且最终会返回一个函数,函数执行后会返回一个 Promise 实例。
  2. 执行 generator 生成器函数时,需使用递归形式进行自执行,直到最后的 done 为 true 为止才返回最终的结果。
  3. 需要进行异常处理。
答案

  // async/await实现
  function asyncFun(callbackFun) {
    return  function() {
      const self = this
      const args = arguments
      return new Promise((resolve, reject) => {
        const gen = callbackFun.apply(self, args)
        function _next(value) { // 用于遍历迭代器
          awaitFun(resolve, reject, gen, _next, _throw, 'next', value)
        }
        function _throw(err) { // 用于迭代器遍历时抛出异常
          awaitFun(resolve, reject, gen, _next, _throw, 'throw', err)
        }
        _next() // 迭代器自执行
      })
    }
  }
  function await(res, rej, gen, nextFun, throwFun, funKey, args) {
    const nextRes = gen[funKey](args)
    if (nextRes.done) {
      res(nextRes.value)
    } else {
      Promise.resolve(nextRes.value).then(nextFun, throwFun)
    }
  }
  

双向数据绑定的实现

在 JavaScript 为双向数据绑定的实现提供了两个 API,分别对应于 ES5 版本的Object.defineProperty和 ES6 版本Proxy

  1. Object.defineProperty版本

    答案
    
      // Object.defineProperty实现双向数据绑定
      const data = {
        text: ''
      }
      const inputEle = document.querySelector('input')
      const spanEle = document.querySelector('span')
      Object.defineProperty(data, 'text', {
        set(val) {
          spanEle.text = val
        }
      })
      inputEle.addEventListener('input', function(e) {
        data.text = this.value
      })
      
  2. Proxy版本

    答案
    
      // Proxy实现双向数据绑定
      const data = {
        text: ''
      }
      const inputEle = document.querySelector('input')
      const spanEle = document.querySelector('span')
      const handler = {
        set(target, key, value) {
          target[key] = value
          spanEle.text = value
          return value
        }
      }
      const proxyObj = new Proxy(data, handler)
      inputEle.addEventListener('input', function(e) {
        proxyObj.text = this.value
      })
      

Object.create方法实现

实现 Object.create 方法,主要是利用寄生式继承。

简单理解,就是先构建一个空构造函数,接着将参数对象赋给构造函数的原型对象,并将原型对象中 constructor 指回空构造函数,最后返回空构造函数实例

答案

  // Obejct.create方法实现
  Object.prototype.myCreate = function(obj) {
    function Noop() {}
    Noop.prototype = obj
    Noop.constructor = Noop
    return new Noop()
  }
  

instanceof 实现

instanceof 的机制就是判断右边的构造函数的原型对象是否在左边对象的__proto__的原型链上。

要实现 instanceof,那么需要注意的是,利用遍历一直寻找左边对象的__proto__,若等于右边构造函数的原型对象,则返回 true,否则直到遍历为 null 为止

答案

  // instanceof实现
  function myInstanceof(left, right) {
    let leftPro = left.__proto__
    let rightPro = right.prototype
    while(leftPro) {
      if(leftPro === rightPro) return true
      leftPro = leftPro.__proto__
    }
    return false
  }
  

Object.getOwnPropertyNames方法实现

Object.getOwnPropertyNames 用于获取一个对象的所有属性名,不包括原型对象上的。

那么,要实现 Object.getOwnPropertyNames 方法,可结合使用 in 操作符和 Object.hasOwnProperty 实现

答案

  // Object.getOwnPropertyNams实现
  Object.myGetOwnPropertyNames = function(obj) {
    if(typeof obj !== 'object') throw new Error('This is not a object...')
    const resultArr = []
    for(let allKey in obj) {
      if(Object.hasOwnProperty.call(obj, allKey)) {
        resultArr.push(allKey)
      }
    }
    return resultArr
  }
  

Promise 实现

Promise 拥有三种状态,分别是pending、fullfilled、rejected。

而 Promise 的实现原理就是,通过队列形式控制每个成功回调以及失败回调的处理,内部使用私有变量保存当前处理状态以及处理所得到的值。接下来一一分析每个方法的实现要点。

  1. 静态resolve方法。

    返回一个Promise实例,先判断当前状态是否为pending,若不是直接返回。

    然后判断参数值是否为Promise实例,若是则必须调用then方法等待其调用完毕才能进行下一步。

    最后需要对成功回调队列和失败回调队列中进行清空处理(即使用shift方法,一一执行其回调函数)。

    上述操作必须在一个setTimeout中进行。

  2. 静态reject方法。

    返回一个Promise实例,先判断当前状态是否为pending,若不是直接返回。

    无需判断为Promise实例,直接对失败回调队列进行清空处理(即使用shift方法,一一执行其回调函数)。

    上述操作必须在一个setTimeout中进行。

  3. then方法。

    返回一个Promise实例,先创建两个处理函数(分别为成功处理函数和失败处理函数)。

    成功处理函数中操作便是判断then方法的第一个参数是否为函数,若是则先执行其函数(其中内部值作为其函数的参数),接着再执行静态resolve方法清空成功回调队列操作。

    失败处理函数中操作便是判断then方法的第二个参数是否为函数,若是则先执行其函数(其中内部值作为其函数的参数),接着再执行静态reject方法清空失败回调队列操作。

    根据状态判断,当内部状态为pending时,则成功回调队列push进成功处理函数,失败回调队列push进失败处理函数,当内部状态为fullfilled时,则直接执行成功处理函数(其中内部值作为其函数的参数),当内部状态为rejected时,则直接执行失败回调处理函数(其中内部值作为其函数的参数)。

  4. catch方法。

    相当于直接执行this.then(undefined, rejectedFun)。

  5. finally方法。

    相当于直接执行this.then(val => MyPromise.resolve(callback()).then(() => val), val => MyPromise.reoslve(callback).then(() => { throw val }))

  6. 静态all方法。

    返回一个Promise实例,创建一个空数组,遍历参数Promise数组,执行每一个Promise的then方法,然后将处理好的值push进空数组中,当判断数组长度与参数Promise数组长度一致时即可使用reoslve将处理好的数组返回。

  7. 静态race方法。

    返回一个Promise实例,遍历参数Promise数组,执行每一个Promise的then方法,一旦有值返回即可直接resolve返回。

答案

  // Promise类实现
  class MyPromise {
    constructor(handleFun) {
      handleFun(this._resolve, this._reject)
      this._status = 'pending' // 当前的promise状态
      this._value = undefined  // 当前promise处理的值
      this._fullFilledQuene = [] // 当前promise接受处理队列
      this._rejectedQuene = [] // 当前promise拒绝处理队列
    }
    _resolve(val) {
      setTimeout(function() {
        if(this._status !== 'pending') return
        this._status = 'fullfilled'
        const runFullfilledQuene = (res) => {
          let callback
          while(callback = this._fullFilledQuene.shift()) {
            callback(res)
          }
        }
        const runRejectedQuene = (err) => {
          let callback
          while(callback = this._rejectedQuene.shift()) {
            callback(err)
          }
        }
        if(val instanceof MyPromise) {
          val.then(res => {
            this._value = res
            runFullfilledQuene(res)
          }, err => {
            runRejectedQuene(err)
          })
        } else {
          this._value = val
          runFullfilledQuene(val)
        }
      })
    }
    _reject(err) {
      setTimeout(function() {
        if(this._status !== 'pending') return
        this._status = 'rejected'
        this._value = err
        let callback
        while(callback = this._rejectedQuene.shift()) {
          callback(err)
        }
      })
    }
    then(fullFilledFun, rejectedFun) {
      return new MyPromise((resolve, reject) => {
        // 封装一个promise回调成功的处理
        const handleFullFilledFun = val => {
          if(typeof fullFilledFun !== 'function') {
            reject(val)
          } else {
            const value = fullFilledFun(val)
            if(value instanceof MyPromise) {
              value.then(res => {
                resolve(res)
              }, err => {
                reject(err)
              })
            }
          }
        }
        // 封装一个promise回调失败的处理
        const handleRejectedFun = err => {
          if(typeof rejectedFun !== 'function') {
            reject(err)
          } else {
            const value = rejectedFun(err)
            if(value instanceof MyPromise) {
              value.then(res => {
                resolve(res)
              }, err => {
                reject(err)
              })
            }
          }
        }
        switch(this._status) {
          case 'pending':
            this._fullFilledQuene.push(handleFullFilledFun)
            this._rejectedQuene.push(handleRejectedFun)
            break
          case 'fullfilled':
            handleFullFilledFun(this._value)
            break
          case 'rejected':
            handleRejectedFun(this._value)
            break
        }
      })
    }
    catch(errFun) {
      return this.then(undefined, errFun)
    }
    finally(callback) {
      return this.then(
        value => MyPromise.resolve(callback()).then(() => value),
        err => MyPromise.resolve(callback()).then(() => err)
      )
    }
    static resolve(val) {
      if(val instanceof MyPromise) return val
      return new MyPromise(resolve => resolve(val))
    }
    static reject(err) {
      return new MyPromise((resovle, reject) => reject(err))
    }
    static all(list) {
      return new MyPromise(resolve => {
        let arrPromise = []
        for(let [index, promiseItem] of list) {
          promiseItem.then(res => {
            arrPromise.push(res)
            if(arrPromise.length === list.length) resolve(arrPromise)
          })
        }
      })
    }
    static race(list) {
      return new MyPromise(resolve => {
        for(let [index, promiseItem] of list) {
          promiseItem.then(res => {
            resolve(res)
          })
        }
      })
    }
  }
  

防抖/节流实现

防抖的原理是,通过闭包缓存定时器,当缓存的定时器不为空时,则使用clearTimeout进行清除,然后再重新赋值为一个setTimeout,频繁操作中直到最后一步操作才会真正起效,前面的操作都会被清除掉

答案

  // 防抖(频繁操作最终只会执行一次)
  function debounce(fn, time) {
    let timeout = undefined
    return function() {
      let context = this
      let args = arguments
      if(!timeout) clearTimeout(timeout)
      timeout = setTimeout(() => {
        fn.apply(context, args)
      }, time)
    }
  }
  

节流的原理是,通过闭包缓存当前时间,当下一步重复操作的时间减去缓存时间大于参数时间时,那么就会直接执行函数,控制在规定的时间内执行函数

答案

  // 节流(频繁操作只会每隔一段时间操作一次)
  function throttle(fn, time) {
    let tempTime = Date.now()
    return function() {
      let currentTime = Date.now()
      if(currentTime - tempTime > time) {
        fn.apply(this, arguments)
        tempTime = currentTime
      }
    }
  }
  

函数柯里化实现

函数柯里化的原理,就是将多个参数的单一函数转化为单一参数的多个函数

在实现函数柯里化中,通过使用递归形式来将多个单一参数转换为多个参数为止。

如:sum(a, b, c, d) 相当于 sum(a)(b)(c)(d),也相当于 sum(a, b)(c)(d)

答案

  // 函数柯里化
  function curry(fn) {
    const judge = (...args) => 
      args.length >= fn.length
        ? fn(...args)
        : (...arg) => judge(...args, ...arg)
    return judge
  }
  

React Summary(全总结)

本文的目的在于对React快速复习的知识点总结,便于后期快速阅览。🤔当然,你可以把它当成是一个React知识点大纲,对于面试前的准备也是一个很好的复习哈 💪。

后期若有新的知识点内容,会继续更新哈 😄。

目录

组件

  1. 创建组件的方式可分为两种,分别为 ES5 和 ES6 两种方式。

    // ES5
    var React = require('react')
    var Hello = React.createClass({
      propTypes: { // 类型检查
        name: React.PropTypes.string
      },
      getDefaultProps: function() { // 获取默认属性
        return {
          name: 'Andraw-lin'
        }
      },
      getInitialState: function() { // 初始化状态state
        return {
          count: 1
        }
      },
      render: function() {
        return <div>hello, { this.props.name } { this.state.count }</div>
      }
    })
    
    // ES6
    import React from 'react'
    import PropTypes from 'prop-types'
    class Hello extends React.Component {
      static propTypes = { // 类型检查
        name: PropTypes.string
      }
      static defaultProps = { // 获取默认属性
        name: 'Andraw-lin'
      }
      constructor(props) {
        super(this)
        this.state = { // 初始化状态state
          count: 1
        }
      }
      render() {
        return <div>Hello, { this.props.name } { this.state.count }</div>
      }
    }
  2. 无状态组件,也叫函数式组件。无状态组件只传入 props 和 context 两个参数,简单滴说,无状态组件不存在 state,也没有生命周期方法,只有一个 render 方法

    function Button({ color = 'blue', text = 'OK' }) {
      return (
        <button className={`btn-${color}`}>
          <em>{text}</em>
        </button>
      )
    }
  3. PureComponent是 react 15.3 后引入的,和普通的Component功能几乎一致,但**PureComponentshouldComponentUpdate不会直接返回true**,而是会对属性进行浅层比较,也就是仅比较直接属性是否相等。

    下面模拟PureComponent组件的实现。

    class Demo extends Component {
      shouldComponentUpdate(nextProps, nextState) {
        const {props, state} = this
        function shallowCompare(a, b) {
          if (a === b) return true
          if (Object.keys(a).length !== Object.keys(b).length) return false
          return Object.keys(a).every(k => a[k] === b[key])
        }
        return !shallowCompare(nextProp, props) && !shallowCompare(nextState, state)
      }
    }

总结:一般情况下,都是使用普通Component,若组件只是作为渲染使用,那么使用无状态组件Functional Component,若组件是基本不变化组件,那么使用纯组件PureComponent

JSX

  1. 在 React 中创建的虚拟元素可以分为两类:DOM 元素(DOM elment)、组件元素(component element),分别对应着原声 DOM 元素与自定义元素。

    其中,DOM 元素标签的首字母是小写,而组件元素则是大写

  2. 针对模板中需要根据浏览器 IE 来输出标签时,需进行如何转化:

    // 日常使用
    <!--[if IE]>
      <p>work in IE brower</p>
    <![endif]-->
    
    // JSX 中使用需进行转化
    {
      (!!window.ActiveXObject || 'ActiveXObject' in window) ?
       <p>work in IE brower</p> : ''
    }
  3. 在 JSX 中使用元素属性时,有两个属性需要注意的是:

    • class 属性改为 className;
    • for 属性改为 htmlFor;
  4. 在 JSX 中表单标签使用 disabled、required、checked、readOnly 等时,若不设置值时,都会默认为 true。直接上栗子🌰:

    <Checkbox checked />
    // 相当于
    <Checkbox checked={true} />
    
    // 一旦设置false时,就需要自行设置而无法简化
    <Checkbox checked={false} />
  5. React 提供 dangerouslySetInnerHTML 属性,可用于转译 HTML 标签的内容,同时可避免 React 转义字符。直接上栗子🌰:

    <div dangouslySetInnerHTML={{__html: 'cc &copy; 2015'}} />

生命周期

组件的生命周期主要分为三个阶段,分别为:挂载阶段、更新阶段和卸载阶段

  1. 挂载阶段会执行以下回调函数。

    • constructor()
    • componentWillMount()
    • render()
    • componentDidMount()
  2. 更新阶段会分为三种情况,分别是父组件更新、自身状态更新、forceUpdate强制更新

    • 父组件更新时,会执行以下回调函数。
      • componentWillReceiveProps()
      • shouldComponentUpdate()
      • render()
      • componentDidUpdate()
    • 自身状态更新时,会执行以下回调函数。
      • shouldComponentUpdate()
      • componentWillUpdate()
      • render()
      • componentDidUpdate()
    • forceUpdate强制更新时,会执行以下回调函数。
      • componentWillUpdate()
      • render()
      • componentDidUpdate()

    注意:shouldComponentUpdate主要用于提升性能,componentWillReceiveProps主要用来将新的props同步到state

  3. 卸载阶段只会执行componentWillUnmount回调函数,主要用于清除定时器、解绑自定义事件,避免内存泄漏。

组件的属性和状态

  1. props都是只读的,不能进行更改。

    其中有一个比较特殊的属性——children,代表当前组件的子组件集合,自定义属性名不能与该名字重复。

    class List extends Component {
      render() {
        return <ol>{ this.props.children }</ol>
      }
    }
    
    <list>
      <li>1</li>
      <li>2</li>
    </list>
  2. 通过配置静态属性defaultProps能给予组件默认属性值

    class User extends Component {
      static defaultProps = {
        name: 'Andraw-lin'
      }
    }
  3. 类型检测PropTypes在版本15.5前都是在React包中,后面的版本都是分离到单独的prop-types包中,需单独引入。

    import PropTypes from 'prop-types'
    
    class User extends Component {
      static propTypes = {
        name: PropTypes.string.isRequired
      }
    }
  4. 初始化state方式有两种。分别为构造函数中定义和普通属性定义。(其中普通属性定义还不是语言标准,属于提案,不过babel已经支持)

    // 构造函数中定义
    class User extends Component {
      constructor() {
        super(this)
        this.state = {
          name: 'Andraw-lin'
        }
      }
    }
    
    // 普通属性定义(还不是语言标准,属于提案,不过babel已经支持)
    class User extends Component {
      state = {
        name: 'Andraw-lin'
      }
    }
  5. setState方法是一个异步方法,React会在一个生命周期内将多次setState操作合并成一次。(这也是为什么在setState后立马取值,是无法取到更新的值原因)

    若想立马获取setState更新后的值,有两种方式,分别是将计算结果存储下来使用setState方法第二个参数回调函数

    // 将计算结果存储下来(最简单方式)
    state = { time: 1 }
    componentWillMount() {
      const newTime = this.state.time + 1
      this.setState({ time: newTime })
      console.log(newTime) // 2
    }
    
    // 使用setState方法第二个参数回调函数
    state = { time: 1 }
    componentWillMount() {
      this.setState({ time: this.state.time + 1 }, () => {
        console.log(this.state.time) // 2
      })
    }
  6. 不要将什么数据都定义到state里,坚持一个基本原则:能发到局部作用中的,能放到this普通属性中的,都不要放到state

    // 局部作用域
    let name = 'Andraw-lin'
    class ...
    
    // this普通属性
    class User extends Component {
      name: 'Andraw-lin'
    }

事件处理

  1. React 基于 Virtual DOM 实现了一个 SyntheticEvent(合成事件)层,组件中定义的事件处理器会接收到一个 SyntheticEvent 对象的实例,与原生的浏览器事件一样拥有同样的接口,同样支持事件的冒泡机制(即使用 stopPropagation 和 preventDefault 方法来中断)。

  2. React中绑定事件方式和HTML绑定事件区别。

    • React绑定事件是驼峰原则(如 onClick),HTML绑定事件是全部小写原则(如 onclick)。
    • React绑定事件处理的是一个函数,HTML绑定事件处理的是一个字符串。
  3. React合成事件实现中,采用了事件代理机制

    不会把处理函数直接绑定到真实的节点上,而是把所有事件绑定到结构的最外层,使用一个统一的事件监听器。该事件监听器维持了一个映射来保存所有组件内部的事件监听和处理函数。当组件挂载或卸载时,只是在这个统一的事件监听器上插入或删除一些对象。当事件发生时,首先被该统一的事件监听器处理,然后在映射里找到真正的事件处理函数并调用。

  4. 在使用 ES6 class 时,无法进行自动绑定 this 为 React 组件本身实例,而需要手动绑定。手动绑定方式有三种,分别为bind方法、构造函数声明和箭头函数。

    // bind方法(传参数或不传参数)
    render() {
      return (
        <button onClick={ this.handleClick.bind(this, 'test') }>Test</button>
      )
    }
    // bind方法(不传参数还可以使用双冒号,还是stage0草案中提供方案,babel已支持)
    render() {
      return (
        <button onClick={ ::this.handleClick.bind }>Test</button>
      )
    }
    
    // 构造函数声明
    constructor(props) {
      super(props)
      this.handleClick = this.handleClick.bind(this)
    }
    
    // 箭头函数(第一种方式)
    handleClick = () => {
      console.log('hanldeClick')
    }
    render() {
      return (
        <button onClick={this.handleClick}>
        	Click me
        </button>
      )
    }
    // 箭头函数(第二种方式)
    handleClick() {}
    render() {
      return (
        <button onClick={ (e) => this.handleClick(e) }>
        	Click me
        </button>
      )
    }
    
  5. React中若想对绑定事件传入相应的参数,有两种方式,分别是bind方式和箭头函数。

    // bind方法
    <button onClick={this.deleteRow.bind(this, id)}>Delete Row</button>
    
    // 箭头函数
    <button onClick={(e) => this.deleteRow(id, e)}></button>
  6. 阻止 React 事件冒泡的行为只能用于 React 合成事件系统中,而无法阻止原生事件的冒泡**。**在原生事件中的阻止冒泡行为,却可以阻止 React 合成事件的传播。

  7. 合成事件和原生事件区别。

    • 合成事件只支持冒泡机制,原生事件则支持 DOM 事件流(即事件捕获阶段、目标对象本身的事件处理程序调用、事件冒泡阶段三个阶段)。

    • 合成事件只是原生事件的一个子集。

    • 合成事件处理的是函数,原生事件处理的则是字符串。

    • 绑定方式不同,主要有以下区别。

      绑定原生事件的方式有很多种,具体如下:

      • 直接在 DOM 元素中绑定

        <button onclick="alert(1);">Test</button>
      • 通过元素的事件属性赋值方式实现绑定

        el.onclick = e => { alert(1); }
      • 通过事件监听函数来实现绑定

        el.addEventListener('click', () => {})
        el.attachEvent('onclick', () => {})

      React 合成事件则简单很多,如下:

      <button onClick={this.handleClick}>Test</button>

组件通信

组件间通信分为四种情况,分别为:父组件向子组件通信、子组件向父组件通信、跨级组件间通信、没有嵌套关系的组件间通信

  1. 父组件向子组件通信

    父组件通过 props 向子组件传递需要的信息。直接上🌰:

    // Parent.js
    import React, {Component} from 'react'
    import Child from './Child'
    export default class Parent extends Component {
      constructor(props) {
        super(props)
        this.state = {
          text: 'Parent'
        }
      }
      render() {
        return (
        	<Child text={this.state.text} />
        )
      }
    }
    
    // Child.js
    import React, {Component} from 'react'
    export default class Child extends Component {
      render() {
        return (
        	<span>{ this.props.text }</span>
        )
      }
    }
  2. 子组件向父组件通信

    子组件若要和父组件进行通信,有两种处理方式:

    • 回调函数

      类似于 Props 传递,只是这次传递的是一个函数,父组件可获取到子组件运行时的状态。直接上🌰:

      // Parent.js
      render() {
        return (
          <Child parentClick={this.parentClickFun} />
        )
      }
      
      // Child.js
      render() {
        return (
          <span onClick={this.props.parentClick}></span>
        )
      }
      
    • 自定义事件机制

      自定义事件可用于子组件向父组件之间的通信,但是使用次数不多,一般是用于没有嵌套关系的组件间通信,下面会讲解。

  3. 跨级组件间通信

    通过context可以让祖先组件直接把属性传递给后代组件

    定义context需双向声明,即在祖先组件中声明静态属性childContextTypes,在后代组件中再次声明静态属性contextTypes,最后在祖先组件普通方法getChildContext中定义传输属性,这样后代组件便可以直接获取相应属性了。

    import PropTypes from 'prop-types'
    // 祖先组件
    class Ancestor extends Component {
      static childContextTypes = {
        name: PropTypes.string
      }
      getChildContext() {
        return {
          name: 'Andraw-lin'
        }
      }
    }
    // 后代组件
    class Child extends Component {
      static contextTypes = {
        name: PropTypes.string
      }
    }
    
  4. 没有嵌套关系的组件间通信

    对于无嵌套关系的组件间通信,可使用自定义事件机制。需注意是,在 componentDidMount 事件中,在组件挂载完成再订阅事件,而在 componentWillUnmount 事件中,在组件卸载再取消订阅事件。使用的是 Nodejs Events 模块实现自定义事件机制,直接上栗子🌰:

    // events.js
    import { EventEmitter } from 'events'
    export default new EventEmitter()
    
    // one.js
    import emitter from './events'
    myClick() {
      emitter.emit('test', { a: 1 })
    }
    
    // two.js
    import emitter from './events'
    componentDidMount() {
      this.clickEvent = emitter.on('test', data => {
        console.log(data)
      })
    }
    componentWillUnmount() {
      emitter.removeLister(this.clickEvent)
    }

当然除了上述列举通信方法外,还可以通用 redux 进行管理。

表单处理

React 对于表单处理上,主要分为两种类型:受控组件和非受控组件

  1. 受控组件

    当表单的状态发生变化时,都会被写入到组件的 state 中,在 React 中被称为受控组件。

    在受控组件中,组件渲染出的状态和它的 value 或 checked prop 相对应。

    React 受控组件更新 state 的流程主要如下:

    • 可通过在初始 state 中设置表单的默认值;
    • 当表单的值发生变化时,调用 onChange 事件处理器;
    • 事件处理器通过合成事件对象 e 拿到改变后的状态,并更新应用的 state;
    • 通过 setState 方法来触发视图的重新渲染,完成表单组件值的更新;

    直接上🌰:

    import React, { Component } from 'react'
    
    class App extends Component {
      constructor(props) {
        super(props)
        this.handleChange = this.handleChange.bind(this)
        this.state = {
          selectValue: 'guangzhou'
        }
      }
      handleChange(e) {
        const { value } = e.target.value
        this.setState({ selectValue: value })
      }
      render() {
        return (
          <select value={selectValue} onChange={handleChange}>
            <option value="guangzhou">广州</option>
            <option value="shanghai">上海</option>
            <option value="beijing">北京</option>
          </select>
        )
      }
    }

    可以睇到,React 本身是一个单向数据流绑定,而在表单上使用 onChange 事件后,就实现了双向数据绑定。

  2. 非受控组件

    如果一个表单组件没有 value props(单选按钮和复选框对应的是 checked prop)时,就可以称为非受控组件。

    非受控组件和受控组件都可以使用 defaultValue 和 defaultChecked 来设置组件的默认状态

    在 React 中,非受控组件是一种反模式,它的值不受组件自身的 state 或 props 控制。一般情况下,都需要通过为其添加 ref 属性来访问渲染后的底层 DOM 元素

    直接上栗子🌰:

    import React, { Component } from 'react'
    
    class App extends React {
      constructor(props) {
        super(props)
        this.handleSubmit = this.handleSubmit.bind(this)
      }
      handleSubmit(e) {
        e.preventDefault()
        const { value } = this.refs.name
        console.log(value)
      }
      render() {
        return (
          <form onSubmit={this.handleSubmit}>
          	<input ref='name' type='text' defaultValue='Andraw-lin' />
            <button type='submit'>submit</button>
          </form>
        )
      }
    }
  3. 对比受控组件和非受控组件

    通过 defaultValue 或者 defaultChecked 来设置表单的默认值,仅仅只会被渲染一次,在后续的渲染中并不会起到作用。举个栗子🌰:

    // 受控组件
    <input 
      value={this.state.value}
      onChange={e => {this.setState({
        value: e.target.value.toUpperCase()
      })}}>
        
    // 非受控组件
    <input 
      defaultValue={this.state.value}
      onChange={e => {this.setState({
        value: e.target.value.toUpperCase()
      })}}>

    上述例子中,受控组件可以将用户输入的英文字母转化为大写后输出展示,而在非受控组件中则不会。多数情况下,对于非受控组件,并不需要通过 change 事件

    受控组件和非受控组件的最大区别就是:非受控组件的状态并不会受用用状态的控制,应用中也多了局部组件状态,而受控组件的值来自于组件的 state。具体体现在如下方面:

    • 性能上的问题

      受控组件在表单值每次发生变化时,都会调用一次 onChange 事件处理器,导致了一部分的性能损耗。

      使用非受控组件不会出现这些问题,但在 React 中仍然是不提倡使用非受控组件

    • 是否需要事件绑定

      受控组件必须使用 onChange 事件,而非受控可以选择性滴使用。

  4. 表单组件的几个重要属性

    • 状态属性

      React 的 form 组件提供了几个属性来展示组件的状态:

      • value: 类型为 text 的 input 组件、textarea 组件以及 select 组件都借助 value 属性来展示应用的状态;
      • checked:类型为 radio 或 checkbox 的组件值为 boolean 类型的 selected 属性 来展示应用的状态;
      • selected:该属性可用于 select 组件下面的 option 上,React 并不建议使用这种方法表示状态,而推荐使用 value 方法
    • 事件属性

      在状态属性发生变化时,会触发 onChange 事件属性,受控组件中的 change 事件与 HTML DOM 中提供的 input 事件行为类似

样式处理

React 中可通过 style prop 来给组件设置行内样式,但需注意的是,style prop 必须是一个对象。在设置样式时,需要注意两点:

  • 自定义组件建议支持 className prop,让用户使用时添加自定义样式;
  • 设置行内样式时要使用对象;

直接上栗子🌰:

// className Prop
render() {
  const myClass = this.state.name ? 'myNameClass' : 'myClass'
  return (
    <span className={myClass}>className</span>
  )
}

// style Prop
const style = {
  color: 'white',
  msTransition: 'all'
}
const component = <span style={style} />
  1. React 样式处理

    • 样式中像素值

      React 会自动对支持数值px的属性添加 px,直接上🌰:

      const style = { height: 10 }

      另外,有些属性除了支持 px 为单位的像素值,还支持数字直接作为值,此时 React 对这些支持数字的属性则不会自动添加 px,如 lineHeight。

    • 使用 classnames 库

      在上述 className prop 栗子中,可以看到,动态类名需根据状态来决定是否添加,一旦一个标签需要特别多样式名也就导致产生多个定义,因此并不友好。使用 classnames 库则可以进行解决,利用语法糖提高开发效率,对上述 className prop 栗子使用 classnames 库进行改造:

      import classNames from 'classnames'
      
      render() {
        const myClassContainer = classNames({
          'myNameClass': this.state.name,
          'myClass': !this.state.name
        })
        return (
          <span className={myClassContainer}>className</span>
        )
      }
  2. CSS Modules

    有兴趣的可以先理解一下 CSS Modules。目前就先上一个 CSS Modules 结合 React 实践,直接上栗子🌰:

    // index.css
    .root { ... }
    .confirm { ... }
    .disabledConfirm { ... }
    
    // index.js
    import React, { Component } from 'react'
    import classNames from 'classnames'
    import indexStyles from './index.css'
    
    class Index extends Component {
      render() {
        const cx = classNames({
          confirm: !this.state.disabled,
          disabledConfirm: this.state.disabled
        })
        return (
          <div className={indexStyles.root}>
            <a className={indexStyles[cx]}>Confirm</a>
          </div>
        )
      }
    }
                      

    一般情况下,组件最外层的节点对应的 class 名称为 root。也许有人会发现,我们需要拼命地写 style.* 来获取对应的类名,这是一个很繁琐和重复的工作,因此可以使用 react-css-modules 库。直接上栗子🌰:

    // index.js
    import React, { Component } from 'react'
    import classNames from 'classnames'
    import CSSModule from 'react-css-modules'
    import indexStyles from './index.css'
    
    class Index extends Component {
      render() {
        const cx = classNames({
          confirm: !this.state.disabled,
          disabledConfirm: this.state.disabled
        })
        return (
          <div styleName={root}>
            <a styleName={cx}>Confirm</a>
          </div>
        )
      }
    }
    export default CSSModules(Index, indexStyles)
    

    使用 react-css-module 对比原有的 CSS Module,具体有如下特点:

    • 不再需要关注是否使用驼峰来命名 class 名;

    • 不用每一次使用 CSS Module 的时候都关联 style 对象;

    • 使用 CSS Module 容易使用 :global 去解决特殊情况。而使用 react-css-module 则是使用 styleName 来表示局部,使用 className 表示全局。直接上栗子:

      // CSS Module
      <div className={indexStyle['test']}></div>
      
      // react css module
      <div styleName={global-css} ></div>
    • 当 styleName 关联了一个 undefined CSS Module 时,react-css-module 会发出一个警告;

组件的抽象与复用

在旧版本React里,实现组件间的抽象和复用主要通过mixin实现。由于mixin方法一致存在重名覆盖问题,对于大型项目将会是一个致命缺陷,因此在新版本中已被消除。

目前要实现组件的抽象与复用,主要有三种方法,分别是继承、组合和高阶组件

  1. 继承

    处理A is B问题。若两个以上的组件一部分功能是一样的,那么可抽象为一个父类,通过继承解决重复问题。

    class PureComponent extends Component {
      shouldComponentUpdate(nextProp, nextState) {
        const {props, state} = this
        function shallowCompare(a, b) {
          if (a === b) return true
          if (Object.keys(a).length !== Object.keys(b).length) return false
          return Object.keys(a).every(k => a[k] === b[key])
        }
        return !shallowCompare(nextProp, props) && !shallowCompare(nextState, state)
      }
    }
    class Parent extends PureComponent {}
    class Child extends PureComponent {}

    关于继承一定要谨慎,如果想不清楚就不要抽象父类。继承设计不好,到后面就会非常脆弱并且不好维护。

  2. 组合

    处理A has B问题。比如汽车和人都会跑,人和跑不是“是”关系,而是“拥有”关系,若使用继承的方法实现抽线,父类就很难抽象出来。而组合却可以很好实现

    javascript中实现组合方式有多种,以下就介绍三种,分别为内部调用、拷贝、ES5中mixin方法

    • 内部调用

      const map = {
        run() {
          this.runState = true
        }
        stop() {
          this.runState = false
        }
      }
      class People {
        run(...args) {
          return map.run.call(this, ...args)
        }
        stop(...args) {
          return map.stop.call(this, ...args)
        }
      }
    • 拷贝

      实现一个extend函数,也可以借用jQuery$.extends方法,又或者ES6中的Object.assign方法。

      function extend(obj1, obj2) {
        Object.keys(obj2).forEach(key => {
          obj1[key] = obj2[key]
        })
      }
      class People {}
      // 使用时
      extend(People.prototype, map)
      $.extends(People.prototype, map)
      Object.assign(People.prototype, map)
    • React ES5 mixin

      var setIntervalMixin = {
        componentWillMount() {
          this.intervals = []
        },
        setInterval() {
          this.intervals.push(setInterval.apply(null, arguments))
        },
        componentWillUnmount() {
          this.intervals.forEach(clearInterval)
        }
      }
      const Demo1 = React.createClass({
        mixins: [setIntervalMixin]
      })
      const Demo2 = React.createClass({
        mixins: [setIntervalMixin]
      })

      不推荐的做法。

  3. 高阶组件

    实现高阶组件的方式有两种:调用传入的组件继承传入的组件

    // 调用传入的组件
    function HOC1(InnerComponnet) {
      return class WrapComponent extends Component {
        render() {
          return (
          	<InnerComponent ...this.props>
            	{ this.props.children }
            </InnerComponent>
          )
        }
      }
    }
    let Demo1 = class extends Component {}
    Demo1 = HOC1(Demo1)
    
    // 继承传入的组件
    function HOC2(InnerComponent) {
      return class WrapComponent extends InnerComponent {}
    }
    let Demo2 = class extends Component {}
    Demo2 = HOC2(Demo2)

    注意:一般只在传入组件外围进行一些操作时,建议使用第一种方法。如果想在传入组件的内部进行一些操作,比如改写render,则使用第二种方法

DOM 相关

  1. React通过ref给了我们饮用组件和DOM元素的能力。

    class User extends Component {
      render() {
        return (
          <input ref={(input) => this.nameInput = input} type="text" />
        )
      }
    }

    ref中值若为函数时,会在componentDidMountcomponentDidUpdate后执行。

  2. React会对输出内容进行XSS过滤,但在某些情况下不想要这个功能,比如在接口返回HTML片段情况下,需要用到**dangerouslySetInnerHTML,它可以将HTML设置到DOM上**。

Hook

  1. 使用Hook的目的在于以下几个方面。

    • 组件间的重复逻辑进行复用。虽然可使用render props和高阶组件方案,但这两类方案都需要重新组织组结构,导致代码难以理解以及不好维护。
    • 对组件内相关联的状态以及逻辑统一管理。如获取后端数据相关变量以及逻辑、订阅事件以及消除事件等。
    • 对函数式组件进行扩展,避免了旧版本函数式组件需要增添状态管理或生命周期等逻辑时,必要地转化为不好理解的class组件。

    另外,需注明的是,Hook并不打算替代class,只是更好滴拓展函数式组件、复用组件间逻辑以及讲相关联逻辑统一管理。最重要的是,Hook 和现有代码可以同时工作,你可以渐进式地使用他们,采用的是渐进策略。

  2. Hook的本质就是 Javascript 函数,使用它时必须遵循两条规则。

    • 只在函数组件内的最顶层使用Hook。即不能在例如for循环、条件语句等里面使用Hook
    • 只在React函数组件内调用Hook
  3. 在单个组件中使用多个State HookEffect Hook时,React是如何知道哪个state对应哪个useStateuseEffect的?

    答案就是**React靠的是Hook调用的顺序**。直接上个栗子🌰。

    function Form() {
      const [name, setName] = useState('Mary')
      useEffect(() => {
        localStorage.setItem('name', name)
      })
      
      const [surname, setSurname] = useState('haha')
      useEffect(() => {
        document.title = name + ' ' + surname
      })
    }
    // 首次渲染
    useState('Mary')
    useEffect()
    useState('haha')
    useEffect()
    // 二次渲染
    useState('Mary')
    useEffect()
    useState('haha')
    useEffect()
    

    只要Hook的调用顺序在多次渲染之间保持一致,React就能正确滴奖内部state和对应Hook进行关联,一旦部分中间某个useStateuseEffect处于条件语句中,那么后面的useStateuseEffect都无法正确运行或获取正确的值

  4. State Hook中使用的 API 是useState,格式如下。

    const [name, setName] = useState(initialState)
    // 返回一个state,以及更新state的函数
    // 在初始渲染期间,返回的状态state和传入第一个参数initialState值相同
    // 在后续重新渲染中,useState返回的第一个值将始终是更新后最新的state
    

    setName更新函数参数可以是数字、字符串或回调函数等。当参数是回调函数时,回调函数的参数就是更新对应state前的值,看栗子🌰。

    <button onClick={() => setName(preName => preName + 'haha.')}></button>
    // 参数preName就是状态Name上一次更新的值
    

    与 class 组件中setState方法不同,useState不会自动合并更新对象。直接看🌰。

    const [name, setName] = useState({ name: 'haha', age: 12 })
    console.log(name)
    // 第一次输出:{ name: 'haha', age: 12 }
    // 第二次输出:{ name: 'hehe' }
    return <button onClick={() => setName({ name: 'hehe' })}>test</button>
    
    // 针对不会合并情况,处理方案有useReducer以及使用展开运算,下面就介绍一下展开运算符做法
    return <button onClick={() => setName(preName => {...preName, ...{ name: hehe }})}>test</button>
    

    最后,setName更新函数中参数,其内部采用的是Object.is比较算法进行比较的,若传入参数不变,则将会跳过子组件的渲染以及effect的执行

  5. Effect Hook中使用的 API 是useEffect,格式如何。

    useEffect(function)
    // 接收一个包含共同逻辑的函数,如改变DOM、订阅取消事件等
    

    默认情况下,effect将在每轮渲染结束后执行,但你可以选择让它某些值改变时才执行

    React 允许effect返回一个函数代表生命周期componentWillUnmount回调。直接看🌰。

    // useEffect中函数就是componentDidMount和componentDidUpdate结合,而返回函数则是componentWillUnmount
    useEffect(() => {
      const subscription = props.source.subscribe()
      return () => {
        subscription.unsubscribe() // 清除订阅
      }
    })
    

    组件在多次渲染时,则在执行下一个effect之前,上一个effect就已被清除。在上述例子中,组件的每一次更新都会创建新的订阅。

    useEffect与生命周期componentDidMountcomponentDidUpdate不同的是,在浏览器完成布局与绘制后,才会按顺序执行useEffect中的函数。若想同步进行,可使用useLayoutEffect来处理

    默认情况下,effect会在每轮组件渲染完成后执行,一旦effect的一来发生变化,它就会被重新创建。useEffect传递第二个参数,就是作为effect所依赖的数组,只有当依赖的数组中的元素值变化时才会执行。看栗子🌰。

    useEffect(() => {
      const subscription = props.source.subscribe()
      return () => {
        subscription.unsubscribe() // 清除订阅
      }
    }, [props.source])
    // 只有当props.source改变时,useEffect的回调才会执行
    

    如果想执行只运行一次的effect(仅在组件挂载和卸载时执行),可传递一个空数组[]作为第二个参数。就是说,当前的effect不依赖于propsstate中的任何值,永远只会在初次渲染后执行一次

Webpack 优化--开发体验篇

在日常的开发过程中,webpack 带给我们的体验中,除了构建速度外,还有就是监听功能。

对于监听功能,相信童鞋们一点也不陌生,最经典莫过于重新编写源代码时,webpack 便能自动进行构建,再或者重新编写源代码后,除了自动构建外,还有自动刷新网页以保持代码最新。

webpack 已内置文件监听变化重新构建的功能,而 webpack-dev-server 则负责刷新浏览器

目录

  1. 使用自动刷新
  2. 开启模块热加载

使用自动刷新

相信童鞋们对于自动刷新并不陌生,因为在开发过程中会经常遇到过。那么自动刷新又是包含哪些功能?

其实要实现自动刷新,必须包含两部分的内容,分别是文件监听功能和一个用于通知浏览器刷新的内置服务器功能。在上面已经提到过,文件监听功能交给了 webpack,而内置服务器则交给了 webpack-dev-server。

接下来我们就要从上述两个功能点进行讲解和优化,以提升开发效率。

优化——文件监听功能

在 webpack 中开启文件监听功能,有两种方式,分别是

  • 在配置文件 webpack.config.js 设置 watch: true;
  • 在执行 webpack 命令时带上 --watch 参数;

我们先来看看,是如何在配置文件中配置的。

// webpack.config.js
module.exports = {
  watch: true,
  // 监听模式运行时的参数
  watchOptions: {
    // 忽略对匹配到的文件夹进行监听
    ignored: /node_modules/,
    // 监听到文件变化时不会立即重新构建,而是要等300ms,节流,默认值也是300ms
    aggregateTimeout: 300,
    // 每秒钟内询问系统中指定文件有没有发生变化的次数
    poll: 1000
  }
}

文件监听功能的工作原理就是,定时获取源文件的最后编辑时间,每次都存下最新的最后编辑时间,如果发现当前获取的和最后一次保存的最后编辑时间不一致就认为是发生了变化

从上述的配置可以看到,当源文件发生变化时,不会立即通知 webpack 重新构建,而是会先缓存下来,收集一段时间的变化后,才会一次性的告诉 webpack。这样做的目的就是防止高频变化引起不停地重新构建,从而导致卡死状态。

那么问题来了,对于多文件列表,webpack 是如何确定需要监听的文件列表呢?

还是最原始的初始化问题,webpack 在执行时会从配置的 Entry 文件出发,递归解析 Entry 文件所依赖的文件,然后将这些文件一一加入到监听列中去

在了解文件监听功能后,我就要回到正题咯,那如何优化呢?

问题点就出在,webpack 在保存文件最新的最后编辑时间时,是需要占用内存的,同时定时检查和周期检查都是需要占用 CPU 以及 I/O。

所以要优化文件监听功能,就应该从减少需要监听的文件数量和降低检查频率入手

  1. 使用 watchOptions.ignore 忽略对 node_modules 文件夹的检查,将减少内存和 CPU 的使用;
  2. watchOptions.aggregateTimeout 的值越大性能越好,避免频繁重新构建;
  3. watchOptions.poll 的值越小越好,降低检查频率;

虽然说对 webpack 文件监听功能来说,的确是一项不错的优化,但是文件监听只是负责重新构建,还是得自己手动去刷新网页才能获取最新编辑代码,那么这也就降低项目的灵活性。

优化——内置服务器功能

既然说到文件监听功能缺少灵活性,那么有木有方法可以提高其灵活性呢?答案就是内置服务器功能。

webpack 将内置服务器功能都交给了 webpack-dev-server ,webpack-dev-server 在使用时都会默认自带开启 webpack 的文件监听功能(这样就不必要再去手动添加配置开启文件监听功能啦😄)

要控制浏览器刷新,有三种方式,分别是

  • 借助浏览器扩展,通过浏览器提供的接口进行刷新,其中 webstorm 编辑器的 LiveEdit 功能就是使用这种方式的。
  • 往开发的网页注入代理(如websocket),通过代理通知浏览器进行刷新。
  • 将开发的网页装进一个 iframe 中,通过刷新 iframe 去看到最新效果。

webpack 中 webpack-dev-server 功能默认采取的是第二种方式,当监听到源文件发生改变时,会使用 websocket 通知浏览器进行刷新相应网页。

我们先看看怎么配置哈。

module.exports = {
  devServer: {
    // 默认情况下,inline都是为true的
    inline: true
  }
}

先回忆一下 devServer.inline 这个配置项,用于控制是否向 Chunk 中注入代理客户端,默认会注入。

在开启 inline 时,devServer 会向每个输出的 Chunk 中注入代理客户端代码,当我们的项目需要输出很多 Chunk 时,就会导致构建缓慢

为什么需要向每个输出的 Chunk 注入代理客户端代码呢?

其实是因为 devServer 并不知道某个网页是依赖哪些 Chunk,索性就对全部 Chunk 注入一个代理客户端

那该如何优化呢?答案就是关闭这个还不够优雅的 inline 模式啦😄

当关闭 devServer 的 inline 模式后,devServer 会默认将网页装进一个 iframe 中去,编辑源代码时也会自动刷新 iframe 中网页内容。同时你也会发现构建的时间也会大大减少,访问时的地址一般是http://localhost:8080/webpack-dev-server/

当然如果你觉得使用 iframe 的方式不够优雅,但又想保持最优的自动刷新功能时,可以直接向你的模板 HTML 中注入代理客户端的脚本文件,如下:

<script src="http://localhost:8080/webpack-dev-server.js"></script>

但是要小心的是,在发布到线上前,一定要把这段脚本加载删掉。

开启模块热加载

说到模块热加载,其实就是一种模块替换机制,在不刷新网页的情况下能保持代码最新,也是 webpack-dev-server 支持的另一种刷新代码方式。

模块热替换相对于自动刷新有哪些优势?

  • 实时预览反应更快,不需要刷新整个页面,只需要刷新相应模块。
  • 保持当前网页的运行状态,例如在使用 Redux 时,当编写源码时,不会刷新页面,并且当前网页的状态也会保持下来,避免重新加载状态。

先来看看如何配置的。

module.exports = {
  devServer: {
    // 告诉webpack开启模块替换模式
    hot: true
  }
}

在构建过程中,会发现比自动刷新时多出三个文件,其实这三个文件用于模块热替换的,相比之下热替换需要占用内存会更大一点。

就算开启模块热替换,也会经常遇到某些源文件更改后并不是替换的,而是自动刷新页面的,这又是为啥?

原因很简单,当子模块发生更新时,更新事件就会像冒泡一样,一层一层地向上传递,直到顶层文件接收了当前变化的模块,即 module.hot.accept(['./AppComponent']) ,这时就会调用相应的 callback 去执行自定义逻辑,当然若顶层并没有接收该模块,就会直接重新刷新网页

那么为什么我们并没有接收 css 模块处理,但修改 css 文件时依然会触发热替换呢?

原因就是 style-loader 会自动注入用于接收 css 的代码

那么怎么接收法,我们可以直接在顶层代码中编写。

if(module.hot) { // 判断是否开启模块热加载
  module.hot.accept(['./AppComponent'], () => { // 接收模块为./AppComponent
    render(<AppComponent />, document.querySelector('#app')) // 当该模块更新时,会重新render
  })
}

另外,可使用 webpack 内置的 NameModulePlugin 功能,相应地输出哪些模块热替换,不然只会输出ID模块进行了热替换,不方便调试。

const NameModulePlugin = require('webpack/lib/NameMoudlePlugin')

module.exports = {
  plugins: [
    new NameModulePlugin()
  ]
}

关于Nginx-你需要知道的点点滴滴

相信作为一个 Web 开发者来说,对 Nginx 肯定不陌生。我们先来看看 Nginx 在官方到底是一个什么定义。

Nginx 是一个异步框架的 Web 服务器,也可以用作反向代理、负载平衡器和 HTTP 缓存。

很容易理解,Nginx 就是一个 Web 服务器,可以很高效滴处理异步请求。

目录

  1. 何为静态页面?又何为动态页面?
  2. Nginx、Apache和Tomcat区别
  3. Nginx-正向代理和反向代理
  4. Nginx-基本结构
  5. Nginx-处理跨域
  6. Nginx-过滤和Rewrite
  7. Nginx-gzip压缩
  8. Nginx-负载均衡
  9. Nginx-缓存机制

何为静态页面?又何为动态页面?

下面我会直接讲解,不讲那么多废话 🙈。

  • 静态页面:通常以 html 结尾的文件,所有数据都是直接写死到文件中,客户端加载静态页面时,无需对数据库进行操作,而是直接将文件内容呈现出来。
  • 动态页面:通常以 php、jsp、asp 结尾的文件,所有数据都是存储到数据库上的,客户端请求文件时,服务端需从数据库中获取数据并动态填充到文件中,最后将一个完整的文件内容直接返回到客户端中。

静态页面更像是我们平时写死的 html 内容,而动态页面更像是前端领域经常提到的模板引擎。那么静态页面和动态页面之间又有何区别?

静态页面由于会将所有内容都写在 html 文件中,因此会显得比较大,并且每次更改内容时都必须生成新的文件。而动态页面刚好相反,由于数据都是动态添加的,所以会显得比较小,但是数据获取内部却需要发出请求,因此访问速度会比静态页面慢。

接着我们再来看看在服务器中,客户端访问静态页面或动态页面流程是怎样的 🤔。

  • 服务器中访问静态页面流程

    客户端访问一个网站时,先经过 DNS 解析得到相应的 IP 地址,接着 HTTP 协议或 HTTPS 协议将客户端请求传到服务端,服务端收到请求后就把网站目录下的 index.html 返回到客户端。

  • 服务器中访问动态页面流程

    相比访问静态页面,访问动态页面则是多了客户端发送请求和服务端处理数据工作。

    客户端访问一个网站时,先经过 DNS 解析得到相应的 IP 地址,接着 HTTP 协议或 HTTPS 协议将客户端请求传到服务端,服务端收到请求找到网站目录下的 index.php 文件,并把该文件传到 php 服务器中,php 服务器利用脚解析成功后再把内容返回到客户端。

    在 php 解析过程中,可能会存在访问数据库获取相应数据,并把数据动态放到内容中。

其实还有一种类型是伪静态页面,原理就是通过将动态页面的URL地址重写,改写成以html、htm等结尾的静态URL地址。实际上还是一个动态页面的 Rewrite 过程,对服务端的消耗会增大。

Nginx、Apache和Tomcat区别

相信童鞋们对于 Apache 和 Tomcat 服务器都不陌生,那么它们三者又有何区别?

  • Nginx:web 服务器。采取异步非阻塞方式,多个连接对应一个进程,在高并发情况下能处理更多的连接请求而不占太多的资源。静态页面处理能力较强,尤其是反向代理服务表现突出,常被用作负载均衡和代理服务器使用。
  • Apache:web 服务器。采取同步阻塞方式,一个连接对应一个进程,极大限制了处理多个请求性能。支持的模块众多,性能稳定,本身只支持静态解析,但可以通过扩展脚本、模块等支持动态页面。在 Rwrite 功能上比 Nginx 好很多,常用于处理动态请求。
  • Tomcat:应用服务器。用来处理 jsp 页面和运行 servlet。

简单总结一下,Nginx 处理对象就是静态页面,采取异步非阻塞方式,常作为反向代理服务。Apache 本身只支持静态页面,可通过 PHP 脚本程序支持动态 PHP 页面或 Tomcat 支持 JSP 页面,由于其支持模块众多以及 Rewrite 功能强大,因此在结合第三方模块解析动态页面层面上比 Nginx 显得尤为突出。

总之,Nginx 适合处理静态请求和反向代理,Apache 适合处理动态请求

Nginx-正向代理和反向代理

在开发中,我们常常听到正向代理以及反向代理这两个词,我们先来看看有什么区别。

  1. 正向代理

    一个位于客户端和服务端之间代理服务器,客户端向代理服务器发送一个请求并指定目标(即原始服务器 IP 地址),然后代理服务器向原始服务器转交请求并将获得的内容返回给客户端。例如我们常常使用的fanqiang。

    正向代理服务的目标是客户端(即对客户端是透明的),客户端可向代理服务器访问到客户端本身无法访问到的服务器资源。正向代理服务器对于服务端不是透明的,服务器并不知道请求方是代理服务器还是客户端

    正向代理

  2. 反向代理

    服务器使用一个代理服务器处理客户端请求,代理服务器可将请求转发到内部网络上的服务器,并将服务器的返回结果直接返回给客户端。

    反向代理服务的目标是服务器(即对服务端是透明的),对客户端请求进行内部网络的转发,进而实现负载均衡。反向代理对于客户端不是透明的,客户端并不知道服务方是代理服务器还是原始服务器

    反向代理

Nginx-基本结构

在理解了正向代理和反向代理后,我们就来看看 Nginx 到底在结构上是长啥样的。下面是 Nginx 的一个基本结构

// ...                 # 全局块
events {               # events块
  // ...
}
http {                 # http块
  server {             # server块
    location path {    # location块
      // ...
    }
    location path {
      // ...
    }
  }
  server {
    location path {
      // ...
    }
  }
}

现在就来解析一下每个字段到底是什么用处。

  • 全局块:配置 Nginx 服务器的用户(组)、允许生成的 worker process 数、进程 PID 等。
  • events块:配置影响 Nginx 服务器与用户的网络连接。
  • http块:配置代理、缓存和日志等绝大多数的功能和第三方模块的配置。
  • server块:配置虚拟主机相关内容。
  • location块:对于请求路由进行匹配并作相应处理,还用于处理数据缓存、地址重定向等逻辑。

Nginx-处理跨域

面试过程中,常常会被问及如何处理跨域,其中使用 Nginx 请求代理是其中的一种实现方案。

首先,我们先来回顾一下同源策略,即同协议、同域名、同端口情况下,才可以在浏览器中正常请求并得到相应内容。

那么当请求不符合同源策略时,Nginx 是如何处理的呢?答案就是 proxy

Nginx 处理跨域的原理是,当网站地址 a.com 向 b.com 发出请求时,先启动一个 Nginx 服务器,配置相应的 server 块名为 a.com,设置 location 对需要跨域的请求进行拦截,并将请求代理到 b.com。配置如下:

server {
  listen       80;
  server_name  a.com;
  location / {
    proxy_pass b.com;
  }
}

Nginx-过滤和Rewrite

在前端路由中,当匹配不到路由时,一般会直接重定向到 404 页面。如果现在有一个业务逻辑,就是当后端返回的状态码为502、500状态码时,我们需要重定向到首页,可以如何做呢?

一般情况下,前端的做法肯定是对 http 状态码进行获取,然后匹配到为500或502时,直接手动重定向到首页。这做法可以肯定是可以的,但未免显得稍微麻烦,我们可以使用 Nginx 对状态进行匹配配置就可以很简单滴实现上述功能。配置如何

error_page 500 502 /test.html;
  location = /test.html {
    root /root/static/html;
  }

除此之外,Nginx 还可以对路由进行重写,如匹配不到路由时直接重定向到首页,配置如何。

location / {
  rewrite  ^.*$ /index.html  redirect;
}

Nginx-gzip压缩

在优化项目上,我们都清楚对于 js 文件和 css 文件进行 gzip 压缩。在所有浏览器中,并不是所有浏览器都支持 gzip 压缩,若浏览器支持 gzip,一般情况下在请求头上默认自动带上Accept-Encoding来标识对 gzip 压缩的支持

Accept-Encoding: gzip, deflate

启动 gzip 压缩同样需要服务端支持,当客户端支持 gzip 压缩,那么服务端只需要返回 gzip 格式文件即可启用 gzip 了,默认响应头字段为Content-Encoding

Content-Encoding: gzip

那么,在 Nginx 中是如何配置来让服务端支持 gzip 压缩的?看看如下配置

gzip                        on;
    gzip_http_version       1.1;        
    gzip_comp_level         5;
    gzip_min_length         1000;
    gzip_types text/csv text/xml text/css text/plain text/javascript application/javascript application/x-javascript application/json application/xml;

可以看到的是,gzip 默认是关闭的,并且配置时需要设定 http 版本为 1.1,为什么?原因是 http 1.1 支持 TCP 持久连接,而 http 1.0 需要配置Connection: keep-alive才会是持久连接,需知道的是持久连接有助于避免每次请求都需要重新 TCP 建立连接。

Nginx-负载均衡

负载均衡原理是利用一定的分配策略将网络负载平衡地分配到网络集群的各个操作单元上,使得单个重负载任务、大量并发请求分担到多个单元上分别处理,从而减少用户的等待时间

在 Nginx 中如何实现负载均衡呢?按照 OSI 七层模型,Nginx 服务器实现的负载均衡一般认为是七层负载均衡

通过硬件实现的负载均衡效果好、效率高、性能稳定,但是缺陷就是成本够高。而通过软件实现的负载均衡则是依赖于均衡算法的选择和程序的健壮性。均衡算法主要分为两大类:静态负载均衡算法和动态负载均衡算法。

其中静态负载均衡算法主要有一般轮询算法、基于比率的加权轮询算法、基于优先级的加权轮询算法,算法较为简单并且在一般网络下都能得到比较好的效果。

而动态负载均衡有基于任务量的最少连接优先算法、基于性能的最快响应优先算法等,在较为复杂的网络环境中适应性强,效果更好。

Nginx 默认情况下采用一般轮询算法,主要使用的配置是 proxy_pass 和 upstream 指令。示例配置如下

upstream haha {
  server 192.168.1.2:80;
  server 192.168.1.3:80;
  server 192.168.1.4:80;
}
server {
  listen: 80;
  server_name: www.test.com;
  index index.html index.htm;
  location / {
    proxy_pass http://haha;
    proxy_set_header: Host Shost;
  }
}

下面就在 Nginx 基础上,对上述列举的算法进行配置

  • 一般轮询算法:将客户端请求按顺序进行轮询分配到相应的服务器中。

    upstream haha {
      server 192.168.1.2:80;
      server 192.168.1.3:80;
      server 192.168.1.4:80;
    }
  • 最少连接优先算法:将客户端请求优先分配到压力较小的服务器中,平衡每个队列的长度。

    upstream haha {
      least_conn;
      server 192.168.1.2:80;
      server 192.168.1.3:80;
      server 192.168.1.4:80;
    }
  • 最快响应优先算法:对客户端所有请求中处理时间最短的优先分配。

    upstream haha {
      fair;
      server 192.168.1.2:80;
      server 192.168.1.3:80;
      server 192.168.1.4:80;
    }
  • 客户端ip绑定:对客户端请求中来自同一个 IP 的请求只分配一台机器,有效解决动态网页存在的 session 共享问题。

    upstream haha {
      ip_hash;
      server 192.168.1.2:80;
      server 192.168.1.3:80;
      server 192.168.1.4:80;
    }

Nginx-缓存机制

Nginx 使用 Proxy Cache 和 Proxy Store 实现代理服务器的缓存机制

  1. Proxy Store 缓存机制

    该指令配置是否在本地磁盘直接对来自代理服务器的响应数据进行缓存不提供缓存过期更新、内存索引建立等功能,不占用内存空间,对静态数据效果够好

    配置如下

    proxy_store on | off | string;
  2. Proxy Cache 缓存机制

    生成专门的进程对磁盘上的缓存文件进行扫描,在内存中建立缓存索引,提高访问效率,并且还会生成专门的管理进程对磁盘上的缓存文件进行过期判定、更新等方面的管理

    Proxy Cache 缓存机制不管在性能上还是在数据管理上要远远优于 Proxy Store 缓存机制。一般配置如下。

    http: {
      // ...
      proxy_cache_path /test/proxyCache levels=1:2 max_size=2m inactive=5m loader_sleep=1m; keys_zone=MYPROXYCACHE:10m # 配置缓存数据存放路径和Proxy Cache使用的内存Cache空间
      proxy_temp_path /test/temp; # 配置响应数据的临时存放目录
      server {
        // ...
        location / {
          proxy_pass http://www.test.com; # Nginx缓存里拿不到资源,向该地址转发请求,拿到新的资源,并进行缓存
          proxy_cache MYPROXYCACHE; # 指定用于页面缓存的共享内存,对应http层设置的keys_zone
          proxy_cache_valid 200 302 1h; # 配置200状态和302状态的响应缓存1小时
        }
      }
    }

另外,附上一个静态资源服务器的配置示范。

location ~* \.(png|gif|jpg|jpeg)$ {
  root    /root/static/; # 指定路径即为Nginx本地路径
  autoindex on;
  access_log  off;
  expires     10h; # 设置过期时间为10小时          
}

常见字符串算法

对字符串数据结构常见的算法进行总结,也为了更好滴应对后面深入学习算法。

表示数值的字符串

请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。

例如:字符串‘+100’、‘5e2’都表示数值,但是12e、1a3.14、1e+4.3就不是数值。

答案

  const isNumber = str => {
    if (!str) return false;
    let hasPoint = false;
    let hasExp = false;
    for (let i = 0; i < str.length; i++) {
      const temp = str[i];
      if (temp <= 9 && temp >= 0) {
        continue;
      } else if (temp === 'e' || temp === 'E') {
        if (hasExp || i === 0 || i === str.length - 1) return false;
        hasExp = true;
        continue;
      } else if (temp === '.') {
        if (hasExp || hasPoint || i === 0 || i === str.length - 1) return false;
        hasPoint = true;
        continue;
      } else if (temp === '-' || temp === '+') {
        if (i !== 0 || str[i - 1] !== 'e' || str[i - 1] !== 'E') return false;
        continue;
      } else {
        return false;
      }
    }
    return true;
  }
  

考虑所有情况。

  • 只能出现数字、符号位(+或-)、小数点、指数位(e或E)。
  • 小数点,指数符号只能出现一次、且不能出现在开头结尾。
  • 指数位出现后,指数位后面不允许出现小数点。
  • 符号位只能出现在开头和指数位后面。

正则表达式匹配

请实现一个函数用来匹配包括.和*的表达式。

其中.表示任意一个字符,而*表示它前面的字符可以出现任意次(包括0次)。

答案

  const isMatch = (str, pattern) => {
    if (!str || !pattern) return false;
    return handleMatch(str, 0, pattern, 0);
  }
  const handleMatch = (str, sIndex, pattern, pIndex) => {
    if (sIndex === str.length && pIndex === pattern.length) return true;
    if (sIndex !== str.length && pIndex === pattern.length) return false;
    if (pIndex < pattern.length - 1 && pattern[pIndex + 1] === '*') {
      if (sIndex < str.length && (str[sIndex] === pattern[pIndex] || pattern[pIndex] === '.')) {
        return handleMatch(str, sIndex, pattern, pIndex + 2) ||
          handleMatch(str, sIndex + 1, pattern, pIndex + 2) ||
          handleMatch(str, sIndex + 1, pattern, pIndex);
      } else {
        return handleMatch(str, sIndex, pattern, pIndex + 2);
      }
    }
    if (sIndex < str.length && (str[sIndex] === pattern[pIndex] || pattern[pIndex] === '.')) {
      return handleMatch(str, sIndex + 1, pattern, pIndex + 1);
    }
    return false;
  } 
  

字符串的全排列

输入一个字符串,按字典序打印出该字符串中字符的所有排列。

例如输入字符串abc,则其全排列的所有字符串为:abc,acb,bac,bca,cab,cba。

答案

  const logPermutation = str => {
    const result = [];
    if (str) {
      const queue = str.split('');
      handlePermutation(queue, result);
    }
    return result;
  }
  const handlePermutation = (queue, result, temp = '', current = '') => {
    current += temp;
    if (queue.length === 0) {
      result.push(current);
      return;
    }
    for (let i = 0; i < queue.length; i++) {
      const temp = queue.shift();
      handlePermutation(queue, result, temp, current);
      queue.push(temp);
    }
  }
  

浅拷贝和深拷贝

拷贝在日常开发里会经常遇到,但是对于拷贝的操作,我们需要格外小心,稍不注意就会出现各类的问题。

拷贝分为浅拷贝和深拷贝,浅拷贝顾名思义就是对于复杂数据类型引用的地址是同一个地址,而深拷贝则是两个不一样的地址。

接下来我就简单总结一下常用的拷贝操作处理。

浅拷贝实现

  1. 遍历赋值实现

    function clone(obj) {
      let newObj = {}
      for(let key in obj) {
        if(Object.hasProperty.call(obj, key)) {
          newObj[key] = obj[key]
        }
      }
      return newObj
    }
  2. Object.assign实现

    var obj = {a: 1}
    var newObj = Object.assign(obj)
    console.log(obj, newObj)
    // {a: 1}, {a: 1}
  3. 扩展运算符实现

    var obj = {a: 1}
    var newObj = {...obj}
    console.log(obj, newObj)
    // {a: 1}, {a: 1}

深拷贝实现

深拷贝相对于浅拷贝来说,可以说是增加不少难度,因为涉及到的类型很多,诸如Function、RegExp、Date等等。

  1. JSON.parse(JSON.stringify(obj))实现

    乞丐版的实现方案,只能简单复制Object类型和Array类型。

    另外会局限性。

    • 无法复制Function类型、RegExp类型、Date类型等
    • 无法处理循环引用的问题
    • 会抛弃对象的constructor,所有的构造函数会指向Object
    var obj = {a: 1, b: {c: 1}}
    var newObj = JSON.parse(JSON.stringify(obj))
    conosle.log(newObj) // {a: 1, b: {c: 1}}
    console.oog(newObj.b === obj.b) // false
  2. Array的concat方法和slice方法

    只能处理数组内部原始数据类型的复制,对于内部元素为引用数据类型时,那么就会简单复制同一个引用地址。

    var arr = [1, {a: 1}]
    var newArr = arr.slice()
    console.log(arr === newArr) // false
    console.log(arr[1] === newArr[1]) // true
  3. 递归加遍历赋值实现

    在上述浅复制中遍历赋值实现里,再加上一个递归调用,便可实现一个基础版的深拷贝。

    function isObject(obj) {
      const type = typeof obj
      return (type !== null) && (type === 'object' || type === 'function') 
    }
    function deepClone(obj) {
      if(!isObject(obj)) return obj
      let newObj = Array.isArray(obj) ? [] : {}
      for(let key in obj) {
        if(Object.hasOwnProperty.call(obj, key)) {
          newObj[key] = deepClone(obj[key])
        }
      }
      return newObj
    }

    上述基础版中,只能简单地实现Object类型和Array类型的深拷贝。接下来我们还需要相应地分析其他情况。

    • 循环引用处理。

      处理循环引用问题,关键点在于,额外创建一个新的存储空间,保存当前新建的对象,一旦在递归过程中发现该对象在存储空间中存在,直接拿出来赋值即可,如果不存在时则直接赋值进去

      采用 WeakMap 开辟存储空间,使用弱引用方式。

      function isObject(obj) {
        const type = typeof obj
        return (obj !== null) && (type === 'object' || type === 'function') 
      }
      function deepClone(obj, map = new WeakMap()) {
        if(!isObject(obj)) return obj
        // 防止循环引用
        if(map.get(obj)) return map.get(obj)
        let newObj = Array.isArray(obj) ? [] : {}
        map.set(obj, newObj) // 存储新的对象在存储空间中
        for(let key in obj) {
          if(Object.hasOwnProperty.call(obj, key)) {
            newObj[key] = deepClone(obj[key], map)
          }
        }
        return newObj
      }

      需要注意的是,若不处理循环引用,那么遇到循环引用时肯定会导致栈溢出问题

    • 深拷贝Function类型、RegExp类型、Date类型等。

      所有类型里面可分为两种类型,分别为可遍历类型和不可遍历类型。

      // 可遍历类型
      const mapTag = '[object Map]';
      const setTag = '[object Set]';
      const arrayTag = '[object Array]';
      const objectTag = '[object Object]';
      
      // 不可遍历类型
      const boolTag = '[object Boolean]';
      const dateTag = '[object Date]';
      const errorTag = '[object Error]';
      const numberTag = '[object Number]';
      const RegexpTag = '[object RegExp]';
      const stringTag = '[object String]';
      const symbolTag = '[object Symbol]';

      接下来,就根据这两种类型进行深拷贝处理。

      • 可遍历类型处理。

        通过可遍历类型的constructor,可以初始化赋值类型。

        function getType(obj) { // 获取数据的类型
          return Object.prototype.toString.call(obj)
        }
        function initData(obj) { // 针对可遍历类型,使用其constructor属性获取构造函数初始化数据
          let ObjFun = obj.constructor
          return new ObjFun()
        }
        const iteratorType = [mapTag, setTag, arrayTag, objectTag] // 先缓存可遍历类型
        function deepClone(obj, map = new WeakMap()) {
          if(!isObject(obj)) { // 针对原始数据类型,直接返回
            return obj
          }
          // 初始化深拷贝对象
          let newObj
          let type = getType(obj)
          if(iteratorType.includes(type)) {
            newObj = initData(obj)
          }
          // 循环引用处理
          if(map.get(obj)) {
            return map.get(obj)
          }
          map.set(obj, newObj)
          // 处理Set类型
          if(type === setTag) {
          	obj.forEach(value => {
              newObj.add(deepClone(value, map))
            })
            return newObj
          }
          // 处理Map类型
          if(type === mapTag) {
          	obj.forEach((value, key) => {
              newObj.set(key, deepClone(value, map))
            })
            return newObj
          }
          // 处理Array、Object类型
          for(let key in obj) {
            if(Object.hasOwnProperty.call(obj, key)) {
              newObj[key] = deepClone(obj[key])
            }
          }
          return newObj
        }
      • 不可遍历类型处理。

        在不可遍历类型里面,其中Boolean、Number、String、Error、Date类型都是可以通过constructor获取构造函数来将值作为参数进行深拷贝。

        function deepCloneOtherType(obj) {
          let ObjFun = obj.constructor
          switch(type) {
            case boolTag:
            case numberTag:
            case stringTag:
            case errorTag:
            case dateTag:
              return new ObjFun(obj)
            case regexpTag:
              return hansleRegExp(obj)
            case symbolTag: 
              return handleSymbol(obj)
            default: 
              return null
          }
        }

        针对正则表达式,需要使用RegExp构造函数,传递相应的source以及exec解析的额外参数值。

        function handleRegExp(obj) {
          const reFlags = /\w*$/
          const result = new obj.constructor(obj.source, reFlags.exec(obj))
          result.lastIndex = obj.lastIndex
          return result
        }

        针对Symbol类型,可通过将Symbol转化为字符串,然后再使用Object()方法转换为相应的Symbol值。

        function handleSymbol(obj) {
          return Object(Symbol.prototype.valueOf.call(obj))
        }
        

        当然,还有一种类型Function类型,对于Function类型我觉得拷贝意义不大,毕竟拷贝后还会创建相应的调用栈,导致性能出现瓶颈。但是要是想拷贝Function类型,如何处理?

        Function类型会有两种情况,分别是普通函数和箭头函数,可使用prototype区分,箭头函数是没有prototype的

        普通函数使用new Function(...参数, 函数体)拷贝,箭头函数使用eval(function.toString())拷贝

        function handleFunction(obj) {
          const paramsReg = /(?<={)(.|\n)+(?=})/m  // 获取普通函数参数
          const bodyReg = /(?<=\().+(?=\)\s+{)/ // 获取普通函数函数体
          const objStr = obj.toString()
          if(obj.prototype) {
            const params = paramsReg.exec(objStr)
            const body = bodyReg.exec(objStr)
            if(body.length) {
              if(params.length) {
                const paramsArr = params[0].split(',')
                return new Function(...paramsArr, body[0]) 
              } else {
                return new Function(body[0])
              }
            } else {
              return null
            }
          } else {
            return eval(objStr)
          }
        }

Review-Question-Git

  1. 讲讲git flow?
  2. git merge 和 get rebase 区别?
  3. git push -u 是什么意思?
  4. git stash是什么?
  5. git reflog 作用是什么?

讲讲git flow?

gitflow 含义:当项目开发者人数多,并行开发功能也多,免不了各种合并的问题和冲突,而 gitflow 就是一个基于 git 的标准工作流,主要是解决在发过程中各种冲突导致开发混乱问题

gitflow 主要包含两种分支,分别是主分支和辅助分支。

主分支

  • master 分支:只存线上的代码,只有确定可以上线时才合并到 master 上,并打 tag。
  • develop 分支:开发时所用到分支。

辅助分支

  • feature 分支:用于开发新功能的分支,必须从 develop 分支代码进行拉取,分支名基本是feature/xxxx(功能相关名字)
  • release 分支:用于辅助版本发布的分支(产品验收)。当 develop 分支已经有了这次上线的所有代码并且通过测试时,可从 develop 分支合并到 release 分支上。
  • hotfix 分支:用于修正生产代码中缺陷的分支。当线上出现紧急 bug 并修复好后,需要从 master 分支衍生出一个 hotfix 分支出来进行单独发布,以免影响到其他提交。

git merge 和 get rebase 区别?

git merge

  • 只处理一次冲突。
  • 保留要合并的提交历史记录,合并后的所有 commit 会按照提交时间从旧到新排列。
  • 查找问题难度大,难维护,引出分支,合并分支,提交历史信息显得杂乱。

git-merge

git rebase

  • 需改变当前分支从 master 上拉出分支的位置。(即 feature 分支)
  • 没有多余的合并历史记录,且合并后的 commit 顺序不一定按提交时间排列。
  • 可能会多次解决同一个地方的冲突(多个提交在处理完冲突后,需使用 squash 来合并)。
  • 维护简单,因为所有提交都会在一条线上。

git-rebase

​ squash 后

git-rebase-squash


git push -u 是什么意思?

绑定默认提交的远程版本库,加了参数 -u 后,以后即可直接用git push就可以代替git push origin master


git stash是什么?

git stash 用于保存当前工作区和暂存区的修改。

常用场景:当开发新功能到一半时,需要临时去改一个紧急的 bug,这时候就不能直接提交已经做好的代码,而是应该使用 git stash 暂时保存已修改的代码,再切换分支去修改 bug 即可。当回到原来修改的位置时,直接使用 git stash apply 即可。


git reflog 作用是什么?

git reflog 可以查看所有分支的所有操作记录,包括 commit 和 reset 的操作,以及被删除的 commit 记录。

git log 则不能查看已经删除的 commit 记录。

其中 HEAD 代表的是当前活跃的分支。

你对同构了解多少

在“刀耕火种”年代,前端工程师只需要给静态页面添加一些样式或动画,基本不会涉及到任何数据逻辑,后端工程师拿到前端编写好的静态页面,就开始匹配页面中需要动态获取的数据(即当用户请求页面时,后端进行处理并返回完整的静态页面),一般都是依赖模板引擎来完成这些任务。因此才会有 JSP、PHP 等的出现。

上面所提及的过程也就是所谓的服务端渲染过程(即 SSR ),前端并不需要理会后端返回的内容正不正确,只需要理会 UI 即可,而这也是早期前端职位一直不被看好的缘故。

随着 React、Vue 等前端框架的发展,浏览器渲染过程(即 CSR )也慢慢崭露头角,但最终带来的却是白屏、首屏加速、SEO 等问题。正因如此,才有了今天要说的同构的解决方案。先甭急,下面还有很多要讲的呢🤔。

目录

  1. SSR vs CSR
  2. 预渲染究竟是啥?
  3. 同构的含义以及作用
  4. 构建同构应用
  5. 同构的原理

SSR vs CSR

上面提及了 SSR 和 CSR,对于陌生的童鞋可能会有疑惑,究竟它们长的是啥样子?我们就从代码层面简单理一下。

SSR,顾名思义就是数据初始化都是交给后端管理,前端只需要渲染页面 UI 即可。

const express = require('express')
const app = express()
app.get('/', (res, req) => {
  res.send(`
  <!DOCTYPE html>
  <html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>SSR</title>
  </head>
  <body>
    <div id="app">
    	<span>Hello, SSR...</span>
    </div>
    <script src="./dist/bundle.js"></script>
  </body>
  </html>
  `)
})

代码中使用了 Express 举了一个栗子,可以看到模板中的所有标签都是直接通过后端返回,包括数据处理。前端拿到这坨字符串后,直接渲染即可。好明显,对于 SSR,由于数据在后端处理上会显得更高的效率,因此在前端渲染时就不会出现诸如白屏、首屏加载慢等问题,而且还有助于 SEO,不利于前后端分离,后端负担过重

再来看看 CSR,后端只返回简单的标签,前端除了负责 UI 展示外还需要处理数据初始化。下面就以 React 举个例子。

// 后端返回的html模板
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>CSR</title>
  </head>
  <body>
    <div id="app"></div>
    <script src="./dist/react.js"></script>
  </body>
</html>

// 前端UI展示以及数据初始化
import React from 'react'
import ReactDom from 'react-dom'
function App() {
  return (
  	<span>Hello, CSR...</span>
  )
}
ReactDom.render(<App />, document.querySelector('#app'))

CSR 对于使用过 React、Vue 等框架的童鞋们应该很好理解,无非就是在指定 DOM 节点下面做一些诸如子节点渲染、路由配置以及数据处理的工作。对于 CSR,常用于操作 SPA(即单页面应用),有利于前后端分离,前端只管理自身业务逻辑以及 UI 展示,后端只需要管理 API 制定,但是却存在白屏、首屏加速以及 SEO 等问题

综上,SSR 有利于用户体验以及市场开拓,而 CSR 则有利于技术人员管理,而这两者刚好就是互补的。

也许,有的童鞋会提出直接将 SSR 和 CSR 相结合不就稳妥了?

答案是正确,由于 SSR 和 CSR 是互补的,所以相结合后刚好就可以很好滴处理上述种种问题。那么问题又来了,要将 SSR 和 CSR 相结合,要怎么做?

我猜你也应该知道答案,那就是今天的主角——同构。

所谓同构,就是写一份代码可同时在浏览器和服务器中运行应用

预渲染究竟是啥?

在讲解同构之前,我先岔开一个话题,那就是简单讲一讲预渲染。

相信部分童鞋对于这个预渲染显得有点陌生,不是都已经有 SSR 和 CSR 了吗?那这个预渲染又是一个什么鬼。。😂

所谓预渲染,其实就是无需服务器实时动态编译,在构建时针对特定路由简单滴生成静态 HTML 文件

咋一看,跟服务端渲染很类似,都是最终输出静态 HTML 文件。但是细心一看,预渲染和服务端渲染还是有很大区别,我列举一下。

  1. 预渲染是在构建时针对路由输出静态 HTML 并且无需任何无服务器,服务端渲染则是通过 Node 作为中间层输出 HTML。
  2. 预渲染的文件输出是不依赖于数据的,而服务端渲染的输出文件却是依赖于数据的。

借用尤大大的说法,如果你调研服务器端渲染(SSR)只是用来改善少数营销页面(例如 /, /about, /contact 等)的 SEO,那么你可能需要预渲染

很简单,预渲染的处理场景就是用于改善少数静态页面的 SEO 的,并且无数据依赖的。

同构的含义以及作用

看完上面的对比描述,相信你应该对同构有一定的了解,但并不妨碍我重复提一下。

同构就是前后端共用一套代码或逻辑,而在这套代码或逻辑中,理想的情况是在浏览器进一步渲染的过程中,判断已有的 DOM 结构和即将渲染出的结构是否相同,若相同则不需要重新渲染 DOM 结构,只进行事件绑定即可。

那么,同构有何作用?

  1. 更好的性能。主要体现在首屏加速、文件更少。
  2. SEO 优化支持。服务端接收到请求后,会返回一个相对完整并包含初始内容的 HTML 文档,所以更有利于搜索引擎爬虫获取数据。
  3. 可维护性强。利用同一套代码管理,避免客户端和服务端同时维护两套代码或逻辑。
  4. 对低端机型更友好。由于初步渲染都在服务端完成,低端机型不需要兼容直接渲染内容即可。
  5. 用户体验更好。

讲了那么多好处,也该说说坏处吧?

  1. 服务端无法完全复用浏览器端代码。
  2. Node 层不稳定,对首屏加载有一定影响。
  3. 服务端逻辑处理增多,增加了复杂性。

总的来说,对于首屏加载速度有追求的技术人员来说,同构还是一个最好的选择。

构建同构应用

在优秀框架 React、Vue 中,其实已经拥有很好的构建同构应用的框架,分别是 Next.js、Nuxt.js。

既然如此,那为啥还要讲解构建同构应用呢?

其实讲这个,目的在于理解同构在构建时的原理,方便大家能对同构有一个更好的认识。话不多说,直接走起!

下面我会以 React 作为举例,从 0 开始构建一个 React 的简单同构应用。

我们先使用 NPM 构建一个本地项目,如下操作。

mkdir react-ssr
cd react-ssr
npm init -y
npm i -D webpack webpack-cli

安装基本配置后,我们就可以正式编写同构应用。

先创建一个公共组件文件App.js,用于展示 UI。

// App.js
import React, { Component } from 'react'
export default function App() {
  return (
  	<span>Hello, React SSR...</span>
  )
}

接着开始实现服务端,由于上面提及过,会使用 Node 作为中间层,所以接下来我会使用 Express。

在 React 中,提供了一个renderToString的 API 用于将组件渲染成对应静态模板。有些童鞋在这一步也许会直接将刚刚创建的 App 组件渲染成相应的静态模板,我是不推荐的,我更推荐的是惰性渲染,也就是等到匹配相应的路由时才去渲染。

接着创建一个服务端渲染静态模板中间惰性渲染文件main-server.js

// main-server.js
import React from 'react'
import { renderToString } from 'react-dom/server'
import App from './App'

export function render() {
  return renderToString(<App />)
}

然后就是创建真正的处理服务端渲染逻辑的后端文件http-server.js

// http-server.js
const express = require('express')
const { render } = require('./dist/bundle-server.js')
const app = express()

app.get('/', (req, res) => {
  res.send(`
  <!DOCTYPE html>
  <html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>React SSR...</title>
  </head>
  <body>
    <div id="app">${ render() }</div>
    <script src="./dist/bundle.js"></script>
  </body>
  </html>
  `)
})
app.use(express.static('.'))
app.listen(3000)

可以看到,当匹配到根路径时,才会在静态模板中执行方法renderToString渲染组件App

至此,服务端编写完毕,是不是觉得妥妥的?接着我们编写客户端代码,React 提供一个hydrate方法,用于匹配服务端渲染的 DOM 节点的,即判断 DOM 节点是否为服务端渲染生成,若是则不再重新渲染该 DOM 节点,直接执行后续操作,有效地避免重复渲染导致的性能损耗。

创建客户端渲染组件文件main-browser.js,并挂载到相应的 DOM 节点中。

// main-browser.js
import React from 'react'
import { render, hydrate } from 'react-dom'
import App from './App'

hydrate(<App />, document.getElementById('app'))

客户端编写完毕,真是万事俱备,只欠东风啊。。就只剩跑起来了。

接下来我们需要将客户端和服务端分别构建,为什么?很简单,因为服务端代码和客户端不能打包到一个地方的,因为两个采取模式不一致。所以我们需要分别为客户端和服务端创建构建文件。

等等,先别急,我们还需要装点东西,那就是 babel、preset-react 和 webpack-node-externals。

  • babel:将编写的 ES6 代码编译成 ES5 代码。
  • preset-react:识别 React 中的 JSX,并将其转译成 React.createElement。
  • webpack-node-externals:能够排除 node_modules 目录中所有模块,用于服务端构建,因为 Node.js 会默认去node_modules目录下寻找和使用第三方模块。

安装上述依赖包命令如下:

npm i -D babel-loader @babel/core @babel/preset-env @babel/preset-react webpack-node-externals

接着就是需要创建.babelrc文件配置 babel 规则啦。

// .babelrc
{
  "presets": ["@babel/preset-env", "@babel/preset-react"]
}

基本配置都弄好,就可以正式配置 webpack。分别创建webpack.config.client.jswebpack.config.server.js文件。

// webpack.config.client.js
const path = require('path')
const nodeExternals = require('webpack-node-externals')

module.exports = {
  mode: 'development',
  entry: './main-browser.js',
  output: {
    filename: "bundle-browser.js",
    path: path.resolve(__dirname, 'dist')
  },
  module: {
    rules: [
      { test: /\.js$/, exclude: /node_modules/, loader: "babel-loader" }
    ]
  }
}

// webpack.config.server.js
const path = require('path')
const nodeExternals = require('webpack-node-externals')

module.exports = {
  mode: 'development',
  entry: './main-server.js',
  target: 'node',
  externals: [nodeExternals()],
  output: {
    libraryTarget: 'commonjs2',
    filename: 'bundle-server.js',
    path: path.resolve(__dirname, './dist')
  },
  module: {
    rules: [
      { test: /\.js$/, exclude: /node_modules/, loader: "babel-loader" }
    ]
  }
}

在服务端构建中,必须在输出中指定libraryTarget: 'commonjs2',首先 Node 中遵循的是 commonjs 规范,以 Commonjs2 规范导出渲染函数,可供 Node 编写 HTTP 服务器代码调用。

最后一步,就是需要在package.json文件中直接编写 Shell 命令啦。在这里,要推荐的是**concurrently**,它的用处就是可以执行多条指令,而不需要在多个命令行中执行多个指令。我们先安装:

npm i -S concurrently

接着就可以在package.json中的scripts属性编写命令。

"scripts": {
  "start": "concurrently \"node ./http-server.js\" \"webpack --watch --config webpack.server.config.js\" \"webpack --config webpack.client.config.js\""
},

最后的最后,就可以执行npm start执行项目,在localhost:3000中看到效果啦。😄

另外,我还推荐大家可以参考一下Egg + React + SSR 服务端渲染,有助于更好滴认识如何构建同构应用。

同构的原理

相信看完上述的简单配置,你们对同构应该会有一个大概的了解 🤔。那么我应该说一下同构的原理。

  1. 客户端发出请求获取index.html页面,Node server 接收到客户端请求后,匹配相应路由。

  2. 当匹配到相应路由后,就需要请求所需要的数据,并以 props 或 context 或 store 形式传入组件,使用renderToStringrenderToNodeStream的 API 将组件渲染为 html 字符串或 stream 流。

  3. 由于客户端拿到 html 字符串或 stream 流后,还需要同步数据,因此服务端还需要将初始化数据注入到 html 字符串或 stream 流中,在客户端中获取到初始数据,最后使用hydrate方法同步服务端渲染的 DOM 节点,从而避免重新渲染相同的节点。

  4. 最后执行组件的componentDidMount生命周期,完成组件内事件绑定和一些交互,浏览器重用了服务端输出的 DOM 节点,整个流程结束。

ES Summary(每个版本)

本文针对Javascript中每个版本的一些知识总结,主要为了方便知识梳理,避免遇到盲点。🤔

(每一年官方出的新版本ES,都会同时进行更新的哈哈)

目录

  1. ES6
  2. ES7
  3. ES8
  4. ES9
  5. ES10
  6. ES11

ES6

ES6中,可以说是整个Javascript改版中最大的一版,下面就来看看主要包含哪些内容。

块级作用域绑定(var、let、const)

  1. 在 Javascript 中不存在块级作用域,只存在全局作用域函数作用域

  2. 使用 var 声明的变量不管在哪声明,都会变量提升到当前作用域的最顶部。看个🌰:

    function test() {
      console.log(a) // 不会报错,直接输出undefined
      if(false) {
        var a = 1
      }
    }
    test()
    
    // 相当于
    function test() {
      var a
      console.log(a)
      if(false) {
        a = 1
      }
    }
    test()

    另外,定义和声明是两码事,如果变量都还没定义就使用时,就会报错(xxx is not defined)。

  3. let 和 const 都能够声明块级作用域,用法和 var 是类似的,let 和 const 都是不会变量提升。看个🌰:

    function test() {
      if(false) {
        console.log(a) // 报错,a is not defined(也是传说中的临时性死区)
        let a = 1
      }
    }
    test()

    let 和 const 必须先声明再访问

    所谓临时性死区,就是在当前作用域的块内,在声明变量前的区域(临时性死区只有 let 和 const 才有)。看个🌰:

    if(false) {
      // 该区域便是临时性死区
      let a = 1
    }
  4. const 声明变量时必须同时赋值,并且不可更改。

  5. 在全局作用域中使用 var 声明的变量会存储在 window 对象中。而使用 let 和 const 声明的变量则只会覆盖 window 对象中同名的属性,而不会替换。看个🌰:

    window.a = 1
    let a = 2
    
    console.log(a) // 2
    console.log(window.a) // 1

    let 和 const 声明的变量会存在一个单独的Script块作用域中(即[[Scopes]]作用域中能找到)。

    // 接着上面列举的栗子
    function aa() {}
    console.dir(aa)
    // 你会发现在输出内容中[[Scopes]],会存在两个作用域,一个是Script,一个是Global
    [[Scopes]]: Scopes[2]
    0: Script {a: 2}
    1: Global {parent: Window, opener: null, top: Window, length: 1, frames: Window, }

字符串和正则表达式

  1. 支持 UTF-16(含义是任何一个字符都适用两个字节表示),其中方法如下:

    • codePointAt:返回参数字符串中给定位置对应的码位,返回值为整数。
    • fromCodePoint:根据指定的码位生成一个字符。
    • normalize:提供Unicode的标准形式,传入一个可选字符串参数,指明应用某种Unicode标准形式。
  2. 字符串中新增的方法有:

    • includes(str, index):检测字符串指定的可选索引中是否存在参数文本。

    • startsWith(str, index):检测字符串头部是否有指定的文本。

    • endsWith(str, index):检测字符串尾部是否有指定的文本。

    • repeat(number):接受一个整数,重复对应字符串整数次。

      console.log('aa'.repeat(2)) // aaaa
  3. 当给正则表达式添加 u 字符时,表示从编码单元操作模式切换为字符模式。

  4. 模板字符串支持多行文本、模板中动态插入变量、模板子面量方法使用。

    • 多行文本。

      `1
      
      2`
    • 模板中动态插入变量。

      let a = 1
      console.log(`${a} haha`) // 1 haha
    • 模板子面量方法。

      function aa(a, ...b) {
        console.log(a, b)
      }
      let a = 1
      aa`hehe ${a} haha`
      // 输出为:
      // ["hehe ", " haha", raw: ["hehe ", " haha"]] [1]

      其中参数 a 表示模板字符串中静态字符。参数 b 表示模板动态变量。

函数

  1. 支持默认参数,默认参数不仅可以为字符串、数字、数组或对象,还可以是一个函数。看个🌰:

    function sum(a = 1, b = 2) {
      return a + b
    }
    sum()

    参数默认值不能被 arguments 识别,看个🌰:

    function sum(a = 1, b = 2) {
      console.log(arguments)
    }
    sum() // {}
    sum(3, 6) // {"0": 3, "1": 6}

    默认参数同样存在临时性死区,看个🌰:

    // 在初始化a时,由于b还没被声明,因此无法直接将b赋值给a
    function aa(a = b, b) {
      return a + b
    }
  2. 支持展开运算符(...),其作用是解构数组和对象。看个🌰:

    // 展开数组或对象
    var a1 = [1, 2]
    var a2 = {a: 1}
    console.log(...a1) // 1 2
    console.log({...a2}) // {a: 1}
    
    // rest参数
    function aa(...obj) {
      console.log(obj)
    }
    aa(a1) // 1 2
    aa(a1, a2) // [[1, 2], {a: 1}]
  3. 支持箭头函数。箭头函数和普通函数的区别有:

    • 箭头函数木有 this,this 指向的是定义该箭头函数所在的对象。
    • 箭头函数没有 super
    • 箭头函数没有 arguments
    • 箭头函数内部不存在 new.target 绑定(构造函数存在)。并且箭头函数中不能使用 new 关键字。
    • 箭头函数不存在原型
  4. 支持尾调用优化。(调用一个函数时都会生成一个函数调用栈,尾调用就可以很好滴避免生成不必要的尾调用阮一峰的尾调用优化讲解

    只有满足以下三个条件时,引擎才会帮我们做好尾调用优化。

    • 函数不是闭包。
    • 尾调用是函数最后一条语句。
    • 尾调用结果作为函数返回。

    看个🌰:

    // 符合尾调用优化情况
    function aa() {
      return bb()
    }
    
    // 无return不优化
    function aa() {
      bb()
    }
    // 不是直接返回函数不优化
    function aa() {
      return 1 + bb()
    }
    // 最后一条语句不是函数不优化
    function aa() {
      const cc = bb()
      return cc
    }
    // 闭包不优化
    function aa() {
      function bb() {
        return 1
      }
      return bb()
    }

    需要知道的是,递归都是很影响性能的,但是有了尾调用后,递归函数的性能将得到有效的提升

    // 斐波那契数列
    // 常做方案
    function fibonacci(n) {
      if(n === 0 || n === 1) return 1;
      return fibonacci(n-1) + fibonacci(n-2);
    }
    
    // 尾递归优化,其中pre是第一项的值,next作为第二项的值
    function fibonacci(n, pre, next) {
      if (n <= 1) return next
      return fibonacci(n - 1, next, pre + next)
    }

对象的扩展

  1. 对象方法和属性支持简写,以及对象属性支持可计算。看个🌰:

    const id = 1
    const obj = {
      id,
      [`test${id}`]: 1,
      printId() {
        return this[`test${id}`]
      }
    }
  2. Object 新增了方法如下:

    • Object.is。判断两个值是否相等。

      console.log(Object.is(NaN, NaN)) // true
      console.log(Object.is(+0, -0)) // false
      console.log(Object.is(5, "5")) //false
    • Object.assign。浅拷贝一个对象,相当于一个 Mixin 功能。

      const obj = {a: 1, b: 2}
      const newObj = Object.assign({...obj, c: 3})
      console.log(newObj) // {a: 1, b: 2, c: 3}
  3. 对象支持同名属性,不过后面的属性会覆盖前面同名的属性。看个🌰:

    const obj = {
      a: 1,
      a: 2
    }
    console.log(obj) // {a: 2}
  4. 遍历对象时,默认都是数字属性按顺序提前,接着就是首字母排序。看个🌰:

    const obj = {
      name: 'haha',
      1: 2,
      a: 1,
      0: 'hehe'
    }
    for(var key in obj) {
      console.log(key)
    }
    // 输出顺序为
    // 0
    // 1
    // name
    // a
  5. 支持实例化后的对象改变原型对象,使用方法Object.setPrototypeOf()

    var parent1 = {a: 1}
    var child = Object.create(parent1)
    console.log(child.a) // 1
    var parent2 = {b: 2}
    child = Object.setPrototypeOf(child, parent2)
    console.log(child.a, child.b) // undefined, 2

解构

  1. 解构的定义是,从对象中提取出更小元素的过程,即对解构出来的元素进行重新赋值

  2. 对象解构必须是同名的属性。看个🌰:

    var obj = {
      a: 1,
      b: 2
    }
    var a = 3, b = 3;
    ({a, b} = obj)
    console.log(a, b) // 1, 2
  3. 数组解构可以有效地处理交换两个变量的值。

    var a = 1, b = 2;
    [a, b] = [b, a]
    console.log(a, b) // 2, 1

    数组解构还可以按需取值,看个🌰:

    var arr = [[1, 2], 3]
    var [[,b]] = arr
    console.log(b) // 2
  4. 混合解构就是对象和数组解构结合,看个🌰:

    var obj = {
      a: 1,
      b: [1, 2, 3]
    };
    ({a, b: [...arr]} = obj)
    console.log(a, arr) // 1, [1, 2, 3]
  5. 解构参数就是直接从参数中获取相应的参数值。看个🌰:

    function aa({a = 1, b = 2} = obj) {
      console.log(a, b)
    }
    aa({a: 3, b: 6}) // 3, 6

Symbol

  1. Symbol 是一种特殊的、不可变的数据类型,可作为对象属性的标识符使用,也是一种原始数据类型。

  2. Symbol 的语法格式为:

    Symbol([desc]) // desc是一个可选参数,用于描述Symbol所用

    创建一个 Symbol,如下:

    const a = Symbol()
    const b = Symbol('1')
    console.log(a, b) // Symbol(), Symbol('1')

    创建 Symbol 不能使用 new

  3. Symbol 最大的用处在于创建对象一个唯一可计算的属性名。看个🌰:

    const obj = {
      [Symbol('name')]: 12,
      [Symbol('name')]: 13
    }
    console.log(obj) // {Symbol(name): 12, Symbol(name): 13}

    有效地避免命名冲突问题。

  4. Symbol 不支持强制转换为其他类型。

  5. 在 ES6 中提出一个 @@iterator方法,所有支持迭代的对象(如数组、Set、Map)都要实现。其中**@@iterator方法的属性键为Symbol.iterator而非字符串**。只要对象定义有Symbol.iterator属性就可以用for...of进行迭代

    // 判断对象是否实现Symbol.iterator属性,就可以判断是否可以使用for...of进行迭代
    if(Symbol.iterator in obj) {
      for(var n of obj) console.log(n)
    }
  6. Symbol 支持全局共享机制,使用 Symbol.for 进行注册,使用 Symbol.keyFor 进行获取。看个🌰:

    // Symbol.for中参数可为任意类型
    let a = Symbol.for(12)
    console.log(Symbol.keyFor(a), typeof Symbol.keyFor(a)) // '12', string

    Symbol.keyFor 最终获取到的是一个字符串值。

  7. Symbol 可作为类的私有属性,使用 Object.keys 或 Object.getOwnPropertyNames 方法都无法获取 Symbol 的属性名只能使用 for...of 或 Object.getOwnPropertySymbols 方法获取。看个🌰:

    var obj = {
      [Symbol('name')]: 'Andraw-lin',
      a: 1
    }
    console.log(Object.keys(obj)) // ["a"]
    console.log(Object.getOwnPropertySymbols(obj)) // [Symbol(name)]

Set和Map

  1. Set 常用于检查对象中是否存在某个键值,Map 则常用于获取已存的信息。

  2. Set 是有序列表,含有相互独立的非重复值。支持的属性和方法如下:

    • size。返回 Set 对象中元素个数。

    • add(value)。在 Set 对象尾部添加一个元素。

    • entries()。返回 Set 对象中[值,值]形式,看个🌰:

      var a = new Set([1, 2, 3])
      for(var [b, c] of a.entries()) {
        console.log(b, c)
      }
      // 输出结果为:
      // 1 1
      // 2 2
      // 3 3
    • forEach(callback)。用于遍历 Set 集合。

    • has(value)。判断 Set 集合中是否存在有指定的值。

  3. Set 集合的特点是没有下标,没有 Key。Set 和 Array可以相互转换,如下:

    // 数组转成Set
    const arr = [1, 2, 3]
    console.log(new Set(arr))
    
    // Set转成数组
    const se = new Set([1, 2, 3])
    console.log([...se])
  4. Set 集合是一个强引用,只要 new Set 实例化的引用存在,就不会释放内存。若定义一个 DOM 元素的 Set 集合,然后在某个 js 中引用了该实例,当页面跳转时,并不会立马释放内存,因为引用还在。WeakSet 就是专门用于释放强引用的

    WeakSet 和 Set 区别:

    • WeakSet 对象中只能存放对象类型,不能存放原始数据类型。而 Set 对象则可以。
    • WeakSet 对象中存储的对象值是弱引用的,若无其他变量或属性引用该对象值,则这个对象值会被当成垃圾回收掉。而 Set 对象则是存储强引用。
    • WeakSet 对象中存储的值是无法被枚举。而 Set 对象则可以枚举。
  5. Map 是存储键值对的有序列表,key 和 value 支持所有数据类型。对比 Set 集合,Map 集合多了 set 方法和 get 方法。看个🌰:

    var m = new Map()
    m.set('name', 'haha')
    m.set('year', '1999')
    console.log(m.get('name'), m.get('year')) // haha, 1999

    支持对象作为 key 值,看个🌰:

    const key = {}
    m.set(key, 'hehe')
    m.get(key) // 'hehe'
  6. 和 WeakSet 一样,也会有 WeakMap 存在,专门针对弱引用。看个🌰:

    var map = new WeakMap()
    var key = document.querySelector('.header')
    map.set(key, 'DOM')
    map.get(key) // 'DOM'
    key.parentNode.removeChild(key)
    key = null

迭代器(Iterator)和生成器(Generator)

  1. 迭代器是一种特殊对象,每一个迭代器对象都有一个 next(),该方法返回一个对象,包括了 value 和 done 属性。使用 ES5 模拟实现迭代器如下:

    function createIterator(items) {
      var i = 0
      return {
        next() {
          var done = (i >= items.length)
          var value = i < items.length ? items[i++] : undefined
          return { done, value }
        }
      }
    }
    var arr = createIterator([1, 2])
    console.log(arr.next()) // { done: false, value: 1 }
    console.log(arr.next()) // { done: false, value: 2 }
    console.log(arr.next()) // { done: true, value: undefined }
  2. 生成器就是一个函数,用于返回迭代器的。使用 * 号声明的函数即为生成器函数,同时需要使用 yield 控制进程。看个🌰:

    function *createIterator() {
      console.log(1)
      yield 1
      console.log(2)
      yield 2
      console.log(3)
    }
    var a = createIterator() // 执行后并不会输出任何东西
    console.log(a.next()) // 先输出1,再输出{ value: 1, done: false }
    console.log(a.next()) // 先输出2,再输出{ value: 2, done: false }
    console.log(a.next()) // 先输出3,再输出{ value: undefined, done: true }

    总结一下,迭代器执行 next() 方法时,只会执行前面到 yield 间的代码,后面代码都会被终止

    同样地,在 for 循环中使用迭代器,遇到 yield 时都会终止进程。看个🌰:

    function *createIterator(items) {
      for(let i = 0; i < items.length;  i++) {
        yield items[i]
      }
    }
    const a = createIterator([1, 2, 3]);
    console.log(a.next()); //{value: 1, done: false}
  3. yield 只能在生成器函数内使用。

  4. 生成器函数还可以使用匿名函数形式创建,看个🌰:

    const createIterator = function *() {
      // ...
    }
  5. 凡是通过生成器得到的迭代器,都是可迭代的对象(即可迭代对象具有 Symbol.iterator 属性),可使用 for...of 进行迭代。看个🌰:

    function *createIterator() {
      yield 1
      yield 2
    }
    var obj = createIterator()
    for(var val of obj) {
      console.log(val)
    }
    // 输出为
    // 1
    // 2

    可迭代对象可访问 Symbol.iterator 直接得到迭代器,看下面

    function *createIterator() {
      yield 1
      yield 2
    }
    var obj = createIterator()
    var newObj = obj[Symbol.iterator]() // 其实obj[Symbol.iterator]相当于createIterator迭代器

    Symbol.iterator 可用于检测一个对象是否可迭代

    typeof obj[Symbol.iterator] === "function"
  6. 默认情况下定义的对象是不可迭代的,但是可以通过 Symbol.iterator 创建迭代器。看个🌰:

    const obj = {
      items: [],
      *[Symbol.iterator]() {
        for (let item of this.items) {
          yield item;
        }
      }
    }
  7. 数组、Set、Map等可迭代对象,其内部已实现迭代器,并且提供3种迭代器调用,分别是:

    • entries():返回一个迭代器,用于返回键值对。

      var arr = [1, 2, 3]
      for(var [key, value] of arr.entries()) {
        console.log(key, value)
      }
      // 输出结果为
      // 0 1
      // 1 2
      // 2 3
    • values():返回一个迭代器,用于返回键值对的value。

      var arr = [1, 2, 3]
      for(var value of arr.values()) {
        console.log(value)
      }
      // 输出结果为
      // 1
      // 2
      // 3
    • keys():返回一个迭代器,用于返回键值对的key。

      var arr = [1, 2, 3]
      for(var key of arr.keys()) {
        console.log(key)
      }
      // 输出结果为
      // 0
      // 1
      // 2
  8. 高级迭代器功能,主要包括传参、抛出异常、生成器返回语句、委托生成器。

    • 传参。next 方法传参数时,会作为上一轮 yield 的返回值,除了第一轮 yield 外,看个🌰:

      function *aa() {
        var a1 = yield 1
        var a2 = 10
        yield a1 + 10
      }
      var a = aa()
      console.log(a.next(2)) // { value: 1, done: false }
      console.log(a.next(100)) // { value: 110, done: false }
    • 抛出异常。

      function *aa() {
        var a1 = yield 1
        var a2 = 10
        yield a1 + 10
      }
      var a = aa()
      console.log(a.next(2)) // { value: 1, done: false }
      console.log(a.throw(new Error('error'))) // error
      console.log(a.next(100)) // 不再执行
    • 生成器种遇到 return 语句时,表示退出操作。

      function *aa() {
        var a1 = yield 1
        return
        yield a1 + 10
      }
      var a = aa()
      console.log(a.next(2)) // { value: 1, done: false }
      console.log(a.next(100)) // { value: undefined, done: true }
    • 委托生成器。其实就是生成器嵌套生成器。

      function *aIterator() {
        yield 1;
      }
      function *bIterator() {
        yield 2;
      }
      function *cIterator() {
        yield *aIterator()
        yield *bIterator()
      }
      var i = cIterator()
      console.log(i.next()) // {value: 1, done: false}
      console.log(i.next()) // {value: 2, done: false}
  9. 异步任务执行器,其实就是用来循环执行生成器。

    因为我们知道生成器需要执行 N 次 next() 方法才能运行完,异步任务执行器就是帮我们做这些事情的。

    function run(taskFn) {
      var task = taskFn() // 调用生成器
      var result = task.next()
      function step() {
        if(!result.done) {
          result = task.next(result.value)
          step()
        }
      }
      step()
    }
    run(function *() {
      let text = yield fetch() // 异步请求获取数据
      doSomething(text) // 处理返回结果
    })

    异步任务执行器,其实就是间接实现了 async 和 await 功能。

类class

  1. 在 ES6 中,将原型的实现写在类中,和 ES5 本质上是一致的,都是需要新建一个类名,然后实现构造函数再实现原型方法。看个🌰:

    class Person {
      constructor(name) { // 新建构造函数
        this.name = name // 私有属性
      }
      sayName() { // 定义一个方法并且赋值到构造函数的原型中
        return this.name
      }
    }

    私有属性的定义,只需要在构造方法中定义this.xx = xx即可

  2. 类声明和函数声明的区别,主要有:

    • 类声明不能提升,而函数声明则会被提升。
    • 类声明中代码会自动强行运行在严格模式下。
    • 类中的所有方法都是不可枚举的,而函数声明的对象中方法则是可以枚举的。
    • 类中的构造函数只能使用 new 来调用,而函数则可以普通调用或 new 来调用。
  3. 类的定义有声明式定义和表达式定义,看个🌰:

    // 声明式定义
    class Person {
      // ...
    }
    
    // 表达式定义
    let person = Class {
      // ...
    }

    类还支持立即调用。

    let person = new Class {
      constructor(name) {
        this.name = name
      }
      sayName() {
        return this.name
      }
    }('Andraw')
    console.log(person.sayName()) // Andraw
  4. 类支持在原型上定义访问器属性。看个🌰

    class Person {
      constructor(name) {
        this.name = name
      }
      get myName() {
        return this.name
      }
      set myName(name) {
        this.name = name
      }
    }
    var descriptor = Object.getOwnPropertyDescriptor(Person.prototype, 'myName')
    console.log('get' in descriptor) // true
    console.log(descriptor.enumerable) // false 表示不可枚举

    类中定义的属性或方法名都是可支持表达式的

    const test = 'sayName'
    class Person{
      constructor(name) {
        this.name = name
      }
      [test]() {
        return this.name
      }
    }

    类中定义的方法同样可以是生成器方法。

    class Person {
      *sayName() {
        yield 1
        yield 2
      }
    }
  5. 静态属性或静态方法,是在属性或方法前面使用 static 关键字。static 修饰的方法或属性只能被类本省直接访问,而不能在实例中访问

    class Person {
      static sayName() {
        return this.name
      }
    }
  6. 在 React 中写一个组件Test时,必须得继承React.Component。其中 Test 组件就是一个派生类。派生类中的构造函数内部必须使用 super()

    关于 super 的使用需要注意:

    • 只可以在派生类中使用 super。派生类是指继承自其他类的新类。
    • 派生类中构造函数访问 this 之前必须要先调用 super(),负责初始化 this
    • 如果不想调用 super,可让类的构造函数返回一个对象。
  7. 当派生类继承于父类时,其父类中的静态成员也会被继承到派生类中,但是静态成员同样只能是被派生类访问,而无法被其实例访问

  8. 在构造函数中可使用 new target(new target 通常表示当前的构造函数名)来阻止实例化类。看个🌰:

    class Person {
      constructor() {
        if(new.target === Person) { // 不允许该类被调用实例化
          throw new Error("error")
        }
      }
    }

数组

  1. 在 ES5 中创建数组的方式有两种,分别是数组子面量(即 var arr = [])和 Array 实例(即 new Array())。

    在 ES6 中新增两种方法创建数组,分别是 Array.of() 和 Array.from()。

    • Array.of()。我们知道 new Array() 中传入一个数字时,表示的是生成多少长度的数组,Array.of() 就是为了处理这种尴尬场面的,看个🌰:

      const a1 = new Array(1)
      const a2 = Array.of(1)
      console.log(a1, a2) // [undefined], [1]
    • Array.from()。用于将类数组转换为数组。

      function aa() {
        const arr = Array.from(arguments)
        console.log(arr)
      }
      aa(1, 2)
      // [1, 2]
      
      // 可传第二个参数,作为第一个参数的转换
      const arr = Array.from(arguments, value => value + 2)
      
      // 可传第三个参数,用来指定this
      
      // Array.from可用于处理数组去重
      Array.from(new Set(...arguments))
  2. 数组新增的方法有。

    • find()。传入一个回调函数,找到数组中符合当前搜索规则的第一个元素,返回它,并且终止搜索。

      var arr = [1, 2, 3]
      console.log(arr.find(n => typeof n === "number")) // 1
    • findIndex()。传入一个回调函数,找到数组中符合当前搜索规则的第一个元素,返回它的下标,终止搜索。

      var arr = [1, 2, 3]
      console.log(arr.find(n => typeof n === "number")) // 0
    • fill()。用新元素替换掉数组内的元素,可以指定替换下标范围。格式和🌰如下:

      // 格式如下
      arr.fill(value, start, end)
      
      // 栗子
      const arr = [1, 2, 3]
      console.log(arr.fill(4)) // [4, 4, 4] 不指定开始和结束,全部替换
      
      const arr1 = [1, 2, 3]
      console.log(arr1.fill(4, 1)) // [1, 4, 4] 指定开始位置,从开始位置全部替换
      
      const arr2 = [1, 2, 3]
      console.log(arr2.fill(4, 0, 2)) // [4, 4, 3] 指定开始和结束位置,替换当前范围的元素
    • copyWithin()。选择数组的某个下标,从该位置开始复制数组元素,默认从0开始复制。格式和🌰如下:

      // 格式如下
      arr.copyWithin(target, start, end)
      
      // 栗子
      const arr = [1, 2, 3, 4, 5]
      console.log(arr.copyWithin(3)) // [1,2,3,1,2] 从下标为3的元素开始,复制数组,所以4, 5被替换成1, 2
      
      const arr1 = [1, 2, 3, 4, 5]
      console.log(arr1.copyWithin(3, 1)) // [1,2,3,2,3] 从下标为3的元素开始,复制数组,指定复制的第一个元素下标为1,所以4, 5被替换成2, 3
      
      const arr2 = [1, 2, 3, 4, 5]
      console.log(arr2.copyWithin(3, 1, 2)) // [1,2,3,2,5] 从下标为3的元素开始,复制数组,指定复制的第一个元素下标为1,结束位置为2,所以4被替换成2

Promise与异步编程

  1. 对 DOM 做事件处理操作,如点击、激活焦点、失去焦点等,再比如使用 Ajax 请求数据时利用回调函数获取返回值,都属于异步编程。

  2. Promise 中文意思就是承诺,Javascript 对你许一个承诺,会在未来某个时刻兑现承诺

    Promise 有生命周期,分别是进行中(pending)、已经完成(fulfilled)、拒绝(rejected)

    Promise 不会直接返回异步函数的执行结果,需要使用 then 方法获取,获取异常回调时,需要使用 catch 方法获取。

    结合 axios 看个🌰,axios 是前端比较热门的 http 请求插件之一。

    // 1. 创建axios实例
    import axios from 'axios'
    export const instance = axios.create()
    
    // 2. 使用axios实例 + Promise获取返回值
    const promise = instance.get('url')
    promise.then(res => console.log(res)).catch(err => console.log(err))
  3. Promise 构造函数只有一个参数,该参数为一个函数,被作为执行器。执行器有两个参数,分别是 resolve() 和 reject() ,前一个表示成功回调,后一个表示失败回调。

    new Promise((resolve, reject) => {
      setTimeout(() => resolve(5), 0)
    }).then(res => console.log(5)) // 5

    Promise 实例只能过 resolve 或者 reject 函数返回数据,并且使用 then 或者 catch 进行获取

    Promise.resolve(1).then(res => console.log(res)) // 1
    Promise.reject(2).catch(res => console.log(res)) // 2
    // 捕获错误时,可使用catch获取
    new Promise((resolve, reject) => {
      if(true) {
        throw new Error('error')
      }
    }).catch(err => console.log(err))
  4. 浏览器和 Node 提供了 unhandledRejection 和 rejectionHandled 两个事件处理 Promise 中没有设置 catch 问题

    // unhandledRejection
    let rejected
    rejected = Promise.reject("It was my wrong!")
    
    process.on("unhandledRjection", function(reason, promise) {
      console.log(reason.message) // It was my wrong!
      console.log(rejected === promise) // true
    })
    
    // rejectionHandled
    let rejected
    rejected = Promise.reject("It was my wrong!")
    
    process.on("rejectionHandled", function(reason, promise) {
      console.log(reason.message) // It was my wrong!
      console.log(rejected === promise) // true
    })

    浏览器中使用上面两个方法只是在 window 对象上监听,而 Node 中使用是在 process 对象上监听。

    unhandledRejection 和 rejectionHandled的区别就是,前者是事件循环中触发,后者则是事件循环后触发。两者都是处理 Promise 中使用 reject 捕获错误时,而没有使用 catch 进行捕获处理

  5. Promise 支持链式调用,有效地解决了回调地狱问题。看个🌰:

    new Promise((resolve, reject) => {
      resolve(1)
    }).then(res => { return res + 1 }).then(res => {console.log(res)}) // 2
  6. 除了 resolve 和 reject 方法外,还有两个方法,便是 Promise.all 和 Promise.race。

    • Promise.all。运行多个 Promise,当全部 Promise 都返回结果时,才会使用 then 进行处理

      Promise.all([
        new Promise(function(resolve, reject) {
          resolve(1)
        }),
        new Promise(function(resolve, reject) {
          resolve(2)
        }),
        new Promise(function(resolve, reject) {
          resolve(3)
        })
      ]).then(arr => {
        console.log(arr) // [1, 2, 3]
      })
    • Promise.race。和 all 方法类似,不过就是当有一个返回结果时,就可以使用 then 进行处理

      Promise.race([
        new Promise(function(resolve, reject) {
          setTimeout(() => resolve(1), 1000)
        }),
        new Promise(function(resolve, reject) {
          setTimeout(() => resolve(2), 10)
        }),
        new Promise(function(resolve, reject) {
          setTimeout(() => resolve(3), 100)
        })
      ]).then(value => {
        console.log(value) // 2
      })
  7. Promise 本身不是异步,只有它的 then 方法或者 catch 方法才是异步。

    目前 ES7 已经支持 async 方案,该方案比 Promise 还强大啊。

    async function a() {
      await function() {}
    }

代理(Proxy)和反射(Reflect)

  1. 代理 Proxy 就是拦截 JS 引擎内部目标的底层对象操作,反射 Reflect 就是针对 Proxy 还原原对象操作方法。

    代理陷阱 覆写的特性 默认特性
    get 读取一个属性值 Reflect.get()
    set 写入一个属性 Reflect.set()
    has in操作符 Reflect.has()
    deleteProperty delete操作符 Reflect.delete()
    getProperty Object.getPropertypeOf() Reflect.getPrototypeOf()
    setProperty Object.setPrototypeOf() Reflect.setPrototypeOf()
    isExtensible Object.isExtensible() Reflect.isExtensible()
    preventExtensions Object.preventExtensions() Reflect.preventExtensions()
    getOwnPropertyDescriptor Object.getOwnPropertyDescriptor() Reflect.getOwnPropertyDescriptor()
    defineProperty Object.defineProperty() Reflect.defineProperty()
    ownKeys Object.ownKeys()、Object.getOwnPropertyNames()、Object.getOwnPropertySymbols() Reflect.ownKeys()
    apply 调用一个函数 Reflect.apply()
    construct 用new调用一个函数 Reflect.construct()

    反射 Reflect 一般和代理 Proxy 结合使用,设置相应的代理方法处理数据时,需同时使用反射 Reflect 对原对象的方法操作一遍

    现在就拿 set 做个🌰。set属性是在要改变Proxy属性值时,进行的预处理,共接收四个参数:

    • target:要进行预处理的目标对象;
    • key:预处理过程的Key值,相当于对象的属性;
    • value:要设置成的值;
    • receiver:改变前的原始值;
    let pro = new Proxy({
      aa: 2
    }, {
      set: (target, key, value, receiver) => console.log(target, key, value, receiver);
    });
    pro.aa = 8;         // {aa: 2} "aa" 8 Proxy {aa: 2}
  2. Proxy的存在能够使函数加上钩子函数,即可理解为在执行方法前预处理一些代码,举个栗子:

    let pro = new Proxy({}, {
      get: (target, key, property) => console.log('haha')
    });
    pro.aa;                 // haha
    pro.bb;                 // haha
  3. 在使用如 http-proxy 插件或 webpack 时,有时需要访问某个 api,通过配置 proxy 跳转到指定的 url 能解决跨域问题。但是该种模式和代理 Proxy 有异曲同工之处,但是机制是不一样的。

    devServer: {
      proxy: [
        {
          context: "/api/*", //代理API
          target: 'https://www.hyy.com', //目标URL
          secure: false
        }
      ]
    }

使用模块封装代码

  1. 模块可以是函数、数据、类,需要指定导出的模块名,才能被其他模块访问。看个🌰

    // 数据模块
    const obj = { a: 1 }
    // 函数模块
    const sum = (a, b) => a + b
    // 类模块
    class Person {
      // ...
    }
  2. 模块引入使用 import 关键字,导入模块方式有两种。

    • 导入指定的模块。

      import { sum } from 'a.js'
      sum(1, 2)
    • 导入全部模块。

      import allFn from 'a.js'
      allFn.sum(1, 2)
  3. 模块导出使用 export 关键字,看个🌰:

    // 导出数据模块
    export const obj = { a: 1 }
    // 导出函数模块
    export const sum = (a, b) => a + b
    // 导出类模块
    export class Person {
      // ...
    }

    需要注意的是,ES6 提供了模块的默认导出,在导出时结合 default 关键字,看个🌰:

    // a.js
    function sum(a, b) {
      return a + b
    }
    export default sum
    
    // b.js
    import sum from 'a.js'
    sum(1, 0)
  4. 不能在语句和函数内使用 export 关键字,只能在模块顶部使用

  5. ES6 提供了两种方式修改模块的导入和导出名,分别是导出时修改和导入时修改,使用 as 关键字

    • 导出时修改。

      // a.js
      function sum(a, b) {
        return a + b
      }
      export default {sum as add}
      
      // b.js
      import add from 'a.js'
      add(1, 2)
    • 导入时修改。

      // a.js
      function sum(a, b) {
        return a + b
      }
      export default sum
      
      // b.js
      import sum as add from 'a.js'
      add(1, 2)
  6. 无绑定导入,是指当模块没有可导出模块时,全都是定义的全局变量,可使用无绑定导入。看个🌰:

    // a.js
    let a = 1
    const PI = 3.1314
    
    // b.js
    import 'a.js'
    console.log(a, PI) // 1, 3.1314
  7. 使用 webpack 打包 js 后,浏览器加载模块时,总是按顺序加载,先加载模块1,再加载模块2,因为 module 类型默认使用 defer 属性

    <script type="module" src="module1.js"></script>
    <script type="module" src="module2.js"></script>

ES7

ES7 在 ES6 基础上仅仅新增了**求幂运算符(**)**和 Array.prototype.includes() 方法。

需要注意的是,在 ES6 中仅仅提供了字符串 includes 实现,而在 ES7 中则在数组中进行完善

求幂运算符(**)

** 运算符相当于 Math 对象中的 pow 求幂方法,使用如下。

2 ** 3 = 8
// 相当于
Math.pow(2, 3) // 8

** 运算符和 +- 运算符用法一致,看个🌰:

let num = 2
num **= 3
// 相当于 num = num ** 3
console.log(num) // 8

Array.prototype.includes()

数组中实现的 includes 方法,用于判断一个数组是否包含一个指定的值,如果包含就返回 true,否则返回 false

includes 和 indexOf 都是使用 === 来进行比较,但是在 includes 中,NaN === NaN 返回的是 true,而 indexOf 则是返回 false

另外,includes 和 indexOf 方法都是认为,+0 === -0

ES8

ES8 也是在 ES6 基础上继续进行拓展。

Object.values()和Object.entries()

在 ES6 中提及过,只有可迭代对象可以直接访问 keys、entries、values 三个方法在 ES8 中在 Object 对象上实现了 values 和 entries 方法,因为 Object 已经支持了 kes 方法,直接看🌰:

var obj = {
  a: 1,
  b: 2
}
console.log(Object.keys(obj)) // ["a", "b"]
console.log(Object.values(obj)) // [1, 2]
console.log(Object.entries(obj)) // [["a", 1], ["b", 2]]

其中,entries 方法还能结合 Map 数据结构。

var obj = {
  a: 1,
  b: 2
}
var map = new Map(Object.entries(obj))
console.log(map.get('a')) // 1
// Map { "a" => 1, "b" => 2 }

字符串追加

  1. 字符串新增方法 String.prototype.padStart 和 String.prototype.padEnd,用于向字符串中追加新的字符串。看个🌰:

    '5'.padStart(2) // ' 5'
    '5'.padStart(2, 'haha') // 'h5'
    '5'.padEnd(2) // '5 '
    '5'.padEnd(2, 'haha') // '5h'

    padStart 和 padEnd 对于格式化输出很有用。

  2. 使用 padStart 方法举个例子,有一个不同长度的数组,往前面追加 0 来使得长度都为 10。

    const formatted = [0, 1, 12, 123, 1234, 12345].map(num => num.toString().padStart(10, '0'))
    console.log(formatted)
    // ["0000000000", "0000000001", "0000000012", "0000000123", "0000001234", "0000012345"]

    使用 padEnd 也是同样的道理。

Object.getOwnPropertyDescriptors

Object.getOwnPropertyDescriptors 直接返回一个对象所有的属性,甚至包括 get/set 函数。

ES2017 引入该函数主要目的在于方便将一个对象浅拷贝给另一个对象,同时也可以将 getter/setter 函数也进行拷贝。意义上和 Object.assign 是不一样的。

直接看个🌰:

var obj = {
  a: 1,
  b: {
    a: 2
  },
  set c(temp) {
    this.d = temp
  },
  get c() {
    return this.d
  }
}
var newObj1 = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj))
console.log(newObj1)
// {
//  c: undefined
//  a: 1
//  b: {a: 2}
//  get c: ƒ c()
//  set c: ƒ c(temp)
//  __proto__: Object
// }
var newObj2 = Object.assign({}, obj)
console.log(newObj2)
// {
//  a: 1
//  b: {a: 2}
//  c: undefined
//  __proto__: Object
// }

在克隆对象方面,Object.assign 只能拷贝源对象中可枚举的自身属性,同时拷贝时无法拷贝属性的特性(如 getter/setter)。而使用 Object.getOwnPropertyDescriptors 方法则可以直接将源对象的所有自身属性(是自身属性啊,不是所有可访问属性!)弄出来,再拿去复制

上面的栗子中就是配合原型,将一个对象中可访问属性都拿出来进行复制,弥补了 Object.getOwnPropertyDescriptors 方法短处(即无法获取可访问原型中的属性)。

若只是浅复制自身属性,还可以结合 Object.defineProperties 来实现

var obj = {
  a: 1,
  b: {
    a: 2
  },
  set c(temp) {
    this.d = temp
  },
  get c() {
    return this.d
  }
}
var newObj = Object.defineProperties({}, Object.getOwnPropertyDescriptors(obj))
conso.e.log(newObj)
// {
//  c: undefined
//  a: 1
//  b: {a: 2}
//  get c: ƒ c()
//  set c: ƒ c(temp)
//  __proto__: Object
// }

允许在函数参数最后添加逗号

听说是为了方便 git 算法更加方便区分代码职责。直接看个🌰。

const sum = (a, b,) => a + b

Async/Await

在 ES8 所有更新中,最有用的一个!!!

async 关键字告诉 Javascript 编译器对于标定的函数要区别对待。当编译器遇到 await 函数时会暂停,它会等到 await 标定的函数返回的 promise,该 promise 要么 resolve 得到结果、要么 reject 处理异常。

直接上一个栗子,对比一下使用 promise 和使用 async 区别。

// 模拟获取userName接口
var getUser= userId
 => new Promise(resolve => {
   setTimeout(() => {
     resolve(userName)
   }, 2000)
 })
// 模拟获取userAge接口
var getUserAge = userName
 => new Promise(resolve => {
   setTimeout(() => {
     if(userName === 'Andraw') {
       resolve('24')
     } else {
       reject('unknown user')
     }
   }, 2000)
 })
// ES6的promise实现方式
function es6Fn(userId) {
  getUser(userId)
    .then(getUserAge)
    .then(age => {
      console.log(age)  
    })
}
// ES8的async实现方式
async function es8Fn(userId) {
  var userName = await getUser(userId)
  var userAge = await getUserAge(userName)
  console.log(userAge)
}

使用 ES8 的 async 异步编程更符合日常开发流程,而 ES6 的 promise 也是一个很好的使用, ES8 的 async 只是在 promise 基础上更上一层楼。

  1. async 函数返回 promise。

    若想获取一个 async 函数的返回结果,则需要使用 promise 的 then 方法

    接着拿上述 ES8 的 async 实现方式来举个例子。

    async function es8Fn(userId) {
      var userName = await getUser(userId)
      var userAge = await getUserAge(userName)
      return userAge
    }
    // 获取es8Fn async函数返回结果
    es8Fn(1).then(userAge => { console.log(userAge) })
  2. 并行处理

    我们知道,每次调用 es8Fn 函数时,都需要等到至少 4 秒时间,若调用 N 次,则需要等到 4N 秒。使用 Promise.all 来并行处理,可以极大释放时间限制。

    async function newES8Fn() {
      var [a, b] = await Promise.all([es8Fn, es8Fn])
      return [a, b]
    }

    上述并行处理后,就可以很好滴避免多次调用而时间耗费的问题。

  3. 错误处理

    对于 async/await 的错误处理,有三种方法可以处理,分别是在函数中使用 try-catch、catch 每一个 await 表达式、catch 整个 async-await 函数。

    • 在函数中使用 try-catch

      async function es8Fn(userId) {
        try {
        	var userName = await getUser(userId)
          var userAge = await getUserAge(userName)
          return userAge 
        } catch(e) {
          console.log(e)
        }
      }
    • catch 每一个 await 表达式

      由于每一个 await 表达式都返回 Promise,对每一个表达式都进行 catch 处理。

      async function es8Fn(userId) {
        var userName = await getUser(userId).catch(e => { console.log(e) })
        var userAge = await getUserAge(userName).catch(e => { console.log(e) })
        return userAge
      }
    • catch 整个 async-await 函数

      async function es8Fn(userId) {
        var userName = await getUser(userId)
        var userAge = await getUserAge(userName)
        return userAge
      }
      es8Fn(1).then(userAge => { console.log(userAge) }).catch(e => { console.log(e) })

ES9

ES9(即ES2018) 主要新增了对象的扩展运算符 Rest 以及 Spread、异步迭代器、Promise支持 finally 方法、正则的扩展。

对象的扩展运算符 Rest 以及 Spread

如果使用过 Object.assign 方法合并对象,应该就很清楚。在 ES6 中,在数组中支持了 Rest 解构赋值和 spread 语法。

// ES6中的Rest
var [a, ...b] = [1, 2, 3, 4, 5, 6]
console.log(a, b) // 1, [2, 3, 4, 5, 6]

// ES6中的spread
function sum(a, ...b) {
  console.log(a, b)
}
sum(1, 2, 3)
// 输出为:1, [2, 3]

ES8 则在对象中支持了 Rest 解构赋值和 Spread 语法

// rest解构赋值
var {x, ...y} = {x: 1, a: 2, b: 3}
console.log(x, y) // 1, { a: 2, b: 3 }

// spread语法,接着上面解构的值
var c = {x, ...y}
console.log(c) // {x: 1, a: 2, b: 3}

异步迭代器和异步生成器

在 ES6 中,如果一个对象具有 Symbol.iterator 方法,那该对象就是可迭代的。目前,只有 Set、Map、数组内部实现 Symbol.iterator 方法,因此都是属于可迭代对象。

var set = new Set([1, 2, 3])
var setFn = set[Symbol.iterator]()
console.log(setFn) // SetIterator {1, 2, 3}
console.log(setFn.next()) // {value: 1, done: false}
console.log(setFn.next()) // {value: 2, done: false}
console.log(setFn.next()) // {value: 3, done: false}
console.log(setFn.next()) // {value: undefined, done: true}

默认的对象是不支持可迭代的,若实现了 Symbol.iterator 方法,那么它也是可迭代的。那么对象的 Symbol.iterator 方法如何实现的呢?

var obj = {
  a: 1,
  b: 2,
  [Symbol.iterator]() {
    var allKeys = Object.keys(this)
    var i = 0
    return {
      next: () => {
      	return {
          value: this[allKeys[i++]],
          done: i > allKeys.length
        }
      }
    }
  }
}
var objFn = obj[Symbol.iterator]()
console.log(objFn) // {next: ƒ}
console.log(objFn.next()) // {value: 1, done: false}
console.log(objFn.next()) // {value: 2, done: false}
console.log(objFn.next()) // {value: undefined, done: true}

上面的实现,还可以再完善一丢。利用生成器

var obj = {
  a: 1,
  b: 2,
  [Symbol.iterator]: function *() {
    for(let key in this) {
      yield this[key]
    }
  }
}
var objFn = obj[Symbol.iterator]()
console.log(objFn) // Generator {_invoke: ƒ}
console.log(objFn.next()) // {value: 1, done: false}
console.log(objFn.next()) // {value: 2, done: false}
console.log(objFn.next()) // {value: undefined, done: true}

由上面可以知道,同步迭代器就是一个特殊对象,里面包含有 value 和 done 两个属性(即 {value, done})。那么异步迭代器又是什么?

异步迭代器,和同步迭代器不同,不返回 {value, done} 形式的普通对象,而是直接返回一个 {value, done} 的 promise 对象

其中,同步迭代器使用 Symbol.iterator 实现,异步迭代器使用 Symbol.asyncIterator 实现

var obj = {
  a: 1,
	b: 2,
  [Symbol.asyncIterator]() {
    var allKeys = Object.keys(this)
    var i = 0
    return {
      next: () => {
        return Promise.resolve({
          value: this[allKeys[i++]],
          done: i > allKeys.length
        })
      }
    }
  }
}
var objAsyncFn = obj[Symbol.asyncIterator]()
console.log(objAsyncFn) // {next: ƒ}
console.log(objAsyncFn.next().then(res => {
  console.log(res) // {value: 1, done: false}
}))
console.log(objAsyncFn.next().then(res => {
  console.log(res) // {value: 2, done: false}
}))
console.log(objAsyncFn.next().then(res => {
  console.log(res) // {value: undefined, done: true}
}))

那么既然有了异步迭代器,就肯定有异步生成器,专门用来生成异步迭代器的

var obj = {
  a: 1,
	b: 2,
  [Symbol.asyncIterator]: async function *() {
    for(let key in this) {
      yield this[key]
    }
  }
}
var objAsyncFn = obj[Symbol.asyncIterator]()
console.log(objAsyncFn) // obj {<suspended>}
console.log(objAsyncFn.next().then(res => {
  console.log(res) // {value: 1, done: false}
}))
console.log(objAsyncFn.next().then(res => {
  console.log(res) // {value: 2, done: false}
}))
console.log(objAsyncFn.next().then(res => {
  console.log(res) // {value: undefined, done: true}
}))

另外,异步迭代器和同步迭代器有一样东西很类似,就是使用 next() 后,是无法知道什么时候才会到最后一个值,在同步迭代器中,需要使用 for...of 进行遍历才能有效地处理迭代器中每一项值

在异步迭代器中,同样支持遍历,不过是 for...await...of 遍历

var obj = {
  a: 1,
	b: 2,
  [Symbol.asyncIterator]: async function *() {
    for(let key in this) {
      yield this[key]
    }
  }
}
(async function() {
  for await (var value of obj) {
    console.log(value)
  }
})()

for...await...of 只会在异步生成器中或异步函数中有效

Promise支持 finally 方法

Promise 成功获取数据时使用 then 方法,处理异常时使用 catch 方法。但是在某些情况下,我们不管成功还是存在异常,都希望 Promise 能够运行一些共同的代码,finally 就是处理这些事情的

Promise.resolve(1).then(res => { console.log(res) }).finally(() => { console.lig('common code...') })
// 输出结果为
// 1
// common code...

正则的扩展

在正则表达式中,点 . 可以表示任意单个字符。

/foo.bar/.test('foo\nbar') // false

上面代码中,为了能够匹配任意字符,ES9 提出了 s 修饰符,使得 . 可以匹配任意单个字符。

/foo.bar/s.test('foo\nbar') // true

还有几个暂不讨论。可自行了解哈

ES10

ES10(即 ES2019) 新增功能相对比较少,都是一些性能的优化。

Array.prototype.flat和Array.prototype.flatMap

在日常开发中,我们常遇到一个问题,那就是将[1, [1, 2], [1, [2, 3]]]扁平化为[1, 1, 2, 1, 2, 3]

以往的经历告诉我们,需要使用第三方库 lodash 来处理,导致了不必要的麻烦,为此,ES10 直接为数组提供了 flat 方法来实现扁平化数组

var arr = [1, [1, 2], [1, [2, 3]]]
console.log(arr.flat(2)) // [1, 1, 2, 1, 2, 3]

flat 方法中参数,表示的是扁平化的层数

另外的方法 flatMap,其实就是数组的 flat 方法和 map 方法结合。

[1, 2, 3].map(x => [x * x]) // [[1], [4], [9]]
[1, 2, 3].flatMap(x => [x * x]) // [1, 4, 9]

Obejct.fromEntries

Object.fromEntries 方法和 ES6 中的 Object.entries 功能刚好相反,Object.entries 是获取一个对象的键值对,而 Object.fromEntries 则是将键值对转化为对象

Object.fromEntries([["a", 1], ["b", 2]]) // {a: 1, b: 2}
Object.entries({a: 1, b: 2}) // [["a", 1], ["b", 2]]

字符串去除首尾空格

ES10 为字符串提供了 trimStart 和 trimEnd 方法,用于去除首尾空格。

'  123'.trimStart() // 123
'123  '.trimEnd() // 123

Symbol.prototype.description

定义 Symbol 类型时,可传入一个字符串作为标志,若想获得该字符串,ES6 并没有提供方法,而 ES10 则提供了 description 属性用于获取 Symbol 的描述信息

var symbol = Symbol('haha')
console.log(symbol.description) // haha

可选的catch参数

在 ES10 之前,使用 try...catch 块时,若不给 catch 函数传递参数,会报错。

ES10 则直接将 catch 参数作为可选。

// ES10前
try {
  // ...
} catch(e) {
  console.log(e)
} 
// ES10后
try {
  // ...
} catch {
  // ...
}

Array.prototype.sort方法由快排转换为Timsort

在 ES10 前,数组的 sort 方法默认采取的是快排,但会存在不稳定性,为此,直接转为使用 Timsort。可自行了解一下。

Timsort 就是将插入排序和归并排序进行合并起来得到的好算法

函数支持toString方法

ES10 支持函数直接以字符串的形式打印出来。

var sum = (a, b) => a + b
console.log(sum.toString()) // (a, b) => a + b

ES11

对于ES11中的变化,大致可以分为以下几部分:

  • 支持私有变量;
  • Promise.allSettled;
  • BigInt全新数据类型;
  • Nullish Coalescing Operator 空位合并运算符;
  • Optional Chaining Operator 可选链运算符;
  • Dynamic Import 动态导入;
  • globalThis 新增全局对象;

私有变量

在类中支持定义私有变量,并且私有变量只有在类中进行访问,而无法在外部使用。

class Person {
  #name = 'Tom';
  constructor(name) {
    this.#name = name;
  }
	getName() {
    return this.name;
  }
	setName(name) {
    this.#name = name;
  }
}

私有变量一律使用#进行修饰,也就只能通过类内部方法暴露出来对私有变量进行间接访问

Promise.allSettled

首先,Promise下有两个比较典型的方法,分别是Promise.allPromise.race

  • Promise.all

    将多个Promise实例包装成一个新的Promise实例,需要注意的是,返回时一旦遇到某个Promise实例失败,那么都将只会返回失败的状态值。只有所有Promise实例都成功后才会返回一个成功的结果数组。

    const p1 = new Promise((res, rej) => { res('p1') });
    const p2 = new Promise((res, rej) => { res('p2') });
    const p3 = Promise.reject('p3');
    
    Promise.all([p1, p2]).then(res => {
      console.log(res);
    }).catch(err => {
      console.log(err);
    });
    // 最终输出['p1', 'p2']
    
    Promise.all([p1, p3, p2]).then(res => {
      console.log(res);
    }).catch(err => {
      console.log(err);
    });
    // 最终输出'p3'
  • Promise.race

    Promise.all类似的是,一旦遇到某个Promise实例失败,那么都会返回失败的状态值。不同的是,如果所有Promise实例都是成功的,那么会返回最先成功的Promise实例

    const p1 = Promise.reject('p1');
    const p2 = Promise.resolve('p2');
    Promise.race([p1, p2]).then(res => {
      console.log(res);
    }).catch(err => {
      console.log(err);
    });
    // 返回'p1'

而对于新的 API,Promise.allSettled则是用于返回所有的Promise实例结果,不管其中的Promise是否失败

Promise.allSettled([Promise.resolve('p1'), Promise.reject('p2')]).then(res => {
  console.log(res);
});
// [{ 'status': 'fulfilled', 'reason': 'p1' },
// { 'status': 'rejected', 'reason': 'p2' }
// ]

BigInt 类型

JS中,Number类型的数据范围是在-(2^53 - 1) ~ (2^53 - 1)之间。那么任意一个数字超出该范围时,都会失去精度,举个例子:

const a = Number.MAX_SAFE_INTEGER + 1;
const b = Number.MAX_SAFE_INTEGER + 2;

console.log(Number.MAX_SAFE_INTEGER); // 9007199254740991
console.log(a) //9007199254740992
console.log(b) //9007199254740992
console.log(a === b); // true

可以看到,JS提供的Number.MAX_SAFE_INTEGERNumber.MIN_SAFE_INTEGER都是作为安全系数,一旦超出该范围将得到意想不到的结果(即丢失精度)。

为了解决这些情况,JS直接提供了一个BigInt类型。

创建BigInt类型的有两种,分别是:

  • 在数字后面加上n
  • 直接使用BigInt创建实例。
const a = 88n;
const b = BigInt('88');
console.log(a); // 88n
console.log(b); // 88n
console.log(a === b); // true

BigInt类型除了不支持+运算外,其他的运算均支持。而且,除法运算会自动向下取舍到最近的整数

额外需要注意的是,当需要将BigInt类型强制转换为布尔类型时,那么只有0n才会被视为false,其他都会被视为true

Nullish Coalescing Operator 空位合并运算符

使用逻辑运算符??当前面的值仅仅为null或者undefined时,返回后面的值,当前面的值不是null或者undefined时,直接返回该值。看个🌰:

0 ?? 5; // 0
NaN ?? 'str'; // NaN
null ?? 1; // 1
undefined ?? 2; // 2

当与其他逻辑运算符使用时,只允许括号包裹好的组合使用,否则报错。看个🌰:

"测试" && undefined ?? "呵呵";  // 报错
("测试" && undefined) ?? "呵呵"; // '呵呵'
("测试" || null) ?? "呵呵"; // '测试'

Optional Chaining Operator 可选链运算符

所谓的可选链运算符,就是为了针对那些访深层对象属性时,中间某个对象突然是undefined或者null情况的

const obj = {
  a: {
    b: null
  }
}
console.log(obj.a.b.c); // 报错
console.log(obj?.a?.b?.c); // undefined

Dynamic Import 动态导入

import()终于纳入了JS 的标准中,在以往的条件导入中,一般都只能通过require()

其中,require()是运行时调用(采用同步的形式进行加载),而import是编译时调用。

标准的import导入采用的是静态导入,也是同步加载,但是会存在消耗加载时间,并且还会存在某些模块在在加载时不存在的情形。同样的,require()同步加载也会存在消耗加载时间问题。

为此,异步动态加载import()能很好滴处理这种情形。

const getUserInfo = await import('./user.js');
getUserInfo();

globalThis

JS根据运行的环境不同,会使用不同的全局对象。其中浏览器的全局对象为window对象、Node的全局对象是globalWebWorker的全局对象则是self

为了统一各个环境下的全局对象,使用globalThis来表示。

// 浏览器
globalThis === window // true
// WebWorker
globalThis === self // true
// Node
globalThis === global // true

常见栈和队列算法

对栈和队列数据结构常见的算法进行总结,也为了更好滴应对后面深入学习算法。

包含 min 函数的栈

定义栈的数据结构,请在该类型中实现一个能够得到栈中所含最小元素的 min 函数,要求时间复杂度为 O(1)。

答案

  const dataStack = [];
  const minStack = [];
  const push = data => {
    dataStack.push(data);
    if (minStack.length === 0 || data < min()) {
      minStack.push(data);
    } else {
      minStack.push(min());
    }
  }
  const pop = () => {
    minStack.pop();
    return dataStack.pop();
  }
  const min = () {
    return minStack.length && minStack[minStack.length - 1];
  }
  

滑动窗口的最大值(双端队列)

给定一个数组 nums,有一个大小为 K 的滑动窗口从数组的最左侧移动到数组的最右侧,你只可以看到在滑动窗口 K 内的数字。

滑动窗口每次只向右移动一位,返回滑动窗口的最大值。

答案

  const getWindowMin = (nums, k) => {
    const queue = [];
    const result = [];
    for (let i = 0; i < nums.length; i++) {
      if (queue.length && i - queue[0] > k - 1) {
        queue.shift();
      }
      let j = queue.length - 1;
      while (j >= 0 && nums[queue[j]] <= nums[i]) {
        queue.pop();
        j--;
      }
      queue.push(i);
      if (i >= k - 1) {
        result.push(nums[queue[0]]);
      }
    }
    return result;
  }
  

用两个栈实现队列

用两个栈来说实现一个队列,完成队列的 Push 和 Pop 操作。

队列中的元素为 int 类型。

答案

  const stack1 = [];
  const stack2 = [];
  const push = data => {
    stack1.push(data);
  }
  const pop = () => {
    if (!stack2.length) {
      while (stack1.length) {
        stack2.push(stack1.pop());
      } 
    }
    return stack2.pop() || null;
  }
  

用两个队列实现栈

用两个队列实现一个栈,完成栈的 Push 和 Pop 操作。

答案

  const queue1 = [];
  const queue2 = [];
  const push = data => {
    if (!queue1.length) {
      queue1.push(data);
      while (queue2.length) {
        queue1.push(queue2.shift());
      }
    } else if (!queue2.length) {
      queue2.push(data);
      while (queue1.length) {
        queue2.push(queue1.shift());
      }
    }
  }
  const pop = () => {
    if (queue1.length) {
      return queue1.shift();
    } else if (queue2.length) {
      return queue2.shift();
    }
  }
  

栈的压入、弹出序列

输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否可能为该栈的弹出顺序。

假设压入栈的所有数字均不相等,如1,2,3,4,5是某栈的压入顺序,序列4,5,3,2,1是该压栈序列对应的一个弹出序列,而4,3,5,1,2就不可能是该压栈序列的弹出序列。

答案

  const isStack = (stack1, stack2) => {
    if (!stack1 || !stack2 || !stack1.length || !stack2.length) return false;
    let index = 0;
    const result = [];
    for (let i = 0; i < stack1.length; i++) {
      result.push(stack1[i]);
      while (result.length && result[result.length - 1] === stack2[index]) {
        result.pop();
        index++;
      }
    }
    return result.length === 0;
  }
  

ES Summary(每个版本)

本文针对Javascript中每个版本的一些知识总结,主要为了方便知识梳理,避免遇到盲点。🤔

(每一年官方出的新版本ES,都会同时进行更新的哈哈)

目录

  1. ES6
  2. ES7
  3. ES8
  4. ES9
  5. ES10

ES6

ES6中,可以说是整个Javascript改版中最大的一版,下面就来看看主要包含哪些内容。

块级作用域绑定(var、let、const)

  1. 在 Javascript 中不存在块级作用域,只存在全局作用域函数作用域

  2. 使用 var 声明的变量不管在哪声明,都会变量提升到当前作用域的最顶部。看个🌰:

    function test() {
      console.log(a) // 不会报错,直接输出undefined
      if(false) {
        var a = 1
      }
    }
    test()
    
    // 相当于
    function test() {
      var a
      console.log(a)
      if(false) {
        a = 1
      }
    }
    test()

    另外,定义和声明是两码事,如果变量都还没定义就使用时,就会报错(xxx is not defined)。

  3. let 和 const 都能够声明块级作用域,用法和 var 是类似的,let 和 const 都是不会变量提升。看个🌰:

    function test() {
      if(false) {
        console.log(a) // 报错,a is not defined(也是传说中的临时性死区)
        let a = 1
      }
    }
    test()

    let 和 const 必须先声明再访问

    所谓临时性死区,就是在当前作用域的块内,在声明变量前的区域(临时性死区只有 let 和 const 才有)。看个🌰:

    if(false) {
      // 该区域便是临时性死区
      let a = 1
    }
  4. const 声明变量时必须同时赋值,并且不可更改。

  5. 在全局作用域中使用 var 声明的变量会存储在 window 对象中。而使用 let 和 const 声明的变量则只会覆盖 window 对象中同名的属性,而不会替换。看个🌰:

    window.a = 1
    let a = 2
    
    console.log(a) // 2
    console.log(window.a) // 1

    let 和 const 声明的变量会存在一个单独的Script块作用域中(即[[Scopes]]作用域中能找到)。

    // 接着上面列举的栗子
    function aa() {}
    console.dir(aa)
    // 你会发现在输出内容中[[Scopes]],会存在两个作用域,一个是Script,一个是Global
    [[Scopes]]: Scopes[2]
    0: Script {a: 2}
    1: Global {parent: Window, opener: null, top: Window, length: 1, frames: Window, }

字符串和正则表达式

  1. 支持 UTF-16(含义是任何一个字符都适用两个字节表示),其中方法如下:

    • codePointAt:返回参数字符串中给定位置对应的码位,返回值为整数。
    • fromCodePoint:根据指定的码位生成一个字符。
    • normalize:提供Unicode的标准形式,传入一个可选字符串参数,指明应用某种Unicode标准形式。
  2. 字符串中新增的方法有:

    • includes(str, index):检测字符串指定的可选索引中是否存在参数文本。

    • startsWith(str, index):检测字符串头部是否有指定的文本。

    • endsWith(str, index):检测字符串尾部是否有指定的文本。

    • repeat(number):接受一个整数,重复对应字符串整数次。

      console.log('aa'.repeat(2)) // aaaa
  3. 当给正则表达式添加 u 字符时,表示从编码单元操作模式切换为字符模式。

  4. 模板字符串支持多行文本、模板中动态插入变量、模板子面量方法使用。

    • 多行文本。

      `1
      
      2`
    • 模板中动态插入变量。

      let a = 1
      console.log(`${a} haha`) // 1 haha
    • 模板子面量方法。

      function aa(a, ...b) {
        console.log(a, b)
      }
      let a = 1
      aa`hehe ${a} haha`
      // 输出为:
      // ["hehe ", " haha", raw: ["hehe ", " haha"]] [1]

      其中参数 a 表示模板字符串中静态字符。参数 b 表示模板动态变量。

函数

  1. 支持默认参数,默认参数不仅可以为字符串、数字、数组或对象,还可以是一个函数。看个🌰:

    function sum(a = 1, b = 2) {
      return a + b
    }
    sum()

    参数默认值不能被 arguments 识别,看个🌰:

    function sum(a = 1, b = 2) {
      console.log(arguments)
    }
    sum() // {}
    sum(3, 6) // {"0": 3, "1": 6}

    默认参数同样存在临时性死区,看个🌰:

    // 在初始化a时,由于b还没被声明,因此无法直接将b赋值给a
    function aa(a = b, b) {
      return a + b
    }
  2. 支持展开运算符(...),其作用是解构数组和对象。看个🌰:

    // 展开数组或对象
    var a1 = [1, 2]
    var a2 = {a: 1}
    console.log(...a1) // 1 2
    console.log({...a2}) // {a: 1}
    
    // rest参数
    function aa(...obj) {
      console.log(obj)
    }
    aa(a1) // 1 2
    aa(a1, a2) // [[1, 2], {a: 1}]
  3. 支持箭头函数。箭头函数和普通函数的区别有:

    • 箭头函数木有 this,this 指向的是定义该箭头函数所在的对象。
    • 箭头函数没有 super
    • 箭头函数没有 arguments
    • 箭头函数内部不存在 new.target 绑定(构造函数存在)。并且箭头函数中不能使用 new 关键字。
    • 箭头函数不存在原型
  4. 支持尾调用优化。(调用一个函数时都会生成一个函数调用栈,尾调用就可以很好滴避免生成不必要的尾调用阮一峰的尾调用优化讲解

    只有满足以下三个条件时,引擎才会帮我们做好尾调用优化。

    • 函数不是闭包。
    • 尾调用是函数最后一条语句。
    • 尾调用结果作为函数返回。

    看个🌰:

    // 符合尾调用优化情况
    function aa() {
      return bb()
    }
    
    // 无return不优化
    function aa() {
      bb()
    }
    // 不是直接返回函数不优化
    function aa() {
      return 1 + bb()
    }
    // 最后一条语句不是函数不优化
    function aa() {
      const cc = bb()
      return cc
    }
    // 闭包不优化
    function aa() {
      function bb() {
        return 1
      }
      return bb()
    }

    需要知道的是,递归都是很影响性能的,但是有了尾调用后,递归函数的性能将得到有效的提升

    // 斐波那契数列
    // 常做方案
    function fibonacci(n) {
      if(n === 0 || n === 1) return 1;
      return fibonacci(n-1) + fibonacci(n-2);
    }
    
    // 尾递归优化,其中pre是第一项的值,next作为第二项的值
    function fibonacci(n, pre, next) {
      if (n <= 1) return next
      return fibonacci(n - 1, next, pre + next)
    }

对象的扩展

  1. 对象方法和属性支持简写,以及对象属性支持可计算。看个🌰:

    const id = 1
    const obj = {
      id,
      [`test${id}`]: 1,
      printId() {
        return this[`test${id}`]
      }
    }
  2. Object 新增了方法如下:

    • Object.is。判断两个值是否相等。

      console.log(Object.is(NaN, NaN)) // true
      console.log(Object.is(+0, -0)) // false
      console.log(Object.is(5, "5")) //false
    • Object.assign。浅拷贝一个对象,相当于一个 Mixin 功能。

      const obj = {a: 1, b: 2}
      const newObj = Object.assign({...obj, c: 3})
      console.log(newObj) // {a: 1, b: 2, c: 3}
  3. 对象支持同名属性,不过后面的属性会覆盖前面同名的属性。看个🌰:

    const obj = {
      a: 1,
      a: 2
    }
    console.log(obj) // {a: 2}
  4. 遍历对象时,默认都是数字属性按顺序提前,接着就是首字母排序。看个🌰:

    const obj = {
      name: 'haha',
      1: 2,
      a: 1,
      0: 'hehe'
    }
    for(var key in obj) {
      console.log(key)
    }
    // 输出顺序为
    // 0
    // 1
    // name
    // a
  5. 支持实例化后的对象改变原型对象,使用方法Object.setPrototypeOf()

    var parent1 = {a: 1}
    var child = Object.create(parent1)
    console.log(child.a) // 1
    var parent2 = {b: 2}
    child = Object.setPrototypeOf(child, parent2)
    console.log(child.a, child.b) // undefined, 2

解构

  1. 解构的定义是,从对象中提取出更小元素的过程,即对解构出来的元素进行重新赋值

  2. 对象解构必须是同名的属性。看个🌰:

    var obj = {
      a: 1,
      b: 2
    }
    var a = 3, b = 3;
    ({a, b} = obj)
    console.log(a, b) // 1, 2
  3. 数组解构可以有效地处理交换两个变量的值。

    var a = 1, b = 2;
    [a, b] = [b, a]
    console.log(a, b) // 2, 1

    数组解构还可以按需取值,看个🌰:

    var arr = [[1, 2], 3]
    var [[,b]] = arr
    console.log(b) // 2
  4. 混合解构就是对象和数组解构结合,看个🌰:

    var obj = {
      a: 1,
      b: [1, 2, 3]
    };
    ({a, b: [...arr]} = obj)
    console.log(a, arr) // 1, [1, 2, 3]
  5. 解构参数就是直接从参数中获取相应的参数值。看个🌰:

    function aa({a = 1, b = 2} = obj) {
      console.log(a, b)
    }
    aa({a: 3, b: 6}) // 3, 6

Symbol

  1. Symbol 是一种特殊的、不可变的数据类型,可作为对象属性的标识符使用,也是一种原始数据类型。

  2. Symbol 的语法格式为:

    Symbol([desc]) // desc是一个可选参数,用于描述Symbol所用

    创建一个 Symbol,如下:

    const a = Symbol()
    const b = Symbol('1')
    console.log(a, b) // Symbol(), Symbol('1')

    创建 Symbol 不能使用 new

  3. Symbol 最大的用处在于创建对象一个唯一可计算的属性名。看个🌰:

    const obj = {
      [Symbol('name')]: 12,
      [Symbol('name')]: 13
    }
    console.log(obj) // {Symbol(name): 12, Symbol(name): 13}

    有效地避免命名冲突问题。

  4. Symbol 不支持强制转换为其他类型。

  5. 在 ES6 中提出一个 @@iterator方法,所有支持迭代的对象(如数组、Set、Map)都要实现。其中**@@iterator方法的属性键为Symbol.iterator而非字符串**。只要对象定义有Symbol.iterator属性就可以用for...of进行迭代

    // 判断对象是否实现Symbol.iterator属性,就可以判断是否可以使用for...of进行迭代
    if(Symbol.iterator in obj) {
      for(var n of obj) console.log(n)
    }
  6. Symbol 支持全局共享机制,使用 Symbol.for 进行注册,使用 Symbol.keyFor 进行获取。看个🌰:

    // Symbol.for中参数可为任意类型
    let a = Symbol.for(12)
    console.log(Symbol.keyFor(a), typeof Symbol.keyFor(a)) // '12', string

    Symbol.keyFor 最终获取到的是一个字符串值。

  7. Symbol 可作为类的私有属性,使用 Object.keys 或 Object.getOwnPropertyNames 方法都无法获取 Symbol 的属性名只能使用 for...of 或 Object.getOwnPropertySymbols 方法获取。看个🌰:

    var obj = {
      [Symbol('name')]: 'Andraw-lin',
      a: 1
    }
    console.log(Object.keys(obj)) // ["a"]
    console.log(Object.getOwnPropertySymbols(obj)) // [Symbol(name)]

Set和Map

  1. Set 常用于检查对象中是否存在某个键值,Map 则常用于获取已存的信息。

  2. Set 是有序列表,含有相互独立的非重复值。支持的属性和方法如下:

    • size。返回 Set 对象中元素个数。

    • add(value)。在 Set 对象尾部添加一个元素。

    • entries()。返回 Set 对象中[值,值]形式,看个🌰:

      var a = new Set([1, 2, 3])
      for(var [b, c] of a.entries()) {
        console.log(b, c)
      }
      // 输出结果为:
      // 1 1
      // 2 2
      // 3 3
    • forEach(callback)。用于遍历 Set 集合。

    • has(value)。判断 Set 集合中是否存在有指定的值。

  3. Set 集合的特点是没有下标,没有 Key。Set 和 Array可以相互转换,如下:

    // 数组转成Set
    const arr = [1, 2, 3]
    console.log(new Set(arr))
    
    // Set转成数组
    const se = new Set([1, 2, 3])
    console.log([...se])
  4. Set 集合是一个强引用,只要 new Set 实例化的引用存在,就不会释放内存。若定义一个 DOM 元素的 Set 集合,然后在某个 js 中引用了该实例,当页面跳转时,并不会立马释放内存,因为引用还在。WeakSet 就是专门用于释放强引用的

    WeakSet 和 Set 区别:

    • WeakSet 对象中只能存放对象类型,不能存放原始数据类型。而 Set 对象则可以。
    • WeakSet 对象中存储的对象值是弱引用的,若无其他变量或属性引用该对象值,则这个对象值会被当成垃圾回收掉。而 Set 对象则是存储强引用。
    • WeakSet 对象中存储的值是无法被枚举。而 Set 对象则可以枚举。
  5. Map 是存储键值对的有序列表,key 和 value 支持所有数据类型。对比 Set 集合,Map 集合多了 set 方法和 get 方法。看个🌰:

    var m = new Map()
    m.set('name', 'haha')
    m.set('year', '1999')
    console.log(m.get('name'), m.get('year')) // haha, 1999

    支持对象作为 key 值,看个🌰:

    const key = {}
    m.set(key, 'hehe')
    m.get(key) // 'hehe'
  6. 和 WeakSet 一样,也会有 WeakMap 存在,专门针对弱引用。看个🌰:

    var map = new WeakMap()
    var key = document.querySelector('.header')
    map.set(key, 'DOM')
    map.get(key) // 'DOM'
    key.parentNode.removeChild(key)
    key = null

迭代器(Iterator)和生成器(Generator)

  1. 迭代器是一种特殊对象,每一个迭代器对象都有一个 next(),该方法返回一个对象,包括了 value 和 done 属性。使用 ES5 模拟实现迭代器如下:

    function createIterator(items) {
      var i = 0
      return {
        next() {
          var done = (i >= items.length)
          var value = i < items.length ? items[i++] : undefined
          return { done, value }
        }
      }
    }
    var arr = createIterator([1, 2])
    console.log(arr.next()) // { done: false, value: 1 }
    console.log(arr.next()) // { done: false, value: 2 }
    console.log(arr.next()) // { done: true, value: undefined }
  2. 生成器就是一个函数,用于返回迭代器的。使用 * 号声明的函数即为生成器函数,同时需要使用 yield 控制进程。看个🌰:

    function *createIterator() {
      console.log(1)
      yield 1
      console.log(2)
      yield 2
      console.log(3)
    }
    var a = createIterator() // 执行后并不会输出任何东西
    console.log(a.next()) // 先输出1,再输出{ value: 1, done: false }
    console.log(a.next()) // 先输出2,再输出{ value: 2, done: false }
    console.log(a.next()) // 先输出3,再输出{ value: undefined, done: true }

    总结一下,迭代器执行 next() 方法时,只会执行前面到 yield 间的代码,后面代码都会被终止

    同样地,在 for 循环中使用迭代器,遇到 yield 时都会终止进程。看个🌰:

    function *createIterator(items) {
      for(let i = 0; i < items.length;  i++) {
        yield items[i]
      }
    }
    const a = createIterator([1, 2, 3]);
    console.log(a.next()); //{value: 1, done: false}
  3. yield 只能在生成器函数内使用。

  4. 生成器函数还可以使用匿名函数形式创建,看个🌰:

    const createIterator = function *() {
      // ...
    }
  5. 凡是通过生成器得到的迭代器,都是可迭代的对象(即可迭代对象具有 Symbol.iterator 属性),可使用 for...of 进行迭代。看个🌰:

    function *createIterator() {
      yield 1
      yield 2
    }
    var obj = createIterator()
    for(var val of obj) {
      console.log(val)
    }
    // 输出为
    // 1
    // 2

    可迭代对象可访问 Symbol.iterator 直接得到迭代器,看下面

    function *createIterator() {
      yield 1
      yield 2
    }
    var obj = createIterator()
    var newObj = obj[Symbol.iterator]() // 其实obj[Symbol.iterator]相当于createIterator迭代器

    Symbol.iterator 可用于检测一个对象是否可迭代

    typeof obj[Symbol.iterator] === "function"
  6. 默认情况下定义的对象是不可迭代的,但是可以通过 Symbol.iterator 创建迭代器。看个🌰:

    const obj = {
      items: [],
      *[Symbol.iterator]() {
        for (let item of this.items) {
          yield item;
        }
      }
    }
  7. 数组、Set、Map等可迭代对象,其内部已实现迭代器,并且提供3种迭代器调用,分别是:

    • entries():返回一个迭代器,用于返回键值对。

      var arr = [1, 2, 3]
      for(var [key, value] of arr.entries()) {
        console.log(key, value)
      }
      // 输出结果为
      // 0 1
      // 1 2
      // 2 3
    • values():返回一个迭代器,用于返回键值对的value。

      var arr = [1, 2, 3]
      for(var value of arr.values()) {
        console.log(value)
      }
      // 输出结果为
      // 1
      // 2
      // 3
    • keys():返回一个迭代器,用于返回键值对的key。

      var arr = [1, 2, 3]
      for(var key of arr.keys()) {
        console.log(key)
      }
      // 输出结果为
      // 0
      // 1
      // 2
  8. 高级迭代器功能,主要包括传参、抛出异常、生成器返回语句、委托生成器。

    • 传参。next 方法传参数时,会作为上一轮 yield 的返回值,除了第一轮 yield 外,看个🌰:

      function *aa() {
        var a1 = yield 1
        var a2 = 10
        yield a1 + 10
      }
      var a = aa()
      console.log(a.next(2)) // { value: 1, done: false }
      console.log(a.next(100)) // { value: 110, done: false }
    • 抛出异常。

      function *aa() {
        var a1 = yield 1
        var a2 = 10
        yield a1 + 10
      }
      var a = aa()
      console.log(a.next(2)) // { value: 1, done: false }
      console.log(a.throw(new Error('error'))) // error
      console.log(a.next(100)) // 不再执行
    • 生成器种遇到 return 语句时,表示退出操作。

      function *aa() {
        var a1 = yield 1
        return
        yield a1 + 10
      }
      var a = aa()
      console.log(a.next(2)) // { value: 1, done: false }
      console.log(a.next(100)) // { value: undefined, done: true }
    • 委托生成器。其实就是生成器嵌套生成器。

      function *aIterator() {
        yield 1;
      }
      function *bIterator() {
        yield 2;
      }
      function *cIterator() {
        yield *aIterator()
        yield *bIterator()
      }
      var i = cIterator()
      console.log(i.next()) // {value: 1, done: false}
      console.log(i.next()) // {value: 2, done: false}
  9. 异步任务执行器,其实就是用来循环执行生成器。

    因为我们知道生成器需要执行 N 次 next() 方法才能运行完,异步任务执行器就是帮我们做这些事情的。

    function run(taskFn) {
      var task = taskFn() // 调用生成器
      var result = task.next()
      function step() {
        if(!result.done) {
          result = task.next(result.value)
          step()
        }
      }
      step()
    }
    run(function *() {
      let text = yield fetch() // 异步请求获取数据
      doSomething(text) // 处理返回结果
    })

    异步任务执行器,其实就是间接实现了 async 和 await 功能。

类class

  1. 在 ES6 中,将原型的实现写在类中,和 ES5 本质上是一致的,都是需要新建一个类名,然后实现构造函数再实现原型方法。看个🌰:

    class Person {
      constructor(name) { // 新建构造函数
        this.name = name // 私有属性
      }
      sayName() { // 定义一个方法并且赋值到构造函数的原型中
        return this.name
      }
    }

    私有属性的定义,只需要在构造方法中定义this.xx = xx即可

  2. 类声明和函数声明的区别,主要有:

    • 类声明不能提升,而函数声明则会被提升。
    • 类声明中代码会自动强行运行在严格模式下。
    • 类中的所有方法都是不可枚举的,而函数声明的对象中方法则是可以枚举的。
    • 类中的构造函数只能使用 new 来调用,而函数则可以普通调用或 new 来调用。
  3. 类的定义有声明式定义和表达式定义,看个🌰:

    // 声明式定义
    class Person {
      // ...
    }
    
    // 表达式定义
    let person = Class {
      // ...
    }

    类还支持立即调用。

    let person = new Class {
      constructor(name) {
        this.name = name
      }
      sayName() {
        return this.name
      }
    }('Andraw')
    console.log(person.sayName()) // Andraw
  4. 类支持在原型上定义访问器属性。看个🌰

    class Person {
      constructor(name) {
        this.name = name
      }
      get myName() {
        return this.name
      }
      set myName(name) {
        this.name = name
      }
    }
    var descriptor = Object.getOwnPropertyDescriptor(Person.prototype, 'myName')
    console.log('get' in descriptor) // true
    console.log(descriptor.enumerable) // false 表示不可枚举

    类中定义的属性或方法名都是可支持表达式的

    const test = 'sayName'
    class Person{
      constructor(name) {
        this.name = name
      }
      [test]() {
        return this.name
      }
    }

    类中定义的方法同样可以是生成器方法。

    class Person {
      *sayName() {
        yield 1
        yield 2
      }
    }
  5. 静态属性或静态方法,是在属性或方法前面使用 static 关键字。static 修饰的方法或属性只能被类本省直接访问,而不能在实例中访问

    class Person {
      static sayName() {
        return this.name
      }
    }
  6. 在 React 中写一个组件Test时,必须得继承React.Component。其中 Test 组件就是一个派生类。派生类中的构造函数内部必须使用 super()

    关于 super 的使用需要注意:

    • 只可以在派生类中使用 super。派生类是指继承自其他类的新类。
    • 派生类中构造函数访问 this 之前必须要先调用 super(),负责初始化 this
    • 如果不想调用 super,可让类的构造函数返回一个对象。
  7. 当派生类继承于父类时,其父类中的静态成员也会被继承到派生类中,但是静态成员同样只能是被派生类访问,而无法被其实例访问

  8. 在构造函数中可使用 new target(new target 通常表示当前的构造函数名)来阻止实例化类。看个🌰:

    class Person {
      constructor() {
        if(new.target === Person) { // 不允许该类被调用实例化
          throw new Error("error")
        }
      }
    }

数组

  1. 在 ES5 中创建数组的方式有两种,分别是数组子面量(即 var arr = [])和 Array 实例(即 new Array())。

    在 ES6 中新增两种方法创建数组,分别是 Array.of() 和 Array.from()。

    • Array.of()。我们知道 new Array() 中传入一个数字时,表示的是生成多少长度的数组,Array.of() 就是为了处理这种尴尬场面的,看个🌰:

      const a1 = new Array(1)
      const a2 = Array.of(1)
      console.log(a1, a2) // [undefined], [1]
    • Array.from()。用于将类数组转换为数组。

      function aa() {
        const arr = Array.from(arguments)
        console.log(arr)
      }
      aa(1, 2)
      // [1, 2]
      
      // 可传第二个参数,作为第一个参数的转换
      const arr = Array.from(arguments, value => value + 2)
      
      // 可传第三个参数,用来指定this
      
      // Array.from可用于处理数组去重
      Array.from(new Set(...arguments))
  2. 数组新增的方法有。

    • find()。传入一个回调函数,找到数组中符合当前搜索规则的第一个元素,返回它,并且终止搜索。

      var arr = [1, 2, 3]
      console.log(arr.find(n => typeof n === "number")) // 1
    • findIndex()。传入一个回调函数,找到数组中符合当前搜索规则的第一个元素,返回它的下标,终止搜索。

      var arr = [1, 2, 3]
      console.log(arr.find(n => typeof n === "number")) // 0
    • fill()。用新元素替换掉数组内的元素,可以指定替换下标范围。格式和🌰如下:

      // 格式如下
      arr.fill(value, start, end)
      
      // 栗子
      const arr = [1, 2, 3]
      console.log(arr.fill(4)) // [4, 4, 4] 不指定开始和结束,全部替换
      
      const arr1 = [1, 2, 3]
      console.log(arr1.fill(4, 1)) // [1, 4, 4] 指定开始位置,从开始位置全部替换
      
      const arr2 = [1, 2, 3]
      console.log(arr2.fill(4, 0, 2)) // [4, 4, 3] 指定开始和结束位置,替换当前范围的元素
    • copyWithin()。选择数组的某个下标,从该位置开始复制数组元素,默认从0开始复制。格式和🌰如下:

      // 格式如下
      arr.copyWithin(target, start, end)
      
      // 栗子
      const arr = [1, 2, 3, 4, 5]
      console.log(arr.copyWithin(3)) // [1,2,3,1,2] 从下标为3的元素开始,复制数组,所以4, 5被替换成1, 2
      
      const arr1 = [1, 2, 3, 4, 5]
      console.log(arr1.copyWithin(3, 1)) // [1,2,3,2,3] 从下标为3的元素开始,复制数组,指定复制的第一个元素下标为1,所以4, 5被替换成2, 3
      
      const arr2 = [1, 2, 3, 4, 5]
      console.log(arr2.copyWithin(3, 1, 2)) // [1,2,3,2,5] 从下标为3的元素开始,复制数组,指定复制的第一个元素下标为1,结束位置为2,所以4被替换成2

Promise与异步编程

  1. 对 DOM 做事件处理操作,如点击、激活焦点、失去焦点等,再比如使用 Ajax 请求数据时利用回调函数获取返回值,都属于异步编程。

  2. Promise 中文意思就是承诺,Javascript 对你许一个承诺,会在未来某个时刻兑现承诺

    Promise 有生命周期,分别是进行中(pending)、已经完成(fulfilled)、拒绝(rejected)

    Promise 不会直接返回异步函数的执行结果,需要使用 then 方法获取,获取异常回调时,需要使用 catch 方法获取。

    结合 axios 看个🌰,axios 是前端比较热门的 http 请求插件之一。

    // 1. 创建axios实例
    import axios from 'axios'
    export const instance = axios.create()
    
    // 2. 使用axios实例 + Promise获取返回值
    const promise = instance.get('url')
    promise.then(res => console.log(res)).catch(err => console.log(err))
  3. Promise 构造函数只有一个参数,该参数为一个函数,被作为执行器。执行器有两个参数,分别是 resolve() 和 reject() ,前一个表示成功回调,后一个表示失败回调。

    new Promise((resolve, reject) => {
      setTimeout(() => resolve(5), 0)
    }).then(res => console.log(5)) // 5

    Promise 实例只能过 resolve 或者 reject 函数返回数据,并且使用 then 或者 catch 进行获取

    Promise.resolve(1).then(res => console.log(res)) // 1
    Promise.reject(2).catch(res => console.log(res)) // 2
    // 捕获错误时,可使用catch获取
    new Promise((resolve, reject) => {
      if(true) {
        throw new Error('error')
      }
    }).catch(err => console.log(err))
  4. 浏览器和 Node 提供了 unhandledRejection 和 rejectionHandled 两个事件处理 Promise 中没有设置 catch 问题

    // unhandledRejection
    let rejected
    rejected = Promise.reject("It was my wrong!")
    
    process.on("unhandledRjection", function(reason, promise) {
      console.log(reason.message) // It was my wrong!
      console.log(rejected === promise) // true
    })
    
    // rejectionHandled
    let rejected
    rejected = Promise.reject("It was my wrong!")
    
    process.on("rejectionHandled", function(reason, promise) {
      console.log(reason.message) // It was my wrong!
      console.log(rejected === promise) // true
    })

    浏览器中使用上面两个方法只是在 window 对象上监听,而 Node 中使用是在 process 对象上监听。

    unhandledRejection 和 rejectionHandled的区别就是,前者是事件循环中触发,后者则是事件循环后触发。两者都是处理 Promise 中使用 reject 捕获错误时,而没有使用 catch 进行捕获处理

  5. Promise 支持链式调用,有效地解决了回调地狱问题。看个🌰:

    new Promise((resolve, reject) => {
      resolve(1)
    }).then(res => { return res + 1 }).then(res => {console.log(res)}) // 2
  6. 除了 resolve 和 reject 方法外,还有两个方法,便是 Promise.all 和 Promise.race。

    • Promise.all。运行多个 Promise,当全部 Promise 都返回结果时,才会使用 then 进行处理

      Promise.all([
        new Promise(function(resolve, reject) {
          resolve(1)
        }),
        new Promise(function(resolve, reject) {
          resolve(2)
        }),
        new Promise(function(resolve, reject) {
          resolve(3)
        })
      ]).then(arr => {
        console.log(arr) // [1, 2, 3]
      })
    • Promise.race。和 all 方法类似,不过就是当有一个返回结果时,就可以使用 then 进行处理

      Promise.race([
        new Promise(function(resolve, reject) {
          setTimeout(() => resolve(1), 1000)
        }),
        new Promise(function(resolve, reject) {
          setTimeout(() => resolve(2), 10)
        }),
        new Promise(function(resolve, reject) {
          setTimeout(() => resolve(3), 100)
        })
      ]).then(value => {
        console.log(value) // 2
      })
  7. Promise 本身不是异步,只有它的 then 方法或者 catch 方法才是异步。

    目前 ES7 已经支持 async 方案,该方案比 Promise 还强大啊。

    async function a() {
      await function() {}
    }

代理(Proxy)和反射(Reflect)

  1. 代理 Proxy 就是拦截 JS 引擎内部目标的底层对象操作,反射 Reflect 就是针对 Proxy 还原原对象操作方法。

    代理陷阱 覆写的特性 默认特性
    get 读取一个属性值 Reflect.get()
    set 写入一个属性 Reflect.set()
    has in操作符 Reflect.has()
    deleteProperty delete操作符 Reflect.delete()
    getProperty Object.getPropertypeOf() Reflect.getPrototypeOf()
    setProperty Object.setPrototypeOf() Reflect.setPrototypeOf()
    isExtensible Object.isExtensible() Reflect.isExtensible()
    preventExtensions Object.preventExtensions() Reflect.preventExtensions()
    getOwnPropertyDescriptor Object.getOwnPropertyDescriptor() Reflect.getOwnPropertyDescriptor()
    defineProperty Object.defineProperty() Reflect.defineProperty()
    ownKeys Object.ownKeys()、Object.getOwnPropertyNames()、Object.getOwnPropertySymbols() Reflect.ownKeys()
    apply 调用一个函数 Reflect.apply()
    construct 用new调用一个函数 Reflect.construct()

    反射 Reflect 一般和代理 Proxy 结合使用,设置相应的代理方法处理数据时,需同时使用反射 Reflect 对原对象的方法操作一遍

    现在就拿 set 做个🌰。set属性是在要改变Proxy属性值时,进行的预处理,共接收四个参数:

    • target:要进行预处理的目标对象;
    • key:预处理过程的Key值,相当于对象的属性;
    • value:要设置成的值;
    • receiver:改变前的原始值;
    let pro = new Proxy({
      aa: 2
    }, {
      set: (target, key, value, receiver) => console.log(target, key, value, receiver);
    });
    pro.aa = 8;         // {aa: 2} "aa" 8 Proxy {aa: 2}
  2. Proxy的存在能够使函数加上钩子函数,即可理解为在执行方法前预处理一些代码,举个栗子:

    let pro = new Proxy({}, {
      get: (target, key, property) => console.log('haha')
    });
    pro.aa;                 // haha
    pro.bb;                 // haha
  3. 在使用如 http-proxy 插件或 webpack 时,有时需要访问某个 api,通过配置 proxy 跳转到指定的 url 能解决跨域问题。但是该种模式和代理 Proxy 有异曲同工之处,但是机制是不一样的。

    devServer: {
      proxy: [
        {
          context: "/api/*", //代理API
          target: 'https://www.hyy.com', //目标URL
          secure: false
        }
      ]
    }

使用模块封装代码

  1. 模块可以是函数、数据、类,需要指定导出的模块名,才能被其他模块访问。看个🌰

    // 数据模块
    const obj = { a: 1 }
    // 函数模块
    const sum = (a, b) => a + b
    // 类模块
    class Person {
      // ...
    }
  2. 模块引入使用 import 关键字,导入模块方式有两种。

    • 导入指定的模块。

      import { sum } from 'a.js'
      sum(1, 2)
    • 导入全部模块。

      import allFn from 'a.js'
      allFn.sum(1, 2)
  3. 模块导出使用 export 关键字,看个🌰:

    // 导出数据模块
    export const obj = { a: 1 }
    // 导出函数模块
    export const sum = (a, b) => a + b
    // 导出类模块
    export class Person {
      // ...
    }

    需要注意的是,ES6 提供了模块的默认导出,在导出时结合 default 关键字,看个🌰:

    // a.js
    function sum(a, b) {
      return a + b
    }
    export default sum
    
    // b.js
    import sum from 'a.js'
    sum(1, 0)
  4. 不能在语句和函数内使用 export 关键字,只能在模块顶部使用

  5. ES6 提供了两种方式修改模块的导入和导出名,分别是导出时修改和导入时修改,使用 as 关键字

    • 导出时修改。

      // a.js
      function sum(a, b) {
        return a + b
      }
      export default {sum as add}
      
      // b.js
      import add from 'a.js'
      add(1, 2)
    • 导入时修改。

      // a.js
      function sum(a, b) {
        return a + b
      }
      export default sum
      
      // b.js
      import sum as add from 'a.js'
      add(1, 2)
  6. 无绑定导入,是指当模块没有可导出模块时,全都是定义的全局变量,可使用无绑定导入。看个🌰:

    // a.js
    let a = 1
    const PI = 3.1314
    
    // b.js
    import 'a.js'
    console.log(a, PI) // 1, 3.1314
  7. 使用 webpack 打包 js 后,浏览器加载模块时,总是按顺序加载,先加载模块1,再加载模块2,因为 module 类型默认使用 defer 属性

    <script type="module" src="module1.js"></script>
    <script type="module" src="module2.js"></script>

ES7

ES7 在 ES6 基础上仅仅新增了**求幂运算符(**)**和 Array.prototype.includes() 方法。

需要注意的是,在 ES6 中仅仅提供了字符串 includes 实现,而在 ES7 中则在数组中进行完善

求幂运算符(**)

** 运算符相当于 Math 对象中的 pow 求幂方法,使用如下。

2 ** 3 = 8
// 相当于
Math.pow(2, 3) // 8

** 运算符和 +- 运算符用法一致,看个🌰:

let num = 2
num **= 3
// 相当于 num = num ** 3
console.log(num) // 8

Array.prototype.includes()

数组中实现的 includes 方法,用于判断一个数组是否包含一个指定的值,如果包含就返回 true,否则返回 false

includes 和 indexOf 都是使用 === 来进行比较,但是在 includes 中,NaN === NaN 返回的是 true,而 indexOf 则是返回 false

另外,includes 和 indexOf 方法都是认为,+0 === -0

ES8

ES8 也是在 ES6 基础上继续进行拓展。

Object.values()和Object.entries()

在 ES6 中提及过,只有可迭代对象可以直接访问 keys、entries、values 三个方法在 ES8 中在 Object 对象上实现了 values 和 entries 方法,因为 Object 已经支持了 kes 方法,直接看🌰:

var obj = {
  a: 1,
  b: 2
}
console.log(Object.keys(obj)) // ["a", "b"]
console.log(Object.values(obj)) // [1, 2]
console.log(Object.entries(obj)) // [["a", 1], ["b", 2]]

其中,entries 方法还能结合 Map 数据结构。

var obj = {
  a: 1,
  b: 2
}
var map = new Map(Object.entries(obj))
console.log(map.get('a')) // 1
// Map { "a" => 1, "b" => 2 }

字符串追加

  1. 字符串新增方法 String.prototype.padStart 和 String.prototype.padEnd,用于向字符串中追加新的字符串。看个🌰:

    '5'.padStart(2) // ' 5'
    '5'.padStart(2, 'haha') // 'h5'
    '5'.padEnd(2) // '5 '
    '5'.padEnd(2, 'haha') // '5h'

    padStart 和 padEnd 对于格式化输出很有用。

  2. 使用 padStart 方法举个例子,有一个不同长度的数组,往前面追加 0 来使得长度都为 10。

    const formatted = [0, 1, 12, 123, 1234, 12345].map(num => num.toString().padStart(10, '0'))
    console.log(formatted)
    // ["0000000000", "0000000001", "0000000012", "0000000123", "0000001234", "0000012345"]

    使用 padEnd 也是同样的道理。

Object.getOwnPropertyDescriptors

Object.getOwnPropertyDescriptors 直接返回一个对象所有的属性,甚至包括 get/set 函数。

ES2017 引入该函数主要目的在于方便将一个对象浅拷贝给另一个对象,同时也可以将 getter/setter 函数也进行拷贝。意义上和 Object.assign 是不一样的。

直接看个🌰:

var obj = {
  a: 1,
  b: {
    a: 2
  },
  set c(temp) {
    this.d = temp
  },
  get c() {
    return this.d
  }
}
var newObj1 = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj))
console.log(newObj1)
// {
//  c: undefined
//  a: 1
//  b: {a: 2}
//  get c: ƒ c()
//  set c: ƒ c(temp)
//  __proto__: Object
// }
var newObj2 = Object.assign({}, obj)
console.log(newObj2)
// {
//  a: 1
//  b: {a: 2}
//  c: undefined
//  __proto__: Object
// }

在克隆对象方面,Object.assign 只能拷贝源对象中可枚举的自身属性,同时拷贝时无法拷贝属性的特性(如 getter/setter)。而使用 Object.getOwnPropertyDescriptors 方法则可以直接将源对象的所有自身属性(是自身属性啊,不是所有可访问属性!)弄出来,再拿去复制

上面的栗子中就是配合原型,将一个对象中可访问属性都拿出来进行复制,弥补了 Object.getOwnPropertyDescriptors 方法短处(即无法获取可访问原型中的属性)。

若只是浅复制自身属性,还可以结合 Object.defineProperties 来实现

var obj = {
  a: 1,
  b: {
    a: 2
  },
  set c(temp) {
    this.d = temp
  },
  get c() {
    return this.d
  }
}
var newObj = Object.defineProperties({}, Object.getOwnPropertyDescriptors(obj))
conso.e.log(newObj)
// {
//  c: undefined
//  a: 1
//  b: {a: 2}
//  get c: ƒ c()
//  set c: ƒ c(temp)
//  __proto__: Object
// }

允许在函数参数最后添加逗号

听说是为了方便 git 算法更加方便区分代码职责。直接看个🌰。

const sum = (a, b,) => a + b

Async/Await

在 ES8 所有更新中,最有用的一个!!!

async 关键字告诉 Javascript 编译器对于标定的函数要区别对待。当编译器遇到 await 函数时会暂停,它会等到 await 标定的函数返回的 promise,该 promise 要么 resolve 得到结果、要么 reject 处理异常。

直接上一个栗子,对比一下使用 promise 和使用 async 区别。

// 模拟获取userName接口
var getUser= userId
 => new Promise(resolve => {
   setTimeout(() => {
     resolve(userName)
   }, 2000)
 })
// 模拟获取userAge接口
var getUserAge = userName
 => new Promise(resolve => {
   setTimeout(() => {
     if(userName === 'Andraw') {
       resolve('24')
     } else {
       reject('unknown user')
     }
   }, 2000)
 })
// ES6的promise实现方式
function es6Fn(userId) {
  getUser(userId)
    .then(getUserAge)
    .then(age => {
      console.log(age)  
    })
}
// ES8的async实现方式
async function es8Fn(userId) {
  var userName = await getUser(userId)
  var userAge = await getUserAge(userName)
  console.log(userAge)
}

使用 ES8 的 async 异步编程更符合日常开发流程,而 ES6 的 promise 也是一个很好的使用, ES8 的 async 只是在 promise 基础上更上一层楼。

  1. async 函数返回 promise。

    若想获取一个 async 函数的返回结果,则需要使用 promise 的 then 方法

    接着拿上述 ES8 的 async 实现方式来举个例子。

    async function es8Fn(userId) {
      var userName = await getUser(userId)
      var userAge = await getUserAge(userName)
      return userAge
    }
    // 获取es8Fn async函数返回结果
    es8Fn(1).then(userAge => { console.log(userAge) })
  2. 并行处理

    我们知道,每次调用 es8Fn 函数时,都需要等到至少 4 秒时间,若调用 N 次,则需要等到 4N 秒。使用 Promise.all 来并行处理,可以极大释放时间限制。

    async function newES8Fn() {
      var [a, b] = await Promise.all([es8Fn, es8Fn])
      return [a, b]
    }

    上述并行处理后,就可以很好滴避免多次调用而时间耗费的问题。

  3. 错误处理

    对于 async/await 的错误处理,有三种方法可以处理,分别是在函数中使用 try-catch、catch 每一个 await 表达式、catch 整个 async-await 函数。

    • 在函数中使用 try-catch

      async function es8Fn(userId) {
        try {
        	var userName = await getUser(userId)
          var userAge = await getUserAge(userName)
          return userAge 
        } catch(e) {
          console.log(e)
        }
      }
    • catch 每一个 await 表达式

      由于每一个 await 表达式都返回 Promise,对每一个表达式都进行 catch 处理。

      async function es8Fn(userId) {
        var userName = await getUser(userId).catch(e => { console.log(e) })
        var userAge = await getUserAge(userName).catch(e => { console.log(e) })
        return userAge
      }
    • catch 整个 async-await 函数

      async function es8Fn(userId) {
        var userName = await getUser(userId)
        var userAge = await getUserAge(userName)
        return userAge
      }
      es8Fn(1).then(userAge => { console.log(userAge) }).catch(e => { console.log(e) })

ES9

ES9(即ES2018) 主要新增了对象的扩展运算符 Rest 以及 Spread、异步迭代器、Promise支持 finally 方法、正则的扩展。

对象的扩展运算符 Rest 以及 Spread

如果使用过 Object.assign 方法合并对象,应该就很清楚。在 ES6 中,在数组中支持了 Rest 解构赋值和 spread 语法。

// ES6中的Rest
var [a, ...b] = [1, 2, 3, 4, 5, 6]
console.log(a, b) // 1, [2, 3, 4, 5, 6]

// ES6中的spread
function sum(a, ...b) {
  console.log(a, b)
}
sum(1, 2, 3)
// 输出为:1, [2, 3]

ES8 则在对象中支持了 Rest 解构赋值和 Spread 语法

// rest解构赋值
var {x, ...y} = {x: 1, a: 2, b: 3}
console.log(x, y) // 1, { a: 2, b: 3 }

// spread语法,接着上面解构的值
var c = {x, ...y}
console.log(c) // {x: 1, a: 2, b: 3}

异步迭代器和异步生成器

在 ES6 中,如果一个对象具有 Symbol.iterator 方法,那该对象就是可迭代的。目前,只有 Set、Map、数组内部实现 Symbol.iterator 方法,因此都是属于可迭代对象。

var set = new Set([1, 2, 3])
var setFn = set[Symbol.iterator]()
console.log(setFn) // SetIterator {1, 2, 3}
console.log(setFn.next()) // {value: 1, done: false}
console.log(setFn.next()) // {value: 2, done: false}
console.log(setFn.next()) // {value: 3, done: false}
console.log(setFn.next()) // {value: undefined, done: true}

默认的对象是不支持可迭代的,若实现了 Symbol.iterator 方法,那么它也是可迭代的。那么对象的 Symbol.iterator 方法如何实现的呢?

var obj = {
  a: 1,
  b: 2,
  [Symbol.iterator]() {
    var allKeys = Object.keys(this)
    var i = 0
    return {
      next: () => {
      	return {
          value: this[allKeys[i++]],
          done: i > allKeys.length
        }
      }
    }
  }
}
var objFn = obj[Symbol.iterator]()
console.log(objFn) // {next: ƒ}
console.log(objFn.next()) // {value: 1, done: false}
console.log(objFn.next()) // {value: 2, done: false}
console.log(objFn.next()) // {value: undefined, done: true}

上面的实现,还可以再完善一丢。利用生成器

var obj = {
  a: 1,
  b: 2,
  [Symbol.iterator]: function *() {
    for(let key in this) {
      yield this[key]
    }
  }
}
var objFn = obj[Symbol.iterator]()
console.log(objFn) // Generator {_invoke: ƒ}
console.log(objFn.next()) // {value: 1, done: false}
console.log(objFn.next()) // {value: 2, done: false}
console.log(objFn.next()) // {value: undefined, done: true}

由上面可以知道,同步迭代器就是一个特殊对象,里面包含有 value 和 done 两个属性(即 {value, done})。那么异步迭代器又是什么?

异步迭代器,和同步迭代器不同,不返回 {value, done} 形式的普通对象,而是直接返回一个 {value, done} 的 promise 对象

其中,同步迭代器使用 Symbol.iterator 实现,异步迭代器使用 Symbol.asyncIterator 实现

var obj = {
  a: 1,
	b: 2,
  [Symbol.asyncIterator]() {
    var allKeys = Object.keys(this)
    var i = 0
    return {
      next: () => {
        return Promise.resolve({
          value: this[allKeys[i++]],
          done: i > allKeys.length
        })
      }
    }
  }
}
var objAsyncFn = obj[Symbol.asyncIterator]()
console.log(objAsyncFn) // {next: ƒ}
console.log(objAsyncFn.next().then(res => {
  console.log(res) // {value: 1, done: false}
}))
console.log(objAsyncFn.next().then(res => {
  console.log(res) // {value: 2, done: false}
}))
console.log(objAsyncFn.next().then(res => {
  console.log(res) // {value: undefined, done: true}
}))

那么既然有了异步迭代器,就肯定有异步生成器,专门用来生成异步迭代器的

var obj = {
  a: 1,
	b: 2,
  [Symbol.asyncIterator]: async function *() {
    for(let key in this) {
      yield this[key]
    }
  }
}
var objAsyncFn = obj[Symbol.asyncIterator]()
console.log(objAsyncFn) // obj {<suspended>}
console.log(objAsyncFn.next().then(res => {
  console.log(res) // {value: 1, done: false}
}))
console.log(objAsyncFn.next().then(res => {
  console.log(res) // {value: 2, done: false}
}))
console.log(objAsyncFn.next().then(res => {
  console.log(res) // {value: undefined, done: true}
}))

另外,异步迭代器和同步迭代器有一样东西很类似,就是使用 next() 后,是无法知道什么时候才会到最后一个值,在同步迭代器中,需要使用 for...of 进行遍历才能有效地处理迭代器中每一项值

在异步迭代器中,同样支持遍历,不过是 for...await...of 遍历

var obj = {
  a: 1,
	b: 2,
  [Symbol.asyncIterator]: async function *() {
    for(let key in this) {
      yield this[key]
    }
  }
}
(async function() {
  for await (var value of obj) {
    console.log(value)
  }
})()

for...await...of 只会在异步生成器中或异步函数中有效

Promise支持 finally 方法

Promise 成功获取数据时使用 then 方法,处理异常时使用 catch 方法。但是在某些情况下,我们不管成功还是存在异常,都希望 Promise 能够运行一些共同的代码,finally 就是处理这些事情的

Promise.resolve(1).then(res => { console.log(res) }).finally(() => { console.lig('common code...') })
// 输出结果为
// 1
// common code...

正则的扩展

在正则表达式中,点 . 可以表示任意单个字符。

/foo.bar/.test('foo\nbar') // false

上面代码中,为了能够匹配任意字符,ES9 提出了 s 修饰符,使得 . 可以匹配任意单个字符。

/foo.bar/s.test('foo\nbar') // true

还有几个暂不讨论。可自行了解哈

ES10

ES10(即 ES2019) 新增功能相对比较少,都是一些性能的优化。

Array.prototype.flat和Array.prototype.flatMap

在日常开发中,我们常遇到一个问题,那就是将[1, [1, 2], [1, [2, 3]]]扁平化为[1, 1, 2, 1, 2, 3]

以往的经历告诉我们,需要使用第三方库 lodash 来处理,导致了不必要的麻烦,为此,ES10 直接为数组提供了 flat 方法来实现扁平化数组

var arr = [1, [1, 2], [1, [2, 3]]]
console.log(arr.flat(2)) // [1, 1, 2, 1, 2, 3]

flat 方法中参数,表示的是扁平化的层数

另外的方法 flatMap,其实就是数组的 flat 方法和 map 方法结合。

[1, 2, 3].map(x => [x * x]) // [[1], [4], [9]]
[1, 2, 3].flatMap(x => [x * x]) // [1, 4, 9]

Obejct.fromEntries

Object.fromEntries 方法和 ES6 中的 Object.entries 功能刚好相反,Object.entries 是获取一个对象的键值对,而 Object.fromEntries 则是将键值对转化为对象

Object.fromEntries([["a", 1], ["b", 2]]) // {a: 1, b: 2}
Object.entries({a: 1, b: 2}) // [["a", 1], ["b", 2]]

字符串去除首尾空格

ES10 为字符串提供了 trimStart 和 trimEnd 方法,用于去除首尾空格。

'  123'.trimStart() // 123
'123  '.trimEnd() // 123

Symbol.prototype.description

定义 Symbol 类型时,可传入一个字符串作为标志,若想获得该字符串,ES6 并没有提供方法,而 ES10 则提供了 description 属性用于获取 Symbol 的描述信息

var symbol = Symbol('haha')
console.log(symbol.description) // haha

可选的catch参数

在 ES10 之前,使用 try...catch 块时,若不给 catch 函数传递参数,会报错。

ES10 则直接将 catch 参数作为可选。

// ES10前
try {
  // ...
} catch(e) {
  console.log(e)
} 
// ES10后
try {
  // ...
} catch {
  // ...
}

Array.prototype.sort方法由快排转换为Timsort

在 ES10 前,数组的 sort 方法默认采取的是快排,但会存在不稳定性,为此,直接转为使用 Timsort。可自行了解一下。

Timsort 就是将插入排序和归并排序进行合并起来得到的好算法

函数支持toString方法

ES10 支持函数直接以字符串的形式打印出来。

var sum = (a, b) => a + b
console.log(sum.toString()) // (a, b) => a + b

Webpack 优化--输出质量篇

我们都知道,使用 Webpack 打包后的文件大小,会直接影响到线上用户的体验,如首屏加载时间,包越大时相应的打开时长也会越长,毕竟还是需要时间和网络下载一个包的。😅

那么,我们应该如何去使用 Webpack 更好滴优化打包后的文件呢?接下来我就来简单讲解一些常用方法。

目录

  1. 使用process区分环境
  2. 利用UglifyJS对JavaScript压缩,利用cssnano对css压缩
  3. 配置publicPath指向CDN服务加速
  4. 使用Tree-Shaking去除多余的无用代码
  5. 提取公共代码和第三方代码
  6. 提取懒加载代码(即按需加载的代码)

使用process区分环境

在我们开发 web 时,一般都会分为两个环境,分别是开发环境和生产环境。开发环境就是用于开发者开发时调试使用的,而生产环境则是针对真实用户使用的。

Webpack 已经内置了区分环境功能,当代码中使用 process 模块的语句时,Webpack 就会自动打包进 process 模块的代码以支持非 Node.js 环境。因此,我们可以直接在代码中这样区分环境。

if(process.env.NODE_ENV === 'production') {
  console.log('生产环境')
} else {
  console.log('开发环境')
}

在 Webpack 4 里,已经支持使用 mode 属性来配置环境。当然也可以如下配置。

const DefinePlugin = require('webpack/lib/DefinePlugin')

module.exports = {
  plugins: [
    new DefinePlugin({
      'process.env': {
        NODE_ENV: JSON.stringify('production')
      }
    })
  ]
}

上述配置中,为什么要使用 JSON.stringify 方法定义?环境变量的值必须是一个由双引号包裹的字符串,即"'production'"。

另外,也可以直接在 Webpack 执行命令上带上环境参数。

webpack NODE_ENV=production

这时候在上面的 new DefinePlugin 中就需要这样去获取环境参数进行定义。

new DefinePlugin({
  'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV)
})

利用UglifyJS对JavaScript压缩,利用cssnano对css压缩

在性能提升方面,相信我们都会知道使用 GZip 对文件进行压缩,那么对于文件内容进行压缩则可以选用 UglifyJS,同时 Webpack 已经内置 UglifyJS。

在这里,可以使用 ParallelUglifyJS 开启多个进程对代码进行压缩来加快构建进程,但是 ParallelUglifyJS 底层都是使用 UglifyJS 进行工作的,所以我们有必要了解一下如何使用 UglifyJS 进行代码压缩。

使用 UglifyJS 对代码进行压缩时,除了可以提升网页加载的速度,还可以混淆源码

UglifyJS 的基本原理就是,分析 Javascript 代码语法树,理解代码的含义,从而做到去掉无效代码、去掉日志输出代码、缩短变量名等优化

UglifyJS 中常用的选项如下:

  • sourceMap:是否为压缩后的代码生成对应的 Source Map,默认不生成,开启后耗时会大大增加。
  • output.beautify:是否要保留空格和制表符,默认为保留,为了达到更好的压缩效果,可设置 false。
  • output.comments:是否保留代码中的注释,默认保留,可设置为 false。
  • compress.warning:是否删除没有用到的输出警告信息,默认为输出,可设置为 false。
  • compress.drop_console:是否删除代码中所有的console语句。
  • compress.collapse_vars:是否内嵌虽然已定义但是只用到一次的变量,如 var x = 5; y = x; 转换成 y = 5,默认为不转换,可设置为 true。
  • compress.reduce_vars:是否提取出现多次但是没定义成变量去引用的静态值。如 x = 1; y = 1; 转换成 var a = 1; x = a; y = a,默认不转换,可以设置为 true。

对于使用 UglifyJs 进行配置如下:

const UglifyJsPlugin = require('webpack/lib/optimize/UglifyJsPlugin')

module.exports = {
  plugins: [
    new UglifyJsPlugin({
      compress: {
        warnings: false,
        drop_console: true,
        collapse_vars: true,
        reduce_vars: true
      },
      output: {
        beautify: false,
        comments: false
      }
    })
  ]
}

Webpack 内置的 UglifyJsPlugin 用的是 UglifyJS2。当执行 webpack --optimize-minimize 命令时,会自动注入 UglifyJSPlugin

需要注意的是,UglifyJS 只理解 ES5 语法的代码,因此一般是需要结合 Babel 一起使用。若只是单纯压缩 ES6 代码,则需要用到 UglifyES

如果需要在 Webpack 接入 UglifyES,则需要单独安装最新版的 uglifyjs-webpack-plugin。

npm i -D uglifyjs-webpack-plugin@beta

// 配置如下
const UglifyEsPlugin = reqire('uglifyjs-webpack-plugin')

module.exports = {
  plugins: [
    new UglifyEsPlugin({
      compress: {
        warnings: false,
        drop_console: true,
        collapse_vars: true,
        reduce_vars: true
      },
      output: {
        beautify: false,
        comments: false
      }
    })
  ]
}

在接入 UglifyES 时,需要 babel-loader 不能转换 ES5 代码,即需要在 .babelrc 中去掉 babel-preset-env

既然 JavaScript 代码可以压缩,那么 css 代码能否也压缩一下?

如果压缩 css 代码,需要用到的压缩工具则是 cssnano,基于 PostCSS 实现的

举个例子,在 css 中,margin: 10px 20px 10px 20px; 会被转换成 margin: 10px 20px; 、color: #000 转换成 color: black;。

那么在 Webpack 中怎么接入 cssnano 呢?

其实在 css-loader 中已经内置了 cssnano,若需开启,则只需要加上 minimize 选项即可

module.exports = {
  module: {
    rule: [
      {
        test: /\.css$/,
        use: ExtractTextPlugin.extract({
          use: ['css-loader?minimize']
        })
      }
    ]
  }
}

配置publicPath指向CDN服务加速

相信大家对 CDN 并不陌生,CDN 可以加速网络传输,通过将资源部署到世界各地,使用户在访问时按照就近原则从离最近的服务器上获取资源,从而加快资源的获取

在项目中常用的 CDN 方式是如下:

  • 对于项目中 HTML 文件,直接放在项目根目录下储存,而不是放到 CDN 服务上,同时需要关闭项目服务器的缓存,项目服务器只提供 HTML 文件和数据接口。
  • 针对打包好的 JavaScript、CSS、图片等静态文件,都放在 CDN 服务上,并开启 CDN 缓存,同时为每个文件带上一个 hash 值,如 a.asvf1231sd.js 文件。带上 hash 原因是,当发版新的代码到生产上,避免用户依旧使用的是旧版代码,每次发版都会更新相应的 hash 值告诉浏览器拉取最新的代码。

由于浏览器有一个规则是,在同一时刻针对同一个域名的资源的并行请求有限制,一般为 4 个左右,不同的浏览器可能不同为了避免将所有的 JavaScript、CSS、图片等静态文件只放在一个服务上,可以尝试将这些静态资源分散到不同的 CDN 服务上。(即 JavaScript 文件放到单独的 js CDN 服务上,CSS 文件放到单独的 css CDN 服务上)

既然如此,Webpack 应该如何接入 CDN 呢?要接入 CDN 服务,需要实现以下几点。

  • 使用 publicPath 指向 CDN 服务的绝对路径地址,而不是指向项目的服务地址上。
  • 静态资源的文件名需要带上相应的 hash 值,避免被缓存。
  • 将不同类似的资源放到不同的 CDN 服务上,避免资源的并行下载限制。

直接看看在 Webpack 中如何配置的。

const path = require('path')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const {WebPlugin} = require('web-webpack-plugin')

module.exports = {
  output: {
    filename: '[name]_[chunkhash:8].js',
    path: path.resolve(__dirname, './dist'),
    // 指定存放JavaScript文件的CDN服务地址
    publicPath: '//js.cdn.com/id'
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ExtractTextPlugin.extract({
          use: ['css-loader?minimize'],
          // 指定存放CSS中导入的资源(如图片)的CDN服务地址
          publicPath: '//img.cdn.com/id'
        })
      },
      {
        test: /\.png$/,
        // 为输入的PNG文件加上Hash值,并指定hash的长度为8位
        use: ['file-loader?name=[name]_[hash:8].text']
      }
    ]
  },
  plugins: [
    new WebPlugin({
      // HTML模板所在位置
      template: './template.html',
      // 输出的HTML文件名
      filename: 'index.html',
      // 指定存放css文件的CDN服务地址
      stylePublic: '//css.cdn.com/id/'
    }),
    new ExtractTextPlugin({
      // 为输出的CSS文件名加上hash值,并指定hash的长度为8位
      filename: '[name]_[contentHash:8]'
    })
  ]
}

使用Tree-Shaking去除多余的无用代码

Tree Shaking 可用于剔除 JavaScript 中用不上的死代码,是基于静态的 ES6 模块化语法的(即依赖于 import 和 export)。

简单滴来说,Tree Shaking 不支持其他模块化如 RequireJS、CommonJS 等。

接下来就来看看,是如何配置 Tree Shaking 的。🤔

首先,需要在 babel 的配置下,禁止 ES6 模块化转换成 ES5 的形式,因此在 .babelrc 文件中配置如下:

{
  "presets": [
    "env",
    {
      "modules": false
    }
  ]
}

上述的 modules: false 的含义就是关闭 babel 的模块转换功能,保留原本的 ES6 模块化语法

从 Webpack 2.0 开始,Webpack 自带 Tree Shaking 功能。另外,在执行 webpack 命令时带上 --display-used-exports 参数,可追踪 Tree Shaking 的工作。

Webpack 只是指出哪些函数被用上,而哪些函数没被用上,以及腰剔除用不上的代码,则还要经过 UglifyJS 处理一番。

在使用大量的第三方库时,会发现 Tree Shaking 似乎并不生效,原因是大部分 NPM 包中的代码都采用了 CommonJS 模块化语法。当然有些库已经考虑到这一点,在发布到 NPM 上时会同时提供两份代码,一份用于 CommonJS 模块化语法,一份采用 ES6 模块化语法,并且在 package.json 文件中会分别指出这两份代码的入口。

以 Redux 为例,在其 package.json 中会这样指明入口:

{
  // ...
  "main": "lib/index.js", // 指明采用CommonJS模块化的代码入口
	"jsnext:main": "es/index.js" // 指明采用ES6模块化的代码入口
  // ...
}

在前面已经提到过,resolve.mainFields 用于配置采用哪个字段作为模块的入口描述,因此,为了让 Tree Shaking 对上面的 Redux 有效,需要配置如下:

module.exports = {
  resolve: {
    // 针对NPM中的第三方模块优先采用jsnext:main中指向ES6模块化语法的文件
    mainFields: ['jsnext:main', 'browser', 'main']
  }
}

采用 jsnext:main 作为 ES6 模块化代码的入口是社区的一个约定。

提取公共代码和第三方代码

当每个页面的代码都将公共的代码包含进去,就会造成:

  • 相同的资源被重复加载,浪费用户的流量和服务器的成本;
  • 每个页面需要加载的资源太大,导致网页首屏加载缓慢,影响用户体验;

需要说明的是,在 Webpack 3.0 中使用插件 CommonChunkPlugin 提取公共部分,在 Webpack 4.0 后则需要在 optimization.splitChunk 中进行配置提取公共部分。在官方文档中提到,虽然 CommonChunkPlugin 可用来避免使用重复的依赖,但若想进一步的优化是不可能的。

通常情况下,会按照以下规则进行提取公共代码。

  • 将项目中所用到的第三方代码统一打包到一个文件中(如vendors.js)。
  • 在剔除第三方代码后,再找出所有页面都依赖的公共部分代码,将它们提取出来并放到一个文件中(如commons.js)。
  • 最后再为每个页面都生成一个单独的文件,在这个文件中不再包含第三方代码以及公共部分代码。

也许你会觉得很奇怪,为什么不可以将第三方代码都放到公共部分代码中去?

其实就是为了长期缓存第三方代码。由于第三方代码是基本不会更新,相反页面依赖的公共部分代码则会根据业务需求的不同而发生变化,因此需区别对待。

现在我们就来看看,在 Webpack 4.0 中是如何进行提取第三方代码和页面依赖的公共部分代码的。

module.exports = {
  // ...
  optimization: {
    splitChunks: {
      chunks: 'initial', // 表示同步模块,有三个值,分别是initial(初始化,打包第三方代码用到)、all(所有块)、async(异步块,按需加载用到) 
      minSize: 30000,  // 块的最小值
      maxSize: 0, // 块的最大值
      minChunks: 1, // 拆分前必须共享模块的最小块数
      maxAsyncRequests: 5, // 按需加载时最大并行请求数
      maxInitialRequests: 3, // 入口点的最大并行请求数
      automaticNameDelimiter: '~', // 默认情况下,webpack将使用块的来源和名称生成名称(如vendors~main.js)
      name: true, // 拆分块的名称,提供true将基于块和缓存组密钥自动生成一个名称
      cacheGroups: { // 缓存模块
        vendors: { // 基本框架
          chunks: 'all',
          test: /(react|react-dom|react-dom-router|babel-polyfill|mobx)/,
          priority: 100,
          name: 'vendors',
        },
        d3Venodr: { // 将体积较大的d3单独提取包,指定页面需要的时候再异步加载
          test: /d3/,
          priority: 100, // 设置高于async-commons,避免打包到async-common中
          name: 'd3Venodr',
          chunks: 'async'
        },
        echartsVenodr: { // 异步加载echarts包
          test: /(echarts|zrender)/,
          priority: 100, // 高于async-commons优先级
          name: 'echartsVenodr',
          chunks: 'async'
        },
        'async-commons': { // 其余异步组件加载包
          chunks: 'async',
          minChunks: 2,
          name: 'async-commons',
          priority: 90,
        },
        commons: { // 其余同步加载包
          chunks: 'all',
          minChunks: 2,
          name: 'commons',
          priority: 80,
        }
      }
    }
  }
}

对于 splitChunks 中的 chunks,需要说明的是:

  • all:不管文件是动态还是非动态载入,统一将文件分离。当页面首次载入会引入所有的包。
  • inital:将异步和非异步的文件分离,如果一个文件被异步引入也被非异步引入,那它会被打包两次(注意和all区别),用于分离页面首次需要加载的包。(第三方代码引入)。
  • async:将异步加载的文件分离,首次一般不引入,到需要异步引入的组件才会引入(即按需引入)。

提取懒加载代码(即按需加载的代码)

在上面使用 splitChunkPlugin 提取代码中,其实已经有提及到对懒加载代码的提取,就是 chunk 值为 async 。

相信用过 React、Vue 的童鞋们,肯定对 import() 方法不陌生,它就是用来实现组件的懒加载的。

import() 返回一个 Promise,依赖于 Promise。下面就使用 React-Router 实现组件懒加载栗子。

import React, {PureComponent, createElement} from 'react'
import ReactDom from 'react-dom'

function getAsyncComponent(load) {
  return class AsyncComponent extends PureComponent {
    componentDidMount() {
      load().then({ default: component } => {
        this.setState({
          component
        })
      })
    }
    render() {
      const {component} = this.state
      return component ? createElement(component) : null
    }
  }
}

function App() {
  return (
  	<HashRouter>
    	<div>
    		<nav><link to="/about">About</link></nav>
    	</div>
      <Route path="/about" component={getAsyncComponent(() => import(/* webpackChunkName: about */ './pages/about'))}></Route>
    </HashRouter>
  )
}

以上代码需要在 Webpack 中配置好相应的 babel-loader,将源码先提交给 babel-loader 处理,再提交给 Webpack 处理其中的 import(*) 语句。但是由于 babel-loader 并不认识 import(*) 语句,因此会报错。

为了让 babel-loader 处理 import(*) 语句,需要安装一个 babel 插件 babel-plugin-syntax-dynamic-import。需在 .babelrc 文件做如下配置。

{
  "presets": [
    "env", "react"
  ],
  "plugins": [
    "syntax-dynamic-import"
  ]
}

开启Scope Hoisting

Scope Hoisting 可以让 Webpack 打包出来的代码文件更小、运行更快,又称为作用域提升。

Scope Hoisting 实现的基本原理是,分析模块之间的依赖关系,尽可能将被打散的模块合并到一个函数中,但前提是不能造成代码冗余

那么要开启 Scope Hoisting,在 Webpack 中如何配置呢?只需要用到内置的 ModuleConcatenationPlugin 插件即可开启 Scope Hoisting

const ModuleConcatenationPlugin = require('webpack/lib/optimize/ModuleConcatenationPlugin')

module.exports = {
  plugins: [
    new ModuleConcatenationPlugin()
  ]
}

需要注意的是,Scope Hoisting 依赖源码时需采用 ES6 模块化代码,还需配置 resolve.mainFields。因为大部分 NPM 包的第三方库都是采用 CommomJS 语法,因此需配置 resolve.mainFields 指向使用 ES6 模块化代码。

module.exports = {
  resolve: {
    mainFields: ['jsnext:main', 'browser', 'main']
  }
}

当然,对于采用了非 ES6 模块化的代码,Webpack 则会降级处理且不使用 Scope Hoisting 进行优化。

WebSocket 与 Socket

WebSocket 是 H5 引入的全双工通信方式,让服务端主动推送信息成为了可能。那么下面就来简单总结一下 WebSocket 与 Socket。

WebSocket 通信

WebSocket 是基于 TCP 连接的全双工方式所实现的持久化协议。

那么,WebSocket 在建立连接过程中,需要经历两次握手。分别是:

  • 客户端通过 new WebSocket(url, [ protocol ]) 向服务端发出一个连接连接的请求。请求中会带上两个请求头部,分别是 Ugrade 和 Connection 字段,其中 Ugrade 字段用于表示请求所使用的协议,Connection 则是结合 Ugrade 使用。
  • 服务端收到请求后,就会重新向客户端返回响应内容,响应头会带上一个很重要字段,就是 Sec-WebSocket-Protocol,客户端收到响应内容后,就会检查该字段的合法性,一旦通过就会正式建立连接。

WebSocket 中的 API 主要就是 onopen、onmessage、onclose、onerror 和 send。

建立连接。

var Socket = new WebSocket(url, [protocol] )

当客户端和服务端成功建立连接时,会触发 onopen 事件。

Socket.onopen = function(e) {}

当建立连接不成功时,会触发onerror事件。

Socket.onerror = function(e) {}

当客户端与服务端之间的 WebSocket 连接关闭时,会触发 onclose 事件。

Socket.onclose = function(e) {}

客户端使用 send 向服务端发送信息,使用 onmessage 监听事件。

Socket.send(...)
Socket.onmessage = function(e) {}

Socket 通信

提到 Socket,就不得不提一下 Socket 与 WebSocket 之间区别,那么它们两者之间有什么区别呢?

Socket 并不是一个协议,而是为了方便使用 TCP 或 UDP 而抽象出来的一个层,是位于应用层和传输层之间的一组接口而 WebSocket 是基于 HTTP 协议所编写的,那么 WebSocket 是应用层协议

那么,有人会提问,好像还听说过 Socket.io ,这个又是什么鬼?

其实,Socket.io 是对 WebSocket 的封装,原理其实就是对于高版本浏览器会使用 WebSocket 通信,而对于低版本浏览器则会使用长轮询进行兼容

借用一下别人图。

当然,如果想深入的话,可以看看别人分享的好文。

[基于socket.io快速实现一个实时通讯应用](https://segmentfault.com/a/1190000018944634)

Review-Question-Network

  1. HTTP 常见的请求方式?以及状态码?
  2. get和post在缓存方面有何区别?
  3. 499状态码是什么?应该如何解决?
  4. 讲讲https工作原理
  5. 讲讲https的对称加密算法和非对称加密算法
  6. 谈谈 TCP 三次握手和四次挥手
  7. TCP 与 UDP 区别?
  8. A、B 机器正常连接后,B 机器突然重启,此时 A 处于 TCP 什么状态?
  9. 说说 Websocket
  10. 说说cookie、session和token?
  11. http 与 https 区别在哪?
  12. https 握手过程中,客户端是如何验证数字证书的合法性的?
  13. 说一下 http2 的多路复用
  14. 常见的 web 安全问题有哪些?
  15. 为什么传统上利用多个域名来提供网站资源会更有效?
  16. long polling、websocket、sse 间有何区别?
  17. DNS 解析过程
  18. CDN 原理
  19. TCP 使用什么手段保证可靠传输?
  20. 什么是流量劫持?
  21. http2 将如何影响 js 应用程序打包?
  22. OSI 七层模型
  23. 说说正向代理以及反向代理

HTTP 常见的请求方式?以及状态码?

请求方式

  • get:通过地址访问页面均属于get请求。
  • post: 表单提交。
  • head: 和get类似,只获取请求头。
  • put:添加资源。
  • delete:删除资源。
  • connect:多用于https和websocket。
  • options:当前端使用xhr或fetch方法请求一个跨域资源时,若是非简单请求,浏览器都会自动帮你先出一个预检请求,对应请求方式就是options,如果请求对服务器是安全的,返回204。因此options一般用于取人header响应。

状态码

  1. 1xx。临时回应。
    • 100 Continue 继续,一般在发送post请求时,服务端将返回此信息表示确认,之后发送具体的参数信息。
  2. 2xx。请求成功。
    • 200 OK 正常饭回信息。
    • 201 Created 请求成功且服务器创建新的资源。
    • 202 Accepted 服务器接受请求,但尚未处理。
    • 204 Options 跨域请求预检安全
  3. 3xx。请求目标有变化。
    • 301 Moved Permanently 请求网页已永久移到新位置。
    • 302 Found 临时性重定向。(和301功能相同,都是重定向,但是301的旧地址已经不再存在被改为新地址,而302的旧地址则还在,只是临时跳转而已)。
    • 303 See Other 临时性重定向。
    • 304 Not Modified 自上次修改后,请求的资源未修改过
  4. 4xx。请求错误。
    • 400 Bad Request 请求数据的格式不符合要求。
    • 401 Unauthorized 未授权。
    • 403 Forbidden 禁止访问。
    • 404 Not Found 无法访问。
  5. 5xx。服务端错误。
    • 500 Internal Server Error 服务端错误。
    • 502 Bad Gateway 连接超时。由于服务器当前连接太多,无法给予正常响应,请等待。
    • 503 Service Unavailable 服务器暂时无法处理请求。(可能是维护)

get和post在缓存方面有何区别?

get用于获取某个资源,而post则是用于修改或删除的工作

get 获取某个资源可无需每次都与数据库进行连接,因此可缓存。

post 由于是修改或删除资源,因此每次都必须与数据库进行交互,所以不能使用缓存。

另外,我们常说get请求参数长度有限制,而post请求参数长度不受限制。get的请求参数长度有限制不是http规定的,而是由浏览器和服务器共同决定


499状态码是什么?应该如何解决?

499状态码

含义:客户端请求等待连接已经关闭。

原因:由于服务器长时间不返回响应内容,客户端不想等待。还有一种原因是两次提交post过快。

解决方法:

  • 计时器setTimeout设置大点。
  • nginx配置proxy_ignore_client_abort on。

讲讲https工作原理

https://raw.githubusercontent.com/Andraw-lin/FE-Knowledge-Summary/master/asset/https%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86%E5%9B%BE.png

https 有两部分组成:http 协议和 SSL/TSL 协议。SSL/TSL 协议负责加密解密等安全处理的模块。

https四次握手过程

  1. 浏览器向服务器443端口发起请求,并带上浏览器自身支持的加密算法和hash算法。
  2. 服务器使用请求中加密算法和hash算法,将处理好的数字证书返回给浏览器,数字证书中包含网站地址、加密公钥、证书颁发机构等机构。
  3. 浏览器获得数字证书后,会认证其合法性(颁发机构是否合法、证书中网站地址是否和访问地址一致等等)。若证书是合法的,在浏览器地址栏会显示一个锁头。然后浏览器就会生成一串随机数,并使用证书中的公钥加密。
  4. 浏览器将加密好的随机数再次发送到服务器中。
  5. 服务器收到请求后,使用私钥对随机数进行解密,从而获得随机数。然后以随机数作为密钥使用对称加密算法加密网页(所谓对称,就是将信息和私钥通过某种算法混合在一起),并返回到浏览器中。
  6. 浏览器以随机数为密钥并使用之前约定好的解密算法获取网页内容。

讲讲https的对称加密算法和非对称加密算法

对称加密

含义:接收方和发送方拥有同一把密钥,发送方和接收方加密解密都是使用该把密钥。

好处:对称加密相比非对称加密拥有更高的加解密速度,因为双方都事先知道密钥。

坏处:密钥在传输的过程中可能会被窃取,安全性并没有非对称加密高。

非对称加密

含义:接收方在收到信息前事先创建公钥和私钥,然后将公钥发送到发送方,发送方收到公钥后,处理好数据并使用公钥进行加密,然后再次发送到接收方,接收方使用私钥解密数据进而获取数据。

好处:公钥负责加密,私钥负责解密,而且都是通过接收方产生,发送方并不了解这一切。传输过程中,攻击者无法获得私钥,就算截取到公钥也是没办法对数据进行破解,尤其安全。

坏处:解密都需要统一在一方进行,因此会导致加解密速度效率低于对称算法。

概括:对称加密是双方都拥有加密解密功能,加解密效率高。非对称加密是加密解密规则都是一方产生,然后把加密公钥发到其他人,解密私钥自己保存,安全性更高。


谈谈 TCP 三次握手和四次挥手

TCP 三次握手是建立连接时,四次挥手是断开连接时。

TCP 三次握手

TCP建立连接-三次握手

  • 连接开始时,发送方会发送一个同步标记位 SYN 到接收方,并包含本身的初始序号 a。
  • 接收方收到后,会回复一个同步标记位 SYN 到发送方,并包含对应发送方初始序号的确认标记位 ACK = a + 1,以及初始序号 b。
  • 发送方收到确认信息后,就会发送一次确认标记位 ACK = b + 1 到接收方。随后进入连接建立阶段,最后一次握手可携带数据

TCP 四次挥手

TCP关闭连接-四次挥手

  • 发送方发送一个标记位 FIN 到接收方,包含初始序号 a,并停止发送数据。
  • 接收方收到关闭请求后,会发送一个确认标记位 ACK = a + 1,包含初始序号 b。发送方收到确认后,就会进入等待接收方的关闭请求阶段。
  • 接收方发现没有数据再发到发送方后,就会发出一个标记位 FIN 到发送方,包含初始序号为 c。
  • 发送方收到关闭请求后,会发送一个确认标记位 ACK = c + 1,包含初始序号 d,然后进入等到关闭状态。接收方收到确认后,就会进入关闭状态,大概4分钟后,发送方也会进入关闭状态。

问题:

  1. TCP 建立连接是三次握手,而不是两次或四次?

    若是两次握手,接收方就无法知道发送方是否已经收到确认信号,导致其初始序号无法达成一致,这时候就不会打开连接。另外也是为了避免已失效的报文发到接收方而导致服务出错。

    若是四次握手,那么接收方会将同步位和确认位分开传递,只会导致速度和效率下降,因此合并会提高速度和效率。

  2. TCP 关闭连接是四次挥手,而不是两次?

    首先 TCP 是全双工模式,一方收到 FIN 标记位就意味着另一方不会在发送信息过来,但是本身还是能进行发送信息。


TCP 与 UDP 区别?

  1. TCP 是面向连接的。UDP 是面向无连接的,即发送数据前不需要先建立连接。
  2. TCP 提供可靠传输(即传送的数据是无差错、不丢失、不重复且按序到达),适合大数据量的交换。UDP 是尽最大的努力交付,即不保证可靠交付。
  3. TCP 面向字节流。UDP 面向报文,在网络拥塞时不会使得传播速度降低,因此适合网络电话和视频会议。
  4. TCP 只能是一对一的。UDP 支持一对一,一对多。
  5. TCP 首部由20个字节。UDP 则只有8个字节。

A、B 机器正常连接后,B 机器突然重启,此时 A 处于 TCP 什么状态?

服务器与客户建立连接后

若服务器进程终止。则服务器会发送 FIN 标志位到客户进行关闭。

若服务器主机奔溃。则可能会发生以下情况:

  • 若服务器不重启,客户继续工作,就会发现服务器并没有回应,路由器聪明的话,则会提示客户目的地不可大。
  • 若服务器重启,客户继续工作,然而服务已丢失客户信息,收到客户数据后响应重置位 RST,来让服务器与客户重新建立连接。

说说 Websocket

websocket 是一个持久化协议,基于 http,服务端可以主动 push。

兼容的方法有

  • 长轮询

    setInterval 定时发送 ajax,缺陷就是会有很多无用的请求。

  • 长连接

    客户端向服务端发送请求,服务器收到请求后会保持住连接,直到有消息时才返回响应信息。

    async function subscribe() {
      let response = await fetch("/subscribe");
    
      if (response.status == 502) {
        // 连接超时错误,
        // 当连接挂起太长可能会发生,远程服务器或者代理会关闭它
        // 重新连接
        await subscribe();
      } else if (response.status != 200) {
        // 显示错误
        showMessage(response.statusText);
        // 1 秒后重连
        await new Promise(resolve => setTimeout(resolve, 1000));
        await subscribe();
      } else {
        // 得到消息
        let message = await response.text();
        showMessage(message);
        await subscribe();
      }
    }
    
    subscribe();
  • Server-Sent Event(即SSE)

    单工模式,只允许服务端向客户端发送信息,而 WebSocket 则是双工模式,使用上没啥区别。

    if ('EventSource' in window) {
      var source = new EventSource(url, { withCredentials: true });
    
      /* open事件回调函数 */
      source.onopen = function(){ 
        console.log('SSE通道已建立...');
      };
    
      /* message事件回调函数 */
      source.onmessage = function(evt){
        console.log(evt.data);
      }
    
      /* error事件回调函数 */
      source.onerror = function(evt){
        console.log('SSE通道发生错误');
      }
    
      /* 自定义事件回调 */
      source.addEventListener('foo', function (event) {
        var data = event.data;
        // handle message
      },false);
    
      /* 关闭SSE */
      source.close()
    }

用法有:new WebSocket(url)ws.onerrorws.onclosews.onopenws.onmessagews.send


说说cookie、session和token?

http 是无状态的,为了能辨别身份,才有了 cookie 和 session 出现,而 cookie 只是实现的 session 的一种方案。

  • 客户端发送请求到服务器。
  • 服务器创建一个 session,session 中存储了用户角色、登陆时间等等。
  • 服务器向客户端返回一个 sessionid,写入客户端的 cookie。
  • 客户端接下来的每次请求都会默认带上 cookie ,将 sessionid 传回服务器。
  • 服务器收到 sessionid,会找到前期保存的数据,由此得知用户的身份。

session 缺陷:多个系统下 session 无法共享,解决方案有 session 全局复制、使用 nginx 将请求的 IP 映射到对应的机器上、把 session 数据放在一个 Redis 中。

token 是客户端和服务器间进行传递的一段字符串。服务端不再保存 session 状态,全部由客户端进行保存,token 就相当于一个通行证。

  • 客户端发送请求到服务器。
  • 服务返回一个经过加密的token,并由客户端负责存储,可存到 local storage 或 cookie。
  • 客户端接下来的每次请求,都需要手动加上该 token。
  • 服务器对 token 进行解码,如果 token 有效,则处理请求。
  • 一旦用户登出,客户端就需要销毁 token。

token 可有效防护 CSRF 攻击。


http 与 https 区别在哪?

  1. https 协议需要到 ca 申请证书,一般是需要收费的。
  2. http 是超文本传输协议,信息是明文传输的(即不加密)。https 是具有安全性的 ssl 加密传输协议。
  3. http 和 https 是使用完全不同的连接方式(即建立连接握手过程),用的端口也不同,前者是 80,后者是 443。
  4. https 是 ssl + http 协议构建的可加密传输、身份验证的网络协议,比 http 协议安全。

https 握手过程中,客户端是如何验证数字证书的合法性的?

客户端会有一个有效证书串,一般的浏览器都会内置很多常见服务器的证书,特殊服务器就需要前期通过手工将证书添加到客户端。


说一下 http2 的多路复用

Http2 采用的是二进制格式传输,取代了 http1.x 的文本格式,因为二进制格式解析更高效。

http1.x 中,并发多个请求需要多个 TCP 连接,浏览器为了控制资源会有 6-8 TCP连接限制。

Http2 中,同域名下所有通信都在单个连接上完成,消除了因多个 TCP 连接而带来的延时和内存消耗,而且单个连接上可并行交错请求和响应,之间互不干扰


常见的 web 安全问题有哪些?

  1. XSS

    • 非持久型 XSS(反射型XSS)

      原理:攻击者构造一个恶意 URL 链接诱导用户点击,服务器收到 URL 对应的请求并没有对 URL 参数(包含恶意脚本)进行过滤就拼接到html页面发给浏览器,浏览器解析执行。

    • 持久型 XSS(存储型XSS)

      原理:一般出现在表单功能中,如文章留言、提交文本信息等。攻击者将恶意代码发送到服务器(如在留言中使用恶意代码),服务没进行过滤就直接保存在数据库,下次再请求时读取相关内容(会执行恶意脚本),将恶意内容拼接到html中返回给浏览器。

    避免方案:

    • 转义字符。对用户任何输入都要进行过滤。
    • 设置cookie为http-only(也是防护xss窃取cookie最有效方式)。
  2. CSRF

    原理:利用用户身份做恶意操作。用户在正规网站上登录成功并存储身份标识到cookie中,若此时没退出,再访问恶意网站,恶意网站中有个请求访问正规网站中的接口,由于标识会是自动带上到请求中,这时候就巧妙利用cookie自动被带上请求漏洞伪造用户身份做恶意操作。

    避免方案:

    • 设置cookie的samesite属性(该属性表示cookie不能跨域)。
    • referer checked。(检查请求是从哪里跳转过来的)。
    • 使用token回话机制。
    • 加入验证码机制。
  3. URL 跳转漏洞

    原理:巧妙利用网站中链接参数中跳转参数(类似igloa的previousurl),诱导用户点击,当用户操作后,由于会回到原来这个参数链接,这时候就会去到恶意网站,进一步操作获取用户信息,就会导致XSS。

    避免方案:referer checked或限制。

  4. SQL 注入

    原理:在表单中输入恶意SQL语句,到服务器中进行解析。

    避免方案:

    • 服务端检查输入数据是否符合预期。
    • 转义字符。

为什么传统上利用多个域名来提供网站资源会更有效?

  1. CDN 缓存更方便。
  2. 突破浏览器的并发控制。
  3. 避免同域携带cookie问题,安全隔离,节省带宽。

关于多域名,也不是说越多越好,因为浏览器做dns解释是需要耗费时间,若走https还需要证书娇艳和部署问题。因此一定要适量增加域名


long polling、websocket、sse 间有何区别?

  1. 兼容性上,长轮询兼容性最好,而sse仅支持ie9+。
  2. 服务器负载上,长轮询会占据一部分cpu资源,而 sse 和 websocket 则不会创建。
  3. 客户端负载上,长轮询使用异步方式,而 sse 和 websocket 则是会花费一小部分资源,都是采用浏览器的内置实现方式。
  4. 时间线上,长轮询接近实时,sse 一般会延时3秒但可调整,websocket肯定是实时的

DNS 解析过程

举个🌰,主机m.xyz.com打算查询主机y.abc.com的内容,具体查询过程如下:

  1. 主机m.xyz.com先向本地域名服务器dns.xyz.com进行递归查询(使用一个UDP用户数据包的报文);
  2. 当本地域名服务器dns.xyz.com无法找到对应的IP地址时(在其映射表中获高速缓存中查询),会向根域名服务器进行发出DNS查询请求,采用的是迭代查询(使用一个UDP用户数据包的报文);
  3. 根域名服务器告诉本地域名服务器应该去找顶级域名服务器dns.com进行查询(使用一个UDP用户数据包的报文);
  4. 本地域名服务器向顶级域名服务器dns.com发出一个DNS查询请求(使用一个UDP用户数据包的报文);
  5. 当顶级域名服务器dns.com无法找到对应的IP地址时(在其映射表中获高速缓存中查询),会告诉本地域名服务器应该找权限服务器dns.abc.com进行查询(使用一个UDP用户数据包的报文);
  6. 本地域名服务器向权限域名服务器dns.abc.com发出一个DNS查询请求(使用UDP用户数据包的报文);
  7. 权限域名服务器dns.abc.com找到对应的IP地址,并返回给本地域名服务器(使用一个UDP用户数据包的报文);
  8. 本地域名服务器收到权限域名服务器返回的IP地址后,会把结果返回给主机m.xyz.com(使用一个UDP数据包的报文);

CDN 原理

  1. 用户主机先想本地域名服务器发送DNS查询IP请求;
  2. 本地域名服务器根据域名从右向左开始解析,先向根域名服务器发送DNS查询IP请求(先假设在本地域名服务器缓存中该DNS查询记录已过期);
  3. 若根域名服务器找不到,会直接返回一个域名权限DNS记录到本地域名服务器;
  4. 本地域名服务器根据域名权限DNS记录寻找对应权限域名服务器,并发出DNS查询IP请求;
  5. 权限域名服务器若发现该域名已开启CDN服务,则会返回一个CNAME给本地域名服务器;
  6. 本地域名服务器根据这个CNAME去访问智能调度DNS服务器进行查询IP地址;
  7. 智能调度DNS服务器会按照一定的算法和策略,将最靠近用户主机的CDN节点对应资源的IP地址返回给本地域名服务器;
  8. 本地域名服务器将CDN节点对应资源的IP地址直接交给用户主机;
  9. 用户主机得到对应IP地址后,可直接访问较近的节点服务器上对应的资源;
  10. CDN服务器会返回对应资源给用户主机;

cdn也是先沿用dns解析过程,在最后一步发现是开启cdn才会进行下一步处理。


TCP 使用什么手段保证可靠传输?

  1. 将数据截断成合理的长度。
  2. 超时重发。
  3. 校验包有错就会丢掉报文段,不给出响应,超时重发数据。
  4. 对于重复数据会丢弃重复数据。
  5. 流量控制。

什么是流量劫持?

流量劫持是一种很古老攻击方式,通过恶意修改浏览器、锁定主页或不停弹出窗口,强制用户访问某些网站,从而导致用户流量损失。

实现劫持手段有:

  • 域名劫持:劫持域名的DNS解析结果,将HTTP请求劫持到特定的IP上,使得客户端与攻击者服务器连接。
  • 直接流量修改:恶意插入内容,如广告弹窗等。

阻止手段:

  • 开启HTTPS。
  • 限制网站权限,利用服务器安全设置来提高网站的安全性。
  • 新增httpdns解析服务器来获取dns。

http2 将如何影响 js 应用程序打包?

从多路复用入手。

目前模块中改动一行代码都需要重新下载整个打包后的文件,而http2多路复用可以有效地把代码分割到小模块中,更好滴利用缓存,确保我们有效地利用用户的带宽。


OSI 七层模型

OSI 七层模型

  • 应用层;
  • 表示层;
  • 会话层; // 其中应用层, 表示层, 会话层对应五层结构和 TCP/IP 四层结构的应用层
  • 运输层;
  • 网络层;
  • 数据链路层;
  • 物理层;

TCP/IP 四层机构

  • 应用层; // 包含有TELNET, TCP, SMTP等;
  • 运输层; // TCP 或 UDP;
  • 网际层IP;
  • 网络接口层;

五层结构

  • 应用层; // 通过应用进程间的交互来完成特定网络应用;
  • 运输层; // 向两个主机中进程之间的通信提供通用的数据传输服务;
  • 网际层; // 为分组交换网上的不同主机提供通信服务, 分组也称为IP数据包;
  • 数据链路层; // 将IP数据包组装成帧, 在两个相邻节点间的链路上传送帧 不仅要检错, 而且还要纠错;
  • 物理层; // 所传的数据单位是比特;

说说正向代理以及反向代理

正向代理:一个位于客户端和目标服务器之间代理服务器。为了从原始服务器取得内容,客户端向代理服务器发送一个请求,并且指定目标服务器之后代理向目标目标服务器转交并获得内容返回到客户端,例如ss。

反向代理:对客户端来说,反向代理就好像目标服务器,并且客户端无需任何设置。客户端向反向代理服务器发送请求,接着反向代理判断请求走向对应地方,对于客户端来说,代理服务器后面的服务都是不需要知道的,只需要把代理服务器当成真正的服务器即可。

https://raw.githubusercontent.com/Andraw-lin/FE-Knowledge-Summary/master/asset/%E6%AD%A3%E5%90%91%E4%BB%A3%E7%90%86%E5%92%8C%E5%8F%8D%E5%90%91%E4%BB%A3%E7%90%86.png

正向代理和反向代理区别

  • 正向代理需客户端主动设置代理服务器IP或域名访问和服务器ip去获取相应内容。反向代理无需客户端任何操作设置,直接访问代理服务器ip即可获得相应的内容,但代理服务器内部自动根据访问内容进行跳转的细节对于客户端来说都是未知的。
  • 正向代理是代理服务器为客户端收发请求。反向代理是代理服务器为服务器收发请求。
  • 在用途上,正向代理是为了在防火墙内的局域网提供访问外部网络的途径。反向代理则是将防火墙后面的服务器提供给外部用户访问。

最主要的区别就是是否指定具体的目标服务器、客户端是否做设置。

使用场景上

  • 正向代理一般用在如fanqiang上。
  • 反向代理则是用处多多,可用于保护原始服务器、负载均衡、外网发布等功能。负载均衡是当反向代理服务器不止一个时,甚至把它们做成集群,当更多的客户端访问服务器时,让不同代理服务器应答不同的用户,并返回响应内容。

Nginx作为时下最流行的HTTP服务器之一,同时它是一个反向代理服务器,也是反向代理服务器。Nginx就是性能非常好的反向代理服务器,用来做负载均衡

常见树算法

对树数据结构常见的算法进行总结,也为了更好滴应对后面深入学习算法。

中序遍历

  • 递归实现。

    答案
    
      const midMap1 = (node, result) => {
        if (node) {
          midMap1(node.left, result);
          result.push(node.val);
          midMap1(node.right, result);
        }
        return result;
      }
      
  • 非递归实现。

    答案
    
      const midMap2 = node => {
        const result = [];
        const stack = [];
        let currentNode = node;
        while (currentNode || stack.length) {
          while (currentNode) {
            stack.push(currentNode);
            currentNode = currentNode.left;
          }
          currentNode = stack.pop();
          result.push(currentNode.val);
          currentNode = currentNode.right;
        }
      }
      

前序遍历

  • 递归实现。

    答案
    
      const qianMap1 = (node, result) => {
        if (node) {
          result.push(node.val);
          qianMap1(node.left, result);
          qianMap1(node.right, result);
        }
        return result;
      }
      
  • 非递归实现。

    答案
    
      const qianMap2 = node => {
        const result = [];
        const stack = [];
        let currentNode = node;
        while (currentNode || stack.length) {
          while (currentNode) {
            result.push(currentNode.val);
            stack.push(currentNode);
            currentNode = currentNode.left;
          }
          currentNode = stack.pop();
          currentNode = currentNode.right;
        }
      }
      

后序遍历

  • 递归实现。

    答案
    
      const houMap1 = (node, result) => {
        if (node) {
          houMap1(node.left, result);
          houMap1(node.right, result);
          result.push(node.val);
        }
        return result;
      }
      
  • 非递归实现。

    答案
    
      const houMap2 = node => {
        const result = [];
        const stack = [];
        let currentNode = node;
        let pre = null;
        while (currentNode || stack.length) {
          while (currentNode) {
            stack.push(currentNode);
            currentNode = currentNode.left;
          }
          currentNode = stack[stack.length - 1];
          if (!currentNode.right || currentNode.right === pre) {
            currentNode = currentNode.pop();
            pre = currentNode;
            result.push(currentNode.val);
            currentNode = null;
          } else {
            currentNode = currentNode.right;
          }
        }
      }
      

重建二叉树

已知前序遍历和中序遍历,返回二叉树

答案

  const resetTree = (pre, mid) => {
    if (pre.length === 0) return null;
    if (pre.length === 1) return new Node(pre[0]);
    const root = pre[0];
    const index = mid.indexOf(root);
    const preLeft = pre.slice(1, index + 1);
    const preRight = pre.slice(index + 1);
    const midLeft = mid.slice(0, index);
    const midRight = mid.slice(index + 1);
    root.left = resetTree(preLeft, midLeft);
    root.right = resetTree(preRight, midRight);
    return root;
  }
  

已知前序遍历和中序遍历,返回后序遍历

答案

  const returnHouMap = (pre, mid, result = []) => {
    if (pre.length === 0) return null;
    if (pre.length === 1) return pre[0];
    const root = pre[0];
    const index = mid.indexOf(root);
    const preLeft = pre.slice(1, index + 1);
    const preRight = pre.slice(index + 1);
    const midLeft = mid.slice(0, index);
    const midRight = mid.slice(index + 1);
    const leftNode = returnHouMap(preLeft, midLeft, result);
    const rightNode = returnHouMap(preRight, midRight, result);
    result.push(leftNode, rightNode, root);
    return result;
  }
  

对称二叉树

判断一颗树是否为对称的

答案

  const isDuiChen = root => {
    if (root) {
      return isSameNode(root.left, root.right);
    }
    return false;
  }
  const isSameNode = (aNode, bNode) => {
    if (!aNode && !bNode) return true;
    if (!aNode || !bNode) return false;
    if (aNode.val !== bNode.val) return false;
    return isSameNode(aNode.left, bNode.right) && isSameNode(aNode.right, bNode.left);
  }
  

二叉树的镜像

将一颗二叉树转换成其镜像返回

答案

  const returnJingXiang = root => {
    if (!root) return root;
    returnJingXiang(root.left);
    returnJingXiang(root.right);
    const leftNode = root.left;
    const rightNode = root.right;
    root.left = rightNode;
    root.right = leftNode;
    return root;
  }
  

二叉搜索树的第K个节点

给定一个二叉搜索树,找出其中的第K小的节点

答案

  const findKNode = (root, k) => {
    const stack = [];
    let currentNode = root;
    while (currentNode || stack.length) {
      while (currentNode) {
        stack.push(currentNode.left);
        currentNode = currentNode.left;
      }
      currentNode = stack.pop();
      k--;
      if (k === 0) return currentNode.val;
      currentNode = currentNode.right;
    }
    return null;
  }
  

二叉搜索树的后序遍历

输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历结果

答案

  const isHouMap = arr => {
    if (arr && arr.length) {
      const root = arr[arr.length - 1];
      let i = 0;
      for (; i < arr.length - 1; i++) {
        if (arr[i] > root) break;
      }
      let j = i;
      for (; j < arr.length - 1; j++) {
        if (arr[j] < root) return false;
      }
      let left = true;
      if (i > 0) {
        left = isHouMap(arr.slice(0, i));
      }
      let right = true;
      if (j < arr.length - 1) {
        right = isHouMap(arr.slice(i, j));
      }
      return left && right;
    }
    return false;
  }
  

二叉树的最大深度

给定一个二叉树,找出其最大深度

答案

  const getMaxDepth = root => {
    if (!root) return 0;
    return Math.max(getMaxDepth(root.left) + 1, getMaxDepth(root.right) + 1);
  }
  

二叉树的最小深度

给定一个二叉树。找出其最下深度

答案

  const getMinDepth = root => {
    if (!root) return 0;
    if (!root.left) return 1 + getMinDepth(root.right);
    if (!root.right) return 1 + getMinDepth(root.left);
    return Math.min(getMinDepth(root.left), getMinDepth(root.right)) + 1;
  }
  

平衡二叉树

输入一颗二叉树,判断二叉树是否为平衡二叉树

答案

  const ispingHeng = root => {
    if (!root) return true;
    return [0, 1].includes(Math.abs(getMaxDepth(root.left) - getMaxDepth(root.right)));
  }
  

二叉树中和胃某一值的路径

输入一颗二叉树的根结点和一个整数,打印出二叉树中结点值的和为输入整数的所有路径。

路径定义为从树的根结点开始往下已知到叶结点所形成的一条路径。

答案

  const getPath = (root, num) => {
    const result = [];
    if (root) handlePath(root, num, result, [], 0);
    return result;
  }
  const handlePath = (node, num, result, stack, sum) => {
    stack.push(node.val);
    sum += node.val;
    if (!node.left && !node.right && sum === num) result.push([...stack]);
    if (node.left) handlePath(node.left, num, result, stack, sum);
    if (node.right) handlePath(node.right, num, result, stack, sum);
    stack.pop();
  }
  

二叉搜索树与双向链表

输入一颗二叉搜索树,将该二叉搜索树转换成一个排序的双向链表

要求不能创建任何新的结点,只能调整树中结点指针的指向。

答案

  const changeTree = root => {
    if (root) {
      let currentNode = root;
      handleChangeTree(root, null);
      while (currentNode) {
        currentNode = currentNode.left;
      }
      return currentNode;
    }
    return null;
  }
  const handleChangeTree = (node, pre) => {
    if (node.left) {
      pre = handleChangeTree(node.left, pre);
    }
    node.left = pre;
    if (pre) {
      pre.right = node;
    }
    pre = node;
    if (node.right) {
      pre = handleChangeTree(node.right, pre);
    }
    return pre;
  }
  

序列化二叉树

请实现两个函数,分别用来序列化和反序列化二叉树

答案

  const serize = node => {
    const result = [];
    if (!node) {
      result.push(null);
    } else {
      result.push(node.val);
      serize(node.left);
      serize(node.right);
    }
    return result;
  }
  const deserize = arr => {
    if (!arr.length) return null;
    const root = arr.shift();
    if (root !== null) {
      const node = new Node(root.val);
      node.left = deserize(arr);
      node.right = deserize(arr);
      return node;
    }
    return null;
  }
  

二叉树的下一个节点

给定一个二叉树中的一个节点,请找出中序遍历顺序的下一个节点并且返回。

注意树中的节点不仅仅包含左右节点,同时还包含指向父节点的指针

答案

  const getNextNode = target => {
    if (!target) return null;
    if (target.right) {
      let node = target.right;
      while (node.left) {
        node = node.left;
      }
      return node;
    } else {
      while (target) {
        if (!target.next) {
          return null;
        }
        if (target === target.pre.left) {
          return target;
        }
        target = target.next;
      }
      return target;
    }
  }
  

树的子结构

输入两颗二叉树A、B,判断B是不是A的子结构。

约定空树不是任何树的子结构。

答案

  const isChildTree = (aNode, bNode) => {
    let result = false;
    if (aNode && bNode) {
      if (aNode === bNode) {
        result = isSameTree(aNode, bNode);
      }
      if (!result) {
        result = isChildTree(aNode.left, bNode);
      }
      if (!result) {
        result = isChildTree(aNode.right, bNode);
      }
    }
    return result;
  }
  const isSameTree = (aNode, bNode) => {
    if (bNode === null) return true;
    if (aNode === null || aNode !== bNode) return false;
    return isSameTree(aNode.left, bNode.left) && isSameTree(aNode.right, bNode.right);
  }
  

前端中常见的算法题

本文主要总结一下常见的算法题,注重积累,所谓厚积薄发啦。🤔

找出数组中重复次数最多的元素

栗子:

[1, 2, 3, 1, 5, 6] 重复次数最的元素为 1

中心思路:

新创建一个对象,开始遍历数组,每次都判断当前遍历的元素是否在对象中,有就将对象相应的元素值 +1,若不存在则初始化其值为1,另外需要两个变量分别保存当前出现最多的次数值和出现次数最多的元素,一旦在遍历过程中大于该保存次数,则直接把该次数重新赋予在变量中以及属性变量中。

水仙花数

先简单说明一下什么是水仙花数,其实就是一个数字有 N 位数,那么这个数字每一位数字的 N 次方和就等于本身。

栗子:

153 = 1^3 + 5^3 + 3^3

370 = 3^3 + 7^3 + 0^3

1634 = 14^4 + 64^4 + 34^4 + 44^4

判断一个数是否为水仙花数思路:

直接将一个数 A 转化为字符串 B,拿出它的长度 L,遍历字符串 B,每个元素的 L 次方相加,直接判断总和是否和 A 相等,若是则为水仙花数,若不是则不是水仙花数。

给出 n,找到所有的 n 位十进制水仙花数思路:

先确定要搜索的范围,10 ** (n-1) ~ 10 ** n,接着从最小值到最大值之间的数进行遍历,判断每一个是否为水仙花数,拿出来。

反转一个3位整数

栗子

123 反转后为 321

900 反转后为 9

数组操作

+[number.toString()].reverse().join('')

相当于

parseInt([number.toString()].reverse().join(''))

爬楼梯

栗子

一个小孩爬一个 n 层台阶的楼梯,他每次可以跳 1 步、2 步或者 3 步,那么 n 层楼梯有多少种不同的爬法?

类似斐波那契数列处理,递归**

1层楼梯,1中方案

2层楼梯,2种方案

3层楼梯,4中方案

4层楼梯,7种方案

5层楼梯,13种方案

好明显,n层楼梯有F(n-1) + F(n-2) + F(n-3)种方案。

丑数

栗子

设计一个算法,找出只含素因子 2,3,5的第 n 小的数。

符合条件的数如:1, 2, 3, 4, 5, 6, 8, 9, 10, 12, 15, 16, 18, 20, 24, ...

需要说明的是,符合条件的数基本都是素因子的倍数,除了 1 之外。

处理思路

通过三个变量分别保存2,3,5三个值的初始索引,遍历n次,每次都将2,3,5乘以各自变量,然后取最小值push进一个新的数组中,相应的最小值索引加1,以此类推。

// [1, 2, 3, 4, 5, 6, 8, 9, 10, 12, 15, 16, 18, 20, 24]
const nthUglyNumber = function(n) {
  let arr = [1];
  let min,
    nex2,
    nex3,
    nex5,
    i2 = i3 = i5 = 0;
  for (let i = 1; i < n; i++) {
    // 除了第一个数,每个数都是2、3、5的倍数,把它们的倍数找出来,数字较小添加进去
    nex2 = arr[i2] * 2;
    nex3 = arr[i3] * 3;
    nex5 = arr[i5] * 5;
    min = Math.min(nex2, nex3, nex5);
    // 增加他们的倍数 为下次计算做准备
    if (min === nex2) i2++;
    if (min == nex3) i3++;
    if (min == nex5) i5++;
    arr.push(min);
  }
  return arr[arr.length - 1];
  // return arr
}

你需要了解的 HTTP 2.0

先参考一下别人文章,后期单独总结。
面试官问:你了解HTTP2.0吗?

HTTP 1.x 缺陷

  1. 请求阻塞。
    在 HTTP 1.0 中,一个TCP连接只能发送一个请求,当接收到响应后便会立马断开连接,由于消耗性能缘故。在 HTTP 1.1 后支持使用Connection: Keep-Alive保持一个TCP连接一直打开状态,因此在 HTTP 1.1 之后,一个TCP连接可以发送多个请求响应(不过需要注意的是,由于浏览器限制原因,同一个Host的TCP连接最多只能并行处理6个请求,为此超出6个的话,就必须等待这6个完成了才能发送请求)

  2. 多个TCP连接
    浏览器会有一个并行下载资源的限制,在同域名下的请求最多只能处理6个,一旦超出6个便会出现上述的所说的请求阻塞问题。
    为此,就会有了多个不同域名的TCP连接,以及CDN的出现。但是建立TCP连接成本很高,也需要成本去进行管理

  3. 头部请求响应字段均采用文本格式
    计算机之间的通信最终都会以二进制的形式进行通信,由于 HTTP 1.x 均采用文本格式,在数据到达计算机时都会需要转化成响应的二进制形式。但文本格式唯一很好方式便是方便开发者调试。

  4. 客户端需要主动请求

HTTP 2.0 关键点

  1. 采用二进制格式
    在请求响应字段均采用二进制方式,能更加有效在两个计算机之间进行通信。

  2. 多路复用
    主要解决了 HTTP 1.x 线头阻塞和多个TCP连接问题。在 HTTP 2.0 开始,一个TCP可以进行所有的通信,简单地说,就是不管是否为同一个Host域名的都可以在同一个TCP连接中进行通信。

  3. 请求响应头部压缩

  4. 服务端推送

ES Summary(每个版本)

本文针对Javascript中每个版本的一些知识总结,主要为了方便知识梳理,避免遇到盲点。🤔

(每一年官方出的新版本ES,都会同时进行更新的哈哈)

目录

  1. ES6
  2. ES7
  3. ES8
  4. ES9
  5. ES10

ES6

ES6中,可以说是整个Javascript改版中最大的一版,下面就来看看主要包含哪些内容。

块级作用域绑定(var、let、const)

  1. 在 Javascript 中不存在块级作用域,只存在全局作用域函数作用域

  2. 使用 var 声明的变量不管在哪声明,都会变量提升到当前作用域的最顶部。看个🌰:

    function test() {
      console.log(a) // 不会报错,直接输出undefined
      if(false) {
        var a = 1
      }
    }
    test()
    
    // 相当于
    function test() {
      var a
      console.log(a)
      if(false) {
        a = 1
      }
    }
    test()

    另外,定义和声明是两码事,如果变量都还没定义就使用时,就会报错(xxx is not defined)。

  3. let 和 const 都能够声明块级作用域,用法和 var 是类似的,let 和 const 都是不会变量提升。看个🌰:

    function test() {
      if(false) {
        console.log(a) // 报错,a is not defined(也是传说中的临时性死区)
        let a = 1
      }
    }
    test()

    let 和 const 必须先声明再访问

    所谓临时性死区,就是在当前作用域的块内,在声明变量前的区域(临时性死区只有 let 和 const 才有)。看个🌰:

    if(false) {
      // 该区域便是临时性死区
      let a = 1
    }
  4. const 声明变量时必须同时赋值,并且不可更改。

  5. 在全局作用域中使用 var 声明的变量会存储在 window 对象中。而使用 let 和 const 声明的变量则只会覆盖 window 对象中同名的属性,而不会替换。看个🌰:

    window.a = 1
    let a = 2
    
    console.log(a) // 2
    console.log(window.a) // 1

    let 和 const 声明的变量会存在一个单独的Script块作用域中(即[[Scopes]]作用域中能找到)。

    // 接着上面列举的栗子
    function aa() {}
    console.dir(aa)
    // 你会发现在输出内容中[[Scopes]],会存在两个作用域,一个是Script,一个是Global
    [[Scopes]]: Scopes[2]
    0: Script {a: 2}
    1: Global {parent: Window, opener: null, top: Window, length: 1, frames: Window, }

字符串和正则表达式

  1. 支持 UTF-16(含义是任何一个字符都适用两个字节表示),其中方法如下:

    • codePointAt:返回参数字符串中给定位置对应的码位,返回值为整数。
    • fromCodePoint:根据指定的码位生成一个字符。
    • normalize:提供Unicode的标准形式,传入一个可选字符串参数,指明应用某种Unicode标准形式。
  2. 字符串中新增的方法有:

    • includes(str, index):检测字符串指定的可选索引中是否存在参数文本。

    • startsWith(str, index):检测字符串头部是否有指定的文本。

    • endsWith(str, index):检测字符串尾部是否有指定的文本。

    • repeat(number):接受一个整数,重复对应字符串整数次。

      console.log('aa'.repeat(2)) // aaaa
  3. 当给正则表达式添加 u 字符时,表示从编码单元操作模式切换为字符模式。

  4. 模板字符串支持多行文本、模板中动态插入变量、模板子面量方法使用。

    • 多行文本。

      `1
      
      2`
    • 模板中动态插入变量。

      let a = 1
      console.log(`${a} haha`) // 1 haha
    • 模板子面量方法。

      function aa(a, ...b) {
        console.log(a, b)
      }
      let a = 1
      aa`hehe ${a} haha`
      // 输出为:
      // ["hehe ", " haha", raw: ["hehe ", " haha"]] [1]

      其中参数 a 表示模板字符串中静态字符。参数 b 表示模板动态变量。

函数

  1. 支持默认参数,默认参数不仅可以为字符串、数字、数组或对象,还可以是一个函数。看个🌰:

    function sum(a = 1, b = 2) {
      return a + b
    }
    sum()

    参数默认值不能被 arguments 识别,看个🌰:

    function sum(a = 1, b = 2) {
      console.log(arguments)
    }
    sum() // {}
    sum(3, 6) // {"0": 3, "1": 6}

    默认参数同样存在临时性死区,看个🌰:

    // 在初始化a时,由于b还没被声明,因此无法直接将b赋值给a
    function aa(a = b, b) {
      return a + b
    }
  2. 支持展开运算符(...),其作用是解构数组和对象。看个🌰:

    // 展开数组或对象
    var a1 = [1, 2]
    var a2 = {a: 1}
    console.log(...a1) // 1 2
    console.log({...a2}) // {a: 1}
    
    // rest参数
    function aa(...obj) {
      console.log(obj)
    }
    aa(a1) // 1 2
    aa(a1, a2) // [[1, 2], {a: 1}]
  3. 支持箭头函数。箭头函数和普通函数的区别有:

    • 箭头函数木有 this,this 指向的是定义该箭头函数所在的对象。
    • 箭头函数没有 super
    • 箭头函数没有 arguments
    • 箭头函数内部不存在 new.target 绑定(构造函数存在)。并且箭头函数中不能使用 new 关键字。
    • 箭头函数不存在原型
  4. 支持尾调用优化。(调用一个函数时都会生成一个函数调用栈,尾调用就可以很好滴避免生成不必要的尾调用阮一峰的尾调用优化讲解

    只有满足以下三个条件时,引擎才会帮我们做好尾调用优化。

    • 函数不是闭包。
    • 尾调用是函数最后一条语句。
    • 尾调用结果作为函数返回。

    看个🌰:

    // 符合尾调用优化情况
    function aa() {
      return bb()
    }
    
    // 无return不优化
    function aa() {
      bb()
    }
    // 不是直接返回函数不优化
    function aa() {
      return 1 + bb()
    }
    // 最后一条语句不是函数不优化
    function aa() {
      const cc = bb()
      return cc
    }
    // 闭包不优化
    function aa() {
      function bb() {
        return 1
      }
      return bb()
    }

    需要知道的是,递归都是很影响性能的,但是有了尾调用后,递归函数的性能将得到有效的提升

    // 斐波那契数列
    // 常做方案
    function fibonacci(n) {
      if(n === 0 || n === 1) return 1;
      return fibonacci(n-1) + fibonacci(n-2);
    }
    
    // 尾递归优化,其中pre是第一项的值,next作为第二项的值
    function fibonacci(n, pre, next) {
      if (n <= 1) return next
      return fibonacci(n - 1, next, pre + next)
    }

对象的扩展

  1. 对象方法和属性支持简写,以及对象属性支持可计算。看个🌰:

    const id = 1
    const obj = {
      id,
      [`test${id}`]: 1,
      printId() {
        return this[`test${id}`]
      }
    }
  2. Object 新增了方法如下:

    • Object.is。判断两个值是否相等。

      console.log(Object.is(NaN, NaN)) // true
      console.log(Object.is(+0, -0)) // false
      console.log(Object.is(5, "5")) //false
    • Object.assign。浅拷贝一个对象,相当于一个 Mixin 功能。

      const obj = {a: 1, b: 2}
      const newObj = Object.assign({...obj, c: 3})
      console.log(newObj) // {a: 1, b: 2, c: 3}
  3. 对象支持同名属性,不过后面的属性会覆盖前面同名的属性。看个🌰:

    const obj = {
      a: 1,
      a: 2
    }
    console.log(obj) // {a: 2}
  4. 遍历对象时,默认都是数字属性按顺序提前,接着就是首字母排序。看个🌰:

    const obj = {
      name: 'haha',
      1: 2,
      a: 1,
      0: 'hehe'
    }
    for(var key in obj) {
      console.log(key)
    }
    // 输出顺序为
    // 0
    // 1
    // name
    // a
  5. 支持实例化后的对象改变原型对象,使用方法Object.setPrototypeOf()

    var parent1 = {a: 1}
    var child = Object.create(parent1)
    console.log(child.a) // 1
    var parent2 = {b: 2}
    child = Object.setPrototypeOf(child, parent2)
    console.log(child.a, child.b) // undefined, 2

解构

  1. 解构的定义是,从对象中提取出更小元素的过程,即对解构出来的元素进行重新赋值

  2. 对象解构必须是同名的属性。看个🌰:

    var obj = {
      a: 1,
      b: 2
    }
    var a = 3, b = 3;
    ({a, b} = obj)
    console.log(a, b) // 1, 2
  3. 数组解构可以有效地处理交换两个变量的值。

    var a = 1, b = 2;
    [a, b] = [b, a]
    console.log(a, b) // 2, 1

    数组解构还可以按需取值,看个🌰:

    var arr = [[1, 2], 3]
    var [[,b]] = arr
    console.log(b) // 2
  4. 混合解构就是对象和数组解构结合,看个🌰:

    var obj = {
      a: 1,
      b: [1, 2, 3]
    };
    ({a, b: [...arr]} = obj)
    console.log(a, arr) // 1, [1, 2, 3]
  5. 解构参数就是直接从参数中获取相应的参数值。看个🌰:

    function aa({a = 1, b = 2} = obj) {
      console.log(a, b)
    }
    aa({a: 3, b: 6}) // 3, 6

Symbol

  1. Symbol 是一种特殊的、不可变的数据类型,可作为对象属性的标识符使用,也是一种原始数据类型。

  2. Symbol 的语法格式为:

    Symbol([desc]) // desc是一个可选参数,用于描述Symbol所用

    创建一个 Symbol,如下:

    const a = Symbol()
    const b = Symbol('1')
    console.log(a, b) // Symbol(), Symbol('1')

    创建 Symbol 不能使用 new

  3. Symbol 最大的用处在于创建对象一个唯一可计算的属性名。看个🌰:

    const obj = {
      [Symbol('name')]: 12,
      [Symbol('name')]: 13
    }
    console.log(obj) // {Symbol(name): 12, Symbol(name): 13}

    有效地避免命名冲突问题。

  4. Symbol 不支持强制转换为其他类型。

  5. 在 ES6 中提出一个 @@iterator方法,所有支持迭代的对象(如数组、Set、Map)都要实现。其中**@@iterator方法的属性键为Symbol.iterator而非字符串**。只要对象定义有Symbol.iterator属性就可以用for...of进行迭代

    // 判断对象是否实现Symbol.iterator属性,就可以判断是否可以使用for...of进行迭代
    if(Symbol.iterator in obj) {
      for(var n of obj) console.log(n)
    }
  6. Symbol 支持全局共享机制,使用 Symbol.for 进行注册,使用 Symbol.keyFor 进行获取。看个🌰:

    // Symbol.for中参数可为任意类型
    let a = Symbol.for(12)
    console.log(Symbol.keyFor(a), typeof Symbol.keyFor(a)) // '12', string

    Symbol.keyFor 最终获取到的是一个字符串值。

  7. Symbol 可作为类的私有属性,使用 Object.keys 或 Object.getOwnPropertyNames 方法都无法获取 Symbol 的属性名只能使用 for...of 或 Object.getOwnPropertySymbols 方法获取。看个🌰:

    var obj = {
      [Symbol('name')]: 'Andraw-lin',
      a: 1
    }
    console.log(Object.keys(obj)) // ["a"]
    console.log(Object.getOwnPropertySymbols(obj)) // [Symbol(name)]

Set和Map

  1. Set 常用于检查对象中是否存在某个键值,Map 则常用于获取已存的信息。

  2. Set 是有序列表,含有相互独立的非重复值。支持的属性和方法如下:

    • size。返回 Set 对象中元素个数。

    • add(value)。在 Set 对象尾部添加一个元素。

    • entries()。返回 Set 对象中[值,值]形式,看个🌰:

      var a = new Set([1, 2, 3])
      for(var [b, c] of a.entries()) {
        console.log(b, c)
      }
      // 输出结果为:
      // 1 1
      // 2 2
      // 3 3
    • forEach(callback)。用于遍历 Set 集合。

    • has(value)。判断 Set 集合中是否存在有指定的值。

  3. Set 集合的特点是没有下标,没有 Key。Set 和 Array可以相互转换,如下:

    // 数组转成Set
    const arr = [1, 2, 3]
    console.log(new Set(arr))
    
    // Set转成数组
    const se = new Set([1, 2, 3])
    console.log([...se])
  4. Set 集合是一个强引用,只要 new Set 实例化的引用存在,就不会释放内存。若定义一个 DOM 元素的 Set 集合,然后在某个 js 中引用了该实例,当页面跳转时,并不会立马释放内存,因为引用还在。WeakSet 就是专门用于释放强引用的

    WeakSet 和 Set 区别:

    • WeakSet 对象中只能存放对象类型,不能存放原始数据类型。而 Set 对象则可以。
    • WeakSet 对象中存储的对象值是弱引用的,若无其他变量或属性引用该对象值,则这个对象值会被当成垃圾回收掉。而 Set 对象则是存储强引用。
    • WeakSet 对象中存储的值是无法被枚举。而 Set 对象则可以枚举。
  5. Map 是存储键值对的有序列表,key 和 value 支持所有数据类型。对比 Set 集合,Map 集合多了 set 方法和 get 方法。看个🌰:

    var m = new Map()
    m.set('name', 'haha')
    m.set('year', '1999')
    console.log(m.get('name'), m.get('year')) // haha, 1999

    支持对象作为 key 值,看个🌰:

    const key = {}
    m.set(key, 'hehe')
    m.get(key) // 'hehe'
  6. 和 WeakSet 一样,也会有 WeakMap 存在,专门针对弱引用。看个🌰:

    var map = new WeakMap()
    var key = document.querySelector('.header')
    map.set(key, 'DOM')
    map.get(key) // 'DOM'
    key.parentNode.removeChild(key)
    key = null

迭代器(Iterator)和生成器(Generator)

  1. 迭代器是一种特殊对象,每一个迭代器对象都有一个 next(),该方法返回一个对象,包括了 value 和 done 属性。使用 ES5 模拟实现迭代器如下:

    function createIterator(items) {
      var i = 0
      return {
        next() {
          var done = (i >= items.length)
          var value = i < items.length ? items[i++] : undefined
          return { done, value }
        }
      }
    }
    var arr = createIterator([1, 2])
    console.log(arr.next()) // { done: false, value: 1 }
    console.log(arr.next()) // { done: false, value: 2 }
    console.log(arr.next()) // { done: true, value: undefined }
  2. 生成器就是一个函数,用于返回迭代器的。使用 * 号声明的函数即为生成器函数,同时需要使用 yield 控制进程。看个🌰:

    function *createIterator() {
      console.log(1)
      yield 1
      console.log(2)
      yield 2
      console.log(3)
    }
    var a = createIterator() // 执行后并不会输出任何东西
    console.log(a.next()) // 先输出1,再输出{ value: 1, done: false }
    console.log(a.next()) // 先输出2,再输出{ value: 2, done: false }
    console.log(a.next()) // 先输出3,再输出{ value: undefined, done: true }

    总结一下,迭代器执行 next() 方法时,只会执行前面到 yield 间的代码,后面代码都会被终止

    同样地,在 for 循环中使用迭代器,遇到 yield 时都会终止进程。看个🌰:

    function *createIterator(items) {
      for(let i = 0; i < items.length;  i++) {
        yield items[i]
      }
    }
    const a = createIterator([1, 2, 3]);
    console.log(a.next()); //{value: 1, done: false}
  3. yield 只能在生成器函数内使用。

  4. 生成器函数还可以使用匿名函数形式创建,看个🌰:

    const createIterator = function *() {
      // ...
    }
  5. 凡是通过生成器得到的迭代器,都是可迭代的对象(即可迭代对象具有 Symbol.iterator 属性),可使用 for...of 进行迭代。看个🌰:

    function *createIterator() {
      yield 1
      yield 2
    }
    var obj = createIterator()
    for(var val of obj) {
      console.log(val)
    }
    // 输出为
    // 1
    // 2

    可迭代对象可访问 Symbol.iterator 直接得到迭代器,看下面

    function *createIterator() {
      yield 1
      yield 2
    }
    var obj = createIterator()
    var newObj = obj[Symbol.iterator]() // 其实obj[Symbol.iterator]相当于createIterator迭代器

    Symbol.iterator 可用于检测一个对象是否可迭代

    typeof obj[Symbol.iterator] === "function"
  6. 默认情况下定义的对象是不可迭代的,但是可以通过 Symbol.iterator 创建迭代器。看个🌰:

    const obj = {
      items: [],
      *[Symbol.iterator]() {
        for (let item of this.items) {
          yield item;
        }
      }
    }
  7. 数组、Set、Map等可迭代对象,其内部已实现迭代器,并且提供3种迭代器调用,分别是:

    • entries():返回一个迭代器,用于返回键值对。

      var arr = [1, 2, 3]
      for(var [key, value] of arr.entries()) {
        console.log(key, value)
      }
      // 输出结果为
      // 0 1
      // 1 2
      // 2 3
    • values():返回一个迭代器,用于返回键值对的value。

      var arr = [1, 2, 3]
      for(var value of arr.values()) {
        console.log(value)
      }
      // 输出结果为
      // 1
      // 2
      // 3
    • keys():返回一个迭代器,用于返回键值对的key。

      var arr = [1, 2, 3]
      for(var key of arr.keys()) {
        console.log(key)
      }
      // 输出结果为
      // 0
      // 1
      // 2
  8. 高级迭代器功能,主要包括传参、抛出异常、生成器返回语句、委托生成器。

    • 传参。next 方法传参数时,会作为上一轮 yield 的返回值,除了第一轮 yield 外,看个🌰:

      function *aa() {
        var a1 = yield 1
        var a2 = 10
        yield a1 + 10
      }
      var a = aa()
      console.log(a.next(2)) // { value: 1, done: false }
      console.log(a.next(100)) // { value: 110, done: false }
    • 抛出异常。

      function *aa() {
        var a1 = yield 1
        var a2 = 10
        yield a1 + 10
      }
      var a = aa()
      console.log(a.next(2)) // { value: 1, done: false }
      console.log(a.throw(new Error('error'))) // error
      console.log(a.next(100)) // 不再执行
    • 生成器种遇到 return 语句时,表示退出操作。

      function *aa() {
        var a1 = yield 1
        return
        yield a1 + 10
      }
      var a = aa()
      console.log(a.next(2)) // { value: 1, done: false }
      console.log(a.next(100)) // { value: undefined, done: true }
    • 委托生成器。其实就是生成器嵌套生成器。

      function *aIterator() {
        yield 1;
      }
      function *bIterator() {
        yield 2;
      }
      function *cIterator() {
        yield *aIterator()
        yield *bIterator()
      }
      var i = cIterator()
      console.log(i.next()) // {value: 1, done: false}
      console.log(i.next()) // {value: 2, done: false}
  9. 异步任务执行器,其实就是用来循环执行生成器。

    因为我们知道生成器需要执行 N 次 next() 方法才能运行完,异步任务执行器就是帮我们做这些事情的。

    function run(taskFn) {
      var task = taskFn() // 调用生成器
      var result = task.next()
      function step() {
        if(!result.done) {
          result = task.next(result.value)
          step()
        }
      }
      step()
    }
    run(function *() {
      let text = yield fetch() // 异步请求获取数据
      doSomething(text) // 处理返回结果
    })

    异步任务执行器,其实就是间接实现了 async 和 await 功能。

类class

  1. 在 ES6 中,将原型的实现写在类中,和 ES5 本质上是一致的,都是需要新建一个类名,然后实现构造函数再实现原型方法。看个🌰:

    class Person {
      constructor(name) { // 新建构造函数
        this.name = name // 私有属性
      }
      sayName() { // 定义一个方法并且赋值到构造函数的原型中
        return this.name
      }
    }

    私有属性的定义,只需要在构造方法中定义this.xx = xx即可

  2. 类声明和函数声明的区别,主要有:

    • 类声明不能提升,而函数声明则会被提升。
    • 类声明中代码会自动强行运行在严格模式下。
    • 类中的所有方法都是不可枚举的,而函数声明的对象中方法则是可以枚举的。
    • 类中的构造函数只能使用 new 来调用,而函数则可以普通调用或 new 来调用。
  3. 类的定义有声明式定义和表达式定义,看个🌰:

    // 声明式定义
    class Person {
      // ...
    }
    
    // 表达式定义
    let person = Class {
      // ...
    }

    类还支持立即调用。

    let person = new Class {
      constructor(name) {
        this.name = name
      }
      sayName() {
        return this.name
      }
    }('Andraw')
    console.log(person.sayName()) // Andraw
  4. 类支持在原型上定义访问器属性。看个🌰

    class Person {
      constructor(name) {
        this.name = name
      }
      get myName() {
        return this.name
      }
      set myName(name) {
        this.name = name
      }
    }
    var descriptor = Object.getOwnPropertyDescriptor(Person.prototype, 'myName')
    console.log('get' in descriptor) // true
    console.log(descriptor.enumerable) // false 表示不可枚举

    类中定义的属性或方法名都是可支持表达式的

    const test = 'sayName'
    class Person{
      constructor(name) {
        this.name = name
      }
      [test]() {
        return this.name
      }
    }

    类中定义的方法同样可以是生成器方法。

    class Person {
      *sayName() {
        yield 1
        yield 2
      }
    }
  5. 静态属性或静态方法,是在属性或方法前面使用 static 关键字。static 修饰的方法或属性只能被类本省直接访问,而不能在实例中访问

    class Person {
      static sayName() {
        return this.name
      }
    }
  6. 在 React 中写一个组件Test时,必须得继承React.Component。其中 Test 组件就是一个派生类。派生类中的构造函数内部必须使用 super()

    关于 super 的使用需要注意:

    • 只可以在派生类中使用 super。派生类是指继承自其他类的新类。
    • 派生类中构造函数访问 this 之前必须要先调用 super(),负责初始化 this
    • 如果不想调用 super,可让类的构造函数返回一个对象。
  7. 当派生类继承于父类时,其父类中的静态成员也会被继承到派生类中,但是静态成员同样只能是被派生类访问,而无法被其实例访问

  8. 在构造函数中可使用 new target(new target 通常表示当前的构造函数名)来阻止实例化类。看个🌰:

    class Person {
      constructor() {
        if(new.target === Person) { // 不允许该类被调用实例化
          throw new Error("error")
        }
      }
    }

数组

  1. 在 ES5 中创建数组的方式有两种,分别是数组子面量(即 var arr = [])和 Array 实例(即 new Array())。

    在 ES6 中新增两种方法创建数组,分别是 Array.of() 和 Array.from()。

    • Array.of()。我们知道 new Array() 中传入一个数字时,表示的是生成多少长度的数组,Array.of() 就是为了处理这种尴尬场面的,看个🌰:

      const a1 = new Array(1)
      const a2 = Array.of(1)
      console.log(a1, a2) // [undefined], [1]
    • Array.from()。用于将类数组转换为数组。

      function aa() {
        const arr = Array.from(arguments)
        console.log(arr)
      }
      aa(1, 2)
      // [1, 2]
      
      // 可传第二个参数,作为第一个参数的转换
      const arr = Array.from(arguments, value => value + 2)
      
      // 可传第三个参数,用来指定this
      
      // Array.from可用于处理数组去重
      Array.from(new Set(...arguments))
  2. 数组新增的方法有。

    • find()。传入一个回调函数,找到数组中符合当前搜索规则的第一个元素,返回它,并且终止搜索。

      var arr = [1, 2, 3]
      console.log(arr.find(n => typeof n === "number")) // 1
    • findIndex()。传入一个回调函数,找到数组中符合当前搜索规则的第一个元素,返回它的下标,终止搜索。

      var arr = [1, 2, 3]
      console.log(arr.find(n => typeof n === "number")) // 0
    • fill()。用新元素替换掉数组内的元素,可以指定替换下标范围。格式和🌰如下:

      // 格式如下
      arr.fill(value, start, end)
      
      // 栗子
      const arr = [1, 2, 3]
      console.log(arr.fill(4)) // [4, 4, 4] 不指定开始和结束,全部替换
      
      const arr1 = [1, 2, 3]
      console.log(arr1.fill(4, 1)) // [1, 4, 4] 指定开始位置,从开始位置全部替换
      
      const arr2 = [1, 2, 3]
      console.log(arr2.fill(4, 0, 2)) // [4, 4, 3] 指定开始和结束位置,替换当前范围的元素
    • copyWithin()。选择数组的某个下标,从该位置开始复制数组元素,默认从0开始复制。格式和🌰如下:

      // 格式如下
      arr.copyWithin(target, start, end)
      
      // 栗子
      const arr = [1, 2, 3, 4, 5]
      console.log(arr.copyWithin(3)) // [1,2,3,1,2] 从下标为3的元素开始,复制数组,所以4, 5被替换成1, 2
      
      const arr1 = [1, 2, 3, 4, 5]
      console.log(arr1.copyWithin(3, 1)) // [1,2,3,2,3] 从下标为3的元素开始,复制数组,指定复制的第一个元素下标为1,所以4, 5被替换成2, 3
      
      const arr2 = [1, 2, 3, 4, 5]
      console.log(arr2.copyWithin(3, 1, 2)) // [1,2,3,2,5] 从下标为3的元素开始,复制数组,指定复制的第一个元素下标为1,结束位置为2,所以4被替换成2

Promise与异步编程

  1. 对 DOM 做事件处理操作,如点击、激活焦点、失去焦点等,再比如使用 Ajax 请求数据时利用回调函数获取返回值,都属于异步编程。

  2. Promise 中文意思就是承诺,Javascript 对你许一个承诺,会在未来某个时刻兑现承诺

    Promise 有生命周期,分别是进行中(pending)、已经完成(fulfilled)、拒绝(rejected)

    Promise 不会直接返回异步函数的执行结果,需要使用 then 方法获取,获取异常回调时,需要使用 catch 方法获取。

    结合 axios 看个🌰,axios 是前端比较热门的 http 请求插件之一。

    // 1. 创建axios实例
    import axios from 'axios'
    export const instance = axios.create()
    
    // 2. 使用axios实例 + Promise获取返回值
    const promise = instance.get('url')
    promise.then(res => console.log(res)).catch(err => console.log(err))
  3. Promise 构造函数只有一个参数,该参数为一个函数,被作为执行器。执行器有两个参数,分别是 resolve() 和 reject() ,前一个表示成功回调,后一个表示失败回调。

    new Promise((resolve, reject) => {
      setTimeout(() => resolve(5), 0)
    }).then(res => console.log(5)) // 5

    Promise 实例只能过 resolve 或者 reject 函数返回数据,并且使用 then 或者 catch 进行获取

    Promise.resolve(1).then(res => console.log(res)) // 1
    Promise.reject(2).catch(res => console.log(res)) // 2
    // 捕获错误时,可使用catch获取
    new Promise((resolve, reject) => {
      if(true) {
        throw new Error('error')
      }
    }).catch(err => console.log(err))
  4. 浏览器和 Node 提供了 unhandledRejection 和 rejectionHandled 两个事件处理 Promise 中没有设置 catch 问题

    // unhandledRejection
    let rejected
    rejected = Promise.reject("It was my wrong!")
    
    process.on("unhandledRjection", function(reason, promise) {
      console.log(reason.message) // It was my wrong!
      console.log(rejected === promise) // true
    })
    
    // rejectionHandled
    let rejected
    rejected = Promise.reject("It was my wrong!")
    
    process.on("rejectionHandled", function(reason, promise) {
      console.log(reason.message) // It was my wrong!
      console.log(rejected === promise) // true
    })

    浏览器中使用上面两个方法只是在 window 对象上监听,而 Node 中使用是在 process 对象上监听。

    unhandledRejection 和 rejectionHandled的区别就是,前者是事件循环中触发,后者则是事件循环后触发。两者都是处理 Promise 中使用 reject 捕获错误时,而没有使用 catch 进行捕获处理

  5. Promise 支持链式调用,有效地解决了回调地狱问题。看个🌰:

    new Promise((resolve, reject) => {
      resolve(1)
    }).then(res => { return res + 1 }).then(res => {console.log(res)}) // 2
  6. 除了 resolve 和 reject 方法外,还有两个方法,便是 Promise.all 和 Promise.race。

    • Promise.all。运行多个 Promise,当全部 Promise 都返回结果时,才会使用 then 进行处理

      Promise.all([
        new Promise(function(resolve, reject) {
          resolve(1)
        }),
        new Promise(function(resolve, reject) {
          resolve(2)
        }),
        new Promise(function(resolve, reject) {
          resolve(3)
        })
      ]).then(arr => {
        console.log(arr) // [1, 2, 3]
      })
    • Promise.race。和 all 方法类似,不过就是当有一个返回结果时,就可以使用 then 进行处理

      Promise.race([
        new Promise(function(resolve, reject) {
          setTimeout(() => resolve(1), 1000)
        }),
        new Promise(function(resolve, reject) {
          setTimeout(() => resolve(2), 10)
        }),
        new Promise(function(resolve, reject) {
          setTimeout(() => resolve(3), 100)
        })
      ]).then(value => {
        console.log(value) // 2
      })
  7. Promise 本身不是异步,只有它的 then 方法或者 catch 方法才是异步。

    目前 ES7 已经支持 async 方案,该方案比 Promise 还强大啊。

    async function a() {
      await function() {}
    }

代理(Proxy)和反射(Reflect)

  1. 代理 Proxy 就是拦截 JS 引擎内部目标的底层对象操作,反射 Reflect 就是针对 Proxy 还原原对象操作方法。

    代理陷阱 覆写的特性 默认特性
    get 读取一个属性值 Reflect.get()
    set 写入一个属性 Reflect.set()
    has in操作符 Reflect.has()
    deleteProperty delete操作符 Reflect.delete()
    getProperty Object.getPropertypeOf() Reflect.getPrototypeOf()
    setProperty Object.setPrototypeOf() Reflect.setPrototypeOf()
    isExtensible Object.isExtensible() Reflect.isExtensible()
    preventExtensions Object.preventExtensions() Reflect.preventExtensions()
    getOwnPropertyDescriptor Object.getOwnPropertyDescriptor() Reflect.getOwnPropertyDescriptor()
    defineProperty Object.defineProperty() Reflect.defineProperty()
    ownKeys Object.ownKeys()、Object.getOwnPropertyNames()、Object.getOwnPropertySymbols() Reflect.ownKeys()
    apply 调用一个函数 Reflect.apply()
    construct 用new调用一个函数 Reflect.construct()

    反射 Reflect 一般和代理 Proxy 结合使用,设置相应的代理方法处理数据时,需同时使用反射 Reflect 对原对象的方法操作一遍

    现在就拿 set 做个🌰。set属性是在要改变Proxy属性值时,进行的预处理,共接收四个参数:

    • target:要进行预处理的目标对象;
    • key:预处理过程的Key值,相当于对象的属性;
    • value:要设置成的值;
    • receiver:改变前的原始值;
    let pro = new Proxy({
      aa: 2
    }, {
      set: (target, key, value, receiver) => console.log(target, key, value, receiver);
    });
    pro.aa = 8;         // {aa: 2} "aa" 8 Proxy {aa: 2}
  2. Proxy的存在能够使函数加上钩子函数,即可理解为在执行方法前预处理一些代码,举个栗子:

    let pro = new Proxy({}, {
      get: (target, key, property) => console.log('haha')
    });
    pro.aa;                 // haha
    pro.bb;                 // haha
  3. 在使用如 http-proxy 插件或 webpack 时,有时需要访问某个 api,通过配置 proxy 跳转到指定的 url 能解决跨域问题。但是该种模式和代理 Proxy 有异曲同工之处,但是机制是不一样的。

    devServer: {
      proxy: [
        {
          context: "/api/*", //代理API
          target: 'https://www.hyy.com', //目标URL
          secure: false
        }
      ]
    }

使用模块封装代码

  1. 模块可以是函数、数据、类,需要指定导出的模块名,才能被其他模块访问。看个🌰

    // 数据模块
    const obj = { a: 1 }
    // 函数模块
    const sum = (a, b) => a + b
    // 类模块
    class Person {
      // ...
    }
  2. 模块引入使用 import 关键字,导入模块方式有两种。

    • 导入指定的模块。

      import { sum } from 'a.js'
      sum(1, 2)
    • 导入全部模块。

      import allFn from 'a.js'
      allFn.sum(1, 2)
  3. 模块导出使用 export 关键字,看个🌰:

    // 导出数据模块
    export const obj = { a: 1 }
    // 导出函数模块
    export const sum = (a, b) => a + b
    // 导出类模块
    export class Person {
      // ...
    }

    需要注意的是,ES6 提供了模块的默认导出,在导出时结合 default 关键字,看个🌰:

    // a.js
    function sum(a, b) {
      return a + b
    }
    export default sum
    
    // b.js
    import sum from 'a.js'
    sum(1, 0)
  4. 不能在语句和函数内使用 export 关键字,只能在模块顶部使用

  5. ES6 提供了两种方式修改模块的导入和导出名,分别是导出时修改和导入时修改,使用 as 关键字

    • 导出时修改。

      // a.js
      function sum(a, b) {
        return a + b
      }
      export default {sum as add}
      
      // b.js
      import add from 'a.js'
      add(1, 2)
    • 导入时修改。

      // a.js
      function sum(a, b) {
        return a + b
      }
      export default sum
      
      // b.js
      import sum as add from 'a.js'
      add(1, 2)
  6. 无绑定导入,是指当模块没有可导出模块时,全都是定义的全局变量,可使用无绑定导入。看个🌰:

    // a.js
    let a = 1
    const PI = 3.1314
    
    // b.js
    import 'a.js'
    console.log(a, PI) // 1, 3.1314
  7. 使用 webpack 打包 js 后,浏览器加载模块时,总是按顺序加载,先加载模块1,再加载模块2,因为 module 类型默认使用 defer 属性

    <script type="module" src="module1.js"></script>
    <script type="module" src="module2.js"></script>

ES7

ES7 在 ES6 基础上仅仅新增了**求幂运算符(**)**和 Array.prototype.includes() 方法。

需要注意的是,在 ES6 中仅仅提供了字符串 includes 实现,而在 ES7 中则在数组中进行完善

求幂运算符(**)

** 运算符相当于 Math 对象中的 pow 求幂方法,使用如下。

2 ** 3 = 8
// 相当于
Math.pow(2, 3) // 8

** 运算符和 +- 运算符用法一致,看个🌰:

let num = 2
num **= 3
// 相当于 num = num ** 3
console.log(num) // 8

Array.prototype.includes()

数组中实现的 includes 方法,用于判断一个数组是否包含一个指定的值,如果包含就返回 true,否则返回 false

includes 和 indexOf 都是使用 === 来进行比较,但是在 includes 中,NaN === NaN 返回的是 true,而 indexOf 则是返回 false

另外,includes 和 indexOf 方法都是认为,+0 === -0

ES8

ES8 也是在 ES6 基础上继续进行拓展。

Object.values()和Object.entries()

在 ES6 中提及过,只有可迭代对象可以直接访问 keys、entries、values 三个方法在 ES8 中在 Object 对象上实现了 values 和 entries 方法,因为 Object 已经支持了 kes 方法,直接看🌰:

var obj = {
  a: 1,
  b: 2
}
console.log(Object.keys(obj)) // ["a", "b"]
console.log(Object.values(obj)) // [1, 2]
console.log(Object.entries(obj)) // [["a", 1], ["b", 2]]

其中,entries 方法还能结合 Map 数据结构。

var obj = {
  a: 1,
  b: 2
}
var map = new Map(Object.entries(obj))
console.log(map.get('a')) // 1
// Map { "a" => 1, "b" => 2 }

字符串追加

  1. 字符串新增方法 String.prototype.padStart 和 String.prototype.padEnd,用于向字符串中追加新的字符串。看个🌰:

    '5'.padStart(2) // ' 5'
    '5'.padStart(2, 'haha') // 'h5'
    '5'.padEnd(2) // '5 '
    '5'.padEnd(2, 'haha') // '5h'

    padStart 和 padEnd 对于格式化输出很有用。

  2. 使用 padStart 方法举个例子,有一个不同长度的数组,往前面追加 0 来使得长度都为 10。

    const formatted = [0, 1, 12, 123, 1234, 12345].map(num => num.toString().padStart(10, '0'))
    console.log(formatted)
    // ["0000000000", "0000000001", "0000000012", "0000000123", "0000001234", "0000012345"]

    使用 padEnd 也是同样的道理。

Object.getOwnPropertyDescriptors

Object.getOwnPropertyDescriptors 直接返回一个对象所有的属性,甚至包括 get/set 函数。

ES2017 引入该函数主要目的在于方便将一个对象浅拷贝给另一个对象,同时也可以将 getter/setter 函数也进行拷贝。意义上和 Object.assign 是不一样的。

直接看个🌰:

var obj = {
  a: 1,
  b: {
    a: 2
  },
  set c(temp) {
    this.d = temp
  },
  get c() {
    return this.d
  }
}
var newObj1 = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj))
console.log(newObj1)
// {
//  c: undefined
//  a: 1
//  b: {a: 2}
//  get c: ƒ c()
//  set c: ƒ c(temp)
//  __proto__: Object
// }
var newObj2 = Object.assign({}, obj)
console.log(newObj2)
// {
//  a: 1
//  b: {a: 2}
//  c: undefined
//  __proto__: Object
// }

在克隆对象方面,Object.assign 只能拷贝源对象中可枚举的自身属性,同时拷贝时无法拷贝属性的特性(如 getter/setter)。而使用 Object.getOwnPropertyDescriptors 方法则可以直接将源对象的所有自身属性(是自身属性啊,不是所有可访问属性!)弄出来,再拿去复制

上面的栗子中就是配合原型,将一个对象中可访问属性都拿出来进行复制,弥补了 Object.getOwnPropertyDescriptors 方法短处(即无法获取可访问原型中的属性)。

若只是浅复制自身属性,还可以结合 Object.defineProperties 来实现

var obj = {
  a: 1,
  b: {
    a: 2
  },
  set c(temp) {
    this.d = temp
  },
  get c() {
    return this.d
  }
}
var newObj = Object.defineProperties({}, Object.getOwnPropertyDescriptors(obj))
conso.e.log(newObj)
// {
//  c: undefined
//  a: 1
//  b: {a: 2}
//  get c: ƒ c()
//  set c: ƒ c(temp)
//  __proto__: Object
// }

允许在函数参数最后添加逗号

听说是为了方便 git 算法更加方便区分代码职责。直接看个🌰。

const sum = (a, b,) => a + b

Async/Await

在 ES8 所有更新中,最有用的一个!!!

async 关键字告诉 Javascript 编译器对于标定的函数要区别对待。当编译器遇到 await 函数时会暂停,它会等到 await 标定的函数返回的 promise,该 promise 要么 resolve 得到结果、要么 reject 处理异常。

直接上一个栗子,对比一下使用 promise 和使用 async 区别。

// 模拟获取userName接口
var getUser= userId
 => new Promise(resolve => {
   setTimeout(() => {
     resolve(userName)
   }, 2000)
 })
// 模拟获取userAge接口
var getUserAge = userName
 => new Promise(resolve => {
   setTimeout(() => {
     if(userName === 'Andraw') {
       resolve('24')
     } else {
       reject('unknown user')
     }
   }, 2000)
 })
// ES6的promise实现方式
function es6Fn(userId) {
  getUser(userId)
    .then(getUserAge)
    .then(age => {
      console.log(age)  
    })
}
// ES8的async实现方式
async function es8Fn(userId) {
  var userName = await getUser(userId)
  var userAge = await getUserAge(userName)
  console.log(userAge)
}

使用 ES8 的 async 异步编程更符合日常开发流程,而 ES6 的 promise 也是一个很好的使用, ES8 的 async 只是在 promise 基础上更上一层楼。

  1. async 函数返回 promise。

    若想获取一个 async 函数的返回结果,则需要使用 promise 的 then 方法

    接着拿上述 ES8 的 async 实现方式来举个例子。

    async function es8Fn(userId) {
      var userName = await getUser(userId)
      var userAge = await getUserAge(userName)
      return userAge
    }
    // 获取es8Fn async函数返回结果
    es8Fn(1).then(userAge => { console.log(userAge) })
  2. 并行处理

    我们知道,每次调用 es8Fn 函数时,都需要等到至少 4 秒时间,若调用 N 次,则需要等到 4N 秒。使用 Promise.all 来并行处理,可以极大释放时间限制。

    async function newES8Fn() {
      var [a, b] = await Promise.all([es8Fn, es8Fn])
      return [a, b]
    }

    上述并行处理后,就可以很好滴避免多次调用而时间耗费的问题。

  3. 错误处理

    对于 async/await 的错误处理,有三种方法可以处理,分别是在函数中使用 try-catch、catch 每一个 await 表达式、catch 整个 async-await 函数。

    • 在函数中使用 try-catch

      async function es8Fn(userId) {
        try {
        	var userName = await getUser(userId)
          var userAge = await getUserAge(userName)
          return userAge 
        } catch(e) {
          console.log(e)
        }
      }
    • catch 每一个 await 表达式

      由于每一个 await 表达式都返回 Promise,对每一个表达式都进行 catch 处理。

      async function es8Fn(userId) {
        var userName = await getUser(userId).catch(e => { console.log(e) })
        var userAge = await getUserAge(userName).catch(e => { console.log(e) })
        return userAge
      }
    • catch 整个 async-await 函数

      async function es8Fn(userId) {
        var userName = await getUser(userId)
        var userAge = await getUserAge(userName)
        return userAge
      }
      es8Fn(1).then(userAge => { console.log(userAge) }).catch(e => { console.log(e) })

ES9

ES9(即ES2018) 主要新增了对象的扩展运算符 Rest 以及 Spread、异步迭代器、Promise支持 finally 方法、正则的扩展。

对象的扩展运算符 Rest 以及 Spread

如果使用过 Object.assign 方法合并对象,应该就很清楚。在 ES6 中,在数组中支持了 Rest 解构赋值和 spread 语法。

// ES6中的Rest
var [a, ...b] = [1, 2, 3, 4, 5, 6]
console.log(a, b) // 1, [2, 3, 4, 5, 6]

// ES6中的spread
function sum(a, ...b) {
  console.log(a, b)
}
sum(1, 2, 3)
// 输出为:1, [2, 3]

ES8 则在对象中支持了 Rest 解构赋值和 Spread 语法

// rest解构赋值
var {x, ...y} = {x: 1, a: 2, b: 3}
console.log(x, y) // 1, { a: 2, b: 3 }

// spread语法,接着上面解构的值
var c = {x, ...y}
console.log(c) // {x: 1, a: 2, b: 3}

异步迭代器和异步生成器

在 ES6 中,如果一个对象具有 Symbol.iterator 方法,那该对象就是可迭代的。目前,只有 Set、Map、数组内部实现 Symbol.iterator 方法,因此都是属于可迭代对象。

var set = new Set([1, 2, 3])
var setFn = set[Symbol.iterator]()
console.log(setFn) // SetIterator {1, 2, 3}
console.log(setFn.next()) // {value: 1, done: false}
console.log(setFn.next()) // {value: 2, done: false}
console.log(setFn.next()) // {value: 3, done: false}
console.log(setFn.next()) // {value: undefined, done: true}

默认的对象是不支持可迭代的,若实现了 Symbol.iterator 方法,那么它也是可迭代的。那么对象的 Symbol.iterator 方法如何实现的呢?

var obj = {
  a: 1,
  b: 2,
  [Symbol.iterator]() {
    var allKeys = Object.keys(this)
    var i = 0
    return {
      next: () => {
      	return {
          value: this[allKeys[i++]],
          done: i > allKeys.length
        }
      }
    }
  }
}
var objFn = obj[Symbol.iterator]()
console.log(objFn) // {next: ƒ}
console.log(objFn.next()) // {value: 1, done: false}
console.log(objFn.next()) // {value: 2, done: false}
console.log(objFn.next()) // {value: undefined, done: true}

上面的实现,还可以再完善一丢。利用生成器

var obj = {
  a: 1,
  b: 2,
  [Symbol.iterator]: function *() {
    for(let key in this) {
      yield this[key]
    }
  }
}
var objFn = obj[Symbol.iterator]()
console.log(objFn) // Generator {_invoke: ƒ}
console.log(objFn.next()) // {value: 1, done: false}
console.log(objFn.next()) // {value: 2, done: false}
console.log(objFn.next()) // {value: undefined, done: true}

由上面可以知道,同步迭代器就是一个特殊对象,里面包含有 value 和 done 两个属性(即 {value, done})。那么异步迭代器又是什么?

异步迭代器,和同步迭代器不同,不返回 {value, done} 形式的普通对象,而是直接返回一个 {value, done} 的 promise 对象

其中,同步迭代器使用 Symbol.iterator 实现,异步迭代器使用 Symbol.asyncIterator 实现

var obj = {
  a: 1,
	b: 2,
  [Symbol.asyncIterator]() {
    var allKeys = Object.keys(this)
    var i = 0
    return {
      next: () => {
        return Promise.resolve({
          value: this[allKeys[i++]],
          done: i > allKeys.length
        })
      }
    }
  }
}
var objAsyncFn = obj[Symbol.asyncIterator]()
console.log(objAsyncFn) // {next: ƒ}
console.log(objAsyncFn.next().then(res => {
  console.log(res) // {value: 1, done: false}
}))
console.log(objAsyncFn.next().then(res => {
  console.log(res) // {value: 2, done: false}
}))
console.log(objAsyncFn.next().then(res => {
  console.log(res) // {value: undefined, done: true}
}))

那么既然有了异步迭代器,就肯定有异步生成器,专门用来生成异步迭代器的

var obj = {
  a: 1,
	b: 2,
  [Symbol.asyncIterator]: async function *() {
    for(let key in this) {
      yield this[key]
    }
  }
}
var objAsyncFn = obj[Symbol.asyncIterator]()
console.log(objAsyncFn) // obj {<suspended>}
console.log(objAsyncFn.next().then(res => {
  console.log(res) // {value: 1, done: false}
}))
console.log(objAsyncFn.next().then(res => {
  console.log(res) // {value: 2, done: false}
}))
console.log(objAsyncFn.next().then(res => {
  console.log(res) // {value: undefined, done: true}
}))

另外,异步迭代器和同步迭代器有一样东西很类似,就是使用 next() 后,是无法知道什么时候才会到最后一个值,在同步迭代器中,需要使用 for...of 进行遍历才能有效地处理迭代器中每一项值

在异步迭代器中,同样支持遍历,不过是 for...await...of 遍历

var obj = {
  a: 1,
	b: 2,
  [Symbol.asyncIterator]: async function *() {
    for(let key in this) {
      yield this[key]
    }
  }
}
(async function() {
  for await (var value of obj) {
    console.log(value)
  }
})()

for...await...of 只会在异步生成器中或异步函数中有效

Promise支持 finally 方法

Promise 成功获取数据时使用 then 方法,处理异常时使用 catch 方法。但是在某些情况下,我们不管成功还是存在异常,都希望 Promise 能够运行一些共同的代码,finally 就是处理这些事情的

Promise.resolve(1).then(res => { console.log(res) }).finally(() => { console.lig('common code...') })
// 输出结果为
// 1
// common code...

正则的扩展

在正则表达式中,点 . 可以表示任意单个字符。

/foo.bar/.test('foo\nbar') // false

上面代码中,为了能够匹配任意字符,ES9 提出了 s 修饰符,使得 . 可以匹配任意单个字符。

/foo.bar/s.test('foo\nbar') // true

还有几个暂不讨论。可自行了解哈

ES10

ES10(即 ES2019) 新增功能相对比较少,都是一些性能的优化。

Array.prototype.flat和Array.prototype.flatMap

在日常开发中,我们常遇到一个问题,那就是将[1, [1, 2], [1, [2, 3]]]扁平化为[1, 1, 2, 1, 2, 3]

以往的经历告诉我们,需要使用第三方库 lodash 来处理,导致了不必要的麻烦,为此,ES10 直接为数组提供了 flat 方法来实现扁平化数组

var arr = [1, [1, 2], [1, [2, 3]]]
console.log(arr.flat(2)) // [1, 1, 2, 1, 2, 3]

flat 方法中参数,表示的是扁平化的层数

另外的方法 flatMap,其实就是数组的 flat 方法和 map 方法结合。

[1, 2, 3].map(x => [x * x]) // [[1], [4], [9]]
[1, 2, 3].flatMap(x => [x * x]) // [1, 4, 9]

Obejct.fromEntries

Object.fromEntries 方法和 ES6 中的 Object.entries 功能刚好相反,Object.entries 是获取一个对象的键值对,而 Object.fromEntries 则是将键值对转化为对象

Object.fromEntries([["a", 1], ["b", 2]]) // {a: 1, b: 2}
Object.entries({a: 1, b: 2}) // [["a", 1], ["b", 2]]

字符串去除首尾空格

ES10 为字符串提供了 trimStart 和 trimEnd 方法,用于去除首尾空格。

'  123'.trimStart() // 123
'123  '.trimEnd() // 123

Symbol.prototype.description

定义 Symbol 类型时,可传入一个字符串作为标志,若想获得该字符串,ES6 并没有提供方法,而 ES10 则提供了 description 属性用于获取 Symbol 的描述信息

var symbol = Symbol('haha')
console.log(symbol.description) // haha

可选的catch参数

在 ES10 之前,使用 try...catch 块时,若不给 catch 函数传递参数,会报错。

ES10 则直接将 catch 参数作为可选。

// ES10前
try {
  // ...
} catch(e) {
  console.log(e)
} 
// ES10后
try {
  // ...
} catch {
  // ...
}

Array.prototype.sort方法由快排转换为Timsort

在 ES10 前,数组的 sort 方法默认采取的是快排,但会存在不稳定性,为此,直接转为使用 Timsort。可自行了解一下。

Timsort 就是将插入排序和归并排序进行合并起来得到的好算法

函数支持toString方法

ES10 支持函数直接以字符串的形式打印出来。

var sum = (a, b) => a + b
console.log(sum.toString()) // (a, b) => a + b

About React Render And Update(个人见解)

可参考一下别人写的好文章。

React主打函数式**,设计理念就是all in js,另外推崇的是数据不可变性,所有的数据更新都需手动去操作,便于提高应用的可控性。

本文就是以React为主体,简单滴谈一谈个人对于React在渲染层面以及更新层面的一些见解,当中难免会出现一些错误的观点,还请各位童鞋大大们多多指点一下哈 🙈。

  1. Render(渲染)

    1.1 第一次渲染情况

    1.2 后续渲染情况

  2. Update(更新之调度任务)

Render(渲染)

在分析Render前,我先引一段代码。

import React from 'react'
import ReactDOM from 'react-dom'

class App extends React.component {
  // ...
  // 省略
}

ReactDOM.render(<App />, document.querySelector('#root'))

相信大家对于上述这坨代码尤为熟悉,不管编写的App组件有多复杂,终究都会通过React.render方法来挂载到相应的DOM元素下。

那么问题来了,在ReactDOM.render方法中究竟发生了什么事情?

在这里,我会说明一下,Render过程中会分为两种情况,分别是:

  • 第一次渲染情况,从root开始,先创建一个Virtual DOM,再直接渲染出一个真实DOM
  • 后续渲染情况,先创建一个新Virtual DOM,与旧Virtual DOM进行diff,再批量滴渲染到真实DOM

第一次渲染情况

先来看看第一次渲染情况中到底发生了什么事情?下面我就不对源码贴出,直接说出来(当中对于部分不理解童鞋难免会有些难以理解,后面有时间我会单独讲讲React源码哈~)

  1. 首先在调用ReactDOM.render时,其内部会执行一个legacyRenderSubtreeIntoContainer方法,该方法会判断是否已存在root

  2. 当判断不存在root时,就会使用legacyCreateRootFromDOMContainer方法创建一个root对象,并挂载在container._reactRootContainer(即document.querySelector('#root')._reactRootContainer)中。

  3. 在方法legacyCreateRootFromDOMContainer中,会先将容器内部的所有节点进行移除。

    <div id="root"></div>

    在很多情况下,我们我像上面这样去编写根模板。接着就会使用ReactRoot构造函数创建root对象。(即**root对象 --> new ReactRoot()**)。

  4. 在构造函数ReactRoot中,会使用createContainer方法创建一个FiberRoot对象,并挂载在root对象的_internalRoot属性中,其中createContainer方法使用的是createFiberRoot方法创建。

    需要说明的是,fiber是一个树结构,和DOM树中节点一一对应,即每一个DOM节点都会对应一个fiber对象,而FiberRoot对象对应根节点Root

  5. createFiberRoot方法中,使用FiberRootNode构造函数创建FiberRoot对象,然后使用createHostRootFiber方法创建一个RootFiber对象,并挂载到FiberRoot对象的current属性中(FiberRoot.current === RootFiber)。

  6. 对于FiberRoot对象,只需理解其两个属性,分别是containerInfocurrent属性。其中前者代表是document.querySelector('#root'),后者代表的是RootFiber

  7. 对于RootFiber对象,我们先来看看要理解的属性。

    {
      // ...
      stateNode: any,
      return: Fiber | null,
      child: Fiber | null,
      sibling: Fiber | null,
      effectTag: SideEffectTag,
      alternate: Fiber | null
      // ...
    }
    • stateNode:指向FiberRoot对象。
    • return:指向当前节点的父节点。
    • child:指向当前节点的子节点。
    • sibling:指向当前节点的兄弟节点。
    • effectTag:指代记录DOM操作,如增删改。
    • alternate:指向其旧树对应的节点。需要清楚的是,存在新旧两个fiber树,旧树称为old tree,新树称为workInProgress tree,其中前者代表渲染好的DOM树,后者则代表正在执行更新的fiber树,当更新结束后,workInProgress tree将会替换old tree

下面我们就以一张图简单看看下面这段代码在第一次渲染时,所存在的关系。

import React from 'react'
import ReactDOM from 'react-dom'

function App() {
  return (
  	<div>
    	<p></p>
    	<span></span>
    </div>
  )
}

ReactDOM.render(<App />, document.querySelector('#root'))

React第一次渲染关系图

后续渲染情况

接下来就要说的就是后续渲染情况,上面已经说过,当发现已经存在旧的Virtual DOM后(即已存在root对象),则会创建一个新的Virtual DOM,并且通过diff算法,批量更新到真实DOM中。

那么在后续的渲染过程中,又是如何处理的?我们接着看 🤔

  1. 由于已存在root对象(即new ReactRoot()),会直接调用root.render(children, callback)方法(即ReactRoot.prototype.render)。在该方法中,首先先取得root._internalRootFiberRoot对象,然后直接构建new ReactWork()(该构建对象作用是便于组件渲染或更新后把render中的callback全部调用一遍)。

  2. 接着调用updateContainer方法,该方法中会通过FiberRoot对象中的current属性获取到fiber tree(即旧树)。然后通过requestCurrentTime方法计算currentTime任务的当前时间,通过computeExpirationForFiber方法计算任务过期时间。

  3. 在方法requestCurrentTime中,通过performance.now() - orginalStartTimeMs得到currentTime当前时间,其中performance.now()是当前时间,orginalStartTimeMs则是整个React应用初始化时间(初始化时就创建,固定不变的常量),得到的**currentTime则是当前任务距离应用初始化时间的时间间隔**。

    最后得到currentTime 时间间隔进行取整处理,即(currentTime | 0)

  4. 接着计算expirationTime过期时间,该时间和优先级相关,值越大,优先级就越高。在方法computeExpiration中,其实就是将当前时间performance.now()加上一个优先级常量得到expiration过期时间

    React**分为五种优先级,分别是

    类型 优先级 优先级常量
    ImmediatePriority(立即执行优先级) 1 -1
    UserBlockingPriority(用户交互优先级) 2 250
    NormalPriority(正常优先级) 3 5000
    LowPriority(低优先级) 4 10000
    IdlePriority(空闲优先级) 5 maxSigned31BitInt(约1073741823)
  5. 最后会创建一个update对象,该对象主要跟setState相关联,主要理解属性包括

    // update对象
    {
      // ...
      expirationTime: expirtationTime, // 这次更新的过期时间
      payload: null, // setState的第一个参数
      callback: null, // setState的第二个参数
      next: null, // 用于寻找队列中的下一个节点
      nextEffect: null, // 用于执行队列中下一个节点的DOM相关操作
      // ...
    }

    对于任何一个触发render的操作,都会创建一个update对象,使用队列方式进行存储,其中next属性就是用于查找下一个update节点

    对于批量更新的过程中,由于创建多个update对象,在等到浏览器客户端空闲时,就会按优先级大小进行批量更新,但是也需要重点结合expirationTime过期时间,这个过程也是所谓的调度任务。(这也是能解释为什么多次调用setState,最后都只是更新一次~🤔)

Update(更新之调度任务)

先抛一个问题,当我们setState时,在后续取到的值依然是旧的值?相信大家都清楚,在React中会通过调度任务进行分配,那么setState由于后续需要批量更新,优先级不高,所以都会延后处理,这就导致为什么setState后取到的值依然是旧的值。

那么问题来了,调度的作用是什么?

我们知道,当点击一个按钮时,若setState更新状态,就会触发组件进行渲染,由于js是单线程的,若此时还要等待组件渲染结束后再去做一些关键js逻辑处理,只会让用户感觉到卡顿的感觉,为此,调度的作用就是来处理这些事情的。

调度的作用就是,react会根据任务的优先级计算出各自的expirationTime过期时间,然后根据过期时间逐个放入到一个队列中进行管理,当浏览器空闲时,就会先处理优先级高的任务,并且优先级高的任务可以中断优先级低的任务

那么剩下一个问题,它是如何实现的呢?

答案就是根据任务优先级计算过期时间通过requestIdleCallback方法

对于计算过期时间,在上面已经提及,现在就不再论述。

现在就来看看requestIdleCallback方法,到底该方法做了哪些事情。

该方法其实就是在浏览器空闲时期依次调用事件队列中的任务,内部实现采用的是宏任务requestAnimationFrame事件以及setTimeout定时器来实现的。

rAFID = requestAnimationFrame(function(timestamp) {
	localClearTimeout(rAFTimeoutID);
	callback(timestamp);
});
rAFTimeoutID = setTimeout(function() {
	localCancelAnimationFrame(rAFID);
	callback(getCurrentTime());
}, 100)

可以看到的是,当不支持requestAnimationFrame时,就会使用setTimeout进行补救~

现在我们就来总结一下,调度任务的过程:

  1. 当有更新任务发生时,调度器会先根据策略分配其一个优先级,比如动画的优先级会高于更新状态优先级。
  2. 接着根据分配好的优先级计算出过期时间(即当前时间 + 优先级),优先级越高那么时间就越近。
  3. 计算好过期时间后就会存储到一个任务队列中。
  4. 等待到浏览器空闲时,即主线程的代码都执行完后,接着调用requestIdleCallback方法来执行任务队列中方法,先判断任务过期时间是否过期,若过期则会立即优先执行该任务,若无则会按照优先级大小执行。

需要特别说明的是,在React 15版本之前,当组件更新时,就需要递归向下遍历整个虚拟DOM判断需要更新的地方,这种处理存在的弊端在于无法中断,必须更新完所有组件才会停止。因此在更新耗时长的过程中将会阻塞主线程,从而导致用户的交互、动画等不能及时响应。

React 16版本后,采用新的Fiber架构(即虚拟DOM升级版),将整个更新任务拆分成一个个小的任务,并且控制这些任务的执行。简单来说,Fiber架构实现的就是,任务都是按照优先级高低执行,优先级高的任务可以中断优先级低任务。其核心包括fiber数据结构和调度器

diff流程上,和Vue实现是差不多的,有兴趣的童鞋,可以看看我这篇文章 【Vue 源码分析 】如何在更新 Patch 中进行 Diff

Review-Question-JavaScript

  1. JavaScript有几种类型的值?
  2. 栈、堆之间的区别
  3. 原生对象、宿主对象、内置对象间区别
  4. JavaScript中判断数据类型的方法有哪些?
  5. Array(...) 和 Array.of(...) 区别
  6. Object.is() 和操作符 ==、=== 有何区别
  7. for循环为何比forEach循环性能好
  8. 纯函数的概念
  9. 理解执行上下文
  10. 闭包含义、作用和影响
  11. 常见内存泄漏有哪些?应该如何避免?
  12. Ajax 原理以及手写
  13. 如何防止重复发送Ajax请求
  14. JS 异步解决方案
  15. 说一下promise,以及如何实现
  16. setTimeout、promise 和 async/await 区别
  17. 说一下 async/await,手写实现一个简单的 async 函数
  18. 讲一讲跨域
  19. 线性存储结构和链式存储结构的区别?
  20. 讲讲事件委托/代理?
  21. 事件流?
  22. 讲一讲 JavaScript 并发模型
  23. input 表单如何实现防抖?如何处理中文输入?
  24. 异步加载 js 脚本的方式有哪些?
  25. 说说 rest ?

JavaScript有几种类型的值?

JavaScript可分为两种类型的值,分别为堆和栈,其中栈存储的是原始数据类型,堆存储的则是引用数据类型。

  • 栈:undefined、null、number、string、boolean、symbol
  • 堆:object、array、function

原始数据类型正是由于占据空间小、大小固定的优点才在栈中能保存下来。至于引用数据类型,由于占据空间大且大小不固定,因此在栈中存储了指针,指针则是直接指向了该实体在堆中存放的地址,当解释器使用引用值时,会先在栈中检索,然后再根据地址找到在堆中的实体。


栈、堆之间的区别

  1. 申请空间方式不同

    栈是系统自动分配空间,栈中定义的数据在生命周期运行后会被系统自动回收释放。

    堆是程序员分配空间,堆中定义的数据在生命周期运行后不会被系统自动回收释放,需程序员手动释放,常见如WeakSet、WeakMap,释放不当会导致内存泄漏。

  2. 申请空间大小不同

    栈是一块连续的内存区域,存储大小一般受限于1M或2M。

    堆是不连续的内存区域,存储大小一般受限于计算机系统有效的虚拟内存,使用数的数据结构实现。


原生对象、宿主对象、内置对象间区别

  • 原生对象:在运行状态中动态创建的对象,需要使用new进行实例化。

  • 宿主对象:在浏览器中,window对象及其子对象中如DOM对象等,在 Node 中,global对象及其子对象。

  • 内置对象:是原生对象的一个子集,包含有NumberDate等,有一个对象很特殊,分别是Math不需要实例化。


JavaScript中判断数据类型的方法有哪些?

  1. typeof

    能准确无误地判断基础数据类型,但无法正确判断object类型。

    console.log(typeof 1) // 'number'
    console.log(typeof 'g') // 'string'
    console.log(typeof true) // 'boolean'
    console.log(typeof Symbol()) // 'symbol'
    console.log(typeof undefined) // 'undefined'
    
    console.log(typeof function a() {}) // 'function'
    console.log(typeof new Date()) 'object'
    console.log(typeof {}) 'object'
  2. Object.prototype.toString

    最推荐使用的方案,能兼容所有类型情况。

    function type(obj) {
      const t = Object.prototype.toString.call(obj)
      return t.slice(8, t.length - 1)
    }
    [
      'Number',
      'String', 
      'Boolean', 
      'Symbol', 
      'Null',
      'Undefined',
      'Object',
      'Array'
      'Function',
      'Date',
      'RegExp'
    ].foreach(typeItem => {
     	type[`is${typeItem}`] = function(obj) {
        return typeItem === type(obj)
      }
    })
  3. instanceof

    能准确无误地判断复杂数据类型,不能正确判断基本数据类型。

    console.log(1 instanceof Number) // false
    console.log('b' instanceof String) // false
    
    console.log(function a() {} instanceof Function) // true
    console.log(/ab/ instanceof RegExp) // true
    console.log({} instanceof Object) // true
  4. Constructor

    都能正确无误地表示复杂数据类型和基本数据类型,不过缺陷就是类型的constructor属性指向容易被改变,因此也是不怎么推荐。

    var a = 1
    console.log(a.constructor === Number) // true
    console.log('b'.constructor === String) // true
    
    function c() {}
    console.log(c.constructor === Function) // true
    console.log(/ab/.constructor === RegExp) // true
    console.log({}.constructor === Object) // true

Array(...) 和 Array.of(...) 区别

Array(...) 作用是接受参数返回一个数组,但是会存在一个缺陷,就是如果只传入一个参数且该参数是数字时,会直接返回一个空数组,长度即为传入的数字。

var arr = new Array(2)
console.log(arr) // [empty, empty]
console.log(arr.length) // 2

Array.of(...) 就是解决 Array(...) 缺陷的。

var arr = Array.of(1)
console.log(arr) // [1]

Object.is() 和操作符 ==、=== 有何区别

  1. Object.is() 功能上基本类似于 === ,但会有一些很细微的区别
    • Object.is() 认为 NaN 和 NaN 相等,=== 则相反。
    • Object.is() 认为 +0 和 -0 相等,=== 则相反。
  2. == 操作符只比较值是否相同,一旦类型不同时,就会进行强制转换再来进行比较。

for循环为何比forEach循环性能好

(个人见解)for循环是基于数组顺序进行遍历的,而 forEach 方法则是基于数组中迭代器进行遍历的,即相当于基于链表形式进行。

基于数组顺序进行读取就好比如读取哈希表形式直接获取,而基于迭代器进行读取则需要每次都从本身的next指针进行获取下一次要遍历的值,这就导致性能有一定差距。


纯函数的概念

纯函数也是函数的一种,但具有两个比较重要的特点

  • 相同的输入得到相同的输出。
  • 无副作用,即函数内部的操作不会影响到外部的。

理解执行上下文

执行上下文可以理解为一个对象,其包含有三个方面内容:

  • 与执行环境相关联的变量对象,环境定义的所有变量和函数都保存在这个对象中。
  • 作用域链,执行环境执行时就会创建。
  • this 指向。

当代码在全局环境执行时,会创建一个全局上下文。遇到函数后,创建一个函数执行上下文,并且被 push 进执行栈的顶层,当执行完函数后,就会被 pop 掉。这样一来,控制权又会被交回给全局上下文。


闭包含义、作用和影响

含义:闭包是一个函数对另一个函数变量的引用(有权访问另一个函数作用域的函数都是闭包)。

作用:被引用的函数有权访问上一级的函数作用域。

影响:闭包中的私有变量不会被回收,增加内存损耗,导致内存泄漏。


常见内存泄漏有哪些?应该如何避免?

  1. 冗余的全局变量。用完设为null
  2. 未销毁的计时器或毁掉函数。使用clearTimeout
  3. DOM引用。用完设为null
  4. 闭包。用完设为null
  5. 事件监听。使用removeEventListener

Ajax 原理以及手写

原理:

在客户端和服务端之间增加一个中间层即Ajax引擎,客户端使用XmlHttpRequest对象来向服务器发异步请求,从服务端获取到数据,再用js操作DOM更新页面。

手写:

function createHttp(url) {
	let xhr = new XMLHttpRequest() // 1. 建立连接
  xhr.open('get', url, true) // 2. 连接服务器
  xhr.send({a: 1}) // 3. 发送请求
  xhr.onreadystatechange = function() { // 4. 接受请求
    if(xhr.readystate === 4) {
      if(xhr.status === 200) {
        // ...更新操作
      }
    }
  }
}

如何防止重复发送Ajax请求

  1. 用户点击按钮发送请求后,就要立马禁止按钮点击。
  2. 节流。
  3. 使用XMLHttpRequest.abort方法去掉上一个请求。

JS 异步解决方案

  1. 回调函数

    缺陷就是回调地狱。

  2. promise

    解决回调地狱,采用链式调用,缺陷就是无法取消 Promise。

  3. generator

    可以控制函数的执行。

  4. async/await

    作为generator的语法糖,基于promise进行实现,不需要像 promise 写一大堆 then 链,同时解决了回调地狱问题。


说一下promise,以及如何实现

...待续


setTimeout、promise 和 async/await 区别

含义:

  • setTimeout:作为一个计时器,使用回调函数方式处理逻辑,第二个参数表示的是最小时间,不能保证在准确的时间内调用回调函数。

  • promise:作为一个容器,其构造函数本身执行过程是同步,保存里某个未来要结束的事件。then方法执行过程是异步的,返回一个新的promise,便于解决回调地狱。

  • async/await:基于promise实现的generator语法糖。非阻塞的。

在说区别前,需提前说一下的是,js 是单线程的,具体会把任务分为宏任务和微任务。

  • 宏任务:整体代码 script、setTimeout、setInterval。
  • 微任务:Promise、process.nextTick。

js 执行的顺序如下:

https://raw.githubusercontent.com/Andraw-lin/FE-Knowledge-Summary/master/asset/js%E6%89%A7%E8%A1%8C%E9%A1%BA%E5%BA%8F.png

  • 第一个宏任务就是整体的script代码,执行主线程的代码,遇到异步代码,就会将其回调函数放到事件队列中。这样第一个宏任务就完成了。
  • 接下来就会判断是否有微任务,按顺序执行微任务。
  • 然后,回到初始状态,再次判断是否有宏任务,再次执行,以此类推。

区别:

  • setTimeout 和 Promise 中 then 执行顺序不同,上面已经提到。
  • async函数隐式返回一个promise,任何一个 await 语句后面的 Promise 对象变为 reject 状态,那么整个 async 都会中断执行。

说一下 async/await,手写实现一个简单的 async 函数

...待续


讲一讲跨域

  1. jsonp。
  2. 服务端设置CORS。
  3. window.postMessage。
  4. document.domain。
  5. 服务端代理,如 node 代理转发。

线性存储结构和链式存储结构的区别?

主要从栈和链表区别、调用栈和循环队列入手。


讲讲事件委托/代理?

事件委托,又被称为事件代理,就是将原本需要绑定的事件委托给父元素,让父元素担当事件监听的职务。

事件委托的原理就是DOM元素事件冒泡。其好处就是提高性能,减少内存占用和事件注册。

举个例子,ul 标签中有多个 li 标签,不应该在每一个 li 标签上都添加一个点击事件,应该使用事件委托,直接在 ul 标签上加一个点击事件即可。


事件流?

事件流,也叫事件模型,主要分为两种,分别是事件捕获流和事件冒泡流。

事件捕获流就是从根节点出发,往子节点一直寻找,直到找到目标节点为止。

事件冒泡流就是从目标节点出发,往父节点一直向上执行,直到根节点为止。

DOM事件流分为三个阶段,分别是:事件捕获阶段、获取目标节点阶段和事件冒泡阶段。

阻止事件冒泡:event.stopPropagation()

阻止默认事件:event.preventDefault()


讲一讲 JavaScript 并发模型

从js执行过程来讲,即同步、异步的执行过程。


input 表单如何实现防抖?如何处理中文输入?

通过compositionstartcompositionupdatecompositionend事件处理中文输入以及插入防抖实现。


异步加载 js 脚本的方式有哪些?

  1. 指定 async 属性。并行下载,只要脚本下载完就会直接执行。
  2. 指定 defer 属性。并行下载,会等到 dom 加载完后才执行,defer 脚本的执行会在 onload 事件之前。
  3. 动态创建 script 标签引入。
  4. 利用 xhr 异步加载 js 脚本。

说说 rest ?

主要包含两个含义

  1. 表现层。

    使用 URI 执行资源的实体,URI 只代表资源的位置。至于资源的具体表现形式,需要通过请求头中的AcceptContent-Type两个字段指定。

  2. 状态转化。

    HTTP 是无状态的协议,因此状态都保存在服务端。此时用户想操作服务端,就必须使用 HTTP 动词来引起服务端的状态变化。

    而 HTTP 动词主要包括:

    • get:获取资源。
    • post:新建资源或更新资源。
    • put:更新资源。
    • delete:删除资源。

初探 Dart

Dart 是一门面向对象的编程语言,它就像是所有语言的优点都集中在一起,包括 JS 中异步函数 async / await、java 和 c# 中的强类型以及范型等。

编译模式

Dart 是一门既支持 JIT(即时编译)也支持 AOT(提前编译)的语言。何解?

所谓的 JIT(即时编译),就像 JS 那样在本地开发时,对于开发的代码只需要保存后,直接就能在客户端内得到体现。优点就是在开发期间内能够做到快速开发(如热重载)缺点就是每次即时编译都需要将代码动态编译成机器码,直观上可能会造成一些小卡顿(毕竟编译时需要时间鸭😅当然对于电脑配置高还是会没啥问题)。

另外,AOT(提前编译)就是针对在发布前,提前将源代码编译成机器码,这样对于客户端来说,加载二进制代码会更加快。

常用数据类型

Dart 数据类型中,支持 num 类型String 类型bool 类型list 集合类型(即数组)Map 类型以及 Set 类型,额外的类型还有 var 类型、dynamic 类型以及 Object 类型。

num类型

num 类型中有两种子类,分别是 int 类型和 double 类型。看个🌰你就会明白。

void initNum() {
  num a = 1; // 定义整数类型
  num b = 1.1; // 定义小数类型
  int c = 2; // 定义整数类型
  double d = 2.1; // 定义小数类型
}

String类型

使用字符串类型跟 JS 差不多,对于字符串中插入变量可以使用 + 符号,也可以使用 $ 符号来输出,区别就在于,Dart 中对于变量的调用可以省略{},当然对于表达式还是需要的。

void initString() {
  String a = 'haha';
  num b = 'bibi';
  print('hehehe, $a${b+1}'); // hehehe, haha2
}

另外,字符串类型还支持一些常用的API的。

  • substring;
  • indexOf;
  • startsWith;
  • replaceAll;
  • split;

bool类型

布尔类型就更好理解,还会额外支持且运算符||以及或运算符&&

void initBool() {
  bool success = true;
  bool fail = false;
  print(success && fail); // false
  print(success || fail); // true
}

List类型

List 集合就相当于 JS 中的 Array。若不对集合中类型进行指定,那么默认里面存储的元素都是 dynamic 类型一旦指定范型,就只能添加约束的类型数据

void initList() {
  List list1 = [1, 2, 'hahaha']; // 范型为dynamic
  List<int> list2 = [1, 2, 3]; // 范型为int
}

日常使用 List 支持使用 add 方法添加一个元素,以及使用 addAll 添加一个集合

若果需要遍历 List,可以使用的方式有:

  • for循环;
  • for...in...遍历;
  • forEach方法;

Map类型

Map 类型类似于 JS 中对象,在不指定类型情况下默认都是 Map 类型。

void initMap() {
  Map map1 = {
    0: 'jeje',
    'haha': 1
  }; // 直接通过key:value形式定义即可
  Map<int, int> map2 = {
    0: 1,
    1: 2
  }; // 指定Map的范型
}

获取 Map 中类型值时,就按照对象获取即可。如上述中获取 key 为 haha 的值:map1['haha']

若果需要遍历Map,则可以是以下方式:

  • forEach方法;
  • for...in...遍历;

Set类型

Set 类型跟 Map 类型很类似,特别是在字面量语法上,所以上述也提到定义对象时,在不指定类型情况下默认都是 Map 类型。

Set 和 List 最大的两个不同分别是:

  • Set 是无序的,List 是有序的
  • Set 中元素是不重复的,List 中元素是可重复的

那么要使用 Set 类型时就需要指定相关类型。

void initSet() {
  Set<String> names = {"1", 'haha'};
  var age<int> = {12, 2, 3}
}

dynamic类型、var类型以及Object类型

dynamic类型指的就是动态数据类型,使用它就表示定义的变量会关闭类型检查,例如有如下代码:

dynamic a = 1;
a.haha();

以上代码在 AOT 中是不会被检查出错误的,但在运行时却会报错。因此,在绝大多数情况下是不会使用到该类型的

var类型会自动推断类型,而且一旦指定了类型就不可以再进行修改

Obeject类型就是所有Dart对象的基类,使用它就可以直接调用toString方法以及hashCode方法。再看如下代码:

Object a = 1;
a.haha();

以上代码在 AOT 中就会直接报错,因为找不到 haha 这个方法。

明显滴,dynamic 类型和 Object 类型的区别就在于编译时是否对静态类型进行检查。当然,var则是自动推断类型

总所周知,Dart 是一门面向对象的开发语言,其中类在面向对象中占据的绝对中心的地位,接下来我们就来看看在 Dart 中编写类是如何实现的。

类的定义

在 Dart 中l类编写的格式如何:

class 类名 {
  类型 成员名;
 	返回值类型 方法名(参数列表) {
    // 函数处理
  };
}

需要注意的是,在使用 Dart 开发时,在方法中使用成员变量时都是会默认省略 this 的,但是遇到命名冲突时则需要补上 this。举个例子:

class Person {
  num age;
  void getAge() {
  	num resultAge = age + 1;
    print('age = $resultAge');
  }
}
void main() {
  var person = Person();
  person.age = 18;
  person.getAge();
}

在上面的代码中,可以看到,我把 new 关键字直接删掉了。这点也需要注意的,从 Dart 2.0 开始,new 关键字是可以省略

构造函数的编写

在 Dart 中编写的构造函数和 JS 中编写会很不一样,我们都知道在 JS 中,会习惯使用 constructor 关键字来作为一个类的构造函数。

那么在 Dart 中,需要注意以下几点:

  • 编写类时,如果不编写构造函数,Dart 会默认分配一个无参数的构造方法
  • 当编写自定义构造方法后,原来分配的无参数构造方法将会失效。

我们先来看看,在 Dart 中是如何编写一个构造方法的:

class Person {
  num age;
  Person(num age) {
    this.age = age;
  }
}

当然,Dart 提供了一种更便捷编写构造方法的语法糖:

class Person {
  num age;
  Person(this.age);
}

上述的编写和第一种编写的是等价的。

命名构造方法

在实际开发当中,我们会常常遇到一种情况,就是一个类只适应某个格式的参数,而无法适配其他格式的参数,这样一来,就不得不每次都需要将参数转化成统一格式来传递。

很明显,上述方式其实就是 java 中常用到的函数重载,而 Dart 中是不支持函数的重载,当然 JS 中也是不支持函数的重载,举个例子:

// JS
function overload(a) {
  console.log(a)
}
function overload(a, b) {
  console.log(a, b)
}

// 好明显,后面的重名函数会覆盖掉前面的函数。

为了解决这一缺陷,Dart 通过命名构造方法来实现模拟实现函数的重载

class Person {
  num age;
  Person(this.age);
  Person.fromMap(Map<String, Object> map) {
    this.age = map['age'];
  }
}
void main() {
  var person1 = Person(18);
  var person2 = Person({ 'age': 19 });
}

初始化列表

初始化列表是为了解决类中定义的 final 变量动态初始化的

如何理解?我们先来看看如下栗子🌰:

class Person {
  final String gender;
  Person(String gender) {
    this.gender = gender;
  }
}

咋一看,感觉没啥问题,但是当你编写完后,肯定会通过不了编译,必须报错!这就是定义的 gender 属性是不可重新赋值的。我们先来简单分析一下。

相信你们还记得 JS 中 new 过程吧?其实这里也是一样道理,当 Dart 执行构造方法时,Person 对象其实已经初始化完毕了,那么再执行this.gender = gender时肯定报错。接下来我们就需要保证this.gender在 Person 初始化之前必须进行相应赋值

我们可以很方便滴使用语法糖来解决。

class Person {
  final String gender;
  Person(this.gender);
}

这时候问题又来了,如果类中存在一个成员变量的值是表达式动态赋值时如何处理?

好明显,如果使用上面语法糖方式肯定是只能写死的,并不能处理问题。而初始化列表就是为了才诞生出来的,看个🌰你就会明白啦:

class Person {
  final String name;
  final String gender;
  final String description;
  Person(String name, String gender) : 
  	this.name = name,
    this.gender = gender,
    this.description = '$name is a $gender';

	void handlePrint() {
    print(description);
  }
}

void main() {
  var person = Person('Tom', 'boy');
  person.handlePrint();
}

初始化列表的语法很简单,就是在构造方法后面加上:,然后就是将 final 值进行赋值,多个赋值之间是需要逗号相隔的

重定向构造方法

回到上面的函数重载问题,我们先看以下🌰:

class Person {
  String name;
  num age;
  Person(this.name, this.age);
}

明显滴,上述 Person 类在初始化时只能传入两个参数,若有时候想传入一个参数时,如何处理?

当然你可以使用命名构造函数,但也有一种情况就是使用重定向构造函数

class Person {
  String name;
  num age;
  Person(this.name, this.age);
  Person.handleMsg(String name) : this(name, 18);
}

void main() {
  var person = Person.handleMsg('Tom');
  print(person.age);
}

常量构造函数

常量构造函数的作用就在于,相同参数创建的两个对象是同一个

判断两个对象是否相等,使用的是identical方法

class Person {
  final String name;
  final int age;
  const Person(this.name, this.age);
}
void main() {
  var person1 = const Person('Tom', 18);
	var person2 = const Person('Tom', 18);
	print(identical(person1, person2));
  
  var person3 = Person('Andraw', 19);
  var person4 = Person('Andraw', 19);
  print(identical(person3, person4));
}

要使用常量构造函数,必须要注意以下几点。

  • 创建常量对象时,必须使用const关键字,否则是两个不同的对象。
  • 常量构造函数中,成员变量必须是final类型的,而且构造方法也必须使用const关键字来修饰
  • (额外)当常量变量的结果赋值给一个 const修饰的变量时,那么const是可以省略不写的。

工厂构造方法

回到上面相同参数希望得到相同的两个对象问题,除了上面使用常量构造函数方法外,还可以使用factory工厂构造函数来实现。

对于普通的构造函数,在创建时会默认返回一个对象,无需我们手动调用return,而对于工厂构造方法,则需要我们手动调用return返回一个对象

其中,工厂构造函数使用的核心其实就是 JS 中的闭包原理

我们来看个栗子🌰:

class Person {
  String name;
  static final Map<String, Object> _cache = <String, Person>{}; // 用于缓存创建好的对象
	factory Person.formName(String name) {
    if (_cache.containsKey(name)) {
      return _cache[name];
    } else {
      final p = Person(name);
      _cache[name] = p;
      return p;
    }
  }

	Person(this.name);
}
void main() {
  var person1 = Person.formName('Tom');
  var person2 = Person.formName('Tom');
  print(identical(person1, person2)); // true
}

成员变量的setter和getter

类中成员变量默认会带有 setter 和 getter,当然我们也可以自定义它们俩。

class Person {
  String name;
  void set setName(String value) {
    this.name = value;
  }
  String get getName() {
    return this.name
  }
  Person(this.name);
}

另外,在 Dart 中编写函数时,可以使用箭头函数语法糖进行简写。如下:

class Person {
  String name;
  void set setName(String name) => this.name = name;
	String get getName => this.name;
	Person(this.name);
}

类的继承

在 Dart 中类的继承使用extends关键字,而且子类中使用super来访问父类。

类的继承会拥有以下主要特点:

  • 父类中除了构造函数外,其他所有的成员变量和方法均可被继承
  • 子类可对父类的方法进行重写。
  • 子类中可以使用初始化列表来调用父类的构造方法,以此来初始化相应的属性。
  • 父类中不编写构造函数时,那么子类在初始化时将隐含调用父类的无参数默认构造方法(即无需我们手动调用)。
  • 若父类中已经编写构造函数,那么子类必须在初始化列表中通过super关键字显式调用父类的构造方法
class Person {
	String name;
  Person(this.name);
  void printName() {
    print(`The person is $name`);
  }
}
class Man extends Person {
  int age;
  Man(String name, int age) :
  	this.age = age,
    super(name);
	@override
	void printName() {
    super.print(); // 这里可以选择性调用父类的print方法
    print(`The man is $name, and his age is $age.`);
  }
}

可以看到,若对父类的方法进行重写时,用到的是@override关键字

抽象类

抽象类使用的是abstract关键字。

在抽象类中,既可以有没有具体实现的方法(简称抽象方法),也有具体实现的方法

另外,抽象类是禁止被实例化的,其中抽象方法必须要被子类进行实现,而抽象类中具体实现的方法可以不被子类重写

abstract class Person {
  String name;
  Person(this.name);
  void getName() => this.name;
	void getDescription();
}

class Man extends Person {
  @override
  void getDescription() => print('The man\'s name is $name');
	Man(String name) : super(name);
}

静态成员变量和静态方法

类中的静态变量以及静态方法使用的是static关键字。

并且,静态成员变量以及静态方法只能通过原始类来调用,而且无法通过this进行调用

class Person {
  String firstName = 'hehe';
  // 无法通过this访问静态属性
  static String lastName = 'haha';
  void printFirstName() {
    print('The firstName is $firstName.');
  }
  static void printLastName() {
    print('The lastName is $lastName.');
  }
}
void main() {
  var person = Person();
  person.firstName = 'hihi';
  person.printFirstName();

  Person.lastName = 'BaBa';
  Person.printLastName();
}

隐式接口

在 Dart 中只支持单继承,而不支持多继承,只能通过implements方式实现多继承

默认情况下,每个类在定义时就相当于也默认定义了一个接口

当将一个类作为接口使用时,那么实现该接口的类必须实现该接口中所有的方法,而且在实现所有的方法中都不能调用super方法

class Person {
  void getName();
  void printName() {
    print('Person.');
  }
}
abstract class People {
  void run();
}
class Man implements Person, People {
  @override
  void getName() => print('hahaha...');
  @override
  void printName() => print('hehe...');
  @override
  void run() => print('running...');
}

Mixin混合

既然implements用于实现某个接口中所有的方法,那么有没有一种方案是可以直接复用某个类中方法呢?

而**Mixin就是为了实现某个类复用另外一个类中方法才诞生的**。使用的是mixin 关键字创建类。

mixin Runner {
  run() {
    print('running...');
  }
}
mixin Flyer {
  fly() {
    print('flying...');
  }
}
class Bird with Runner, Flyer {};
void main() {
  var bird = Bird();
  bird.run();
  bird.fly();
}

显而易见的是,Mixin需要使用with来链接

枚举类型

枚举类型在 Dart 中常用于表示固定数量的常量值

其中,枚举类型使用 enum 关键字来定义:

enum Colors { red, orange, blue };
void main() {
  print(Colors.red);
}

另外,在枚举类型中有两种常见的属性,分别是indexvalues

enum Colors { red, orange, blue };
void main() {
  print(Colors.red.index); // 0
  print(Colors.values); // [Colors.red, Colors.orange, Colors.blue]
}

泛型

如果你使用 TS,那么对泛型肯定不陌生。那泛型是什么?简单来讲,泛型就是静态或动态用于指定某一种类型

使用上很简单,看个🌰:

// 静态指定List为字符串类型
List list1<String> = ['haha', 'hehe'];
// 动态指定某个类中成员变量类型
class Person<T> {
  T age;
  Person(this.age);
}
var person1 = Person<String>('18');
var person2 = Person<int>(18);

那么,使用泛型的作用有哪些?

  • 正确指定泛型可以提高代码质量
  • 使用泛型可以减少重复冗余的代码

第一点好理解,但是第二点如何理解?

举个例子,在项目我们创建一个用于缓存对象的接口:

abstract class ObjectCache {
  Object getByKey(String key);
  void setByKey(String key, Object value);
}

然后突然有一天,发现还需要定义一个用于缓存字符串的接口,接着你又会这样去写:

abstract class StringCache {
  String getByKey(String key);
  void setByKey(String key, String value);
}

泛型就是能够通过动态指定类型来复用代码,如下:

abstract class ValueCache<T> {
  T getByKey(String key);
  void setByKey(String key, T value);
}

接下来,我们就来看看在哪些地方可以用上泛型。

List和Map中泛型使用

类型的检查可以使用 runtimeType 属性来读出。

在 List 中可以直接限制存储数据类型。

List list1<String> = ['haha', 'hehe'];
List list2 = [123, 'hihi'];
List list3 = <String>['hehe', 'hihi'];
print(list1.runtimeType); // List<String>
print(list2.runtimeType); // List<Object>

同样地,在 Map 中使用泛型其实是一个样子的。

Map<String, String> map1 = {'haha': 'hehe'};
Map map2 = <String, String>{'hehe': 'hihi'};

类中泛型的使用

接着看上面那个缓存栗子🌰,我们一开始可能只会存储对象类型,但是为了以后更好拓展,我们需要根据输入的类型来动态存储相应的类型。

为了能够动态根据输入类型来存储相应的数据类型,有两种实现方案:

  1. 直接使用Object类型。

    abstract class ValueCache {
      Object getByKey(String key);
      void setByKey(String key, Object value);
    }
    class Cache extends ValueCache {
      Map value = <String, Object>{};
      Object getByKey(String key) => value[key];
      void setByKey(String key, Object value) => this.value[key] = value;
    }
    void main() {
      Cache temp = Cache();
      temp.setByKey('test', 123);
      print(temp.getByKey('test').runtimeType); // int
    }
  2. 使用泛型

    abstract class ValueCache<T> {
      T getByKey(String key);
      void setByKey(String key, T value);
    }
    class Cache extends ValueCache<num> {
      Map value = <String, num>{};
    	num getByKey(String key) => value[key];
    	setByKey(String key, num value) => this.value[key] = value;
    }
    main() {
      Cache cache = Cache();
      cache.setByKey('haha', 123.3);
      print(cache.getByKey('haha').runtimeType); // double
    }

在使用泛型过程中,如果希望存储的类只能是 num 类型呢?如何处理?其实也有两种方式:

  • 直接写死 num 类型,缺陷就是每次都要写类型,一旦更改全部都要手动更改
  • 继承 num 类型(这种写法更妥),即使改动也只需要改动一个地方即可。
// 直接写死num类型
abstract class ValueCache<num> {
  num getByKey(String key);
  void setByKey(String key, num value);
}

// 继承num类型
abstract class ValueCache<T extends num> {
  T getByKey(String key);
  void setByKey(String key, T value);
}

方法参数中泛型的使用

泛型除了应用在类中,还可以应用在方法参数中,格式大体一致。如下:

T getList<T>(List<T> list) {
	return list[0];
}
Map<K, T> getMap<K, T>(K key, T value) {
	return <K, T>{ key, value };
}

main() {
  List list = ['haha', 'hehe'];
  print(getList(list).runtimeType); // String
  print(getMap(123, 'hehe')); // { 123: 'hehe' }
}

库的导入以及导出

在 Dart 2.0 中,库的导入以及导出常用的方式是import/export

当然,也可以使用part关键字将一个Dart文件进行拆分,但是官方已经不再建议使用,都是建议import/export方式。举个🌰:

// math.dart
num sum(num num1, num num2) => num1 + num2;

// util.dart
library util;
export './math.dart';

// main.dart
import 'lib/util.dart';
main() {
  print(sum(1, 1.1));
}

另外,Dart 还支持懒加载库文件,在需要时候再进行加载。好处如下:

  • 减少 APP 启动时间。
  • 执行 A/B 测试,测试不同的实现。

其中,懒加载需结合async/await来实现

import 'package:greetings/hello.dart' deferred as hello;

// 在需要用到hello这个方法地方
Future greet() async {
  await hello.loadLibrary();
  hello.printCreeting();
}

可以看到,要懒加载一个库,必须遵守以下规则:

  • 先使用deferred as来导入
  • 在需要用到的地方,调用loadLibrary函数来加载库
  • 必须结合async/await 使用,而且调用库中方法时必须要在await之后

异步模型

在 Dart 中,实现异步操作主要是使用Future以及async/await,其中Future完全可以理解成 JS 中的Promise

另外,Dart 和 JS 一样,都是单线程的

要理解Future,你就得脑补一下 JS 的Event Loop

深入了解Future

Future 在执行的过程中,可划分为两种状态,分别为

  • 未完成状态(uncompleted):表示 Future 正在执行内部操作时。
  • 完成状态(completed):表示 Future 内部操作已完成,会返回一个值或抛出异常。

我们先来看个 Dart 中同步阻塞的🌰:

import 'dart:io';
String getData() {
  sleep(Duration(seconds: 3));
  return 'hahaha';
}
main() {
  print(1);
  print(getData());
  print(2);
}

上述代码执行后,会先输出1,然后隔三秒后再接着输出'hahaha'和2。

明显,同步阻塞会影响到用户的体验,那么我们可以使用 Future 来优化。

import 'dart:io';
Future<String> getData() {
  return Future<String>(() {
  	sleep(Decoration(seconds: 3));
  	return 'hahaha';
  })
}
main() {
  print(1);
  final getDataFuture = getData();
  getDataFuture.then((value) {
  	print(value);                   
  })
  print(2);
}

使用 Future 来包装异步模块后,上述的代码执行会先输出1和2,等到3秒后再输出'hahaha'。

对于 Future 中抛出异常时,使用的是 catchError(与 Promise一样)。

main() {
  print(1);
  final getDataFuture = getData();
  getDataFuture.then((value) {
  	print(value);                   
  }).catchError((err) {
  	print(err);         
  })
}

当然,Future 能像 Promise 一样进行链式调用

Future 还有如下 API:

  • Future.value():指定一个完成值。

    main() {
      print(1);
      Future.value(2).then((value) {
      	print(value);                     
      });
    	print(3);
    }
    
    // 1
    // 3
    // 2
  • Future.error():指定一个错误值。

    main() {
      Future.error('hehe').catchError((error) {
    		print(error);                               
    	});
    }
    
    // 'hehe'
  • Future.delayed(时间, 回调函数):延迟一定时间执行回调函数。

    main() {
      Future.delayed(Duration(seconds: 3), () {
      	return '3秒后信息';               
    	}).then((value) {
      	print(value);        
      });
    }
    
    // 3秒后会输出3秒后信息

async/await

脑补一下 ES7 中的async/await,可以很容易发现其实是一个意思,可能在写法位置上可能会稍微的不一样,其他都是一样的。我们直接来看个🌰:

Future<String> getData() async{
  final result = await Future.delayed(Duration(seconds: 3), () {
  	return 'haha';                                    
	});
	return 'This is $result.';
}
main() async{
  final str = await getData();
  print(str);
}

// 3秒后输出haha

可以看到,写法上大体一致,就是 async 在 JS 中习惯写在函数名的前面,而在 Dart 中需要写到{}函数体前面

Event Loop

说起 Dart 的事件循环,想必你肯定会提到 JS 中Event Loop。不可否认,Dart 和 JS 中的Event Loop都是为了解决单线程中异步模型的方案

事件队列和微任务队列

Dart 中可以将所有事件区分为事件队列微任务队列。所有事件就包括有IO、点击、绘制以及计时器等。

其中,微任务队列中事件执行优先级要高于事件队列中事件

那么,在 Dart 中是如何执行Event Loop的?

  • Dart 的代码经过编译后,会先执行 main 中函数
  • main 函数执行完,一个事件循环Event Loop就会正式启动,然后开始执行队列中的任务
  • 先观察微任务队列中是否为空,若不是则会按照先进先出顺序执行微任务队列中的任务
  • 执行完微任务队列中所有任务后,接着观察事件队列中任务,同样按照先进先出的顺序执行事件队列中任务

Dart 中可通过 async 下的scheduleMicrotask方法创建一个微任务

import 'dart:async';
main() {
  scheduleMicrotask(() {
    print('haha');
  });
}

问题来了,事件队列和微任务队列队列中存的任务都会有哪些?

  • 微任务队列:main中主体代码、scheduleMicrotask中回调函数、Future的then回调函数。
  • 事件队列:Future回调函数。

看个🌰:

main() {
  final future1 = Future(() {
    print(1);
    scheduleMicrotask(() => print(7));
    return 2;
  });
  final future2 = Future(() {
    print(3);
    return 4;
  });
  future2.then((value) => print(value));
  future1.then((value) => print(value));
  scheduleMicrotask(() => print(5));
  print(6);
}

// 输出的顺序依次为:6--5--1--2--7--3--4

我们来分析一下:

  • main中主体代码因为会放进微任务队列中,因此优先执行,输出6。
  • 由于main主体代码执行了scheduleMicrotask,也放进了微任务队列,输出5。
  • 微任务队列已清空,再看看事件队列,由于future1先注册了,因此先执行,输出1。
  • 上面提及过,then会被注册到微任务队列中,与此同时,future1函数体内有又调用了scheduleMicrotask,因此scheduleMicrotask 中回调函数会注册到微任务队列中,排在future1的then的后面,所以future1中函数体执行完后,将优先权给回微任务队列,先输出2,然后输出7。
  • 微任务队列已清空,future2注册进事件队列,因此会输出3,最后微任务队列中有future2的then,因此输出4。

Vue3.0 都有哪些新特性

在接下来要出现的 Vue 3.0 中,它更注重的点就在于以下几点。

  • 更快
  • 更小
  • 更易于维护
  • 更好的多端渲染支持
  • 新功能

那么下面我就看看都有哪些大的改动。

Virtual DOM重构

在 Vue 2.x 中,Virtual DOM 的最小粒度就是组件,那么只有相应的组件发生改变时,才会执行相应的 compiler --> render --> generate 三个阶段。

那么问题来了,在旧版本的 Vue 中,每次的更新都会遍历整个模板进行编译,包括很多静态节点每次更新导致的静态节点的重复遍历就会导致很多性能浪费

其实在 Vue 2.0 后续的版本中,已经有对这一步进行相应的优化,那就是将解析得到的 AST 进行相应的静态节点或静态树标注,这样可以避免下次在遍历的过程中直接跳过。但是另外一个问题出来了,那就是依然还需要对整个模板 Template 进行相应的遍历

在 Vue 3.0 之后,就采取动静结合策略,来处理这种情况。

简单来说,就是在编译时,现将 Template 中有用到指令的节点记录保存下来,并存到一个数组当中,那么下一次更新遍历时,只需要对该数组进行相应的遍历更新即可。这样就极大滴将 Virtual DOM 的最小粒度将到了动态指令当中,而对于那些静态节点只会在第一次编译时处理,其他情景都会忽略。

当然,动静结合使得一个 VNode Tree 直接划分成了一个区块树 Block Tree。拿尤大大 PPT 中的一张图作为示例。

转化后的区块树长的是这个模样。

Proxy 代理数据绑定

相信我们知道,在 Vue 3.0 版本之前,都是基于 Object.defineProperty 实现相应的数据绑定,通过设置相应的 getter 以及 setter 来将一个对象的属性进行响应式设置。

那么问题来了,对于以下的情况,Object.defineProperty 是无法进行相应的数据绑定的。

  • 直接向对象新增属性。
  • 对于数组中通过索引值新增元素值。
  • 通过数组的 length 改变其数组的元素项。

对于上述的问题,Vue 还特意使用 $set 全局 API 实现上述的监听。为此在 Vue 3.0 之后,便不再需要担心这些问题的出现啦,通过 ES6 中的 Proxy API 可以完美解决上述的问题。

有兴趣的童鞋可以直接跳到以下链接看看。

Proxy API

抛弃 Class API,拥抱 Function-based API

class API 固然可以更好滴支持 TS,但是依旧存在的问题就是,Props 和其他需要注入到 this 的属性仍然存在声明类型的问题。另外就是,除了类型支持外,class API 并没有带来更好的优势

那么我们先看看,所谓的 Function-based API 长的是什么样子的。

const App = {
    setup() {
        // data
        const count = value(0)
        // computed
        const plusOne = computed(() => count.value + 1)
        // method
        const increment = () => { count.value++ }
        // watch
        watch(() => count.value * 2, v => console.log(v))
        // lifecycle
        onMounted(() => console.log('mounted!'))
        // 暴露给模板或渲染函数
        return { count }
    }
}

相比 class API,Function-based API 具有以下优势。

  • 更好的 TS 类型推到支持。
  • 更好的逻辑复用能力。
  • Tree shaking更加友好。
  • 代码更容易压缩。

另外的一点,Function-based API 对于 Mixin 特别友好。

需要说明一点,原本 Mixin 会存在以下问题。

  • 容易导致命名空间冲突。
  • 模版数据来源不清晰。

那么 React 中的高阶组件依然存在着问题。

  • Props 命名空间冲突。
  • Props 来源不清晰。
  • 额外的组件实例性能消耗。

但是使用 Function-based API 后,将会更友好滴解决以上问题。

// 旧Mixin写法
const mousePositionMixin = {
  data() {
    return {
      x: 0,
      y: 0
    }
  },
  mounted() {
  	window.addEventListener('mousemove', this.update) 
  },
  destroyed() {
    window.removeEventLister('mousemove', this.update)
  },
  methods: {
    update(e) {
      this.x = e.pageX
      this.y = e.pageY
    }
  }
}

// Function-based API
const useMousePosition = function() {
  const x = value(0)
  const y = value(0)
  const update = e => {
    this.x = e.pageX
    this.y = e.pageY
  }
  onMounted(() => {
    window.addEventListener('mousemove', this.update) 
  })
  onDestroyed(() => {
    window.removeEventLister('mousemove', this.update)
  })
  return {x, y}
}

// 在组件中使用Function-based API包装的Mixin
new Vue({
  template: '...',
  data() {
    const {x, y} = useMousePosition()
    return {x, y}
  }
})

Function-based API 跟 React Hooks 非常的类似,同样具有逻辑复用功能。但是唯一的一点不同在于,React Hooks 在组件更新时都会将所有的 Hooks 都调用一遍,而 Function-based API 则只会调用一次

常见链表算法

对链表数据结构常见的算法进行总结,也为了更好滴应对后面深入学习算法。

从尾到头打印链表

输入一个链表,按链表值从尾到头的顺序返回一个数组。

答案

  const returnReverseLink = head => {
    const result = [];
    while (head) {
      result.unshift(head.val);
      head = head.next;
    }
    return result;
  }
  

反转链表

输入一个链表,反转链表后,输出新链表的表头。

答案

  const reverseLink = head => {
    let currentNode = null;
    let headNode = head;
    while (head && head.next) {
      currentNode = head.next;
      head.next = currentNode.next;
      currentNode.next = headNode;
      headNode = currentNode;
    }
    return headNode;
  }
  

复杂链表的复制

输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针指向任意一个节点),返回结果为复制后的复杂链表的head。

答案

  const copyComplexLink = head => {
    if (!head) return null;
    copyLink(head);
    copyRandomLink(head);
    return connectLink(head);
  }
  const copyLink = head => {
    let currentNode = head;
    while (currentNode) {
      const cloneNode = {
        val: currentNode.val,
        next: currentNode.next
      };
      currentNode.next = cloneNode;
      currentNode = cloneNode.next;
    }
  }
  const copyRandomLink = head => {
    let currentNode = head;
    while (currentNode) {
      const cloneNode = currentNode.next;
      const random = currentNode.random;
      if (random) {
        cloneNode.random = currentNode.random.next;
      } else {
        cloneNode.random = null;
      }
      currentNode = cloneNode.next;
    }
  }
  const connectLink = head => {
    const cloneHead = head.next;
    let cloneNode = head.next;
    let currentNode = head;
    while (currentNode) {
      if (cloneNode.next) {
        currentNode.next = cloneNode.next;
        cloneNode.next = cloneNode.next.next;
      } else {
        currentNode.next = null;
      }
      currentNode = currentNode.next;
      cloneNode = cloneNode.next;
    }
    return cloneHead;
  }
  

合并两个排序的链表

输入两个单调递增的链表,输出两个链表合成后的链表,当然我们需要合成后的链表满足单调不减规则。

答案

  const concatLink = (aHead, bHead) => {
    if (!aHead) return bHead;
    if (!bHead) return aHead;
    let head;
    if (aHead.val > bHead.val) {
      head = bHead.val;
      head.next = concatLink(aHead, bHead.next);
    } else {
      head = aHead.val;
      head.next = concatLink(aHead.next, bHead);
    }
    return head;
  }
  

链表倒数第K个节点

输入一个链表,输出该链表中倒数第K个节点。

答案

  const getKNode = (head, k) => {
    if (!head || !k) return null;
    let front = head;
    let behind = head;
    let index = 1;
    while (front.next) {
      index++;
      front = front.next;
      if (index > k) {
        behind = behind.next;
      }
    }
    return index >= k ? behind : null;
  }
  

链表中环的入口节点

给一个链表,若其中包含环,请找出该链表的入口节点,否则,输出null。

答案

  const getHuanNode = head => {
    if (!head || !head.next) return null;
    let p1 = head.next;
    let p2 = head.next.next;
    // 判断是否有环,若有环最终两个指针肯定会相交
    while (p1 !== p2) {
      if (!p2 || !p2.next) return null;
      p1 = p1.next;
      p2 = p2.next.next;
    }
    // 获取环的长度
    let length = 1;
    let tNode = p1;
    p1 = p1.next;
    while (p1 !== tNode) {
      length++;
      p1 = p1.next;
    }
    // 从头指针开始,根据长度来获取环的起点
    p1 = head;
    p2 = head;
    while (length !== 0) {
      p2 = p2.next;
      length--;
    }
    while (p1 !== p2) {
      p1 = p1.next;
      p2 = p2.next;
    }
    return p1;
  }
  

两个链表的第一个公共节点

输入两个链表,找出它们的第一个公共节点。

答案

  const getCommonNode = (aHead, bHead) => {
    if (!aHead || !bHead) return null;
    const aLen = getLinkLength(aHead);
    const bLen = getLinkLength(bHead);
    let long, short, diff;
    if (aLen > bLen) {
      long = aHead;
      short = bHead;
      diff = aLen - bLen;
    } else {
      long = bHead;
      short = aHead;
      diff = bLen - aLen;
    }
    while (diff--) {
      long = long.next;
    }
    while (long) {
      if (long === short) return long;
      long = long.next;
      short = short.next;
    }
    return null;
  }
  const getLinkLength = head => {
    let length = 0;
    let currentNode = head;
    while (currentNode) {
      length++;
      currentNode = currentNode.next;
    }
    return length;
  }
  

圆圈中最后剩下的数字

0,1,...,n-1这n个数字排成一个圆圈,从数字0开始,每次从这个圆圈中删除第m个数字,求出这圆圈中剩下的最后一个数字。

答案

  const getLastNode1 = (n, m) => {
    if (!n || !m) return -1;
    const head = {
      val: 0,
      next: null
    }
    let currentNode = head;
    for (let i = 1; i < n; i++) {
      let tempNode = {
        val: i,
        next: null
      }
      currentNode.next = tempNode;
      currentNode = tempNode;
    }
    currentNode.next = head;
    currentNode = head;
    while (currentNode.next !== currentNode) {
      for (let i = 1; i < m; i++) {
        currentNode = currentNode.next;
      }
      currentNode.next = currentNode.next.next;
    }
    return currentNode.val;
  }
  const getLastNode2 = (n, m) => {
    if (!n || !m) return -1;
    const arr = [];
    for (let i = 0; i < n; i++) {
      arr[i] = i;
    }
    let index = 0;
    while (arr.length > 1) {
      index = (index + m) % arr.length - 1;
      if (index >= 0) {
        arr.splice(index, 1);
      } else {
        arr.splice(arr.length - 1, 1);
        index = 0;
      }
    }
    return arr[0];
  }
  const getLastNode3 = (n, m) => {
    if (!n || !m) return -1;
    return handleLastNode2(n, m);
  }
  const handleLastNode3 = (n, m) => {
    if (n === 1) return 0;
    return (handleLastNode3(n - 1, m) + m) % n;
  }
  

删除链表中的节点

给定单链表的头指针和要删除的指针节点,在O(1)时间内删除该节点。

答案

  const deleteNode = (head, node) => {
    if (node.next) { // 若指针不是尾指针时,直接拿下一个指针覆盖即可
      node.val = node.next.val;
      node.next = node.next.next;
    } else if (node === head) { // 若指针是尾指针且只有一个指针时,直接设为空即可
      node = null;
      head = null;
    } else { // 尾指针且不止一个指针时,只能从头遍历,但是也就只能出现1/n次,因为它是尾元素,因此可以间接达到O(1)时间
      node = head;
      while (node.next.next) {
        node = node.next;
      }
      node.next = null;
      node = null;
    }
    return node;
  }
  

删除链表中重复的节点

给定一个单链表,删除出现次数大于1的节点。

答案

  const deleteRepeatNode1 = (head) => {
    if (!head || !head.next) return head;
    let currentNode = head;
    if (currentNode.val === currentNode.next.val) {
      let tempNode = currentNode.next;
      while (tempNode === currentNode.val) {
        tempNode = tempNode.next;
      }
      return deleteRepeatNode1(tempNode);
    } else {
      head.next = deleteRepeatNode1(head.next);
      return head;
    }
  }
  

贪心算法

所谓贪心,顾名思义就是在当前环境下选择最优的方案(即局部最优方案),从而使得后面的结果是全局最优的方案。

贪心算法并不能保证局部所选的最优方案对于最后的结果也是最优的,因此它也是动态规划的一个子集

而且,贪心算法的使用最简单的方式就是,通过遍历,然后每次遍历都拿最优的解出来即可。

找零问题

假设你是一个商店老板,你需要给顾客找零n元钱,你手上有的钱的面值为:100元,50元,20元,5元,1元。请问如何找零使得所需要的钱币数量最少?

该例子其实在我们的生活中很常见,最简单就是每次都拿最大面值的零钱。

举个最简单的例子,需要找零131元,那么我们可以这样:

  • 先从最大面值100元开始,减去100元,剩下31元。
  • 再看50元面值,明显是大于31元,直接跳过。
  • 再看20元面值,减去20元,剩下11元。
  • 再看5元面值,减去5元,剩下6元。
  • 可以再减5元,剩下1元。
  • 因此总共花费100元、20元、5元、5元、1元,最少找零的数量为5个。

可以看到,每次都先从最大面值开始选择,符合贪心算法中的局部最优方案,从而得到最终最优解。

实现如下:

const changeMoney = n => {
  const money = [1, 5, 20, 50, 100];
  const result = [];
  money.sort((a, b) => b - a);
  for (let i = 0; i < money.length; i++) {
    if (n >= money[i]) {
      let mul = parseInt(n/money[i]);
      n -= mul * money[i];
      while (mul) {
      	result.push(money[i]);
        mul--;
      }
    }
  }
  return result;
}

但是找零问题会有些特殊情况并不适合使用贪心算法,举个例子:

现在面值的零钱有1元、5元、11元,这时候要找零15元,该如何处理?

如果单纯按照贪心算法,首先会选择1个11元,然后再选4个1元,总共花费了5个零钱。但是直接找3个5元不是更少吗?

是的,找零问题并不完完全全适合贪心算法,那么什么样的找零才能适合贪心呢?

按照这篇文章怎样的币值可以用贪心算法进行找零的解析,人民币肯定符合!!!但是只要满足以下其中一个条件都能满足贪心算法。

  1. a[i] 和 a[i + 1] 存在倍数关系,即 a[i + 1] = k * a[i]
  2. a[i] 和 a[i + 1] 不存在倍数关系,这时候只需要找到一个整数 k,满足:(k - 1) * a[i] < a[i + 1] 并且 k * a[i] >= a[i + 1]

背包问题

有一个小偷,进到一个店里偷东西,店里每个东西的价值是v,每个东西的重量是w。但小偷只有一个背包,总共能承受的重量是W。请问怎么拿东西能让他拿到的价值最大?

例如:

商品1: v1 = 60,w1 = 10;

商品2: v2 = 100, w2 = 20;

商品3: v3 = 120,w3 = 30;

背包容量:W = 50;

背包问题可以细分为两类问题,分别为:0-1背包和分数背包。

  • 0-1背包:要么全拿走,要么就不拿,直到容量不够为止。
  • 分数背包:不够容量时,可拿商品的一部分走,直到容量耗尽为止。

我们先来看看0-1背包,如果使用贪心算法,我们肯定会选择价值最大的(价值最大即价值/重量最大),因此会选择v1,剩余40容量,再选择v2,剩下容量20,明显是没法再拿v3的,但这就是最优解吗?明显不是,如果我选择v2和v3,刚好用完背包容量,明显这才是最优解,最终可以得出0-1背包问题并不适合使用贪心算法,也就只能使用动态规划。

const products = [
  { v: 60, w: 10 },
  { v: 100, w: 20 },
  { v: 120, w: 30 }
];
const steal = (w, products) => {
  products.sort((a, b) => (a.v/a.w) - (b.v/b.w));
  const result = [];
  for (let i = 0; i < products.length; i++) {
    result[i].push([]);
    for (let j = 0; j <= w; j++) {
      if (j === 0) {
        result[i][j] = 0;
      } else if (i === 0) {
        result[i][j] = j > products[i].w ? products[i].v : 0;
      } else {
        let getLast = 0;
        if (j > products[i].w && j - products[i].w) {
          getLast = products[i].v + result[i - 1][j - products[i].w];
        }
        result[i][j] = Math.max(result[i - 1][j], getLast)
      }
    }
  }
  return result[products.length - 1][w];
}

好了,我们再来回顾一下分数背包问题,当不够容量时,可以直接拿一部分,那么这样一来肯定满足贪心算法了。

const steal = (w, products) => {
  const result = [];
  products.sort((a, b) => (b.v/b.w) - (a.v/a.w));
  for (let i = 0; i < products.length; i++) {
    if (w >= products[i].w) {
      result.push(products[i].v);
      w -= products[i].w;
    } else {
      const temp = w / products[i].w;
      result.push(products[i].v * temp);
      w = 0;
    }
  }
  return result;
}

数字拼接问题

有n个非负整数,将其按照字符串拼接的方式拼接为一个整数,使得拼接后的整数最大。

例如:32,94,128,1286,6,71,那么拼接到的最大整数位94316321286128。

对于两个数 a 和 b,最简单的做法就是判断 ab 和 ba 两个数哪个最大,然后决定 a 和 b 的位置。而这也刚好满足了贪心的**,局部最优就是拿两个数组合的最大值。

const getMaxNumber = arr => {
  if (!arr || !Array.isArray(arr) || arr.length <= 1) return arr;
  arr.sort((a, b) => `${b}${a}` - `${a}${b}`)
  return arr.join('');
}

活动选择问题

假设有 n 个活动,这些活动要占用同一片场地,而场地在某时刻只能提供一个活动使用。

每个活动都有一个开始时间 Si 和结束时间 fi(题目中时间以整数表示),表示活动在 [Si, fi) 区间占用场地。

那么,安排那些活动能够使该场地举办的活动的个数最多?

要使得举办的活动个数最多,那么转过来想,那就是将更多的时间留给后面活动,这样一来,在后面就可以更好滴安排活动。

换句话说,应该把结束时间最早的活动安排在第一个,再在剩下的时间里面继续安排结束时间早的活动。因此是符合贪心算法的。

const setPlan = arr => {
  if (!arr || !Array.isArray(arr)) return arr;
  arr.sort((a, b) => a.end - b.end);
  const result = [];
  let end = 0;
  for (let i = 0; i < arr.length; i++) {
    if (arr[i].start >= end) {
      result.push(arr[i]);
      end = arr[i].end;
    }
  }
  return result;
}

分发饼干

假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。

对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。

要满足尽可能多的孩子能够享受到饼干,用贪心的**,那么就是优先安排胃口值低的孩子,那么后面的孩子就可以在剩下的饼干得到享受。

const findContentChildren = function(g, s) {
  g.sort((a, b) => a - b);
  s.sort((a, b) => a - b);
  let result = 0;
  let gIndex = 0;
  let sIndex = 0;
  while (gIndex < g.length && sIndex < s.length) {
    if (g[gIndex] <= s[sIndex]) {
      result++;
      gIndex++;
    }
    sIndex++;
  }
  return result;
}

贪心算法的核心,每次都得

那么怎么贪?一句话解析,就是尽可能使得当前结果是最好的,以及对于后续的结果也是最好的

比如上面的活动安排,要尽可能安排多个活动,那么就应该考虑先安排结束时间最早的,然后后面的活动才可以有更多的时间进行安排。

又比如上面的分发饼干,要尽可能每个孩子都能分发饼干,那么就应该先将胃口值低的孩子先安排,那么后面的孩子才有更多饼干去选择。

但是,并不一定就可以使得最后的结果是最好的,比如上面的 0-1 背包问题,明显就无法,要结合题目进行分析。

CSS 如何做性能优化

我相信很多童鞋们肯定被问过不少,在面试时,面试官也会经常询问的一道题目。

在开发过程中,我们都知道如何在 JavaScript 层面对性能进行优化。那么在 CSS 层面呢?你会选择如何回答。接下来,我就简单滴说一说 😄。

针对首屏页面,应该选用内敛样式

首先必须知道的是,在 HTML 中我们一般都会使用 link 标签引入相应的 CSS 文件进行解析,那么样式下载速度将会直接影响到 HTML 文档的解析,也就直接导致了白屏。

首屏对于哪一个页面或者是 APP 都显得尤为的重要,那么使用内敛样式在针对首页进行处理的话,就不需要等待下载时间了,而是直接滴解析便可。

压缩 CSS

主要是通过 Webpack 进行相应的压缩,可在 css-loader 中启动 minimize 属性来开启 CSSNano,接着可以压缩相应的 CSS 文件。

Preload 异步加载 CSS

需要理解的是,CSS 会阻塞渲染,也就是说,浏览器在解析文档时,必须要等到 CSS 文件下载完成后才会去做真正的渲染过程。使用 Preload 加载资源会告诉浏览器提前加载相应的资源,并将它们存储到缓存当中,当真正执行时便会直接从缓存中获取。

那么,对于一些非必须的 CSS 使用 Preload 预获取的话,就可以先获取资源,接着浏览器在解析相应的 HTML 文档后,就无须等待下载而是直接从缓存中获取相应资源即可

剔除无用 CSS 代码

在开发过程中,由于一些新的业务迭代,导致原来的页面已经不可用,那么我们就有必要删掉它们 CSS,没必要保留,最愚蠢的方法便是一个一个找出来,毕竟这是很耗时间的。。。

当然也可以借助第三方库实现剔除无用的 CSS 代码,如 uncss。

考虑 BFC 元素

由于我们平时定义的元素在使用 JS 操作过程中,很容易就会导致页面的回流,那么考虑使用 BFC 元素,可以很好避免这个问题,首先 BFC 元素跟外部元素是不会有任何的联系的,它只会让它内部进行相应的回流或者重绘,这样可以很好滴减少整个页面的回流。

合理使用选择器

浏览器在解析 CSS 代码时,都会是直接从右向左进行解析的,主要就是因为每个子节点至多只有一个父节点,那么,不符合就可以直接抛弃,相反一个父节点会有很多个子节点的可能性,所以在正向解析过程会导致很多性能问题。

那么合理使用选择器可有效减少查询过程。

避免使用 @import

@import 引入流程会破坏浏览器的解析流程。浏览器在下载完 link 标签中 CSS 后,会开始解析里面代码,当在解析过程发现使用 @import,又会继续开始新一轮的等待下载时间,这样一来便会阻塞了渲染流程。

所以对于引入样式,尽量使用 link 标签进行引入,避免使用 @import

微前端

可以参考好的文章,后面我会做一个简答的总结。😄

微前端存在的意义和价值

微前端(可以说是一种类似微服务的架构,它将微服务理念应用于浏览器端),就是将单页面应用拆分为多个小型应用所组成的应用。

微前端架构锁拆分出来的小应用,都是可以独立运行、独立开发、独立部署的。

微前端架构的好处在于以下几个方面。

  1. 降低代码耦合。
  2. 适合迁移大型应用代码,单独拆分出来应用可以独立部署,不影响整成使用。
  3. 团队可在拆分应用中安排人手独立开发和维护。

实现微前端常见方式

  1. iframe

    使用iframe容器注入应用,可以说是最古老也是效果不错的方式。iframe容器中可以独立管理不同应用,然后通过postmessage通信或者iframeEl.contentWindow通信

  2. HTTP服务器路由跳转

    最常见的就是使用Ngix HTTP服务器,先将应用拆分为两个小应用,并独立部署后,根据路由跳转到相应的小应用中,但需要注意的是,小应用之间是独立开发的,并不产生耦合条件

  3. 第三方框架实现

    常见的如single-spa框架,其实现原理就是将每个子应用都打包成一个独立的js文件,通过registerApplication接口来约定应用于主框架交互行为

动态规划

借用一个比较典型的生活例子:

有一天,有个爸爸想考一下儿子的数学,就问“儿子,1 + 1 = ?”。

“好简单啊,就是2”。

“好,如果我让你在原来那个式子前面补上 1 +,结果是多少呢?”。

“原来的式子 1 + 1 = 2,那么左边再补上 1 +,就相当于 1 + 2,结果就是3”。

动态规划的定义就是把一个大问题拆解成一个或多个小问题进行求解。

按照定义,我们可以将上面的例子进行转换一下,已知2个1的结果是2,那么3个1的结果会是多少?显然易见,大问题就是3个1,那么我们可以将它拆解一下,就可以得到3个1的结果 = 2个1的结果 + 1,以此类推,我们只需要知道中间的某个计算值即可得到最终大问题的结果。

那么,你会发现里面会存在一个动态规划的关键点,那就是每个小问题的解都会被存储起来,重复调用结果后从而得到大问题的解

也许上面的例子看的不是很明显,再举一个例子:

有 N 个阶梯,一个人每一步只能跨一个台阶或者两个台阶,那么到达 N 个阶梯后总共有多少种跨法?

从题目我们可以分析一下:

  • 大问题:N个阶梯的跨法。
  • 拆解小问题:N - 1 个阶梯再走一步就可以到 N 个阶梯,N - 2 个阶梯再走两步就可以到达 N 个阶梯。

因此,我们很容易得到大问题和小问题之间关系:N个阶梯跨法 = (N - 1 个阶梯跨法 + 1) + (N - 2 个阶梯跨法 + 2)

而大问题和小问题之间的关系恰好就是所谓的状态转移方程

这样一来,我们就可以通过代码来实现 N 个阶梯的跨法:

const getResult = n => {
  if (!n) return 0;
  const result = [];
  result[0] = 1;
  result[1] = 1;
  for (let i = 2; i <= n; i++) {
    result[i] = result[i - 1] + result[i - 2];
  }
  return result[n];
}

你会发现,N 个台阶最终都会重复调用 N - 1个台阶和 N - 2 个台阶结果,这也正是上面将每个小问题的结果存储起来重复调用后得到最终大问题的解。

原理大概有所了解了,那么我们就继续看看其他例子深入理解一下。

斐波那契数列

0、1、1、2、3、5、....,求第 N 个数是多少?

从题目分析一下:

  • 大问题:第 N 个数的结果。
  • 拆解小问题:第 N 个数刚好就是第 N - 1 个数和第 N - 2 个数的和。

因此,我们可以很容易得到状态转移方程:第 N 个数 = (第 N - 1 个数) + (第 N - 2 个数)。

代码实现如下:

const getResult = n => {
  const result = [];
  result[0] = 0;
  result[1] = 1;
  for (let i = 2; i <= n; i++) {
    result[n] = result[n - 1] + result[n - 2];
  }
  return result[n];
}

钢条切割问题

某公司出售钢条,出售的价格与钢条长度之间的关系如下:

长度 i 1 2 3 4 5 6 7 8 9 10
价格 pi 1 5 8 9 10 17 17 20 24 30

现有一段长度为 N 的钢条和上面的价格表,求切割钢条方案,使得总收益最大。

从问题进行分析:

  • 大问题:长度为 N 的钢条的最大收益。
  • 拆解小问题:
    • 当 N 小于 10,那么最大收益就可以为 Max(n) = Math.max(p[n], p[n - 1] + Max(n - n - 1), p[n - 2] + Max(n - n - 2), ..., p[n] + Max(0))。
    • 当 N 大于 10,那么最大收益就可以为 Max(n) = Math.max(Max(n - 1) + Max(1), Max(n - 2) + Max(2), ..., Max(1) + Max(n - 1))。

代码实现如下:

const getResult = n => {
  const price = [0, 1, 5, 8, 9, 10, 17, 17, 20, 24, 30];
  const result = [0, 1];
  for (let i = 2; i <= n; i++) {
    result[i] = 0;
    const mid = parseInt((i - 1) / 2) + 1;
    for (let j = i; j >= mid; j--) {
      const temp = i <= 10 ? price[j] : result[j];
      result[i] = Math.max(temp + result[i - j], result[i]);
    }
  }
  return result[n];
}

最长公共子序列(LCS)

一个序列的子序列定义为:在该序列中删去若干元素后得到的序列,如 “ABCD” 和 “BDF” 都是 “ABCDEFG” 的子序列。

最长公共子序列问题:给定两个序列 X 和 Y,求 X 和 Y 长度最大的公共子序列。

举例:X = “ABBCBDE”,Y = “DBBCDB”,LCS(X, Y) = “BBCD”。

从题目进行拆解分析:

  • 大问题: 两个字符串的最长公共子序列,抽象为两个字符是否相等。
  • 拆解小问题:
    • 当 X 和 Y 两个字符相等时,那么 Result(X, Y) = Result(X - 1, Y - 1) + 1。
    • 当 X 和 Y 两个字符不相等时,那么 Result(X, Y) = Math.max(Result(X - 1, Y), Result(X, Y - 1))。

代码实现如下:

const getResult = (str1, str2) => {
  if (!str1 || !str2) return "";
  const arr1 = str1.split('');
  const arr2 = str2.split('');
  const result = [];
  for (let i = 0; i <= arr1.length; i++) {
    result[i] = [];
    for (let j = 0; j <= arr2.length; j++) {
      if (i === 0) {
        result[i][j] = "";
      } else if (j === 0) {
        result[i][j] = "";
      } else if (arr1[i - 1] === arr2[j - 1]) {
        result[i][j] = result[i - 1][j - 1] + arr1[i - 1];
      } else {
        result[i][j] = result[i - 1][j] > result[i][j - 1] ? result[i - 1][j] : result[i][j - 1];
      }
    }
  }
  return result[arr1.length][arr2.length];
}

Flutter 踩中的坑

相信各位在 Flutter 的开发中都踩过形形色色的坑,特别是构建环境,以下我也稍微总结一下自己在开发过程中遇到的一些小坑,方便日后遇到能够更快捷滴进行处理。

Could not determine the dependencies of task ':app:compileDebugJavaWithJavac'.

在构建安卓 Flutter 应用时,就遇到了这个错,网上处理的方式大概有三种,以 Mac 为例:

  1. 删掉项目中/android/.gradle文件,然后执行flutter clean

  2. Flutter安装目录下的Flutter/packages/flutter_tools/gradle/flutter.gradle文件中,将以下内容进行替换:

    private static final String DEFAULT_MAVEN_HOST = "https://storage.googleapis.com"
    
    // 替换成
    private static final String DEFAULT_MAVEN_HOST = "https://storage.flutter-io.cn/download.flutter.io"
  3. 依然是上述2种文件,将以下内容进行替换:

    buildscript {
        repositories {
            google()
            jcenter()
        }
      	// ...
    }
    
    // 替换成
    buildscript {
        repositories {
            // google()
            // jcenter()
          	maven { url 'https://maven.aliyun.com/repository/google' }
            maven { url 'https://maven.aliyun.com/repository/jcenter' }
            maven { url 'http://maven.aliyun.com/nexus/content/groups/public' }
        }
      	// ...
    }

其实第2、第3种方案都是为了解决科学上网问题,将资源获取更换为国内代理。

但很不幸的是,试完了以上方法我依然无法得到解决!!!

最后一种方案就是,在项目文件中/android/build.gradle,增加如下代码

allprojects {
    repositories {
        google()
        jcenter()
    }
}

// 增加代码
allprojects {
    repositories {
        google()
        jcenter()
        maven { url "http://download.flutter.io" }
    }
}

最后终于得到了解决~。

avdmanager is missing from the android sdk

在使用vscode开发Flutter时,当想启动安卓应用来跑程序,发现报了上面那个错。

这是因为,以 Mac 为例,有些童鞋在安装android sdk时可能会配置在~/.bash_profile,也可能配置在~/.zshrc,因此一定要细心查究

我就是上面两个配置不一致,才导致了问题出现。

解决方案就是,可以通过命令:

flutter config --android-sdk 安卓SDK安装的目录

只需要指定你安装好的android sdk目录位置,就可以唤起来


持续更新中~当然我也欢迎你们能够在评论区分享你们所遇到的坑啦👏

单点登录(SSO)的思考

如果做过大型网站的童鞋们,相信对单点登录肯定不陌生。就好比如天猫和淘宝,当我在淘宝处成功登录之后,再跳去天猫网站,你会发现,在天猫处也会神奇滴登录了。🤔

为什么会这样?没错,就是今天的主角——单点登录的“锅”。

毫无疑问,对于一个拥有多套子系统的公司来说,单点登录是必须实现的,它不仅可以减少开发者对于各个子系统对应的登录功能维护,还能更好滴提升用户体验。

那么问题来了,对于一个单点登录(SSO)的功能实现,可以从哪方面思考呢?下面我们就来一起探讨一下啦...

目录

  1. 传统登录和JWT
  2. 如何实现单点登录?

传统登录和JWT

在讲解之前,我觉得是很有必要提提登录的基本实现的。就好比如,如果我要让你实现一个登录功能,你会如何实现呢?

相信很多童鞋都会知道使用 Session 和 Cookie 实现,那么还有吗?哈哈,相信你也能看到,就是时下最为常用方式 JWT。那么问题来了,它们之间有什么区别?甭急,下面将会简单讲解一下。

传统登录

传统登录的实现其实就是使用 Session 和 Cookie 的,那先看看它们的处理流程是怎样。

  1. 客户端发送登录请求到服务器。
  2. 服务器将会根据客户端传过来的用户名和密码创建一个 session,session 中存储了相应用户角色、登陆时间等等信息。
  3. 服务器向客户端返回一个 sessionid,并写入客户端的 cookie(cookie 其实就相当于是一个用户凭证)。
  4. 客户端接下来的每次请求都会默认带上 cookie ,相当于将 sessionid 传回服务器。
  5. 服务器收到 sessionid,会找到前期保存的数据,由此得知用户的身份。

利用 Session 和 Cookie 技术实现的传统登录功能,过程并不难理解,简单滴说,就是服务端负责管理用户状态和验证,而客户端则负责存储用户状态

那么问题来了,服务端返回的 SessionId 一定是要使用 Cookie 存储吗?还是说可以存储在 SessionStorage 或者 LocalStorange 上?🤔

对于这个问题,其实也是我一直思考的,定性思考一番,其实也是有它的目的。首先可以明确的是,服务端返回的 SessionId 不一定要使用 Cookie 进行存储的,可以使用诸如 SessionStorage、LocalStorage 等方式进行存储的

那么使用 Cookie 存储服务端返回的 SessionID 有什么好处?主要是为了减轻前端开发者负担,由于服务端可以直接通过设置返回头 Set-Cookie 来直接在客户端的 Cookie 中存储 SessionId。这样一来,客户端就可以免去了手动保存 SessionId 的步骤,直接处理相应登录业务逻辑即可。

虽然使用 Cookie 可以很方便开发者的存储相应的用户状态,但是却会导致诸如 XSS、 CSRF 等安全性问题。

JWT

使用 Session 和 Cookie 技术实现的登录功能依然存在着缺陷。

首先就是 Cookie 允许被用户禁止,当一个浏览器禁止使用 Cookie 后,那么服务器中存储的 Session 也被禁止,直接导致相应的用户无法进行登录。另外,当用户访问量很大时,对服务器的压力也会大,因为用户访问量大将会触使服务器需要管理大量用户的 Session,想象一下,访问量达到亿级别时会怎样?毫无疑问,服务器管理的 Session 越多,对服务器本身压力也会大。

那么,有没有一种方式可以克服以上的缺陷呢?有~那便是 JWT。

JWT 是 Json Web Token 的缩写,一个规范用于用户和服务器之间传递安全可靠的信息。(简单滴说,就是所谓的 Token 机制)。让我们先看看 Token 机制的处理流程。

  1. 客户端发送请求到服务器。
  2. 服务返回一个经过加密的token,并由客户端负责存储,可存到 local storage 或 cookie。
  3. 客户端接下来的每次请求,都需要手动加上该 token。
  4. 服务器对 token 进行解码,如果 token 有效,则处理请求。
  5. 一旦用户登出,客户端就需要销毁 token。

Token 机制相对于传统登录方式的最大区别就是,所有的用户状态都交给客户端进行管理和存储

Token 可以看成是一张门票,当你想去看演唱会时,那必须得先展示门票。

一个 JWT 实际上就是一串字符串,由三部分组成,分别是

  • 头部 Header:指定了 JWT 使用的签名算法。
  • 消息体 Payload:包含了签名信息。
  • 签名 Signature:使用 base64url 编码的头信息和消息体拼接(使用.分隔)再通过私有的 secret 计算而来。
// 一个完整的Token可表示为
Token = encodeBase64(header) + '.' + encodeBase64(payload) + '.' + encodeBase64(signature)

Token 机制由于无需在服务端管理用户状态,因此极大滴减轻了服务器的压力,另一方面,由于在登录后的任何请求,都不会再在请求中带上用户标识,这样一来,就可以极大滴避免 CSRF 攻击。

如何实现单点登录?

在大型网站下,实现单点登录变得越来越重要,那么要实现单点登录具体有哪些方案呢?

要实现单点登录,个人总结认为,主要有 Session 共享机制、Cookie 跨域处理、CAS 服务 三种方案。(当然可能还有其他方案,欢迎各位大佬们来分享分享啦 😄)。那么我们就来简单分析一下。

Session 共享的实现,大部分工作都是需要后端配合的,一般涉及到集群方面等,当然如果使用的是 Node的话,可以使用 Redis 实现,将所有的 Session 都放在中间层管理。

下面就讲讲 Cookie 跨域处理和 CAS 服务两种方案,这两种方案也是前端开发者常常需要考虑的方案啦。

Cookie 跨域处理

如果你使用过 Cookie 的话,肯定会知道 Domain 。那么 Domain 究竟是拿来干嘛的?

Cookie 中 Domain 就是拿来约束使用 Cookie 的域名管理。举个例子,如果网站a.test.comb.test.com是可以通过 Cookie 进行通信的,因为 Domain 会被设置为test.com,相反如果网站a.test.comc.haha.com就无法通过 Cookie 进行通信,因为网站c.haha.com无法受到 Domain 限制而无法获取相应的 Cookie 值。

先回顾一下跨域的同源政策,只有同协议、同域名、同端口才符合同源政策

那么在跨域情况下,需要如何让 Cookie 进行通信呢?

  1. 当用户在网站a.test.com进行登录,后端根据用户名和密码判断该用户是网站c.haha.com中用户,会先在a.test.com登录后再返回标识给客户端。

  2. 客户端先配置好 axios 的请求头部字段withCredentials为 true。

    axios.defaults.withCredentials = true

    同时,运维需要在 Ngix 中配置接口允许 CORS,并且设置相应的响应头部字段。

    access-control-allow-credentials: true
    access-control-allow-origin: https://a.test.com

    必须要注意的是,CORS 在允许跨域处理 Cookie 时绝对不能设置 access-control-allow-origin 值为 *,只能针对性的域名

  3. 客户端根据标识调用相应网站的登录接口(如c.haha.com/ligon)去登录,登录成功后,后端会在c.haha.com的 Cookie 中存储 Token 值(此时还是在网站a.test.com中),这时候就可以跳转到c.haha.com主页。

  4. 去到c.haha.com主页后,获取相应 Cookie 中 Token 值配置在请求头部字段 authorization中,便可实现单点登录功能。

在上面的基本流程中,相信你肯定会有问题,甭急,让我们一个个来解决。

在 axios 的请求头部字段设置withCredentials的值为 true 有何用?

其实,withCredentials字段用于允许跨域处理 Cookie。但是单纯配置它是没法起效果的,必须要结合运维在响应头部加上 access-control-allow-credentials 和 access-control-allow-origin 两个字段才能起效果

既然在 CORS 中的 access-control-allow-origin 响应字段必须为针对性的域名而不能设置为 *,那么在本地开发时,如何配置?

要处理这个问题,有两种方案,一种是在 access-control-allow-origin 响应字段中增添一个域名为 localhost。另一种则是配置 webpack-dev-server 中 proxy,如下:

module.exports = {
  // ...
  proxy: {
    '/test/login': {
      traget: 'http://c.haha.com/login'
    },
    onProxyRes: function(proxyRes, req, res) { // 接口返回中间层处理
      // 针对c.haha.com/login登录接口返回手动存储cookie
    }
  }
  // ...
}

时下,使用 Cookie 跨域处理是改动最小方案,也是目前最优的选择方案。

CAS 服务

CAS 服务,相当于所有的网站的登录服务,都统一使用一个登录网站处理。

CAS 分为两部分,分别是 CAS Client(中间登录客户端)和 CAS Server(登录处理服务器)。在现实生活中还是挺多使用到 CAS 的,例如淘宝和天猫网站便是,未登录情况下,无论在淘宝上还是在天猫上点击登录都会跳去相同的登录页面,而这个登录页面其实就是 CAS Client。当登录成功后,会发现无论是在天猫还是淘宝都会是登录状态。

在了解 CAS 服务前,需提前了解以下几个知识点。

  • TCT(Ticket Granting ticket 缩写)

    可比作是一个 Session,CAS Server 会根据用户名和密码生成一个 TGT。

  • TGC(Ticket-granting cookie 缩写)

    其实就是一个 Cookie,用于存放用户身份信息。

  • ST(Service ticket 缩写)

    可简单比作是一个 SessionId,一次性票据,用于验证用的,只能用一次,可以想象成服务端发送给客户端的一张门票。

接下来,我们就来瞅瞅,CAS 服务的流程是如何的。

  1. 用户第一次访问a.test.com网站时,点击登录会重定向到 CAS Server,发现请求中没有 Cookie,那么便会返回状态 302,重定向到 CAS Client 登录页面。

    http://cas.com/login?previosurl=http://a.test.com
  2. 在 CAS Client 中输入用户名和密码并且点击提交后,CAS Server便会生成一个 TGT,再用 TGT 生成一个 ST,然后便会在返回头中 Set-Cookie ,在客户端 Cookie 中存储 ST。

  3. 存储成功后,便重新回到上面的 previosurl 中,并带上一个 ticket 参数(用于后端验证用的)。

    http://a.test.com?ticket=ST-123456-asdasd4fas23qf8

    ticket 后面的值就是 ST。

  4. 回到a.test.com网站后,将参数 ticket 传递给服务端,服务端会将 ticket 拿到 CAS Server 进行验证,验证通过后便会返回登录成功。

  5. 用户第一次访问c.haha.com后,点击登录同样会重定向到 CAS Server 中,这时候由于上一次已经在 CAS Client 客户端的 Cookie 中存储了 ST,所以 CAS Server 会发现请求头部已经带上了上一次登录的 ST,这时候便会直接同定向回c.haha.com中,并且带上参数ticket

    http://c.haha.com?ticket=ST-123456-asdasd4fas23qf8
  6. 回到c.haha.com后,同样会拿 ticket 传递到服务端,服务端拿到 ticket 后便会再次拿到 CAS Server 中进行验证,验证通过便会成功登录。至此,一个单点登录功能便实现。

可以看到的是,CAS 服务原理就是利用中间客户端 CAS Client 复用 Cookie 值实现单点登录功能

回溯算法

回溯算法,也叫试探法,通过遍历每一种可能都走一遍,一旦遇到不符合结果的,就会回溯到上一层,再找另一子节点,以此类推,直到找到符合结果为止。

在生活里面,最常见莫过于迷宫找出路了,在一个分叉路口,当一条路发现不通后,就要回到分叉路口,接着找下一条路,以此类推。

可以看到,回溯算法可以说是穷举法,即把每一种可能都得查询一次,时间复杂度贼高。因此回溯算法常常需要结合剪枝来进行优化。

所谓剪枝,就是在遍历过程中将那些不符合结果的排除掉,具体看下面栗子就会很容易理解。

回溯算法代码段

从代码层面来看,回溯算法基本都是一个套路,先来看下:

const result = [];
const backTrack = (index, targetArr, tempArr) => {
  if (结束条件) {
    result.push([...tempArr]);
    return;
  }
  for (let i = 0; i < targetArr.length; i++) {
    // 剪枝行为
    // 选择当前元素
    tempArr.push(targetArr[i]);
    backTrack(i, targetArr, tempArr);
    // 撤销当前元素
    tempArr.pop();
  }
}

回溯算法的题目大体上可以分为三类:子集组合全排列。只要解决这三类问题,其实回溯算法就基本不是问题了。

子集 I

给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集。

说明:结果里面不能包含重复的子集。

例如:nums = [1, 2, 3],输出为:[1], [1, 2], [1, 3], [2], [2, 3], [3], [1, 2, 3], []

现在我们就来套用上面代码段。

const getChildSet = nums => {
  if (!nums || !Array.isArray(nums)) return null;
  const result = [];
  const backTrack = (index, targetArr, tempArr) => {
    result.push([...tempArr]);
    for (let i = 0; i < targetArr.length; i++) {
      tempArr.push(targetArr[i]);
      backTrack(i + 1, targetArr, tempArr);
      tempArr.pop();
    }
  }
  backTrack(0, nums, []);
  return result;
}

套用起来确实简单,但是当你去执行上面代码时,会发现栈溢出。

好明显,我们还没有做剪枝,接下来我们就来做剪枝操作。

const getChildSet = nums => {
  if (!nums || !Array.isArray(nums)) return null;
  const result = [];
  const backTrack = (index, targetArr, tempArr) => {
    result.push([...tempArr]);
    for (let i = index; i < targetArr.length; i++) {
      tempArr.push(targetArr[i]);
      backTrack(i + 1, targetArr, tempArr);
      tempArr.pop();
    }
  }
  backTrack(0, nums, []);
  return result;
}

只需要将 i 从 index 开始遍历即可,而前面遍历过的元素可以直接跳过,因为没必要再遍历。

子集 II

给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集。

说明:结果不能包含重复的子集。

例如:nums = [1, 2, 2],结果为:[1], [1, 2], [1, 2, 2], [2], [2, 2], []

乍一看,跟上面意思是一样,不同点就在于对于 nums 中重复元素就不要再计算了,不然得到的结果是一样的。

const getChildSet = nums => {
  if (!nums || !Array.isArray(nums)) return null;
  nums.sort((a, b) => a - b);
  const result = [];
  const backTrack = (index, targetArr, tempArr) => {
    result.push([...tempArr]);
    for (let i = index; i < targetArr.length; i++) {
      if (i > index && targetArr[i] === targetArr[i - 1]) continue;
      tempArr.push(targetArr[i]);
      backTrack(i + 1, targetArr, tempArr);
      tempArr.pop();
    }
  }
  backTrack(0, nums, []);
  return result;
}

可以看到,比子集 I 多了两个,一个是先对数组进行排序,另一个则是判断元素是否已经遍历过,直接跳过。换句话说,排序就是为了让判断前后元素是否同一个,从而避免重复遍历。

而这也是剪枝,因此剪枝要根据实际情况进行剪

组合总和 I

给定一个无重复元素的数组 candidates 和一个目标数 target,找出 candidates 中所有可以使数字和为 target 的组合。

candidates 中的数字可以无限制重复被选取。

说明:

  • 所有数字(包括 target)都是正整数。
  • 结果不能包含重复的组合。

例如:candidates = [2, 3, 6, 7], target = 7, 输出为:[7], [2, 2, 3]

如果顺利理解上面子集问题后,那么组合问题就会变得很简单了,我接着套用代码段。

const getComposeSum = (candidates, target) => {
  if (!candidates || !Array.isArray(candidates)) return null;
  const result = [];
  const backTrack = (index, targetArr, tempArr, target) => {
    if (target === 0) {
      result.push([...tempArr]);
      return;
    }
		if (target < 0) return;
    for (let i = index; i < targetArr.length; i++) {
      tempArr.push(targetArr[i]);
      target -= targetArr[i];
      backTrack(i, targetArr, tempArr, target);
      target += targetArr[i];
      tempArr.pop();
    }
  }
  backTrack(0, candidates, [], target);
  return result;
}

组合总和 II

给定一个数组 candidates 和一个目标数 target,找出 candidates 中所有可以使数字和为 target 的组合。

candidates 中每个数字再每个组合中只能使用一次。

说明:

  • 所有数字(包括 target)都是正整数。
  • 结果不能包含重复的组合。

例如:candidates = [10, 1, 2, 7, 6, 1, 5], target = 8,输出为:[1, 7], [1, 2, 5], [2, 6], [1, 1, 6]

明显比组合总和 II 中多了 candidates 是允许出现重复元素的,因此我们要剔除那些重复元素的遍历,避免重复遍历,跟子集 II 类似。

const getComposeSum = (candidates, target) => {
  if (!candidates || !Array.isArray(candidates)) return null;
  candidates.sort((a, b) => a - b);
  const result = [];
  const backTrack = (index, targetArr, tempArr, target) => {
    if (target === 0) {
      result.push([...tempArr]);
      return;
    }
    if (target < 0) return;
    for (let i = index; i < targetArr.length; i++) {
      if (i > index && targetArr[i] === targetArr[i - 1]) continue;
      tempArr.push(targetArr[i]);
      target -= targetArr[i];
      backTrack(i + 1, targetArr, tempArr, target);
      target += targetArr[i];
      tempArr.pop();
    }
  }
  backTrack(0, candidates, [], target);
  return result;
}

全排列 I

给定一个没有重复数字的序列 nums,返回其所有可能的全排列。

例如:nums = [1, 2, 3],输出为:[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]

全排列的问题与前面的子集和组合是一个道理,将每一种情况都列举出来。

const getRange = nums => {
  if (!nums || !Array.isArray(nums)) return null;
  const result = [];
  const backTrack = (index, targetArr, tempArr) => {
    if (tempArr.length === targetArr.length) {
     	result.push([...tempArr]);
      return;
    }
    for (let i = 0; i < targetArr.length; i++) {
      if (tempArr.indexOf(targetArr[i]) > -1) continue;
      tempArr.push(targetArr[i]);
      backTrack(i + 1, targetArr, tempArr);
      tempArr.pop();
    }
  }
  backTrack(0, nums, []);
  return result;
}

全排列 II

给定一个可包含重复数字的序列 nums,返回所有不可重复的全排列。

例如:nums = [1, 1, 2], 输出为:[1, 1, 2], [1, 2, 1], [2, 1, 1]。

由于 nums 中存在重复元素,因此使用全排列 I 中的方式是走不通的。

转个角度,我们可以使用另一个数组专门存储已经遍历过的元素,一旦该元素已经遍历过,直接跳过即可。

另外还需要考虑一个问题,对于重复元素,如果前面那个已经有结果了,那么需要去重处理,进而避免结果中出现重复集合。

const getRange = nums => {
  if (!nums || !Array.isArray(nums)) return null;
  nums.sort((a, b) => a - b);
  const result = [];
  const backTrack = (index, targetArr, tempArr, storage) => {
  	if (tempArr.length === targetArr.length) {
    	result.push([...tempArr]);
      return;
    }
    for (let i = 0; i < targetArr.length; i++) {
      if (storage[i]) continue;
      if (i > 0 && targetArr[i] === targetArr[i - 1] && storage[i - 1]) break;
      tempArr.push(targetArr[i]);
      storage[i] = targetArr[i];
      backTrack(i + 1, targetArr, tempArr, storage);
      storage[i] = undefined;
      tempArr.pop();
    }
  }
  backTrack(0, nums, [], []);
  return result;
}

回溯算法相比前面的动态规划和贪心算法都好理解,基本都是在三大类型题目上---子集、组合、排列。

直接套用代码段,但是需要考虑好剪枝问题,根据实际情况进行剪枝。

Review-Question-CSS

  1. 说说 CSS 不同选择器间权重
  2. link 与 @import 区别
  3. px、em、rem 三种单位区别?
  4. 讲讲 BFC 及其应用
  5. inline-block 如何处理上下左右间隙?
  6. 说说盒子模型?
  7. 讲讲浮动,以及如何清除浮动?
  8. 讲讲常见的布局有哪些?
  9. 实现左边自适应,右边宽度固定布局?
  10. 说说水平居中?垂直居中?绝对居中?等高栏布局实现?
  11. 三栏式布局实现?
  12. 向文档中插入1000个div最好的方案是什么?
  13. 讲讲css sprite
  14. css hack原理及其常用场景?
  15. 什么情况会出现外边距重叠情况?
  16. css 如何提升性能?
  17. png、gif、jpg区别以及使用场景
  18. stylus、sass、less异同
  19. css实现三角形的原理是什么?
  20. 移动端布局方案有哪些?
  21. 浏览器是如何解析css选择器的?为什么要这样解析?
  22. retina 屏幕?如何兼容图形在retina屏幕上的展示
  23. 如何画一条0.5px的线?
  24. css 是如何适配浏览器大小的?
  25. 已知父级盒子宽高,子级img宽高未知,想让img铺满父级盒子且图片不能变形,如何操作?

说说 CSS 不同选择器间权重

!important规则权重大于任何选择器以及行内样式规则。

行内样式,权重加1000。

ID 选择器,权重加100。

class 选择器、属性选择器、伪类选择器,权重加10。

元素标签选择器等其他选择器,权重加1。

权重相同时,按先后顺序应用,后面的属性会覆盖前面的属性。


link 与 @import 区别

  1. link 是 HTML 引入样式的方式,而 @import 则是 CSS 引入样式的方式。
  2. link 支持并行下载,@import 只能串行下载。
  3. 浏览器解析到 link 时,页面会异步加载所引的样式,而 @import 则会等待页面加载完才会被加载。
  4. link 支持动态创建,@import 不支持。

px、em、rem 三种单位区别?

px

含义:在缩放页面时无法自动调整使用该单位的字体、按钮等大小,是相对于屏幕分辨率的。

特点:缩放页面时无法自动调整缩放。

em

含义:值不固定,继承父级元素的字体大小,代表的是倍数。

特点:值不固定,继承父级元素字体大小。

rem

含义:值不固定,继承根元素字体大小,代表的是倍数。为兼容不支持 rem 的浏览器,需加上 px 来优雅降级。

div {
  width: 100px;
  width: 1rem;
}

特点:只需更改根元素的字体大小,即可成比例地调整所有字体大小。


讲讲 BFC 及其应用

BFC 就是块级上下文,盒模型中的一种 css 渲染模式,相当于一个独立的容器,里面的元素与外部互不影响。

创建 BFC 的方式有:

  • html 根元素。
  • float 浮动元素。
  • 绝对定位元素。
  • overflow 为hidden。
  • display 为 table 或 flex。

用处有清除浮动、防止与外部容器的间距问题。


inline-block 如何处理上下左右间隙?

解决左右间隙方式有:

  • 移除空格。
  • 设置 margin 为负值。
  • 父级元素设置 font-size 为 0(最推荐)。
  • 设置 letter-spacing 或 word-spacing。

解决上下间隙方式有:设置 vertical-align 为 middle 即可。(这是因为 inline-block 元素对齐方式是根据基线对齐的


说说盒子模型?

盒子模型分为两种:W3C 标准盒模型和IE盒模型。

W3C 标准盒模型:通过 box-sizing:content-box 设置,width = content。

IE 盒模型:通过 box-sizing:border-box 设置,width = border * 2 + padding * 2 + content。


讲讲浮动,以及如何清除浮动?

浮动元素会脱离文档中普通流,并处于普通流的上方,浮动的元素会默认变成 block 状态。

清除浮动的方式有

  • 父级元素直接设置高度。
  • 父级元素设置伪元素 :after 的属性 clear 为 both。
  • 结尾处加一个空 div 标签或 br 标签,并且设置属性 clear 为 both。
  • 父级设置 overflow 为 hidden。

讲讲常见的布局有哪些?

  1. 固定布局。采用固定值定义宽度,一般使用的单位是 px。

  2. 流体布局。类似于固定布局,但是却使用百分比作为单位,能够适应屏幕的变化。

    .left {
      float: left;
      width: 100px;
      height: 200px;
      background: red;
    }
    .right {
      float: right;
      width: 200px;
      height: 200px;
      background: blue;
    }
    .main {
      margin-left: 120px;
      margin-right: 220px;
      height: 200px;
      background: green;
    }
    <div class="container">
        <div class="left"></div>
        <div class="right"></div>
        <div class="main"></div>
    </div>
  3. 响应式布局。结合媒体查询 @media 来根据屏幕分辨率设置样式。

  4. 圣杯布局

    .container {
      margin-left: 120px;
      margin-right: 220px;
    }
    .main {
      float: left;
      width: 100%;
      height:300px;
      background: green;
    }
    .left {
      position: relative;
      left: -120px;
      float: left;
      height: 300px;
      width: 100px;
      margin-left: -100%;
      background: red;
    }
    .right {
      position: relative;
      right: -220px;
      float: right;
      height: 300px;
      width: 200px;
      margin-left: -200px;
      background: blue;
    }
    <div class="container">
        <div class="main"></div>
        <div class="left"></div>
        <div class="right"></div>
    </div>
  5. 双飞翼布局

    .content {
      float: left;
      width: 100%;
    }
    .main {
      height: 200px;
      margin-left: 110px;
      margin-right: 220px;
      background: green;
    }
    .main::after {
      content: '';
      display: block;
      font-size:0;
      height: 0;
      zoom: 1;
      clear: both;
    }
    .left {
      float:left;
      height: 200px;
      width: 100px;
      margin-left: -100%;
      background: red;
    }
    .right {
      float: right;
      height: 200px;
      width: 200px;
      margin-left: -200px;
      background: blue;
    }
    <div class="content">
        <div class="main"></div>
    </div>
    <div class="left"></div>
    <div class="right"></div>
  6. Flex 布局。

  7. 浮动布局。

其实流体布局、圣杯布局、双飞翼布局实现的都是中间自适应,左右固定的布局


实现左边自适应,右边宽度固定布局?

  1. Flex 布局。

    .container {
      display: flex;
    }
    .left{
      background-color: rebeccapurple;
      height: 200px;
      flex: 1;
    }
    .right{
      background-color: red;
      height: 200px;
      width: 100px;
    }
  2. 浮动布局。

    .container {
      height: 200px;
    }
    .left {
      float: right;
      width: 100px;
      background-color: rebeccapurple;
    }
    .right {
      float: right;
      margin-right: 100px;
      background-color: red;
    }

说说水平居中?垂直居中?绝对居中?等高栏布局实现?

水平居中

  • margin: 0 auto

  • 绝对定位。

    // 负边距
    .child {
      width: 100px;
      position: absolute;
      left: 50%;
      margin-left: -50px;
    }
    
    // transform变形
    .child {
      width: 100px;
      position: absolute;
      left: 50%;
      transform: translateX(-50%);
    }
  • flex 布局。

    .parent {
      display: flex;
      justify-content: center;
    }

垂直居中

  • 表格布局。

    <div id="wrapper">
       <div id="cell">
           <div>
               <div id="content"></div>
           </div>
       </div>
    </div>
    #wrapper {
       display: table;            //最外层必须要设置成table布局
       height: 600px;
       width: 300px;
       background: black;
    }
    #cell {
       display: table-cell;       //第二层要设置成table的单元格
       vertical-align: middle;    //进行居中
    }
    #content{
       width: 100px;
       height: 100px;
       background: orange;
    }
  • 绝对定位。

    // 负边距
    .child {
      height: 100px;
      position: absolute;
      top: 50%;
      margin-top: -50px;
    }
    
    // transform变形
    .child {
      height: 100px;
      position: absolute;
      top: 50%;
      transform: translateY(-50%);
    }
  • Flex 布局。

    .parent {
      display: flex;
      align-items: center;
    }

绝对居中

  • 绝对定位。

    // 常用方式
    .child {
      left: 0;
      right: 0;
      top: 0;
      bottom: 0;
      margin: auto;
    }
    
    // 还可以使用margin负边距、transform变形
  • flex 布局。

    // 常用方式
    .parent {
      display: flex;
      justify-content: center;
      align-items: center;
    }
    // 子项margin自适应
    .parent {
      display: flex;
    }
    .child {
      margin: auto;
    }
  • table 布局。

    .parent {
      display: table;
      width: 200px;
      height: 200px;
    }
    .child {
      display: table-cell;
      text-align: center;
      vertical-align: middle;
    }
    .content {
      display: inline-block;
    }
  • Grid 布局。

    // 常用方式
    .parent {
      display: grid;
    }
    .child {
      justify-self: center;
      align-self: center;
    }
    // 子项margin自适应
    .parent {
      display: grid;
    }
    .child {
      margin: auto;
    }

等高栏、等高布局实现

  • 绝对定位。

    .parent {
      position: relative;
    }
    .child {
      position: absolute;
      top: 0;
      bottom: 0;
      overflow: hidden;
    }
  • Flex 布局。

    .parent {
      display: flex;
    }
    .child {
      flex: 1;
    }

line-height 是不是等于高度设置的值?答案不是的,每个行内元素都拥有行内框,line-height 设置的就是行内框的高度,而 height 才是设置块高度。


三栏式布局实现?

  1. 圣杯布局。
  2. 流体布局,即使用浮动布局实现。
  3. 绝对定位。

向文档中插入1000个div最好的方案是什么?

通过 box-sizing:content-box 设置,width = content。

使用DocumentFragment机制。它会提供一个缓冲机制,将新建的 DOM 节点放到内存中,等节点都构造好后,再将DocumentFragment对象添加到页面中,便能做到一次渲染。


讲讲css sprite

含义:将多个小图片合并成一个图片,通过使用 background-position 和元素尺寸调节需要显示的背景图案。

优点:有效地减少 HTTP 请求。缺点:维护麻烦。


css hack原理及其常用场景?

原理:由于不同的浏览器对于css解释不一致,会导致页面效果不一样。针对不同浏览器解释的差异进行编写css。

常用场景

  • 浏览器默认的margin和padding不同。解决方案:加一个全局的* {margin: 0; padding:0;}
  • IE支持使用条件注释来按需引入脚本。
  • Chrome下默认字体大小最小只能到12px显示,若想更小,可使用-webkit-text-size-adjust: none;

什么情况会出现外边距重叠情况?

出现外边距重叠情况

  • 两个或多个普通流块级元素,垂直方向上的margin会重叠。
  • 元素自身的margin-bottom和margin-top相邻时也会折叠。

解决方案

  • 使用BFC避免;
  • 使用inline-block属性

css 如何提升性能?

  1. css 压缩以及合并。
  2. 使用 css sprite 合并减少http请求。
  3. 合理使用选择器。
  4. 提取共用样式。

png、gif、jpg区别以及使用场景

png

  • 无损压缩。
  • 文件小。
  • 适合做图标、背景、按钮。

gif

  • 无损压缩。
  • 支持简单动画。
  • 适合做简单动画。

jpg

  • 有损压缩。
  • 可控制压缩质量。
  • 不支持透明。
  • 适合做图片。

stylus、sass、less异同

  1. 都有变量、混合、嵌套、继承、颜色混合五大特性。
  2. sass无全局变量,less和stylus有类似其他语言的作用域概念。
  3. sass基于Ruby编写的,less和stylus则是基于NPM相应库进行编译的。

postcss是一个平台,提供了解析器,能够将css解析成抽象语法树。因此可以在其基础上编写一些插件,如autoprefixer等。


css实现三角形的原理是什么?

将上、左、右三条边大小并设置颜色为 transparent 透明,将元素本身的宽高都设置为0。

#demo {
  width: 0;
  height: 0;
  border-width: 20px;
  border-style: solid;
  border-color: transparent transparent orange transparent;
}

移动端布局方案有哪些?

  1. 百分比 + 媒体查询

    比较费时的一种布局方案,某些场景需要结合媒体查询来做兼容,维护麻烦,一般不建议。

  2. 固定的设备宽度

    在做移动开发的时候很多人都会加上viewport的配置,那么固定设备宽度的布局就是根据这个来设置的,将viewport里面的宽度width设置成设计稿的宽度,也就是说原本是width=device-width,即宽度为设备的宽度,假如在iphone6上显示的时候,那么页面的宽度就是375px; 当我们将width设置成设计稿的宽度的,假如设计稿是750px,而我们的css也按设计的尺寸来做,例如一个图片是200px*200px,那么在css上也是宽高都是写200px,也就是1:1的比例。那么在375px的手机上显示的时候,就会缩小2倍显示,以此类推,在320px的宽度的时候,就会缩小2.3倍显示,在414px的宽度的时候就会缩小1.8倍。

    缺陷就是缩小时设备会显得模糊,因为固定的原因,会将设备放大了。

  3. flexible.js

    手机淘宝一个方案,需引入flexible.js脚本,在页面中布局使用的是rem作为单位,而非px。例如,在320px屏幕里,1rem=32px。

  4. vw + rem

    vw会自动计算,例如在iphone6上设置html的font-size为100px,那么用vw替代就是,font-size=100/375=0.266666666vw,接下来遇到iphone5,会自动计算为font-size为85.33333。

    好处就是不需要引入js,直接自动计算并在后续布局使用rem即可,1rem = 10vw,1vw = 视口宽度的1%。


浏览器是如何解析css选择器的?为什么要这样解析?

浏览器解析css选择器的规则是从右向左进行解析的,这样会提高查找选择器所对应元素的效率。

为什么?

首先,DOM 解析完后会构建 DOM tree,然后等到 CSS 解析后,就会得到 CSS style rules。而 DOM tree 和 style rules 结合就会得到 render tree 即渲染树。

若从根元素开始往下找,即从左往右,首先要明白的是,一个父节点是有两个字节点,若一个字节点不符合就需要回溯,再去找其他字节点,这无异就导致了性能损耗。

若从目标元素开始找,由于一个字节点只能有一个父节点,因此父节点不符合时就可以直接排除了无需再进行其他检查,提升效率。


retina 屏幕?如何兼容图形在retina屏幕上的展示

retina 是苹果推出的新型屏幕,由四个原像素去描述一个新像素点(即压缩了2倍),且存在0.5个像素点这样的描述,需要兼容。

兼容图形在retina屏幕正常显示的方式

  • 动态更改为2倍大小的图片。

    if (window.devicePixelRatio > 1) {
      let images = $('img')
      images.each(function(i) {
        let x1 = $(this).attr('src')
        let x2 = x1.replace(/(.*)(.w+)/, "$1@2x$2")
        $(this).attr('src', x2)
      })
    } 
  • Image-set 控制

    #logo {
      background: url(pic.png) 0 0 no-repeat;
      background-image: -webkit-image-set(url(pic.png) 1x, url([email protected]) 2x);
      background-image: -moz-image-set(url(pic.png) 1x,url(images/[email protected]) 2x);
      background-image: -ms-image-set(url(pic.png) 1x,url(images/[email protected]) 2x);
      background-image: -o-image-set(url(url(pic.png) 1x,url(images/[email protected]) 2x);
    }
  • 媒体查询


如何画一条0.5px的线?

  1. 直接设置高度为0.5px。

  2. 使用scale进行缩放。

    .hr {
      height: 1px;
      transform: scaleY(0.5);
    }
  3. 使用颜色渐变linear-gradient。

    .hr {
      height: 1px;
      background: linear-gradient(0deg, #fff, #000);
    }
  4. 使用阴影box-shadow。

    .hr {
      height: 1px;
      background: none;
      box-shadow: 0 0.5px 0 #000;
    }
  5. 设置viewport。

    <meta name="viewport" content="width=device-width,initial-sacle=0.5">

css 是如何适配浏览器大小的?

  1. 使用viewport。
  2. 使用百分比布局结合媒体查询。

已知父级盒子宽高,子级img宽高未知,想让img铺满父级盒子且图片不能变形,如何操作?

使用新属性object-fit,指定可替换元素的内容应该如何适应到其使用的高度和宽度确定框。

.parent {
  width: 200px;
  height: 200px;
}
img {
  object-fit: cover;
  width: 100%;
  height: 100%;
}

ES Summary(每个版本)

本文针对Javascript中每个版本的一些知识总结,主要为了方便知识梳理,避免遇到盲点。🤔

(每一年官方出的新版本ES,都会同时进行更新的哈哈)

目录

  1. ES6
  2. ES7
  3. ES8
  4. ES9
  5. ES10

ES6

ES6中,可以说是整个Javascript改版中最大的一版,下面就来看看主要包含哪些内容。

块级作用域绑定(var、let、const)

  1. 在 Javascript 中不存在块级作用域,只存在全局作用域函数作用域

  2. 使用 var 声明的变量不管在哪声明,都会变量提升到当前作用域的最顶部。看个🌰:

    function test() {
      console.log(a) // 不会报错,直接输出undefined
      if(false) {
        var a = 1
      }
    }
    test()
    
    // 相当于
    function test() {
      var a
      console.log(a)
      if(false) {
        a = 1
      }
    }
    test()

    另外,定义和声明是两码事,如果变量都还没定义就使用时,就会报错(xxx is not defined)。

  3. let 和 const 都能够声明块级作用域,用法和 var 是类似的,let 和 const 都是不会变量提升。看个🌰:

    function test() {
      if(false) {
        console.log(a) // 报错,a is not defined(也是传说中的临时性死区)
        let a = 1
      }
    }
    test()

    let 和 const 必须先声明再访问

    所谓临时性死区,就是在当前作用域的块内,在声明变量前的区域(临时性死区只有 let 和 const 才有)。看个🌰:

    if(false) {
      // 该区域便是临时性死区
      let a = 1
    }
  4. const 声明变量时必须同时赋值,并且不可更改。

  5. 在全局作用域中使用 var 声明的变量会存储在 window 对象中。而使用 let 和 const 声明的变量则只会覆盖 window 对象中同名的属性,而不会替换。看个🌰:

    window.a = 1
    let a = 2
    
    console.log(a) // 2
    console.log(window.a) // 1

    let 和 const 声明的变量会存在一个单独的Script块作用域中(即[[Scopes]]作用域中能找到)。

    // 接着上面列举的栗子
    function aa() {}
    console.dir(aa)
    // 你会发现在输出内容中[[Scopes]],会存在两个作用域,一个是Script,一个是Global
    [[Scopes]]: Scopes[2]
    0: Script {a: 2}
    1: Global {parent: Window, opener: null, top: Window, length: 1, frames: Window, }

字符串和正则表达式

  1. 支持 UTF-16(含义是任何一个字符都适用两个字节表示),其中方法如下:

    • codePointAt:返回参数字符串中给定位置对应的码位,返回值为整数。
    • fromCodePoint:根据指定的码位生成一个字符。
    • normalize:提供Unicode的标准形式,传入一个可选字符串参数,指明应用某种Unicode标准形式。
  2. 字符串中新增的方法有:

    • includes(str, index):检测字符串指定的可选索引中是否存在参数文本。

    • startsWith(str, index):检测字符串头部是否有指定的文本。

    • endsWith(str, index):检测字符串尾部是否有指定的文本。

    • repeat(number):接受一个整数,重复对应字符串整数次。

      console.log('aa'.repeat(2)) // aaaa
  3. 当给正则表达式添加 u 字符时,表示从编码单元操作模式切换为字符模式。

  4. 模板字符串支持多行文本、模板中动态插入变量、模板子面量方法使用。

    • 多行文本。

      `1
      
      2`
    • 模板中动态插入变量。

      let a = 1
      console.log(`${a} haha`) // 1 haha
    • 模板子面量方法。

      function aa(a, ...b) {
        console.log(a, b)
      }
      let a = 1
      aa`hehe ${a} haha`
      // 输出为:
      // ["hehe ", " haha", raw: ["hehe ", " haha"]] [1]

      其中参数 a 表示模板字符串中静态字符。参数 b 表示模板动态变量。

函数

  1. 支持默认参数,默认参数不仅可以为字符串、数字、数组或对象,还可以是一个函数。看个🌰:

    function sum(a = 1, b = 2) {
      return a + b
    }
    sum()

    参数默认值不能被 arguments 识别,看个🌰:

    function sum(a = 1, b = 2) {
      console.log(arguments)
    }
    sum() // {}
    sum(3, 6) // {"0": 3, "1": 6}

    默认参数同样存在临时性死区,看个🌰:

    // 在初始化a时,由于b还没被声明,因此无法直接将b赋值给a
    function aa(a = b, b) {
      return a + b
    }
  2. 支持展开运算符(...),其作用是解构数组和对象。看个🌰:

    // 展开数组或对象
    var a1 = [1, 2]
    var a2 = {a: 1}
    console.log(...a1) // 1 2
    console.log({...a2}) // {a: 1}
    
    // rest参数
    function aa(...obj) {
      console.log(obj)
    }
    aa(a1) // 1 2
    aa(a1, a2) // [[1, 2], {a: 1}]
  3. 支持箭头函数。箭头函数和普通函数的区别有:

    • 箭头函数木有 this,this 指向的是定义该箭头函数所在的对象。
    • 箭头函数没有 super
    • 箭头函数没有 arguments
    • 箭头函数内部不存在 new.target 绑定(构造函数存在)。并且箭头函数中不能使用 new 关键字。
    • 箭头函数不存在原型
  4. 支持尾调用优化。(调用一个函数时都会生成一个函数调用栈,尾调用就可以很好滴避免生成不必要的尾调用阮一峰的尾调用优化讲解

    只有满足以下三个条件时,引擎才会帮我们做好尾调用优化。

    • 函数不是闭包。
    • 尾调用是函数最后一条语句。
    • 尾调用结果作为函数返回。

    看个🌰:

    // 符合尾调用优化情况
    function aa() {
      return bb()
    }
    
    // 无return不优化
    function aa() {
      bb()
    }
    // 不是直接返回函数不优化
    function aa() {
      return 1 + bb()
    }
    // 最后一条语句不是函数不优化
    function aa() {
      const cc = bb()
      return cc
    }
    // 闭包不优化
    function aa() {
      function bb() {
        return 1
      }
      return bb()
    }

    需要知道的是,递归都是很影响性能的,但是有了尾调用后,递归函数的性能将得到有效的提升

    // 斐波那契数列
    // 常做方案
    function fibonacci(n) {
      if(n === 0 || n === 1) return 1;
      return fibonacci(n-1) + fibonacci(n-2);
    }
    
    // 尾递归优化,其中pre是第一项的值,next作为第二项的值
    function fibonacci(n, pre, next) {
      if (n <= 1) return next
      return fibonacci(n - 1, next, pre + next)
    }

对象的扩展

  1. 对象方法和属性支持简写,以及对象属性支持可计算。看个🌰:

    const id = 1
    const obj = {
      id,
      [`test${id}`]: 1,
      printId() {
        return this[`test${id}`]
      }
    }
  2. Object 新增了方法如下:

    • Object.is。判断两个值是否相等。

      console.log(Object.is(NaN, NaN)) // true
      console.log(Object.is(+0, -0)) // false
      console.log(Object.is(5, "5")) //false
    • Object.assign。浅拷贝一个对象,相当于一个 Mixin 功能。

      const obj = {a: 1, b: 2}
      const newObj = Object.assign({...obj, c: 3})
      console.log(newObj) // {a: 1, b: 2, c: 3}
  3. 对象支持同名属性,不过后面的属性会覆盖前面同名的属性。看个🌰:

    const obj = {
      a: 1,
      a: 2
    }
    console.log(obj) // {a: 2}
  4. 遍历对象时,默认都是数字属性按顺序提前,接着就是首字母排序。看个🌰:

    const obj = {
      name: 'haha',
      1: 2,
      a: 1,
      0: 'hehe'
    }
    for(var key in obj) {
      console.log(key)
    }
    // 输出顺序为
    // 0
    // 1
    // name
    // a
  5. 支持实例化后的对象改变原型对象,使用方法Object.setPrototypeOf()

    var parent1 = {a: 1}
    var child = Object.create(parent1)
    console.log(child.a) // 1
    var parent2 = {b: 2}
    child = Object.setPrototypeOf(child, parent2)
    console.log(child.a, child.b) // undefined, 2

解构

  1. 解构的定义是,从对象中提取出更小元素的过程,即对解构出来的元素进行重新赋值

  2. 对象解构必须是同名的属性。看个🌰:

    var obj = {
      a: 1,
      b: 2
    }
    var a = 3, b = 3;
    ({a, b} = obj)
    console.log(a, b) // 1, 2
  3. 数组解构可以有效地处理交换两个变量的值。

    var a = 1, b = 2;
    [a, b] = [b, a]
    console.log(a, b) // 2, 1

    数组解构还可以按需取值,看个🌰:

    var arr = [[1, 2], 3]
    var [[,b]] = arr
    console.log(b) // 2
  4. 混合解构就是对象和数组解构结合,看个🌰:

    var obj = {
      a: 1,
      b: [1, 2, 3]
    };
    ({a, b: [...arr]} = obj)
    console.log(a, arr) // 1, [1, 2, 3]
  5. 解构参数就是直接从参数中获取相应的参数值。看个🌰:

    function aa({a = 1, b = 2} = obj) {
      console.log(a, b)
    }
    aa({a: 3, b: 6}) // 3, 6

Symbol

  1. Symbol 是一种特殊的、不可变的数据类型,可作为对象属性的标识符使用,也是一种原始数据类型。

  2. Symbol 的语法格式为:

    Symbol([desc]) // desc是一个可选参数,用于描述Symbol所用

    创建一个 Symbol,如下:

    const a = Symbol()
    const b = Symbol('1')
    console.log(a, b) // Symbol(), Symbol('1')

    创建 Symbol 不能使用 new

  3. Symbol 最大的用处在于创建对象一个唯一可计算的属性名。看个🌰:

    const obj = {
      [Symbol('name')]: 12,
      [Symbol('name')]: 13
    }
    console.log(obj) // {Symbol(name): 12, Symbol(name): 13}

    有效地避免命名冲突问题。

  4. Symbol 不支持强制转换为其他类型。

  5. 在 ES6 中提出一个 @@iterator方法,所有支持迭代的对象(如数组、Set、Map)都要实现。其中**@@iterator方法的属性键为Symbol.iterator而非字符串**。只要对象定义有Symbol.iterator属性就可以用for...of进行迭代

    // 判断对象是否实现Symbol.iterator属性,就可以判断是否可以使用for...of进行迭代
    if(Symbol.iterator in obj) {
      for(var n of obj) console.log(n)
    }
  6. Symbol 支持全局共享机制,使用 Symbol.for 进行注册,使用 Symbol.keyFor 进行获取。看个🌰:

    // Symbol.for中参数可为任意类型
    let a = Symbol.for(12)
    console.log(Symbol.keyFor(a), typeof Symbol.keyFor(a)) // '12', string

    Symbol.keyFor 最终获取到的是一个字符串值。

  7. Symbol 可作为类的私有属性,使用 Object.keys 或 Object.getOwnPropertyNames 方法都无法获取 Symbol 的属性名只能使用 for...of 或 Object.getOwnPropertySymbols 方法获取。看个🌰:

    var obj = {
      [Symbol('name')]: 'Andraw-lin',
      a: 1
    }
    console.log(Object.keys(obj)) // ["a"]
    console.log(Object.getOwnPropertySymbols(obj)) // [Symbol(name)]

Set和Map

  1. Set 常用于检查对象中是否存在某个键值,Map 则常用于获取已存的信息。

  2. Set 是有序列表,含有相互独立的非重复值。支持的属性和方法如下:

    • size。返回 Set 对象中元素个数。

    • add(value)。在 Set 对象尾部添加一个元素。

    • entries()。返回 Set 对象中[值,值]形式,看个🌰:

      var a = new Set([1, 2, 3])
      for(var [b, c] of a.entries()) {
        console.log(b, c)
      }
      // 输出结果为:
      // 1 1
      // 2 2
      // 3 3
    • forEach(callback)。用于遍历 Set 集合。

    • has(value)。判断 Set 集合中是否存在有指定的值。

  3. Set 集合的特点是没有下标,没有 Key。Set 和 Array可以相互转换,如下:

    // 数组转成Set
    const arr = [1, 2, 3]
    console.log(new Set(arr))
    
    // Set转成数组
    const se = new Set([1, 2, 3])
    console.log([...se])
  4. Set 集合是一个强引用,只要 new Set 实例化的引用存在,就不会释放内存。若定义一个 DOM 元素的 Set 集合,然后在某个 js 中引用了该实例,当页面跳转时,并不会立马释放内存,因为引用还在。WeakSet 就是专门用于释放强引用的

    WeakSet 和 Set 区别:

    • WeakSet 对象中只能存放对象类型,不能存放原始数据类型。而 Set 对象则可以。
    • WeakSet 对象中存储的对象值是弱引用的,若无其他变量或属性引用该对象值,则这个对象值会被当成垃圾回收掉。而 Set 对象则是存储强引用。
    • WeakSet 对象中存储的值是无法被枚举。而 Set 对象则可以枚举。
  5. Map 是存储键值对的有序列表,key 和 value 支持所有数据类型。对比 Set 集合,Map 集合多了 set 方法和 get 方法。看个🌰:

    var m = new Map()
    m.set('name', 'haha')
    m.set('year', '1999')
    console.log(m.get('name'), m.get('year')) // haha, 1999

    支持对象作为 key 值,看个🌰:

    const key = {}
    m.set(key, 'hehe')
    m.get(key) // 'hehe'
  6. 和 WeakSet 一样,也会有 WeakMap 存在,专门针对弱引用。看个🌰:

    var map = new WeakMap()
    var key = document.querySelector('.header')
    map.set(key, 'DOM')
    map.get(key) // 'DOM'
    key.parentNode.removeChild(key)
    key = null

迭代器(Iterator)和生成器(Generator)

  1. 迭代器是一种特殊对象,每一个迭代器对象都有一个 next(),该方法返回一个对象,包括了 value 和 done 属性。使用 ES5 模拟实现迭代器如下:

    function createIterator(items) {
      var i = 0
      return {
        next() {
          var done = (i >= items.length)
          var value = i < items.length ? items[i++] : undefined
          return { done, value }
        }
      }
    }
    var arr = createIterator([1, 2])
    console.log(arr.next()) // { done: false, value: 1 }
    console.log(arr.next()) // { done: false, value: 2 }
    console.log(arr.next()) // { done: true, value: undefined }
  2. 生成器就是一个函数,用于返回迭代器的。使用 * 号声明的函数即为生成器函数,同时需要使用 yield 控制进程。看个🌰:

    function *createIterator() {
      console.log(1)
      yield 1
      console.log(2)
      yield 2
      console.log(3)
    }
    var a = createIterator() // 执行后并不会输出任何东西
    console.log(a.next()) // 先输出1,再输出{ value: 1, done: false }
    console.log(a.next()) // 先输出2,再输出{ value: 2, done: false }
    console.log(a.next()) // 先输出3,再输出{ value: undefined, done: true }

    总结一下,迭代器执行 next() 方法时,只会执行前面到 yield 间的代码,后面代码都会被终止

    同样地,在 for 循环中使用迭代器,遇到 yield 时都会终止进程。看个🌰:

    function *createIterator(items) {
      for(let i = 0; i < items.length;  i++) {
        yield items[i]
      }
    }
    const a = createIterator([1, 2, 3]);
    console.log(a.next()); //{value: 1, done: false}
  3. yield 只能在生成器函数内使用。

  4. 生成器函数还可以使用匿名函数形式创建,看个🌰:

    const createIterator = function *() {
      // ...
    }
  5. 凡是通过生成器得到的迭代器,都是可迭代的对象(即可迭代对象具有 Symbol.iterator 属性),可使用 for...of 进行迭代。看个🌰:

    function *createIterator() {
      yield 1
      yield 2
    }
    var obj = createIterator()
    for(var val of obj) {
      console.log(val)
    }
    // 输出为
    // 1
    // 2

    可迭代对象可访问 Symbol.iterator 直接得到迭代器,看下面

    function *createIterator() {
      yield 1
      yield 2
    }
    var obj = createIterator()
    var newObj = obj[Symbol.iterator]() // 其实obj[Symbol.iterator]相当于createIterator迭代器

    Symbol.iterator 可用于检测一个对象是否可迭代

    typeof obj[Symbol.iterator] === "function"
  6. 默认情况下定义的对象是不可迭代的,但是可以通过 Symbol.iterator 创建迭代器。看个🌰:

    const obj = {
      items: [],
      *[Symbol.iterator]() {
        for (let item of this.items) {
          yield item;
        }
      }
    }
  7. 数组、Set、Map等可迭代对象,其内部已实现迭代器,并且提供3种迭代器调用,分别是:

    • entries():返回一个迭代器,用于返回键值对。

      var arr = [1, 2, 3]
      for(var [key, value] of arr.entries()) {
        console.log(key, value)
      }
      // 输出结果为
      // 0 1
      // 1 2
      // 2 3
    • values():返回一个迭代器,用于返回键值对的value。

      var arr = [1, 2, 3]
      for(var value of arr.values()) {
        console.log(value)
      }
      // 输出结果为
      // 1
      // 2
      // 3
    • keys():返回一个迭代器,用于返回键值对的key。

      var arr = [1, 2, 3]
      for(var key of arr.keys()) {
        console.log(key)
      }
      // 输出结果为
      // 0
      // 1
      // 2
  8. 高级迭代器功能,主要包括传参、抛出异常、生成器返回语句、委托生成器。

    • 传参。next 方法传参数时,会作为上一轮 yield 的返回值,除了第一轮 yield 外,看个🌰:

      function *aa() {
        var a1 = yield 1
        var a2 = 10
        yield a1 + 10
      }
      var a = aa()
      console.log(a.next(2)) // { value: 1, done: false }
      console.log(a.next(100)) // { value: 110, done: false }
    • 抛出异常。

      function *aa() {
        var a1 = yield 1
        var a2 = 10
        yield a1 + 10
      }
      var a = aa()
      console.log(a.next(2)) // { value: 1, done: false }
      console.log(a.throw(new Error('error'))) // error
      console.log(a.next(100)) // 不再执行
    • 生成器种遇到 return 语句时,表示退出操作。

      function *aa() {
        var a1 = yield 1
        return
        yield a1 + 10
      }
      var a = aa()
      console.log(a.next(2)) // { value: 1, done: false }
      console.log(a.next(100)) // { value: undefined, done: true }
    • 委托生成器。其实就是生成器嵌套生成器。

      function *aIterator() {
        yield 1;
      }
      function *bIterator() {
        yield 2;
      }
      function *cIterator() {
        yield *aIterator()
        yield *bIterator()
      }
      var i = cIterator()
      console.log(i.next()) // {value: 1, done: false}
      console.log(i.next()) // {value: 2, done: false}
  9. 异步任务执行器,其实就是用来循环执行生成器。

    因为我们知道生成器需要执行 N 次 next() 方法才能运行完,异步任务执行器就是帮我们做这些事情的。

    function run(taskFn) {
      var task = taskFn() // 调用生成器
      var result = task.next()
      function step() {
        if(!result.done) {
          result = task.next(result.value)
          step()
        }
      }
      step()
    }
    run(function *() {
      let text = yield fetch() // 异步请求获取数据
      doSomething(text) // 处理返回结果
    })

    异步任务执行器,其实就是间接实现了 async 和 await 功能。

类class

  1. 在 ES6 中,将原型的实现写在类中,和 ES5 本质上是一致的,都是需要新建一个类名,然后实现构造函数再实现原型方法。看个🌰:

    class Person {
      constructor(name) { // 新建构造函数
        this.name = name // 私有属性
      }
      sayName() { // 定义一个方法并且赋值到构造函数的原型中
        return this.name
      }
    }

    私有属性的定义,只需要在构造方法中定义this.xx = xx即可

  2. 类声明和函数声明的区别,主要有:

    • 类声明不能提升,而函数声明则会被提升。
    • 类声明中代码会自动强行运行在严格模式下。
    • 类中的所有方法都是不可枚举的,而函数声明的对象中方法则是可以枚举的。
    • 类中的构造函数只能使用 new 来调用,而函数则可以普通调用或 new 来调用。
  3. 类的定义有声明式定义和表达式定义,看个🌰:

    // 声明式定义
    class Person {
      // ...
    }
    
    // 表达式定义
    let person = Class {
      // ...
    }

    类还支持立即调用。

    let person = new Class {
      constructor(name) {
        this.name = name
      }
      sayName() {
        return this.name
      }
    }('Andraw')
    console.log(person.sayName()) // Andraw
  4. 类支持在原型上定义访问器属性。看个🌰

    class Person {
      constructor(name) {
        this.name = name
      }
      get myName() {
        return this.name
      }
      set myName(name) {
        this.name = name
      }
    }
    var descriptor = Object.getOwnPropertyDescriptor(Person.prototype, 'myName')
    console.log('get' in descriptor) // true
    console.log(descriptor.enumerable) // false 表示不可枚举

    类中定义的属性或方法名都是可支持表达式的

    const test = 'sayName'
    class Person{
      constructor(name) {
        this.name = name
      }
      [test]() {
        return this.name
      }
    }

    类中定义的方法同样可以是生成器方法。

    class Person {
      *sayName() {
        yield 1
        yield 2
      }
    }
  5. 静态属性或静态方法,是在属性或方法前面使用 static 关键字。static 修饰的方法或属性只能被类本省直接访问,而不能在实例中访问

    class Person {
      static sayName() {
        return this.name
      }
    }
  6. 在 React 中写一个组件Test时,必须得继承React.Component。其中 Test 组件就是一个派生类。派生类中的构造函数内部必须使用 super()

    关于 super 的使用需要注意:

    • 只可以在派生类中使用 super。派生类是指继承自其他类的新类。
    • 派生类中构造函数访问 this 之前必须要先调用 super(),负责初始化 this
    • 如果不想调用 super,可让类的构造函数返回一个对象。
  7. 当派生类继承于父类时,其父类中的静态成员也会被继承到派生类中,但是静态成员同样只能是被派生类访问,而无法被其实例访问

  8. 在构造函数中可使用 new target(new target 通常表示当前的构造函数名)来阻止实例化类。看个🌰:

    class Person {
      constructor() {
        if(new.target === Person) { // 不允许该类被调用实例化
          throw new Error("error")
        }
      }
    }

数组

  1. 在 ES5 中创建数组的方式有两种,分别是数组子面量(即 var arr = [])和 Array 实例(即 new Array())。

    在 ES6 中新增两种方法创建数组,分别是 Array.of() 和 Array.from()。

    • Array.of()。我们知道 new Array() 中传入一个数字时,表示的是生成多少长度的数组,Array.of() 就是为了处理这种尴尬场面的,看个🌰:

      const a1 = new Array(1)
      const a2 = Array.of(1)
      console.log(a1, a2) // [undefined], [1]
    • Array.from()。用于将类数组转换为数组。

      function aa() {
        const arr = Array.from(arguments)
        console.log(arr)
      }
      aa(1, 2)
      // [1, 2]
      
      // 可传第二个参数,作为第一个参数的转换
      const arr = Array.from(arguments, value => value + 2)
      
      // 可传第三个参数,用来指定this
      
      // Array.from可用于处理数组去重
      Array.from(new Set(...arguments))
  2. 数组新增的方法有。

    • find()。传入一个回调函数,找到数组中符合当前搜索规则的第一个元素,返回它,并且终止搜索。

      var arr = [1, 2, 3]
      console.log(arr.find(n => typeof n === "number")) // 1
    • findIndex()。传入一个回调函数,找到数组中符合当前搜索规则的第一个元素,返回它的下标,终止搜索。

      var arr = [1, 2, 3]
      console.log(arr.find(n => typeof n === "number")) // 0
    • fill()。用新元素替换掉数组内的元素,可以指定替换下标范围。格式和🌰如下:

      // 格式如下
      arr.fill(value, start, end)
      
      // 栗子
      const arr = [1, 2, 3]
      console.log(arr.fill(4)) // [4, 4, 4] 不指定开始和结束,全部替换
      
      const arr1 = [1, 2, 3]
      console.log(arr1.fill(4, 1)) // [1, 4, 4] 指定开始位置,从开始位置全部替换
      
      const arr2 = [1, 2, 3]
      console.log(arr2.fill(4, 0, 2)) // [4, 4, 3] 指定开始和结束位置,替换当前范围的元素
    • copyWithin()。选择数组的某个下标,从该位置开始复制数组元素,默认从0开始复制。格式和🌰如下:

      // 格式如下
      arr.copyWithin(target, start, end)
      
      // 栗子
      const arr = [1, 2, 3, 4, 5]
      console.log(arr.copyWithin(3)) // [1,2,3,1,2] 从下标为3的元素开始,复制数组,所以4, 5被替换成1, 2
      
      const arr1 = [1, 2, 3, 4, 5]
      console.log(arr1.copyWithin(3, 1)) // [1,2,3,2,3] 从下标为3的元素开始,复制数组,指定复制的第一个元素下标为1,所以4, 5被替换成2, 3
      
      const arr2 = [1, 2, 3, 4, 5]
      console.log(arr2.copyWithin(3, 1, 2)) // [1,2,3,2,5] 从下标为3的元素开始,复制数组,指定复制的第一个元素下标为1,结束位置为2,所以4被替换成2

Promise与异步编程

  1. 对 DOM 做事件处理操作,如点击、激活焦点、失去焦点等,再比如使用 Ajax 请求数据时利用回调函数获取返回值,都属于异步编程。

  2. Promise 中文意思就是承诺,Javascript 对你许一个承诺,会在未来某个时刻兑现承诺

    Promise 有生命周期,分别是进行中(pending)、已经完成(fulfilled)、拒绝(rejected)

    Promise 不会直接返回异步函数的执行结果,需要使用 then 方法获取,获取异常回调时,需要使用 catch 方法获取。

    结合 axios 看个🌰,axios 是前端比较热门的 http 请求插件之一。

    // 1. 创建axios实例
    import axios from 'axios'
    export const instance = axios.create()
    
    // 2. 使用axios实例 + Promise获取返回值
    const promise = instance.get('url')
    promise.then(res => console.log(res)).catch(err => console.log(err))
  3. Promise 构造函数只有一个参数,该参数为一个函数,被作为执行器。执行器有两个参数,分别是 resolve() 和 reject() ,前一个表示成功回调,后一个表示失败回调。

    new Promise((resolve, reject) => {
      setTimeout(() => resolve(5), 0)
    }).then(res => console.log(5)) // 5

    Promise 实例只能过 resolve 或者 reject 函数返回数据,并且使用 then 或者 catch 进行获取

    Promise.resolve(1).then(res => console.log(res)) // 1
    Promise.reject(2).catch(res => console.log(res)) // 2
    // 捕获错误时,可使用catch获取
    new Promise((resolve, reject) => {
      if(true) {
        throw new Error('error')
      }
    }).catch(err => console.log(err))
  4. 浏览器和 Node 提供了 unhandledRejection 和 rejectionHandled 两个事件处理 Promise 中没有设置 catch 问题

    // unhandledRejection
    let rejected
    rejected = Promise.reject("It was my wrong!")
    
    process.on("unhandledRjection", function(reason, promise) {
      console.log(reason.message) // It was my wrong!
      console.log(rejected === promise) // true
    })
    
    // rejectionHandled
    let rejected
    rejected = Promise.reject("It was my wrong!")
    
    process.on("rejectionHandled", function(reason, promise) {
      console.log(reason.message) // It was my wrong!
      console.log(rejected === promise) // true
    })

    浏览器中使用上面两个方法只是在 window 对象上监听,而 Node 中使用是在 process 对象上监听。

    unhandledRejection 和 rejectionHandled的区别就是,前者是事件循环中触发,后者则是事件循环后触发。两者都是处理 Promise 中使用 reject 捕获错误时,而没有使用 catch 进行捕获处理

  5. Promise 支持链式调用,有效地解决了回调地狱问题。看个🌰:

    new Promise((resolve, reject) => {
      resolve(1)
    }).then(res => { return res + 1 }).then(res => {console.log(res)}) // 2
  6. 除了 resolve 和 reject 方法外,还有两个方法,便是 Promise.all 和 Promise.race。

    • Promise.all。运行多个 Promise,当全部 Promise 都返回结果时,才会使用 then 进行处理

      Promise.all([
        new Promise(function(resolve, reject) {
          resolve(1)
        }),
        new Promise(function(resolve, reject) {
          resolve(2)
        }),
        new Promise(function(resolve, reject) {
          resolve(3)
        })
      ]).then(arr => {
        console.log(arr) // [1, 2, 3]
      })
    • Promise.race。和 all 方法类似,不过就是当有一个返回结果时,就可以使用 then 进行处理

      Promise.race([
        new Promise(function(resolve, reject) {
          setTimeout(() => resolve(1), 1000)
        }),
        new Promise(function(resolve, reject) {
          setTimeout(() => resolve(2), 10)
        }),
        new Promise(function(resolve, reject) {
          setTimeout(() => resolve(3), 100)
        })
      ]).then(value => {
        console.log(value) // 2
      })
  7. Promise 本身不是异步,只有它的 then 方法或者 catch 方法才是异步。

    目前 ES7 已经支持 async 方案,该方案比 Promise 还强大啊。

    async function a() {
      await function() {}
    }

代理(Proxy)和反射(Reflect)

  1. 代理 Proxy 就是拦截 JS 引擎内部目标的底层对象操作,反射 Reflect 就是针对 Proxy 还原原对象操作方法。

    代理陷阱 覆写的特性 默认特性
    get 读取一个属性值 Reflect.get()
    set 写入一个属性 Reflect.set()
    has in操作符 Reflect.has()
    deleteProperty delete操作符 Reflect.delete()
    getProperty Object.getPropertypeOf() Reflect.getPrototypeOf()
    setProperty Object.setPrototypeOf() Reflect.setPrototypeOf()
    isExtensible Object.isExtensible() Reflect.isExtensible()
    preventExtensions Object.preventExtensions() Reflect.preventExtensions()
    getOwnPropertyDescriptor Object.getOwnPropertyDescriptor() Reflect.getOwnPropertyDescriptor()
    defineProperty Object.defineProperty() Reflect.defineProperty()
    ownKeys Object.ownKeys()、Object.getOwnPropertyNames()、Object.getOwnPropertySymbols() Reflect.ownKeys()
    apply 调用一个函数 Reflect.apply()
    construct 用new调用一个函数 Reflect.construct()

    反射 Reflect 一般和代理 Proxy 结合使用,设置相应的代理方法处理数据时,需同时使用反射 Reflect 对原对象的方法操作一遍

    现在就拿 set 做个🌰。set属性是在要改变Proxy属性值时,进行的预处理,共接收四个参数:

    • target:要进行预处理的目标对象;
    • key:预处理过程的Key值,相当于对象的属性;
    • value:要设置成的值;
    • receiver:改变前的原始值;
    let pro = new Proxy({
      aa: 2
    }, {
      set: (target, key, value, receiver) => console.log(target, key, value, receiver);
    });
    pro.aa = 8;         // {aa: 2} "aa" 8 Proxy {aa: 2}
  2. Proxy的存在能够使函数加上钩子函数,即可理解为在执行方法前预处理一些代码,举个栗子:

    let pro = new Proxy({}, {
      get: (target, key, property) => console.log('haha')
    });
    pro.aa;                 // haha
    pro.bb;                 // haha
  3. 在使用如 http-proxy 插件或 webpack 时,有时需要访问某个 api,通过配置 proxy 跳转到指定的 url 能解决跨域问题。但是该种模式和代理 Proxy 有异曲同工之处,但是机制是不一样的。

    devServer: {
      proxy: [
        {
          context: "/api/*", //代理API
          target: 'https://www.hyy.com', //目标URL
          secure: false
        }
      ]
    }

使用模块封装代码

  1. 模块可以是函数、数据、类,需要指定导出的模块名,才能被其他模块访问。看个🌰

    // 数据模块
    const obj = { a: 1 }
    // 函数模块
    const sum = (a, b) => a + b
    // 类模块
    class Person {
      // ...
    }
  2. 模块引入使用 import 关键字,导入模块方式有两种。

    • 导入指定的模块。

      import { sum } from 'a.js'
      sum(1, 2)
    • 导入全部模块。

      import allFn from 'a.js'
      allFn.sum(1, 2)
  3. 模块导出使用 export 关键字,看个🌰:

    // 导出数据模块
    export const obj = { a: 1 }
    // 导出函数模块
    export const sum = (a, b) => a + b
    // 导出类模块
    export class Person {
      // ...
    }

    需要注意的是,ES6 提供了模块的默认导出,在导出时结合 default 关键字,看个🌰:

    // a.js
    function sum(a, b) {
      return a + b
    }
    export default sum
    
    // b.js
    import sum from 'a.js'
    sum(1, 0)
  4. 不能在语句和函数内使用 export 关键字,只能在模块顶部使用

  5. ES6 提供了两种方式修改模块的导入和导出名,分别是导出时修改和导入时修改,使用 as 关键字

    • 导出时修改。

      // a.js
      function sum(a, b) {
        return a + b
      }
      export default {sum as add}
      
      // b.js
      import add from 'a.js'
      add(1, 2)
    • 导入时修改。

      // a.js
      function sum(a, b) {
        return a + b
      }
      export default sum
      
      // b.js
      import sum as add from 'a.js'
      add(1, 2)
  6. 无绑定导入,是指当模块没有可导出模块时,全都是定义的全局变量,可使用无绑定导入。看个🌰:

    // a.js
    let a = 1
    const PI = 3.1314
    
    // b.js
    import 'a.js'
    console.log(a, PI) // 1, 3.1314
  7. 使用 webpack 打包 js 后,浏览器加载模块时,总是按顺序加载,先加载模块1,再加载模块2,因为 module 类型默认使用 defer 属性

    <script type="module" src="module1.js"></script>
    <script type="module" src="module2.js"></script>

ES7

ES7 在 ES6 基础上仅仅新增了**求幂运算符(**)**和 Array.prototype.includes() 方法。

需要注意的是,在 ES6 中仅仅提供了字符串 includes 实现,而在 ES7 中则在数组中进行完善

求幂运算符(**)

** 运算符相当于 Math 对象中的 pow 求幂方法,使用如下。

2 ** 3 = 8
// 相当于
Math.pow(2, 3) // 8

** 运算符和 +- 运算符用法一致,看个🌰:

let num = 2
num **= 3
// 相当于 num = num ** 3
console.log(num) // 8

Array.prototype.includes()

数组中实现的 includes 方法,用于判断一个数组是否包含一个指定的值,如果包含就返回 true,否则返回 false

includes 和 indexOf 都是使用 === 来进行比较,但是在 includes 中,NaN === NaN 返回的是 true,而 indexOf 则是返回 false

另外,includes 和 indexOf 方法都是认为,+0 === -0

ES8

ES8 也是在 ES6 基础上继续进行拓展。

Object.values()和Object.entries()

在 ES6 中提及过,只有可迭代对象可以直接访问 keys、entries、values 三个方法在 ES8 中在 Object 对象上实现了 values 和 entries 方法,因为 Object 已经支持了 kes 方法,直接看🌰:

var obj = {
  a: 1,
  b: 2
}
console.log(Object.keys(obj)) // ["a", "b"]
console.log(Object.values(obj)) // [1, 2]
console.log(Object.entries(obj)) // [["a", 1], ["b", 2]]

其中,entries 方法还能结合 Map 数据结构。

var obj = {
  a: 1,
  b: 2
}
var map = new Map(Object.entries(obj))
console.log(map.get('a')) // 1
// Map { "a" => 1, "b" => 2 }

字符串追加

  1. 字符串新增方法 String.prototype.padStart 和 String.prototype.padEnd,用于向字符串中追加新的字符串。看个🌰:

    '5'.padStart(2) // ' 5'
    '5'.padStart(2, 'haha') // 'h5'
    '5'.padEnd(2) // '5 '
    '5'.padEnd(2, 'haha') // '5h'

    padStart 和 padEnd 对于格式化输出很有用。

  2. 使用 padStart 方法举个例子,有一个不同长度的数组,往前面追加 0 来使得长度都为 10。

    const formatted = [0, 1, 12, 123, 1234, 12345].map(num => num.toString().padStart(10, '0'))
    console.log(formatted)
    // ["0000000000", "0000000001", "0000000012", "0000000123", "0000001234", "0000012345"]

    使用 padEnd 也是同样的道理。

Object.getOwnPropertyDescriptors

Object.getOwnPropertyDescriptors 直接返回一个对象所有的属性,甚至包括 get/set 函数。

ES2017 引入该函数主要目的在于方便将一个对象浅拷贝给另一个对象,同时也可以将 getter/setter 函数也进行拷贝。意义上和 Object.assign 是不一样的。

直接看个🌰:

var obj = {
  a: 1,
  b: {
    a: 2
  },
  set c(temp) {
    this.d = temp
  },
  get c() {
    return this.d
  }
}
var newObj1 = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj))
console.log(newObj1)
// {
//  c: undefined
//  a: 1
//  b: {a: 2}
//  get c: ƒ c()
//  set c: ƒ c(temp)
//  __proto__: Object
// }
var newObj2 = Object.assign({}, obj)
console.log(newObj2)
// {
//  a: 1
//  b: {a: 2}
//  c: undefined
//  __proto__: Object
// }

在克隆对象方面,Object.assign 只能拷贝源对象中可枚举的自身属性,同时拷贝时无法拷贝属性的特性(如 getter/setter)。而使用 Object.getOwnPropertyDescriptors 方法则可以直接将源对象的所有自身属性(是自身属性啊,不是所有可访问属性!)弄出来,再拿去复制

上面的栗子中就是配合原型,将一个对象中可访问属性都拿出来进行复制,弥补了 Object.getOwnPropertyDescriptors 方法短处(即无法获取可访问原型中的属性)。

若只是浅复制自身属性,还可以结合 Object.defineProperties 来实现

var obj = {
  a: 1,
  b: {
    a: 2
  },
  set c(temp) {
    this.d = temp
  },
  get c() {
    return this.d
  }
}
var newObj = Object.defineProperties({}, Object.getOwnPropertyDescriptors(obj))
conso.e.log(newObj)
// {
//  c: undefined
//  a: 1
//  b: {a: 2}
//  get c: ƒ c()
//  set c: ƒ c(temp)
//  __proto__: Object
// }

允许在函数参数最后添加逗号

听说是为了方便 git 算法更加方便区分代码职责。直接看个🌰。

const sum = (a, b,) => a + b

Async/Await

在 ES8 所有更新中,最有用的一个!!!

async 关键字告诉 Javascript 编译器对于标定的函数要区别对待。当编译器遇到 await 函数时会暂停,它会等到 await 标定的函数返回的 promise,该 promise 要么 resolve 得到结果、要么 reject 处理异常。

直接上一个栗子,对比一下使用 promise 和使用 async 区别。

// 模拟获取userName接口
var getUser= userId
 => new Promise(resolve => {
   setTimeout(() => {
     resolve(userName)
   }, 2000)
 })
// 模拟获取userAge接口
var getUserAge = userName
 => new Promise(resolve => {
   setTimeout(() => {
     if(userName === 'Andraw') {
       resolve('24')
     } else {
       reject('unknown user')
     }
   }, 2000)
 })
// ES6的promise实现方式
function es6Fn(userId) {
  getUser(userId)
    .then(getUserAge)
    .then(age => {
      console.log(age)  
    })
}
// ES8的async实现方式
async function es8Fn(userId) {
  var userName = await getUser(userId)
  var userAge = await getUserAge(userName)
  console.log(userAge)
}

使用 ES8 的 async 异步编程更符合日常开发流程,而 ES6 的 promise 也是一个很好的使用, ES8 的 async 只是在 promise 基础上更上一层楼。

  1. async 函数返回 promise。

    若想获取一个 async 函数的返回结果,则需要使用 promise 的 then 方法

    接着拿上述 ES8 的 async 实现方式来举个例子。

    async function es8Fn(userId) {
      var userName = await getUser(userId)
      var userAge = await getUserAge(userName)
      return userAge
    }
    // 获取es8Fn async函数返回结果
    es8Fn(1).then(userAge => { console.log(userAge) })
  2. 并行处理

    我们知道,每次调用 es8Fn 函数时,都需要等到至少 4 秒时间,若调用 N 次,则需要等到 4N 秒。使用 Promise.all 来并行处理,可以极大释放时间限制。

    async function newES8Fn() {
      var [a, b] = await Promise.all([es8Fn, es8Fn])
      return [a, b]
    }

    上述并行处理后,就可以很好滴避免多次调用而时间耗费的问题。

  3. 错误处理

    对于 async/await 的错误处理,有三种方法可以处理,分别是在函数中使用 try-catch、catch 每一个 await 表达式、catch 整个 async-await 函数。

    • 在函数中使用 try-catch

      async function es8Fn(userId) {
        try {
        	var userName = await getUser(userId)
          var userAge = await getUserAge(userName)
          return userAge 
        } catch(e) {
          console.log(e)
        }
      }
    • catch 每一个 await 表达式

      由于每一个 await 表达式都返回 Promise,对每一个表达式都进行 catch 处理。

      async function es8Fn(userId) {
        var userName = await getUser(userId).catch(e => { console.log(e) })
        var userAge = await getUserAge(userName).catch(e => { console.log(e) })
        return userAge
      }
    • catch 整个 async-await 函数

      async function es8Fn(userId) {
        var userName = await getUser(userId)
        var userAge = await getUserAge(userName)
        return userAge
      }
      es8Fn(1).then(userAge => { console.log(userAge) }).catch(e => { console.log(e) })

ES9

ES9(即ES2018) 主要新增了对象的扩展运算符 Rest 以及 Spread、异步迭代器、Promise支持 finally 方法、正则的扩展。

对象的扩展运算符 Rest 以及 Spread

如果使用过 Object.assign 方法合并对象,应该就很清楚。在 ES6 中,在数组中支持了 Rest 解构赋值和 spread 语法。

// ES6中的Rest
var [a, ...b] = [1, 2, 3, 4, 5, 6]
console.log(a, b) // 1, [2, 3, 4, 5, 6]

// ES6中的spread
function sum(a, ...b) {
  console.log(a, b)
}
sum(1, 2, 3)
// 输出为:1, [2, 3]

ES8 则在对象中支持了 Rest 解构赋值和 Spread 语法

// rest解构赋值
var {x, ...y} = {x: 1, a: 2, b: 3}
console.log(x, y) // 1, { a: 2, b: 3 }

// spread语法,接着上面解构的值
var c = {x, ...y}
console.log(c) // {x: 1, a: 2, b: 3}

异步迭代器和异步生成器

在 ES6 中,如果一个对象具有 Symbol.iterator 方法,那该对象就是可迭代的。目前,只有 Set、Map、数组内部实现 Symbol.iterator 方法,因此都是属于可迭代对象。

var set = new Set([1, 2, 3])
var setFn = set[Symbol.iterator]()
console.log(setFn) // SetIterator {1, 2, 3}
console.log(setFn.next()) // {value: 1, done: false}
console.log(setFn.next()) // {value: 2, done: false}
console.log(setFn.next()) // {value: 3, done: false}
console.log(setFn.next()) // {value: undefined, done: true}

默认的对象是不支持可迭代的,若实现了 Symbol.iterator 方法,那么它也是可迭代的。那么对象的 Symbol.iterator 方法如何实现的呢?

var obj = {
  a: 1,
  b: 2,
  [Symbol.iterator]() {
    var allKeys = Object.keys(this)
    var i = 0
    return {
      next: () => {
      	return {
          value: this[allKeys[i++]],
          done: i > allKeys.length
        }
      }
    }
  }
}
var objFn = obj[Symbol.iterator]()
console.log(objFn) // {next: ƒ}
console.log(objFn.next()) // {value: 1, done: false}
console.log(objFn.next()) // {value: 2, done: false}
console.log(objFn.next()) // {value: undefined, done: true}

上面的实现,还可以再完善一丢。利用生成器

var obj = {
  a: 1,
  b: 2,
  [Symbol.iterator]: function *() {
    for(let key in this) {
      yield this[key]
    }
  }
}
var objFn = obj[Symbol.iterator]()
console.log(objFn) // Generator {_invoke: ƒ}
console.log(objFn.next()) // {value: 1, done: false}
console.log(objFn.next()) // {value: 2, done: false}
console.log(objFn.next()) // {value: undefined, done: true}

由上面可以知道,同步迭代器就是一个特殊对象,里面包含有 value 和 done 两个属性(即 {value, done})。那么异步迭代器又是什么?

异步迭代器,和同步迭代器不同,不返回 {value, done} 形式的普通对象,而是直接返回一个 {value, done} 的 promise 对象

其中,同步迭代器使用 Symbol.iterator 实现,异步迭代器使用 Symbol.asyncIterator 实现

var obj = {
  a: 1,
	b: 2,
  [Symbol.asyncIterator]() {
    var allKeys = Object.keys(this)
    var i = 0
    return {
      next: () => {
        return Promise.resolve({
          value: this[allKeys[i++]],
          done: i > allKeys.length
        })
      }
    }
  }
}
var objAsyncFn = obj[Symbol.asyncIterator]()
console.log(objAsyncFn) // {next: ƒ}
console.log(objAsyncFn.next().then(res => {
  console.log(res) // {value: 1, done: false}
}))
console.log(objAsyncFn.next().then(res => {
  console.log(res) // {value: 2, done: false}
}))
console.log(objAsyncFn.next().then(res => {
  console.log(res) // {value: undefined, done: true}
}))

那么既然有了异步迭代器,就肯定有异步生成器,专门用来生成异步迭代器的

var obj = {
  a: 1,
	b: 2,
  [Symbol.asyncIterator]: async function *() {
    for(let key in this) {
      yield this[key]
    }
  }
}
var objAsyncFn = obj[Symbol.asyncIterator]()
console.log(objAsyncFn) // obj {<suspended>}
console.log(objAsyncFn.next().then(res => {
  console.log(res) // {value: 1, done: false}
}))
console.log(objAsyncFn.next().then(res => {
  console.log(res) // {value: 2, done: false}
}))
console.log(objAsyncFn.next().then(res => {
  console.log(res) // {value: undefined, done: true}
}))

另外,异步迭代器和同步迭代器有一样东西很类似,就是使用 next() 后,是无法知道什么时候才会到最后一个值,在同步迭代器中,需要使用 for...of 进行遍历才能有效地处理迭代器中每一项值

在异步迭代器中,同样支持遍历,不过是 for...await...of 遍历

var obj = {
  a: 1,
	b: 2,
  [Symbol.asyncIterator]: async function *() {
    for(let key in this) {
      yield this[key]
    }
  }
}
(async function() {
  for await (var value of obj) {
    console.log(value)
  }
})()

for...await...of 只会在异步生成器中或异步函数中有效

Promise支持 finally 方法

Promise 成功获取数据时使用 then 方法,处理异常时使用 catch 方法。但是在某些情况下,我们不管成功还是存在异常,都希望 Promise 能够运行一些共同的代码,finally 就是处理这些事情的

Promise.resolve(1).then(res => { console.log(res) }).finally(() => { console.lig('common code...') })
// 输出结果为
// 1
// common code...

正则的扩展

在正则表达式中,点 . 可以表示任意单个字符。

/foo.bar/.test('foo\nbar') // false

上面代码中,为了能够匹配任意字符,ES9 提出了 s 修饰符,使得 . 可以匹配任意单个字符。

/foo.bar/s.test('foo\nbar') // true

还有几个暂不讨论。可自行了解哈

ES10

ES10(即 ES2019) 新增功能相对比较少,都是一些性能的优化。

Array.prototype.flat和Array.prototype.flatMap

在日常开发中,我们常遇到一个问题,那就是将[1, [1, 2], [1, [2, 3]]]扁平化为[1, 1, 2, 1, 2, 3]

以往的经历告诉我们,需要使用第三方库 lodash 来处理,导致了不必要的麻烦,为此,ES10 直接为数组提供了 flat 方法来实现扁平化数组

var arr = [1, [1, 2], [1, [2, 3]]]
console.log(arr.flat(2)) // [1, 1, 2, 1, 2, 3]

flat 方法中参数,表示的是扁平化的层数

另外的方法 flatMap,其实就是数组的 flat 方法和 map 方法结合。

[1, 2, 3].map(x => [x * x]) // [[1], [4], [9]]
[1, 2, 3].flatMap(x => [x * x]) // [1, 4, 9]

Obejct.fromEntries

Object.fromEntries 方法和 ES6 中的 Object.entries 功能刚好相反,Object.entries 是获取一个对象的键值对,而 Object.fromEntries 则是将键值对转化为对象

Object.fromEntries([["a", 1], ["b", 2]]) // {a: 1, b: 2}
Object.entries({a: 1, b: 2}) // [["a", 1], ["b", 2]]

字符串去除首尾空格

ES10 为字符串提供了 trimStart 和 trimEnd 方法,用于去除首尾空格。

'  123'.trimStart() // 123
'123  '.trimEnd() // 123

Symbol.prototype.description

定义 Symbol 类型时,可传入一个字符串作为标志,若想获得该字符串,ES6 并没有提供方法,而 ES10 则提供了 description 属性用于获取 Symbol 的描述信息

var symbol = Symbol('haha')
console.log(symbol.description) // haha

可选的catch参数

在 ES10 之前,使用 try...catch 块时,若不给 catch 函数传递参数,会报错。

ES10 则直接将 catch 参数作为可选。

// ES10前
try {
  // ...
} catch(e) {
  console.log(e)
} 
// ES10后
try {
  // ...
} catch {
  // ...
}

Array.prototype.sort方法由快排转换为Timsort

在 ES10 前,数组的 sort 方法默认采取的是快排,但会存在不稳定性,为此,直接转为使用 Timsort。可自行了解一下。

Timsort 就是将插入排序和归并排序进行合并起来得到的好算法

函数支持toString方法

ES10 支持函数直接以字符串的形式打印出来。

var sum = (a, b) => a + b
console.log(sum.toString()) // (a, b) => a + b

Webpack 优化--构建速度篇

在日常的开发构建过程或生产打包构建过程中,我们都会遇到过因为构建速度非常缓慢导致耗时严重,不仅得让我们把时间浪费在等待的时间,最重要的还得会直接影响到我们的加班啊...😂

所以优化 Webpack 的构建速度显得尤为重要。当项目庞大时构建的耗时可能会变得更长,每次等待构建的耗时加起来也会是一个大数目。

为此,得总结一下如何在 Webpack 构建过程里加快速度。🤔

目录

  1. 使用resolve缩少文件的搜索范围
  2. DllPlugin构建动态链接库
  3. HappyPack多个进程处理文件
  4. ParalleUglifyPlugin多个进程压缩文件
  5. thread-loader(webpack4.0官方推荐)
  6. cache-loader、HardSourceWebpackPlugin处理文件缓存
  7. terser-webpack-plugin 压缩 js 代码

使用resolve缩少文件的搜索范围

Webpack 在启动后会从配置的 Entry 入口出发,解析出文件中的导入语句,再递归解析

Webpack 在解析文件过程中,遇到导入语句时,会做两件事:

  • 根据导入语句去寻找对应的要导入的文件。
  • 找到对应文件后,根据匹配到的后缀名,然后使用配置中相应的 Loader 处理该文件。

在小型项目中,上述两个过程都会很快,但是对于一个庞大系统来说,寻找解析就会显得尤为吃力,这样构建速度也会慢慢凸显出来。

Loader 中使用 include 针对性地处理文件

在 Loader 中通过配置其 include 属性,可针对性地对主要目录进行递归解析,可有效地避免处理其他没必要目录的递归解析。

module: {
  rules: [
    {
      test: /\.js$/,
      use: ['babel-loader'],
      include: path.resolve(__dirname, 'src') // 只对src目录进行递归解析,其他目录一律不解析
    }
  ]
}

配置 resolve.modules 减少第三方模块查找流程

如果有了解过 NodeJs 的童鞋,相信对于其加载模块机制会了解,这里就来简单描述一下。

在 Javascript 主进程执行过程中,当遇到要加载的第三方模块时,会在当前文件对应的目录下的 node_modules 文件夹去寻找该模块,若无则会向上一层的目录下的 node_modules (即 ../node_modules 文件夹)去查找,以此类推。

在上面这个过程中,无疑会产生很多没必要的遍历查找过程,为此,配置 resolve.modules 会直接指定在相应的目录下进行查找,以减少没必要的查找过程

resolve: {
  modules: [path.resolve(__dirname, 'node_modules')]
}

配置 resolve.mainFields 减少第三方模块入口查找流程

在开发过程中安装的第三方模块,都会自带一个 package.json 文件,用于描述该模块的属性,包括入口文件的定义。

第三方模块的入口文件可以有 browser、module、main 三个属性进行定义。而 reslove.mainFields 可用于配置直接从哪个属性进行入手加载第三方模块。默认情况下,reslove.mainFields 和当前环境 target 相关联。

  • 当 target 为 web 或者 webworker 时,默认值是["browser", "module", "main"];
  • 当 target 为其他情况时,值是["module", "main"];

由于绝大多数第三方模块都采用 main 字段来描述入口,为减少搜索入口步骤,我们可以直接配置 resolve.mainFields 为 main

resolve: {
  mainFields: ["main"]
}

配置 resolve.alias 减少完整第三方模块的查找依赖流程

以 Vue 为例,在打包后,都会存在两个文件,一个是 vue.js,另一个是 vue.min.js。前者用于开发环境,而后者用于生产环境。

开发环境下,Webpack 在构建过程遇到需要引入 vue 时,会直接寻找 vue.js。我们应该知道 vue.js 会分为很多个文件组成,在执行 vue.js 时就需要根据不同场景去引入相应模块进行解析,这过程就会导致耗时大。

为此,配置 resolve.alias 可以让 Webpack 在处理 vue 时,直接使用单独、完整的 vue.min.js 文件,从而跳过耗时的递归解析操作

resolve: {
  alias: {
    'vue': path.resolve(__dirname, './node_modules/vue/dist/vue.min.js')
  }
}

需要注意的是,不是所有的第三方模块都可以这样处理。只有完整的第三方模块才可以,那什么是完整的第三方模块。

我们知道,一个 vue 构建项目,基本每一个源代码都需要用到,因此它是完整的,再比如 lodash.js,我们在开发过程中,只需要按需引入部分方法,而且有绝大部分的源码我们是没必要引用的,因此它是不完整的。

配置 resolve.alias 只能在需要全部源代码的第三方模块进行使用,按需使用的第三方模块一律不能使用

若强制在不完整的第三方模块中使用 resolve.alias 配置,那么在 Tree-Shaking 过程中就无法去掉没用的代码,从而增大了包的体积,得不偿失啊...一定要慎用!!!

配置 resolve.extensions 减少文件后缀名匹配流程

相信很多童鞋都写过 import MyComponent from 'components/MyComponent' 这样类似语句,可以看到的是,一般情况下引入相应的 Vue 组件或 React 组件,都是不会加入相应的后缀名。

知道为啥不?

那是很多时候,在 Webpack 的 resolve.extensions 中配置了。在导入语句中没带文件后缀时,配置 resolve.extensions 可自动带上后缀名询问相应文件是否存在,默认值是 ["js", "json"]。

resolve: {
  extensions: ["vue"]
}

配置 resolve.extensions 可针对性地减少文件后缀名的匹配流程。

配置 module.noParse 忽略对未采用模块化文件的递归解析处理

形如 jQuery、ChartJS 等模块都是木有采用模块化标准的,如果要让 Webpack 解析这些文件将会耗费更多时间。

再比如前面讲到的,使用 vue.min.js 文件,由于 min.js 文件未采用模块化标准,都是包含全部源代码的。通过配置 module.noParse 可忽略对 vue.min.js 文件的递归解析处理。

module: {
  noParse: [/vue\min\.js$/],
}

DllPlugin构建动态链接库

在 window 系统会经常看到 DLL 后缀名文件,这些文件叫动态链接库,在一个动态链接库中保存着其他模块需调用的函数和数据。(这就有点像一张表,表里面保存了其他模块需要调用的函数以及数据对应的地址,方便查找)

要在项目中接入动态链接库的**,需要做到以下几点。

  • 将网页依赖的基础模块抽离出来,打包到一个个单独的动态链接库中,在一个动态链接库中可包含多个模块。
  • 当遇到需导入的模块存在于某个动态链接库中时,该模块将不会再被打包,而是直接到动态链接库中获取。
  • 页面依赖的所有动态链接库都需要被加载。

动态链接库加快构建速度的核心**是,大量复用模块的动态链接库只需要被编译一次,在后面的构建过程中被动态链接库包含的模块都不会重新编译,而是直接使用动态链接库中的代码

动态链接库中常常包含最常用的第三方模块,如 react、react-dom 等,所以只要不升级这些模块的版本,动态链接库就不需要重新编译。

在 Webpack 中,已经内置支持对动态链接库。主要通过两个内置插件接入,分别是

  • DllPlugin 插件。用于打包出一个个单独的动态链接库文件
  • DllReferencePlugin 插件。用于告诉 Webpack 使用了哪些动态链接库。

以 React 为例,在接入 DllPlugin 后,构建出来的目录结构是如下:

|-- main.js
|-- polyfill.dll.js // 包含所有依赖的polyfill,如Promise、fetch等API
|-- polyfill.manifest.json
|-- react.dll.js // 包含React的基础运行模块,如react和react-dom模块
|-- react.manifest.json

dll.js 文件中其实蕴含着大量模块的代码,这些模块代码都被放到一个数组中,用数组的索引作为其模块的 ID 标识,通过一个 dll_ 前缀变量全局暴露出来,即可以通过 window 进行访问(如 window.dll_react)。

manifest.json 文件是由 DllPlugin 生成的,描述了与对应的 dll.js 文件中包含哪些模块以及每个模块的路径和ID

动态链接库文件相关的文件需要一份独立的构建输出,用于主构建使用。新建一个 webpack_dll.config.js 文件

// webpack_dll.config.js
const path = require('path')
const DllPlugin = reqiure('webpack/lib/DllPlugin')

module.exports = {
  entry: {
    react: ['react', 'react-dom'],
    polyfill: ['core-js/fn/object/assign', 'core-js/fn/promise', 'whatwg-fetch']
  },
  output: {
    filename: '[name].dll.js',
    path: path.resolve(__dirname, 'dist'),
    // 存放动态链接库的全局变量名称,例如react就是_dll_react
    // 加上_dll_是为了防止全局变量冲突
    library: '_dll_[name]'
  },
  plugins: [
    new Dllplugin({
      // 动态链接库的全局变量名称,需和output.library中保持一致
      // 该字段也是输出的manifest.json文件中name字段的值,如在react.manifest.json文件中就有name: "_dll_react"
      name: '_dll_[name]',
      // 定义manifest.json文件输出时的文件名称
      path: path.join(__dirname, 'dist', '[name].manifest.json')
    })
  ]
}

接下来看看,如何在主流程的 webpack.config.js 中配置。

// webpack.config.js
const path = require('path')
const DllReferencePlugin = require('webpack/lib/DllReferencePlugin')

module.exports = {
  entry: {
    main: './main.js'
  },
  output: {
    filename: '[name].js',
    path: path.resolve(__dirname, 'dist')
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: ['babel-loader'],
        exclude: path.resolve(__dirname, 'node_modules')
      },
    ]
  },
  plugins: [
    // 告诉webpack使用了哪些动态链接库
    new DllReferencePlugin({
      manifest: require('./dist/react.manifest.json')
    }),
    new DllReferencePlugin({
      manifest: require('./dist/polyfill.manifest.json')
    })
  ]
}

在执行时,先将动态链接库相关文件编译出来,因为 Webpack 配置文件中定义的 DllReferencePlugin 依赖这些文件。因此在执行构建时流程如下:

  • 第一次执行时,动态链接库相关文件还没编译出来,需执行 webpack --config webpack_dll.config.js 命令,先编译得到动态链接库相关文件。
  • 第二次执行以后,由于动态链接库相关文件已经存在,可直接执行 webpack 命令运行项目。会发现项目构建速度比第一次快很多,因为动态链接库文件只会编译一遍。

HappyPack多个进程处理文件

在使用 HappyPack 时,可进行分解任务和管理线程。先来看看 webpack 如何接入 HappyPack。

const path = require('path')
const ExtractTextPlugin = require('extract-text-plugin')
const HappyPack = require('happypack')

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        // 将对.js文件的处理交给id为babel的happypack实例
        use: ['happypack/loader?id=babel'],
        exclude: path.resolve(__dirname, 'dist', 'node_modules')
      },
      {
        test: /\.css$/,
        // 将对.css文件的处理交给id为babel的happypack实例
        use: ExtractTextPlugin.extract({
          use: ['happypack/loader?id=css']
        })
      }
    ]
  },
  plugins: [
    // 创建happypack实例处理任务
    new HappyPack({
      id: 'babel',
      loaders: ['babel-loader?cacheDirectory'],
      // ...其他配置项
    }),
    // 创建happypack实例处理任务
    new HappyPack({
      id: 'css',
      loaders: ['css-loader'],
      // ...其他配置项
    }),
    new ExtractTextPlugin({
      name: '[name].css'
    })
  ]
}

上述配置中,所有文件的处理都交给了 happypack/loader ,根据 id 来告诉 happypack/loader 选择哪个 happypack 实例处理文件。

另外,在 happypack 实例中,还支持使用三个参数,分别是

  • threads:代表开启几个子进程处理这类型的文件,默认是3个。
  • verbose:是否允许使用 happypack 输出日志,默认是true。
  • threadPool:代表共享进程池,多个 Happypack 实例都是使用同一个共享进程池中的子进程去处理任务,以防止资源占用过多。

一般情况下,可按如下接入 happypack。

const happyThreadPool = HappyPack.ThreadPool({ size: 5 }) // 构造共享进程池,在进程池中包含5个子进程

modules.export = {
  plugins: [
    // 创建happypack实例处理任务
    new HappyPack({
      id: 'babel',
      loaders: ['babel-loader?cacheDirectory'],
      // ...其他配置项
      threadPool: happyThreadPool
    }),
    // 创建happypack实例处理任务
    new HappyPack({
      id: 'css',
      loaders: ['css-loader'],
      // ...其他配置项
      threadPool: happyThreadPool
    }),
    new ExtractTextPlugin({
      name: '[name].css'
    })
  ]
}

在使用 HappyPack 前,需如下安装:

npm i -D happypack

在 webpack 构建流程中,使用 loader 对文件的转换操作无疑是最耗时的流程

HappyPack 核心原理就是将 loader 对文件的转换操作分解到多个进程中去并行处理,从而减少构建时间

实例化 HappyPack 时,其实就是告诉 HappyPack 核心调度器通过一系列 loader 去转换一类文件,并且指定如何为该类文件转换操作分配子进程。在执行 webpack 时,核心调度器会将一个个任务分配给当前空闲子进程,子进程处理完毕就会将结果发送给核心调度器,它们间的数据交换就是通过进程间的通信 API 实现的

ParalleUglifyPlugin多个进程压缩文件

在发生产时,我们都经常使用 uglifyJS 对源代码进行压缩处理。在压缩代码过程中,需要先将 js 代码解析成抽象语法树 AST,接着再去应用各种规则分析和处理 AST,这过程计算量可谓是巨大的,耗时特别大。

使用 uglifyJS 会对文件进行一个一个的压缩,并不会并行处理,这无疑是耗时最大的。借鉴于 HappyPack 多进程分配任务优点,使用 ParalleUglifyPlugin 可以开启多个子进程,将对多个文件的压缩工作分配给多个子进程完成,每个子进程其实都是使用 uglifyJS 进行压缩,但是区别就是并行处理

const ParalleUglifyPlugin = require('webpack-parallel-uglify-plugin')

module.exports = {
  plugins: [
    // 使用ParallelUglifyPlugin并行压缩输出Javascript代码
    new ParallelUglifyPlugin({
      // 传递给uglifyJS的参数
      uglifyJS: {
        output: {
          // 紧凑输出
          beautify: false,
          // 删除所有注释
          comments: false
        },
        compress: {
          // 在UglifyJs删除没有用到的代码时不输出警告
					warnings: false,
          // 删除所有的console语句,可兼容IE浏览器
          drop_console: true,
          // 内嵌已定义但是只用到一次的变量
          collapse_vars: true
        }
      }
    })
  ]
}

当然 ParalleUglifyPlugin 还支持其它参数,就不一一列举,可自行观察其官方文档。需要注意的是,ParalleUglifyPlugin 同时内置了 UglifyJS 和 UglifyES,UglifyES 可并行压缩 ES6 代码

在使用 ParalleUglifyPlugin 时,需安装:

npm i -D ParalleUglifyPlugin

需要说明的一点是,ParalleUglifyPlugin 在 4.0 版本已经被废弃掉了,Github上 基本是处于一个没人管理和维护状态

thread-loader(webpack4.0官方推荐)

原理就是,将该 loader 放置于其他 loader 的前面,那么其他 loader 就会在一个单独的 worker poll (工程池)中运行,其中一个 worker 就相当于一个 Nodejs 进程,每个 worker 处理时间上限为 600ms。

我们都知道,webpack 在解析模块时都是单线程执行,也就说明每个模块都会按顺序滴逐个进行解析,这样无疑就是最耗时工作。那么 thread-loader 可以很好处理该过程。

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        // 创建一个 js worker 池
        use: [ 
          'thread-loader',
          'babel-loader'
        ] 
      },
      {
        test: /\.s?css$/,
        exclude: /node_modules/,
        // 创建一个 css worker 池
        use: [
          'style-loader',
          'thread-loader',
          {
            loader: 'css-loader',
            options: {
              modules: true,
              localIdentName: '[name]__[local]--[hash:base64:5]',
              importLoaders: 1
            }
          },
          'postcss-loader'
        ]
      }
      // ...
    ]
    // ...
  }
}

据说,happypack 作者已经不在进行维护,推荐使用的就是 thread-loader

##cache-loader、HardSourceWebpackPlugin处理文件缓存

cache-loader只针对 loader,就是说对于性能开销大的 loader 处理,可以使用 cache-loader 对其结果进行缓存,那么在第二次编译时就可以直接忽略。

module.exports = {
  module: {
    rules: [
      {
        test: /\.ext$/,
        use: ['cache-loader', ...loaders],
        include: path.resolve('src'),
      },
    ],
  },
};

需要注意的是,cache-loader 不能滥用,要选择最耗性能的 loader 上使用

那么,另外一个 HardSourceWebpackPlugin 作用就在于,第一次构建花费正常的时间,那么在第二次构建将显着加快(据说是 90% 以上)

terser-webpack-plugin 压缩 js 代码

在 Webpack 4.0 开始,webpack 内部的 webpack.optimize.UglifyJsPlugin 已经不再支持,也不推荐使用 parallelUglifyPlugin。它重新内置 terser-webpack-plugin 插件压缩优化代码。

terser-webpack-plugin 用于 ES6+ 的 Javascript 解析器,当然也支持多进程

module.exports = {
  optimization: {
    minimizer: [
      new TerserPlugin({
        parallel: true,
      }),
    ],
  },
};

Webpack 究竟是如何运行的?

在日常开发过程中,我们是需要经常将心思放在如何使用 Webpack 优化开发体验以及输出的质量,却很少把目光放在 Webpack 的启动流程、loader 和 plugin 基本原理等等方面上。

那么,现在我们就来一起来简单缕一缕,究竟 Webpack 是怎么运行的?以及 loader 和 plugin 又是如何工作的?

目录

  1. 现在构建工具对比
  2. Webpack构建流程
  3. 理解 Loader
  4. 理解 Plugin

现代构建工具对比

在探究之前,我个人觉得是很有必要说一说的,那就是现代构建工对比。为什么?

在前端快速发展的这几年,如何高效滴构建项目变得越来越重要,但是在百花争艳的构建工具里面,究竟是选用哪一款?那么现代构建工具有那么多,肯定得有它们适用场合,因此了解它们也是很有必要的。🤔

今天要讲的现代工具,主要是 NPM Script、Grunt、Glup、Fis3、Webpack、Rollup。下面我就来看看它们究竟是用来干啥的。

NPM Script

如果你用过 package.json,那么你对它肯定不陌生。NPM Script 是 NPM 内置的一个功能,允许在 package.json 文件里面使用 scripts 字段定义的任务。举个🌰:

{
  "scripts": {
    "start": "node main.js",
    "prod": "node prod.js"
  }
}

在定义上述的任务后,我们就可以直接在项目根路径下执行如下命令:

npm start

执行 NPM 相应的命令后,会根据 package.json 执行命令对应的任务

毫无疑问,NPM Script 的优点就是内置,无需安装其他依赖,缺点就是功能太简单,不能方便滴管理多个任务之间的依赖

Grunt

Grunt 和 NPM Script 一样,都只是一个任务执行者。但相比之下,Grunt 有着大量现成的插件封装了常见的任务,也能管理任务之间的依赖关系,并能自动化滴执行依赖的任务,每个任务的具体代码和依赖关系写在 Gruntfile.js 里。

module.exports = function(grunt) {

  // Project configuration.
  grunt.initConfig({
    pkg: grunt.file.readJSON('package.json'),
    uglify: {
      options: {
        banner: '/*! <%= pkg.name %> <%= grunt.template.today("yyyy-mm-dd") %> */\n'
      },
      build: {
        src: 'src/<%= pkg.name %>.js',
        dest: 'build/<%= pkg.name %>.min.js'
      }
    }
  });

  // Load the plugin that provides the "uglify" task.
  grunt.loadNpmTasks('grunt-contrib-uglify');

  // Default task(s).
  grunt.registerTask('default', ['uglify']);

};

在项目根目录执行命令 grunt,就会启动 uglify 功能对指定的 JavaScript 文件进行压缩,上述配置中还从源文件 package.json 中读取信息。

Grunt 最大的优点在于能够通过大量现成插件进行功能的拓展,缺点就是集成度不高,几乎都是依赖第三方插件,无法做到开箱即用。

Grunt 相当于是 NPM Script 的进化版,它的诞生就是弥补 NPM Script 的不足。

Gulp

Glup 是一个基于流的自动化构建工具。除了可以管理和执行任务,还支持监听文件、读写文件。

const { src, dest } = require('gulp');
const babel = require('gulp-babel');

exports.default = function() {
  return src('src/*.js')
    .pipe(babel())
    .pipe(dest('output/'));
}

上述栗子中,通过读取 src 目录下的所有 js 文件,通过 babel 插件转换后,然后写入到 output 目录下。

Gulp 的优点就是引入了流的概念,可以更好支持读写文件以及监听文件变化,缺点和 Grunt 一样,集成度依然不高,都是需要第三方插件完成大部分功能

Glup 可以看成是 Grunt 的加强版,相比于 Grunt,Glup 增加了监听文件、读写文件、流式处理的功能。

Fis3

Fis3 是一款来自百度的优秀国产构建工具。相比 Grunt 和 Glup,它集成了 Web 开发中的常用功能,主要包含如下:

  • 读写文件。通过 fis.match 读文件
  • 资源定位。解析文件之间的依赖关系
  • 文件缓存。通过 useHash 配置输出文件时为文件 URL 加上 md5 戳,以优化浏览器的缓存
  • 文件编译。通过 parser 配置文件解析器来完成文件转换,如 ES6 转换成 ES5。
  • 压缩资源。通过 optimizer 配置代码压缩方法。
  • 图片合并。通过 spriter 配置合并 CSS 里导入的图片到一个文件中,从而减少 HTTP 请求。
fis.match('*.{js, css, png}', { // 为文件添加md5
  useHash: true
})
fis.match('*.ts', { // 转化ts文件
  parser: fis.plugin('typescript')
})

Fis3 的优点是集成了各种 Web 开发所需的构建功能,配置简单并且开箱即用。缺点就是官方目前不再进行更新和维护,也不支持最新版的 Node.js

Fis3 相比 Grunt 和 Glup 构建工具来说,进一步加强了集成功能,如果将 Grunt、Glup 比作汽车的发动机话,则可以将 Fis3 比作一辆完整的汽车。

Webpack

作为今天的主角——Webpack,可以说是目前集成功能最全、使用最广泛的构建工具。

Webpack 是一个 JavaScript 打包模块化的工具,在 Webpack里一切文件皆模块,通过 Loader 转换文件,通过 Plugin 注入事件钩子,最后输出由多个模块组合成 Chunk 并转换成相应的文件进行输出

module.exports = {
	entry: './main.js',
  output: {
    filename: '[name].js',
    path: path.resolve(__dirname, './dist')
  }
}

Webpack 的优点在于专注处理模块化功能,能做到开箱即用、一步到位,能通过 Plugin 进行扩展,并且社区也活跃。缺点就是只能用于模块化开发的项目(其实这也不算是缺点,个人觉得)

相比之下,webpack 可以说是一个完整的集成度、完善的第三方插件管理和社区活跃的模块化构建工具。

Rollup

Rollup 是一个和 Webpack 很类似但专注于 ES6 的模块打包工具。它的亮点在于能够针对 ES6 源码进行 Tree Shaking 以及对没用的代码进行 Scope Hoisting,从而优化输出质量。当然,Webpack 也在内部实现这些功能。

相比之下,Rollup 在用于打包 JavaScript 库时比 Webpack 更有优势,因为其打包出来的文件更小。但它的缺点就是还不够完善,在很多场景下还找不到相应的解决方案。

Webpack构建流程

要了解 Webpack 是怎么进行构建的,首先得下列几个核心概念进行入手。

  • Entry:Webpack 构建的入口,也就是输入配置。
  • Output:Webpack 构建的输出配置。
  • Module:模块配置,一切文件都是模块,一个模块对应一个文件。
  • Chunk:代码块,一个代码块由一个或多个模块构成,用于代码合并和分割。
  • Loader:模块转换器,用于将模块的原内容转换成新内容。
  • Plugin:插件扩展功能器,用于监听Webpack在生命周期内广播事件时执行相应的回调逻辑。

那么,Webpack 的构建流程是如何的呢?

其实 Webpack 的构建流程可分为三个阶段,分别是初始化阶段、编译阶段、输出阶段。下面就简单总结一下。

  1. 初始化阶段

    首先会初始化参数(shell脚本中参数和配置中参数合并),根据参数初始化 compiler 编译对象,接着就是加载配置项里的插件(创建相应插件实例,开始监听 Webpack 声明周期中的广播事件)。

  2. 编译阶段

    将初始化阶段得到的 compiler 编译对象,执行 run 方法正式开始进行编译。

    首先会从配置项里的 Entry 入手,找到所有的入口模块。

    接着从入口模块开始递归寻找,将匹配到的模块使用相应的 Loader 进行转换,重复该过程直到所有的依赖模块寻找完为止。

    最后,编译结束后将得到所有转换好的模块以及各个模块之间的依赖关系。

  3. 输出阶段

    根据编译阶段得到的各个模块间依赖关系,将一个个模块开始组合成一个 Chunk,并将 Chunk 转换文件,最终将文件输出到文件系统中。

在了解完 Webpack 的构建流程后,我相信你肯定和我有一个疑问,那就是为什么不直接将转换好的模块直接转变成文件输出?而是将多个模块根据依赖关系组成 Chunk 并转变成文件再输出?

其实组成一个 Chunk 是有目的的,就是为了减少网络请求。如果仅仅是把转换好的模块转变成文件来输出,是没问题的,但是在浏览器中,访问一个模块文件时,当发现它还有依赖的文件,那么就需要通过网络请求相应的依赖文件,这样就不得不耗费时间在网络请求上。另外浏览器环境不像 Nodejs 那样能够快速滴从本地加载一个模块文件,因此根据依赖关系组装成一个 Chunk 并转换成文件后,将大大减少对网络的依赖程度。

理解 Loader

Loader 就像一个翻译员,能将源文件经过转换后输出新的内容,并且一个文件还能经过链式的 Loader 处理。

在这里,我会先提一个问题,就是常问的,多个 Loader 的执行顺序是怎样的?为什么?

其实,Loader 的执行顺序是从右到左的。Webpack 是由于跟随函数式编程的缘故,才会采取了从右到左的顺序执行 Loader。

Webpack 实现 Loader 执行选择函数式编程中的 compose 方式,而不是选择 pipe 方式(pipe 方式实现的是从左到右,如果要实现从左到右也不难)。在函数式编程中,实现方式都是从右到左的,看个🌰:

// 摘自网上例子
const compose = (...fns) => x => fns.reduceRight((v, f) => f(v), x);
const add1 = n => n + 1; //加1
const double = n => n * 2; // 乘2
const add1ThenDouble = compose(
  double,
  add1
);
add1ThenDouble(2); // 6
// ((2 + 1 = 3) * 2 = 6) 

一个 Loader 就是一个 Node.js 模块,这个模块需要导出一个函数。导出函数的工作就是获得处理前的原内容,对原内容执行处理后返回处理后的内容。

那么如果要编写一个简单的 Loader ,需要怎样子操作呢?

由于 Loader 是运行在 Node.js 中,所以我们可以调用任意的 Node.js 自带的 API,或者安装第三方模块进行调用,先看个最简单的编写栗子:

const sass = require('node-sass')
module.exports = function(source) {
  return sass(source)
}

另外,Webpack 还提供了一些 API 供 Loader 调用,简单列举一下常用的 API。

  • this.callback。用于 webpack 和 loader 间的通信,在 loader 使用时,告诉 webpack 返回结果。
  • this.context。假如当前 loader 处理的文件是 /src/main.js ,则 this.context 等于 /src。
  • this.resource。当前处理文件的完整请求路径。
  • this.resourceQuery。当前处理文件的 queryString。
  • this.traget。相当于 Webpack 配置中的 Target。
  • this.emitFile。输出一个文件。
  • this.clearDependcies。清除当前正在处理文件的所有依赖。
  • this.addDependcies。当前处理文件添加其依赖的文件。

那么编写完 Loader 后,如何让 Webpack 加载本地 Loader?

  1. NPM link

    NPM link 专门用于开发和调试本地的 NPM 模块的,能做到不发布模版的情况下,将本地的一个正在开发的模版的源码链接到项目 node_modules 目录下,让项目可以直接使用本地的 NPM 模块。

    要完成 NPM 的步骤,需按以下实现:

    • 确保在本地开发好 NPM 模块中 package.json 已经配置好。
    • 在本地的 NPM 模块目录下执行 npm link 命令,将本地模块注册到全局。
    • 在项目根目录下执行 npm link loader-name,将第二步注册到全局的本地 NPM 模块链接到项目的 node_modules 下,其中 loader-name 是指在第一步的 package.json 文件中配置的模块名称。
  2. ResolveLoader

    为了让 Webpack 加载放在本地的 loader,可以直接配置 resolveLoader.modules。

    module.exports = {
      resolveLoader: {
        modules: ['node_modules', './loaders/']
      }
    }

    上面配置中,当在 node_modules 中找不到相应的 loader 时,便会从 loaders 文件夹中去寻找。

理解 Plugin

在 Webpack 运行的生命周期中会广播许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。

在开发 Plugin 时最常用的两个对象是 Compiler 对象和 Compilation 对象,它们是 Plugin 和 Webpack 之间的桥梁。

  • Compiler 对象:包含了 Webpack 环境的所有配置信息(即 options、loaders、plugins 等信息),可以理解为 Webpack 实例。
  • Compilation 对象:包含了当前 Webpack 的模块资源、编译生成资源、变化的文件等。每当监测到一个文件发生变化,便有一次新的 Compilation 对象被创建。

那么,Compiler 对象和 Compilation对象之间的区别是什么?Compiler 对象代表了整个 Webpack 从启动到关闭的生命周期,而 Compilation 对象只代表一次新的编译

另外,Webpack 通过 Tagable 来组织这条复杂的生产线,在 Webpack 的运行过程中会广播事件,Plugin 只需要监听它关心的事件,就能加入这条生产线中,并能改变生产线的运作

Webpack 的事件流机制采用的是观察者模式,和 Node.js 中的 EventEmitter 非常相似,并且 Compiler 对象和 Compilation 对象都继承自 Tapable,可在 Compiler 对象和 Compilation 对象上广播和监听事件。

// 广播事件
compiler.apply('eventName', params)

// 监听事件
compiler.plugin('eventName', function(params) { 
  //... 
})

因此,在编写一个 Plugin 时,可如下操作:

// TextPlugin.js
class TextPlugin {
  constructor(options) { // 在构造函数中获取用户为该插件传入的配置
    // ...
  }
  apply(compiler) { // Webpack会调用TextPlugin实例的apply方法为插件实例传入compiler对象
    compiler.plugin('eventName', function(params) {
      // ...
    })
  }
}
module.exports = TextPlugin // 导出plugin

// webpack.config.js
const TextPlugin = require('./TextPlugin')
module.exports = {
  plugins: [
    new TextPlugin()
  ]
}

移动端适配方案

在当年 Web 端盛行的年代,处处为了处理不同浏览器之间的兼容性而猛抓头脑,本以为在手机时代的到来后,移动端网页浏览基本不需要处理兼容性问题时,却恰恰出现另一个必须处理的事情,那就是今天要讲的移动端网页适配方案。

在我们上班时,经常是设计师以 iphone6 作为基准(即物理像素为 750px)设计好设计图后,交给前端开发者,然后由开发者定义一套适配方案来适配其他任意尺寸的手机

那么在开始之前必须先了解某些知识点。

必须先了解的像素知识

  1. 物理像素(physical pixel)。

    一个物理像素就是显示手机频幕上最小的物理显示单位

    那么我们平时说 iPhone 6 的分辨率是 750 * 1334,那么横向物理像素值就是 750px,竖向物理像素值就是 1334 px。

  2. 设备的独立像素(density indenpendent pixel)。

    设备的独立像素也叫逻辑像素,其中 CSS 像素就是设备独立像素的一种

    那么我们平时在浏览器的控制台做自适应调试时,常常可以发现 iPhone 6 是 375 * 667,其中 375 就是 CSS 像素值。

  3. 设备的像素比(device pixel ratio)。

    设备的像素比的计算可以按照如下计算。

    设备的像素比 = 物理像素 / 设备的独立像素

1px的物理像素问题

在日常开发中,设计师以 iPhone 6 作为基准来设计相应的设计图,那么他们在设计图中画一条 1px 的线时,那么粗心的开发者如果没有做移动端适配方案的话,就会直接写 1px 出来。

当然上述的操作肯定不能通过设计师们的审核,因为在真机上看到的效果便是一条很粗的线。为什么?

不卖关子啦,其实就是由于 iPhone 6 的 CSS 像素值为 375px,也就说明在 iPhone 6 中设置宽度为 375px 时,就相当于设置全屏。问题来了,当在 375px 中设置一个 1px 的线时,放到真机 750px 中显示肯定会变粗为 2px

简单说明,1px 是相对物理像素而言,而 0.5px 时才是相对 CSS 像素值而言。

既然知道了问题所在,那么解决方案有哪些?接下来我就简单总结一下。

  1. 媒体查询(-webkit-min-device-pixel-ratio)

    .border {
      border: 1px solid #000;
    }
    @media screen and (-webkit-min-device-pixel-ratio: 2) {
      .border {
        border: 0.5px solid #000;
      }
    }
    @media screen and (-webkit-min-device-pixel-ratio: 3) {
      .border {
        border: 0.333px solid #000;
      }
    }

    优点:能有效地根据设备像素比来设置相应的边框长度。

    缺点:安卓和低版本的IOS并不适用。

  2. transform变形

    .border {
      &::after {
        content: '';
       	position: absolute;
       	border: 1px solid #000;
        transform: scale(0.5);
        -webkit-transform: scale(0.5);
        pointer-events: none; /* 防止点击触发 */
        @media screen and (min-device-pixel-ratio:3),(-webkit-min-device-pixel-ratio:3){
          -webkit-transform: scale(0.33);
          transform: scale(0.33);
        }
      }
    }

    优点:兼容性相对较好。

    缺点:对于圆角部分无法识别,也无法做到变形。

  3. box-shadow阴影

    .border {
      -webkit-box-shadow: 0 1px 1px -1px #000;
    }

    优点:基本兼容所有场景,包括圆角。

    缺点:颜色不好处理,稍微处理不顺就会体验效果不好。

  4. linear-gradient颜色浅变

    .border {
      height: 1px;
      background: linear-gradient(0deg, #fff, #000);
    }

    优点:可以处理颜色部分。

    缺点:对于部分安卓手机兼容性并不友好。

  5. 动态设置 viewpoint 中的 initial-sacle 值

    下面会讲到这种方案,也是手机淘宝所使用方案

移动端适配方案

  1. 百分比布局 + 媒体查询。

    .container {
      width: 300px;
    }
    @media screen and (max-width: 375px) {
      .container {
        width: 150px;
      }
    }

    相对比较传统方案,需要兼容所有的手机尺寸,对于维护会造成很大的成本,因为新出一款新的不同尺寸手机都需要立马兼容。

  2. Flex 弹性布局

    对于 Flex 弹性布局,是目前天猫手机端一直沿用的方式。

    必须先固定好 viewpoint

    <meta name="viewpoint" content="width=device-width, initial-scale=1, user-scalable=no">

    对于编写元素的高度和宽度时,必须要将高度写死,宽度自适应

    这样一来,当移动端的屏幕大小发生变化时,元素也会跟着变化。

  3. rem 布局

    必须先设置好 viewpoint

    <meta name="viewpoint" content="width=device-width, initial-scale=1, user-scalable=no">

    接着要动态计算根元素的 font-size 值

    const clientWidth = document.documentElement.clientWidth
    if(clientWidth > 750) clientWidth = 750
    document.documentElement.style.fontSize = clientWidth / 7.5 + 'px'

    对于上述代码,简单阐述一下,设计师的设计稿是一般以 750px 物理像素作为标准来做设计图的,那么以 100 作为参照数(即100px = 1rem),主要是为了好计算,那么 750px 物理像素就是相当于 7.5 em了。

    那么 document.documentElement.clientWidth 获取的是设备宽度,即 CSS 像素,因此在 iPhone 6 中为 375px,那么根元素的 font-size 值便为 375 / 7.5 = 50px。

    那么在接下来编写元素的宽高或者边距时,则需要自己去计算。

    // 以前没做适配的写法,其中100px是设计稿的宽度值
    .container {
      width: 100px;
      height: 200px;
    }
    
    // rem布局写法,即在原来px基础上除以7.5
    .container {
      width: 13.33rem;
      height: 26.66rem;
    }

    这种方案也是移动端网易页面在使用的方案。缺点就在于需要开发者手动去计算,当然也可以使用 sass 中的函数来处理

    @function pxToRem($num) {
      @return ($num/7.5) * 1rem;
    }
    .container {
      width: pxToRem(100);
      height: pxToRem(100);
    }
  4. rem布局 + viewpoint缩放(也叫手机淘宝处理方案)

    手机淘宝处理方案,相当于在 rem 布局情况下,再对 viewpoint 中 inial-scale 进行动态加载

    动态加载 viewpoint 中 inial-scale 。

    const scale = 1 / window.devicePixelRatio
    document.querySelector('meta[name="viewpoint"]').setAttribute('content', `initial-scale=${scale}, maximum-scale=${scale}, minimum-scale=${scale}, width=device-width, user-scalabel=no`)

    动态计算根元素的 font-size 值。淘宝另一个基准就是,html 元素的font-size计算公式为 font-size = deviceWidth / 10

    document.documentElement.style.fontSize = documemt.documentElement.clientWidth / 10 + 'px'

    在接下来的容器添加宽高属性时,也跟 rem 一样步骤,不过重点就在于,淘宝的计算会比网易难,需要自行计算。

    @function pxToRem($num) {
      @return ($num / 75) * 1rem;
    }
    .container {
      width: pxToRem(100);
      height: pxToRem(100);
    }

    拿淘宝来说的,他们用的设计稿是750的,所以html的font-size就是75px,如果某个元素时150px的宽,换算成rem就是150 / 75 = 2rem

    为此,手机淘宝还特意封装了这种做法,叫 flexble.js。详情可看下面

    https://github.com/amfe/lib-flexible

  5. vw 布局

    类似于 rem 布局,不过 rem 是相对根元素的 font-size 值确定,而 vw 相对屏幕视口宽度(即横向分辨率)确定,也就是长度等于 (视口宽度 / 100)

    举个例子,在 750px 的设计图中,当看到一个元素标注的字体大小为 32px 时,那么就需要使用 vw 单位转化,也就是等于 32 / (750 / 100)vw。

    需要注意的是,必须设置 viewpoint 属性

    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1">

可参考一下别人的好文章。

[移动端前端适配方案对比](

Review-Question-React

  1. 说说react的virtual dom及其diff的实现方式
  2. 说一说react的生命周期
  3. react中组件有哪几种类型?那么创建组件又有哪几种方式?
  4. 在react和vue中,组件上写上key的作用是什么?
  5. 聊聊setState机制
  6. 谈谈对react批量更新(batchUpdates)理解?
  7. react如何区别component和dom?
  8. 说说react组件间的通信方式都有哪些
  9. 如何将一个子组件单独渲染出来和父组件平级关系?
  10. react组件都有哪些优化手段?
  11. 说说react的事件机制
  12. 有了解过Fiber吗?
  13. 谈谈高阶组件和基类
  14. 谈谈React.Component、React.PureComponent和无状态组件间区别
  15. react-router的实现以及原理
  16. redux和vuex的设计**有什么异同?
  17. redux为什么要把reducer设计成纯函数?
  18. 了解过redux-saga和dva吗?

说说react的virtual dom及其diff的实现方式

当下react中的virtual dom主要使用Fiber架构实现,核心包括fiber数据结构以及调度器。

fiber数据结构可以说是一种升级版virtual dom,采用的是链表的方式,其中child属性指向子fiber对象,return指向父fiber对象,sibling指向兄弟fiber对象,并且真实DOM结构中每一个节点都对应一个fiber对象。

所谓调度器,主要处理两件事情,根据任务的优先级计算好过期时间以及采用requestIdleCallback方法实现调度。其中过期时间的计算是当前时间 + 优先级,实现的requestIdleCallback则是使用requestAnimationFramesetTimeout实现。原理就是,将整个更新任务拆分成一个个小的任务,并且可控制这些任务的执行,高优先级的会比低优先级的优先执行,而且优先级高的还可以中断优先级低的

至于diff的实现,原理上和Vue实现的一致,可观看这篇文章 【Vue 源码分析 】如何在更新 Patch 中进行 Diff


说一说react的生命周期

react 16版本之前,生命周期主要分为三个阶段,分别是挂载阶段、更新阶段、卸载阶段。

  1. 挂载阶段
    • constructor
    • componentWillMount
    • render
    • componentDidMount
  2. 更新阶段
    • componentWillReceiveProps
    • shouldComponentUpdate
    • componentWillUpdate
    • render
    • componentDidUpdate
  3. 卸载阶段
    • componentWillUnmount

react 16版本开始,已经抛弃了componentWillReceivePropscomponentWillMountcomponentWillUpdate,引入了getDerivedStateFromPropsgetSnapshotBeforeUpdatecomponentDidCatch

  1. 挂载阶段
    • constructor
    • static getDerivedStateFromProps
    • render
    • componentDidMount
  2. 更新阶段
    • static getDerivedStateFromProps
    • shouldComponentUpdate
    • render
    • getSnapshotBeforeUpdate
    • componentDidUpdate
  3. 卸载阶段
    • componentWillUnmount

getDerivedStateFromProps:静态方法,存在目的是让组件在props变化时更新state与组件本身无相关,无法直接访问组件上任何数据。触发机制是组件本身任何情况下的更新都会触发该回调函数(官方不推荐使用)。官方替代方案主要有两种:让组件变成完全受控组件和让组件变成不完全受控组件结合key属性(key值变化时,React会重新创建组件而非直接更新)。返回null则说明不需要更新state。

getSnapshotBeforeUpdate:在组件update之前发生,返回一个值,作为componentDidUpdate第三个参数。

若需要根据props更新后直接请求后段获取数据,改用componentDidUpdate,参数为prevPropsprevStatesnapshot,可直接根据prevProps与现在props判断该次更新是否为props更新导致。


react中组件有哪几种类型?那么创建组件又有哪几种方式?

react中组件类型有三种,分别是无状态组件、普通组件、PureComponent

创建组件方式有函数式定义无状态组件、ES5中使用React.createClass创建、ES6使用class extends React.Component创建。


在react和vue中,组件上写上key的作用是什么?

由于reactvue采用的都是diff策略,而key的作用则可以在新老节点对比时准确地判断该节点是否需要重新渲染,避免了不必要的渲染从而提升性能。常常应用于列表以及组件,一旦key值变更,将直接重新创建该元素或组件而不是直接更新。


聊聊setState机制

首先,setState的处理采取的是队列机制完成更新。当调用setState时,会将需要更新的state浅合并后放入状态队列,而不会立即更新state,队列机制会采取批量更新的形式对state进行更新。(当然为了处理这种情况,可以使用function作为参数模拟数据合并)

先看下setState入更新队列源码。

function enqueueUpdate(component) {
  // ...
  if (!batchingStrategy.isBatchingUpdates) {
    batchingStaretegy.batchedUpdates(enqueueUpdate, component)
    return
  }
  dirtyComponents.push(component)
}
// 先简单看看batchingStrategy对象
var batchingStrategy = {
  isBatchingUpdates: false,

  batchedUpdates: function(callback, a, b, c, d, e) {
    // ...
    batchingStrategy.isBatchingUpdates = true;
    
    transaction.perform(callback, null, a, b, c, d, e);
  }
}

简单滴讲,在生命周期调用setState前,其实就已经进入了enqueueUpdate方法并且设置isBatchingUpdatestrue。因此当直接调用setState时直接丢进了dirtyComponent中而不会立马更新处理。

总结下来就是,如果是由React引发的事件处理(比如通过onClick引发的事件处理或生命周期),调用setState不会同步更新this.state,除此之外的setState调用会同步执行this.state。所谓“除此之外”,指的是绕过React通过addEventListener直接添加的事件处理函数,还有通过setTimeout/setInterval产生的异步调用。

至于setState机制,简单总结便是,React会将任务全部放进一个队列中,根据isBatchingUpdates标记判断是否同步更新state状态,默认为false,表示setState会同步更新状态。实现上,只有React中引发的事件处理过程才会异步处理,其他处理都会同步处理

详细可以看看这里https://github.com/sisterAn/blog/issues/26。


谈谈对react批量更新(batchUpdates)理解?

直接参考一下从源码全面剖析 React 组件更新机制


react如何区别component和dom?

使用ReactDomrender方法可以确认。根据在render方法中传入的第一个参数来确定,当传入的是jsx时,就会使用React.createElement来创建相应的虚拟DOM,只有当传入的是函数式组件或class组件就会认为这就是一个组件。


说说react组件间的通信方式都有哪些

  1. 父向子间通信

    使用props传递数据。

  2. 子向父间通信

    使用props传递的callback

  3. 跨级间通信

    使用context通信。

  4. 无任何嵌套关系间通信

    使用EventEmitter自定义事件机制。

    使用Redux数据管理。


如何将一个子组件单独渲染出来和父组件平级关系?

  1. 使用unstable_renderSubtreeIntoContainer实现

    export default class A extends Component {
      componentDidMount() {
        this.parentElement = ReactDOM.findDOMNode(this).parentElement;
        this.renderChild();
      }
      componentDidUpdate() {
        this.renderChild();
      }
      renderChild() {
        const renderACom = (
          <div>
            {this.render()}
            {this.props.children}
          </div>
        )
        ReactDOM.unstable_renderSubtreeIntoContainer(
          this, renderACom, this.parentElement
        );
      }
      render() {
        return <div>a组件</div>;
      }
    }
  2. 使用createPortal实现

    export default class A extends Component {
      componentDidMount() {
        this.forceUpdate();
      }
      render() {
        const renderACom = (
          <div>
            <div>a组件</div>
            {this.props.children}
          </div>
        )
        if (!ReactDOM.findDOMNode(this)) {
          return <div></div>;
        }
        return ReactDOM.createPortal(
          renderACom, ReactDOM.findDOMNode(this).parentElement
        );
      }
    }

react组件都有哪些优化手段?

  1. 尽量多滴使用函数式组件或PureComponent
  2. 对一些不必要的重渲,需要使用shouldComponentUpdate生命周期进行过滤。
  3. 运用immutable。
  4. 对组件拆分要有一个把控,要考虑好可控组件以及不可控组件。

说说react的事件机制

react基于 Virtual DOM 实现了一个 SyntheticEvent(合成事件)层,组件中定义的事件处理器会接收到一个 SyntheticEvent 对象的实例,与原生的浏览器事件一样拥有同样的接口,同样支持事件的冒泡机制

它与原生事件区别主要在于驼峰规则、处理对象(合成事件处理的是函数,而原生事件处理的却是字符串)。

另外,合成事件的实现中采用的是事件代理机制。不会把处理函数直接绑定到真实的节点上,而是把所有事件绑定到结构的最外层,使用一个统一的事件监听器,该事件监听器维持了一个映射来保存所有组件内部的事件监听和处理函数。当事件发生时,首先被该统一的事件监听器处理,然后在映射里找到真正的事件处理函数并调用,当组件挂载或卸载时,只是在这个统一的事件监听器上插入或删除一些对象。


有了解过Fiber吗?

Fiber架构可以说是react在虚拟DOM上的一个重构。主要包含的内容就是fiber数据结构和调度器。处理的目标是针对动画、布局和手势。

通过将渲染任务进行拆分小任务,并根据各自的优先级计算好过期时间,采用增量渲染的方式,每次只执行一小段渲染任务,然后把任务放回给主线程,这样就可以很好滴避免由于长时间渲染而导致主线程被阻塞。

有兴趣的童鞋可以看看这位童鞋的分享,我觉得真的讲的很好。这可能是最通俗的 React Fiber(时间分片) 打开方式


谈谈高阶组件和基类

高阶组件是将组件作为参数,通过包裹的形式,为传入的组件增加功能并返回一个新的react组件。

基类是可为子类继承方法或属性的一个类。

react中推崇的是组合而非继承,因此常常推荐使用的是高阶组件。其中高阶组件常用地方主要体现在属性代理组件、反向继承组件以及组合多个高阶组件


谈谈React.Component、React.PureComponent和无状态组件间区别

React.Component作为一个常见组件写法,可拥有单独的状态state以及生命周期,但有一个缺陷就是,父组件更新即使传入的props数据不变,也会直接影响该组件重新渲染。

React.PureComponent就是在React.Component上的一个升级,使用生命周期shouldComponentUpdate对传入数据props进行浅层比较,若无变化就不会重新渲染该组件,其他行为和React.Component一致。

无状态组件只接受数据props的传入,不会存在任何组件内部状态state以及生命周期,只负责UI的渲染,因此更多推荐的是使用无状态组件。


react-router的实现以及原理

先说下实现,react-router根据环境的不同采取不同的实现,主要分为三种情况,分别是:

  1. 老版本浏览器的history实现。通过hash实现,对应createHashHistory方法。
  2. 高版本浏览器实现。通过h5里的history实现,对应createBrowserHistory方法。
  3. Node环境下实现。通过存储在memory里实现,对应createMemoryHistory方法。

接着说下hashhistory之间区别。

  • hash模式:hash会被包含在URL中,不会被包含在http请求中,对后端完全没影响,即改变hash不会重新加载页面。另外**hash模式原理是onhashchange事件**。
  • history模式:刚好相反,history模式会去直接请求接口获取页面。history模式则是通过popState事件进行监听

react-router基本原理是:URL对应Location对象,而UI则由React.Component决定,直接将Locationcomponent之间的同步问题


redux和vuex的设计**有什么异同?

共同点: 两者都是作为全局状态管理的工具库,**都是差不多:

  • 使用state存储状态。
  • 使用dispatch分发action触发数据的变更。
  • redux使用reducer统一管理数据变化的操作,而vuex中则是用mutation管理。
  • 使用getter形式获取状态。

区别:

  • 在处理异步情况下会有所不同,redux采用中间件形式,而vuex则使用在commit前直接处理异步。
  • 状态变更时,redux不会直接更改原数据,对于引用类型采取新建方式,而vuex则是直接修改状态属性来触发更新操作。

redux为什么要把reducer设计成纯函数?

首先针对的是引用类型,由于redux在源码的实现上对于引用类型只会进行浅比较,我们都知道深比较是很耗性能的。

所以若直接修改原数据返回一个同引用地址的对象(即只是更改它内容),在react会认为是一个没有人变化对象,就会导致无法触发重新渲染。


了解过redux-saga和dva吗?

redux-saga就是用于管理副作用如异步获取数据,访问浏览器缓存的一个中间件。其中reducer负责处理state更新,sagas负责协调异步操作。

dva是阿里体验技术开发的react应用框架,主要用于解决组件之间通信问题。在引入redux后,又由于redux没有异步操作,又需要引入redux-saga等插件,才有了dva的出现。

dva = React-Router + Redux + Redux-saga

dva的最简结构:

import dva from 'dva';
const App = () => <div>Hello dva</div>;

// 创建应用
const app = dva();
app.model(model)
// 注册视图
app.router(() => <App />);
// 启动应用
app.start('#root');

Javascript 中浮点数的精度问题

在日常开发中,常常会遇到前端需要计算业务,由于精度问题,后端一般都不会放心滴将计算都交给前端来算,特别是涉及到金钱方面的业务(少0.1元还好,少100元可就大亏呀...)。为此,前端同事一般都不敢背这锅,直接交给后端全盘接手业务。🤣

既然谈到精度问题,那究竟所谓的精度问题又是如何的呢?我们接着探究下去。

目录

  1. 精度问题
  2. 二进制和十进制间互转
  3. 如何解决精度问题?

精度问题

相信大家在刚学习 js 时,就有听说过千万别使用 js 来进行计算,因为 js 计算起来可是有误差?

那么为什么 js 在计算时就有误差呢?

答案就是今天的主题——浮点数的精度问题。

在讲解之前,我们先来看看一些常出现浮点数精度导致的计算出现误差。

// 加法 =====================
// 0.1 + 0.2 = 0.30000000000000004
// 0.7 + 0.1 = 0.7999999999999999
// 0.2 + 0.4 = 0.6000000000000001
// 2.22 + 0.1 = 2.3200000000000003
 
// 减法 =====================
// 1.5 - 1.2 = 0.30000000000000004
// 0.3 - 0.2 = 0.09999999999999998
 
// 乘法 =====================
// 19.9 * 100 = 1989.9999999999998
// 19.9 * 10 * 10 = 1990
// 1306377.64 * 100 = 130637763.99999999
// 1306377.64 * 10 * 10 = 130637763.99999999
// 0.7 * 180 = 125.99999999999999
// 9.7 * 100 = 969.9999999999999
// 39.7 * 100 = 3970.0000000000005
 
// 除法 =====================
// 0.3 / 0.1 = 2.9999999999999996
// 0.69 / 10 = 0.06899999999999999

上面的🌰,都是借用一下网上提供的一些常见由于浮点数精度问题导致计算有误差。

按照官方的解释,Javascript 遵循的是[IEEE 754 二进制浮点数算术标准](https://baike.baidu.com/item/IEEE 754/3869922?fr=aladdin)中64位双精度浮点数。

根据这个标准,Javascript 开发中定义数据一般都是十进制,因此在计算过程中,都会将所有十进制数值转换成二进制数值来进行计算

那么,又如何理解64位双精度浮点数呢?我们来看个结构图就清楚啦。

64位双精度浮点数

现在就来简单阐述一下上面结构图的提到的词汇。

  • 符号部分:占1位,使用 s 表示,其中0开头表示为正数,1开头表示为负数。
  • 指数部分:占11位,使用 e 表示,用于存储指数部分。
  • 尾数部分:占52位,使用 f 表示,真正用于存储有效数字部分(必须得敲黑板!)

符号部分用于决定一个数值的正负,指数部分则决定了一个数值的大小,尾数部分决定一个数值的精度。一个有效数字的描述在正常情况下,则是直接由符号位和尾数部分所决定,因此 Javascript 提供的有效数字为 53 个二进制位(即符号位+52位尾数)

正是由于有效数字为 53 个二进制位,所以数值也被限制在 -(2^53 - 1) ~ 2^53 - 1 之间(即Number.MAX_SAFE_INTEGER === 9007199254740991 到 Number.MIN_SAFE_INTEGER === -9007199254740991之间)。

一旦超出这个数值范围会怎样?答案就是截取。

二进制超出范围时截取方式也是直接导致了 Javascript 中计算的精度问题

让我们来看个🌰,你就知道了。

console.log(90071992547409910) // 90071992547409900

由上面的栗子就可以清楚看到,由于数值 90071992547409910 直接超出了最大范围 9007199254740991 ,最终将会进行截取,而截取后输出的结果就为 90071992547409900。

二进制和十进制间互转

大概了解 Javascript 精度问题的引起原因后,现在就来简单回顾一下,数值是如何转化为二进制的。毕竟这都是大学里学过的知识,现在还得来简单列一下,避免看到二进制就懵逼哈哈。😄(当然你可以直接跳过哈...)

二进制转化为十进制,采用的是按权相加方式。

1100 = 1 * 2^0 + 1 * 2^1 + 0 * 2^2 + 0 * 2^3 = 3
// 二进制1100相当于十进制3

十进制整数部分转化为二进制,采用的是除2取余,逆序排列方式。借用一个图

十进制整数部分转化为二进制

十进制小数部分转化为二进制,采用的是乘2取整,顺序排列方式。继续借用一个图

十进制小数部分转化为二进制

那么,现在就拿0.1 + 0.2举个例子。先看看0.1和0.2转化为二进制后的数值。

0.1 -> 0.0001100110011001...(无限)
0.2 -> 0.0011001100110011...(无限)

可以看到,0.1和0.2转化为二进制后,后面都是循环无限的,就算它们进行相加后也还是循环无限的。那么问题来了,有效数值范围是53位,一旦超过就会被截取,这也直接导致截取到的结果不是我们想要的。

0.1 + 0.2 === 0.30000000000000004

如何解决精度问题?

既然 Javascript 由于遵循 IEEE 754 二进制浮点运算的标准,那么前端就真的无法解决所谓的精度问题了吗?答案却是否定的。我们就来看看究竟有哪些方法可以解决?

  1. 引用第三方库。

    一般情况下,都是尽量不需要前端计算,为此复杂的计算问题都交给服务器进行计算。同样地,我们将计算问题都可以交给第三方库处理,是一种比较方便的处理方案

    • Math.js

      Math.js 是专门为 JavaScript 和 Node.js 提供的一个广泛的数学库,提供集成解决方案来处理不同的数据类型。

    • decimal.js

      为 JavaScript 提供十进制类型的任意精度数值。

    • big.js

      一个小型,快速的JavaScript库,用于任意精度的十进制算术运算。

  2. 使用 toFixed 方法。

    相信大部分童鞋都使用过 toFixed 方法处理精度问题,toFixed 方法接受一个参数,用于保留小数点后几位小数,得到的数字会以四舍五入的方式进行转化。

    console.log((0.1 + 0.2).toFixed(1)) // 0.3

    但是 toFixed 方法会存在某些小问题,在 IE 上测试是正常的,一旦到 chrome,可能就会有一些小毛病出来了。看下面这个🌰:

    console.log((1.25).toFixed(1)) // 1.3 正确
    console.log((1.225).toFixed(2)) // 1.23 正确
    console.log((1.2225).toFixed(3)) // 1.222 错误
    console.log((1.22225).toFixed(4)) // 1.2223 正确
    console.log((1.222225).toFixed(5))  // 1.22222 错误
    console.log((1.2222225).toFixed(6)) // 1.222222 错误

    可以看到,toFixed 方法虽好,但在处理某些浮点数时,不同浏览器也会得到不一样的结果(即精度丢失)

    为此,网上有些大神们也是推荐直接重写数值的 toFixed 方法。

    Number.prototype.toFixed = function(len) {
      if(len > 20 || len < 0) {
        throw new Error('toFixed method must length in 0 ~ 20')
      }
      const num = Number(this) // 将.123转化为0.123
      if(isNaN(num) || num >= Math.pow(10, 21)) { // 判断是否为NaN,以及不能超过1e+21
        return num.toString()
      }
      if(len === undefined || len === 0) { // 判断参数是否不合法
        return Math.round(num).toString()
      }
      const result = num.toString() // 转化数值为字符串
      const numArr = result.split('.') // 将字符串根据.号转换为数组
      const intNum = numArr[0] // 获取整数部分
      const deciNum = numArr[1] // 获取小数部分
      if(numArr.length < 2) {
        return `${intNum}.`.padEnd(len)
      }
      if(deciNum.length <= len) {
      	return `${intNum}.${deciNum}`.padEnd(len - deciNum.length)
      } else {
        const newDeciNum = deciNum.slice(0, len - 1)
        return `${intNum}.${newDeciNum}`
      }
    }

    重写的 toFixed ,就是将数值进行字符串的一系列操作得到需要的结果。

  3. 将浮点数转化为字符串取整处理。

    相信很多童鞋都会使用过,将浮点数取整后,再进行运算,最后再除以相应的倍数得到最终结果。可别说,这种算法的效果也是杠杠的。🤔

    console.log((0.1 * 10 + 0.2 * 10) / 10) // 0.3

    既然这算法这么好用,那么为啥不推广呢?细究下去,会发现依然存在问题。

    console.log((35.41 * 100 + 0.1 * 10) / 100) // 35.419999999999995

    可以看到,问题依然还是会存在,那么究竟如何处理是好?答案就是字符串处理

    简单来说,就是将浮点数值使用字符串处理方式将小数点去掉,变成一个整数形式,再进行运算,运算得到的结果再除以相应的倍数即可。这样就可以有效避免以上的坑了。

    var floatObj = (function() {
      var isIntegar = obj => Math.floor(obj) === obj // 判断数值是否为整数
      var toIntegar = floatNum => { // 将数值转化为字符串处理
        var ret = { rate: 1, num: 0 }
        if(isIntegar(floatNum)) {
          ret.num = floatNum
          return ret
        }
        var floatStr = floatNum.toString()
        var deciNum = floatStr.split('.')[1]
        var integarNum = Number(floatStr.replace('.', ''))
    		ret.num = integarNum
        ret.rate = Math.pow(10, deciNum.length)
        return ret
      }
      var add = (a, b) => { // 两个浮点数相加
        var floatA = toIntegar(a)
        var floatB = toIntegar(b)
        var { rate: rateA, num: numA } = floatA
        var { rate: rateB, num: numB } = floatB
        var resultRate = rateA > rateB ? rateA : rateB
        var resultNum
        if(rateA === rateB) {
          resultNum = numA + numB
        } else if (rateA > rateB) {
          resultNum = numA + numB * (rateA / rateB)
        } else {
          resultNum = numA * (rateB / rateA) + numB
        }
        return resultNum / resultRate
      }
      var subtract = (a, b) => { // 两个浮点数相减
        var floatA = toIntegar(a)
        var floatB = toIntegar(b)
        var { rate: rateA, num: numA } = floatA
        var { rate: rateB, num: numB } = floatB
        var resultRate = rateA > rateB ? rateA : rateB
        var resultNum
        if(rateA === rateB) {
          resultNum = numA - numB
        } else if (rateA > rateB) {
          resultNum = numA - numB * (rateA / rateB)
        } else {
          resultNum = numA * (rateB / rateA) - numB
        }
        return resultNum / resultRate
      }
      var multiply = (a, b) => { // 两个浮点数相乘
        var floatA = toIntegar(a)
        var floatB = toIntegar(b)
        var { rate: rateA, num: numA } = floatA
        var { rate: rateB, num: numB } = floatB
        var resultNum = (numA * numB) / (rateA * rateB)
        return resultNum
      }
      var divide = (a, b) => { // 两个浮点数相除
        var floatA = toIntegar(a)
        var floatB = toIntegar(b)
        var { rate: rateA, num: numA } = floatA
        var { rate: rateB, num: numB } = floatB
        var resultNum = (numA / numB) * (rateB / rateA)
        return resultNum
      }
      return { add, subtract, multiply, divide }
    })()

常见数组算法

对数组数据结构常见的算法进行总结,也为了更好滴应对后面深入学习算法。

把数组排成最小的树

输入一个正整数数组,把数组里所有的数字拼起来排成一个数,然后输出拼接后的所有数字里面最小的一个。

答案

  const getMinNum = nums => {
    if (!nums || nums.length === 0) return '';
    return nums.sort(compare).join('');
  }
  const compare = (a, b) => {
    const front = `${a}${b}`;
    const behind = `${b}${a}`;
    return front - behind;
  }
  

第一个只出现一次的字符

在一个字符串中找到第一个只出现一次的字符,并返回它的位置,如果没有就直接返回 -1。

答案

  // 时间复杂度:O(n),  空间复杂度:O(n)
  const getOneStr1 = str => {
    if (!str) return -1;
    const map = {};
    for (let i = 0; i < str.length; i++) {
      if (map[str[i]]) map[str[i]] = map[str[i]] + 1;
      else map[str[i]] = 1;
    }
    for (let i = 0; i < str.lenth; i++) {
      if (map[str[i]] === 1) return i;
    }
    return -1;
  }

// 时间复杂度:O(n^2),空间复杂度:O(0)
const getOneStr2 = str => {
if (!str) return -1;
for (let i = 0; i < str.length; i++) {
if (str.indexOf(str[i]) === str.lastIndexOf(str[i])) return i;
}
return -1;
}

调整数组顺序使奇数位于偶数前面

输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有奇数都在偶数前面。

答案

  const changeArr = arr => {
    let start = 0;
    let end = arr.length - 1;
    while (start < end) {
      while (arr[start] % 2 === 1) start++;
      while (arr[end] % 2 === 0) end--;
      if (start < end) {
        [arr[start], arr[end]] = [arr[end], arr[start]];
      }
    }
    return arr;
  }
  

构建乘积数组

给定一个数组 A [0, 1, ..., n - 1],请构建一个数组 B [0, 1, ..., n - 1],其中 B 中的元素,B[i] = A[0] * A[1] * ... * A[i - 1] * A[i + 1] * ... * A[n - 1]。

答案

  const createChenArr = arr => {
    if (Array.isArray(arr) && arr.length) {
      const leftArr = new Array(arr.length).fill(0);
      const rightArr = new Array(arr.length).fill(0);
      for (let i = 0; i < arr.length; i++) leftArr[i] = i === 0 ? 1 : leftArr[i - 1] * arr[i - 1];
      for (let i = arr.length - 1; i >= 0; i--) rightArr[i] = i === arr.length - 1 ? 1 : rightArr[i + 1] * arr[i + 1];
      const result = [];
      for (let i = 0; i < arr.length; i++) {
        if (i === 0) {
          result[i] = rightArr[0];
        } else if (i === arr.length - 1) {
          result[i] = leftArr[i];
        } else {
          result[i] = leftArr[i - 1] * rightArr[i + 1];
        }
      }
      return result;
    }
    return arr;
  }
  

和为S的连续正整数序列

输入一个正数 S,打印出所有和为 S 的连续正数序列。

例如:输入 15,有序序列有 [1, 2, 3, 4, 5]、[4, 5, 6]、[7, 8]。

答案

  const getContinueArr = target => {
    const result = [];
    const temp = [1, 2];
    let start = 1;
    let end = 2;
    let sum = 3;
    while (end < target) {
      while (sum < target && end < target) {
        temp.push(++end);
        sum += end;
      }
      while (sum > target && start < end) {
        temp.shift();
        sum -= start++;
      }
      if (sum === target && temp.length) {
        result.push([...temp]);
        temp.push(++end);
        sum += end;
      }
    }
    return result;
  }
  

和为S的两个数字

输入一个递增排序的数组和一个数字 S,在数组中查找两个数,使得它们的和正好是 S,如果有多对数字的和等于 S,输出两个数的乘积最小的。

答案

  const getMinSum = (arr, target) => {
    if (Array.isArray(arr) && arr.length) {
      let start = 0;
      let end = arr.length - 1;
      while (start < end) {
        const sum = arr[start] + arr[end];
        if (sum === target) {
          return [arr[start], arr[end]];
        } else if (sum > target) {
          end--;
        } else {
          start++;
        }
      }
    }
    return [];
  }
  

连续子数组的最大和

输入一个整形数组,数组里面有正数也有负数。数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值,要求时间复杂度为 O(n)。

例如,{6, -3, -2, 7, -15, 1, 2, 2},连续子数组的最大和为8(从第0个开始,到第3个为止)。

答案

  var maxSubArray = function(nums) {
    if (!nums.length) return 0
    let sum = nums[0]
    let result = nums[0]
    for (let i = 1; i < nums.length; i++) {
      if (sum < 0) {
        sum = nums[i]
      } else {
        sum += nums[i]
      }
      result = Math.max(sum, result)
    }
    return result
  };
  

两数之和

输入一个整数数组 nums,和一个目标值 target,请在该数组中找出和为目标值的那两个整数,并返回他们的数组下标。

答案

  const getTwoSum = (nums, target) => {
    const map = {};
    if (Array.isArray(nums)) {
      for (let i = 0; i < nums.length; i++) {
        const diff = target - nums[i];
        if (map[diff]) return [map[nums[i]], map[diff]];
        map[nums[i]] = i;
      }
      return [];
    }
  }
  

扑克牌顺子

扑克牌中随机抽 5 张牌,判断是不是一个顺子,即这 5 张牌是不是连续的。

其中 2 - 10为数字本身,A 为 1,J 为 11 ... 大小王可以看成任何数字,可以把它当作 0 处理。

答案

  const getThreeArr = nums => {
    const result = [];
    if (Array.isArray(nums) && nums.length) {
      const numsArr = nums.sort((a, b) => a - b);
      for (let i = 0; i < numsArr.length; i++) {
        if (i && numsArr[i] === nums[i - 1]) continue;
        let left = i + 1;
        let right = numsArr.length - 1;
        while (left < right) {
          const sum = numsArr[i] + numsArr[left] + numsArr[right];
          if (sum === 0) {
            result.push([numsArr[i], numsArr[left], numsArr[right]]);
          }
          if (sum <= 0) {
            while (numsArr[left] === nums[left + 1]) left++; 
          } else {
            while (numsArr[right] === nums[right - 1]) right--; 
          }
        }
      }
    }
    return result;
  }
  

三数之和

给定一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素a,b,c,使得 a + b + c = 0?找出所有满足条件且不重复的三元组。

答案

  const isSmooth = arr => {
    if (Array.isArray(arr) && arr.length) {
      let max = 0;
      let min = 14;
      for (let i = 0; i < arr.length; i++) {
        if (arr[i] === 0) continue;
        max = Math.max(max, arr[i]);
        min = Math.min(min, arr[i]);
    }
    return max > min ? (max - min < 5) : false;
  }
  

数组中的逆序对

在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。

输入一个数组,求出这个数组中的逆序对的总数 P。

答案

  let count = 0;
  const mergeSort = nums => {
    if (!nums || !nums.length) return nums;
    const mid = parseInt((nums.length - 1) / 2);
    const leftArr = nums.slice(0, mid + 1);
    const rightArr = nums.slice(mid + 1, nums.length);
    return merge(mergeSort(leftArr), mergeSort(rightArr)); 
  }
  const merge = (leftArr, rightArr) => {
    let left = 0;
    let right = 0;
    const result = [];
    while (left < leftArr.length && right < rightArr.length) {
      if (leftArr[left] > rightArr[right]) {
        count += rightArr.length - right;
        result.push(rightArr[right++]);
      } else {
        result.push(leftArr[left++]);
      }
    }
    while (left < leftArr.length) result.push(leftArr[left++]);
    while (right < rightArr.length) result.push(rightArr[right++]);
    return result;
  }
  

顺时针打印矩阵

输入一个矩阵,按照从外向里顺时针的顺序依次打印出每一个数字。

例子:输入如下 4 x 4 矩阵

1 2 3 4

5 6 7 8

9 10 11 12

打印出数字为:1,2,3,4,5,6,7,8,9,10,11,12。

答案

  const logNumber = nums => {
    if (!nums || !nums.length) return nums;
    const row = nums.length;
    const col = nums[0].length;
    const result = [];
    let left = 0;
    let right = col - 1;
    let top = 0;
    let bottom = row - 1;
    let index = 1;
    while (index < row * col) {
      for (let i = left; i <= right; i++) {
        result.push(nums[top][i]);
        index++;
      }
      top++;
      for (let i = top; i <= bottom; i++) {
        result.push(nums[i][right]);
        index++;
      }
      right--;
      for (let i = right; i >= left; i--) {
        result.push(nums[bottom][i]);
        index++;
      }
      bottom--;
      for (let i = bottom; i >= top; i--) {
        result.push(nums[i][left]);
        index++;
      }
      left++;
    }
    return result;
  }
  

四数之和

给定一个包含 n 个整数的数组 nums,判断 nums 中是否存在四个元素 a,b,c,d,使得 a + b + c + d = 0。找出所有满足条件且不重复的四元组。

答案

  const getFourSum = (nums, target) => {
    if (!nums || nums.length < 4) return [];
    const result = [];
    nums.sort((a, b) => a - b);
    for (let i = 0; i < nums.length - 3; i++) {
      if (i && nums[i] === nums[i - 1]) continue;
      if (nums[i] + nums[i + 1] + nums[i + 2] + nums[i + 3] > target) break;
      for (let j = i + 1; j < nums.length - 2; j++) {
        if (j > i + 1 && nums[j] === nums[j - 1]) continue;
        let left = j + 1;
        let right = nums.length - 1;
        while (left < right) {
          const sum = nums[i] + nums[j] + nums[left] + nums[right];
          if (sum === target) {
            result.push([nums[i], nums[j], nums[left], nums[right]]);
          }
          if (sum <= target) {
            while (nums[left] === nums[++left]); 
          } else {
            while (nums[right] === nums[--right]);
          }
        }
      }
    }
    return result;
  }
  

在排序数组中查找数字

统计一个数字在排序数组中出现的次数。

答案

  const findAmount = (nums, target) => {
    if (!nums || !nums.length) return -1;
    let start = 0;
    let end = nums.length - 1;
    let left = -1;
    let right = -1;
    while (start < end) {
      const mid = parseInt((start + end) / 2);
      if (nums[mid] === target) {
        left = mid;
        end = mid - 1;
      } else if (nums[mid] > target) {
        end = mid - 1;
      } else {
        start = mid + 1;
      }
    }
    start = 0;
    end = nums.length - 1;
    while (start < end) {
      const mid = parseInt((start + end) / 2);
      if (nums[mid] === target) {
        right = mid;
        left = mid + 1;
      } else if (nums[mid] > target) {
        end = mid - 1;
      } else {
        start = mid + 1;
      }
    }
    return (left !== -1 && right !== -1 && left < right) ? right - left + 1 : -1;
  }
  

Perfomance 性能监控

在前端开发里,常常需要对某个页面进行相应的性能监控,接着拿着数据来跟运维部门进行协调分析,看看到底是哪部分导致了整个页面的速度。

那么监控需要用的就是今天的主角,Performance API,一个专门为了性能而诞生的 API,兼容性必须得 IE9+(其实兼容性已经足够好了...)。

接下来我们就来深入理解 Performance。

如何使用 Performance API

只需要在项目的入口文件中使用以下代码即可。

var performance = window.performance || window.msPerformance || window.webkitPerformance
if(performance) {
  // 访问performance
}

那么到底 Performance API 中带地都有哪些东西?在这里我就直接列出来。

  • memory。与内存相关。
  • navigation。与页面来源相关。
  • timing:与页面性能时间相关。

需要注意的是,在某些情况下,例如页面还没初始化完成时,这时候直接访问performance.timing是为 null 的。为了处理这种情况,我们可以使用一个定时器来解决。

var performance = window.performance || window.msPerformance || window.webkitPerformance
if(performance) {
  let performanceTiming = performance.timing
  let performanceInterval = setInterval(() => {
    if(performanceTiming) {
      clearInterval(performanceInterval)
      // ...
    }
  }, 100)
}

接下来我们就拿 memory、navigation 和 timing 来分析一下。

performance.memory 处理内存

在 performance.memory 中主要包含有三个值,分别是。

  • usedJSHeapSize。
  • totalJSHeapSize。
  • jsHeapSizeLimit。

usedJSHeapSize 主要用于表示 Javascript 对象(包括V8引擎内部对象)所占用的内存数。

totalJSHeapSize 主要用于表示整个项目中可使用的内存数。

jsHeapSizeLimit 主要用于表示内存大小的限制。

需要注意的是,当 usedJSHeapSize > totalJSHeapSize 时,会导致内存泄漏

performance.navigation 处理页面来源

在 performance.navigation 中只有两个值,分别是:

  • redirectCount。
  • type。

redirectCount 用于表示在同源页面情况下,若有重定向的话,页面经历多少次重定向到达当前页面,默认为 0。

type 表示到达当前页面的方式,其中:

  • 0 表示直接进入当前页面,并不是重定向或者刷新页面。
  • 1 表示通过调用 window.location.reload() 方法刷新到达当前页面。
  • 2 表示通过浏览器原生的前进后退按钮到达当前页面。
  • 255 表示非以上方式到达当前页面。

需要注意的是,redirectCount 的值必须是在同源页面情况下的重定向到达当前页面才会有值

performance.timing 处理页面性能时间

要说 performance 中最主要的 API 莫过于 timing 了,主要的功能就是记录页面运行的每一步时间,可用于统计以及和运维部门之间协调的主要工具。

那么 performance.timing 长什么样呢?先看看下面这张图。

performance api

相信很多童鞋都会看过这个图,那么图上的每一个时间节点都是表示什么?别急,接下来我们就来一一解答。

timing属性名 含义
navigationStart 上一个页面卸载时(即还没调用 unload 事件)的时间戳,必须是同源页面,否则值就和 fetchStart 相等,或者没有上一个页面时,值也和 fetchStart 相等
redirectStart 第一个HTTP重定向的开始时间戳
redirectEnd 最后一个HTTP重定向的结束时间戳
fetchStart 浏览器已经准备好使用 HTTP 请求来获取文档的时间戳,会出现在检查缓存之前
domainLookupStart DNS查询的开始时间戳
domainLooupEnd DNS查询的结束时间戳
connectStart TCP开始建立连接的时间戳
secureConnectStart HTTPS开始建立握手连接的时间戳
requestStart 浏览器正式发送HTTP请求获取文档的时间戳
responseStart 浏览器收到服务端的第一个字节的时间戳
unloadEventStart 上一个页面卸载后调用 unload 事件的开始时间戳,若没有上一个页面,值会为 0
unloadEventEnd 上一个页面卸载后调用 unload 事件的结束时间戳,若没有上一个页面,值会为 0
responseEnd 浏览器收到服务端的最后一个字节的时间戳
domLoading 当前页面DOM结构开始解析
domInteractive 当前页面DOM结构结束解析,开始加载内嵌资源时
domContentLoadedEventStart 所有需要被执行的脚本已经开始解析的时间戳
domContentLoadedEventEnd 所有需要被执行的脚本已经被执行的时间戳
domCompleted 当前文档解析完成的时间戳
loadEventStart load事件触发的时间戳
loadEventEnd load事件已经执行完的时间戳

那么接下来就看看如何计算一些常见的性能监测点。

  • 重定向耗时:redirectEnd - redirectStart
  • DNS查询耗时:domainLookupEnd - domainLookupStart
  • TCP连接耗时:connectEnd - connectStart
  • HTTP请求耗时:responseStart - requestStart
  • 解析dom树耗时:domCompleted - domInteractive
  • 白屏时间:responseStart - navigationStart
  • dom ready时间:domContentLoadedEventEnd - navigationStart
  • onload 时间:loadEventEnd - navigationStart

需要注意的是,当到达 domInteractive 时间点时,用户是可以与网站进行交互的

最后的最后,可参考一下别人写的好文章。

深入理解前端性能监控

回流(重排)与重绘

可参考别人的好文章,后面我就做一个简单的总结而已。
浏览器的回流与重绘

定义

回流,也叫重排,当渲染树中元素的尺寸、结构、或某些位置发生改变时,浏览器需要对全部文档或部分文档进行重新渲染的过程。

重绘,当渲染树中元素某些属性的改变并不影响其位置或尺寸,又或者影响文档中其他元素时(如background、color等属性),那么浏览器不会进行重新渲染,而是简简单单地对其进行重新绘制的过程。

因此,回流一定会引起重绘,而重绘则不一定会引起回流

浏览器如何运作回流和重绘

回流耗损的性能比重绘要大,由于在回流的过程中,需要重新对自身元素或其他元素的大小或位置进行重新计算,而重绘只会对自身元素进行简单的重新绘制而不会对其他元素进行计算。

那么,在回流或重绘过程中,浏览器的运作会是怎样的呢?

浏览器对于文档中频繁的回流或重绘过程,会将它们依次地放进一个队列中进行维护,当放进的回流或重绘过程的数量达到了队列的最大值时,便会一次性按照队列的先进先出的原则,对队列中所有的过程都进行批量处理,极大地提升操作效率。

常见的避免回流处理

对于 CSS,常见的处理如下:

  1. 避免使用 table 布局(由于table布局内部相对复杂,在table布局中回流时的计算会比其他元素的计算多大概3倍时间,因此不推荐使用)

  2. 避免在属性中使用表达式,如css3中的calc()、scale()等等

  3. 对于使用计算的元素尽量应用在BFC中

对于 Javascript,常见的处理如下:

  1. 避免频繁操作样式。对于频繁操作样式的处理,应一次性重写以及一次性更改。

  2. 避免频繁操作DOM。对于需要多次操作DOM时,可使用H5中documentFragment,在其上面应用所有的DOM操作,最后一次性地添加到文档中。

  3. 避免频繁读取引起回流或重绘的属性(如clientWidth、offsetWidth、width等等)。对于需要多次读取回流或重绘的属性时,应该对其进行缓存处理。

谈谈 Vue 和 React 间的区别

相信各位对于 Vue 和 React 都不陌生,尤其是在日常开发过程中基本没有离开过它们。那么要是有人询问你,用了这两个框架这么久,你觉得它们之间有什么不一样的地方呢?

甭急,下面就根据我自己的思考,来谈谈它们之间的区别到底有哪些。

数据是否可变性

在 Vue 中,推崇的是数据响应式,通过封装修改响应式属性自动通知 Watcher 依赖更新视图的过程,让开发者注重处理逻辑。

而在 React 中, 推崇的则是数据的不可变性,由于 React 追随的是函数式编程,因此会结合 Immutable 来实现数据的不可变。

处理组件的**不一致

在 Vue 中,实现一个组件需要结合 Template、CSS、Javascript三部分内容,分别对应于结构层、表示层、行为层。

而在 React 中,由于追随的是函数式编程,在处理组件时一律采取 JavaScript 实现,一句话总结便是 All In JS。

处理扩展的方式不一致

在 Vue 中,如果需要对一个组件进行延伸或拓展,需要使用 Mixin 实现。

而在 React 中,则采取的是高阶组件实现,**是组合逻辑。

处理内置功能不一致

在 Vue 中,基本很多功能都会内置,如自带指令系统、双向数据绑定功能等。

而在 React 中,基本都是基于原生 JS 实现,因此很多功能或插件都会通过社区来提供。

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.