Skip to content

JavaScript专题之深浅拷贝 #32

Open
@mqyqingfeng

Description

@mqyqingfeng
Owner

前言

拷贝也是面试经典呐!

数组的浅拷贝

如果是数组,我们可以利用数组的一些方法比如:slice、concat 返回一个新数组的特性来实现拷贝。

比如:

var arr = ['old', 1, true, null, undefined];

var new_arr = arr.concat();

new_arr[0] = 'new';

console.log(arr) // ["old", 1, true, null, undefined]
console.log(new_arr) // ["new", 1, true, null, undefined]

用 slice 可以这样做:

var new_arr = arr.slice();

但是如果数组嵌套了对象或者数组的话,比如:

var arr = [{old: 'old'}, ['old']];

var new_arr = arr.concat();

arr[0].old = 'new';
arr[1][0] = 'new';

console.log(arr) // [{old: 'new'}, ['new']]
console.log(new_arr) // [{old: 'new'}, ['new']]

我们会发现,无论是新数组还是旧数组都发生了变化,也就是说使用 concat 方法,克隆的并不彻底。

如果数组元素是基本类型,就会拷贝一份,互不影响,而如果是对象或者数组,就会只拷贝对象和数组的引用,这样我们无论在新旧数组进行了修改,两者都会发生变化。

我们把这种复制引用的拷贝方法称之为浅拷贝,与之对应的就是深拷贝,深拷贝就是指完全的拷贝一个对象,即使嵌套了对象,两者也相互分离,修改一个对象的属性,也不会影响另一个。

所以我们可以看出使用 concat 和 slice 是一种浅拷贝。

数组的深拷贝

那如何深拷贝一个数组呢?这里介绍一个技巧,不仅适用于数组还适用于对象!那就是:

var arr = ['old', 1, true, ['old1', 'old2'], {old: 1}]

var new_arr = JSON.parse( JSON.stringify(arr) );

console.log(new_arr);

是一个简单粗暴的好方法,就是有一个问题,不能拷贝函数,我们做个试验:

var arr = [function(){
    console.log(a)
}, {
    b: function(){
        console.log(b)
    }
}]

var new_arr = JSON.parse(JSON.stringify(arr));

console.log(new_arr);

我们会发现 new_arr 变成了:

不能拷贝函数

浅拷贝的实现

以上三个方法 concat、slice、JSON.stringify 都算是技巧类,可以根据实际项目情况选择使用,接下来我们思考下如何实现一个对象或者数组的浅拷贝。

想一想,好像很简单,遍历对象,然后把属性和属性值都放在一个新的对象不就好了~

嗯,就是这么简单,注意几个小点就可以了:

var shallowCopy = function(obj) {
    // 只拷贝对象
    if (typeof obj !== 'object') return;
    // 根据obj的类型判断是新建一个数组还是对象
    var newObj = obj instanceof Array ? [] : {};
    // 遍历obj,并且判断是obj的属性才拷贝
    for (var key in obj) {
        if (obj.hasOwnProperty(key)) {
            newObj[key] = obj[key];
        }
    }
    return newObj;
}

深拷贝的实现

那如何实现一个深拷贝呢?说起来也好简单,我们在拷贝的时候判断一下属性值的类型,如果是对象,我们递归调用深拷贝函数不就好了~

var deepCopy = function(obj) {
    if (typeof obj !== 'object') return;
    var newObj = obj instanceof Array ? [] : {};
    for (var key in obj) {
        if (obj.hasOwnProperty(key)) {
            newObj[key] = typeof obj[key] === 'object' ? deepCopy(obj[key]) : obj[key];
        }
    }
    return newObj;
}

性能问题

尽管使用深拷贝会完全的克隆一个新对象,不会产生副作用,但是深拷贝因为使用递归,性能会不如浅拷贝,在开发中,还是要根据实际情况进行选择。

下期预告

难道到这里就结束了?是的。然而本篇实际上是一个铺垫,我们真正要看的是 jquery 的 extend 函数的实现,下一篇,我们会讲一讲如何从零实现一个 jquery 的 extend 函数。

专题系列

JavaScript专题系列目录地址:https://github.com/mqyqingfeng/Blog

JavaScript专题系列预计写二十篇左右,主要研究日常开发中一些功能点的实现,比如防抖、节流、去重、类型判断、拷贝、最值、扁平、柯里、递归、乱序、排序等,特点是研(chao)究(xi) underscore 和 jQuery 的实现方式。

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。

Activity

522363215

522363215 commented on Jul 12, 2017

@522363215

期待下一篇!!!

allen3xiaokai

allen3xiaokai commented on Jul 13, 2017

@allen3xiaokai

养肥了再看一遍

yunlzhang

yunlzhang commented on Aug 11, 2017

@yunlzhang

null应该特殊考虑一下吧,在深拷贝中,值为null会赋值一个空对象

mqyqingfeng

mqyqingfeng commented on Aug 11, 2017

@mqyqingfeng
OwnerAuthor

@yunlzhang 感谢指出,现在的 deepCopy 方法确实有这个问题

deepCopy({
        value: null
})

的值为:

{value: {}}

这篇的目的在于讲解深浅拷贝的概念以及深浅拷贝的思路,下一篇 《JavaScript专题之从零实现jQuery的extend》 才是讲解深浅拷贝的详细实现,在下一篇的 extend 方法就有对于 null 的处理~

mengxin-FE

mengxin-FE commented on Aug 16, 2017

@mengxin-FE

楼主对js的理解这么透彻,是怎么学的啊?

mqyqingfeng

mqyqingfeng commented on Aug 17, 2017

@mqyqingfeng
OwnerAuthor

@mengxin-FE javaScript 我还有很多地方需要研究,倒不算透彻,不过谢谢夸奖哈~ 如果说学习方法的话,就是确定一个要研究的主题,然后大量阅读该主题相关的文章,尽量保证每篇文章都能理解,如果不能理解,第二天再看一遍,直到看懂为止,如果可以的话,再写写文章,将学到的知识梳理出来,与大家分享~

Tvinsh

Tvinsh commented on Oct 11, 2017

@Tvinsh
function deepClone (obj) {
  if (Array.isArray(obj)) {
    return obj.map(deepClone)
  } else if (obj && typeof obj === 'object') {
    var cloned = {}
    var keys = Object.keys(obj)
    for (var i = 0, l = keys.length; i < l; i++) {
      var key = keys[i]
      cloned[key] = deepClone(obj[key])
    }
    return cloned
  } else {
    return obj
  }
}

这样也可以

mqyqingfeng

mqyqingfeng commented on Oct 11, 2017

@mqyqingfeng
OwnerAuthor

@Tvinsh 确实可以,感谢分享哈~

naihe138

naihe138 commented on Oct 26, 2017

@naihe138

我从《你不知道的 JavaScript》一书看到,工具函数 JSON.stringify(..) 在将JSON对象序列化为字符串时也用到了 ToString 。请注意, JSON 字符串化并非严格意义上的强制类型转换,因为其中也涉及 ToString 的相 关规则。
对大多数简单值来说, JSON 字符串化和 果总是字符串:toString()的效果基本相同,只不过序列化的结

JSON.stringify( 42 ); // "42" 
JSON.stringify( "42" ); // ""42""(含有双引号的字符串) 
JSON.stringify( null ); // "null" 
JSON.stringify( true ); // "true"

所有 安全的 JSON 值 (JSON-safe)都可以使用 JSON.stringify(..) 字符串化。 安全的 JSON 值是指能够呈现为有效 JSON 格式的值。

下面敲黑板划重点:

为了简单起见, 我们来看看什么是 不安全的 JSON 值 。 undefined 、 function 、 symbol (ES6+)和包含循环引用(对象之间相互引用,形成一个无限循环)的 对象 都不符合 JSON 结构标准,支持 JSON 的语言无法处理它们。

JSON.stringify(..) 在对象中遇到 undefined 、 function 和 symbol 时会自动将其忽略, 在 数组中则会返回 null (以保证单元位置不变)。

例如:

JSON.stringify( undefined ); 
JSON.stringify( function(){} );
JSON.stringify( [1,undefined,function(){},4] ); 
JSON.stringify({ a:2, b:function(){} } );
// undefined // undefined
// "[1,null,null,4]"
// "{"a":2}"

对包含循环引用的对象执行 JSON.stringify(..) 会出错。
...

mqyqingfeng

mqyqingfeng commented on Oct 26, 2017

@mqyqingfeng
OwnerAuthor

@naihe138 非常感谢补充,o( ̄▽ ̄)d JSON.stringify 这部分确实写得太浅薄了。

67 remaining items

xsfxtsxxr

xsfxtsxxr commented on Jan 4, 2021

@xsfxtsxxr

大佬深拷贝没考虑环么?

CoderCxb

CoderCxb commented on Jan 6, 2021

@CoderCxb

有两个问题

typeof obj[key] === 'object' 无法判断null的情况
如果拷贝的是类的实例对象 方法无法拷贝 并且输出的对象的类型是Object而不是class
var deepCopy = function(obj) {
if (!(obj instanceof Object)) return obj;
var newObj = obj instanceof Array ? [] : Object.create(obj.proto);
for (var key of Object.getOwnPropertyNames(obj)) {
newObj[key] = obj[key] instanceof Object ? deepCopy(obj[key]) : obj[key];
}
return newObj;
}
进行了略微的修改,大佬你看看
文章写得很好,点个赞!

growYdp

growYdp commented on Feb 8, 2021

@growYdp

深拷贝的实现中 好像数组还是浅拷贝

zxk-github

zxk-github commented on Apr 15, 2021

@zxk-github

想咨询一下
var newObj = obj instanceof Array ? [] : {}; 这一句修改修改为:
var newObject = new obj.constructr(); 存在什么风险吗

opamine

opamine commented on Jun 13, 2021

@opamine

想咨询一下
var newObj = obj instanceof Array ? [] : {}; 这一句修改修改为:
var newObject = new obj.constructr(); 存在什么风险吗

严格意义上讲两个风险差不多,从原型链方面考虑:1. 开发者会存在修改 obj 的原型的情况,此时 instanceof 判断失效,constructor 同样不起作用 2. 基于前者,开发者还可以自定义原型、修改 constructor 变量指向等

能用 typeof 和 Object.prototype.toString.call() 就优先使用这两个

SilenceTiger

SilenceTiger commented on Aug 30, 2021

@SilenceTiger

完美深复制window.XMLHttpRequest这种类型的有啥办法吗? 上面的方法丢失构造方法或者原型方法

w2xi

w2xi commented on Apr 8, 2022

@w2xi

最新的浏览器可以使用 structuredClone API 来深拷贝了 structuredClone

浏览器兼容:

Chrome Firefox Edge Safari Opera
98 ✔ 94 ✔ 98 ✔ 15.4 ✔ 84 ✔
justorez

justorez commented on Mar 16, 2023

@justorez

处理循环引用的版本:

function deepClone(target, map = new WeakMap()) {
    if (map.has(target)) {
        return map.get(target)
    }

    // 特殊处理:正则、日期
    if (isRegExp(target) || isDate(target)) {
        return new target.constructor(target)
    }

    if (isObject(target)) {
        const cloneTarget = isArray(target) ? [] : {}
        map.set(target, cloneTarget)  // 缓存循环引用的拷贝结果
        for (const prop in target) {
            if (target.hasOwnProperty(prop)) {
                cloneTarget[prop] = deepClone(target[prop], map)
            }
        }
        return cloneTarget
    } else {
        return target
    }
}

拷贝函数,有两种写法貌似可以:

eval(target.toString())
// or
new Function(`return ${target.toString()}`)()
ethanzhongyi

ethanzhongyi commented on Mar 10, 2024

@ethanzhongyi

hi,问下深拷贝中对于递归调用的怎么处理

justorez

justorez commented on Mar 12, 2024

@justorez

hi,问下深拷贝中对于递归调用的怎么处理

@ethanzhongyi 用 WeakMap 缓存已处理的对象

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @wweggplant@LeoTao@allenGKC@mqyqingfeng@uniquexiaobai

        Issue actions

          JavaScript专题之深浅拷贝 · Issue #32 · mqyqingfeng/Blog