如何理解js中的不可变对象

733 阅读3分钟

参照文档:

react 更新已渲染的元素React 元素是不可变对象MDN # Object.freeze()

1.基本数据类型和引用数据类型

在js中,所有的基本数据类型(Undefined, Null, Boolean, Number, String, Symbol)都是不可变的,但是对象(Object,Array,Function)是可变的。 基本数据类型值存在栈中,而 object 存在栈中的是地址值(对堆中地址的引用)。下面obj1和obj2指向同一个地址,所以修改obj2的某个属性,obj1也同步改变,这就是所谓可变的对象。

    var a = 1;
    var b = a;
    b = 2;
    console.log('a', a)

    const obj1 = {
        name: 'bwf',
        age: 18,
        job: {
            name: 'web',
            salary: '15k'
        }
    }
    const obj2 = obj1;
    obj2.name = 'wmy'
    console.log('obj1', obj1)

2那么如果创建不可变的对象呢?

第一次尝试 const !

注意:const实际上保证的,并不是变量的值不得改动,而是变量指向的内存地址不得改动。所以上面 的obj1虽然是用const定义的,但是内容还是被改变了

继续尝试:Object.freeze()

Object.freeze(obj1);
obj2.age = 30;
console.log('obj1', obj1) //obj1的age没有被改变
obj2.job = {
    name: 'java',
    salary: '20k'
}
console.log('obj1', obj1) //obj1的job没有被改变 对象的第一层拷贝
obj2.job.name = 'web'
console.log('obj1', obj1) //obj1的job的name还是被改变le 对象的深层次拷贝

我们可以发现仍然可以更改嵌套对象。

最终解决方案 为了使对象完全不可变,我们还需要freeze()所有的嵌套对象。例如(方法来自MDN

 function deepFreeze(object) {
    // Retrieve the property names defined on object
    const propNames = Object.getOwnPropertyNames(object);

    // Freeze properties before freezing self

    for (const name of propNames) {
        const value = object[name];

        if (value && typeof value === "object") {
            deepFreeze(value);
        }
    }

    return Object.freeze(object);
}

3 react和redux中的数据都是不可变对象

不可变意味着不能直接修改原数据,即state的值。如果需要修改,需要对原数据进行拷贝

react

数组

bad :下面示例是不会触发视图更新的,因为push方法虽然改变了原数组,但是是同一个引用,react的更新机制是浅比较,如果前后2次的地址相同,它会认为数据没有更新,从而不会更新视图。

const addTodo = (content)=>{
    todoList.push(
        {
            id:id++,
            content
        }
    )
    setTodoList(todoList)
}

创建不可变对象的方式一:concat

let id = 0;
const todoListInitial =[
    {
        id,
        content:'吃饭'
    },
] 
const [todoList,setTodoList] = useState(todoListInitial)
const addTodo = (content)=>{
    const newTodoList = todoList.concat()
    newTodoList.push(
        {
            id:id++,
            content
        }
    )
    setTodoList(newTodoList)
}

创建不可变对象的方式一:扩展运算符

let id = 0;
const todoListInitial =[
    {
        id,
        content:'吃饭'
    },
] 
const [todoList,setTodoList] = useState(todoListInitial)
const addTodo = (content)=>{
    todoList.push(
        {
            id:id++,
            content
        }
    )
    setTodoList([...todoList])
}

对象

const Person = ()=>{
    const p = {
        name:'bwf',
        age:18
    }
    const [bwf,setBwf] = useState(p);
    const addSex = ()=>{
        // 错误方式:下面这样写因为是同一个引用地址,不会触发视图更新
        bwf.sex= '女'
        setBwf(bwf)

        // 正确方式一 :对象扩展运算符
        bwf.sex= '女'
        setBwf({...bwf});
        
        // or
        bwf.sex= '女'
        setBwf({...bwf,sex:'女'})

        // 正确方式二 Object.assign
        const newBwf = Object.assign({},bwf)
        newBwf.sex= '女'
        setBwf(newBwf);
    }
    return (
       <div>
        {Object.keys(bwf).map(item=><li>{bwf[item]}</li>)}
        <button onClick={addSex}>点我添加一个性别属性</button>
       </div>
    )
}

react性能优化中也有提到

class ListOfWords extends React.PureComponent {
  render() {
    return <div>{this.props.words.join(',')}</div>;
  }
}

class WordAdder extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      words: ['marklar']
    };
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    // 这部分代码很糟,而且还有 bug:数据改了,视图不更新
    const words = this.state.words;
    words.push('marklar');
    this.setState({words: words});
    我修改的方式
     // this.setState({
     //     words:words.concat()
     //  });
    
      this.setState({
        words:[...words]
      });
  }

  render() {
    return (
      <div>
        <button onClick={this.handleClick} />
        <ListOfWords words={this.state.words} />
      </div>
    );
  }
}

避免该bug最简单的方式是避免更改你正用于 props 或 state 的值。例如,上面 handleClick 方法可以用 concat 重写:

下面使用函数式更新state,我可以借鉴下

handleClick() {
  this.setState(state => ({
    words: state.words.concat(['marklar'])
  }));
}
handleClick() {
  this.setState(state => ({
    words: [...state.words, 'marklar'],
  }));
};

又例如,我们有一个叫做 colormap 的对象。我们希望写一个方法来将 colormap.right 设置为 'blue'。我们可以这么写:

function updateColorMap(colormap) {
  colormap.right = 'blue';
}

为了不改变原本的对象,我们可以使用 Object.assign 方法:

function updateColorMap(colormap) {
  return Object.assign({}, colormap, {right: 'blue'});
}

或者对象扩展属性

function updateColorMap(colormap) {
  return {...colormap, right: 'blue'};
}

当处理深层嵌套对象时,以 immutable (不可变)的方式更新它们令人费解。如遇到此类问题,请参阅 Immer 或 immutability-helper。这些库会帮助你编写高可读性的代码,且不会失去 immutability (不可变性)带来的好处。

redux 中的不可变数据