JavaScript深拷贝原理与实现详解

作者:半夏时间:2026-10-30 12:42:39浏览:143

本文详细解析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"}"

总结

本文大致总结了深拷贝的概念及其实现方法。在不同场景下,应根据具体业务需求判断是否使用深拷贝。

作者最新文章