资讯专栏INFORMATION COLUMN

不可变数据

lanffy / 1176人阅读

摘要:为什么要有不可变数据首先,不可变数据类型是源于函数式编程中的,是一条必备的准则。另外在中的广泛应用,也让函数式编程火热,而函数式编程最重要的原则之一就是不可变数据,所以你在使用的时候,改变必须返回新的。

不可变数据 引入

我是通过使用 React 才去关注 immutable data 这个概念的。事实上,你去搜 immutable 的 JS 相关文章,也基本都是近两年的,大概是随着 React 的推广才备受关注。但是这篇文章不会去介绍 React 是如何在意 immutable data 的,而是从原生 JS,写一些自己的思考。

个人 blog,欢迎 star。https://github.com/sunyongjian

可变/不可变对象

可变对象是一个可在其创建后修改状态的对象,而不可变对象则是创建之后,不能再修改状态,对其任何删改操作,都应返回一个新的对象。

一个例子开始:

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 都是可变数据。
不过字符串和数字就不会。

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 = strb = 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 中几种原生的拷贝方法,了解他们能实现的程度。

Object.assign

像这样:

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} 的地址。还有几个方法也是这样的。

Object.freeze
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,看起来是真的“冻结”了,不可变了,其实效果是一样的,为了效率,做的浅拷贝。

deconstruction 解构
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 原生对象的方法,是没有给我们提供深拷贝功能的。

deep-clone

如何去做深拷贝

原生

拿上面的栗子来说,我们去实现深拷贝。

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 性能优化。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/89779.html

相关文章

  • 第3章:抽象数据类型(ADT)和面向对象编程(OOP) 3.1数据类型和类型检查

    摘要:所有变量的类型在编译时已知在程序运行之前,因此编译器也可以推导出所有表达式的类型。像变量的类型一样,这些声明是重要的文档,对代码读者很有用,并由编译器进行静态检查。对象类型的值对象类型的值是由其类型标记的圆。 大纲 1.编程语言中的数据类型2.静态与动态数据类型3.类型检查4.易变性和不变性5.快照图6.复杂的数据类型:数组和集合7.有用的不可变类型8.空引用9.总结 编程语言中的数据...

    zhangqh 评论0 收藏0
  • React 状态管理库: Mobx

    摘要:关心性能的情况下,需要手动设置这时就需要引入状态管理库。现在常用的状态管理库有和,本文会重点介绍,然后会将和进行对比,最后展望下未来的状态管理方面趋势。如果在任何地方都修改可观察数据,将导致页面状态难以管理。 React 是一个专注于视图层的库。React 维护了状态到视图的映射关系,开发者只需关心状态即可,由 React 来操控视图。 在小型应用中,单独使用 React 是没什么问题...

    liujs 评论0 收藏0
  • String:String类型为什么可变

    摘要:性能当字符串是不可变时,字符串常量池才有意义。字符串常量池的出现,可以减少创建相同字面量的字符串,让不同的引用指向池中同一个字符串,为运行时节约很多的堆内存。 在学习Java的过程中,我们会被告知 String 被设计成不可变的类型。为什么 String 会被 Java 开发者有如此特殊的对待?他们的设计意图和设计理念到底是什么?因此,我带着以下三个问题,对 String 进行剖析: ...

    zhiwei 评论0 收藏0
  • WebGL2系列之可变纹理

    摘要:除此之外,还可以通过函数独立指定纹理的每个的级别。这种绘图时检查可能代价很高,而使用不可变纹理可以避免这种情形。不可变纹理使用不可变纹理,可以减少上文中提到的因检查而导致的性能开销。不可变纹理指的是纹理的一种分配方式,而不是值纹理的内容。 纹理背景知识 在WebGL1中,纹理包括2D纹理和立方体纹理,在实际的使用中,如果纹理的图片是宽和高是2的幂,可以自动生成纹理的mipmap。除此之...

    xinhaip 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<