简述
JavaScript 定义变量时,有两种类型,分别是 原始类型 和 引用类型。
原始类型有:
- Boolean
- Null
- Undefined
- Number
- String
引用类型有:
- Object
- Array
- Function
const age = 28; // 原始类型
cosnt name = 'Tom'; // 原始类型
const loading = false; // 原始类型
const user = undefined; // 原始类型
const response = null; // 原始类型
const user = { name: 'Tom' }; // 引用类型
const users = ['tom', 'ben']; // 引用类型
const foo = () => ({}); // 引用类型
在其他技术Blog中,原始类型也会被称为 基本类型、基础类型、原始值,其实大家都是在描述同一样东西。在Mozilla官方文档上,原始类型称 Primitive value,引用类型称 Reference value。
那这两个类型有什么区别呢?
在 JavaScript 引擎中有两个地方可以存储数据,分别是 Stack(栈) 和 Heap(堆),当原始类型做赋值操作的时候,是直接把变量和实际的值(31、'tom'、false等)存储到Stack中,引用类型赋值操作却有点区别,它会在Heap创建对象值,然后在Stack创建变量并且标记Heap的地址(或说内存地址)。
原始类型
var a = 'hello';
var b = a;
a = 'goodbye';
console.log(a); // 'goodbye'
console.log(b); // 'hello'
上面是原始类型的操作例子,我们定义了一个变量a并且赋值'hello',然后再定义一个变量b,并且把a赋值给b,因为b拷贝了a的值,所以当修改a的时候,b变量是不会受影响的,下图展现了原始类型赋值的内部过程。
引用类型
var a = { name: "tom" };
var b = a;
a.name = "ben";
console.log(a); // {name: "ben"}
console.log(b); // {name: "ben"}
我们现在来看另外一个例子,上面是引用类型的操作例子,我们定义一个变量a并且赋值它一个对象,然后再定义一个新变量b,并且把a赋值给b,此时把a对象里的name属性修改为'ben',如果按常规推理来说,结果应该是一个'tom'一个'ben',但是当我们输出a、b的时候,却发现a、b的name属性都为'ben',我们刚刚明明只是修改了a变量的属性,为什么b变量也受到了影响呢?
因为引用类型的赋值与原始类型不同,它会在Heap创建对象值,然后在Stack创建变量并且标记Heap的地址,所以当我们把a赋值给b的时候,不是拷贝了真实的值,只是拷贝了a的引用地址,当我们修改a的对象时,b也会受到影响,下图展现了引用类型赋值的内部过程。
所以上述例子,修改a对象属性时,b的对象属性也会一起改变,因为a、b变量指向都是同一个对象。
那如果我们需要拷贝a变量真实的值,应该怎样做呢?
最简单方法是使用ES6的展开运算符(spread operator),如下图例子,这时候的b才是拷贝了a的真实值,不受a的修改影响。
var a = { name: 'tom' };
var b = {...a};
a.name = 'ben';
console.log(a); // { name: 'ben' }
console.log(b); // { name: 'tom' }
以下的方式是常用的数组对象拷贝方式,大家可以了解以下
const data = [1,2,3]
// 1. 展开运算符 (Spread Operator) - 浅拷贝
const newData = [...data]
// 2. Object.assign() - 浅拷贝
const newData = Object.assign([], data)
// 3. JSON - 深拷贝
const newData = JSON.parse(JSON.stringify(data))
// 4. 使用lodash、ramda库去操作 - 浅拷贝/深拷贝
const newData = _.cloneDeep(data);
PS:日常开发中,我更建议大家使用lodash、ramda等等的库去进行数组对象的拷贝,可读性会高一些,而且这些库对数组对象操作上有一定的性能优化。
浅拷贝 和 深拷贝
不知道大家有没有留意到上面的常用数组对象拷贝方式里面的注释,有些注释了浅拷贝(shallow copy),有些注释了深拷贝(deep copy),那这两种拷贝方式有什么区别呢?大家可以看看以下例子
const users = [
{ name: "tom", friends: ["eddy"] }
];
const newUsers = [...users];
users[0].friends = [];
console.log(users); // [ { name: 'tom', friends: [] } ]
console.log(newUsers); // [ { name: 'tom', friends: [] } ]
大家是不是有点疑惑,我明明使用了ES6的展开运算符把users的真实值赋值给newUsers,为什么当修改users时,newUsers还是被受到影响呢?
因为使用ES6展开运算符是浅拷贝,浅拷贝只覆盖数组对象的第一层,其他的都是引用,如果你想要拷贝数组对象中嵌套元素的真实值,需要使用深拷贝,如下
const users = [
{ name: "tom", friends: ["eddy"] }
];
const newUsers = JSON.parse(JSON.stringify(users))
users[0].friends = [];
console.log(users); // [ { name: 'tom', friends: [] } ]
console.log(newUsers); // [ { name: 'tom', friends: [ 'eddy' ] } ]
PS:总结来说,如果你要拷贝的数组对象中有数组对象元素,那你就需要使用深拷贝,否则使用浅拷贝即可。
参考资源
developer.mozilla.org/en-US/docs/…
stackoverflow.com/questions/3…