我是经过使用 React 才去关注 immutable data 这个概念的。事实上,你去搜 immutable 的 JS 相关文章,也基本都是近两年的,大概是随着 React 的推广才备受关注。可是这篇文章不会去介绍 React 是如何在乎 immutable data 的,而是从原生 JS,写一些本身的思考。git
我的 blog,欢迎 star。https://github.com/sunyongjianes6
可变对象是一个可在其建立后修改状态的对象,而不可变对象则是建立以后,不能再修改状态,对其任何删改操做,都应返回一个新的对象。github
一个例子开始:算法
var x = { a: 1 } var y = x; x.a = 2; console.log(y); //{ a: 2 }
这在咱们刚开始学 js 的时候就知道了,js 中的对象都是参考(reference)类型,x = y
是对象赋值引用,二者共用一个对象的空间,因此 x 改动了,y 天然也改变。数据库
数组也是同样的:编程
var ary = [1, 2, 3]; var list = ary; ary.push(4); console.log(list); // [1, 2, 3, 4]
在 JS 中,objects, arrays,functions, classes, sets, maps 都是可变数据。
不过字符串和数字就不会。redux
var str = 'hello world'; var sub = str; str = str.slice(0, 5); console.log(sub); // 'hello world' var a = 1; var b = a; a += 2; console.log(b); // 1
像这样,sub = str
,b = a
的赋值操做,都不会影响以前的数据。设计模式
首先,不可变数据类型是源于函数式编程中的,是一条必备的准则。函数式对数据处理的时候,经过把问题抽象成一个个的纯函数,每一个纯函数的操做都会返回新的数据类型,都不会影响以前的数据,保证了变量/参数的不可变性,增长代码可读性。数组
另外,js 中对象可变的好处多是为了节约内存,相比字符串、数字,它承载的数据量更大更多,不可变带来每次操做都要产生新的对象,新的数据结构,这与 js 设计之初用来作网页中表单验证等简单操做是有悖的。并且,咱们最开始也确实感觉到可变带来的便捷,可是反之它带来的反作用远超过这种便捷,程序越大代码的可读性,复杂度也愈来愈高。性能优化
举一个栗子:
const data = { name: 'syj', age: 24, hobby: 'girl', location: 'beijing' } // 有一个改变年龄的方法 function addAge(obj) { obj.age += 1; return obj; } // 一个改变地址的方法 function changeLocation(obj, v) { obj.location = v; return obj; } // 这两个方法我期待的是获得只改变想改变的属性的 data console.log(addAge(data)); console.log(changeLocation(obj, 'shanghai'));
但实际上 addAge 已经把原始数据 data 改变了,当我再去使用的时候,已是被污染的数据。这个栗子其实没有那么的典型,由于没有结合业务,可是也能够说明一些问题,就是可变数据带来的不肯定影响。这两个函数都是有“反作用”的,即对传入数据作了修改,当你调用两次 addAge,获得的倒是两个彻底不一样的结果,这显然不是咱们想要的。若是遵循不可变数据的原则,每次对原始数据结构的修改、操做,都返回新的数据结构,就不会出现这种状况。关于返回新的数据结构,就须要用到数据拷贝。
以前 y = x
这样的操做,显然是没法完成数据拷贝的,这只是赋值引用,为了不这种对象间的赋值引用,咱们应该更多的使用 const
定义数据对象,去避免这种操做。
而咱们要给新对象(数据)建立一个新的引用,也就是须要数据拷贝。然而对象的数据结构一般是不一样的(嵌套程度等),在数据拷贝的时候,须要考虑到这个问题,若是对象是深层次的
比较一下 JS 中几种原生的拷贝方法,了解他们能实现的程度。
像这样:
const x = { a: 1 }; const y = Object.assign({}, x); x.a = 11; console.log(y); // { a: 1 }
诚然,这次对 y 的赋值,再去改变 x.a 的时候,y.a 并无发生变化,保持了不变性。你觉得就这么简单吗?看另外一个栗子:
const x = { a: 1, b: { c: 2 } }; const y = Object.assign({}, x); x.b.c = 22; console.log(y); // { a: 1, b: { c: 22}}
对 x 的操做,使 y.b.c 也变成了 22。为何?由于 Object.assign 是浅拷贝,也就是它只会赋值对象第一层的 kv,而当第一层的 value 出现 object/array 的时候,它仍是会作赋值引用操做,即 x,y 的 b 共用一个 {c: 2}
的地址。还有几个方法也是这样的。
const x = { a: 1, b: { c: 2 } }; const y = Object.freeze(x); x.a = 11; console.log(y); x.b.c = 22; console.log(y); // { a: 1, b: { c: 22}}
freeze,看起来是真的“冻结”了,不可变了,其实效果是同样的,为了效率,作的浅拷贝。
const x = { a: 1, b: { c: 2 } }; const y = { ...x }; x.a = 11; console.log(y); x.b.c = 22; console.log(y);
es6 中的新方法,解构。数组也同样:
const x = [1, 2, [3, 4]]; const y = [...x]; x[2][0] = 33; console.log(y); // [1, 2, [33, 4]]
一样是浅拷贝。
JS 原生对象的方法,是没有给咱们提供深拷贝功能的。
如何去作深拷贝
原生
拿上面的栗子来讲,咱们去实现深拷贝。
const x = { a: 1, b: { c: 2 } }; const y = Object.assign({}, x, { b: Object.assign({}, x.b) }) x.b.c = 22; console.log(y); // { a: 1, b: { c: 2 } }
不过这只是嵌套很少的时候,而更深层次的,就须要更复杂的操做了。实际上,deep-clone 确实没有一个统一的方法,须要考虑的地方挺多,好比效率,以及是否应用场景(是否每次都须要 deep-clone)。还有在 js 中,还要加上 hasOwnProperty 这样的判断。写个简单的方法:
function clone(obj) { // 类型判断。 isActiveClone 用来防止重复 clone,效率问题。 if (obj === null || typeof obj !== 'object' || 'isActiveClone' in obj) { return obj; } //多是 Date 对象 const result = obj instanceof Date ? new Date(obj) : {}; for (const key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { obj['isActiveClone'] = null; result[key] = clone(obj[key]); delete obj['isActiveClone']; } } return result; } var x = { a: 1, b: 2, c: { d: 3 } } console.log(clone(x));
JSON
最简单,偷懒的一种方式,JSON 的序列化再反序列化。
const y = JSON.parse(JSON.stringify(x));
普通的 string,number,object,array 都是能够作深拷贝的。不过这个方法比较偷懒,是存在坑的,好比不支持 NaN,正则,function 等。举个栗子:
const x = { a: function() { console.log('aaa') }, b: NaN, } const y = JSON.parse(JSON.stringify(x)); console.log(y.b); y.a()
试一下就知道了。
Library
一般实现 deep-clone 的库:lodash
,$.extend(true, )
... 目前最好用的是 immutable.js
。 关于 immutable 的经常使用用法,以后会整理一下。
不变性可让数据持久化变得容易。当数据不可变的时候,咱们的每次操做,都不会引发初始数据的改变。也就是说在必定时期内,这些数据是永久存在的,而你能够经过读取,实现相似于“回退/切换快照”般的操做。这是咱们从函数式编程来简单理解这个概念,而不涉及硬盘存储或者数据库存储的概念。
首先,不管数据结构的深浅,每次操做都对整个数据结构进行完整的深拷贝,效率会很低。这就牵扯到在作数据拷贝的时候,利用数据结构,作一些优化。例如,咱们能够观察某次操做,到底有没有引发深层次数据结构的变化,若是没有,咱们是否是能够只作部分改变,而没变化的地方,仍是能够共用的。这就是部分持久化。我知道的 immutable 就是这么作的,两个不可变数据是会共用某部分的。
js 的对象天生是可变的?
我以为做者应该是设计之初就把 js 做为一种灵活性较高的语言去作的,而不可变数据涉及到数据拷贝的算法问题,深拷贝是能够实现的,可是如何最优、效率最高的实现拷贝,并保持数据不可变。这个地方是能够继续研究的。
为何不可变数据的热度愈来愈高?
随着 js 应用的场景愈来愈多,业务场景也愈来愈复杂,一些早就沉淀下来的编程思惟,也被引入 js 中,像 MVC,函数式等等。经典的编程思想,设计模式永远都是不过期的,而不可变数据结构也是如此。而我以为真正让它受关注的,仍是 React 的推出,由于 React 内部就是经过 state/props 比较(===
)去判断是否 render 的,三个等号的比较就要求新的 state 必须是新的引用。另外 Redux 在 React 中的普遍应用,也让函数式编程火热,而函数式编程最重要的原则之一就是不可变数据,因此你在使用
Redux 的时候,改变 store 必须返回新的 state。因此,React-Redux 全家桶,让 immutable data 备受关注,而 immutable,就是目前最好的实现方案。
以后会探究 immutable data 在 React 中的重要性,包括 diff,re-render,redux。天然而然也能够总结出这方面的 React 性能优化。