JavaScript中的常量和深复制

概述

一些特殊情况下会需要深度复制出一个全新的对象(内存级别的复制),深复制有几种方法,各自有不同的使用场景和性能。

多种深复制方式以及实验结果分析

JSBench是一个测试JavaScript片段代码性能的网站,该网站会将输入的多片JavaScript代码的执行速度进行对比。

假设需要对以下代码需要被深复制,先来看四种深复制的方法。

1
2
3
4
5
6
7
8
9
10
const foo = {
a: "hello",
b: "world",
c: 11,
arr: [1, 2, 3, 4],
child: {
a: 22,
str: "whazzup"
}
};

实践一:结合JSON.stringify()和JSON.parse()方法复制对象。

这种方法的缺点是不能正确地处理日期格式,无法处理NaN等特殊数据。

1
const bar1 = JSON.parse(JSON.stringify(foo));

实践二:使用jQuery.extend()方法复制对象。

1
const bar2 = $.extend(true, {}, foo);

实践三:Lodash是一个使用的JavaScript基本工具方法库。使用lodash.cloneDeep()方法复制对象。

1
const bar3 = _.cloneDeep(foo, true);

实践四:使用angular.copy()方法复制对象。

1
const bar4 = angular.copy(foo);

使用JSBench多次实验后的结果排名均大同小异,最有效的方式是使用angular.copy()方法进行深复制,其次是lodash.clone(),再次是JSON.stringify()和JSON.parse(),最后是jQuery.extend()方法。

实际上从多次实验结果(仅仅从百分比来考虑的话)来看,Lodash和stringify差异均不大,stringify效果略优于Lodash。

变量和常量

ES6中引入了两个新的关键字,let用于声明变量,const用于声明常量。

实际上在代码中的绝大多数变量都只是用来临时存储,曾经看到过国外几篇博客和一些StackOverflow的回答称,绝大多数情况下都应该使用const来定义常量。但是需要注意的是,const定义的基本类型虽然不会被改变,但const定义的对象是可以被改变的。

1
2
3
4
5
6
const foo = 5;
foo = 6; // Error
const bar = {
baz: 5,
};
bar.baz = 6;

有一种可行的方式是使用Object.freeze()方法固定住当前的属性和值。

1
2
3
4
5
6
const foo = {
baz: 5,
};
const bar = Object.freeze(foo);
bar.baz = 6; // 不会报错,但也没有改变值
console.log(bar.baz); // 5

这种方法的缺点在于,他只能固定当前层的对象中保存着基本类型值得属性,如果有嵌套的对象存在的话,则需要递归进行freeze。

即便如此,在ES6中,对于let,var和const关键字的使用,个人建议尽量使用const,需要变量的时候再用let,至于var应该被彻底禁止。其实现在绝大多数现代浏览器对ES6都支持的非常好了,因此一般情况下的JavaScript也如此推荐,而需要兼容老版浏览器的情况下则尽量使用诸如babel之类的编译器。

ImmutableJS

前面我们已经提到了实践中应该尽量使用const关键字来定义常量,然而即使是使用const关键字,如果定义的是对象的话也是可以改变其中的内容的,而要想固定一个对象则必须递归调用Object.freeze()方法对对象的每一层嵌入对象作处理。

Facebook推出的ImmutableJS是不可变数据和深复制的一种非常好的实践和选择,它可以帮助我们解决深复制和常量的问题。

跟踪状态的改变和维护状态是让应用开发变得困难的主要原因。使用不可变数据开发促使你以与以往不同的方式思考数据如何在你的应用中流动。

以下是基本的示例。

1
2
3
4
5
6
import Immutable from 'immutable';
const map1 = Immutable.Map({foo: 1, bar: 2});
const map2 = map1.set('bar', 3);
console.log(map1.get('bar')); // 2
console.log(map2.get('bar')); // 3
console.log(map1 === map2); // false

这里创建了一个Immutable.Map类型的数据map1,然后使用set()方法对map1进行了属性修改,并将返回值赋值了给了map2,但是从之后的输出结果可以看出,map1和map2实际上是两个完全不同的对象。

实际上,如果同一层面上的数据都是不可变的,则深度复制就变得没有意义了,存在两个值完全相同地址不同的对象除了浪费内存意外毫无意义,因为所有的数据都是不可变的。所以除非手动创建,不然如果不修改Immutable对象的值,Immutable是不会返回一个新的内存的对象的。

关于ImmutableJS,结合React和Redux使用是非常好的实践。具体推荐官网的文档这篇文章

参考

  1. JSBench
  2. Lodash
  3. ImmutableJS
  4. Introduction to Immutable.js and Functional Programming Concepts - Sebastián Peyrott