本篇文章是JavaScript 函数式编程 学习系列第二篇,感兴趣也可以先去看第一篇:
- 一文理解JavaScript中的函数式编程的概念
- JavaScript数据类型对函数式编程的影响
- 不可变数据方案之immer.js实现探索
前言
前文 一文理解JavaScript中的函数式编程的概念 中写了函数式编程的概念,本篇文章继上文之后,来梳理 JavaScript 数据类型对函数式编程的影响。
函数式编程编程的核心就是 纯函数
和隔离 副作用
,为了让 纯函数
保持纯粹,纯函数的参数或者内部引用的外部数据应该是不可变数据。但 JavaScript 中的数据类型并不是都是不可变的,而数据类型的可变性,很有可能让 纯函数
变的不纯。
因此,本篇文章的目的有两点:
- 探索 JavaScript 的数据类型来了解的可变数据的根源。
- JavaScript 的可变数据数据是怎么让
纯函数
变得不纯的? - 如何解决
可变数据
的影响?
JavaScript中 的数据类型中的可变数据
在 JavaScript 中,数据类型有以下 8 种:
string
number
boolean
null
undefined
symbol
-- 在 es6 中被加入bigint
-- es6+ 被加入object
注意点:
在 JavaScript 中,变量是没有类型的,值才有类型。变量可以在任何时候,持有任何值。
原始类型(基本类型)
上面 8 中类型除了 object
,其他都是原始类型,原始类型存储的都是值,其特点有两点:
- 没有方法可以直接调用
- 原始类型的数据是不可被改变的,改变一个变量的值,并不是把值改变了,而是让变量拥有新的值。
注意点:
'1'.toString()
或者false.toString()
等可以用的原因是被强制转换成了 String 类型也就是对象类型,所以可以调用toString
函数。- 对于
null
来说,很多人会认为它是个对象类型,其实是错误的。typeof null
会输出object
,这只是 JS 存在的一个悠久 Bug,而且好像永远不会也不会被修复,因为有太多已经存在的 web 的内容依存着这个 bug。注: 在 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 中的对象类型的数据是可变,而可变性,就代表了不确定性,纯函数
中使用了不确性的数据就会导致不纯,因为其违背了 纯函数
的特征:不受外界影响,不影响外界。
比如,在管理系统有一个权限处理的代码:
export const userA = {
name: "小明",
info: {
age: 20,
hobby: ["篮球"]
},
};
// 获取用户信息文本数据
export function getUserInfoText(userInfo) {
return `${userInfo.name} 今年 ${userInfo.info.age} 岁`;
}
在小明的专属页面显示小明的详细信息,运行了 getUserInfoText(userA)
,页面上显示 “小明 今年 20 岁”。
这时候要新建一个小张的专属页面页面,并显示小张的信息,然后爱好也小明相同,于是想拿来直接使用。
const userB = userA;
userB.name = "小张"
userB.info.age = 21
最后发现小明的专属页面上也显示了 “小张 今年 21 岁”。
这就是 getUserInfoText
函数在不知不觉收到了影响,命名参数中传递的还是 userA ,但返回的结果却不相同,违背了 纯函数
“不受外界影响,不影响外界” 的特征。
如何解决可变数据的影响?
数据拷贝
从使用函数方的角度来看,既然造成这个问题的原因是因为传递进去的数据是 可变数据
,那么我就复制一份数据传递给函数内部使用,随便你怎么修改,都不会影响外界其他数据。
比如我们使用前面新增 userB 时,直接进行深拷贝:
const userB = JSON.parse(JSON.string(userA));
userB.name = "小张"
userB.info.age = 21
console.log(userB.name === userA.name); // false
console.log(userB.info.age === userA.info.age); // false
进行拷贝后的数据传递给 getUserInfoText
函数,就不会出现问题了。这里的 JSON.parse(JSON.string(xxx))
只能针对部分数据类型,对不少类型是不支持的,具体可以去看一下 关于JSON.parse(JSON.stringify(obj))实现深拷贝应该注意的坑 这篇文章。
使用不可变数据方案
拷贝的数据比较大的时候,会出现性能问题,因此出现了不可变数据的方案。
现在不可变数据常见的有两种: 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 数据的不确定性。 - 分析了如何解决
可变数据
的影响:深拷贝
和使用不可变数据结构
.
参考:
开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 18 天,点击查看活动详情