1. 防抖 debounce
推导链
Q1: 防抖是什么?
→ 事件触发后等待 n 秒才执行,期间再触发就重新计时
→ 场景:搜索框输入、窗口 resize
Q2: 核心逻辑?
→ 每次触发先清除上一个定时器,再设置新定时器
→ 闭包保存 timer
Q3: 立即执行版?
→ 第一次立即执行,后续等待
→ 用 immediate 参数控制
Q4: Hooks 版怎么写?
→ useRef 保存 timer(跨渲染保持引用)
→ useEffect 监听值变化
变体速查
| 形态 | 核心 |
|---|---|
| JS 基础版 | clearTimeout + setTimeout |
| JS 立即执行版 | immediate 参数,首次立即执行 |
| Hooks useDebounce | 返回防抖后的值 |
| Hooks useDebounceFn | 返回防抖后的函数 |
代码
// 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() // 只输出一次// 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 | 返回节流后的函数 |
代码
// 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) // 首次立即执行,之后每秒执行一次// 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: 占位符版?
→ 用特殊值(如 _)表示跳过某个参数
→ 后续调用时填充占位符位置
代码
// 定长版 (根据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)()) // 184. 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 串联函数
// 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 = 95. call / apply / bind
推导链
Q1: call/apply/bind 干嘛?
→ 改变函数的 this 指向
→ call 逐个传参,apply 数组传参,bind 返回新函数
Q2: 怎么实现改变 this?
→ 把函数挂到目标对象上,用对象调用
→ 用 Symbol 避免属性冲突,调用完删掉
Q3: bind 的特殊之处?
→ 返回新函数,支持分批传参
→ 作为构造函数时 this 指向实例
代码
// 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, Beijing6. new / instanceof
推导链
Q1: new 做了什么?
→ 1. 创建空对象
→ 2. 原型链接(__proto__ 指向构造函数 prototype)→ 3. 执行构造函数,this 指向新对象
→ 4. 返回对象(构造函数返回对象则用它,否则用新对象)
Q2: instanceof 做了什么?
→ 沿着原型链找,看能否找到构造函数的 prototype
代码
// 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)) // false7. 继承
推导链
Q1: 为什么要继承?
→ 复用父类的属性和方法
Q2: 寄生组合继承怎么做?
→ 属性:子类构造函数里 Parent.call(this)
→ 方法:Object.setPrototypeOf(Child.prototype, Parent.prototype)
→ 修复 constructor
Q3: ES6 class 继承?
→ extends + super
代码
// 寄生组合继承(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// 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 Max8. 千分位格式化
推导链
Q1: 什么是千分位?
→ 1234567 → 1,234,567
Q2: 怎么实现?
→ 从右往左遍历,每 3 位加逗号
→ 注意处理负数和小数
代码
// 循环版(不用正则)
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.899. URL解析
推导链
Q1: 解析什么?
→ ?a=1&b=2 → { a: '1', b: '2' }
→ 支持重复 key 转数组、中文解码、无值 key 为 true
Q2: 怎么处理?
→ split('?') 取参数部分
→ split('&') 分割每个参数
→ split('=') 分割 key 和 value
代码
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 }