immutable的使用背景
最近想写这篇文章,主要是工作中采用react
框架,相信很多小伙伴使用过react
,react
有个基本准则就是数据不可变,只读,这样做的好处就是可以提高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
dom
的diff
,这就产生了浪费。这种情况其实很常见,当然你可能会说,你可以使用PureRenderMixin
来解决呀,PureRenderMixin
是个好东西,我们可以用它来解决一部分的上述问题,但PureRenderMixin
只是简单的浅比较,不使用于多层比较。那怎么办?自己去做复杂比较的话,性能又会非常差 方案就是使用immutable.js
可以解决这个问题。因为每一次state
更新只要有数据改变,那么PureRenderMixin
可以立刻判断出数据改变,可以大大提升性能.
Immutable 优点
Immutable
降低了Mutable
带来的复杂度 可变(Mutable)
数据耦合了Time
和Value
的概念,造成了数据很难被回溯- 节省内存
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
实例的类。类似于JavaScript
的Object
,但是只接收特定字符串为key
,具有默认值。Seq
: 序列,但是可能不能由具体的数据结构支持。Collection
: 是构建所有数据结构的基类,不可以直接构建 上面那么多常用的也就是List
和Map
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方法
- toJS(): Array
toJS()
方法是将List
对象转为普通的js
对象:
const arr=[1,2,3]
const list=List(arr);
const newList=list.toJS()
console.log(newList) // [1,2,3]
- toJSON(): Array
toJS()
方法是将List
对象转为普通的json
对象:
const arr=[1,2,3]
const list=List(arr);
const newList=list.toJSON()
console.log(newList) // [1,2,3]
- toArray(): Array
toArray()
方法是将List
对象转为普通的array
对象:
const arr=[1,2,3]
const list=List(arr);
const newList=list.toArray()
console.log(newList) // [1,2,3]
- 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}
- 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]
- get(index: number): T | undefined
get
方法用于获取特定位置的元素
const arr=[1,2,3]
const list=List(arr);
const value = list.get(1);
console.log(value); //2
- 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]
- 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]
- clear(): List
clear
方法用于清空对象
const arr=[1,2,3]
const list=List(arr);
const newList = list.clear();
console.log(newList.toJS()); //[]
- 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]
- 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
- 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]
- 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]
- updateIn(keyPath: Iterable): this
updateIn
方法用于删除指定位置的值,与delete
方法很类似,但是可以处理更复杂的数据结构,注:updateIn
与setIn
的区别在于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]
- 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);
- 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);
- 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);
- 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]
- 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]
- 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]
- findIndex()
findIndex()
方法返回数组中满足提供的测试函数的第一个元素的索引。若没有找到对应元素则返回-1。
const arr = ["菠萝", "苹果", "香蕉", "梨子"];
const list = List(arr);
const index = list.findIndex((item) => item === "香蕉");//找出数组中元素为"香蕉"的元素下标
console.log(index); // 2
- findLastIndex()
findIndex()
方法从后往前找返回数组中首次满足提供的测试函数的第一个元素的索引。若没有找到对应元素则返回-1。
const arr = ["菠萝", "苹果", "香蕉", "梨子"];
const list = List(arr);
const index = list.findLastIndex((item) => item === "香蕉");//找出数组中元素为"香蕉"的元素下标
console.log(index); // 2
//相信在这里有些小伙伴会出现疑问,为啥和findIndex返回值一样 其实当数组值唯一时,两者几乎没有差别,但是当数组的值很多一样的时候,正序遍历和逆序遍历就有区别了。
- indexOf(searchValue: T): number
indexOf('searchElement')
查找元素首次出现在数组的索引 未找到返回-1
const arr = ["菠萝", "苹果", "香蕉", "梨子"];
const list = List(arr);
const index = list.indexOf("香蕉");//找出数组中元素为"香蕉"的元素下标
console.log(index); // 2
- lastIndexOf(searchValue: T): number
lastIndexOf
查找元素从前往后首次出现在数组的索引 未找到返回-1
const arr = ["菠萝","香蕉", "苹果", "香蕉", "梨子"];
const list = List(arr);
const index = list.lastIndexOf("香蕉");//找出数组中元素为"香蕉"的元素下标
console.log(index); // 3
- 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
}
- values() values方法返回一个包含数组中每个索引值的Array Iterator对象
const arr = ["菠萝", "香蕉", "苹果", "香蕉", "梨子"];
const list = List(arr);
const iterator = list.keys();
for (const item of iterator) {
console.log(item); // "菠萝", "香蕉", "苹果", "香蕉", "梨子"
}
- 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>
- 构建
Map
对象Map()
构造函数不常用,一般都是通过Immutable.fromJS()
将一个JS
原生对象转换为一个Immutable
对象
const data=Map({title:'大峡谷'})
console.log(data);
Map.of()
构建Map
对象 这是另外一种构建Map
对象的方法
const data = Map.of('key1','value1','key2','value2','key3','value3');
Map().toJS()
将Map
对象转化为普通对象
const data=Map({title:'大峡谷'})
console.log(data.toJS()); // {title:'大峡谷'}
Map().toJSON()
将Map
对象转化为普通对象 结果与toJS()
一样
const data=Map({title:'大峡谷'})
console.log(data.toJS()); // {title:'大峡谷'}
Map.isMap({})
判断对象是否是Map
对象 对原生Object
不生效
console.log(Map.isMap({})); // false
console.log(Map.isMap(Map({}))); // true
Map.size()
size
这是Map
的一个静态属性,用于获取对象属性的个数
console.log(Map({key: "value2", key1: "value1"}).size);// 2
console.log(Map.of({x:1}, 2, [3], 4).size);// 2
- 添加元素
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}
- 添加元素
setIn
方法
console.log(Immutable.fromJS([1, 2, 3, {a: 45, b: 64}]).setIn(['3', 'a'], 1000).toJS());//[1, 2, 3, {a: 1000, b: 64}]
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"}
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());
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
isEmpty()
判断Map对象是否为空
console.log(Immutable.fromJS({}).isEmpty()); // true
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
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
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
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}
- 排序方法
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}}
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}
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}}}
- 连接
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}
#
脑图
最后放上一张脑图(查看原图):
分享笔记