Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JavaScript专题之深浅拷贝 #32

Open
mqyqingfeng opened this issue Jul 12, 2017 · 72 comments
Open

JavaScript专题之深浅拷贝 #32

mqyqingfeng opened this issue Jul 12, 2017 · 72 comments

Comments

@mqyqingfeng
Copy link
Owner

mqyqingfeng commented Jul 12, 2017

前言

拷贝也是面试经典呐!

数组的浅拷贝

如果是数组,我们可以利用数组的一些方法比如: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,对作者也是一种鼓励。

@522363215
Copy link

期待下一篇!!!

@zhouyingkai1
Copy link

养肥了再看一遍

@yunlzhang
Copy link

yunlzhang commented Aug 11, 2017

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

@mqyqingfeng
Copy link
Owner Author

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

deepCopy({
        value: null
})

的值为:

{value: {}}

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

@mengxin-FE
Copy link

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

@mqyqingfeng
Copy link
Owner Author

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

@Tvinsh
Copy link

Tvinsh commented Oct 11, 2017

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
Copy link
Owner Author

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

@naihe138
Copy link

我从《你不知道的 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
Copy link
Owner Author

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

@ghost
Copy link

ghost commented Nov 3, 2017

@mqyqingfeng

可以这样说吗?

=是浅拷贝

sliceconcat如果拷贝基本类型元素的数组是深拷贝,否则是浅拷贝

@mqyqingfeng
Copy link
Owner Author

@veedrin 可以,只是不知道为什么,我觉得怪怪的,好像一般不会这样描述……

@JHanLu
Copy link

JHanLu commented Nov 9, 2017

请问深拷贝的时候遇到相互引用的情况怎么处理?

@mqyqingfeng
Copy link
Owner Author

@JHanLu 这个可以参照下一篇 extend 的实现方式 #33

@UNDERCOVERj
Copy link

这个深拷贝问题很大,没有考虑dom对象,正则对象,时间对象

@mqyqingfeng
Copy link
Owner Author

@UNDERCOVERj 确实没有考虑这些场景,不过常遇到的场景都是普通对象的拷贝,这样的也够了~

@allenGKC
Copy link

写的很清楚,谢谢楼主

@lizhongzhen11
Copy link

star一下以示支持

@CodeLittlePrince
Copy link

深拷贝这样写应该会好点:

var deepCopy = function(obj) {
    if (obj === null || typeof obj !== 'object') return obj;
    ...
}

@ishowman
Copy link

Hi,你的深拷贝方法我觉得还是有点问题。如果遇到对象内嵌函数,typeof判断返回的是function,还是复制了指针而不是整个函数吧?可以看看下面代码理解我描述的问题

var arr = [function(){
    console.log(a)
}, {
    b: function(){console.log(b)
}], 
new_arr = deepCopy(arr);
arr[0] === new_arr[0] // true
new_arr[1].b === arr[1].b // true

by the way, 博主的基础真是扎实,佩服

@mqyqingfeng
Copy link
Owner Author

@ishowman 函数的复制是一个很难解决的问题呀,即使是 jQuery 的 extend 也没有去处理函数……

@youzaiyouzai666
Copy link

浅拷贝:es6中有两种新方法
方法1:
` let [...spread]= [12, 5, 8, 130, 44];

//等同于:let spread = 浅克隆([12, 5, 8, 130, 44]) `

方法2:
Array.from(array)//创建一个新数组

@zhoufanglu
Copy link

这句可以优化
var newObj = obj instanceof Array ? [] : {};
可以这么写 var newObj = new obj.constructor
我还想问下,如果里面好几个元素都是一样的,请问怎么优化? 遇到面试官问了。。

@vnues
Copy link

vnues commented Jul 13, 2020

@mqyqingfeng

可以这样说吗?

=是浅拷贝

sliceconcat如果拷贝基本类型元素的数组是深拷贝,否则是浅拷贝
并不能这么说 浅拷贝和赋值不能混为一谈 之所以需要深浅拷贝是因为赋值时候 如果遇到引用类型时候 两个变量直接会相互影响 所以是为了解决这一现象而采取的方案

赋值:

  • 基本数据类型:赋值,赋值之后两个变量互不影响

  • 引用数据类型:赋,两个变量具有相同的引用,指向同一个对象,相互之间有影响 (为了解决这种场景)

@leviding
Copy link

leviding commented Jul 19, 2020

这句可以优化
var newObj = obj instanceof Array ? [] : {};
可以这么写 var newObj = new obj.constructor
我还想问下,如果里面好几个元素都是一样的,请问怎么优化? 遇到面试官问了。。

请问你所说的,好几个元素都是一样的,是什么意思?我所理解的,如果是数组,即使值一样,但它们的 index 不一样吧?如果是对象,即使它们的值一样,但它们的 key 不一样吧?

是指将多个对象/数组拷贝合并到一个对象/数组中的情况吗?不是单单地对一个数组/对象进行深/浅拷贝。

@wweggplant
Copy link

//其实,简单粗暴copy;只有,function不满足
 function deepCopy(obj) {
      return new Promise((resolve) => {
        const {port1, port2} = new MessageChannel();
        port2.onmessage = ev => resolve(ev.data);
        port1.postMessage(obj);
      });
    }

    deepCopy(obj).then((copy) => {           // 请记住`MessageChannel`是异步的这个前提!
        let copyObj = copy;
        console.log(copyObj, obj)
        console.log(copyObj == obj)
    });

postMessage应该也能实现

@Kitaharakasusa
Copy link

为什么还要用hasOwnProperty判断一下啊

@lazyken
Copy link

lazyken commented Sep 14, 2020

if (typeof obj !== 'object') return;
是不是return obj比较好

@tancgo
Copy link

tancgo commented Oct 2, 2020

function deepClone(obj) {
  function isObject(o) {
    return typeof o === "object" && o !== null;
  }

  if (!isObject(obj)) {
    throw new Error("非对象");
  }

  const newObj = obj instanceof Array ? [] : {};

  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      const cur = obj[key];
      newObj[key] = isObject(cur) ? deepClone(cur) : cur;
    }
  }

  return newObj;
}

@pythonfirst
Copy link

为什么还要用hasOwnProperty判断一下啊

因为for in 不仅遍历对象自身属性,还会遍历继承的inumerable 属性,这里只拷贝自身属性。

@lqPrototype
Copy link

为什么没有人说递归深拷贝有缺点的: 1. 栈溢出、2. 尾递归优化、3. 动态规划来解决.

@xsfxtsxxr
Copy link

大佬深拷贝没考虑环么?

@CoderCxb
Copy link

CoderCxb commented Jan 6, 2021

有两个问题

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
Copy link

growYdp commented Feb 8, 2021

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

@zxk-github
Copy link

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

@opamine
Copy link

opamine commented Jun 13, 2021

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

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

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

@SilenceTiger
Copy link

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

@w2xi
Copy link

w2xi commented Apr 8, 2022

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

浏览器兼容:

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

@justorez
Copy link

处理循环引用的版本:

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
Copy link

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

@justorez
Copy link

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

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

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

No branches or pull requests