1.函数柯里化
1.1 概念
函数柯里化:在计算机科学中,柯里化(英语:Currying),又译为卡瑞化或加里化,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。
柯里化 作为一种高阶技术, 可以提升函数的复用性和灵活性。
函数柯里化 (Currying) 是一种将多个参数的函数转换为单个参数函数的技术。
转换完毕之后的函数只传递函数的一部分参数来调用,并且返回一个新的函数去处理剩下的参数。
例子:
// 调整函数 sum
function sum(num1, num2) {
return num1 + num2
}
// 改写为 可以实现如下效果
console.log(sum(1)(2))//
// 1. sum改为接收一个参数,返回一个新函数
// 2. 新函数内部将参数1,参数2累加并返回
function sum(num1) {
return function (num2) {
return num1 + num2
}
}
1.2 求和函数
1.2.1 全局变量
需求:
function sum(a, b, c, d, e) {
return a + b + c + d + e
}
// 改写函数sum实现:参数传递到5个即可实现累加
// sum(1)(2)(3)(4)(5)
// sum(1)(2,3)(4)(5)
// sum(1)(2,3,4)(5)
// sum(1)(2,3)(4,5)
核心步骤
- 定义数组保存参数
- 函数接收不定长参数
- 调用时将传入的参数,添加到数组中,并判断数组长度:
- 满足长度要求:累加并返回结果
- 未达到长度要求:继续返回函数本身
let nums = []
function currySum(...args) {
nums.push(...args)
if (nums.length >= 5) {
let res = nums.slice(0, 5).reduce((prev, curv) => prev + curv, 0)
nums = []
return res
} else {
return currySum
}
}
1.2.1 使用闭包
需求:
- 使用闭包将上一节代码中的全局变量,保护起来
- 支持自定义累加的参数个数
闭包:有权访问另一个函数作用域中的变量的函数;一般情况就是在一个函数中包含另一个函数。
function sumMaker(length){
// 逻辑略
}
// 支持5个累加
const sum5 = sumMaker(5)
// 支持7个累加
const sum7 = sumMaker(7)
sum7(1,2,3)(4,5,6,7)
核心步骤:
- 定义函数,接收参数,用来确定参数个数
- 内部将上一节的逻辑拷贝进去
- 返回原函数
- 通过这样的调整,可以让自定义参数的个数,并且没有上一节的全局变量数组
function sumMaker(length) {
let nums = []
function inner(...args) {
nums.push(...args)
if (nums.length >= length) {
let res = nums.slice(0, length).reduce((prev, curv) => prev + curv, 0)
nums = []
return res
} else {
return inner
}
}
return inner
}
1.3 类型判断
需求:
将下列4个类型判断函数,改写为通过函数typeOfTest
动态生成
// 有如下4个函数
function isUndefined(thing) {
return typeof thing === 'undefined'
}
function isNumber(thing) {
return typeof thing === 'number'
}
function isString(thing) {
return typeof thing === 'string'
}
function isFunction(thing) {
return typeof thing === 'function'
}
// 改为通过 typeOfTest 生成:
const typeOfTest =function(){
// 参数 和 逻辑略
}
const isUndefined = typeOfTest('undefined')
const isNumber = typeOfTest('number')
const isString = typeOfTest('string')
const isFunction = typeOfTest('function')
// 可以通过 isUndefined,isNumber,isString,isFunction 来判断类型:
isUndefined(undefined) // true
isNumber('123') // false
isString('memeda') // true
isFunction(() => { }) // true
核心步骤:
- 定义函数,接收需要判断的类型名
- 内部返回一个新的函数
- 新函数接收需要判断的具体的值
- 新函数内部根据外层函数传入的类型,以及传入的值进行判断并返回结果
const typeOfTest = (type) => {
return (thing) => {
return typeof thing === type
}
}
1.4 封装axios请求
需求:
将如下请求函数,变为通过axiosPost
函数动态生成
// 项目开发中不少请求的 请求方法 是相同的,比如
axios({
url: 'url',
method: 'get'
})
axios({
url: 'url',
method: 'get',
params: {
//
}
})
axios({
url: 'url',
method: 'post',
data: ''
})
axios({
url: 'url',
method: 'post',
data: '',
headers: {
}
})
// 固定请求参数,请求方法固定,其他参数从外部传递进来
requestWithMethod('get')({
url: '',
params: {},
headers: {}
})
requestWithMethod('post')({
url: '',
headers: {},
data: {}
})
核心步骤:
- 定义函数,接收请求类型
- 函数内部调用
axios
发请求
function requestWithMethod(method) {
return (config) => {
return axios({
method,
...config
})
}
}
函数柯里化是一种函数式编程思想:将多个参数的函数转换为单个参数函数,调用时返回新的函数接收剩余参数
2.js设计模式
设计模式的指的是:在面向对象软件设计过程中针对特定问题的简洁而优雅的解决方案。通俗一点说,设计模式就是给面向对象软件开发中的一些好的设计取个名字。
目前说到设计模式,一般指的是《设计模式:可复用面向对象软件的基础》一书中提到的23种常见的软件开发设计模式,JavaScript中不需要生搬硬套这些模式。
2.1 工厂模式
2.1.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)
2.1.2 应用场景
- vue3中创建实例的api改为createApp,vue2中是new Vue。 Vue3迁移指南
- vue3中,没有影响所有Vue实例的api了,全都变成了影响某个app对象的api,比如Vue.component–>app.component
- axios.create基于传入的配置,创建一个新的请求对象,可以用来设置多个基地址。axios实例
// 1. 基于不同基地址创建多个 请求对象 const request1 = axios.create({ baseURL: "基地址1" }) const request2 = axios.create({ baseURL: "基地址2" }) // 2. 通过对应的请求对象,调用接口即可 request1({ url: '基地址1的接口' }) request2({ url: '基地址2的接口' })
2.2 单例模式
2.2.1 概念
单例模式指的是,在使用这个模式时,单例对象整个系统需要保证只有一个存在。
举例:
通过静态方法getInstance获取唯一实例
const s1 = SingleTon.getInstance()
const s2 = SingleTon.getInstance()
console.log(s1===s2)//true
核心步骤:
- 定义类
- 私有静态属性:#instance
- 提供静态方法getInstance:
- 调用时判断#instance是否存在:
- 存在:直接返回
- 不存在:实例化,保存,并返回
class SingleTon {
constructor() { }
// 私有属性,保存唯一实例
static #instance
// 获取单例的方法
static getInstance() {
if (SingleTon.#instance === undefined) {
// 内部可以调用构造函数
SingleTon.#instance = new SingleTon()
}
return SingleTon.#instance
}
}
2.2.2 实际应用
-
vant组件库中的弹框组件,保证弹框是单例。
多次弹框,不会创建多个弹框,复用唯一的弹框对象 -
vue中注册插件,用到了单例的思想(只能注册一次)
判断插件是否已经注册,已注册,直接提示用户
2.3 观察者模式
在对象之间定义一个一对多的依赖,当一个对象状态改变的时候,所有依赖的对象都会自动收到通知。
举例:
dom
事件绑定
window.addEventListener('load', () => {
console.log('load触发1')
})
window.addEventListener('load', () => {
console.log('load触发2')
})
window.addEventListener('load', () => {
console.log('load触发3')
})
- Vue中的
watch
:
watch: {
num(newVal, oldVal) {
console.log('oldVal:',oldVal)
console.log('newVal:',newVal)
}
}
2.4 发布订阅模式
发布订阅模式可以实现的效果类似观察者模式,但是两者略有差异:一个有中间商(发布订阅模式),一个没中间商(观察者模式)。
应用
实现事件总线的核心逻辑:
- 添加类,内部定义私有属性
#handlers={}
,以对象的形式来保存回调函数 - 添加实例方法:
$on
:- 接收事件名和回调函数
- 内部判断并将回调函数保存到
#handlers
中,以{事件名:[回调函数1,回调函数2]}
格式保存
$emit
- 接收事件名和回调函数参数
- 内部通过
#handlers
获取保存的回调函数,如果获取不到设置为空数组[]
- 然后挨个调用回调函数即可
$off
- 接收事件名
- 将
#handlers
中事件名对应的值设置为undefined
即可
$once
- 接收事件名和回调函数
- 内部通过
$on
注册回调函数, - 内部调用
callback
并通过$off
移除注册的事件
const bus = new MyEmitter()
// 注册事件
bus.$on('事件名1',回调函数)
bus.$on('事件名1',回调函数)
// 触发事件
bus.$emit('事件名',参数1,...,参数n)
// 移除事件
bus.$off('事件名')
// 一次性事件
bus.$once('事件名',回调函数)
class MyEmmiter{
#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)
})
}
}
2.5 原型模式
在原型模式下,当我们想要创建一个对象时,会先找到一个对象作为原型,然后通过克隆原型的方式来创建出一个与原型一样(共享一套数据/方法)的对象。在JavaScript
中,Object.create
就是实现原型模式的内置api
。
const student= {
name: 'xdk'
}
const nstudent = Object.create(student)
cosole.log(nstudent === student) // false
应用
vue2
中重写数组方法:
调用方法时(push
,pop
,shift
,unshift
,splice
,sort
,reverse
)可以触发视图更新:官方文档,源代码
2.6 代理模式
代理模式指的是拦截和控制与目标对象的交互,核心是,通过一个代理对象拦截对原对象的直接操纵。
应用:缓存代理
核心语法:
- 创建对象缓存数据
- 拦截获取数据的请求:
- 已有缓存:直接返回缓存数据
- 没有缓存:去服务器获取数据并缓存
// 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]
}
2.7 迭代器模式
2.7.1 概念
迭代器模式提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露该对象的内部表示。简而言之就是:遍历
遍历作为日常开发中的高频操作,JavaScript中有大量的默认实现:比如
Array.prototype.forEach
:遍历数组NodeList.prototype.forEach
:遍历dom,document.querySelectorAllfor in
for of
2.7.2 for in 和for of 区别
Object.prototype.objFunc = function () { }
Array.prototype.arrFunc = 'arrFunc'
const foods = ['土豆', '番茄', '黄瓜']
for (const key in foods) {
console.log('key', key)
}
// key 0
// key 1
// key 2
// key arrFunc
// key objFunc
for (const iterator of foods) {
console.log('iterator', iterator)
}
// iterator 土豆
// iterator 番茄
// iterator 黄瓜
2.7.3 可迭代协议和迭代器协议
如何自定义可迭代对象?
需要符合2个协议:可迭代协议和迭代器协议,其实就是按照语法要求实现功能而已。
- 可迭代协议:传送门
- 给对象增加方法
[Symbol.iterator](){}
- 返回一个符合迭代器协议的对象
- 给对象增加方法
- 迭代器协议:传送门
- next方法,返回对象:
{done:true}
,迭代结束{done:false,value:'xx'}
,获取解析并接续迭代
- next方法,返回对象:
- 实现方式
1. 手写
2.Generator
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 }
}
}
// ------------- 使用Generator -------------
// function* cityGenerator() {
// yield '北京'
// yield '上海'
// yield '广州'
// }
// const city= cityGenerator()
// return city
}
}
for (const iterator of obj) {
console.log('iterator:', iterator)
}
3 防抖
3.1 概念
常见的前端性能优化方案,可以防止JS高频渲染页面时出现的视觉抖动(卡顿):比如
- 示例1:页面改变尺寸时,同步调整图表的大小
- 示例2:输入内容时,结合ajax进行搜索并渲染结果
如果内容的渲染速度过快,都可能会造成抖动效果,并且连带会浪费性能。
3. 频繁执行逻辑代码,耗费浏览器性能
4. 频繁发送请求去服务器,耗费服务器性能
适用场景:
- 在触发频率高的事件中
- 频率高的事件: resize、input 、scroll 、keyup….
- 执行耗费性能操作
- 耗费性能的操作:操纵页面、网络请求….
- 需要实现的效果:连续操作之后只有最后一次生效
这个时候就可以适用防抖来进行优化
3.2 实现防抖
防抖优化之后的效果可以通过一些具体的网站来进行确认,比如12306,他就是通过防抖进行的优化:
- 在输入内容的时候没有发送请求
- 输入完毕之后,稍等一会才发送请求去服务器
这就是防抖的效果: 连续事件停止触发后,一段时间内没有再次触发,就执行业务代码。
核心步骤
- 开启定时器,保存定时器id
- 清除已开启的定时器
输入框+搜索 例子优化代码如下:
let timeId
document.querySelector('.search-city').addEventListener('input', function () {
// 2. 清除已开启的定时器
clearTimeout(timeId)
// 1. 开启定时器,保存定时器id
timeId = setTimeout(() => {
renderCity(this.value)
}, 500)
})
3.3 lodash的debounce方法
实际开发中一般不需要手写防抖,因为已经有库里面提供了对应的方法,可以直接调用,也可以自己手写实现debounce
。
3.3.1 debounce 方法
lodash
工具库中的debounce
方法 官方文档
_.debounce(func, [wait=0], [options=])
参数
func
(Function):要防抖动的函数。[wait=0]
(number):需要延迟的毫秒数。[options=]
(Object):选项对象。[options.leading=false]
(boolean):指定在延迟开始前调用。[options.maxWait]
(number):设置func
允许被延迟的最大值。[options.trailing=true]
(boolean):指定在延迟结束后调用。
返回值
(Function):返回新的 debounced(防抖动)函数。
注意:
- 实际开发时一般给前2个参数即可,然后适用返回的函数替换原函数即可
- 项目中如果有
lodash
那么直接使用它提供的debounce
即可,不仅可以实现防抖,原函数中的this
和参数
均可以正常使用
3.3.2 debounce 实现原理
手写实现debounce
函数,实现lodash
中debounce
方法的核心功能
需求:
- 参数:
func
(Function): 要防抖动的函数。[wait=0]
(number): 需要延迟的毫秒数。
- 返回值:
- (Function):返回新的 debounced(防抖动)函数。
核心步骤
- 返回防抖动的新函数
- 原函数中的
this
可以正常使用 - 原函数中的参数可以正常使用
function debounce(func, wait = 0) {
let timeId
// 防抖动的新函数
return function (...args) {
let _this = this
clearTimeout(timeId)
timeId = setTimeout(function () {
// 通过apply调用原函数,并指定this和参数
func.apply(_this, args)
}, wait)
}
}
4 节流
4.1 概念
常见的前端性能优化方案,它可以防止高频触发事件造成的性能浪费。
比如:播放视频时同步缓存播放时间,如果要多设备同步,还需要通过ajax
提交到服务器
高频触发耗费性能的操作,会造成性能浪费。
适用场景:在触发频率高的事件中,执行耗费性能操作,连续触发,单位时间内只有一次生效。
优化之前: 每当触发事件就会执行业务逻辑
优化之后: 触发事件之后延迟执行逻辑,在逻辑执行完毕之后无法再次触发
4.2 实现节流
使用节流将播放器记录时间的例子优化:
核心步骤:
- 开启定时器,并保存 id
- 判断是否已开启定时器
- 定时器执行时,id设置为空
// 播放器案例优化之后代码
let timeId
video.addEventListener('timeupdate', function () {
if (timeId !== undefined) {
return
}
timeId = setTimeout(() => {
console.log('timeupdate触发')
localStorage.setItem('currentTime', this.currentTime)
timeId = undefined
}, 3000)
})
4.3 lodash的throttle方法
实际开发中一般不需要手写节流,因为已经有库里面提供了对应的方法,可以直接调用,也可以自己手写实现throttle
。
4.3.1 throttle 方法
lodash
工具库中的throttle
方法。官方文档
_.throttle(func, [wait=0], [options=])
参数
func
(Function):要节流的函数。[wait=0]
(number):需要节流的毫秒。[options=]
(Object):选项对象。[options.leading=true]
(boolean):指定调用在节流开始前。[options.trailing=true]
(boolean):指定调用在节流结束后。
返回值
(Function):返回节流的函数。
注意
- 实际开发时一般会给3个参数,然后使用返回的函数替换原函数即可
- 参数3:
options.leading=true
默认为true
,开始时触发节流函数,一般设置为false
- 参数3:
- 项目中如果有
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)
throttle 实现原理
手写实现throttle
函数,实现lodash
中throttle
方法的核心功能
需求:
- 参数:
func
(Function): 要节流的函数。[wait=0]
(number): 需要节流的毫秒。
- 返回值:
- (Function):返回节流的函数
核心步骤
- 返回节流的新函数
- 原函数中的
this
可以正常使用 - 原函数中的参数可以正常使用
// 节流工具函数
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)
}
}