数组和对象的不变性

本文将使用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.jsdeep-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的方式

  

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.jsreact-addons-update等工具库。

你可能感兴趣的:(数组和对象的不变性)