JS中实现深复制

在JS中有5种基本数据类型:Undefined、Null、Boolean、Number和String。这5种基本数据类型是按值访问的,在复制的时候可以直接使用赋值操作。例:

var num=3;
var numCopy=num; //numCopy=3

但是JS中还有5种引用类型(Object、Array、Date、RegExp和Function)以及3种基本包装类型(Boolean、Number和String)。引用类型的值是保存在内存中的对象,引用类型的值是按引用访问的,因此,直接赋值操作只是相当于复制了一个保存对象的引用而并不是实际的对象,也即是浅复制。例:

var obj={'name':'Bob'};
var objCopy=obj;
obj===objCopy //true

这样的话操作objCopy实际上相当于操作obj:

objCopy.name="John";
obj.name //John

下面就针对于JS种的这些数据类型写一个函数(本文最后附有完整代码),实现对所有数据类型进行深复制的操作。该函数接受一个参数(obj),首先使用typeof操作符区分obj的类型,typeof操作符可能返回下列某个字符串:

  • “undefined”——如果这个值未定义;
  • “boolean”——如果这个值是布尔值;
  • “string”——如果这个值是字符串;
  • “number”——如果这个值是数值;
  • “function”——如果这个值是函数;
  • “object”——如果这个值是对象或null;

函数首先处理Null类型除外的基本类型:

function deepCopy(obj){
    var result;
    switch(typeof obj){
        case "undefined":
            break;
        case "boolean":
            result=!!obj;
            break;
        case "string":
            result=obj+"";
            break;
        case "number":
            result=obj+0;
            break;
    }
    return result;
}

上面的代码处理了4种基本数据类型。而对于引用类型也是要区分的,因为对引用类型使用typeof操作符会返回”function”和”object”两种结果,首先看”function”,JS中的函数其实也是个对象,而函数名相当于只是一个指向该对象的指针,而由于JS中没有函数重载的概念,因此对函数进行深复制并没有太大的意义,但是为了完整性还是对此进行实现,而对函数的深复制通过ECMAScript 5中定义的bind()方法来实现,该方法会创建一个函数的实例,其this值会被绑定到传给bind()函数的值。代码如下:

        case "function":
            result=obj.bind(this);
            break;

Function类型深复制
图1、Function类型深复制


下面开始讨论最复杂的一种情况:当typeof操作符返回”object”时。此时要分几种情况来讨论,首先处理obj等于null的情况。代码如下:

        case "object":
            if(obj===null){
                result=null;
            }

其次是区分具体类型的问题,很显然此时不能再用typeof操作符进一步区分了。我们知道在任何值上调用Object原生的toString()方法都会返回一个[object NativeConstructorName]格式的字符串,如:Object.prototype.toString.call([])==”[object Array]”,而Object.prototype.toString.call({})==”[object Object]”,因此可以使用该方式来进一步区分引用类型。代码如下:

        case "object":
            if (obj === null) {
                result = null;
            } else {
                var constructorName = Object.prototype.toString.call(obj).slice(8, -1);
                if (constructorName == "Array") { //Array类型
                    result = [];
                    for (var i = 0; i < obj.length; i++) {
                        result[i] = deepCopy(obj[i]); //注1
                    }
                } else if (constructorName == "Date") { //Date类型
                    result = new Date(obj.valueOf()); //注2
                } else if (constructorName == "RepExp") { //RepExp类型
                    var pattern = obj.valueOf(); //注3
                    var flags = '';
                    flags += pattern.global ? 'g' : '';
                    flags += pattern.ignoreCase ? 'i' : '';
                    flags += pattern.multiline ? 'm' : '';
                    result = new RegExp(pattern.source, flags);
                } else if (constructorName == "Boolean") { //基本包装类型中的Boolean类型
                    result = new Boolean(obj.valueOf()); //注4
                } else if (constructorName == "Number") { //基本包装类型中的Number类型
                    result = new Number(obj.valueOf());
                } else if (constructorName == "String") { //基本包装类型中的String类型
                    result = new String(obj.valueOf());
                } else { //Object类型
                    var Constructor = obj.constructor; //注5
                    result = new Constructor();
                    for (var attr in obj) {
                        if (obj.hasOwnProperty(attr)) {
                            result[attr] = deepCopy(obj[attr]);
                        }
                    }
                }
            }
            break;

然后对上述代码进行分析一下。
注1:此处需使用递归,因为ECMAScript数组的每一项可以保存任意类型的数据,因此这里并不能使用简单的赋值。比如数组中的某一项是对象,那此时如果直接用赋值的方式,问题就和本文第二段描述的问题相同了。
注2:对于Date类型来说,它的valueOf()方法返回日期的毫秒表示,也即是obj.valueOf()返回的是自UTC(Coordinated Universal Time,国际协调时间)1970年1月1日午夜(零时)开始到obj经过的毫秒数。此时调用Date的构造函数传入该毫秒数就可以得到一个与obj值相等的日期对象,只是它们是两个不同的引用,因此实现了深复制。
注3:ECMAScript通过RegExp类型来支持正则表达式。RegExp的valueOf()方法返回正则表达式本身,而RegExp的每个实例都包含以下5个属性:globel 表示是否设置了g标志;ignoreCase 表示是否设置了i标志;lastIndex 表示开始搜索下一个匹配项的字符位置,从0算起;multiline 表示是否设置了m标志;source 正则表达式的字符串表示。因此可以通过这些信息调用RegExp类型的构造函数创建正则表达式类型的实例。
注4:基本包装类型属于特殊的引用类型。可以使用new调用基本包装类型的构造函数,这样得到的是对应类型的对象。建议是永远都不要使用这种方式。
注5:此处考虑到自定义数据类型,因此要保留该对象的构造函数指针,具体参见下面的示例。
综合数组类型深复制示例
图2、综合数组类型深复制示例
自定义对象深复制
图3、自定义对象深复制
上图3中创建了自定义类型Person,在创建了该类型的实例后调用方法进行深复制,从打印的结果可以看出:原始的obj和复制出来的newObj均属于Person类型,而对newObj的修改并不会造成obj的修改。由此,实现了自定义对象的深复制。


总结:本文通过对JS中各种类型进行分析,定义了一个方法在JS中实现了深复制。其实深复制和浅复制的概念只是针对引用类型而言,因为JS不允许直接访问内存中的位置,也就是说不能直接操作对象的内存空间。因此,针对基本类型使用的复制操作在针对引用类型复制时是不能达到期望效果的。由于本人知识有限,如有错误希望大家能指正。期望与大家共同学习进步。


附:深复制函数完整代码

function deepCopy(obj) {
    var result;
    switch (typeof obj) {
        case "undefined":
            break;
        case "boolean":
            result = !!obj;
            break;
        case "string":
            result = obj + "";
            break;
        case "number":
            result = obj + 0;
            break;
        case "object":
            if (obj === null) {
                result = null;
            } else {
                var constructorName = Object.prototype.toString.call(obj).slice(8, -1);
                if (constructorName == "Array") { //Array类型
                    result = [];
                    for (var i = 0; i < obj.length; i++) {
                        result[i] = deepCopy(obj[i]); //注1
                    }
                } else if (constructorName == "Date") { //Date类型
                    result = new Date(obj.valueOf()); //注2
                } else if (constructorName == "RepExp") { //RepExp类型
                    var pattern = obj.valueOf(); //注3
                    var flags = '';
                    flags += pattern.global ? 'g' : '';
                    flags += pattern.ignoreCase ? 'i' : '';
                    flags += pattern.multiline ? 'm' : '';
                    result = new RegExp(pattern.source, flags);
                } else if (constructorName == "Boolean") { //基本包装类型中的Boolean类型
                    result = new Boolean(obj.valueOf()); //注4
                } else if (constructorName == "Number") { //基本包装类型中的Number类型
                    result = new Number(obj.valueOf());
                } else if (constructorName == "String") { //基本包装类型中的String类型
                    result = new String(obj.valueOf());
                } else { //Object类型
                    var Constructor = obj.constructor; //注5
                    result = new Constructor();
                    for (var attr in obj) {
                        if (obj.hasOwnProperty(attr)) {
                            result[attr] = deepCopy(obj[attr]);
                        }
                    }
                }
            }
            break;
        case "function":
            result = obj.bind(this);
            break;
        default:
            result = obj;
            break;
    }
    return result;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值