Skip to content

一文搞懂深浅拷贝及 js 相关插件

1.引用拷贝(赋值)

直接将一个对象变量赋值给一个变量,这样的作法只是复制了对象的内存地址,两个变量指向了一个对象,任何一个变量操作了对象的属性都会影响到另一个变量,这种对于同一个对象的操作,本质上根本就没有实现复制,因为还是只有一个对象,所以引用拷贝并不算对象拷贝

2.浅拷贝

注意:浅拷贝不是直接赋值!!!而是一种单独的拷贝方式!

浅拷贝是一种复制对象的技术,它创建一个新的对象,来接受你要重新复制或引用的对象值,新对象与原始对象的第一级属性(如基本类型属性和对象引用)具有相同的值。如果对象属性是基本的数据类型,复制的就是基本类型的值给新对象;但如果属性引用数据类型,复制的就是内存中的地址,而不会递归地复制引用对象本身,如果其中一个对象改变了这个内存中的地址,肯定会影响到另一个对象。

1. object.assign(传空对象)拷贝对象

object.assign 是 ES6 中 object 的一个方法,该方法可以用于JS 对象的合并等多个用途,其中一个用途就是可以进行浅拷贝

该方法的第一个参数是拷贝的目标对象,后面的参数是拷贝的来源对象(也可以是多个来源)。 ​

object.assign 的语法为:Object.assign(target, …sources)

java
const target = {};

const source = { a: { b: 1 } };

Object.assign(target, source); //只要target是空对象就可以了!

console.log(target); // { a: { b: 1 } };

但是使用 object.assign 方法有几点需要注意: ​

  • 不会拷贝对象的继承属性
  • 不会拷贝对象的不可枚举的属性
  • 可以拷贝 Symbol 类型的属性。
java
const obj1 = {
  a: {
    b: 1
  },
  c: 1,
  sym: Symbol(1)
};

Object.defineProperty(obj1, 'innumerable', { //给obj1对象定义一个innumerable属性

  value: '1',

  enumerable: false

});

const obj2 = {};

Object.assign(obj2, obj1)

obj1.a.b = 2;
obj1.c = 2;

console.log('obj1', obj1); // {a: {b: 2}, c: 2, sym: Symbol(1), innumerable: "1"}

console.log('obj2', obj2); // {a: {b: 2}, c: 1, sym: Symbol(1)}

从上面的样例代码中可以看到,利用 object.assign 也可以拷贝 Symbol 类型的对象,但是如果到了对象的第二层属性 obj1.a.b 这里的时候,前者值的改变也会影响后者的第二层属性的值,说明其中依旧存在着访问共同堆内存的问题,也就是说这种方法还不能进一步复制,而只是完成了浅拷贝的功能。 ​

2. 对象扩展运算符 拷贝对象

对象的扩展运算符的语法为:let cloneObj = { …obj };

java
/* 对象的拷贝 */

const obj = {
  a: 1,
  b: {
    c: 1
  }
}

const obj2 = {
  ...obj
}

obj.a = 2

console.log(obj) //{a:2,b:{c:1}}
console.log(obj2); //{a:1,b:{c:1}}

obj.b.c = 2

console.log(obj) //{a:2,b:{c:2}}
console.log(obj2); //{a:1,b:{c:2}}

/* 数组的拷贝 */

let arr = [1, 2, 3];

let newArr = [...arr]; //跟arr.slice()是一样的效果

扩展运算符 和 object.assign 有同样的缺陷,也就是实现的浅拷贝的功能差不多,但是如果属性都是基本类型的值,使用扩展运算符进行浅拷贝会更加方便。 ​

3. concat 拷贝数组(不传参)

数组的 concat 方法其实也是浅拷贝,所以连接一个含有引用类型的数组时,需要注意修改原数组中的元素的属性,因为它会影响拷贝之后连接的数组。不过 concat 只能用于数组的浅拷贝,使用场景比较局限。 ​

js
const arr = [1, 2, 3, { a: 1 }];
const newArr = arr.concat(); //不传参数即可浅拷贝一个新的对象
newArr[1] = 0;
console.log(arr); // [ 1, 2, 3, {a: 1} ]
console.log(newArr); // [ 1, 0, 3, { a: 1 } ]
newArr[3].a = 4;
console.log(arr); // [ 1, 2, 3, {a: 4} ]
console.log(newArr); // [ 1, 2, 3, {a: 4} ]

4. 扩展运算符 拷贝数组

扩展运算符也是典型的对于数组的浅拷贝:

js
const arr = [1, 2, 3, { a: 1 }];
const newArr = [...arr];
newArr[1] = 0;
console.log(arr); // [ 1, 2, 3, {a: 1} ]
console.log(newArr); // [ 1, 0, 3, { a: 1 } ]
newArr[3].a = 4;
console.log(arr); // [ 1, 2, 3, {a: 4} ]
console.log(newArr); // [ 1, 2, 3, {a: 4} ]

5. slice 拷贝数组

slice 方法也比较有局限性,因为它仅仅针对数组类型。slice 方法会返回一个新的数组对象,这一对象由该方法的前两个参数来决定原数组截取的开始和结束位置**(第三个参数表示步进值,用于确定从数组中提取元素的间隔)**,是不会影响和改变原始数组的。但是,数组元素是引用类型的话,也会影响到原始数组。

slice 的语法为:arr.slice(begin, end);

java
const arr = [1, 2, { val: 4 }];

const newArr = arr.slice();

newArr[2].val = 5;
newArr[1] = 3;

console.log(arr);  //[ 1, 2, { val: 5 } ]

从上面我们可以看出,这就是浅拷贝的限制所在——它只能拷贝一层对象。如果存在对象的嵌套,那么浅拷贝将无能为力。因此深拷贝就是为了解决这个问题而生的,它能解决多层对象嵌套问题,彻底实现拷贝。 ​

我们总结一下浅拷贝的原理: ​

  1. 对基础类型做一个最基本的一个拷贝;
  2. 对引用类型开辟一个新的存储,并且拷贝一层对象属性。

以下是对浅拷贝的简单实现:

java
const shallowClone = (target) => {

  if (typeof target === 'object' && target !== null) {

    const cloneTarget = Array.isArray(target) ? []: {};

    for (let prop in target) {

      if (target.hasOwnProperty(prop)) {

          cloneTarget[prop] = target[prop];

      }

    }

    return cloneTarget;

  } else {

    return target;

  }

}

3.深拷贝

浅拷贝只是创建了一个新的对象,复制了原有对象的基本类型的值,而引用数据类型只拷贝了一层属性,再深层的还是无法进行拷贝。深拷贝则不同,对于复杂引用数据类型,其在堆内存中完全开辟了一块内存地址,并将原有的对象完全复制过来存放。

深拷贝后的对象与原始对象是相互独立、不受影响的,彻底实现了内存上的分离。 ​

总的来说,深拷贝的原理可以总结如下: ​

将原对象从内存中完整地拷贝出来(递归地复制所有嵌套对象的值)一份给新对象,并从堆内存中开辟一个全新的空间存放新对象,且新对象的修改并不会改变原对象,二者实现真正的分离。这意味着新对象和原始对象之间没有任何共享的引用。

1. JSON.stringify

把一个对象序列化成为 JSON 的字符串,并将对象里面的内容转换成字符串,最后再用 JSON.parse() 的方法将 JSON 字符串生成一个新的对象。

js
const obj1 = { a: 1, b: [1, 2, 3] };

const str = JSON.stringify(obj1);

const obj2 = JSON.parse(str);

console.log(obj2); //{a:1,b:[1,2,3]}

obj1.a = 2;

obj1.b.push(4);

console.log(obj1); //{a:2,b:[1,2,3,4]}

console.log(obj2); //{a:1,b:[1,2,3]}

可以看到通过改变 obj1 的 b 属性,其实可以看出 obj2 这个对象也不受影响。 ​

但是,JSON.stringify 并不是那么完美的,它也有局限性。 ​

  • 拷贝的对象的值中如果有函数、undefined、symbol 这几种类型,经过 JSON.stringify 序列化之后的字符串中这个键值对会消失

    注意:不管 symbol 类型的是 key 还是 value,都会在 JSON.stringify 的时候丢失。

  • 拷贝 Date 引用类型会变成字符串

  • 无法拷贝不可枚举的属性;——>和 object.assign 一样

  • 无法拷贝对象的原型链(无法拷贝继承的属性);——>和 object.assign 一样

  • 拷贝 RegExp 引用类型会变成空对象

  • 对象中含有 NaN、Infinity 以及 -Infinity,JSON 序列化的结果会变成 null

  • 无法拷贝对象的循环引用,即对象成环 (obj1[key] = obj2)。

我们通过代码来看下效果。

js
let obj = {
  func: function () {
    alert(1);
  },
  obj: { a: 1 },
  arr: [1, 2, 3],
  und: undefined,
  reg: /123/,
  date: new Date(0),
  NaN: NaN,
  infinity: Infinity,
  sym: Symbol("1"),
  [Symbol("1")]: 1,
};

Object.defineProperty(obj, "innumerable", {
  enumerable: false,

  value: "innumerable",
});

console.log("obj", obj); // { NaN: NaN , arr: (3) [1, 2, 3] ,date: Thu Jan 01 1970 08:00:00 GMT+0800 (中国标准时间) {}, func: ƒ(), infinity: Infinity , obj: { a: 1 } , reg: /123/, sym: Symbol(1), und: undefined, innumerable: "innumerable" }

const str = JSON.stringify(obj);

const obj1 = JSON.parse(str);

console.log("obj1", obj1); // { NaN: null, arr: (3) [1, 2, 3], date: "1970-01-01T00:00:00.000Z", infinity: null, obj: {a: 1}, reg: {} }

对象的循环引用?

在 JavaScript 中,对象的循环引用是指两个或多个对象相互引用,形成一个环状的引用关系。这种情况可能会导致内存泄漏,因为循环引用会阻止垃圾回收器正确地清理这些对象。

以下是一个简单的示例,演示了对象的循环引用:

js
const objA = {};
const objB = {};

objA.child = objB;
objB.parent = objA;

//自己引用自己也是循环引用:
objA.child = objA;

在这个示例中,objA 包含一个指向 objB 的属性 child,而 objB 包含一个指向 objA 的属性 parent。这就形成了一个循环引用,objAobjB 互相引用。

解决对象的循环引用问题需要小心处理,以避免内存泄漏。以下是一些处理循环引用的方法:

  1. 手动解除引用:您可以手动解除循环引用,将其中一个对象的属性设置为 nullundefined,以断开引用链。

    js
    objA.child = null;
    objB.parent = null;

    这将断开 objAobjB 之间的循环引用。

  2. 使用 WeakMap:在某些情况下,可以使用 WeakMap 数据结构来管理对象之间的引用关系。WeakMap 会自动处理循环引用的问题,因为它们不会阻止对象被垃圾回收。

    您可以将 objA 包裹在 WeakMap 中,然后通过 objB 作为键来访问它,或者反过来。这样可以建立一种关联,使得一个对象可以引用另一个对象的值(而不是引用,因为相当于把对象新建了一份),而不会阻止垃圾回收。

    例如,可以将 objA 包裹在 WeakMap 中,然后将 objB 作为键来关联它:

    js
    // 创建一个 WeakMap
    const objectMap = new WeakMap();
    
    // 创建对象
    const objA = {};
    
    // 将 objA 包裹在 WeakMap 中,以便通过 objA属性 访问它
    objectMap.set(objA, objA);
    
    // 获取 objA
    const relatedObjA = objectMap.get(objA);
    
    console.log(relatedObjA === objA); // 输出 true,objA 是通过 objA属性 访问的
  3. 使用适当的设计模式:在编写代码时,尽量避免创建循环引用。使用适当的设计模式来管理对象之间的关系,以减少循环引用的发生。

在处理循环引用时,需要谨慎操作,以确保不会导致内存泄漏问题。每个应用场景都可能需要不同的解决方案,具体取决于代码的结构和需求。

2. 手写实现完整的深拷贝方法

下面是一个实现 deepClone 函数封装的例子,有几点需要注意下。 ​

  • WeakMap 是弱引用类型,可以有效防止内存泄漏;
  • 能够遍历对象的不可枚举属性以及 Symbol 类型,我们可以使用 Reflect.ownKeys 方法;
  • 利用 Object 的 getOwnPropertyDescriptors 方法可以获得对象的所有属性,以及对应的特性;
  • 结合 Object 的 create 方法创建一个新对象,并继承传入原对象的原型链;
  • 当参数为 Date、RegExp 类型,则直接生成一个新的实例返回;
js
const isComplexDataType = (obj) =>
  (typeof obj === "object" || typeof obj === "function") && obj !== null; //如果是对象或者函数类型

const deepClone = function (obj, hash = new WeakMap()) {
  if (obj.constructor === Date) return new Date(obj); //日期对象直接返回一个新的日期对象

  if (obj.constructor === RegExp) return new RegExp(obj); //正则对象直接返回一个新的正则对象

  //如果循环引用了就用 weakMap 来解决
  if (hash.has(obj)) return hash.get(obj); //属性是某个对象

  //获取描述符信息对象
  let allDesc = Object.getOwnPropertyDescriptors(obj);

  //创建一个新的对象:继承原型链,并复制属性和描述信息,相当于直接把这个对象浅拷贝了一下!
  let cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc);

  hash.set(obj, cloneObj); //把新的对象设置到WeakMap里面,供后面的属性进行使用(当要使用遍历过的对象作为属性的时候)

  for (let key of Reflect.ownKeys(obj)) {
    //Reflect.ownKeys(obj)获取所有属性,包括不可枚举的,但是不包括继承的

    cloneObj[key] =
      isComplexDataType(obj[key]) && typeof obj[key] !== "function"
        ? deepClone(obj[key], hash)
        : obj[key]; //如果是对象类型就继续递归地进行deepClone,否则直接赋值
  }

  return cloneObj;
};

// 下面是验证代码

let obj = {
  num: 0,
  str: "",
  boolean: true,
  unf: undefined,
  nul: null,
  obj: { name: "我是一个对象", id: 1 },
  arr: [0, 1, 2],
  func: function () {
    console.log("我是一个函数");
  },
  date: new Date(0),
  reg: new RegExp("/我是一个正则/ig"),
  [Symbol("1")]: 1,
};

Object.defineProperty(obj, "innumerable", {
  enumerable: false,
  value: "不可枚举属性",
});

obj = Object.create(obj, Object.getOwnPropertyDescriptors(obj));

obj.loop = obj; // 设置loop成循环引用的属性

let cloneObj = deepClone(obj);

cloneObj.arr.push(4);

console.log("obj", obj);

console.log("cloneObj", cloneObj);

Object.getOwnPropertyDescriptors(obj) 得到的内容传入 Object.create 方法是什么用法?

Object.getOwnPropertyDescriptors(obj) 返回一个包含对象所有自身属性的描述符的对象,包括属性的值、可枚举性、可配置性、可写性等信息。这个方法通常用于克隆对象,以保留对象属性的完整描述符。

注意:它返回的是一个对象,其中的键是属性名,值是对应属性的描述符对象。不是返回属性的键(即属性名列表),而只返回属性的描述符信息。

Object.create(proto, descriptors) 方法是用于创建一个新对象,并可选择传入一个属性描述符对象。该属性描述符对象包含了要添加到新对象的属性以及这些属性的描述符。

当您传入 Object.getOwnPropertyDescriptors(obj) 返回的属性描述符对象给 Object.create() 方法时,它会创建一个新对象,该新对象继承了 proto 对象的原型,并且包含了描述符对象中定义的属性及其描述符。

下面是一个示例:

js
const obj = {
  prop: 123,
};

// 获取对象的属性描述符
const descriptors = Object.getOwnPropertyDescriptors(obj);

// 使用属性描述符创建一个新对象,并继承 obj 的原型
const newObj = Object.create(Object.getPrototypeOf(obj), descriptors);

console.log(newObj.prop); // 输出 123,新对象继承了属性 prop 并保留了其描述符

在这个示例中,Object.getOwnPropertyDescriptors(obj) 获取了对象 obj 的属性描述符,然后通过 Object.create() 创建了一个新对象 newObj,该对象继承了 obj 的原型,并且具有与 obj 相同的属性及其描述符。这可以用于复制对象的属性及其描述符而不仅仅是属性的值。

3.普通的手写递归版

注意:这里实现的效果还是和 JSON.stringify 差不多,不能处理特殊的情况

js
function isObject(value) {
  //const valueType = typeof value
  //return (value !== null) && (valueType === "object" || valueType === "function")
  return value instanceof Object;
}

function deepClone(originValue) {
  // 判断传入的originValue是否是一个对象类型
  if (!isObject(originValue)) {
    return originValue; //如果不是对象,那说明是一个属性值,直接返回就可以了!后面的每一层属性值都是这里返回的!
  }

  const newObject = {}; //每次递归返回的一个新的拷贝值的对象
  for (const key in originValue) {
    //把每个属性都遍历进行重新赋值!使用了for in
    //每一个都等于递归!
    newObject[key] = deepClone(originValue[key]); //将属性值一一对应进行深拷贝,一层一层最终返回成一个完全一样的对象
  }
  return newObject; //第一层,以及后面的每一层对象,都是这里返回的!
}

// 测试代码
const obj = {
  name: "why",
  age: 18,
  friend: {
    name: "james",
    address: {
      city: "广州",
    },
  },
};

const newObj = deepClone(obj);
console.log(newObj === obj);

obj.friend.name = "kobe";
obj.friend.address.city = "成都";
console.log(newObj);

4.实现深拷贝的插件 lodash、Immutable

1.lodash 的 cloneDeep 函数

2.Immutable.js

Immutable.js 基本介绍 读作[ɪˈmjutəbəl]

Immutable.js 是一个在 JavaScript 中实现不可变数据结构的库。它提供了一组持久化的不可变数据类型,包括 List、Map、Set 等。使用 Immutable.js,你可以创建不可变的数据对象,这意味着一旦创建后就不能被修改每次对不可变数据进行修改时,实际上是创建了一个新的不可变数据对象,而原始对象保持不变。 ——> 高性能的深复制方案

原理

Immutable 实现的原理是 Persistent Data Structure(持久化数据结构),也就是使用旧数据创建新数据时,要保证旧数据同时可用且不变。同时为了避免 deepCopy 把所有节点都复制一遍带来的性能损耗,Immutable 使用了 Structural Sharing(结构共享),即如果对象树中一个节点发生变化,只修改这个节点和受它影响的父节点,其它节点则进行共享(延用原来的地址,不创建新的对象,因为只要没进行操作就没有影响)。

——> 类似于 diff 比较:原数据不变,非必要的内容不重新创建

——> 实现深复制(满足原状态不变性原则),减少内存占用,优化性能

Immutable.js 提供了一些便捷的方法来操作不可变数据,比如添加、删除、修改元素等。由于不可变数据是持久化的,这些操作实际上是创建了新的数据结构,而不是在原始数据上进行修改。这种设计有助于避免副作用,提高代码的可预测性和可维护性。

动图演示:https://upload-images.jianshu.io/upload_images/2165169-cebb05bca02f1772

优势

使用 Immutable.js 可以带来一些好处,例如:

  1. 线程安全:由于不可变数据是只读的,可以在多线程环境下安全地共享和访问,无需担心并发修改的问题。
  2. 性能优化:由于不可变数据的持久性特性,可以通过共享结构来减少内存占用,并且在对数据进行修改时,可以进行快速的结构共享和复制(实现深拷贝)。
  3. 简化状态管理:不可变数据结构适用于应用程序的状态管理,例如在 React 组件中,可以通过不可变数据来跟踪状态的变化,并更方便地进行比较和更新。

Immutable 中常用类型(Map,List)

下载:

bash
npm i immutable

1.Map(object)

作用:把普通对象包裹为不可变对象 ——> 深加工的 object

缺点:只能包裹一层,否则里层还是只能复制引用,多层还要里面再包 Map

js
import { Map } from "immutable";

var obj = {
  name: "kerwin",
  age: 100,
};

var oldImmuObj = Map(obj);
var newImmuObj = oldImmuObj.set("name", "xiaoming");
// console.log(oldImmuObj,newImmuObj) //两个对象完全不一样

//1 get获取immutalble对象某个属性
console.log(oldImmuObj.get("name"), newImmuObj.get("name"));

//2 immutable对象转换为 ===> 普通对象,然后再获取某个属性
console.log(oldImmuObj.toJS().name, newImmuObj.toJS().name);

使用案例:一层结构 ——> 可用…代替

jsx
state = {
  info:{
    name:"kerwin",
    age:100
  }
}

render() {
  return (
    <div>
      <button onClick={()=>{
          var old = Map(this.state.info) //转为immutable 对象
          var newImmu = old.set("name","xiaoming").set("age",18) //只能一个一个修改
          this.setState({
            info:newImmu.toJS() //要设置为普通对象结构
          })
        }}>click</button>
      {this.state.info.name}--
      {this.state.info.age}
    </div>
  )
}

使用案例:多层结构 ——> 较为麻烦

jsx
state = {
  info:Map({
    name:"kerwin",
    select:"aa",
    filter:Map({ //多层的话,还要给引用类型再套一个 Map 才可以!
      text:"",
      up:true,
      down:false
    })
  })
}

componentDidMount() {
  console.log(this.state.info.get("filter"))
}

render() {
  return (
    <div>
      <button onClick={()=>{
          this.setState({
            info:this.state.info.set("name","xiaoming").set("select","dwadwa")
          })
        }}>click</button>
      {this.state.info.get("name")} {/*这时取值要用 get 取*/}
    </div>
  )
}

唯一的好处:无变化时可以复用地址,这样就可以让对象的比较顺利进行,优化性能

jsx
state = {
  info:Map({
    name:"kerwin",
    select:"aa",
    filter:Map({ //多层的话,还要给引用类型再套一个 Map 才可以!
      text:"",
      up:true,
      down:false
    })
  })
}
{/* 唯一的好处:
如果这个某个属性给另一个组件作为 props用,那么包裹了 Map 之后可以被检测到相关属性并没有更改(因为 immutable 会判断如果没有变化就直接复用,复用的意思是直接使用原来的地址),不需要重新渲染,优化了性能*/}
<button onClick={()=>{
    this.setState({
  		info:this.state.info.set("name","xiaoming") //只改变 name,没有改变 filter
    })
  }}>click</button>
<Child filter={this.state.info.get("filter")}/>

class Child extends Component{
  shouldComponentUpdate(nextProps, nextState) {
    if(this.props.filter === nextProps.filter){ //比较引用地址是否相等,如果发生了变化,那么地址一定不相等,如果没变化,那么 immutable 会复用地址,地址一定相等。
      //不用 immutable 的话,父组件同样要用 json 的方式深复制更新状态(一旦深复制更新,那么就会生成不同地址的属性对象了),这时就要用json.stringify(this.props.filter) === json.stringify(nextProps.filter) 判断字符串内容是否相等,也可以!
      return false
    }
    return true
  }

  render(){
    return <div> child </div>
  }
  componentDidUpdate(){
    console.log("componentDidUpdate")
  }
}

注意:===的方式比较对象

这种比较方式只适用于判断引用是否相等,而不是判断对象的值是否相等。

判断对象值是否相等,需要转换为字符串,或者使用深层次比较的库(如 lodash 的 isEqual 方法)。

2.List(array)

作用:把普通数组包裹为不可变数组

缺点:只能包裹一层,否则里层还是只能复制引用,多层还要里面再包 List

优点:对于数组的操作方法和原生的 js 完全一样

jsx
import { List } from "immutable";
var arr = List([1, 2, 3]);

var arr2 = arr.push(4); //不会影响老的数组结构
var arr3 = arr2.concat([5, 6, 7]);
console.log(arr.toJS(), arr2.toJS(), arr3.toJS());

//使用案例:
export default class App extends Component {
  state = {
    favor: List(["aaa", "bbb", "ccc"]),
  };
  render() {
    return (
      <div>
        {
          /*immutable 对象的数组,也可以直接用 map 遍历*/
          this.state.favor.map((item) => (
            <li key={item}>{item}</li>
          ))
        }
      </div>
    );
  }
}
//push, set, unshift or splice 都可以直接用,返回一个新的immutable对象

3.merge , concat

连接 List 的方法

4.toJS

转换回 js 普通对象的方法

5.fromJS

创建复杂数据结构 immutable 对象的解决方案,配合 setIn 和 updateIn 方法来使用

案例:个人信息修改

jsx
import { List,Map } from 'immutable'
import React, { Component } from 'react'

export default class App extends Component {
  state = {
    info:Map({
      name:"kerwin",
      location:Map({
        province:"辽宁",
        city:"大连"
      }),
      favor:List(["读书","看报","写代码"])
    })
  }
  render() {
    return (
      <div>
        <h1>个人信息修改</h1>
        <button onClick={()=>{
            this.setState({
              info: this.state.info.set("name","xiaomng").set("location",this.state.info.get("location").set("city","沈阳"))
            })
          }}>修改</button>
        <div>
          {this.state.info.get("name")}
          <br/>
          {
            this.state.info.get("location").get("province")
          }
          -
          {
            this.state.info.get("location").get("city")
          }
          <br/>
          {
            this.state.info.get("favor").map((item,index)=>
                                             <li key={item}>{item}
                                               <button onClick={()=>{
                                                   console.log(index)

                                                   this.setState({
                                                     info:this.state.info.set("favor",
{/*把点击项值改为 1*/}                                                                              this.state.info.get("favor").splice(index,1))
                                                   })
                                                 }}>del</button>
                                             </li>
                                            )
          }
        </div>
      </div>
    )
  }
}

但是,我们发现这样写起来非常复杂,并且有一个问题:本质上我们应该不知道后端返回的数据结构是 obj 还是 list 还是普通类型,我们没法针对性地用 Map 或者 List 包裹!

我们可以用 fromJs 来优化:

jsx
import { fromJS } from 'immutable'
import React, { Component } from 'react'

export default class App extends Component {
  state = {
    info:fromJS({
      name:"kerwin",
      location:{
        province:"辽宁",
        city:"大连"
      },
      favor:["读书","看报","写代码"]
    })
  }

  render() {
    return (
      <div>
        <h1>个人信息修改</h1>
        <button onClick={()=>{
            {/*setIn 方法实现深修改,第一个参数为数组,指定每层的属性*/}
            this.setState({
              info: this.state.info.setIn(["name"],"xiaoming")
              .setIn(["location","city"],"沈阳")
            })
          }}>修改</button>
        <div>
          {this.state.info.get("name")}
          <br/>
          {
            this.state.info.get("location").get("province")
          }
          -
          {
            this.state.info.get("location").get("city")
          }
          <br/>
          {
            this.state.info.get("favor").map((item,index)=>
                                             <li key={item}>{item}
                                               <button onClick={()=>{
                                                   console.log(index)
// 把点击项值改为 1:setIn也可以修改数组,因为数组的本质也是对象,属性就是索引值!但是会报错,因为对象的 key 不应该是数字
// this.setState({
//     info:this.state.info.setIn(["favor",index],1)
// })
                                                   this.setState({
 {/*updateIn 方法实现数组深修改,第二个参数为回调函数,把数组换成我们 return 的值(一层其实用 update 也行)*/}                                     info:this.state.info.updateIn(["favor"],(list)=>list.splice(index,1))
                                                   })
                                                 }}>del</button>
                                             </li>
                                            )
          }
        </div>
      </div>
    )
  }
}

总结

如果只有一层(数组或者对象),没必要用 immutable,用…即可

多层复杂的再使用 immutable

并且在使用 immutable 的时候,最合理的方案是使用 fromJs,这样不用关心这个数据具体是什么数据结构!

一句话:immutable的特点就是改变就重新创建(因为是不可变的对象,变了就新建),没改变就沿用原来的地址,避免不必要的创建

使用 immutable 优化 redux 的 reducer

因为 redux 的 reducer 要求是一个纯函数,应当满足不能改写参数数据 prevState

  • 不得改写参数数据(因为参数数据 preState,就是先前的状态)

这也是最重要的一点,redux 中的状态更新并不是通过修改先前的状态对象来实现的,而是通过创建一个新的状态对象并将其替换旧的状态对象来实现的。这样做的目的是保持状态的不可变性,以便更好地管理状态变更的追踪和控制。

jsx
import { fromJS } from "immutable";
const CityReducer = (
  prevState = {
    cityName: "北京",
    //  ...
  },
  action
) => {
  //临时转换更好:给外界交代的应该永远是一个js对象
  let newState = fromJS(prevState); //保证prevState不会有任何改变
  switch (action.type) {
    case "change-city":
      // newState.cityName = action.payload
      return newState.set("cityName", action.payload).toJS();
    default:
      return prevState;
  }
};
export default CityReducer;

3.深拷贝的使用场景

在 Vue 中,通常需要使用深拷贝(deep copy)的情况包括:

  1. 数据传递:当您将一个包含对象或数组的数据传递给子组件时,如果不使用深拷贝,子组件对传递的数据的修改可能会影响父组件中的原始数据。这会导致不可预测的行为和数据污染。
  2. 数据缓存:当您需要在某一时刻捕获某个对象的快照,并且在后续操作中需要对快照进行修改,而不影响原始对象时,深拷贝是很有用的。这可用于撤销或还原操作。
  3. 数据副本:有时,您需要在不修改原始数据的情况下创建数据的独立副本,以便在不同地方使用。这可以确保不同部分的代码不会相互干扰。
  4. 避免引用循环:当数据包含循环引用时,例如对象 A 引用了对象 B,对象 B 又引用了对象 A,如果不使用深拷贝,尝试克隆这样的数据结构可能导致无限递归。深拷贝可以避免这种问题。
  5. 异步操作:当您在异步操作中使用数据时,如果不使用深拷贝,可能会在异步操作完成之前对数据进行修改,从而导致意外行为。

在 Vue 中,您可以使用一些方法来执行深拷贝,例如:

  • 使用 JSON.parse(JSON.stringify(obj)) 来执行深拷贝,但需要注意该方法有一些限制,例如不能复制函数、正则表达式等。
  • 使用第三方库,如 lodash 中的 cloneDeep 方法,可以更可靠地执行深拷贝操作。
  • 自己编写递归函数来执行深拷贝,这需要更多的工作,但可以完全满足您的需求。

总之,深拷贝在 Vue 中通常用于确保数据的独立性,以避免意外的副作用和数据共享问题。需要谨慎使用深拷贝,因为它可能会导致性能损失,特别是在处理大型数据结构时。

immutable 插件的真实使用场景

Immutable.js 是一个用于管理不可变数据的 JavaScript 库,它提供了一些数据结构和方法,用于创建和操作不可变数据。这种不可变性的数据管理模式在许多应用中都有一些有用的使用场景,以下是一些真实的使用场景:

  1. React 应用状态管理:Immutable.js 在 React 应用中的状态管理中非常有用。通过使用 Immutable 数据结构,您可以在更新状态时创建新的状态副本,而不是在原始状态上进行修改。这有助于跟踪状态的更改,避免不必要的重新渲染(shouldComponentUpdate,判断特定属性是否发生变化),以及提高性能。
  2. Redux 和 Flux 应用:Redux 和 Flux 是常用的状态管理库,Immutable.js 可以与它们一起使用,以确保状态的不可变性(保证 reducer 函数是纯函数)。这有助于提高状态的可预测性和可维护性。
  3. 服务器数据缓存:当从服务器获取数据并缓存到前端时,Immutable.js 可以用于创建缓存的不可变副本。这可以确保在缓存数据上的任何修改都不会影响原始数据,同时也方便进行数据比较和更新。
  4. 数据历史记录和时间旅行:不可变数据结构允许您轻松地跟踪数据的历史记录,从而实现时间旅行(time travel)功能。这在调试和回溯数据时非常有用。
  5. 多线程和并发编程:不可变数据结构是线程安全的,因为它们不会被多个线程同时修改。这使得并发编程更加容易。
  6. 数据比较和差异计算:Immutable.js 提供了一些方法,可以用于比较两个不可变数据结构之间的差异,这在数据同步和更新时非常有用。
  7. 避免副作用:使用不可变数据可以帮助您避免在代码中引入副作用,因为不可变数据不会被意外修改。

总之,Immutable.js 在许多情况下都有用,尤其是在需要确保数据的不可变性、提高性能和简化状态管理的场景中。然而,它并不适用于所有应用,因此需要根据具体情况来考虑是否使用它。在某些情况下,普通的 JavaScript 对象和数组可能足够了,而在需要更严格的数据管理和控制时,Immutable.js 可能是一个有力的工具。