在Redux中, reducer函数遵循不可变更新模式, 而对象和数组在JavaScript中是通过引用传递(pass by reference), 因此如果直接修改对象和数组, 原对象和数组就会改变, 于是我们需要对对象和数组的拷贝进行修改, 下面整理了一些修改时要避免以及推荐使用的方法.

可以在jsbin中输入代码用以测试效果.

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<script src="https://unpkg.com/expect/umd/expect.min.js"></script>
<script src="https://wzrd.in/standalone/deep-freeze@latest"></script>
<title>JS Bin</title>
</head>
<body>
</body>
</html>

在html模板中引入expect库用以断言, deep-freeze库用以避免可变数据(avoid mutation).

数组

插入元素

❎ push

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const addCounter = (list) => {
list.push(0);
return list;
};

const testAddCounter = () => {
const listBefore = [];
const listAfter = [0];
deepFreeze(listBefore);

expect(
addCounter(listBefore)
).toEqual(listAfter);

};

testAddCounter();
console.log('All tests passed');

上述代码中, 使用push在list中添加元素, 尽管达到要求插入了元素, 但是修改了listBefore, 无法通过deepFreeze那一部分的测试, 这在redux应用中是不被允许的, 我们应该要避免初始state的改变, 有两种方法可以解决这个问题:

  1. 用concat方法代替push方法, 修改的是原数组的拷贝
  2. 使用ES6扩展运算符(...)

代码如下:

✅ concat或spread operator(扩展运算符...)

1
2
3
4
5
6
const addCounter = (list) => {
// 1. concat()
return list.concat([0]);
// 2. spread operator
// return [...list, 0];
};

移除元素

❎ splice

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const removeCounter = (list, index) => {
list.splice(index, 1);
return list;
};

const testRemoveCounter = () => {
const listBefore = [0, 10, 20];
const listAfter = [0, 20];
deepFreeze(listBefore);

expect(
removeCounter(listBefore, 1)
).toEqual(listAfter);

};

testRemoveCounter();

console.log('All tests passed');

使用splice方法直接改变listBefore, 不可取, 改进版本:

✅ slice和...

1
2
3
4
5
6
7
const removeCounter = (list, index) => {
return [
...list.slice(0, index),
...list.slice(index + 1)
];
// 拷贝index之前和之后的元素并将两者合并
};

使用扩展运算符和slice方法, 拷贝index之前和之后的元素, 并将两者合并后返回, 不改变原有的list, 其中扩展运算符也可以用cancat方法代替实现, 但扩展运算符更简便.

修改数组中某个元素值

❎ 直接修改元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const incrementCounter = (list, index) => {
list[index]++;
return list;
};

const testIncrementCounter = () => {
const listBefore = [0, 10, 20];
const listAfter = [0, 11, 20];

deepFreeze(listBefore);
expect(
incrementCounter(listBefore, 1)
).toEqual(listAfter);

};

testIncrementCounter();

console.log('All tests passed');

修改某个元素值所使用的方法和移除元素所使用方法类似, 都是用slice和…创建数组的拷贝后再进行相应操作.

✅ 修改拷贝

1
2
3
4
5
6
7
const incrementCounter = (list, index) => {
return [
...list.slice(0,index),
list[index] + 1,
...list.slice(index + 1)
];
};

对象


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const toggleTodo = (todo) => {
todo.completed = !todo.completed;
return todo;
};

const testToggleTodo = () => {
const todoBefore = {
id: 0,
text: 'Learn Redux',
completed: false
};
const todoAfter = {
id: 0,
text: 'Learn Redux',
completed: true
};
deepFreeze(todoBefore);

expect(
toggleTodo(todoBefore)
).toEqual(todoAfter);

};

testToggleTodo();
console.log('All tests passed.');

有一种解决方法是手动对原对象进行拷贝, 然后仅修改需要修改的那一部分属性, 尽管能通过测试, 但是如果之后在对象中添加属性, 有可能会忘记更新拷贝的对象, 所以这种方法不可取.

1
2
3
4
5
6
7
const toggleTodo = (todo) => {
return {
id: todo.id,
text: todo.text,
completed: !todo.completed
};
};

✅ ES6语法: Object.assign()

1
2
3
4
5
const toggleTodo = (todo) => {
return Object.assign({}, todo, {
completed: !todo.completed
});
};

✅ spread operator(...)

1
2
3
4
5
6
const toggleTodo = (todo) => {
return {
...todo,
completed: !todo.completed
};
};

参考