解锁 JavaScript 函数密码:闭包与柯里化实战指南

88 阅读7分钟

一、函数:JavaScript 的一等公民(First-Class Citizen)

在 JavaScript 中,函数被赋予 "一等公民" 的特殊地位,这意味着它具备与基本数据类型同等的操作权限:可赋值、可传递、可返回。这种特性是函数式编程的基石,也是 JavaScript 灵活编程范式的核心体现。

1.1 一等公民的三大特性实践

// 特性1:函数赋值给变量
const greet = function() {
    console.log('Hello, Function!');
};
greet(); // 输出:Hello, Function!

// 特性2:函数作为参数传递
function runAction(action) {
    action();
}
runAction(greet); // 输出:Hello, Function!

// 特性3:函数作为返回值
function createLogger() {
    return function(message) {
        console.log(`[LOG] ${message}`);
    };
}
const logger = createLogger();
logger('This is a logged message'); // 输出:[LOG] This is a logged message

1.2 函数对象的隐藏属性

除了作为可执行代码,函数本身也是对象,拥有以下关键属性:

属性说明示例
name函数名称,匿名函数显示为 ""或"anonymous"function() {}.name → ""
length定义时的参数个数(a,b)=>{}.length → 2
prototype构造函数的原型对象,用于实现继承Function.prototype
caller调用当前函数的函数引用(严格模式下不可用)function a() {b();} function b() {console.log(b.caller);}

二、函数的多元形态与编程范式

2.1 函数的五种基础形态对比

2.1.1 形态分类与代码示例

函数类型语法特征核心特性典型应用场景
匿名函数无函数名定义临时使用、作为回调参数setTimeout(() => {}, 1000)
函数声明function fn() {} 形式存在函数提升(hoisting),可在声明前调用封装可复用逻辑
函数表达式const fn = function() {} 形式无函数提升,需先赋值再调用模块导出、事件监听
箭头函数(a,b) => a+b 简洁语法无独立this/arguments,词法作用域绑定回调函数、数组方法回调
递归函数函数内部调用自身解决可分解为子问题的场景(如树遍历、阶乘计算)数据结构遍历、数学递归问题

2.1.2 代码示例解析

// 1. 匿名函数:作为回调参数
document.addEventListener('click', function(event) {
    console.log('Clicked at:', event.clientX, event.clientY);
});

// 2. 函数声明与函数表达式的提升差异
console.log(declaration()); // 正常输出:声明函数
console.log(expression()); // 报错:expression is not a function

function declaration() {
    return '声明函数';
}

const expression = function() {
    return '表达式函数';
};

// 3. 箭头函数的this绑定特性
const obj = {
    name: 'Demo',
    getSelf() {
        // 普通函数的this指向调用者
        setTimeout(function() {
            console.log(this.name); // 输出:undefined(this指向window)
        }, 100);
        
        // 箭头函数的this继承自定义时的作用域
        setTimeout(() => {
            console.log(this.name); // 输出:Demo(this指向obj)
        }, 100);
    }
};
obj.getSelf();

2.2 立即执行函数(IIFE)的双重形态与应用价值

// 形态1:括号包裹函数定义
(function IIFE() {
    const privateData = '保密数据';
    console.log('IIFE执行,访问私有数据:', privateData);
})(); // 直接调用

// 形态2:函数定义后直接调用
(function() {
    const module = {
        init() {
            console.log('模块初始化');
        }
    };
    window.App = module; // 暴露模块接口
}());

// IIFE的核心应用场景
// 1. 创建私有作用域,避免全局污染
// 2. 实现模块模式(Module Pattern)
// 3. 处理函数作用域内的变量封闭

三、闭包:JavaScript 的 "作用域保鲜剂"

3.1 闭包的核心机制与内存模型

闭包是指函数与其引用的外层作用域变量的绑定组合,其核心特征是:当内部函数被保存到外部时,外层函数的作用域不会被释放,从而形成 "变量保鲜" 效果。

// 闭包经典案例:计数器实现
function createCounter() {
    let count = 0; // 闭包捕获的自由变量
    
    // 内部函数形成闭包,保持对count的引用
    return {
        increment() {
            count++;
            return count;
        },
        decrement() {
            count--;
            return count;
        },
        getValue() {
            return count;
        }
    };
}

const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.decrement()); // 1
console.log(counter.getValue()); // 1

// 闭包内存模型说明:
// 1. createCounter执行完毕后,count变量未被垃圾回收
// 2. 因为返回的对象方法中保留了对count的引用
// 3. 内存中形成了一条从counter对象到count变量的引用链

3.2 闭包的典型应用场景

3.2.1 数据私有化(Private Data)

// 闭包实现类的私有属性
function User(name) {
    // 私有属性,只能通过闭包方法访问
    let privateName = name;
    
    return {
        // 公有方法:访问私有数据
        getName() {
            return privateName;
        },
        setName(newName) {
            privateName = newName;
        }
    };
}

const user = User('Alice');
console.log(user.getName()); // Alice
user.setName('Bob');
console.log(user.getName()); // Bob
console.log(user.privateName); // undefined(无法直接访问私有属性)

3.2.2 函数防抖与节流(Debounce & Throttle)

// 闭包实现防抖函数(事件触发后延迟执行,期间多次触发会重置计时)
function debounce(func, delay) {
    let timer = null;
    return function(...args) {
        if (timer) clearTimeout(timer);
        timer = setTimeout(() => {
            func.apply(this, args);
        }, delay);
    };
}

// 闭包实现节流函数(控制事件在单位时间内只执行一次)
function throttle(func, limit) {
    let inThrottle = false;
    return function(...args) {
        if (!inThrottle) {
            func.apply(this, args);
            inThrottle = true;
            setTimeout(() => {
                inThrottle = false;
            }, limit);
        }
    };
}

// 应用示例
const debouncedSearch = debounce(() => {
    console.log('执行搜索请求');
}, 300);

const throttledScroll = throttle(() => {
    console.log('处理滚动事件');
}, 500);

四、柯里化(Currying):函数的参数魔法变形术

4.1 柯里化的核心思想与数学渊源

柯里化源于数理逻辑中的柯里化函数(Curried Function) ,其核心思想是将一个多参数函数转换为一系列单参数函数的嵌套调用。这种技术使得函数可以逐步接收参数,每次调用返回一个处理部分参数的新函数,直到所有参数收集完毕后执行最终计算。

4.2 手写通用柯里化函数与原理剖析

// 通用柯里化函数实现
function curry(func) {
    // 核心递归函数:收集参数并判断是否执行
    return function curried(...args) {
        // 如果已收集参数数量 >= 目标函数所需参数数量
        if (args.length >= func.length) {
            // 执行原函数并返回结果
            return func.apply(this, args);
        } else {
            // 否则返回新的柯里化函数,继续收集参数
            return function(...newArgs) {
                return curried.apply(this, [...args, ...newArgs]);
            };
        }
    };
}

// 示例:将三元加法函数柯里化
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));       // 6

// 柯里化过程解析:
// 1. 第一次调用 curriedSum(1) 返回新函数,已收集参数 [1]
// 2. 第二次调用 (2) 返回新函数,已收集参数 [1,2]
// 3. 第三次调用 (3) 时参数数量达标,执行 sum(1,2,3)

4.3 柯里化的实战应用场景

4.3.1 参数复用与函数定制

// 场景:创建多个特定维度的坐标转换函数
function transform(x, y, z) {
    return { x, y, z };
}

// 柯里化后复用x轴参数
const transformX = curry(transform)(10); // 固定x=10

// 生成不同场景的转换函数
const transform2D = transformX(20);      // 固定x=10, y=20
const transform3D = transformX(20, 30); // 固定x=10, y=20, z=30

console.log(transform2D()); // { x: 10, y: 20, z: undefined }
console.log(transform3D()); // { x: 10, y: 20, z: 30 }

4.3.2 惰性求值与性能优化

// 场景:大数据计算中的延迟执行
function heavyCalculation(a, b, c, d) {
    console.log('执行复杂计算...');
    return a * b + c - d;
}

const curriedCalculation = curry(heavyCalculation);

// 先收集参数,延迟执行计算
const partialCalculation = curriedCalculation(10)(20);

// 当真正需要结果时再传入剩余参数
console.log(partialCalculation(5)(3)); // 输出:执行复杂计算... 202

五、类数组对象与数组的转换技术

5.1 类数组对象的核心特征

在 JavaScript 中,类数组对象(Array-like Object)具有以下特点:

  • 拥有length属性
  • 可以通过索引(数字键)访问元素
  • 但不具备数组的原生方法(如mapfilterreduce等)

常见的类数组对象包括:

  • 函数内部的arguments对象
  • DOM 操作返回的NodeList
  • 字符串(可通过索引访问字符)

5.2 四种转换类数组为数组的方法

// 场景:将DOM节点列表转换为数组
const divs = document.querySelectorAll('div');

// 方法1:Array.from()(ES6+推荐方式)
const divArray1 = Array.from(divs);
console.log(divArray1 instanceof Array); // true

// 方法2:扩展运算符(ES6+,需类数组支持迭代)
const divArray2 = [...divs];
console.log(divArray2.map(div => div.className));

// 方法3:Array.prototype.slice.call()(兼容ES5)
const divArray3 = Array.prototype.slice.call(divs);
console.log(divArray3.filter(div => div.id));

// 方法4:Array.prototype.concat.apply()(较少使用)
const divArray4 = Array.prototype.concat.apply([], divs);
console.log(divArray4.length);

5.3 arguments 对象的特殊处理

// 函数内将arguments转换为数组
function processArgs() {
    // 传统ES5方式
    const args1 = Array.prototype.slice.call(arguments);
    // ES6+方式
    const args2 = Array.from(arguments);
    const args3 = [...arguments];
    
    // 应用:筛选出数字参数并求和
    const numbers = args1.filter(arg => typeof arg === 'number');
    const sum = numbers.reduce((total, num) => total + num, 0);
    return sum;
}

console.log(processArgs(1, 'a', 2, true, 3)); // 6