js 对象深拷贝
JS 里面的变量类型可以分为 基本类型 和 引用类型 。
在使用过程中,引用类型经常会产生一些无法意识到的副作用,所以在现代 JS 开发过程中,有经验的开发者都会在特定位置有意识的写下断开引用的不可变数据类型。
// 因为引用所带来的副作用:
var a = [{ val: 1 }]
var b = a.map(item => item.val = 2)
// 期望:b 的每一个元素的 val 值变为 2,但最终 a 里面每个元素的 val 也变为了 2
console.log(a[0].val) // 2
在发现这样的问题之后,解决方案也很简单。一般来说当需要传递一个引用类型的变量(例如对象)进一个函数时,我们可以使用 Object.assign 或者 ... 对对象进行解构,成功断掉一层的引用。 例如上面的问题我们可以改用下面的这种写法:
var a = [{ val: 1 }]
var b = a.map(item => ({ ...item, val: 2 }))
console.log(a[0].val) // 1
console.log(b[0].val) // 2
对于深层级的对象我们可以实现一个递归方法来实现对象的深拷贝
function deepClone(obj) {
const keys = Object.keys(obj)
return keys.reduce((memo, current) => {
const value = obj[current]
if (typeof value === 'object') {
// 如果当前结果是一个对象,那我们就继续递归这个结果
return {
...memo,
[current]: deepClone(value),
}
}
return {
...memo,
[current]: value,
}
}, {})
}
但是往往在实际的开发过程中这样的方法并无法覆盖所有情况
- key 里面 getter,setter 以及原型链上的内容
- value 是一个 Symbol
- value 内部出现了一些循环引用等
- 频繁的递归会对性能造成很大的影响
immutable.js
immutable-js 使用了另一套数据结构的 API ,与我们的常见操作有些许不同,它将所有的原生数据类型(Object, Array等)都会转化成 immutable-js 的内部对象(Map,List 等),并且任何操作最终都会返回一个新的 immutable 的值。
const { fromJS } = require('immutable')
const data = {
val: 1,
desc: {
text: 'a',
},
}
const a = fromJS(data)
const b = a.set('val', 2)
console.log(a.get('val')) // 1
console.log(b.get('val')) // 2
const pathToText = ['desc', 'text']
const c = a.setIn([...pathToText], 'c')
console.log(a.getIn([...pathToText])) // 'a'
console.log(c.getIn([...pathToText])) // 'c'
在 immutable-js 的数据结构中,深层次的对象 在没有修改的情况下仍然能够保证严格相等,这也是 immutable-js 的另一个特点 「深层嵌套对象的结构共享」。即嵌套对象在没有改动前仍然在内部保持着之前的引用,修改后断开引用,但是却不会影响之前的结果。
但是由于immutable的api与js的原生api有不少的出入,因此在使用和迁移的现有代码过程的时候有较高的成本。
immer.js
与 immutable-js 最大的不同,immer 是使用原生数据结构的 API 而不是像 immutable-js 那样转化为内置对象之后使用内置的 API
const produce = require('immer')
const state = { done: false, val: 'string', }
const newState = produce(state, (draft) => {
draft.done = true
})
console.log(state.done) // false
console.log(newState.done) // true
通过上面的例子我们能发现,所有具有副作用的逻辑都可以放进 produce 的第二个参数的函数内部进行处理。在这个函数内部对原来的数据进行任何操作,都不会对原对象产生任何影响。
这里我们可以在函数中进行任何操作,例如 push splice 等非 immutable 的 API,最终结果与原来的数据互不影响。
总结
- Immer 的 API 非常简单,上手几乎没有难度,同时项目迁移改造也比较容易。immutable-js 上手就复杂的多,使用 immutable-js 的项目迁移或者改造起来会稍微复杂一些。
- Immer 需要环境支持 Proxy 和 defineProperty,否则无法使用。但 immutable-js 支持编译为 ES3 代码,适合所有 JS 环境。
- Immer 的运行效率受到环境因素影响较大。immutable-js 的效率整体来说比较平稳,但是在转化过程中要先执行 fromJS 和 toJS,所以需要一部分前期效率的成本。