核心前提:JavaScript 中函数是一等公民 (可作为参数 / 返回值 / 赋值),这是所有高阶技巧的基础;
AOP(面向切面编程)
-
概念:AOP (Aspect Oriented Programming) 核心是不修改原函数代码,在函数执行的「前置 / 后置 / 异常」阶段插入额外逻辑 (如日志、埋点、性能监控),实现业务逻辑与通用逻辑解耦;
-
实现原理:通过重写函数的 prototype (或直接包装函数),在原函数执行前后注入逻辑;
-
手写实现:
// 给 Function 原型扩展前置/后置切面方法 Function.prototype.before = function (beforeFn) { const self = this; // 保存原函数引用 return function (...args) { beforeFn.apply(this, args); // 执行前置逻辑 return self.apply(this, args); // 执行原函数并返回结果 }; }; Function.prototype.after = function (afterFn) { const self = this; return function (...args) { const result = self.apply(this, args); // 先执行原函数 afterFn.apply(this, args); // 执行后置逻辑 return result; // 返回原函数结果 }; }; // 测试 function coreFn(name) { console.log(`核心逻辑:处理 ${name} 的请求`); return `结果:${name} 处理完成`; } // 包装核心函数:前置埋点,后置日志 const wrappedFn = coreFn .before((name) => console.log(`[前置] 埋点:${name} 请求开始`)) .after((name) => console.log(`[后置] 日志:${name} 请求结束`)); console.log(wrappedFn("用户A")); // 输出: // [前置] 埋点:用户A 请求开始 // 核心逻辑:处理 用户A 的请求 // [后置] 日志:用户A 请求结束 // 结果:用户A 处理完成 -
应用场景:
- 接口请求的前置参数校验、后置响应格式化;
- 埋点统计 (如按钮点击前记录用户行为);
- 性能监控 (记录函数执行耗时);
单例模式
-
概念:保证一个类全局只有一个实例,且提供唯一访问入口 (如 Vuex 的 store、浏览器的 window 对象);
-
实现方式:
const Singleton = (function () { let instance = null; // 闭包保存实例 function createInstance() { // 真实业务逻辑:创建实例(如初始化配置、连接数据库) return { name: "单例实例", doSomething() { console.log("执行单例方法"); }, }; } return { getInstance() { if (!instance) { instance = createInstance(); } return instance; }, }; })(); // 测试:多次获取实例是否为同一个 const a = Singleton.getInstance(); const b = Singleton.getInstance(); console.log(a === b); // truefunction createModal() { // 模拟创建弹窗(重量级操作,惰性执行) const div = document.createElement("div"); div.innerHTML = "全局唯一弹窗"; div.style.display = "none"; document.body.appendChild(div); return div; } // 通用单例包装函数 function getSingleton(fn) { let instance = null; return function () { return instance || (instance = fn.apply(this, arguments)); }; } // 包装创建弹窗的函数 const createSingleModal = getSingleton(createModal); // 测试:两次调用返回同一个DOM元素 const modal1 = createSingleModal(); const modal2 = createSingleModal(); console.log(modal1 === modal2); // true -
应用场景:
- 全局弹窗、loading 组件;
- 数据库连接池、Redis 客户端;
- Vuex/Pinia 的 store 实例;
惰性函数
-
概念:函数首次执行时动态重写自身逻辑,后续执行直接使用重写后的逻辑,避免重复判断 (如环境检测、兼容性处理);
-
实现原理:利用函数的可重写特性,首次执行时将函数体替换为目标逻辑,后续调用无需再走判断分支;
-
手写实现:
function addEvent(element, type, handler) { // 首次执行:检测环境并重写函数 if (element.addEventListener) { addEvent = function (element, type, handler) { element.addEventListener(type, handler, false); }; } else if (element.attachEvent) { addEvent = function (element, type, handler) { element.attachEvent(`on${type}`, handler); }; } else { addEvent = function (element, type, handler) { element[`on${type}`] = handler; }; } // 执行重写后的函数 addEvent(element, type, handler); } // 测试:首次调用重写函数,后续直接执行对应逻辑 const btn = document.getElementById("btn"); addEvent(btn, "click", () => console.log("点击了")); // 首次执行重写 addEvent(btn, "click", () => console.log("再次点击")); // 直接执行 addEventListenerfunction getViewportSize() { // 首次执行:重写函数 getViewportSize = function () { return { width: window.innerWidth || document.documentElement.clientWidth, height: window.innerHeight || document.documentElement.clientHeight, }; }; // 执行重写后的逻辑 return getViewportSize(); } // 测试:首次调用重写,后续直接返回结果 console.log(getViewportSize()); // 首次调用:执行 “重写函数 + 兼容性判断 + 返回结果”,这一步和常规写法耗时一致 console.log(getViewportSize()); // 后续调用:直接执行重写后的函数(仅返回视口大小,无任何兼容性判断),省去了每次的 || 逻辑运算 -
应用场景:
- 浏览器兼容性处理 (如事件绑定、Promise 降级);
- 高频调用的工具函数 (如获取设备信息、计算位置);
- 避免重复的环境检测 (如判断是否为移动端);
手写 bind 函数
-
概念:bind 是函数原型方法,作用是绑定函数的 this 指向,并支持预设参数,返回一个新函数(执行时才调用原函数);
-
核心特性:
- 绑定 this 指向,不受调用方式影响;
- 支持柯里化传参 (预设参数);
- 绑定后的函数可被 new 调用 (此时 this 指向实例,忽略绑定的 this);
-
手写实现:
Function.prototype.myBind = function (context, ...bindArgs) { // 1. 校验调用者是否为函数 if (typeof this !== "function") { throw new TypeError("Bind must be called on a function"); } const self = this; // 保存原函数 // 2. 定义返回的新函数(使用剩余参数接收调用时的参数) const boundFn = function (...callArgs) { // 合并bind时的参数和调用时的参数 const finalArgs = [...bindArgs, ...callArgs]; // 关键:new调用时this指向实例,否则指向bind的context return self.apply(this instanceof boundFn ? this : context, finalArgs); }; // 3. 继承原函数的原型(保证new调用时原型链正确) boundFn.prototype = Object.create(self.prototype); // 修复constructor指向 boundFn.prototype.constructor = boundFn; return boundFn; }; // 测试 const person = { name: "张三" }; function sayHi(age, gender) { console.log(`我是${this.name},${age}岁,${gender}`); } // 绑定 this 和预设参数 const boundSayHi = sayHi.myBind(person, 20); boundSayHi("男"); // 输出:我是张三,20岁,男 // new 调用(忽略绑定的this) const instance = new boundSayHi("女"); // 输出:我是 undefined,20岁,女(this指向instance) console.log(instance instanceof sayHi); // true console.log(instance.constructor === sayHi); // 验证原型链正确性(true) -
应用场景
- 固定函数的 this 指向 (如 React 类组件中绑定事件处理函数);
- 预设函数参数 (柯里化场景);
- 延迟执行函数 (如定时器中绑定 this);
函数柯里化
-
概念:柯里化 (Currying) 是将多参数函数拆解为一系列单参数函数,逐步接收参数,直到参数齐全后执行原函数;
-
核心:分步传参、延迟执行;
-
实现原理:利用闭包保存已传入的参数,返回新函数接收剩余参数,直到参数数量满足原函数要求;
-
手写实现:
function curry(fn) { const argLength = fn.length; // 原函数的参数个数 // 改用递归思路,每次传入已收集的参数 function curried(...collectedArgs) { // 若参数已凑齐,执行原函数 if (collectedArgs.length >= argLength) { return fn.apply(this, collectedArgs.slice(0, argLength)); // slice 防止参数过多 } // 未凑齐则返回新函数,新函数调用时合并参数 return function (...newArgs) { return curried.apply(this, collectedArgs.concat(newArgs)); }; } return curried; } // 测试 function sum(a, b, c) { return a + b + c; } const curriedSum = curry(sum); console.log(curriedSum(1)(2)(3)); // 6 console.log(curriedSum(1, 2)(3)); // 6 console.log(curriedSum(1, 2, 3)); // 6function curryAdvanced(fn) { const execute = (...collectedArgs) => { // 返回一个新函数,用于接收下一批参数 return (...newArgs) => { // 若传入空参数(调用()),执行原函数并返回结果 if (newArgs.length === 0) { return fn(...collectedArgs); } // 否则拼接参数,继续返回接收函数 return execute(...collectedArgs, ...newArgs); }; }; return execute(); } // 测试 const sumAdvanced = (...args) => args.reduce((a, b) => a + b, 0); const curriedSumAdv = curryAdvanced(sumAdvanced); console.log(curriedSumAdv(1)(2)(3)()); // 6(最后传空参数执行) console.log(curriedSumAdv(1, 2)(3, 4)()); // 10 -
核心场景:
// 基础请求函数 function request(method, baseURL, url, data) { console.log(`[${method}] ${baseURL}${url}`, data); // 实际业务中:return axios({ method, url: baseURL + url, data, headers }); } // 柯里化请求函数 const curryRequest = curry(request); // 1. 绑定 GET 方法 + 固定 baseURL(复用:所有GET请求都用这个baseURL) const getRequest = curryRequest('GET', 'https://api.example.com'); // 2. 复用:不同接口只需传 url + data const getUser = getRequest('/user'); const getOrder = getRequest('/order'); // 调用:无需重复传 method 和 baseURL getUser({ id: 1 }); // [GET] https://api.example.com/user {id:1} getOrder({ orderId: 100 }); // [GET] https://api.example.com/order {orderId:100}// 基础验证函数:(规则, 值) => 布尔值 function validate(rule, value) { const rules = { mobile: /^1[3-9]\d{9}$/, email: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/, required: (v) => !!v.trim() }; return rules[rule].test ? rules[rule].test(value) : rules[rule](value); } // 柯里化验证函数 const curryValidate = curry(validate); // 1. 绑定规则:生成专用验证函数(复用) const validateMobile = curryValidate('mobile'); const validateEmail = curryValidate('email'); const validateRequired = curryValidate('required'); // 2. 调用:只需传校验值 console.log(validateMobile('13800138000')); // true console.log(validateEmail('test@example.com')); // true console.log(validateRequired('')); // false// 基础筛选函数:(分类, 时间范围, 状态, 列表) => 筛选结果 function filterList(category, timeRange, status, list) { return list.filter(item => item.category === category && item.time >= timeRange.start && item.time <= timeRange.end && item.status === status ); } // 柯里化筛选函数 const curryFilter = curry(filterList); // 分步收集参数: // 1. 第一步:选分类(如「商品」) const filterGoods = curryFilter('goods'); // 2. 第二步:选时间范围(如近7天) const filterGoods7d = filterGoods({ start: Date.now() - 7*24*3600*1000, end: Date.now() }); // 3. 第三步:选状态(如「已上架」),参数齐全,执行筛选 const filterGoods7dOnSale = filterGoods7d('onSale'); // 调用:传入列表,完成筛选 const list = [{ category: 'goods', time: Date.now(), status: 'onSale' }, ...]; console.log(filterGoods7dOnSale(list));// 基础计算函数:(单价, 数量, 折扣) => 最终价格 function calcPrice(price, count, discount) { return price * count * discount; } const curryCalc = curry(calcPrice); // 第一步:绑定单价(如 99 元) const calc99 = curryCalc(99); // 第二步:绑定数量(如 2 件) const calc99x2 = calc99(2); // 第三步:绑定折扣(如 0.8),执行计算 console.log(calc99x2(0.8)); // 99*2*0.8 = 158.4// 柯里化工具函数 const add = curry((a, b) => a + b); const multiply = curry((a, b) => a * b); const toNumber = (str) => Number(str); // 函数组合:pipe(从左到右执行) const pipe = (...fns) => (x) => fns.reduce((acc, fn) => fn(acc), x); // 组合逻辑:转数字 → 加10 → 乘2 const processNum = pipe(toNumber, add(10), multiply(2)); // 调用 console.log(processNum('5')); // (5 → 5+10=15 → 15*2=30) → 30import React from 'react'; // 柯里化点击处理函数:(id, e) => 逻辑 const handleItemClick = curry((id, e) => { console.log(`点击了ID为${id}的项`, e); }); function ItemList() { const list = [{ id: 1, name: '项1' }, { id: 2, name: '项2' }]; return ( <ul> {list.map(item => ( // 提前绑定 id,渲染时不创建新函数(优化性能) // 若不用柯里化,需写 onClick={(e) => handleItemClick(item.id, e)},每次渲染都会创建新匿名函数,影响性能 <li key={item.id} onClick={handleItemClick(item.id)}> {item.name} </li> ))} </ul> ); }
扁平化(数组 / 函数)
前置知识
-
reduce 的语法
/** * callback 回调函数 * - accumulator 累计器 * - currentValue 当前值 * - index 当前索引 * - array 调用 reduce 的数组 * initialValue 为第一次调用 callback 函数时的第一个参数的值,若没有提供初始值,则将使用数组中第一个元素 */ arr.reduce(callback(accumulator, currentValue, index, array), initialValue) -
使用 reduce 的第一个参数,每次调用的参数和返回值如下表:
[0, 1, 2, 3, 4].reduce(function (accumulator, currentValue, currentIndex, array) { return accumulator + currentValue; });callback accumulator currVal Index array return value 第一次调用 0 1 1 [0, 1, 2, 3, 4] 1 第二次调用 1 2 2 [0, 1, 2, 3, 4] 3 第三次调用 3 3 3 [0, 1, 2, 3, 4] 6 第四次调用 6 4 4 [0, 1, 2, 3, 4] 10 -
提供一个初始值作为 reduce 方法的第二个参数,每次调用的参数和返回值如下表:
[0, 1, 2, 3, 4].reduce((accumulator, currentValue, currentIndex, array) => { return accumulator + currentValue }, 10)callback accumulator currVal Index array return value 第一次调用 10 0 0 [0, 1, 2, 3, 4] 10 第二次调用 10 1 1 [0, 1, 2, 3, 4] 11 第三次调用 11 2 2 [0, 1, 2, 3, 4] 13 第四次调用 13 3 3 [0, 1, 2, 3, 4] 16 第五次调用 16 4 4 [0, 1, 2, 3, 4] 20
数组扁平化
-
概念:将多维数组转换为一维数组 (如 [1, [2, [3]]] → [1, 2, 3]);
-
手写实现:
function flatten(arr) { let result = []; for (let item of arr) { if (Array.isArray(item)) { result = result.concat(flatten(item)); // 递归处理子数组 } else { result.push(item); } } return result; } console.log(flatten([1, [2, [3, 4], 5]])); // [1,2,3,4,5]function flattenReduce(arr) { return arr.reduce((acc, cur) => { return acc.concat(Array.isArray(cur) ? flattenReduce(cur) : cur); }, []); } console.log(flattenReduce([1, [2, [3, 4], 5]])); // [1,2,3,4,5]function flattenES6(arr) { while (arr.some(item => Array.isArray(item))) { arr = [].concat(...arr); } return arr; } console.log(flattenES6([1, [2, [3, 4], 5]])); // [1,2,3,4,5]// flat(depth):depth 为扁平化深度,Infinity 表示无限深度 console.log([1, [2, [3, 4], 5]].flat(Infinity)); // [ 1, 2, 3, 4, 5 ]
函数扁平化(函数组合)
-
概念:将多个嵌套执行的函数「扁平化」为线性执行 (如 fn3(fn2(fn1(x))) → compose(fn3, fn2, fn1)(x));
-
手写实现:
// compose:从右到左执行(如 compose(f,g,h) → x => f(g(h(x)))) function compose(...fns) { if (fns.length === 0) return (x) => x; // 无函数时返回原值 if (fns.length === 1) return fns[0]; // 单个函数直接返回 return fns.reduce((a, b) => (...args) => a(b(...args))); } // 测试 const add1 = (x) => x + 1; const mul2 = (x) => x * 2; const sub3 = (x) => x - 3; // compose:sub3(mul2(add1(10))) → (10+1)*2-3 = 19 console.log(compose(sub3, mul2, add1)(10)); // 19// pipe:从左到右执行(如 pipe(f,g,h) → x => h(g(f(x)))) function pipe(...fns) { if (fns.length === 0) return (x) => x; // 无函数时返回原值 if (fns.length === 1) return fns[0]; // 单个函数直接返回 return fns.reduce((a, b) => (...args) => b(a(...args))); } // 测试 const add1 = (x) => x + 1; const mul2 = (x) => x * 2; const sub3 = (x) => x - 3; // pipe:add1(mul2(sub3(10))) → (10-3)*2+1 = 15 console.log(pipe(sub3, mul2, add1)(10)); // 15 -
应用场景:
- 数组扁平化:处理后端返回的多维数据、树形结构转一维;
- 函数扁平化:React 中间件、Redux 的 compose 增强器、函数式编程中的逻辑组合;
剑指 Offer 30.包含min函数的栈
上一篇