面试必备-JS原理(下)-JS中的this、防抖和节流、继承、fetch、Generator、设计模式

40 阅读31分钟

1. JS中的this

1.1 如何确认this的值

1.1.1 开启严格模式

在非严格模式下,总是指向一个对象,在严格模式下可以是任意值,开启严格模式可以使用如下两种方式:

  1. 在整个脚本顶部开启
  2. 在函数顶部开启
// 为整个脚本开启严格模式
'use strict'
function func() {
  // 为函数开启严格模式
  'use strict'
}

1.1.2 面试回答

如何确认this指向:

  1. 全局执行环境中,指向全局对象(浏览器环境指的是Window)(非严格模式、严格模式)

  2. 如何开启严格模式:

    // 为整个脚本开启严格模式
    'use strict'
    function func() {
      // 为函数开启严格模式
      'use strict'
    }
    
  3. 函数内部,取决于函数被调用的方式

    1. 直接调用的this值:
      1. 非严格模式:全局对象(Window)
      2. 严格模式:undefined
    2. 对象方法调用时的this值为调用者(非严格模式、严格模式)
// 1.全局执行环境
// 非严格模式: 不做任何设置,直接写就是非严格模式
// console.log(this) // Window
// 严格模式: 代码顶部加上 'use strict' 即可
// 'use strict' // 为整个脚本开启严格模式
// console.log(this) // Window

// 2.函数内部
//  2.1 直接调用-非严格模式
// function func() {
//   console.log(this) // 全局对象Window
// }
// func()

//  2.1 直接调用-严格模式
// function func() {
//   'use strict'
//   console.log(this) // undefined
// }
// func()

//  2.2 对象方法调用
// const food = {
//   name: '猪脚饭',
//   eat() {
//     'use strict'
//     console.log('吧唧吧唧')
//     console.log(this) // food对象
//   }
// }
// food.eat() // 严格模式和非严格模式下 调用者food对象

1.2 如何改变this指向/如何指定this的值

1.2.1 面试回答

如何改变this指向,有2类改变this指向的方法,分别是:

  1. 调用函数时并传入具体的this
    1. call:从第二个参数开始挨个传递参数
    2. apply:在第二个参数以数组的形式传递参数(apply和arr
  2. 创建函数时绑定this
    1. bind:返回一个绑定了this以及参数(可选)的新函数
    2. 箭头函数:创建时会绑定上一级作用域中的this

1.2.2 具体说明

主要有2类改变函数内部this指向的方法:

  1. 调用函数并传入具体的this
    1. call-文档链接: func.call(thisArg,参数1,参数2...)
      1. 参数1:this
      2. 参数2-n:传递给函数的参数
    2. apply-文档链接:数组作为参数 func.apply(thisArg,[参数1,参数2...])
      1. 参数1:this
      2. 参数2:以数组的形式,传递给函数的参数
  2. 创建绑定this的函数:
    1. bind-文档链接:const bindFunc = func.bind(thisArg, 绑定参数1, 绑定参数2...) 返回一个绑定了this的新函数
    2. 箭头函数-文档链接:最近的this是谁,就是谁(内部的this不是由自己决定的,而是找上一级作用域的this

调用函数并传入具体的this:

  1. 函数的call方法
function funcA(p1, p2) {
  console.log('funcA-调用')
  console.log(this)
  console.log('p1:', p1)
  console.log('p2:', p2)
}
const obj = {
  name: 'jack'
}
// 以指定的this调用函数,并通过 从第二个参数开始依次传递参数
// call参数 
// 参数1 this值
// 参数2-参数n 挨个传入函数的参数
funcA.call(obj, 1, 2)

// 以指定的this调用函数,并通过 数组的形式 传递参数
// apply参数
// 参数1 this值
// 参数2 以数组的形式传入函数的参数
funcA.apply(obj, [3, 4])

创建绑定this的函数:

function funcB(p1, p2) {
  console.log('funcB-调用')
  console.log(this) // son对象
  console.log('p1:', p1) // p1:123
  console.log('p2:', p2) // p2:666
}
const person = {
  name: 'itheima'
}
// bind参数
// 参数1 this值
// 参数2-参数n 绑定的参数
// 返回一个绑定了`this`的新函数
const bindFuncB = funcB.bind(person, 123)
bindFuncB(666) 

const food = {
  name: 'zs',
  eat() {
    console.log(this) // food对象
    setTimeout(() => {
      console.log(this) // food对象
    }, 1000)
//    setTimeout(function () {
//       console.log(this) // Window对象
//    }, 1000)
  }
}
food.eat()

const student = {
  name: 'lilei',
  sayHi: function () {
    console.log(this) // student 对象
    // 箭头会从自己作用域链的上一层继承this
    const inner = () => {
      console.log('inner-调用了')
      console.log(this) // student 对象
    }
    inner()
  }
}
student.sayHi()

1.3 手写callapplybind

1.3.0 剩余参数-文档链接

function func(...args){
  console.log(args)// 以数组的形式获取传入的所有参数
}
func('西蓝花','西葫芦','西洋参','西芹')

1.3.1 手写call方法

const person = {
  name: 'itheima'
}
function func(numA, numB) {
  console.log(this)
  console.log(numA, numB)
  return numA + numB
}
// 参数1:指定的this值
// 参数2-参数n:原函数参数
const res = func.myCall(person, 2, 8)
console.log('返回值为:', res)

核心步骤有4步

  1. 如何定义myCall?
  2. 如何让函数内部的this为某个对象?
  3. 如何让myCall接收参数2-参数n?
  4. 使用 Symbol 调优myCall
// 1. 如何定义`myCall`
Function.prototype.myCall = function () {
  // 逻辑略
}
// 2 设置this并调用原函数
Function.prototype.myCall = function (thisArg) {
  // this 是调用myCall的 函数
  // thisArg 指定的this
  // 为他添加一个自定义属性,让函数成为他的该属性
  thisArg['fn'] = this
  // 调用并获取结果
  const res = thisArg['fn']()
  // 移除添加的自定义属性
  delete thisArg['fn']
}
// 3 接收剩余参数并返回结果
Function.prototype.myCall = function (thisArg, ...args) {
  thisArg['fn'] = this
  // 调用并获取结果
  const res = thisArg['fn'](...args)
  // 移除添加的自定义属性
  delete thisArg['fn']
  // 返回调用结果
  return res
}
// 4 使用`Symbol`调优`myCall`
Function.prototype.myCall = function (thisArg, ...args) {
  // 使用Symbol生成唯一标记,避免和原属性冲突
  const fn = Symbol()
  thisArg[fn] = this
  const res = thisArg[fn](...args)
  // 移除添加的自定义属性
  delete thisArg[fn]
  // 返回调用结果
  return res
}

// --------测试代码--------
const person = {
  name: 'itheima'
}
function func(numA, numB) {
  console.log(this)
  console.log(numA, numB)
  return numA + numB
}
// 参数1:指定的this值
// 参数2-参数n:原函数参数
const res = func.myCall(person, 2, 8)
console.log('返回值为:', res)
面试回答

手写call方法的步骤为:

  1. function的原型上添加myCall方法,保证所有函数都可以调用
  2. 方法内部,通过动态为对象添加方法的形式来指定this指向
  3. 调用完毕之后通过delete关键字删除上一步动态增加的方法
  4. 方法的名字通过 Symbol 进行设置,避免和默认名重复
  5. 使用剩余参数的形式传递参数2-参数n(函数参数)
Function.prototype.myCall = function (thisArg, ...args) {
  const fn = Symbol()
  thisArg[fn] = this
  const res = thisArg[fn](...args)
  delete thisArg[fn]
  return res
}

1.3.2 手写apply方法

const person = {
  name: 'itheima’
}
function func(numA, numB) {
  console.log(this)
  console.log(numA, numB)
  return numA + numB
}
const res = func.myApply(person, [2, 8])
console.log('返回值为:', res)

核心步骤:

  1. 如何定义myApply?
    1. 定义在原型上
  2. 如何让函数内部的this为某个对象?
    1. 动态给对象添加方法,通过对象.方法()调用即可
    2. 使用Symbol来生成方法名
  3. 如何让myApply接收参数?
    1. 定义参数2即可
    2. 传递给原函数时需要使用...展开
// 1. 如何定义`myApply`
Function.prototype.myApply = function () {
  // 逻辑略
}
// 2 如何让函数内部的`this`为某个对象
Function.prototype.myApply = function (thisArg) {
  // 为他添加一个自定义属性,让函数成为他的该属性
  // 使用Symbol生成唯一标记,避免和原属性冲突
  const fn = Symbol()
  thisArg[fn] = this
  const res = thisArg[fn]()
  // 移除添加的自定义属性
  delete thisArg[fn]
  // 返回调用结果
  return res
}
// 3 如何让`myApply`接收参数
Function.prototype.myApply = function (thisArg, args) {
  const fn = Symbol()
  thisArg[fn] = this
  // 调用并获取结果
  // 用... 将args展开传入
  const res = thisArg[fn](...args)
  delete thisArg['fn']
  // 返回调用结果
  return res
}
// 测试代码
const person = {
  name: 'itheima’
}

function func(numA, numB) {
  console.log(this)
  console.log(numA, numB)
  return numA + numB
}
const res = func.myApply(person, [2, 8])
console.log('返回值为:', res)
面试回答

手写apply方法

  1. function的原型上添加myApply方法,保证所有函数都可以调用
  2. 方法内部,通过动态为对象添加方法的形式来指定this指向
  3. 调用完毕之后通过delete关键字删除上一步动态增加的方法
  4. 方法的名字通过 Symbol 进行设置,避免和默认名重复
  5. 直接使用数组传递函数的参数,内部调用时结合...运算符展开数组
Function.prototype.myApply = function (thisArg, args) {
  const fn = Symbol()
  thisArg[fn] = this
  const res = thisArg[fn](...args)
  delete thisArg[fn]
  return res
}

1.3.3 手写bind方法

const person = {
  name: 'itheima'
}

function func(numA, numB, numC, numD) {
  console.log(this)
  console.log(numA, numB, numC, numD)
  return numA + numB + numC + numD
}

const bindFunc = func.myBind(person, 1, 2)

const res = bindFunc(3, 4)
console.log('返回值:', res)

核心步骤为2步

  1. 如何返回一个绑定了this的函数?
  2. 如何实现绑定的参数,及传入的参数合并?
// 1 如何返回一个绑定了`this`的函数
Function.prototype.myBind = function (thisArg) {
  // myBind函数调用时,this就是函数本身 
  return () => {
    // 通过call方法将传入的 thisArg 作为this进行调用
    this.call(thisArg)
  }
}

// 2 如何实现绑定的参数,及传入的参数合并
// ...args 接收绑定参数
Function.prototype.myBind = function (thisArg, ...args) {
  // ...args2 接收调用时的参数
  return (...args2) => {
    // thisArg 需要指定的this
    // args 调用myBind时传入的参数
    // args2 调用新函数时传入的参数
   return this.call(thisArg, ...args, ...args2)
  }
}

// 手写bind不用call()
Function.prototype.myBind = function (thisArg, ...args) {
    const fn = Symbol()
    thisArg[fn] = this
    return (...args2) => {
      const res =  thisArg[fn](...args, ...args2)
      delete thisArg[fn]
      return res
    }
  }

// 测试代码
const person = {
  name: 'itheima'
}

function func(numA, numB, numC, numD) {
  console.log(this)
  console.log(numA, numB, numC, numD)
  return numA + numB + numC + numD
}

const bindFunc = func.myBind(person, 1, 2)

const res = bindFunc(3, 4)
console.log('返回值:', res)
面试回答

手写bind方法

  1. function原型上添加myBind函数,参数1为绑定的this,参数2-参数2为绑定的参数
  2. 内部返回一个新箭头函数,目的是绑定作用域中的this
  3. 返回的函数内部,通过call进行this和参数绑定
  4. 通过call的参数2和参数3指定绑定的参数,和调用时传递的参数
Function.prototype.myBind = function (thisArg, ...args) {
  return (...args2) => {
   return this.call(thisArg, ...args, ...args2)
  }
}

2. 防抖和节流

2.1 防抖

2.1.1 防抖的适用场景

常见的前端性能优化方案 , 它可以防止JS高频渲染页面时出现的视觉抖动(卡顿):

比如

  1. 示例1:页面改变尺寸时,同步调整图表的大小
  2. 示例2:输入内容时,结合ajax进行搜索并渲染结果

如果内容的渲染速度过快,都可能会造成抖动效果,并且连带会浪费性能

  1. 频繁执行逻辑代码,耗费浏览器性能
  2. 频繁发送请求去服务器,耗费服务器性能

适用场景:

  1. 在触发频率高的事件中
    • 频率高的事件:resize、input 、scroll 、keyup….
  2. 执行耗费性能操作
    • 耗费性能的操作:操纵页面、网络请求….
  3. 需要实现的效果:连续操作之后只有最后一次生效

这个时候就可以适用防抖来进行优化

2.1.2 手写防抖

防抖优化之后的效果可以通过一些具体的网站来进行确认,比如12306,他就是通过防抖进行的优化:

  1. 在输入内容的时候没有发送请求
  2. 输入完毕之后,稍等一会才发送请求去服务器

这个就是防抖的效果: 连续事件停止触发后,一段时间内没有再次触发,就执行业务代码

核心实现步骤

  1. 开启定时器,保存定时器id

  2. 清除已开启的定时器

image.png

那个输入框+搜索的例子优化之后代码如下:

let timeId
document.querySelector('.search-city').addEventListener('input', function () {
  //  2. 清除已开启的定时器
  clearTimeout(timeId)
  //  1. 开启定时器,保存定时器id
  timeId = setTimeout(() => {
    renderCity(this.value)
  }, 500)
})

2.1.3 lodash的debounce方法

实际开发中一般不需要手写防抖,因为已经有库里面提供了对应的方法,可以直接调用,也可以自己手写实现debounce,一会咱们就从这两个方面进行讲解:

  1. lodash工具库中的debounce方法(常用)
  2. debounce的实现原理(高频面试题)

官网传送门

_.debounce(func, [wait=0], [options=])

参数

  1. func (Function):要防抖动的原函数。
  2. [wait=0] (number):需要延迟的毫秒数。
  3. [options=] (Object):其他配置/选项对象。
    • [options.leading=false] (boolean):指定在延迟开始前调用。
    • [options.maxWait] (number): 设置 func 允许被延迟的最大值。
    • [options.trailing=true] (boolean): 指定在延迟结束后调用。

返回

(Function):返回新的 debounced(防抖动)函数。

注意:

  1. 实际开发时一般给前2个参数即可,然后适用返回的函数替换原函数即可
  2. 项目中如果有lodash那么直接使用它提供的debounce即可,不仅可以实现防抖,原函数中的this参数均可以正常使用
2.1.3.1 手写debounce函数

手写实现debounce函数,实现lodashdebounce方法的核心功能

需求:

  1. 参数:
    1. func (Function):要防抖动的函数。
    2. [wait=0] (number):需要延迟的毫秒数。
  2. 返回值:
    1. (Function):返回新的 debounced(防抖动)函数。

核心步骤:

  1. 返回防抖动的新函数

  2. 原函数中的this可以正常使用

  3. 原函数中的参数可以正常使用

function debounce(func, wait = 0) {
  let timeId
  // 防抖动的新函数
  // 闭包,只要外部还在使用返回的函数,timerId就不会被回收
  return function (...args) {
    let _this = this
    clearTimeout(timeId)
    timeId = setTimeout(function () {
      // 通过apply调用原函数,并指定this和参数
      func.apply(_this, args)
    }, wait)
  }
}

2.2 节流

2.2.1 节流的适用场景

常见的前端性能优化方案,它可以防止高频触发事件造成的性能浪费:比如

  1. 播放视频时同步缓存播放时间
  2. 如果要多设备同步,还需要通过ajax提交到服务器

高频触发耗费性能的操作,会造成性能浪费

适用场景:在触发频率高的事件中, 执行耗费性能操作,连续触发,单位时间内只有一次生效

优化之前: 每当触发事件就会执行业务逻辑

优化之后:触发事件之后延迟执行逻辑,在逻辑执行完毕之后无法再次触发

2.2.2 手写节流

使用节流将播放器记录时间的例子优化:

核心步骤:

  1. 开启定时器,并保存 id

  2. 判断是否已开启定时器

  3. 定时器执行时 , id设置为空

// 播放器案例优化之后代码
let timeId
video.addEventListener('timeupdate', function () {
  if (timeId !== undefined) {
    return
  }
  timeId = setTimeout(() => {
    console.log('timeupdate触发')
    localStorage.setItem('currentTime', this.currentTime)
    timeId = undefined
  }, 3000)
})

2.2.3 lodash的throttle方法

实际开发中一般不需要手写节流,因为已经有库里面提供了对应的方法,可以直接调用,也可以自己手写实现throttle,一会咱们就从这两个方面进行讲解:

  1. lodash工具库中的throttle方法(常用)
  2. throttle的实现原理(高频面试题)

文档传送门

_.throttle(func, [wait=0], [options=])

参数

  1. func (Function):要节流的函数。
  2. [wait=0] (number):需要节流的毫秒。
  3. [options=] (Object):选项对象。
    • [options.leading=true] (boolean):指定调用在节流开始前/节流开始时是否触发函数默认是true。
    • [options.trailing=true] (boolean):指定调用在节流结束后。

返回

(Function):返回节流的函数。

注意:

  1. 实际开发时一般会给3个参数即可,然后适用返回的函数替换原函数即可
    1. 参数3:options.leading=true默认为true,开始时触发节流函数,一般设置为false
  2. 项目中如果有lodash那么直接使用它提供的throttle即可,不仅可以实现节流,原函数中的this参数均可以正常使用
// 播放器案例使用`lodash` 优化之后的结果如下
const func = function (e) {
  console.log('timeupdate触发')
  console.log('e:', e)
  localStorage.setItem('currentTime', this.currentTime)
}

const throttleFn = _.throttle(func, 1000, { leading: false })

video.addEventListener('timeupdate', throttleFn)
2.2.3.1 手写throttle方法

手写实现throttle函数,实现lodashthrottle方法的核心功能

需求:

  1. 参数:
    1. func (Function): 要节流的函数。
    2. [wait=0] (number): 需要节流的毫秒。
  2. 返回值:
    1. (Function): 返回节流的函数

核心步骤:

  1. 返回节流的新函数

  2. 原函数中的this可以正常使用

  3. 原函数中的参数可以正常使用

// 节流工具函数
function throttle(func, wait = 0) {
  let timeId
  return function (...args) {
    if (timeId !== undefined) {
      return
    }
    const _this = this
    timeId = setTimeout(() => {
      func.apply(_this, args)
      timeId = undefined
    }, wait)
  }
}

2.3 防抖和节流对比

防抖:常见的前端性能优化方案,它可以防止JS高频渲染页面时出现的视觉抖动(卡顿)

节流:常见的前端性能优化方案,它可以防止高频触发事件造成的性能浪费

简单理解:防抖是回城,节流是技能冷却

适用场景

  • 防抖:在触发频率高的事件中,执行耗费性能操作,连续操作之后只有最后一次生效
  • 节流:在触发频率高的事件中,执行耗费性能操作, 连续触发,单位时间内只有一次生效

image.png

2.3.1 lodash工具库中的防抖(debounce)和节流(throttle)

image.png

小建议:如果项目使用了lodash优先使用它的debounce或throttle方法,它可以支持 this 和 参数

2.3.2 手写实现debounce和throttle函数

步骤:

  1. 返回 防抖 or 节流 的新函数
  2. 原函数中的this可以正常使用
  3. 原函数中的参数可以正常使用
// 防抖工具函数
function debounce(func, wait = 0) {
    let timeId
    return function (...args) {
        let _this = this
        clearTimeout(timeId)
        timeId = setTimeout(function () {
            func.apply(_this, args)
        }, wait)
    }
}

// 节流工具函数
function throttle(func, wait = 0) {
    let timeId
    return function (...args) {
        if (timeId !== undefined) {
            return
        }
        const _this = this
        timeId = setTimeout(() => {
            func.apply(_this, args)
            timeId = undefined
        }, wait)
    }
}

3. JS继承

继承:继承可以使子类具有父类的各种属性和方法,而不需要再次编写相同的代码

ES6:基于Class实现继承

  1. class核心语法
  2. class实现继承
  3. 静态属性和私有属性

ES5:基于原型和构造函数实现继承 (命名来源于 《JavaScript高级程序设计》)

  1. 原型链继承
  2. 借用构造函数继承
  3. 组合继承
  4. 原型式继承
  5. 寄生式继承
  6. 寄生组合式继承(主要掌握,其余了解)

3.0 提前掌握

3.0.1 Object.create核心用法-文档链接

  const person = {
    name: 'itheima',
    foods: ['西蓝花', '西红柿', '西葫芦']
  }
  // 将传入的对象作为原型,创建一个新对象(浅拷贝)
  const clone = Object.create(person)
  console.log(clone === person) // false
  clone.name = 'itheima'
  clone.foods.push('西北风')
  console.log(clone.foods === person.foods)// true

3.0.2 Object.assign核心用法-文档链接

const person = {
name: 'itheima',
foods: ['西蓝花', '西红柿', '西葫芦']
}
const son = {
name: 'rose',
}
// 参数1 目标对象
// 参数2 源对象
// 将源对象的自身属性复制到目标对象,并返回目标对象
const returnTarget = Object.assign(son, person)

console.log(returnTarget === son)// true
console.log(son.name)// itheima
console.log(son.foods === person.foods)// true

3.1 JS继承-ES5

传送门:继承与原型链

传送门:继承(计算机科学)

传送门:JavaScript高级程序设计

传送门:MDN-Object.create

传送门:MDN-Object.assign

2.1.1 (重点)ES5-寄生组合式继承

所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。其背后的基本思路是:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型 原型的一个副本而已。

核心步骤:

  1. 通过构造函数来继承属性
  2. 通过原型链来继承方法
// 继承原型函数
function inheritPrototype(son, parent){
    const prototype = object.create(parent.prototype)
    prototype.constructor = son
    
 //    const prototype = object.create(parent.prototype, {
 //      constructor: {
 //          value: son
 //     }
 //    })
    son.prototype = prototype
}

// 父类
function Parent(name) {
  this.name = name
  this.foods = ['西蓝花', '西葫芦', '西红柿']
}

Parent.prototype.sayHi = function () {
  console.log(this.name, `我喜欢吃,${this.foods}`)
}

// 子类借用父类的构造函数
function Son(name, age) {
  Parent.call(this, name)
  this.age = age
}
// 完成原型继承
inheritPrototype(Son,Parent)
// 可以继续在原型上添加属性/方法
Son.prototype.sayAge = function () {
  console.log('我的年龄是', this.age)
}

const son1 = new Son('jack', 18)
const son2 = new Son('rose', 16)
面试回答

ES5-寄生组合式继承

  1. 寄生组合式继承的核心步骤是:通过构造函数来继承属性,通过原型链来继承方法
  2. 寄生组合式继承和组合式继承的区别是:原型链的继承并没有调用父类的构造函数,而是直接基于父类的原型创建一个新副本实现继承

ES5-原型链实现继承

核心步骤:希望继承谁,就将谁作为原型

缺点:父类中的引用数据类型,会被所有子类共享

// 父类
function Parent(name) {
  this.name = name
  this.foods = ['西蓝花', '西红柿']
  this.sayFoods = function () {
    console.log(this.foods)
  }
}
// 子类
function Son() {
}
// 将父类的实例 作为子类的原型
Son.prototype = new Parent('jack')
const s1 = new Son()
s1.sayFoods()// ['西蓝花', '西红柿']

const s2 = new Son()
s2.sayFoods() // ['西蓝花', '西红柿']

s2.foods.push('西葫芦')

s2.sayFoods()// ['西蓝花', '西红柿', '西葫芦']
s1.sayFoods()// ['西蓝花', '西红柿', '西葫芦']
面试回答:

ES5-原型链实现继承

  1. 将父类的实例作为子类的原型实现继承
  2. 这种继承方法的缺点是父类中的引用类型数据会被所有子类共享

ES5-构造函数继承

核心步骤:在子类的构造函数中通过callapply父类的构造函数

缺点:子类没法使用父类原型上的属性/方法

// 父类
function Parent(name) {
  this.name = name
}
Parent.prototype.sayHi = function () {
  console.log('你好,我叫:', this.name)
}

// 子类
function Son(name) {
  Parent.call(this, name)
}

const s1 = new Son('lucy')
const s2 = new Son('rose')
s1.sayHi() // 报错
面试回答:

ES5-构造函数继承

  1. 在子类的构造函数中通过call或者apply调用父类的构造函数
  2. 这种继承方法的缺点是:子类没法使用父类原型上的属性/方法

ES5-组合继承

通过组合继承,结合上面2种方法的优点

核心步骤:

  1. 通过原型链继承公共的属性和方法
  2. 通过构造函数继承实例独有的属性和方法

特点:调用了2次构造函数

// 父类
function Person(name) {
  this.name = name
}
// 方法加父类原型上
Person.prototype.sayHi = function () {
  console.log(`你好,我叫${this.name}`)
}
// 子类构造函数
function Student(name, age) {
  // 调用父类构造函数传入this
  Person.call(this, name)
  // 子类独有的属性和方法单独设置
  this.age = age
}
// 设置子类的原型为 父类实例
Student.prototype = new Person()
// 调用子类的构造函数
const s = new Student('李雷', 18)
// 可以使用原型链上的 属性和方法 也可以使用 通过构造函数获取的父类的属性和方法
面试回答:

ES5-组合继承

  1. 组合继承的核心步骤有2步:

    1. 通过原型链继承公共的属性和方法
    2. 通过构造函数继承实例独有的属性和方法
  2. 组合继承的特点:调用2次父类的构造函数,浪费性能

ES5-原型式继承

直接基于对象实现继承

核心步骤:对某个对象进行浅拷贝(工厂函数或Object.create),实现继承

缺点:父类中的引用数据类型,会被所有子类共享

// 可以用 Object.create替代
function objectFactory(obj) {
  function Fun() { }
  Fun.prototype = obj
  return new Fun()
}
const parent = {
  name: 'parent',
  age: 25,
  friend: ['rose', 'ice', 'robot'],
  sayHi() {
    console.log(this.name, this.age)
  }
}
const son1 = objectFactory(parent)
const son2 = objectFactory(parent)
son1.friend.push('lucy')
console.log(son2.friend)
面试回答:

ES5-原型式继承

  1. 原型式继承的核心步骤是:对某个对象进行浅拷贝,可以通过内置apiObject.create实现,不需要调用构造函数即可实现继承,主要针对于继承对象的情况
  2. 原型式继承的缺点是:父类中的引用数据类型,会被所有子类共享

ES5-寄生式继承

核心步骤:

定义工厂函数,并在内部:

  1. 对传入的对象进行浅拷贝(公共属性/方法)
  2. 为浅拷贝对象增加属性/方法(独有属性/方法)
function createAnother(origin) {
  // Object.create基于原型创建新对象,对属性进行浅拷贝
  const clone = Object.create(origin)
  // 为对象增加属性/方法
  clone.sayHi = function () {
    console.log('你好')
  }
  return clone
}
const parent = {
  name: 'parent',
  foods: ['西蓝花', '炒蛋', '花菜']
}
const son1 = createAnother(parent)
const son2 = createAnother(parent)
面试回答:

寄生式继承

  1. 寄生式继承的核心步骤是:基于对象创建新对象(可以使用Object.create),并且为新创建的对象增加新的属性和方法

  2. 寄生式继承和上一节学习的原型式继承的区别是:创建出来的新对象,会额外的增加新的属性/方法

3.2 JS继承-ES6

在ES6中class关键字的使用,并且使用它来实现继承

传送门:mdn类

传送门:阮一峰ES6-class

传送门:mdn-super

ES6中推出了class类,类是用于创建对象的模板,他们用代码封装数据以处理该数据,JS 中的类建立在原型上。

class可以看作是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。

3.2.1 class核心语法

  1. 如何定义及使用:
  2. 如何定义实例属性/方法:
// 定义类
class Person {
  // 公有属性/实例属性,方便一眼确认有哪些
  name
  food = '西红柿' // 定义属性并设置默认值
  // 构造方法,类似于构造函数,new的时候会调用,内部的this就是实例化的对象
  constructor(name, food) {
    this.name = name
    this.food = food
    // 动态添加属性(不推荐)
    this.other = 'zs'
  }
  // 实例方法
  sayHi() {
    console.log(`你好,我叫${this.name},我喜欢吃${this.food}`)
  }
}
const p = new Person('小黑', '西蓝花')
p.sayHi()
面试回答:

class核心语法:

  1. 通过class 类名{}的形式来定义类
  2. 内部直接写实例属性,可以设置默认值,
  3. 实例方法的添加方式为方法名(){}
  4. 构造函数通过constructor进行添加
  5. 通过new 类名()创建实例,会调用构造函数constructor
class Person{
    name
    food='西兰花炒蛋'
    constructor(name){
      this.name=name
    }
    sayHi(){
      console.log('你好,我叫:',this.name)
    }
}

3.2.2 class实现继承

extends:关键字用于类声明或者类表达式中,以创建一个类,该类是另一个类的子类。

super:关键字用于访问对象字面量或类的原型([[Prototype]])上的属性,或调用父类的构造函数。

关键语法:

  1. 子类通过extends继承父类
  2. 子类构造函数中通过super调用父类构造函数
// 在上一份代码的基础上继续编写下面代码
class Student extends Person {
  song
  constructor(name, food, song) {
    // 子类构造函数使用this以前必须调用super
    super(name, food)
    this.song = song
  }
  // 添加方法
  sing() {
    console.log(`我叫${this.name},我喜欢唱${this.song}`)
  }
}
const s = new Student('李雷', '花菜', '孤勇者')
s.sayHi()
s.sing()
面试回答:

class实现继承

  1. 子类通过extends继承继承父类
  2. 子类如果需要重新定义构造函数,必须在内部通过super关键字调用父类的构造函数

3.2.3 class私有,静态属性和方法

静态:类(class)通过 static 关键字定义静态方法。不能在类的实例上调用静态方法,而应该通过类本身调用。

私有:类属性在默认情况下是公有的,但可以使用增加哈希前缀 # 的方法来定义私有类字段,声明和访问时也需要加上。

补充语法:

  1. 私有属性/方法的定义及使用(内部调用)
  2. 静态属性/方法的定义及使用(类直接访问)
class Person {
  constructor(name) {
    this.name = name
  }
  // 通过#作为前缀添加的属性会变为私有
  // 私有属性
  #secret = '我有一个小秘密,就不告诉你'
  // 私有方法
  #say() {
    // 私有属性可以在
    console.log('私有的say方法')
  }
  info() {
    // 在类的内部可以访问私有属性调用私有方法
    console.log(this.#secret)
    this.#say()
  }

  // 通过 static定义静态属性/方法
  static staticMethod() {
    console.log('这是一个静态方法')
    console.log(this)
  }
  static info = '直立行走,双手双脚'
}

const p = new Person('jack')
console.log(p)
// 外部无法访问 点语法访问直接报错,通过[]无法动态获取
console.log(p['#secret'])
p.info()
// 通过类访问静态属性/方法
Person.staticMethod()
console.log(Person.info)
面试回答:
  1. class中私有属性/方法

    1. 定义和使用时需要使用关键字#
    2. 私有属性只能在类的内部使用,外部无法使用(代码中)
    3. Chrome的控制台中为了方便调试,可以直接访问
  2. class中静态属性/方法

    1. 定义和使用时需要使用关键字static
    2. 通过类访问
    3. 静态方法中的this是类

4. fetch

这一节咱们来学习内置函数fetch

传送门-fetch

传送门-Response

传送门-Headers

4.0 URLSearchParams核心用法-文档链接

// 实例化时支持传入JS对象
const params = new URLSearchParams({ name: 'jack', age: 18 })
// toString方法 返回搜索参数组成的字符串,可直接使用在 URL 上。
console.log(params.toString())

ajax&axios&fetch的关系:

ajaxajax 是一种基于原生 JavaScript 的异步请求技术。它使用 XMLHttpRequest 对象来发送请求和接收响应。

axiosaxios 是一个基于 Promise 的 HTTP 客户端,可以在浏览器和 Node.js 中使用。它提供了更高级别的封装,使发送请求和处理响应更加简单和灵活。【项目中一般使用axios】

fetchfetch 是浏览器内置的 API,用于发送网络请求。它提供了一种现代化、基于 Promise 的方式来进行网络通信。用法和axios类似,但相比于 axios,它的功能和封装级别更为简单。【一个项目只有几个请求时使用】

全局的fetch函数用来发起获取资源请求.他返回一个promise,这个promise会在请求响应后被resolve,并传回Response对象

这一节咱们会学习的有:

  1. fetch核心语法

  2. fetch结合URLSearchParams发送get请求:

    1. const obj = {
          name:'jack',
          age:18
      }
      name=jack&age=17
      
  3. fetch发送post请求,提交FormData数据

  4. fetch发送post请求,提交JSON数据

4.1 fetch核心语法

核心语法:

  1. 如何发请求:
  2. 如何处理响应:
  3. 如何处理异常?

测试用接口

fetch(资源地址,{...配置项对象})
.then(response=>{
    // 接收请求
})

async function func() {
    const res = await fetch('请求地址')
    res.status
    const data = await res.json()
}

面试回答:

fetch核心语法

  1. fetch函数的参数:
    1. 参数1:请求的url地址
    2. 参数2:以对象的形式设置请求相关的内容比如,方法,请求头,提交的数据等.
  2. fetch获取到响应结果,需要如何解析:
fetch(参数1,参数2)
.then(response=>{
    // 接收请求
})

4.2 fetch结合URLSearchParams发送get请求:

需求:

  1. 使用fetch结合URLSearchParams调用地区查询接口
;(async function () {
  const params = new URLSearchParams({
    pname: '广东省',
    cname: '广州市'
  })
  const url = `http://hmajax.itheima.net/api/area?${params.toString()}`
  // 1. 如何发送请求?默认是get方法
  // 参数1 url地址
  // 返回的是 Promise对象,通过await等待获取response对象
  const res = await fetch(url)
  if (res.status >= 200 && res.status < 300) {
    // 2. 如何处理响应(JSON)?
    // .json方法解析json,返回的是Promise对象 继续通过await等待
    const data = await res.json()
  } else {
    // 3. 如何处理异常?
    console.log('请求异常', res.status)
  }
})()

面试回答:

fetch结合URLSearchParams发送get请求:

  1. fetch发送get请求时,不需要设置请求方法,因为默认的就是get
  2. URLSearchParams可以用来创建或者解析查询字符串,这里通过它将对象转为查询字符串

4.3 post请求-提交JSON

核心语法:

  1. 如何设置请求头?
async function func() {
    const headers = new Headers()
    headers.append('key','value')
    const res = await fetch('请求地址', {
        method: '请求方法',
        body: '提交数据',
        headers:headers
    })
}

需求:

  1. fetch发送post请求,提交JSON数据
  2. 测试接口-用户注册

核心步骤:

  1. 根据文档设置请求头
  2. 通过配置项设置,请求方法,请求头,请求体
    ; (async function () {
      // 通过headers设置请求头
      const headers = new Headers()
      // 通过 content-type指定请求体数据格式
      headers.append('content-type', 'application/json')
      // 参数1 url
      // 参数2 请求配置
      const res = await fetch('http://hmajax.itheima.net/api/register', {
        method: 'post',// 请求方法
        headers, // 请求头
        // 请求体
        body: JSON.stringify({ username: 'itheima9876', password: '123456' })
      })
      const json = await res.json()
      console.log(json)
    })()

面试回答:

post请求-提交JSON

  1. fetch函数的第二个参数可以设置请求头,请求方法,请求体,根据接口文档设置对应的内容即可
  2. 可以通过JSON.stringify将对象转为JSON

4.4 post请求-提交FormData

核心语法:

  1. 如何设置请求方法?
  2. 如何提交数据?
async function func() {
    const res = await fetch('请求地址', {
        method: '请求方法',
        body: '提交数据'
    })
}

需求:

  1. fetch发送post请求,提交FormData数据(上传+回显)
  2. 测试接口-上传图片

核心步骤:

  1. 通过FormData添加文件
  2. 通过配置项设置,请求方法,请求体(FormData不需要设置请求头)
<!--选择图片 -->
<input type="file" class="file" accept="image/*">
<!--图片回显 -->
<img class="icon" src="" alt="">
<script>
  document.querySelector('.file').addEventListener('change', async function (e) {
      // 生成FormData对象并添加数据
      const data = new FormData()
      data.append('img', this.files[0])
      const res = await fetch('http://hmajax.itheima.net/api/uploadimg', {
        method: 'post',
        body: data
      })
      const resData = await res.json()
      console.log(resData)
      // 回显
      document.querySelector('.icon').src = resData.data.url
    })
</script>

面试回答:

  1. post请求-提交FormData

    1. fetch提交FormData时不需要额外的设置请求头

    2. 实例化FormData之后可以通过append(key,value)方法来动态添加数据

  2. ajax&axios&fetch的关系

    1. ajax是一种基于原生JavaScript的异步请求技术,他使用XMLHttpRequest对象来发送请求和接收响应---学习原理时适用

    2. axios是一个基于Promise的http客户端,可以在浏览器和Node.js中使用.他提供了更高级别的封装,使发送请求和处理响应更加简单和灵活---项目开发时适用

    3. fetch是浏览器内置的API,用于发送网络请求.它基于Promise的方式来进行网络通信.和axios类似.但是功能和封装级别比axios更为简单---项目简单,不想导入axios时适用

4.5 兼容ie10+:

  1. promise-polyfill
  2. whatwg-fetch

5. Generator

这一节咱们来学习generator传送门-Generator

Generator 函数是 ES6 提供的一种异步编程解决方案

5.1 Generator-核心语法

Generator对象由生成器函数返回并且它符合可迭代协议迭代器协议

生成器函数在执行时能暂停,后面又能从暂停处继续执行。

他可以用来控制流程,语法行为和之前学习的函数不一样

核心语法:

  1. 如何定义生成器函数:
  2. 如何获取generator对象
  3. yield表达式的使用
  4. 通过for of获取每一个yield的值
// 1. 通过function* 创建生成器函数 
function* foo() {
  // 遇到yield表达式时会暂停后续的操作
  yield 'a'
  yield 'b'
  yield 'c'
  return 'd'
}
// 2. 调用函数获取生成器
const f = foo()
// 3. 通过next方法获取yield 之后的表达式结果,会被包装到一个对象中
// 执行一次next 即可获取一次 yield之后的表达式结果
const res1 = f.next()
console.log(res1)// {value: 'a', done: false}
const res2 = f.next()
console.log(res2)// {value: 'b', done: false}
const res3 = f.next()
console.log(res3)// {value: 'c', done: false}
// 最后一次可以拿到return的结果
const res4 = f.next()
console.log(res4)// {value: 'd', done: true} 
// done 为true之后,获取到的value为undefined
const res5 = f.next()
console.log(res5)// {value: undefined, done: true} 


// 4. 通过for of 获取每一个yield之后的值,
const f2 = foo()
for (const iterator of f2) {
  console.log(iterator)
  // a
  // b
  // c
}

面试回答:

Generator-核心语法

  1. 可以通过生成器函数(function* xxx(){})来生成Generator对象:
  2. 通过Generator对象的next方法可以获取yield表达式之后的结果

5.2 Generator-管理异步

核心步骤:异步操作之前加上yield

function* cityGenerator() {
  yield axios('http://hmajax.itheima.net/api/city?pname=北京')
  yield axios('http://hmajax.itheima.net/api/city?pname=广东省')
}
const city = cityGenerator()
city.next().value.then(res => {
  console.log('res:', res)
  return city.next().value
}).then(res => {
  console.log('res:', res)
})

5.2.1 Generator-id生成器

需求:使用Generator实现一个id生成器id

function* idGenerator() {
    // 逻辑略
}
const idMaker = idGenerator()

// 调用next方法,获取id(每次累加1)
const { value: id1 } = idMaker.next()
console.log(id1)
const { value: id2 } = idMaker.next()
console.log(id2)

核心步骤:

  1. 定义生成器函数
  2. 内部使用循环,通过yield返回id并累加
// 1. 通过function* 创建生成器函数 
function* generator() {
  let id = 0
  // 无限循环
  while (true) {
    // id累加并返回
    yield id++
  }
}
// 2. 调用函数获取生成器
const idMaker = generator()
// 3. 需要id的时候 通过next获取即可
const { value: id1 } = idMaker.next()
console.log(id1)
const { value: id2 } = idMaker.next()
console.log(id2)
面试回答:

Generator-id生成器

  1. 生成器函数内部的代码会在调用next方法时执行,利用这一特点,可以实现任意的生成器,需要时调用next即可获取结果

5.2.2 Generator-流程控制

遇到yield表达式时会暂停后续的操作

需求:使用Generator实现流程控制

function* weatherGenerator() {
	// 逻辑略
    yield axios()
}
// 获取Generator实例
const weather = weatherGenerator()
// 依次获取 北上广深的天气 (axios)
weather.next()

核心步骤:

  1. yield后面跟上天气查询逻辑
  2. 接口文档-天气预报
  3. 参考code:北京 110100 上海 310100 广州 440100 深圳 440300
  <button class="getWeather">天气查询</button>
  <script src="https://cdn.bootcdn.net/ajax/libs/axios/1.3.6/axios.js"></script>
  <script>
    /**
     * 需求:流程控制,依次查询,北上广深的天气预报
     * 参考code: 北京 110100  上海 310100  广州 440100 深圳 440300
     * 接口文档: https://apifox.com/apidoc/project-1937884/api-49760220
     * */
    function* weatherGenerator() {
      // 北京
      yield axios('http://hmajax.itheima.net/api/weather?city=110100')
      // 上海
      yield axios('http://hmajax.itheima.net/api/weather?city=310100')
      // 广州
      yield axios('http://hmajax.itheima.net/api/weather?city=440100')
      // 深圳
      yield axios('http://hmajax.itheima.net/api/weather?city=440300')
    }

    const cityWeather = weatherGenerator()
    document.querySelector('.getWeather').addEventListener('click', async () => {
      const res = await genCity.next()
      console.log(res)
    })
  </script>
面试回答:

Generator-流程控制

  1. 使用Generator控制流程的本质是利用yield关键字来分隔逻辑比如示例中依次调用了多个接口,通过yield分隔,通过next来触发调用

6. JS设计模式

传送门:wiki-设计模式

传送门:JavaScript设计模式与开发实践

设计模式的指的是:在面向对象软件设计过程中针对特定问题的简洁而优雅的解决方案。通俗一点说,设计模式就是给面向对象软件开发中的一些好的设计取个名字。

目前说到设计模式,一般指的是《设计模式:可复用面向对象软件的基础》一书中提到的23种常见的软件开发设计模式。

JavaScript中不需要生搬硬套这些模式,咱们结合实际前端开发中的具体应用场景,来看看有哪些常用的设计模式

这一节咱们会学习:

  1. JS中的常用设计模式
  2. 设计模式在开发/框架中的应用场景

6.1 工厂模式

在JavaScript中,工厂模式的表现形式就是一个直接调用即可返回新对象的函数

// 定义构造函数并实例化
function Dog(name){
    this.name=name
}
const dog = new Dog('柯基')

// 工厂模式
function ToyFactory(name,price){
    return {
        name,
        price
    }
}
const toy1 = ToyFactory('布娃娃',10)
const toy2 = ToyFactory('玩具车',15)

应用场景

Vue3-createApp

Vue2->Vue3:

image.png 调整原因:

  1. 避免在测试期间,全局配置污染其他测试用例

  2. 全局改变Vue实例的行为,移到Vue实例上

具体说明:

  1. 弃用了new Vue,改成了工厂函数createApp-传送门
  2. 任何全局改变 Vue 行为的 API(vue2) 现在都会移动到应用实例上(vue3)
  3. 就不会出现,Vue2中多个Vue实例共享,相同的全局设置,可以实现隔离
<!DOCTYPE html>
<html lang="zh-CN">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    #app1,
    #app2 {
      border: 1px solid #000;
    }
  </style>
</head>

<body>
  <h2>vue2-全局注册组件</h2>
  <div id="app1">
    实例1
    <my-title></my-title>
  </div>
  <div id="app2">
    实例2
    <my-title></my-title>
  </div>
  <script src="https://cdn.bootcdn.net/ajax/libs/vue/2.7.9/vue.js"></script>
  <script>
    Vue.component('my-title', {
      template: '<h2 style="color:orange">标题组件</h2>'
    })
    const app1 = new Vue({
      el: "#app1"
    })

    const app2 = new Vue({
      el: "#app2"
    })

  </script>
</body>

</html>
axios.create:
  1. 基于传入(自定义)的配置创建一个新的axios实例,传送门
  2. 项目中有2个请求基地址如何设置?
// 1. 基于不同基地址创建多个 请求对象
const request1 = axios.create({
baseURL: "基地址1"
})
const request2 = axios.create({
baseURL: "基地址2"
})
const request3 = axios.create({
baseURL: "基地址3"
})

// 2. 通过对应的请求对象,调用接口即可
request1({
url: '基地址1的接口'
})
request2({
url: '基地址2的接口'
})
request3({
url: '基地址3的接口'
})

面试回答:

  1. 工厂模式:JS中的表现形式,返回新对象的函数(方法)

      function sayHi(){} // 函数
      const obj ={
          name:'jack',
          sayHello(){} // 方法
      }
    
  2. 日常开发中,有2个很经典的场景

    1. vue3-createApp:将全局改变Vue实例的行为,移到Vue实例上
      1. vue3中创建实例的api改为createAppvue2中是new Vue
      2. Vue3中,没有影响所有Vue实例的api了,全都变成了影响某个app对象的api,比如Vue.component-->app.component
    2. axios-create:基于自定义配置新建实例
      1. axios.create基于传入的配置,创建一个新的请求对象,可以用来设置多个基地址

6.2 单例模式

单例模式指的是,在使用这个模式时,单例对象整个系统需要保证只有一个存在。

  1. 单例方法:

    1. 自己实现
    2. vant中的toast和notify组件
  2. 单例的思想:

    1. vue2中的use方法
    2. vue3中的use方法

需求: 调用 方法 获取单例对象,重复调用获取的是相同对象

  1. 通过静态方法getInstance获取唯一实例

    const s1 = SingleTon.getInstance()
    const s2 = SingleTon.getInstance()
    console.log(s1===s2)//true
    

核心步骤:

  1. 定义类
  2. 添加私有静态属性:#instance
  3. 添加静态方法getInstance
  4. 判断并返回对象
    1. 调用时判断#instance是否存在
    2. 存在:直接返回
    3. 不存在:实例化,保存,并返回
class SingleTon {
   constructor() { }
   // 私有属性,保存唯一实例
   static #instance

  // 获取单例的方法
  static getInstance() {
    if (SingleTon.#instance === undefined) {
      // 内部可以调用构造函数
      SingleTon.#instance = new SingleTon()
    }
    return SingleTon.#instance
  }
}

实际应用

  1. vant组件库中的弹框组件,保证弹框是单例
    1. toast组件:传送门
    2. notify组件:传送门
    3. 如果弹框对象
      1. 不存在,-->创建一个新的
      2. 存在,直接用
  2. vue中注册插件,用到了单例的思想(只能注册一次)
    1. vue2:传送门
    2. vue3:传送门

面试回答:

  1. 单例模式:
    1. 保证,应用程序中,某个对象,只能有一个
  2. 自己实现核心为一个返回唯一实例的方法,比如getInstance
    1. 实例存在->返回
    2. 实力不存在->创建,保存->返回
  3. 应用场景:
    1. vanttoastnotify组件都用到了单例:多次弹框,不会创建多个弹框,复用唯一的弹框对象
    2. vue中注册插件,vue2vue3都会判断插件是否已经注册,已注册,直接提示用户

6.3 观察者模式

在对象之间定义一个一对多的依赖,当一个对象状态改变的时候,所有依赖的对象都会自动收到通知。

image.png

举个例子:

  1. dom事件绑定,比如
window.addEventListener('load', () => {
  console.log('load触发1')
})
window.addEventListener('load', () => {
  console.log('load触发2')
})
window.addEventListener('load', () => {
  console.log('load触发3')
})
  1. Vue中的watch
export default {
    data() {
        return {
            message: '',
        }
    },
    watch: {
        message(newMes, oldMes) {
            console.log('message-change')
        }
    }
}

面试回答:

观察者模式重点说清楚2点即可:

  1. 在对象之间定义一个一对多的依赖,当一个对象状态改变的时候,所有依赖的对象都会自动收到通知。
  2. 常见场景:vue中的watch,dom事件绑定

观察者模式和发布订阅模式的区别也是常见考点,回答方式见下一节

6.4 发布订阅模式

01-应用场景

发布订阅模式可以实现的效果类似观察者模式,但是两者略有差异,一句话描述:一个有中间商(发布订阅模式)一个没中间商(观察者模式)

image.png

应用场景

  1. vue2中的EventBus传送门
  2. vue3中因为移除了实例上对应方法,可以使用替代方案:传送门
    1. 官方推荐,用插件
    2. 我们自己写

02-自己写一个事件总线

需求: 现HMEmitter,支持如下调用:

  1. $on
  2. $emit
  3. $off
  4. $once

image.png

const bus = new HMEmitter()
// 注册事件
bus.$on('事件名1',回调函数)
bus.$on('事件名1',回调函数)

// 触发事件
bus.$emit('事件名',参数1,...,参数n)

// 移除事件
bus.$off('事件名')

// 一次性事件
bus.$once('事件名',回调函数)

核心步骤:

  1. 定义类
  2. 添加私有属性:#handlers={事件1:[f1,f2],事件2:[f3,f4]}
  3. 实例方法:
    1. $on(事件名,回调函数):注册事件
    2. $emit(事件名,参数列表):触发事件
    3. $off(事件名):移除事件
    4. $once(事件名,回调函数):注册一次性事件

基础模板:

<!DOCTYPE html>
<html lang="zh-CN">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
  <h2>自己实现事件总线</h2>
  <button class="on">注册事件</button>
  <button class="emit">触发事件</button>
  <button class="off">移除事件</button>
  <button class="once-on">一次性事件注册</button>
  <button class="once-emit">一次性事件触发</button>
  <script>
    class HMEmmiter {
    	// 逻辑略
    }

    // 简化 querySelector调用
    function qs(selector) {
      return document.querySelector(selector)
    }
    // 注册事件
    qs('.on').addEventListener('click', () => {
    
    })
    // 触发事件
    qs('.emit').addEventListener('click', () => {
   
    })
    // 移除事件
    qs('.off').addEventListener('click', () => {
      
    })
    // 一次性事件注册
    qs('.once-on').addEventListener('click', () => {
     
    })
    // 一次性事件触发
    qs('.once-emit').addEventListener('click', () => {
     
    })
  </script>
</body>

</html>
class HMEmmiter {
  #handlers = {}
  // 注册事件
  $on(event, callback) {
    if (!this.#handlers[event]) {
      this.#handlers[event] = []
    }
    this.#handlers[event].push(callback)
  }
  // 触发事件
  $emit(event, ...args) {
    const funcs = this.#handlers[event] || []
    funcs.forEach(func => {
      func(...args)
    })
  }
  // 移除事件
  $off(event) {
    this.#handlers[event] = undefined
  }
  // 一次性事件
  $once(event, callback) {
    this.$on(event, (...args) => {
      callback(...args)
      this.$off(event)
    })
  }
}

面试回答:

  1. 发布订阅模式:可以实现的效果类似观察者模式,但是两者略有差异,一句话描述:一个有中间商(发布订阅模式)一个没中间商(观察者模式)
  2. 经典的场景是vue2中的EventBusvue3移除了实例的$on$off$once方法,如果还需要使用:
    1. 使用第三方插件
    2. 自己实现事件总线
  3. 自己实现事件总线的核心逻辑
    1. 添加类,内部定义私有属性#handlers={},以对象的形式来保存回调函数
    2. 添加实例方法:
      1. $on:
        1. 接收事件名和回调函数
        2. 内部判断并将回调函数保存到#handlers中,以{事件名:[回调函数1,回调函数2]}格式保存
      2. $emit
        1. 接收事件名和回调函数参数
        2. 内部通过#handlers获取保存的回调函数,如果获取不到设置为空数组[]
        3. 然后挨个调用回调函数即可
      3. $off
        1. 接收事件名
        2. #handlers中事件名对应的值设置为undefined即可
      4. $once
        1. 接收事件名和回调函数
        2. 内部通过$on注册回调函数,
        3. 内部调用callback并通过$off移除注册的事件

6.5 原型模式

原型模式是创建型模式的一种,其特点在于通过 复制 一个已经存在的实例来返回新的实例,而不是新建实例。

在原型模式下,当我们想要创建一个对象时,会先找到一个对象作为原型,然后通过克隆原型的方式来创建出一个与原型一样(共享一套数据/方法)的对象。在JavaScript中,Object.create就是实现原型模式的内置api

应用场景

  1. Object.create:将对象作为原型,创建新对象

    const food = {
      name: '西兰花',
      eat () {
        console.log('好吃')
      }
    }
    const nFood = Object.create(food)
    console.log(nFood === food) // false
    
  2. Vue2中的数组方法:

    1. Vue2源码

    2. 数组变更方法

vue2中重写数组方法:

  1. 调用方法时(push,pop,shift,unshift,splice,sort,reverse)可以触发视图更新:传送门
  2. 源代码:传送门
  3. 测试一下:
<!DOCTYPE html>
<html lang="zh-CN">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
  <h2>原型模式</h2>
  <div id="app"></div>
  <script src="https://cdn.bootcdn.net/ajax/libs/vue/2.7.9/vue.js"></script>
  <script>
    const app = new Vue({
      el: "#app", data: {
        foods: ['西瓜', '西葫芦', '西红柿']
      }
    })
    console.log(app.foods.push === Array.prototype.push)
  </script>

</body>

</html>

面试回答

  1. 原型模式:

    1. 基于某个对象,创建一个新的对象
    2. JS中,通过Object.create就是实现了这个模式的内置api
    3. 比如vue2中重写数组方法就是这么做的
  2. vue2中数组重写了7个方法,内部基于数组的原型Array.prototype创建了一个新对象

  3. 创建的方式是通过Object.create进行浅拷贝

  4. 重写的时候:

    1. 调用数组的原方法,获取结果并返回---方法的功能和之前一致
    2. 通知了所有的观察者去更新视图
const app = new Vue({
    el:"#app",
    data:{
        arr:[1,2,3]
    }
})
app.arr.push === Array.prototype.push //false

6.6 代理模式

代理模式是为一个对象提供一个代用品或占位符,以便控制对它的访问

代理模式指的是拦截和控制与目标对象的交互

image.png

这里我们来看一个非常经典的代理模式的应用:缓存代理

image.png

核心语法:

  1. 创建对象缓存数据
  2. 获取数据时,先通过缓存判断:
    1. 有直接获取,缓存,并返回
    2. 没有:调用接口
//  1. 创建对象缓存数据
const cache = {}
async function searchCity(pname) {
  // 2. 判断是否缓存数据
  if (!cache[pname]) {
    // 2.1 没有:查询,缓存,并返回
    const res = await axios({
      url: 'http://hmajax.itheima.net/api/city',
      params: {
        pname
      }
    })
    cache[pname] = res.data.list
  }
  // 2.2 有:直接返回
  return cache[pname]
}

document.querySelector('.query').addEventListener('keyup', async function (e) {
  if (e.keyCode === 13) {
    const city = await searchCity(this.value)
    console.log(city)
  }
})

面试回答:

  1. 代理模式的核心是,通过一个代理对象拦截对原对象的直接操纵
  2. 比如可以通过缓存代理:
    1. 缓存获取到的数据
    2. 拦截获取数据的请求:
      1. 已有缓存:直接返回缓存数据
      2. 没有缓存:去服务器获取数据并缓存
  3. 提升数据获取效率,降低服务器性能消耗

6.7 迭代器模式

可以让用户透过特定的接口巡访容器中的每一个元素而不用了解底层的实现(遍历)

迭代器模式提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露该对象的内部表示。简而言之就是:遍历

遍历作为日常开发中的高频操作,JavaScript 有大量的默认实现:比如

  1. Array.prototype.forEach:遍历数组
  2. NodeList.prototype.forEach:遍历dom,document.querySelectorAll
  3. for in
  4. for of

面试题

for infor of 的区别?
  1. for...in 语句以任意顺序迭代一个对象的除Symbol以外的可枚举属性,包括继承的可枚举属性。
    1. 对象默认的属性以及动态增加的属性都是可枚举属性
    2. 遍历出来的是属性名
    3. 继承而来的属性也会遍历(原型链上动态增加的也可以遍历)
  2. for...of语句可迭代对象(包括 ArrayMapSetStringTypedArrayarguments 对象等等)上创建一个迭代循环
    1. for of不会遍历继承而来的属性
    2. 遍历出来的是属性值
// for-in 遍历数组
Object.prototype.objFunc = function () { }
Array.prototype.arrFunc = 'arrFunc'

const foods = ['西瓜', '西葫芦', '西兰花']
for (const key in foods) {
  console.log('for-in:key', key)
}
// 打印结果:
// for-in:key 0
// for-in:key 1
// for-in:key 2
// for-in:key arrFunc
// for-in:key objFunc
// for-in 遍历对象
Object.prototype.objFunc = function () { }
Array.prototype.arrFunc = 'arrFunc'

const obj = {a: 1, b: 2}
for (const key in obj) {
  console.log('for-in:key', key)
}
// 打印结果:
// for-in:key a
// for-in:key b
// for-in:key objFunc
// for-of 遍历数组
Object.prototype.objFunc = function () { }
Array.prototype.arrFunc = 'arrFunc'

const foods = ['西瓜', '西葫芦', '西兰花']
for (const iterator of foods) {
  console.log('for-of:iterator', iterator)
}
// 打印结果:
// for-of:iterator 西瓜
// for-of:iterator 西葫芦
// for-of:iterator 西兰花

迭代协议

迭代协议并不是新的内置实现或语法,而是协议。这些协议可以被任何遵循某些约定的对象来实现。

迭代协议具体分为两个协议:可迭代协议迭代器协议

可迭代协议:要成为可迭代对象,该对象必须实现  [Symbol.iterator]()  方法,这意味着对象(或者它原型链上的某个对象)必须有一个键为 [Symbol.iterator] 的属性,可通过常量 Symbol.iterator 访问该属性

迭代协议可以定制对象的迭代行为,分为2个协议:

  1. 可迭代协议
    1. 给对象增加属方法 [Symbol.iterator](){}
    2. 返回一个符合迭代器协议的对象
  2. 迭代器协议
    1. next方法,返回对象:
      1. {done:true},迭代结束
      2. {done:false,value:'xx'},获取解析并接续迭代
      3. 实现方式:
        1. 手写
        2. Generator
/**
 * 迭代协议可以定制对象的迭代行为  分为2个协议:
 *  1. 可迭代协议: 增加方法[Symbol.iterator](){} 返回符合 迭代器协议 的对象
 *  2. 迭代器协议: 
 *      有next方法的对象,next方法返回:
 *        已结束: {done:true}
 *        继续迭代: {done:false,value:'x'}
 *    使用Generator
 *    自己实现 对象,next
 * */
const obj = {
  // Symbol.iterator 内置的常量
  // [属性名表达式]
  [Symbol.iterator]() {
  // ------------- 使用Generator -------------
      function* foodGenerator() {
        yield '西兰花'
        yield '花菜'
        yield '西兰花炒蛋'
      }
      const food = foodGenerator()
      return food
    }
}
for (const iterator of obj) {
  console.log('iterator:', iterator)
}
// 打印结果:
// iterator: 西兰花
// iterator: 花菜
// iterator: 西兰花炒蛋
const obj = {
  // Symbol.iterator 内置的常量
  // [属性名表达式]
  [Symbol.iterator]() {
    // ------------- 自己实现 -------------
    const arr = ['北京', '上海', '广州']
    let index = 0
    return {
      next() {
        if (index < arr.length) {
          // 可以继续迭代
          return { done: false, value: arr[index++] }
        }
        // 迭代完毕
        return { done: true }
      }
    }
  }
}
for (const iterator of obj) {
  console.log('iterator:', iterator)
}
// 打印结果:
// iterator: 北京
// iterator: 上海
// iterator: 广州

面试回答

  1. 迭代器模式在js中有大量的默认实现,因为遍历或者说迭代是日常开发中的高频操作,比如forEach,for in,for of

  2. for infor of的区别:

    1. for...in 语句以任意顺序迭代一个对象的除Symbol以外的可枚举属性,包括继承的可枚举属性。
      1. 对象默认的属性以及动态增加的属性都是可枚举属性
      2. 遍历出来的是属性名
      3. 继承而来的属性也会遍历
    2. for...of语句可迭代对象(包括 ArrayMapSetStringTypedArrayarguments 对象等等)上创建一个迭代循环
      1. for of不会遍历继承而来的属性
      2. 遍历出来的是属性值
  3. 如何自定义可迭代对象?

    1. 需要符合2个协议:可迭代协议和迭代器协议,其实就是按照语法要求实现功能而已
    2. 可迭代协议:传送门
      1. 给对象增加属方法 [Symbol.iterator](){}
      2. 返回一个符合迭代器协议的对象
    3. 迭代器协议:传送门
      1. 有next方法的一个对象,内部根据不同情况返回对应结果:
        1. {done:true},迭代结束
        2. {done:false,value:'xx'},获取解析并接续迭代
      2. 实现方式:
        1. 自己手写实现逻辑
        2. 直接返回一个Generator

6.8 参考资料

  1. 阮一峰-《ECMAScript 6 教程》
  2. 图灵社区-JavaScript高级程序设计