Skip to content
📖预计阅读时长:0 分钟字数:0

1. 防抖 debounce

推导链

Q1: 防抖是什么?
→ 事件触发后等待 n 秒才执行,期间再触发就重新计时
→ 场景:搜索框输入、窗口 resize

Q2: 核心逻辑?
→ 每次触发先清除上一个定时器,再设置新定时器
→ 闭包保存 timer

Q3: 立即执行版?
→ 第一次立即执行,后续等待
→ 用 immediate 参数控制

Q4: Hooks 版怎么写?
→ useRef 保存 timer(跨渲染保持引用)
→ useEffect 监听值变化

变体速查

形态核心
JS 基础版clearTimeout + setTimeout
JS 立即执行版immediate 参数,首次立即执行
Hooks useDebounce返回防抖后的值
Hooks useDebounceFn返回防抖后的函数

代码

javascript
// JS 版(支持 immediate)
function debounce(fn, immediate, delay) {
	let timerId = null
	
	return function(...args) {
		// 立即执行:首次且没有定时器时执行
		if(immediate && !timerId) {
			fn.apply(this, args)
		}
		
		if(timerId) {
			clearTimerout(timerId)
			timerId = null
		}
		
		timerId = setTimeout(() => {
			if(!immediate) {
				fn.apply(this, args)
			}
			timerId = null
		}, delay)
	}
}

// 测试
const log = debounce(() => console.log('debounced'), false, 300)
log(); log(); log()  // 只输出一次
javascript
// Hooks: useDebounce (防抖值)
import { useState, useEffect } from 'react'

function useDebounce<T> (value: T, delay: number): T {
	const [debouncedValue, setDebouncedValue] = useState(value)
	
	useEffect(() => {
		const timer = setTimeout(() => setDebouncedValue(value), delay)
		return () => clearTimeout(timer)
	}, [value, delay])
	
	return debouncedValue
}

// Hooks: useDeounceFn (防抖函数)
import { useRef, useCallback } from 'react'

function useDebounceFn<T extends (...args: any[]) => any>(fn: T, delay: number) {
	const timerRef = useRef<NodeJS.Timeout | null> (null)
	
	return useCallback((...args: Parameters<T>) => {
		if(timerRef.current) clearTimeout(timerRef.current)
		timerRef.current = setTimeout(() => fn(...args), delay)
	}, [fn, delay])
}

2. 节流 throttle

推导链

Q1: 节流是什么?
→ 单位时间内只执行一次,不管触发多少次
→ 场景:滚动事件、按钮防重复点击

Q2: 组合版怎么实现?
→ 时间戳 + 定时器
→ 首次立即执行,停止后还执行最后一次

Q3: Hooks 版?
→ useRef 保存 lastTime

变体速查

形态特点
JS 时间戳版首次立即执行,停止后不再执行
JS 组合版首次立即执行 + 停止后还执行一次(最常用)
Hooks useThrottleFn返回节流后的函数

代码

javascript
// JS组合版 (首次立即执行 + 停止后还执行一次)
function throttle(fn, option, delay) {
	const { immediate = true, tailing = true } = option
	let lastTime = 0
	let timerId = null
	
	return function(...args) {
		const now = Date.now()
		if(!immediate && lastTime === 0) {
			lastTime = now
		}
		
		const remaining = delay - (now - lastTime)
		
		if(remaining <= 0) {
			if(timerId) {
				clearTimeout(timerId)
				timerId = null
			}
			fn.apply(this, args)
			lastTime = now
		} else if (!timerId && tailing) {
			timerId = setTimeout(() => {
				fn.apply(this, args)
				lastTime = immediate ? Date.now() : 0
				timerId = null
			}, remaining)
		}
	}
}

// 测试
const log = throttle(() => console.log('throttled', Date.now()), {}, 1000)
setInterval(log, 100)  // 首次立即执行,之后每秒执行一次
javascript
// Hooks: useThrottleFn
import { useRef, useCallback } from 'react'

function useThrottleFn<T extends (...args: any[]) => any>(fn: T, delay: number) {
  const lastTimeRef = useRef(0)
  
  return useCallback((...args: Parameters<T>) => {
    const now = Date.now()
    if (now - lastTimeRef.current >= delay) {
      lastTimeRef.current = now
      fn(...args)
    }
  }, [fn, delay])
}

// Hooks: useThrottle(节流值)
import { useState, useEffect, useRef } from 'react'

function useThrottle<T>(value: T, delay: number): T {
  const [throttledValue, setThrottledValue] = useState(value)
  const lastTimeRef = useRef(Date.now())
  
  useEffect(() => {
    const now = Date.now()
    if (now - lastTimeRef.current >= delay) {
      lastTimeRef.current = now
      setThrottledValue(value)
    } else {
      const timer = setTimeout(() => {
        lastTimeRef.current = Date.now()
        setThrottledValue(value)
      }, delay - (now - lastTimeRef.current))
      return () => clearTimeout(timer)
    }
  }, [value, delay])
  
  return throttledValue
}

3. 柯里化 curry

推导链

Q1: 柯里化是什么?
→ 把多参数函数转成单参数函数链
fn(a, b, c)fn(a)(b)(c)

Q2: 怎么知道参数收集够了?
→ 比较 fn.length(形参个数)和已收集参数个数
→ 够了就执行,不够就返回新函数继续收集

Q3: 占位符版?
→ 用特殊值(如 _)表示跳过某个参数
→ 后续调用时填充占位符位置

代码

javascript
// 定长版 (根据fn.length 判断 )
function curry(fn) {
	return function curried(...args) {
		if(args.length >= fn.length) {
			return fn.apply(this, args)
		}
		return (...nextArgs) => curried(...args, ...nextArgs)
	}
}

// 测试
const add = (a, b, c) => a + b + c
const curriedAdd = curry(add)
console.log(curriedAdd(1)(2)(3))    // 6
console.log(curriedAdd(1, 2)(3))    // 6
console.log(curriedAdd(1)(2, 3))    // 6

// curry Add
function curryVariadic(fn) {
	function next(prevArgs) {
		return function (...newArgs) {
			// 空调用 触发执行
			if(newArgs.length === 0) {
				return fn.apply(this, prevArgs)
			}
			return next([...prevArgs, ...newArgs])
		}
	}
	return next([])
}
// 测试:非定长求和
const sum = (...nums) => nums.reduce((acc, n)
=> acc + n, 0)
const curriedSum = curryVariadic(sum)
console.log(curriedSum(1)(2)(3)())      // 6
console.log(curriedSum(1, 2)(3, 4)())   // 10
console.log(curriedSum(5, 6, 7)())      // 18

4. compose / pipe

推导链

Q1: compose 是什么?
→ 函数组合,从右到左执行
compose(f, g, h)(x) = f(g(h(x)))

Q2: pipe 是什么?
→ 和 compose 相反,从左到右执行
pipe(f, g, h)(x) = h(g(f(x)))

Q3: 怎么实现?
→ reduce/reduceRight 串联函数

javascript
// compose:从右到左
const compose = (...fns) => (x) => fns.reduceRight((acc, fn) => fn(acc), x)

// pipe:从左到右
const pipe = (...fns) => (x) => fns.reduce((acc, fn) => fn(acc), x)

// 测试
const add1 = x => x + 1
const mul2 = x => x * 2
const square = x => x * x

console.log(compose(add1, mul2, square)(2))  // (2^2 * 2) + 1 = 9
console.log(pipe(square, mul2, add1)(2))     // (2^2 * 2) + 1 = 9

5. call / apply / bind

推导链

Q1: call/apply/bind 干嘛?
→ 改变函数的 this 指向
→ call 逐个传参,apply 数组传参,bind 返回新函数

Q2: 怎么实现改变 this?
→ 把函数挂到目标对象上,用对象调用
→ 用 Symbol 避免属性冲突,调用完删掉

Q3: bind 的特殊之处?
→ 返回新函数,支持分批传参
→ 作为构造函数时 this 指向实例

代码

javascript
// call
Function.prototype.myCall = function(context, ...args) {
    context = context ?? globalThis
    const symbolFn = Symbol('fn')
    context[symbolFn] = this
    const result = context[symbolFn](...args)
    delete context[symbolFn]
    return result
}

// apply(和 call 只是参数形式不同)
Function.prototype.myApply = function(context, args) {
    if (!args) {
        args = []
    } else if (!isArrayLike(args)) {
        throw new TypeError('')
    }
    context = context ?? globalThis
    args = Array.from(args)
    const symbolFn = Symbol('fn')
    context[symbolFn] = this
    const result = context[symbolFn](...args)
    delete context[symbolFn]
    return result
}

function isArrayLike(args) {
    return args !== null
    && typeof args !== 'function'
    && typeof args.length === 'number'
    && args.length >= 0
    && args.length % 1 === 0
    && args.length <= Number.MAX_SAFE_INTEGER
}

function create(prototype) {
    function F () {}
    F.prototype = prototype
    return new F()  
}
// bind
Function.prototype.myBind = function(context, ...args) {
    context = context ?? globalThis
    const fn = this
    const bound = function(...rest) {
        if (this instanceof bound) {
            return new fn(...args, ...rest) 
        } 
        return fn.call(context, ...args, ...rest)
    }

    bound.prototype = Object.create(bound)
    return bound
}

// 测试
const obj = { name: 'Tom' }
function greet(age, city) {
  console.log(`${this.name}, ${age}, ${city}`)
}
greet.myCall(obj, 18, 'Beijing')   // Tom, 18, Beijing
greet.myApply(obj, [18, 'Beijing']) // Tom, 18, Beijing
const bound = greet.myBind(obj, 18)
bound('Beijing')  // Tom, 18, Beijing

6. new / instanceof

推导链

Q1: new 做了什么?
→ 1. 创建空对象
→ 2. 原型链接(__proto__ 指向构造函数 prototype)→ 3. 执行构造函数,this 指向新对象
→ 4. 返回对象(构造函数返回对象则用它,否则用新对象)

Q2: instanceof 做了什么?
→ 沿着原型链找,看能否找到构造函数的 prototype

代码

javascript
// new
function myNew(Constructor, ...args) {
  // 1. 创建空对象,原型指向构造函数的 prototype
  const obj = Object.create(Constructor.prototype)
  // 2. 执行构造函数
  const result = Constructor.apply(obj, args)
  // 3. 返回对象
  return result instanceof Object ? result : obj
}

// instanceof
function myInstanceof(obj, Constructor) {
  if (obj === null || typeof obj !== 'object') return false
  let proto = Object.getPrototypeOf(obj)
  while (proto) {
    if (proto === Constructor.prototype) return true
    proto = Object.getPrototypeOf(proto)
  }
  return false
}

// 测试
function Person(name) { this.name = name }
const p = myNew(Person, 'Tom')
console.log(p.name)  // Tom
console.log(myInstanceof(p, Person))  // true
console.log(myInstanceof(p, Object))  // true
console.log(myInstanceof(p, Array))   // false

7. 继承

推导链

Q1: 为什么要继承?
→ 复用父类的属性和方法

Q2: 寄生组合继承怎么做?
→ 属性:子类构造函数里 Parent.call(this)
→ 方法:Object.setPrototypeOf(Child.prototype, Parent.prototype)
→ 修复 constructor

Q3: ES6 class 继承?
→ extends + super

代码

javascript
// 寄生组合继承(ES5 最佳方案)
function Animal(name) {
  this.name = name
  this.colors = ['white']
}
Animal.prototype.sayName = function() {
  console.log('My name is', this.name)
}

function Dog(name) {
  Animal.call(this, name)  // 继承属性
}
// 继承方法
Object.setPrototypeOf(Dog.prototype, Animal.prototype)
Dog.prototype.constructor = Dog

// 测试
const dog = new Dog('Max')
dog.sayName()  // My name is Max
console.log(dog instanceof Animal)  // true
javascript
// ES6 class 继承
class Animal {
  constructor(name) {
    this.name = name
    this.colors = ['white']
  }
  sayName() {
    console.log('My name is', this.name)
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name)  // 必须先调用 super
    this.breed = breed
  }
}

const dog = new Dog('Max', 'Golden')
dog.sayName()  // My name is Max

8. 千分位格式化

推导链

Q1: 什么是千分位?
12345671,234,567

Q2: 怎么实现?
→ 从右往左遍历,每 3 位加逗号
→ 注意处理负数和小数

代码

javascript
// 循环版(不用正则)
function formatNumber(num) {
  let [int, dec] = String(num).split('.')
  const isNegative = int[0] === '-'
  if (isNegative) int = int.slice(1)
  
  let res = ''
  const len = int.length
  for (let i = len - 1; i >= 0; i--) {
    const rightPos = len - i - 1
    if (rightPos !== 0 && rightPos % 3 === 0) {
      res = ',' + res
    }
    res = int[i] + res
  }
  
  return (isNegative ? '-' : '') + res + (dec ? '.' + dec : '')
}

// toLocaleString 版(简洁)
function formatNumberLocale(num) {
  return num.toLocaleString('en-US')
}

// 测试
console.log(formatNumber(1234567))       // 1,234,567
console.log(formatNumber(-1234567.89))   // -1,234,567.89

9. URL解析

推导链

Q1: 解析什么?
?a=1&b=2{ a: '1', b: '2' }
→ 支持重复 key 转数组、中文解码、无值 key 为 true

Q2: 怎么处理?
→ split('?') 取参数部分
→ split('&') 分割每个参数
→ split('=') 分割 key 和 value

代码

javascript
function parseQuery(url) {
  const query = url.split('?')[1]
  if (!query) return {}
  
  const params = query.split('&')
  const res = {}
  
  params.forEach(param => {
    let [key, value] = param.split('=')
    // 无值的 key 为 true
    value = value ? decodeURIComponent(value) : true
    // 能转数字就转数字
    if (value !== true) {
      value = isNaN(Number(value)) ? value : parseFloat(value)
    }
    // 重复 key 转数组
    if (!res.hasOwnProperty(key)) {
      res[key] = value
    } else {
      res[key] = [].concat(res[key], value)
    }
  })
  
  return res
}


// 测试
console.log(parseQuery('?name=Tom&age=18'))
// { name: 'Tom', age: 18 }

console.log(parseQuery('?id=1&id=2&city=%E5%8C%97%E4%BA%AC&enabled'))
// { id: [1, 2], city: '北京', enabled: true }