JavaScript数据类型对函数式编程的影响示例解析

作者:木棉时间:2026-10-27 01:36:53浏览:347

本文深入探讨JavaScript数据类型对函数式编程的影响。文章指出,Object类型的可变性容易导致纯函数产生副作用。通过对比原始类型与引用类型,分析了数据共享带来的问题,并介绍了深拷贝、Immutable.js以及immer.js等解决可变数据影响的具体方案,旨在帮助开发者在JS中更好地实践函数式编程。

前言

作为JavaScript函数式编程系列的第二篇,建议读者先阅读前作:

  • 一文理解JavaScript中的函数式编程的概念
  • JavaScript数据类型对函数式编程的影响
  • 不可变数据方案之immer.js实现探索

承接前文对函数式编程概念的阐述,本文将继续梳理JavaScript数据类型如何影响函数式编程的实践。

函数式编程的核心在于纯函数与副作用隔离。为确保纯函数的纯粹性,其参数或内部引用的外部数据应保持不可变。然而,JavaScript中的数据类型并非全部不可变,这种可变性可能导致纯函数失去纯粹性。

本文旨在达成以下目标:

  • 探究JavaScript数据类型的可变性根源。
  • 分析JavaScript中的可变数据如何导致纯函数失效。
  • 探讨解决可变数据影响的方法。

JavaScript中数据的可变性分析

JavaScript包含以下8种数据类型:

  • null
  • undefined
  • boolean
  • number
  • symbol -- 在 es6 中被加入
  • bigint -- es6+ 被加入
  • object

需注意:

在JavaScript中,变量本身没有类型,只有值具有类型。变量可在任意时刻持有任意类型的值。

原始类型(基本类型)

上述8种类型中,除object外均为原始类型。原始类型存储的是具体值,具有以下特征:

  • 无法直接调用方法。
  • 原始类型的值不可被修改。修改变量的值,实质是让变量指向一个新的值,而非改变原有值。

需注意:

  • '1'.toString()或false.toString()之所以能执行,是因为原始值被强制转换为了String类型的对象,从而可以调用toString方法。
  • 关于null,许多人误认为它是对象类型,这是错误的。typeof null返回object,这是JavaScript长期存在的一个Bug,且预计不会修复,因为大量现有Web内容依赖此行为。注:在JS早期32位系统中,为了性能采用低位存储类型信息,000开头代表对象,而null表示为全零,因此被错误判断为object。尽管内部类型判断代码已更新,但该Bug仍被保留。

对象类型(引用类型)

除原始类型外,object属于对象类型。与原始类型不同,原始类型存储值,而对象类型存储内存地址。

经典示例:

var c = 1;
var d = c;
d = 2;
console.log(c === d) // false
var a = {
    name: "张三",
    age: 20
}
var b = a;
b.age = 21;
console.log(a.age === b.age) // true

示例中,变量a的值赋给变量b,b修改age属性后,a的age属性也随之改变。这是因为var b = a将a的对象引用地址赋给了b,此时a和b指向内存中的同一数据。

而c给d赋值的是值,并非引用,相当于复制了一份数据。

由此可知,原始类型的数据是不可变的,而对象类型的数据是可变的。

JavaScript为何会导致纯函数不纯?

JavaScript中的对象类型数据具有可变性,这种可变性意味着不确定性。在纯函数中使用不确定数据会破坏其纯粹性,因为它违背了纯函数的特征:不受外界影响,也不影响外界。

以下通过一个例子说明:

A同学编写了如下代码,初始化了一个名为“zhangsan”的用户。

export const defaultUserInfo = {
    name: "名称",
    age: 20,
    hobby: ["玩耍"]
};
export function initUser(userTemplate, name, age) {
    const newUser = userTemplate;
    newUser.name = name;
    newUser.age = age;
    return newUser;
}
const zhangsan = userInit(userDefaultInfo, "zhangsan", 21);

随后,B同学在开发其他页面时,看到有初始化用户信息的方法,直接复制该方法并初始化了一个“lisi”用户。

import { defaultUserInfo, initUser } from "xxx模块"。
const lisi = userInit(userDefaultInfo, "lisi", 21);

测试时,B同学发现初始化的用户信息正确,便未检查A同学之前的代码是否正确。上线后发现所有用户都变成了lisi。因为userDefaultInfo是引用类型,userInit(userDefaultInfo, "xxx", xx)操作的都是内存中的同一个对象。原因在于A和B开发者犯了一个错误:将可变数据传递到了userInit函数内部进行处理,即便进行了浅层拷贝,问题依然存在。根本原因在于向函数传递了可变数据。

我们校验纯函数有效性的关键依据始终是“针对已知输入,能否给出符合预期的输出”。上述例子中的initUser函数并未违背此规则,但在可变数据的影响下,它产生了副作用,影响了外界已有的数据。

如何解决可变数据的影响?

数据拷贝

从函数调用者的角度来看,既然问题根源在于传递的是可变数据,那么可以复制一份数据传递给函数内部使用。无论函数如何修改,都不会影响外界其他数据。

例如,在使用前例中的initUser函数时,先拷贝一份数据:

function copyFunc(object) {
    return JSON.parse(JSON.string(object));
}
const zhangsan = userInit(copyFunc(userDefaultInfo), "zhangsan", 21);
const lisi = userInit(copyFunc(userDefaultInfo), "lisi", 21);
console.log(zhangsan.name === lisi.name); // false

将拷贝后的数据传递给userInit函数,即可避免问题。这里的copyFunc仅适用于部分数据类型,对许多类型不支持,具体可参考关于JSON.parse(JSON.stringify(obj))实现深拷贝应该注意的坑这篇文章。

从被调用函数的角度来看,在使用object类型数据时,函数内部应尽量避免修改外界传入或使用的object数据(无论是通过参数传递还是直接使用外部对象)。若需修改,建议先拷贝一份再操作。

例如:

export function initUser(userTemplate, name, age) {
    const newUser = copyFunc(userTemplate);
    newUser.name = name;
    newUser.age = age;
    return newUser;
}

使用不可变数据方案

当数据拷贝体积较大时,会出现性能问题,因此出现了不可变数据方案。

目前常见的不可变数据方案有两种:Immutable.js和immer.js。它们都能在操作数据后返回新数据,而不影响之前的数据。

Immutable.js实现了持久化数据结构,其原理说明(引用自immutable.js和immer):

  • 使用旧数据创建新数据时,需保证旧数据同时可用且不变。为避免deepCopy复制所有节点带来的性能问题,immutable采用结构共享方式,即如果对象树中一个节点改变,只修改该节点及其受影响的父节点,其他节点共享。
  • immutable-js使用了另一套数据结构API,它会将原生数据类型转化为immutable-js内部对象。

因此,使用Immutable.js必须严格遵循其自定义的数据操作方法。

immer.js利用es6的Proxy拦截数据操作来实现,具体原理可查阅剖析Immer.js工作原理与设计模式一文,也可在网上搜索相关资料。

总结

  • 分析JavaScript中数据的可变性根源:Object数据结构。
  • 探索其可变数据对纯函数的影响:Object数据的不确定性。
  • 分析解决可变数据影响的方法:深拷贝和使用不可变数据结构。

参考:

  • JavaScript 函数式编程实践指南
  • immutable.js 和 immer)

以上就是JavaScript数据类型对函数式编程的影响示例解析的详细内容,更多关于JavaScript数据类型函数式编程的资料请关注本站其它相关文章!

作者最新文章