- Table of Contents
- 封装
- 可读性
- 正确性
- once函数封装
- throttle函数封装
- debounce函数封装
- 函数拦截器封装
- 纯函数
- 高阶函数的范式
《前端十日谈》笔记
Fri Oct 21 2022 Posted 2 years ago
代码的封装性、可读性和正确性 #
封装 #
控制红绿灯循环
const traffic = document.querySelector('.traffic')
// loop函数依赖于外部环境traffic
function loop() {
traffic.className = 'traffic pass'
setTimeout(() => {
traffic.className = 'traffic wait'
setTimeout(() => {
traffic.className = 'traffic stop'
setTimeout(loop, 3500)
}, 1500)
}, 5000)
}
loop()
数据抽象: 把数据定义并聚合成能被过程处理的对象,交由特定的过程处理,简单来说就是数据的结构化。
const traffic = document.querySelector('.traffic')
// signalLoop产生了副作用,因为它改变了subject的className
function signalLoop(subject, signals = []) {
const signalCount = signals.length
function updateState(i) {
const { signal, duration } = signals[i % signalCount]
subject.className = signal
setTimeout(updateState.bind(null, i + 1), duration)
}
updateState(0)
}
// 数据抽象
const signals = [
{ signal: 'traffic pass', duration: 5000 },
{ signal: 'traffic wait', duration: 3500 },
{ signal: 'traffic stop', duration: 1500 },
]
signalLoop(traffic, signals)
去除副作用
const traffic = document.querySelector('.traffic')
// 将原本改变外部变量的操作用回调的方法传给signalLoop
function signalLoop(subject, signals = [], onSignal) {
const signalCount = signals.length
function updateState(i) {
const { signal, duration } = signals[i % signalCount]
onSignal(subject, signal)
setTimeout(updateState.bind(null, i + 1), duration)
}
updateState(0)
}
const signals = [
{ signal: 'pass', duration: 5000 },
{ signal: 'wait', duration: 3500 },
{ signal: 'stop', duration: 1500 },
]
signalLoop(traffic, signals, (subject, signal) => {
subject.className = `traffic ${signal}`
})
可读性 #
使用Promise
封装setTimeout
function wait(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms)
})
}
const traffic = document.querySelector('.traffic');
// 简化第一版的loop方法
(async function () {
while (1) {
await wait(5000)
traffic.className = 'traffic wait'
await wait(1500)
traffic.className = 'traffic stop'
await wait(3500)
traffic.className = 'traffic pass'
}
}())
const traffic = document.querySelector('.traffic')
async function signalLoop(subject, signals = [], onSignal) {
const signalCount = signals.length
for (let i = 0; ;i++) {
const { signal, duration } = signals[i % signalCount]
await onSignal(subject, signal)
await wait(duration)
}
}
const signals = [
{ signal: 'pass', duration: 5000 },
{ signal: 'wait', duration: 3500 },
{ signal: 'stop', duration: 1500 },
]
signalLoop(traffic, signals, (subject, signal) => {
subject.className = `traffic ${signal}`
})
正确性 #
洗牌函数
function shuffle(items) {
return [...items].sort((a, b) => Math.random() > 0.5 ? -1 : 1)
}
const weights = Array(9).fill(0)
for (let i = 0; i < 10000; i++) {
const testItems = [1, 2, 3, 4, 5, 6, 7, 8, 9]
shuffle(testItems)
testItems.forEach((item, idx) => weights[idx] += item)
}
console.log(weights)
// [45071, 45016, 49406, 50455, 50727, 50205, 50981, 52346, 55793]
// 每次结果有变化,但总的来说前面的数字小,后面的数字大
改进:每次从数组中随机取一个元素并将它放到新的数组中。
function shuffle(items) {
items = [...items]
const ret = []
while (items.length) {
const idx = Math.floor(Math.random() * items.length)
const item = items.splice(idx, 1)[0]
ret.push(item)
}
return ret
}
let items = [1, 2, 3, 4, 5, 6, 7, 8, 9]
items = shuffle(items)
console.log(items)
改进:splice
的时间复杂度太高,改为每次从数组中随机取一个元素并将其与数组的第i个元素交换
function shuffle(items) {
items = [...items]
for (let i = items.length; i > 0; i--) {
const idx = Math.floor(Math.random() * i);
[items[idx], items[i - 1]] = [items[i - 1], items[idx]]
}
return items
}
let items = [1, 2, 3, 4, 5, 6, 7, 8, 9]
items = shuffle(items)
console.log(items)
改进:如果抽奖次数有限制,那么在抽到奖品后就不需要再进行随机
function *shuffle(items) {
items = [...items]
for (let i = items.length; i > 0; i--) {
const idx = Math.floor(Math.random() * i);
[items[idx], items[i - 1]] = [items[i - 1], items[idx]]
yield items[i - 1]
}
}
const items = [...new Array(100).keys()]
let n = 0
for (const item of shuffle(items)) {
console.log(item)
if (n++ >= 5)
break
}
过程抽象提升系统的可维护性 #
过程抽象 == 高阶函数抽取 ?
once函数封装 #
function once(fn) {
return function (...args) {
if (fn) {
const result = fn.apply(this, args)
fn = null
return result
}
}
}
once
函数可以被称为函数装饰器(高阶函数),即用函数来包装函数,并且不会改变原始函数,只拓展功能。
在原来的基础上可以对 once
进行拓展:当返回的函数被多次执行后调用另一个函数。
function once(fn, replacer = null) {
return function (...args) {
if (fn) {
const result = fn.apply(this, args)
fn = null
return result
}
if (replacer)
return replacer.apply(this, args)
}
}
const store = {
init: once(() => {
/** initial data **/
}, () => {
throw new Error('init should only be called once')
})
}
throttle函数封装 #
function throttle(fn, delay = 250) {
let timer = null
return function (...args) {
if (!timer) {
const result = fn.apply(this, args)
timer = setTimeout(() => {
timer = null
}, delay)
return result
}
}
}
节流是让事件处理函数隔一个指定毫秒再触发
使用 throttle
实现 once
function once(fn) {
return throttle(fn, Infinity)
}
debounce函数封装 #
function debounce(fn, delay = 250) {
let timer = null
return function (...args) {
if (timer)
clearTimeout(timer)
timer = setTimeout(() => {
return fn.apply(this, args)
}, delay)
}
}
防抖是忽略中间的操作,只响应用户最后一次操作
函数拦截器封装 #
function deprecate(fn, oldApi, newApi) {
const message = `The ${oldApi} is deprecated.Please use the ${newApi} instead.`
const notice = once(console.warn)
return function (...args) {
notice(message)
return fn.apply(this, args)
}
}
当我们想要修改函数库中的某个API,我们可以选择不修改代码本身,而是对这个API进行修饰,修饰的过程可以抽象为拦截它的输入或输出。
function intercept(fn, { beforeCall = null, afterCall = null }) {
return function (...args) {
if (!beforeCall || beforeCall.call(this, args) !== false) {
// do not excute fn if beforeCall() return false
const result = fn.apply(this, args)
if (afterCall)
afterCall.call(this, result)
return result
}
}
}
/** 校验参数,监测性能 **/
function sum(...list) {
return list.reduce((a, b) => a + b)
}
sum = intercept(sum, {
beforeCall(args) {
if (args.some(a => typeof a !== 'number')) {
console.log('sum() only recive Number')
return false
}
console.log(`The argument is ${args}`)
console.time('sum')
},
afterCall(ret) {
console.log(`The resulte is ${ret}`)
console.timeEnd('sum')
}
})
sum(1, 2, 3, 4, 5)
/** 调整参数顺序 **/
const mySetTimeout = intercept(setTimeout, {
beforeCall(args) {
[args[0], args[1]] = [args[1], args[0]]
}
})
mySetTimeout(1000, () => {
console.log('done')
})
纯函数 #
export function setStyle(el, key, value) {
el.style[key] = value
}
export function setStyles(els, key, value) {
els.forEach(el => setStyle(el, key, value))
}
上面两个函数的缺点:依赖外部环境(el元素),并且还会改变这个环境。
带来的问题:提高了测试成本。因为为了测试以上两个函数,需要构建不同的DOM元素结构,然后再去获取元素或元素列表,最后再根据执行函数后的DOM元素所呈现的效果来判断函数是否符合预期。
什么纯函数?:具有确定性、无副作用、幂等的特点的函数。也就是说,纯函数不依赖外部环境,也不改变外部环境,不管调用几次,不管什么时候调用,只要参数确定,返回值就确定。
function batch(fn) {
return function (subject, ...args) {
if (Array.isArray(subject)) {
return subject.map((s) => {
return fn.call(this, s, ...args)
})
}
else {
return fn.call(this, subject, ...args)
}
}
}
export const setStyle = batch((el, key, value) => {
el.style[key] = value
})
高阶函数的范式 #
// High Order Function
function HOF0(fn) {
return function (...args) {
return fn.apply(this, args)
}
}
HOF0
是高阶函数的等价范式,或者说,HOF0
修饰的函数功能和原函数fn
的功能完全相同。因为被修饰后的函数就只是采用调用的this
上下文和参数来调用fn
,并将结果返回。也就是说,执行它和直接执行fn
完全没区别。其他的函数装饰器就是在HOF0
的基础上,要么对参数进行修改,如batch
,要么对返回结果进行修改,如once
、throttle
、debounce
和batch
。
在范式的基础上可以设计出其他的高阶函数:
function continous(reducer) {
return function (...args) {
return args.reduce((a, b) => reducer(a, b))
}
}
const add = continous((a, b) => a + b)
console.log(add(1, 2, 3, 4)) // 1 + 2 + 3 + 4 = 10
与batch
类似,continous
也可以用来创建批量操作元素的方法,只不过参数和用法需要调整一下。如下代码所示:
const setStyle = continous(([key, value], el) => {
el.style[key] = value
return [key, value]
})
const list = document.querySelectorAll('li:nth-child(2n+1)')
setStyle(['color', 'red'], ...list)
注意到因为continous
是递归迭代执行,我们要把list
展开传入setStyle
。
如果我们想要直接使用list
作为参数而不是传...list
,我们可以再实现一个高阶函数来处理它:
function fold(fn) {
return function (...args) {
const lastArg = args[args.length - 1]
if (lastArg.length)
return fn.call(this, ...args.slice(0, -1), ...lastArg)
return fn.call(this, ...args)
}
}
const setStyle = fold(continous(([key, value], el) => {
el.style[key] = value
return [key, value]
}))
const list = document.querySelectorAll('li:nth-child(2n+1)')
setStyle(['color', 'red'], list)
fold
函数判断最后一个参数是一个数组或类数组(如NodeList),那么将它展开传给原函数fn
(相对于被修饰的原函数而言是折叠了参数,所以用fold
命名这个高阶函数)。
接下来,我们可以调整一下参数顺序,让setStyle更接近batch那一版:
function reverse(fn) {
return function (...args) {
return fn.apply(this, args.reverse)
}
}
const setStyle = reverse(fold(continous(([key, value], el) => {
el.style[key] = value
return [key, value]
})))
const list = document.querySelectorAll('li:nth-child(2n+1)')
setStyle(list, ['color', 'red'])
然后,我们可以把参数['color', 'red']
展开,所有我们需要实现一个与fold
相反的spread
高阶函数:
function spread(fn) {
return function (first, ...rest) {
return fn.call(this, first, rest)
}
}
const setStyle = spread(reverse(fold(continous(([key, value], el) => {
el.style[key] = value
return [key, value]
}))))
const list = document.querySelectorAll('li:nth-child(2n+1)')
setStyle(list, 'color', 'red')
所以最终我们得到了和上一个故事一样的效果的setStyle
方法:
function batch(fn) {
return spread(reverse(fold(continous(fn))))
}
const setStyle = batch(setStyle)
另外,像这样spread(reverse(fold(continous...)))
嵌套的写法,我们也可以用高阶函数改变成更加友好的形式:
function pipe(...fns) {
return function (input) {
return fns.reduce((a, b) => {
return b.call(this, a)
}, input)
}
}
const double = x => x * 2
const pow2 = x => x ** 2
const half = x => x / 2
const cacl = pipe(double, pow2, half)
const result = cacl(10) // (10 * 2) ** 2 / 2 = 200
pipe
本身也可以用高阶函数continous
重新定义为:
const pipe = continous((prev, next) => {
return function (input) {
return next.call(this, prev.call(this, input))
}
})