Intro首先明确两个概念:
深拷贝和浅拷贝只针对像 Object, Array 这样的复杂对象的,如果只是值类型数据,则不存在所谓的深拷贝;
浅拷贝只拷贝一层对象的属性,而深拷贝则递归拷贝了所有层级。
这就引出了另一个基础的概念:数据类型。
数据类型
通常把数据类型分为 值类型 和 引用类型:
注:Symbol 是 ES6 引入了一种新的原始数据类型,表示独一无二的值。
以Number类型举例
var a = 1;
var b = a; // b: 1
b = 2;
console.log(a, b); // a: 1, b: 2
可以看出基本类型并没有什么问题,接下来看引用数据类型的例子:
var obj1 = {id: '1'};
var obj2 = obj1; // [object Object] { id: 1 }
obj2.id = 2;
console.log(obj1, obj2); // [object Object] { id: 2 }, [object Object] { id: 2 }
可以看到,当修改 obj2的id属性时,obj1的id属性也同时修改了,这是因为 Javascript 存储对象都是存在内存里的一块地址,而简单的浅拷贝或者赋值只是把对象的引用(或者说指针)拷贝给了另一个对象,无论哪个变量修改,其实操作的都是内存里的同一个对象。
【补】最新的 ECMAScript 标准定义了 8 种数据类型:
接着深究,就牵涉到了栈和堆的概念。
栈和堆
栈指的是栈内存(stack):
Javascript中的基本类型按值存在栈内存中,数据的大小都是确定或者是在一定范围内的,由系统自动分配和释放,所以更容易对内存进行管理;
堆指的是堆内存(heap):
Javascript中引用类型如Object、Array、Function等存在于堆中,指针存放在栈中。当访问引用类型时,先在栈中找到对应的地址指针,然后根据指针找到堆中的数据。
底层原理清楚后,就更能理解数据类型的一些行为了。
实现
明白了数据类型以及栈和堆,就能很容易的理解深拷贝和浅拷贝:
浅拷贝
方法一:遍历被拷贝对象的属性
function copy(obj) {
var copyObj = {};
for(var key in obj) {
copyObj[key] = obj[key];
}
return copyObj;
}
var o1 = {name: 'o1'}
var o2 = copy(o1);
console.log(o2); // [object Object] { name: "o1" }
方法二:Object.assign
Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象分配到目标对象。它将返回目标对象。
var source = {success: true, count: 1};
var target = Object.assign({}, source); // [object Object] { count: 1, success: true }
深拷贝
方法一:JSON.parse(JSON.stringify(data))
var source = {success: true, content: {isUserLogin: true}};
var target = JSON.parse(JSON.stringify(source)); // {success: true, content: {isUserLogin: true}}
但是序列化有很多问题:
如果对象中有函数或undefined,则有可能将函数丢失;
如果对象中有NaN、Infinity、-Infinity,则变成null;
如果对象中有Date、Error等对象,则会变成空对象。
如果确定对象中的数据是“常规”的,则序列化是最简单实现深拷贝的方法。
方法二:递归
function deepClone(source) {
if (source && typeof source === 'object') {
var target = source instanceof Array ? [] : {};
// var target = Object.prototype.toString.call(source) === '[object Array]' ? [] : {};
for (var k in source) {
if (source.hasOwnProperty(k)) {
if (source[k] && typeof source[k] === 'object') {
target[k] = deepClone(source[k]);
} else {
target[k] = source[k];
}
}
}
return target;
} else {
return source;
}
}
首先判断是否为对象,如果不是,则直接返回;
如果是对象,则判断源对象是数组还是对象,来初始化target;
遍历源对象,如果熟悉是基本类型,则直接赋值,否则就递归调用deepClone函数自身;
接下来可优化的点:
如果是Function、Date、RegExp等类型怎么处理;
如果对象自身的属性指向自身怎么处理;
考虑遍历的性能;
第三方库的实现
jQuery
$.clone
和 $.extend
在 jQuery 中有一个叫 $.clone()
的方法,可是它并不是用于一般的 JS 对象的深拷贝,而是用于 DOM 对象,无需关注。
可以通过添加一个参数来实现递归extend。调用$.extend(true, {}, …)
就可以实现深拷贝。
var obj1 = {success: true, content: {isLogin: true}};
var obj2 = $.extend({}, obj1); // 浅拷贝
var obj3 = $.extend(true, {}, obj1); // 深拷贝
Lodash
_.clone
浅拷贝,_.clone(obj, true)
等价于深拷贝_.cloneDeep(obj)
Outro
考察的知识点
栈和堆的概念;
基本数据类型和引用数据类型;
递归思想;