本文将使用JS原生的方法对数组和对象进行不可变的操作。
数组不变性
1.往数组中添加新的item
下面以往数组中添加一个0为例子:
1.使用push
const addCounter = (list) => {
list.push(0);
return list;
}
通过push, unshift, shift, pop方法改变数组长度, 都会导致原数组改变, 这不符合函数编程中的不变性质
2.使用concat
const addCounter = (list) => {
return list.concat([0])
}
使用concat将返回一个新的数组, 原数组不改变, 这正是函数编程中常常需要的
3.使用ES6 spread操作符
改进concat:
const addCounter = (list) => {
return [
...list,
0
];
}
2.移除数组中的某个元素
下面以数组[1, 2, 3]移除2,变为[1, 3]为例子:
1.使用splice
const removeCounter = (list, index) => {
list.splice(index, 1);
return list;
}
使用splice会改变原数组
2.使用slice + concat
const removeCounter = (list, index) => {
return list
.slice(0, index)
.concat(list.slice(index + 1))
};
slice截取数组中的部分items,返回新的数组, 不改变原数组
3.使用ES6 spread 操作符
同样上面的例子也可以进行改进
const removeCounter = (list, index) => {
return [
...list.slice(0, index),
...list.slice(index + 1)
];
}
3.数组长度不变,改变数组中元素
上面2个例子都是对数组长度进行改变,下面对数组的某个元素进行改变,比如[2, 4, 6]变为[2, 5, 6]
1.直接对指定元素操作
const incrementCounter = (list, index) => {
list[index]++;
return list;
}
这样做会改变原数组
2.使用slice + concat
const incrementCounter = (list, index) => {
return list
.slice(0, index)
.concat([list[index]++])
.concat(list.slice(index + 1))
}
这种方法和前面提到的方法其实是一类的, 都是利用slice, concat不会改变原数组,并且返回数组,这样可以进行链式操作
3.使用ES6 spread 操作符
const incrementCounter = (list, index) => {
return [
...list.slice(0, index),
list[index]++,
...list.slice(index + 1)
];
}
上面的例子可以使用 expect.js
和 deep-freeze.js
两个库进行测试
JSBIN 地址如下: immutable Array test
对象不变性
改变对象中的某个属性, 比如将
let todo = {
id: 0,
task: "learn Redux",
isCompleted: false
};
变为:
todo = {
id: 0,
task: "learn Redux",
isCompleted: true
};
1.使用对象属性直接赋值
const toggleTask = (todo) => {
todo.isCompleted = !todo.isCompleted;
return todo;
}
这样做会改变todo对象,这不是我们希望看到的
2.使用 ES6 Object.assign()
const toggleTask = (todo) => {
return Object.assign(
{},
todo,
{isCompleted: !todo.isCompleted}
);
}
Object.assign将从第二个参数后面的属性都添加到第一个参数里面, 这样就不会改变原来的对象, 使用这个方法时注意使用babel-polyfill
3.使用ES7 Object spread
我们知道ES6引入了Array spread操作符, ES7对对象也引入了相应的Object spread操作符
const toggleTask = (todo) => {
return {
...todo,
isCompleted: !todo.isCompleted
};
}
其中重复的属性在ES6中不会出现错误,后面添加的属性会覆盖掉前面添加的属性, 比如:
{
name: "James Sawyer",
age: 29,
job: "soft engineer"
}
// 改变上面的属性
{
name: "James Sawyer",
age: 29,
job: "soft engineer",
age: 30 // 重写上面的29
}
额外的测试库
expect.js用于测试某个值,然后查看返回的期望值是否一致;
deep-freeze.js 测试库,用于改变对象的扩展性
上面的例子为:
// 引入上面两个库, npm或script的方式
<script src="https://wzrd.in/standalone/expect@latest"></script>
<script src="https://wzrd.in/standalone/deep-freeze@latest"></script>
const toggleTask = (todo) => {
return {
...todo,
isCompleted: !todo.isCompleted
};
};
const testToggleTask = () => {
// 对象之前的值
const todoBefore = {
id: 0,
task: "learn Redux",
isCompleted: false
};
// 调用上面函数toggleTask之后的值
const todoAfter = {
id: 0,
task: "learn Redux",
isCompleted: true
};
// 冻结todoBefore,如果改变该对象,则会抛出异常
deepFreeze(todoAfter);
// 使用expect.js中的方法
expect(
toggleTask(todoBefore)
).toEqual(todoAfter);
};
// 调用上面的测试函数
testToggleTask();
// 如果上面的函数没有异常则会在console中输出下面这句话
console.log("Test all passed");
总结
函数的immutable在函数式编程规范中是很重要的概念, 除了上面介绍的方法之外, 还可以引入一些库对数组对象进行操作, 使之具有不变性, 比如react中使用的 immutable.js和react-addons-update等工具库。