runoops.com

immutable及其常用用法

immutable的使用背景

最近想写这篇文章,主要是工作中采用react框架,相信很多小伙伴使用过reactreact有个基本准则就是数据不可变,只读,这样做的好处就是可以提高react性能。但是使用的时候很令人头痛就是改变数据很复杂,如下代码(代码是基于umi的状态管理的文件):

export default {
  namespace: "mock",
  state: {
    mockData: {
        msg:123,
        value:''
    },
  },
  reducers: {
    setMockData(state, payload) {
      return {
          ...state,
          mockData:{
          ...mockData,
          msg:payload.data.msg
          }
      };
    },
  },
}

相信很多人看见上面的代码头都大了,当要修改数据的时候,这样会导致数据操作过深而使得程序复杂化。为了保证数据的不可变性,引入immutable后代码会变得更加优雅和简单。immutable一旦创建,就不能更改的数据,对immutable对象的任何修改或删除添加都会返回一个新的immutable对象,实现原理是持久化数据结构,在使用旧数据创建新数据的时候,会保证旧数据同时可用且不变,同时为了避免深度复制复制所有节点的带来的性能损耗,immutable使用了结构共享,即如果对象树种的一个节点发生变化,只修改这个节点和受他影响的父节点,其他节点则共享。

什么是 Immutable Data

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

一个说明不可变的例子

// 原生对象
let a1 = {
    b: 1,
    c: {
        c1: 123
    }
};

let b1 = a1;
b1.b = 2;

console.log(a1.b, b1.b); // 2, 2
console.log(a1 === b1); // true
console.log(a1.c === b1.c); // true

// immutable.js 的Map
let a2 = Immutable.fromJS({
    b: 1,
    c: {
        c1: 123
    }
});

let b2 = a2.set('b', 2);

// 对 Immutable 对象的任何修改或添加删除操作都会返回一个新的 Immutable 对象
console.log(a2.get('b'), b2.get('b')); // 1, 2  对象 a2 的 b 值并没有变成2。
console.log(a2 === b2); //  false

//如果对象树中一个节点发生变化,只修改这个节点和受它影响的父节点,其它节点则进行共享。
console.log(a2.get('c') === b2.get('c')); //true

为什么要在React.js中使用Immutable

  • 它是一个完全独立的库,无论基于什么框架都可以用它。意义在于它弥补了Javascript 没有不可变数据结构的问题
  • 由于是不可变的,可以放心的对对象进行任意操作。在React开发中,频繁操作state对象或是store,配合immutableJS快、安全、方便。 熟悉React.js的都应该知道,React.js是一个UI = f(states)的框架,为了解决更新的问题,React.js使用了virtual dom,virtual dom通过diff修改dom,来实现高效的dom更新。但是有一个问题。当state更新时,如果数据没变,你也会去做virtual domdiff,这就产生了浪费。这种情况其实很常见,当然你可能会说,你可以使用PureRenderMixin来解决呀,PureRenderMixin是个好东西,我们可以用它来解决一部分的上述问题,但PureRenderMixin只是简单的浅比较,不使用于多层比较。那怎么办?自己去做复杂比较的话,性能又会非常差 方案就是使用immutable.js可以解决这个问题。因为每一次state更新只要有数据改变,那么PureRenderMixin可以立刻判断出数据改变,可以大大提升性能.

Immutable 优点

  • Immutable 降低了 Mutable 带来的复杂度 可变(Mutable)数据耦合了 TimeValue 的概念,造成了数据很难被回溯
  • 节省内存 Immutable.js 使用了 Structure Sharing 会尽量复用内存,甚至以前使用的对象也可以再次被复用。没有被引用的对象会被垃圾回收
import { Map} from 'immutable';
let a = Map({
  select: 'users',
  filter: Map({ name: 'Cam' })
})
let b = a.set('select', 'people');

a === b; // false
a.get('filter') === b.get('filter'); // true
  • Undo/Redo,Copy/Paste,甚至时间旅行这些功能做起来小菜一碟 因为每次数据都是不一样的,只要把这些数据放到一个数组里储存起来,想回退到哪里就拿出对应数据即可,很容易开发出撤销重做这种功能。

  • 并发安全 传统的并发非常难做,因为要处理各种数据不一致问题,因此『聪明人』发明了各种锁来解决。但使用了 Immutable 之后,数据天生是不可变的,并发锁就不需要了。

  • 拥抱函数式编程 Immutable 本身就是函数式编程中的概念,纯函数式编程比面向对象更适用于前端开发。因为只要输入一致,输出必然一致,这样开发的组件更易于调试和组装。

Immutable 缺点

  • 需要学习新的 API
  • 增加了资源文件大小
  • 容易与原生对象混淆

Immutable 的几种数据类型

  • List: 有序索引集,类似JavaScript中的Array
  • Map: 无序索引集,类似JavaScript中的Object
  • OrderedMap: 有序的Map,根据数据的set()进行排序。
  • Set: 没有重复值的集合。
  • OrderedSet: 有序的Set,根据数据的add进行排序。
  • Stack: 有序集合,支持使用unshift()shift()添加和删除。
  • Range(): 返回一个Seq.Indexed类型的集合,这个方法有三个参数,start表示开始值,默认值为0,end表示结束值,默认为无穷大,step代表每次增大的数值,默认为1.如果start = end,则返回空集合。
  • Repeat(): 返回一个vSeq.Indexe类型的集合,这个方法有两个参数,value代表需要重复的值,times代表要重复的次数,默认为无穷大。
  • Record: 一个用于生成Record实例的类。类似于JavaScriptObject,但是只接收特定字符串为key,具有默认值。
  • Seq: 序列,但是可能不能由具体的数据结构支持。
  • Collection: 是构建所有数据结构的基类,不可以直接构建 上面那么多常用的也就是 ListMap

immutable常用 List API

List是有序的密集型集合,使用场景太多了,是使用immutable必不可少的一个数据结构,学习起来也是非常简单,类似于JS的数组(Array)。如果你熟练掌握了数组的原生的方法,则你在学习该List则会很简单,如果还不太熟悉,请看我的上一篇数组的常用方法。注:在这里定义数据结构采用typescript,代码片段采用ES6的块级作用域避免因变量相同影响其他代码,但是你即使不会使用ts也不会妨碍你学习。学习旅途开始了,代码撸起来。

静态方法

isList是个静态方法,用于判别是否是List结构的数据

const arr = [1, 2, 3];
const list = List(arr);
console.log(List.isList(arr));  // false
console.log(List.isList(list)); // true

of也是一个静态方法,用于创建一个List数组结构

const list=List.of(1,2,3,4)
console.log(list)

属性

size 这是List的一个静态属性,与数组的length使用方法一样

const arr = [1, 2, 3];
const list = List(arr);
const size=list.size();
console.log(size) //3

List方法

  1. toJS(): Array toJS()方法是将List对象转为普通的js对象:
const arr=[1,2,3]
const list=List(arr);
const newList=list.toJS()
console.log(newList) // [1,2,3]
  1. toJSON(): Array toJS()方法是将List对象转为普通的json对象:
const arr=[1,2,3]
const list=List(arr);
const newList=list.toJSON()
console.log(newList) // [1,2,3]
  1. toArray(): Array toArray()方法是将List对象转为普通的array对象:
const arr=[1,2,3]
const list=List(arr);
const newList=list.toArray()
console.log(newList) // [1,2,3]
  1. toObject(): Array toArray()方法是将List对象转为普通的object对象:
const arr=[1,2,3]
const list=List(arr);
const newList=list.toObject()
console.log(newList) // {0: 1, 1: 2, 2: 3}
  1. set(index: number, value: T): List set是用于设置特定位置的数据的一个方法:
const arr = [1, 2, 3];
const list = List(arr);
const newList = list.set(1, 'a');
console.log(list.toJS()); // [1,2,3]
console.log(newList.toJS()); //[1,'a',3]
  1. get(index: number): T | undefined

get方法用于获取特定位置的元素

const arr=[1,2,3]
const list=List(arr);
const value = list.get(1);
console.log(value); //2
  1. delete(index: number): List delete方法用于删除数组中的一个元素
const arr=[1,2,3]
const list=List(arr);
const newList = list.delete(1); //删除第二个元素
console.log(newList.toJS()); //[1,3]
  1. insert(index: number, value: T): List insert方法用于往指定的位置插入一个元素
const arr=[1,2,3]
const list=List(arr);
const newList = list.insert(2, 'a');
console.log(newList.toJS()); //[1,2,'a',3]
  1. clear(): List clear方法用于清空对象
const arr=[1,2,3]
const list=List(arr);
const newList = list.clear(); 
console.log(newList.toJS()); //[]
  1. update(index: number, updater: (value: T) => T): this update方法用于更新数组中的元素,接受两个参数,updater接受一个函数,参数是旧值返回值是要更新的数据
const arr=[1,2,3]
const list=List(arr);
const newList = list.update(1, (value)=>{
    return value*2;
  })
console.log(list.toJS()); //[1,2,3]
console.log(newList.toJS()); //[1,4,3]
  1. setSize(size: number): List setSize方法用于设置List对象的长度大小,超出的部分值为undefined
const arr=[1,2,3]
const list=List(arr);
console.log(list.size);  //3
const newList = list.setSize(5);
console.log(newList.toJS()); //[1, 2, 3, undefined, undefined]
console.log(newList.size);  //5
  1. setIn(keyPath: Iterable, value: any): this setIn方法用于设置指定位置的值,与set方法很类似,但是可以处理更复杂的数据结构 (聪明的小伙伴就会发现这个方法可以解决上面最开始的那个操作数据嵌套过深的问题)
const list = List([1, 2, 3, [4, 5, [6, 7], 8], 9, 10]);
const newList = list.setIn([3, 2, 1], 'a'); // 设置第三个元素[4,5[6,7],8]中的第二个元素[6,7]中的第一个元素7为'a'
console.log(newList.toJS()) //[1, 2, 3, [4, 5, [6, 'a'], 8], 9, 10]
 const list = List([1, 2, 3, [4, 5, {name: '张三'}, 8], 9, 10]);
  // 凡是有In的操作方法,第一个参数需要接收数据所在的路径。
 const newList = list.setIn([3, 2, 'name'], '李四');
 console.log(newList.toJS()) //[1, 2, 3, [4, 5, {name: '李四'}, 8], 9, 10]
  1. deleteIn(keyPath: Iterable): this deleteIn方法用于删除指定位置的值,与delete方法很类似,但是可以处理更复杂的数据结构
const list = List([1, 2, 3, [4, 5, [6, 7], 8], 9, 10]);
const newList = list.deleteIn([3, 2, 1]); // 删除第三个元素[4,5[6,7],8]中的第二个元素[6,7]中的第一个元素7
console.log(newList.toJS()) //[1, 2, 3, [4, 5, [6], 8], 9, 10]
  1. updateIn(keyPath: Iterable): this updateIn方法用于删除指定位置的值,与delete方法很类似,但是可以处理更复杂的数据结构,注:updateInsetIn的区别在于updateIn需要依赖旧值改变数据
const list = List([1, 2, 3, [4, 5, [6, 7], 8], 9, 10]);
const newList = list.updateIn([3, 1],(value=>value*3));
 // 删除第三个元素[4,5[6,7],8]中的第二个元素[6,7]中的第一个元素7
console.log(newList.toJS()) //[1, 2, 3, [4, 15, [6,7], 8], 9, 10]
  1. concat(...valuesOrCollections: Array<Iterable | C>): List<T | C> concat方法与原生的数组concat方法类似,用法基本相同。
const arr1=[1,2,3];
const arr2=[4,5,6];
const list1=List(arr1)
const list2=List(arr2)
const newList=list1.concat(list2)
console.log(newList); 

17.merge()

merge用于合并两个List结构的数据,merge方法效果和concat方法一样,这里不做过多的解释

const arr1=[1,2,3];
const arr2=[4,5,6];
const list1=List(arr1)
const list2=List(arr2)
const newList=list1.merge(list2)
console.log(newList); 
  1. map(mapper: (value: T, key: number, iter: this) => M, context?: any): List map方法用于遍历数据,并返回执行函数后的一个新的数据,得到的任然是个List结构的数据
const arr = [1, 2, 3];
const list = List(arr);
const newList = list.map((item) => item * 2); //将数组的每一项乘以2
console.log(newList);
  1. forEach(mapper: (value: T, key: number, iter: this) => void, context?: any): void forEach方法用于遍历数据,与原生方法一样
const arr = [1, 2, 3];
const list = List(arr);
list.forEach((item) => {
    console.log(item) // 1 2 3
}); //将数组的每一项乘以2
console.log(newList);
  1. filter() filter方法创建一个新数组, 其包含通过所提供函数实现的测试的所有元素。
    const arr = [1, 2, 3, 4, 5, 6, 7];
    const list = List(arr);
    const newList = list.filter((item) => item > 5); //将大于5的元素筛选出来
    console.log(newList.toJS()); //[6,7]
  1. find() find方法返回数组中满足提供的测试函数的第一个元素的值
const arr = [1, 2, 3, 4, 5, 6, 7];
const list = List(arr);
const item = list.find((item) => item > 5);
console.log(item); //[6]
  1. findLast() findLast方法从后往前找返回数组中满足提供的测试函数的第一个元素的值
const arr = [1, 2, 3, 4, 5, 6, 7];
const list = List(arr);
const item = list.findLast((item) => item > 5);
console.log(item); //[7]
  1. findIndex() findIndex()方法返回数组中满足提供的测试函数的第一个元素的索引。若没有找到对应元素则返回-1。
const arr = ["菠萝", "苹果", "香蕉", "梨子"];
const list = List(arr);
const index = list.findIndex((item) => item === "香蕉");//找出数组中元素为"香蕉"的元素下标
console.log(index); // 2
  1. findLastIndex() findIndex()方法从后往前找返回数组中首次满足提供的测试函数的第一个元素的索引。若没有找到对应元素则返回-1。
const arr = ["菠萝", "苹果", "香蕉", "梨子"];
const list = List(arr);
const index = list.findLastIndex((item) => item === "香蕉");//找出数组中元素为"香蕉"的元素下标
console.log(index); // 2 
//相信在这里有些小伙伴会出现疑问,为啥和findIndex返回值一样 其实当数组值唯一时,两者几乎没有差别,但是当数组的值很多一样的时候,正序遍历和逆序遍历就有区别了。
  1. indexOf(searchValue: T): number indexOf('searchElement') 查找元素首次出现在数组的索引 未找到返回-1
const arr = ["菠萝", "苹果", "香蕉", "梨子"];
const list = List(arr);
const index = list.indexOf("香蕉");//找出数组中元素为"香蕉"的元素下标
console.log(index); // 2 
  1. lastIndexOf(searchValue: T): number lastIndexOf查找元素从前往后首次出现在数组的索引 未找到返回-1
const arr = ["菠萝","香蕉", "苹果", "香蕉", "梨子"];
const list = List(arr);
const index = list.lastIndexOf("香蕉");//找出数组中元素为"香蕉"的元素下标
console.log(index); // 3 
  1. keys() keys方法返回一个包含数组中每个索引键的Array Iterator对象
    const arr = ["菠萝", "香蕉", "苹果", "香蕉", "梨子"];
    const list = List(arr);
    const iterator = list.keys();
    for (const item of iterator) {
      console.log(item); // 0 1 2 3 4
    }
  1. values() values方法返回一个包含数组中每个索引值的Array Iterator对象
const arr = ["菠萝", "香蕉", "苹果", "香蕉", "梨子"];
const list = List(arr);
const iterator = list.keys();
for (const item of iterator) {
   console.log(item); // "菠萝", "香蕉", "苹果", "香蕉", "梨子"
}
  1. entries() forEach方法用于遍历数据,与原生方法一样
const arr = ["菠萝", "香蕉", "苹果", "香蕉", "梨子"];
const list = List(arr);
const iterator = list.keys();
for (const item of iterator) {
   console.log(item); // [0,"菠萝"][1,"香蕉"][2,"苹果"][3,"香蕉"][4,"梨子"] 
}

immutable常用Map API

Map非常类似于JavaScript对象

Map 常用方法

Map<K, V>(): Map<K, V>
Map<K, V>(iter: Iterable.Keyed<K, V>): Map<K, V> Map<K, V>(iter: Iterable<any, Array<any>>): Map<K, V> Map<K, V>(obj: Array<Array<any>>): Map<K, V> Map<V>(obj: {[key: string]: V}): Map<string, V> Map<K, V>(iterator: Iterator<Array<any>>): Map<K, V> Map<K, V>(iterable: Object): Map<K, V>
  1. 构建Map对象 Map()

构造函数不常用,一般都是通过Immutable.fromJS()将一个JS原生对象转换为一个Immutable对象

const data=Map({title:'大峡谷'})
console.log(data);
  1. Map.of() 构建Map对象 这是另外一种构建Map对象的方法
const data = Map.of('key1','value1','key2','value2','key3','value3'); 
  1. Map().toJS()Map对象转化为普通对象
const data=Map({title:'大峡谷'}) console.log(data.toJS()); // {title:'大峡谷'}
  1. Map().toJSON()Map对象转化为普通对象 结果与 toJS()一样
const data=Map({title:'大峡谷'}) console.log(data.toJS()); // {title:'大峡谷'}
  1. Map.isMap({}) 判断对象是否是Map对象 对原生Object不生效
console.log(Map.isMap({})); // false console.log(Map.isMap(Map({}))); // true
  1. Map.size() size 这是Map的一个静态属性,用于获取对象属性的个数
console.log(Map({key: "value2", key1: "value1"}).size);// 2
console.log(Map.of({x:1}, 2, [3], 4).size);// 2
  1. 添加元素Set方法
const data = Map({a: {a1: 34}, b: 2, c: 3, d: 444});
console.log(data.set('a', 0).toJS()); // {a: 0, b: 2, c: 3, d: 444}
console.log(data.set('e', 99).toJS());  // {a: 1, b: 2, c: 3, d: 444, e: 99}
  1. 添加元素setIn方法
console.log(Immutable.fromJS([1, 2, 3, {a: 45, b: 64}]).setIn(['3', 'a'], 1000).toJS());//[1, 2, 3, {a: 1000, b: 64}]

  1. mapKeys() mapEntries()Map元素进行处理,返回处理后的对象
//mapKeys() 返回对象
console.log(Immutable.fromJS({a: 5, b: 2, c: 3, d: 444}).mapKeys((key)=>{
    return key + 'hhh';
}).toJS());
// {ahhh: 5, bhhh: 2, chhh: 3, dhhh: 444}

//mapEntries() 返回对象
console.log(Immutable.fromJS({a: 5, b: 2, c: 3, d: 444}).mapEntries(([key, value])=>{
    return [key + 'aaa', value+'hhhh'];
}).toJS());//   {aaaa: "5hhhh", baaa: "2hhhh", caaa: "3hhhh", daaa: "444hhhh"}
  1. merge合并方法 merge() mergeDeep() mergeWith() mergeDeepWith()
// Map const $test = Immutable.fromJS({a: {a1: 222, a3: 456}, b: 2, c: 3, d: 444}); const $test1 = Immutable.fromJS({a: {a1: 222, a2: 234}, b: 2, c: 3, d: 444}); // 浅merge console.log($test.merge($test1).toJS(), $test.toJS()); // $test1 -> $test {a: {a1: 222, a2: 234}, b: 2, c: 3, d: 444} {a: {a1: 222, a3: 456}, b: 2, c: 3, d: 444} // 深merge console.log($test.mergeDeep($test1).toJS(), $test.toJS()); // $test1 -> $test {a: {a1: 222, a2: 234, a3: 456}, b: 2, c: 3, d: 444} {a: {a1: 222, a3: 456}, b: 2, c: 3, d: 444} // 浅merge自定义merge规则 console.log($test.mergeWith((prev, next)=> { // 自定义转换 return prev; }, $test1).toJS(), $test1.toJS()); // 深merge自定义merge规则 console.log($test.mergeDeepWith((prev, next)=> { // 自定义转换 return prev; }, $test1).toJS(), $test1.toJS());
  1. join() 将对象转换为字符串
console.log(Immutable.fromJS({b: 2, a: {a1: 222, a3: 456}, c: 3, d: 444}).join()); // 2,Map { "a1": 222, "a3": 456 },3,444
  1. isEmpty() 判断Map对象是否为空
console.log(Immutable.fromJS({}).isEmpty()); // true
  1. has() hasIn() 检查是否有某个key
console.log(Immutable.fromJS({b: 2, a: {a1: 222, a3: 456}, c: 3, d: 444}).has('a')); // true
console.log(Immutable.fromJS({b: 2, a: {a1: 222, a3: 456}, c: 3, d: 444}).hasIn(['a', 'a3'])); // true
  1. includes() 是否包含某些元素
// 对象是否包含某个元素,对Immutable元素使用Immutable.is 进行比较
console.log(Immutable.fromJS({b: 2, a: {a1: 222, a3: 456}, c: 3, d: 89}).includes('89'));// 数组没有字符89,所以返回 false
console.log(Immutable.fromJS({b: 2, a: {a1: 222, a3: 456}, c: 3, d: '89'}).contains('89'));// true
console.log(Immutable.fromJS({b: 2, a: {a1: 222, a3: 456}, c: 3, d: Immutable.fromJS([6, 5, 4])}).contains(Immutable.fromJS([6, 5, 4])));// true
  1. isSubset() 子集判断
// isSubset()
console.log(Immutable.fromJS({b: 2, a: {a1: 222, a3: 456}}).isSubset(Immutable.fromJS({b: 2, a: {a1: 222, a3: 456}, c: 3, d: 5})));// true
// isSuperset 就是 isSubset 参数掉个个儿
console.log(Immutable.fromJS({b: 2, a: {a1: 222, a3: 456}, c: 3, d: 5}).isSuperset(Immutable.fromJS({b: 2, a: {a1: 222, a3: 456}})));// true
  1. reverse() 反转
console.log(Immutable.fromJS({b: 2, a: {a1: 222, a3: 456}, c: 3, d: 5}).reverse().toJS());
// {d: 5, c: 3, a: {a1: 222, a3: 456}, b: 2}
  1. 排序方法 sort()sortBy() sortBy<C>(comparatorValueMapper: (value: T, key: number, iter: Iterable<number, T>) => C,comparator?: (valueA: C, valueB: C) => number): Iterable<number, T>
console.log(Immutable.fromJS({b: 2, a: 88, c: 3, d: 5}).sort().toJS());// {b: 2, c: 3, d: 5, a: 88} // 传入比较函数 console.log(Immutable.fromJS({b: 2, a: 88, c: 3, d: 5}).sort((a, b) => { if (a < b) { return -1; } if (a > b) { return 1; } if (a === b) { return 0; } }).toJS());// {b: 2, c: 3, d: 5, a: 88} // sortBy console.log(Immutable.fromJS({b: {a: 2}, a: {a: 88}, c: {a: 3}, d: {a: 5}}).sortBy((value, key, obj)=> { return value.get('a') },(a, b) => { if (a < b) { return -1; } if (a > b) { return 1; } if (a === b) { return 0; } }).toJS());// {b: {a: 2}, c: {a: 3}, d: {a: 5}, a: {a: 88}}
  1. flatten() 平铺 参数默认情况下,false 深度平铺,true 浅度平铺1层
>console.log(Immutable.fromJS({b: 2, a: {a1: {a5: 333}, a3: [1,2,3]}, c: 3, d: 5}).flatten().toJS());
// {0: 1, 1: 2, 2: 3, b: 2, a5: 333, c: 3, d: 5}
console.log(Immutable.fromJS({b: 2, a: {a1: {a5: 333}, a3: [1,2,3]}, c: 3, d: 5}).flatten(true).toJS());
// {b: 2, a1: Object, a3: Array[3], c: 3, d: 5}
  1. groupBy() 分组
console.log(Immutable.fromJS({b: {a5: 333}, a: {a5: 333}, c: {a5: 334}, d: {a5: 334}}).groupBy((value) => { return value.get('a5') }).toJS()); // OrderedMap {333: {b: {a5: 333}, a: {a5: 333}}, 334: {c: {a5: 334}, d: {a5: 334}}}
  1. 连接 concat()
const $test1 = Immutable.fromJS({b: 2, a: {a1: {a5: 333}, a3: [1,2,3]}, c: 3, d: 5}); const $test2 = Immutable.fromJS({b1: 22, b: 34}); console.log($test1.concat($test2).toJS()); //{b: 34, a: Object, c: 3, d: 5, b1: 22} 属性 b 被覆盖 console.log($test1.toJS(), $test2.toJS()); //{b: 2, a: {a1: {a5: 333}, c: 3, d: 5} b1: 22, b: 34} #

脑图

最后放上一张脑图(查看原图):

Captcha Code

0 笔记

分享笔记

Inline Feedbacks
View all notes