本文详细解析JavaScript中深拷贝与浅拷贝的区别,深入探讨引用类型赋值导致的内存共享问题。通过Object.prototype.toString方法实现类型判断,递归实现深拷贝函数,并分析函数、DOM元素及JSON转换在深拷贝中的处理局限与解决方案。
JavaScript深拷贝概述
在JavaScript开发中,深拷贝是初学者乃至资深开发者常遇到的难点,许多人对此概念的理解并不透彻。
什么是深拷贝?
与深拷贝相对的概念是浅拷贝,许多初学者在初次接触这一概念时往往感到困惑。
深拷贝的必要性
为什么需要深拷贝?
在编程中,我们经常需要为变量赋值,即为内存地址赋予一个值。然而,当对引用类型数据进行赋值时,新变量与原变量会共享同一块内存区域。这导致修改其中一个变量的值时,另一个变量的值也会随之改变,保持了一致性。
具体示例分析
// 给test赋值了一个对象
var test = {
a: 'a',
b: 'b'
};
// 将test赋值给test2
// 此时test和test2是共享了同一块内存对象,这也就是浅拷贝
var test2 = test;
test2.a = 'a2';
test.a === 'a2'// 为true
通过图解可以更直观地理解这一过程。
这样就能清楚地理解为什么引用类型数据在赋值时会相互影响。
深拷贝函数实现
代码实现
要实现一个深拷贝函数,首先需要了解JavaScript的数据类型。
JavaScript类型判断方法
JavaScript包含以下几种基本数据类型:
类型与描述如下: - undefined:undefined类型仅有一个值undefined,表示变量未被赋值。 - null:null类型仅有一个值null,表示空的对象引用。 - Boolean:Boolean类型有两个取值true和false。 - String:表示文本信息。 - Number:表示数字信息。 - Object:是一系列属性的无序集合,包括函数Function和数组Array。 使用typeof无法准确判断function和array的类型,因此这里采用Object.prototype.toString方法。默认情况下,每个对象都会从Object上继承toString()方法,如果该方法未被对象自身或上层原型覆盖,调用该对象的toString()方法时会返回"[object type]",其中type表示对象类型。
function type(obj) {
var toString = Object.prototype.toString;
var map = {
'[object Boolean]' : 'boolean',
'[object Number]' : 'number',
'[object String]' : 'string',
'[object Function]' : 'function',
'[object Array]' : 'array',
'[object Date]' : 'date',
'[object RegExp]' : 'regExp',
'[object Undefined]': 'undefined',
'[object Null]' : 'null',
'[object Object]' : 'object'
};
return map[toString.call(obj)];
}
深拷贝函数实现
对于非引用类型的值,直接赋值即可;而对于引用类型(object),则需要再次遍历并进行递归赋值。
function deepClone(data) {
var t = type(data), o, i, ni;
if(t === 'array') {
o = [];
}else if( t === 'object') {
o = {};
}else {
return data;
}
if(t === 'array') {
for (i = 0, ni = data.length; i < ni; i++) {
o.push(deepClone(data[i]));
}
return o;
}else if( t === 'object') {
for( i in data) {
o[i] = deepClone(data[i]);
}
return o;
}
}
这里需要注意一点:对于function类型,本文中的实现是直接赋值的,即共享同一个内存值。这是因为函数主要用于完成特定功能,涉及输入和返回值,且上层业务主要关注其功能实现,通常不需要真正对函数进行深拷贝。
那么,function类型该如何拷贝呢?
目前想到的方法是使用new操作符,但这会导致函数执行一遍,其执行结果难以预测。暂时还没有其他更好的想法,欢迎大家提供指导。
至此,深拷贝的实现大致完成。有人可能会问,为什么没有实现浅拷贝?
浅拷贝与数据安全性
什么是浅拷贝?
浅拷贝可以理解为只操作一个共同的内存区域,这种做法存在潜在风险。
如果直接操作共享数据且不加控制,很容易出现数据异常或被其他部分意外更改的情况。因此,建议不要直接操作数据源,而是为数据源封装一些方法来进行CURD(增删改查)操作。
到这里内容基本完整,但作为前端开发者,不仅要考虑JavaScript本身,还需考虑DOM和浏览器环境。
DOM元素与Element类型处理
Element类型处理
查看以下代码,其返回结果是什么?
Object.prototype.toString.call(document.getElementsByTagName('div')[0])
答案是[object HTMLDivElement]。
有时保存了DOM元素,若不小心进行深拷贝,上述深拷贝函数会因缺少对Element元素的判断而出错。判断Element元素需使用instanceof,因为对于不同的标签,toString会返回对应不同标签的构造函数。
function type(obj) {
var toString = Object.prototype.toString;
var map = {
'[object Boolean]' : 'boolean',
'[object Number]' : 'number',
'[object String]' : 'string',
'[object Function]' : 'function',
'[object Array]' : 'array',
'[object Date]' : 'date',
'[object RegExp]' : 'regExp',
'[object Undefined]': 'undefined',
'[object Null]' : 'null',
'[object Object]' : 'object'
};
if(obj instanceof Element) {
return 'element';
}
return map[toString.call(obj)];
}
其他库的深拷贝实现
其他实现方式?
jQuery的实现
详见https://github.com/jquery/jqu...
Underscore的实现
详见https://github.com/jashkenas/...
Lodash的实现
详见https://github.com/lodash/lod...
JSON实现方案
先通过JSON.stringify序列化,再通过JSON.parse反序列化,即可实现深拷贝。但这种方式仅支持基本数值类型。
var obj = {
a: 'a',
b: function(){console.log('b')}
}
//在JSON.stringify的时候就会把function给过滤了。
JSON.stringify(obj)// "{"a":"a"}"
总结
本文大致总结了深拷贝的概念及其实现方法。在不同场景下,应根据具体业务需求判断是否使用深拷贝。