JavaScript 专题之深浅拷贝

发布于 2022-10-30 10:12:10 字数 2756 浏览 232 评论 43

拷贝也是面试经典!

数组的浅拷贝

如果是数组,我们可以利用数组的一些方法比如: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 函数。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(43

如果没有 2022-05-04 13:50:46

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

小嗷兮 2022-05-04 13:50:46
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
  }
}

这样也可以

月隐月明月朦胧° 2022-05-04 13:50:46

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

渔村楼浪 2022-05-04 13:50:46

我从《你不知道的 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(..) 会出错。
...

冷默言语 2022-05-04 13:50:46

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

烟雨凡馨 2022-05-04 13:50:46

@mqyqingfeng

可以这样说吗?

=是浅拷贝

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

撩起发的微风 2022-05-04 13:50:46

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

清醇, 2022-05-04 13:50:46

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

马蹄踏│碎落叶 2022-05-04 13:50:46

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

抚笙 2022-05-04 13:50:46

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

傾城如夢未必闌珊 2022-05-04 13:50:46

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

橘亓 2022-05-04 13:50:46

写的很清楚,谢谢楼主

寂寞笑我太脆弱 2022-05-04 13:50:46

star一下以示支持

救星。 2022-05-04 13:50:46

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

var deepCopy = function(obj) {
    if (obj === null || typeof obj !== 'object') return obj;
    ...
}
随风而去 2022-05-04 13:50:46

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, 博主的基础真是扎实,佩服

场罚期间 2022-05-04 13:50:46

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

一场信仰旅途 2022-05-04 13:50:46

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

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

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

故笙诉离歌 2022-05-04 13:50:46

@KingsonCheng 避免拷贝继承下来的属性.

为啥要避免拷贝继承下来的属性?

。咿呀咿呀哟 2022-05-04 13:50:46

Date RegExp Error Promise Map Set... 写一个好的深拷贝好难呀

贪了杯 2022-05-04 13:50:46

@superwtt
function Person() {} Person.prototype = { name: 'hello' } var p = new Person(); for (var key in p) { console.log(key); //name } 就是这种情况,p只是Person的实例,for...in循环却能拿到Person原型上的name属性

甜尕妞° 2022-05-04 13:50:46

@superwtt
function Person() {} Person.prototype = { name: 'hello' } var p = new Person(); for (var key in p) { console.log(key); //name } 就是这种情况,p只是Person的实例,for...in循环却能拿到Person原型上的name属性

所以意思是,不想把原型上的属性也拷贝过来,指向拷贝这个对象上自己的属性 对吧?

心凉 2022-05-04 13:50:46

感谢分享!

空宴 2022-05-04 13:50:46

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

淡看悲欢离合 2022-05-04 13:50:46

@mqyqingfeng

可以这样说吗?

=是浅拷贝

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

赋值:

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

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

猫腻 2022-05-04 13:50:46

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

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

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

玻璃人 2022-05-04 13:50:46
//其实,简单粗暴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应该也能实现

孤寂小茶 2022-05-04 13:50:46

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

喜你已久 2022-05-04 13:50:46

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

千仐 2022-05-04 13:50:46
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;
}
青萝楚歌 2022-05-04 13:50:46

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

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

风吹雨成花 2022-05-04 13:50:46

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

请爱~陌生人 2022-05-04 13:50:46

大佬深拷贝没考虑环么?

南冥有猫 2022-05-04 13:50:46

有两个问题

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;
}
进行了略微的修改,大佬你看看
文章写得很好,点个赞!

夜未央゛樱花落ζ 2022-05-04 13:50:46

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

柳絮泡泡 2022-05-04 13:50:46

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

桃扇骨 2022-05-04 13:50:46

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

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

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

恍梦境° 2022-05-04 13:50:46

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

稚气少女 2022-05-04 13:50:46

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

浏览器兼容:

ChromeFirefoxEdgeSafariOpera
98

终弃我 2022-05-04 13:50:42

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

記憶穿過時間隧道 2022-05-04 13:49:30

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

deepCopy({
        value: null
})

的值为:

{value: {}}

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

风渺 2022-05-04 13:41:22

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

快乐很简单 2022-05-04 12:55:59

养肥了再看一遍

坏尐絯 2022-05-04 06:16:00

期待下一篇!!!

~没有更多了~

关于作者

尛丟丟

暂无简介

0 文章
0 评论
23 人气
更多

推荐作者

yili302

文章 0 评论 0

晚霞

文章 0 评论 0

LLFFCC

文章 0 评论 0

陌路黄昏

文章 0 评论 0

xiaohuihui

文章 0 评论 0

你与昨日

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文