深拷贝
普通递归实现对象的深拷贝,很多同学都能写出来,或者使用JSON的parse和stringlfy来实现 但会存在一系列问题,所以有以下改进版本;
- 针对能够遍历对象的不可枚举属性以及 Symbol 类型,我们可以使用 Reflect.ownKeys 方法;
- 当参数为 Date、RegExp 类型,则直接生成一个新的实例返回;
- 利用 Object 的 getOwnPropertyDescriptors 方法可以获得对象的所有属性,以及对应的特性,顺便结合 Object 的 create 方法创建一个新对象,并继承传入原对象的隐式原型;
- 利用 WeakMap 类型作为 Hash 表,因为 WeakMap 是弱引用类型,可以有效防止内存泄漏(你可以关注一下 Map 和 weakMap 的关键区别,这里要用 weakMap),作为检测循环引用很有帮助,如果存在循环,则引用直接返回 WeakMap 存储的值。
- 为什么使用Reflect.ownKeys? 因为以下方法中 只有Reflect.ownKeys可满足需求
- for in 遍历对象自身和原型链上所有可枚举的属性
- Object.keys() 返回一个对象自身可枚举属性的数组(不包括原型链上的)
- Object.getOwnPerprotyNames() 返回一个对象自身所有属性的数组(包括不可枚举,但不包括原型链上的属性)
- Object.getOwnPerprotySymbol() 返回一个对象自身所有的symbol属性的数组
- Reflect.ownKeys() = Object.getOwnPerprotyNames() + Object.getOwnPerprotySymbol()
- Object.getOwnPerprotyDescriptors() 获取对象上所有属性的描述符(包括不可枚举,但不包括原型链上的属性)
const isComplexDataType = obj => (typeof obj === 'object' || typeof obj === 'function') && (obj !== null)
const deepClone = function (obj, hash = new WeakMap()) {
if (obj.constructor === Date)
return new Date(obj) // 日期对象直接返回一个新的日期对象
if (obj.constructor === RegExp)
return new RegExp(obj) //正则对象直接返回一个新的正则对象
//如果循环引用了就用 weakMap 来解决
if (hash.has(obj)) return hash.get(obj)
// 传入参数所有键的特性
let allDesc = Object.getOwnPropertyDescriptors(obj)
// 继承原型链
let cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc)
hash.set(obj, cloneObj)
for (let key of Reflect.ownKeys(obj)) {
cloneObj[key] = (isComplexDataType(obj[key]) && typeof obj[key] !== 'function') ? deepClone(obj[key], hash) : obj[key]
}
return cloneObj
}
// 下面是验证代码
let obj = {
num: 0,
str: '',
boolean: true,
unf: undefined,
nul: null,
obj: { name: '我是一个对象', id: 1 },
arr: [0, 1, 2],
func: function () { console.log('我是一个函数') },
date: new Date(0),
reg: new RegExp('/我是一个正则/ig'),
[Symbol('1')]: 1,
};
Object.defineProperty(obj, 'innumerable', {
enumerable: false, value: '不可枚举属性' }
);
obj = Object.create(obj, Object.getOwnPropertyDescriptors(obj))
obj.loop = obj // 设置loop成循环引用的属性
let cloneObj = deepClone(obj)
cloneObj.arr.push(4)
console.log('obj', obj)
console.log('cloneObj', cloneObj)
防抖
应用场景:
- 输入框频繁输入内容,搜索或者提交信息
- 频繁点击按钮
- 监听浏览器滚动事件,完全某些特定操作
- 用户缩放浏览器的resize事件
function debounce(fn, delay, immediate = false, resultCallback) {
// 定义一个定时器,保存上一次的定时器
let timer = null
// 定义一个变量 保存是否执行过fn
let isInvoke = false
function _debounce(...args) {
// 触发时间先判断闭包内的timer是否有值,如果有值则清楚它 这里算是实现防抖的核心了
if (timer) clearTimeout(timer)
// 判断是否需要首次执行
if (immediate && !isInvoke) {
// 进入函数代表用户需要首次执行,并且isInvoke为false表示没有执行过
const result = fn.apply(this, args)
typeof resultCallback === 'function' ? resultCallback(result) : void 0
// 首次执行完,将变量改为true
isInvoke = true
} else {
// 进入函数代表 非首次执行了
timer = setTimeout(() => {
const result = fn.apply(this, args)
typeof resultCallback === 'function' ? resultCallback(result) : void 0
// 当成功执行一次就讲isInvoke设为false,这里如果不更改状态,那么程序永远都只有第一次执行才算首次执行了
// 而我们实际是希望 每一次防抖就要重新计算一次
// 所以在成功执行了函数后,下次再执行也能首次立即执行
isInvoke = false
}, delay);
}
}
// 定义一个方法 取消当前操作
_debounce.cancel = function () {
if (timer) clearTimeout(timer)
timer = null
isInvoke = false
}
return _debounce
}
节流
- 监听页面的滚动事件
- 鼠标移动事件
- 用户频繁点击按钮
- 游戏中的一些设计
function throttle (fn, interval, options = {leading:false, trailing:true}) {
const { leading, trailing, resultCallback } = options
// 定义一个变量 记录上一次的时间
let lastTime = 0
let timer = null
function _throttle (...args) {
// 每次执行函数都获取当前时间
let nowTime = new Date().getTime()
// 如果不需要首次执行,一定要在lastTime为0时才将lastTime等于当前时间
// 如果lastTime不为0时也去赋值,那lastTime就永远都等于nowTime了 不会fn执行函数了
if (!leading && lastTime === 0) lastTime = nowTime
// 定义变量得出剩余时间 公式: 剩余时间 = 间隔时间 - (当前时间 - 上一次时间)
let remainTime = interval - (nowTime - lastTime)
// 如果剩余时间<=0
if (remainTime <= 0) {
// 这里清空是因为,如果程序需要最后一次也执行的时候timer就会有值,如果不把它清空了
// trailing会执行一次,我们本身的剩余时间<=0也会执行一次
if (timer) clearTimeout(timer)
const result = fn.apply(this, args)
typeof resultCallback === 'function' ? resultCallback(result) : void 0
// 执行完fn函数后将上一次时间设置为当前时间
lastTime = nowTime
// 这里加个return 中断后续执行,如果不中断会接着执行后续代码添加定时器
return
}
// 如果需要最后一次执行,并且没有设置过定时器
if (trailing && !timer) {
// 用剩余时间作为最后一次执行的间隔
timer = setTimeout(() => {
// 执行函数就把timer清空了,不然我们下次执行就不会再触发最后一次执行了
timer = null
// lastTime赋值这里很特别 当本次fn执行完毕后,如果我们是需要首次执行 那么我们需要把lastTime设置成当前时间
// 这样只要下次再执行超过了interval就会首次执行了
// 如果不需要首次执行就设置为0 因为当lastTime==0时 我们会将它等于nowTime
lastTime = leading ? new Date().getTime() : 0
const result = fn.apply(this, args)
typeof resultCallback === 'function' ? resultCallback(result) : void 0
}, remainTime);
}
}
_throttle.cancel = function () {
// 如果有timer就清空它
if(timer) clearTimeout(timer)
timer = null
lastTime = 0
}
return _throttle
}