Js函数式编程启蒙:那些我一直在用的模式

更新日期: 2025-10-30 阅读: 46 标签: 模式

今天我想和你分享:用JavaScript重新理解这些概念。


思维模型:函数就是替换

第一个启发是,函数式编程本质上就是替换,就像模板字符串那样。

const name = "Alice";
const age = 30;
const message = `你好 ${name},你今年 ${age} 岁了`;
console.log(message);
// 输出:"你好 Alice,你今年 30 岁了"

函数也是同样的道理。当你调用 square(5) 时,本质上就是在做替换:

const square = (x) => x * x;
console.log(square(5));
// 输出:25
// square(5) 变成了 5 * 5

这个替换模型是所有其他概念的基础。关键是要把函数看作纯粹的转换过程:输入进去,输出出来,没有其他副作用。


摆脱全局依赖

我遇到了一个剧院定价的问题,代码里到处都是全局变量:

// 有问题的写法
let basePrice = 5.0;
let baseAttendance = 120;
let fixedCost = 180;

function computeProfit(price) {
    const attendees = baseAttendance - (price - basePrice) * 150;
    const revenue = attendees * price;
    const cost = fixedCost + 0.04 * attendees;
    return revenue - cost;
}

console.log(computeProfit(6));
// 输出:-358.8(亏钱了!)

问题在于这个函数有隐藏的依赖。测试变得很困难,组合使用几乎不可能。

解决方案是让函数完全自包含:

// 更好的写法:函数生成函数
function makeProfitFunction(config = {}) {
    const {
        basePrice = 5.0,           // 基础票价(美元)
        baseAttendance = 120,      // 基础票价下的观众数
        attendeesPerDollar = 150,  // 每涨价1美元流失的观众
        fixedCost = 180,           // 固定成本
        costPerAttendee = 0.04     // 每位观众的变动成本
    } = config;

    return function computeProfit(price) {
        const attendees = baseAttendance - (price - basePrice) * attendeesPerDollar;
        const revenue = attendees * price;
        const cost = fixedCost + costPerAttendee * attendees;
        return revenue - cost;
    };
}

// 使用
const profitFunc = makeProfitFunction({ basePrice: 6.0, fixedCost: 200 });
console.log(profitFunc(6)); // 输出:515.2
console.log(profitFunc(5)); // 输出:1139.2

这种模式创建了记住配置的闭包。每个利润函数都成了独立的单元,没有外部依赖。


函数是一等公民

最强大的认识是:函数就是数据。你可以传递它们,存储在数组里,自由组合:

const operations = [
    x => x + 10,
    x => x * 2,
    x => x * x
];

// 按顺序应用操作
function compose(value, ops) {
    return ops.reduce((acc, op) => op(acc), value);
}

console.log(compose(2, operations));
// 输出:576
// 步骤:(2 + 10) * 2 = 24,然后 24 * 24 = 576

这开启了我从未想过的可能性。函数成了可以无限组合的积木。


解析器革命:组合子模式

最让人兴奋的是用组合子构建解析器。组合子是接受其他函数作为输入,返回新函数作为输出的函数。小解析函数可以组合起来处理复杂语法

// 基础构建块
function parseDigits(text, index) {
    let n = index;
    while (n < text.length && /\d/.test(text[n])) {
        n++;
    }
    return n > index ? [text.slice(index, n), n] : null;
}

function parseLetters(text, index) {
    let n = index;
    while (n < text.length && /[a-zA-Z]/.test(text[n])) {
        n++;
    }
    return n > index ? [text.slice(index, n), n] : null;
}

console.log(parseDigits("123abc", 0));  // 输出:["123", 3]
console.log(parseLetters("abc123", 0)); // 输出:["abc", 3]
console.log(parseDigits("abc", 0));     // 输出:null


构建解析器生成器

真正的魔法在这里:我们可以创建解析器生成器:

function matchingPredicate(predicate) {
    return function parse(text, index) {
        let n = index;
        while (n < text.length && predicate(text[n])) {
            n++;
        }
        return n > index ? [text.slice(index, n), n] : null;
    };
}

// 现在可以生成解析器了
const parseDigits = matchingPredicate(c => /\d/.test(c));
const parseLetters = matchingPredicate(c => /[a-zA-Z]/.test(c));
const parseAlphaNum = matchingPredicate(c => /[a-zA-Z0-9]/.test(c));

console.log(parseDigits("123abc", 0));  // 输出:["123", 3]
console.log(parseLetters("abc123", 0)); // 输出:["abc", 3]


序列和选择

真正的突破来自序列和选择组合子:

function sequence(...parsers) {
    return function parse(text, index) {
        const results = [];
        let currentIndex = index;

        for (const parser of parsers) {
            const result = parser(text, currentIndex);
            if (!result) return null;

            const [value, newIndex] = result;
            results.push(value);
            currentIndex = newIndex;
        }

        return [results, currentIndex];
    };
}

function choice(...parsers) {
    return function parse(text, index) {
        for (const parser of parsers) {
            const result = parser(text, index);
            if (result) return result;
        }
        return null;
    };
}

function literal(char) {
    return function parse(text, index) {
        if (index < text.length && text[index] === char) {
            return [char, index + 1];
        }
        return null;
    };
}

现在我们可以通过组合构建复杂解析器:

// 解析像 "name=42;" 这样的设置
const parseSetting = sequence(
    parseLetters,
    literal('='),
    parseDigits,
    literal(';')
);

console.log(parseSetting("speed=75;", 0));
// 输出:[["speed", "=", "75", ";"], 9]

// 用解构处理解析结果
const [ast, nextIndex] = parseSetting("speed=75;", 0) || [null, 0];
if (ast) {
    console.log(`解析结果:${ast[0]}=${ast[2]},继续位置:${nextIndex}`);
    // 输出:"解析结果:speed=75,继续位置:9"
}


错误处理:Result模式

我发现了一个优雅的解决方案来处理错误:

class Result {
    constructor(value = null, error = null) {
        this.value = value;
        this.error = error;
    }

    static success(value) {
        return new Result(value, null);
    }

    static failure(error) {
        return new Result(null, error);
    }

    unwrap() {
        if (this.error) {
            throw this.error;
        }
        return this.value;
    }

    isSuccess() {
        return this.error === null;
    }

    map(fn) {
        if (this.error) {
            return this; // 出错时短路
        }

        try {
            return Result.success(fn(this.value));
        } catch (error) {
            return Result.failure(error);
        }
    }

    flatMap(fn) {
        if (this.error) {
            return this;
        }

        try {
            return fn(this.value);
        } catch (error) {
            return Result.failure(error);
        }
    }
}

// 使用示例
const successResult = Result.success(42);
console.log(successResult.unwrap());     // 输出:42

const failureResult = Result.failure(new Error("出错了"));
console.log(failureResult.isSuccess());  // 输出:false


链式操作

现在我们可以优雅地链式操作:

const add10 = x => x + 10;
const double = x => x * 2;
const square = x => x * x;

const result = Result.success(2)
    .map(add10)
    .map(double)
    .map(square);

console.log(result.unwrap()); // 576

这种模式让你可以构建管道,错误会自动传播,不需要每一步都手动检查。


现代JavaScript中的实际应用

这些模式在现代JavaScript中随处可见:

Promise链就是单子的:

fetch('/api/user')
    .then(response => response.json())
    .then(user => user.profile)
    .catch(error => console.error(error));

数组方法使用高阶函数:

const numbers = [1, 2, 3, 4, 5];
const result = numbers
    .filter(x => x % 2 === 0)  // [2, 4]
    .map(x => x * 2)           // [4, 8]
    .reduce((sum, x) => sum + x, 0);  // 12

console.log(result); // 输出:12

react hooks拥抱闭包和函数组合:

function useCounter(initialValue = 0) {
    const [count, setCount] = useState(initialValue);

    const increment = useCallback(() => setCount(c => c + 1), []);
    const decrement = useCallback(() => setCount(c => c - 1), []);

    return { count, increment, decrement };
}


主要收获

学习函数式编程让我理解了那些一直在用但从未深入理解的模式:

  • 组合优于继承:小而专注的函数比大而全的函数更好用。
  • 纯函数更可预测:没有副作用意味着更容易测试和理解。
  • 连接方式很重要:函数如何连接通常比函数内部做什么更重要。
  • 错误处理可以很优雅:Result模式让错误传播变得明确且可组合。
  • 代码生成很强大:创建函数的函数开启了新的架构可能性。

这次经历不是要推崇纯函数式编程,而是展示函数式概念如何让任何代码库都更健壮、更可组合。在JavaScript中,我们可以灵活地根据需要混合这些模式和其他方法。这些模式在JavaScript的函数友好环境中感觉很自然,帮助我写出了更易维护和测试的代码。

本文内容仅供个人学习/研究/参考使用,不构成任何决策建议或专业指导。分享/转载时请标明原文来源,同时请勿将内容用于商业售卖、虚假宣传等非学习用途哦~感谢您的理解与支持!

链接: https://fly63.com/article/detial/13089

js设计模式之单例模式,javascript如何将一个对象设计成单例

单例模式是我们开发中一个非常典型的设计模式,js单例模式要保证全局只生成唯一实例,提供一个单一的访问入口,单例的对象不同于静态类,我们可以延迟单例对象的初始化,通常这种情况发生在我们需要等待加载创建单例的依赖。

前端设计模式:从js原始模式开始,去理解Js工厂模式和构造函数模式

工厂模式下的对象我们不能识别它的类型,由于typeof返回的都是object类型,不知道它是那个对象的实例。另外每次造人时都要创建一个独立的person的对象,会造成代码臃肿的情况。

JavaScript设计模式_js实现建造者模式

建造者模式:是将一个复杂的对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。工厂类模式提供的是创建单个类的模式,而建造者模式则是将各种产品集中起来进行管理,用来创建复合对象

html和xhtml,DOCTYPE和DTD,标准模式和兼容模式

主要涉及知识点: HTML与XHTML,HTML与XHTML的区别,DOCTYPE与DTD的概念,DTD的分类以及DOCTYPE的声明方式,标准模式(Standard Mode)和兼容模式(Quircks Mode),标准模式(Standard Mode)和兼容模式(Quircks Mode)的区别

前端四种设计模式_JS常见的4种模式

JavaScript中常见的四种设计模式:工厂模式、单例模式、沙箱模式、发布者订阅模式

javascript 策略模式_理解js中的策略模式

javascript 策略模式的定义是:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。 策略模式利用组合,委托等技术和思想,有效的避免很多if条件语句,策略模式提供了开放-封闭原则,使代码更容易理解和扩展, 策略模式中的代码可以复用。

javascript观察者模式_深入理解js中的观察者模式

javascript观察者模式又叫发布订阅模式,观察者模式的好处:js观察者模式支持简单的广播通信,自动通知所有已经订阅过的对象。存在一种动态关联,增加了灵活性。目标对象与观察者之间的抽象耦合关系能够单独扩展以及重用。

Vue中如何使用方法、计算属性或观察者

熟悉 Vue 的都知道 方法methods、计算属性computed、观察者watcher 在 Vue 中有着非常重要的作用,有些时候我们实现一个功能的时候可以使用它们中任何一个都是可以的

我最喜欢的 JavaScript 设计模式

我觉得聊一下我爱用的 JavaScript 设计模式应该很有意思。我是一步一步才定下来的,经过一段时间从各种来源吸收和适应直到达到一个能提供我所需的灵活性的模式。让我给你看看概览,然后再来看它是怎么形成的

Flutter 设计模式 - 简单工厂

在围绕设计模式的话题中,工厂这个词频繁出现,从 简单工厂 模式到 工厂方法 模式,再到 抽象工厂 模式。工厂名称含义是制造产品的工业场所,应用在面向对象中,顺理成章地成为了比较典型的创建型模式

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!