最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • [译]如何区分浅拷贝和深拷贝并实现深拷贝?

    正文概述 掘金(吃凹吃凹)   2021-05-12   455

    前言

    新鲜事物肯定是最好的!

    你多半在清楚认识 JavaScript 拷贝之前就已经使用过它了。或许你也听过这个规范:在函数式编程中,你不应该随意操作任何现存数据(译:感觉有点突兀,水平有限)。这篇文章将向你讲述如何在 JavaScript 中安全地拷贝一个值,并避免引发意外的错误。

    什么是拷贝 ?

    一个东西的拷贝看起来像是原来的东西,然而它并不是。同时,当你改变拷贝时,原来的东西可不会发生变化。

    在编程时,我们把值存储在变量里,拷贝意味着用原变量初始化了一个新变量。请注意,拷贝具有两个不同的概念:深拷贝(deep copying)浅拷贝(shallow copying)。深拷贝意味着新变量的所有值都被复制且与原变量毫不相关;浅拷贝则表示着新变量的某些(子)值仍然与原变量相关。

    为了更好的理解深拷贝与浅拷贝,我们需要知道,JavaScript 是如何存储一个值的。

    值的存储方式

    原始数据类型

    原始数据类型包括:

    • Number 如: 1
    • String 如: 'Hello'
    • Boolean 如:true
    • undefined
    • null

    这些类型的值与指定给它们的变量紧密相连,也不会同时与多个变量关联,这意味着你并不需要担心在JavaScript 中复制这些原始数据类型时发生意外:复制它们得到的是一个确确实实独立的副本。

    我们来看一个例子:

    const a = 5
    let b = 6 // 创建 a 的拷贝
    console.log(b) // 6
    console.log(a) // 5
    

    通过执行 b = a ,就可以得到 a 的拷贝。此时,将新值重新指定给 b 时,b 的值会改变,但 a 的值不会随之发生变化。

    [译]如何区分浅拷贝和深拷贝并实现深拷贝?

    复合数据类型—— Object 与数组

    技术上看,数组也是 Object 对象,所以它们有着相似的表现。关于这点,后文我们会详细地介绍。

    在这里,拷贝变得耐人寻味了起来:复合类型的值在被实例化时仅会被创建一次。也就是说,如果我们进行复合类型的拷贝,实际上是分配给拷贝一个指向原对象的引用。

    const a = {
        en: 'Hello',
        de: 'Hallo',
        es: 'Hola',
        pt: 'Olà'
    }
    let b = a
    b.pt = 'Oi'
    console.log(b.pt) // Oi
    console.log(a.pt) // Oi
    

    上面的实例展示了浅拷贝的特征。通常而言,我们并不期望得到这种结果——原变量 a 并不应该受到新变量 b 的影响。当我们访问原变量时,往往造成出乎意料的错误。因为你不清楚错误的原因,可能会在造成错误后进行一会儿的调试,接着“自暴自弃”了起来。

    [译]如何区分浅拷贝和深拷贝并实现深拷贝?

    不用急,让我们看看一些实现深拷贝的方法。

    实现深拷贝的方法

    Object

    有许多方法可以确实地复制一个对象,其中新的 JavaScript 规范提供了我们一种非常快捷的方式。

    展开运算符(Spread operator)

    它在 ES2015 中被引入,它太吊了,因为它实在是简洁方便。它可以把原变量“展开”到一个新的变量中。使用方式如下:

    const a = {
        en: 'Bye',
        de: 'Tschüss'
    }
    let b = {...a} // 没错!就这么简单
    b.de = 'Ciao'
    console.log(b.de) // Ciao
    console.log(a.de) // Tschüss
    

    也可以使用它把两个对象合并在一起,例如 const c = {... a,... b}

    Object.assign

    这种方法在展开运算符出现之前被广泛采用,基本上与后者相同。但在使用它时你可得小心,因为 Object.assign() 方法的第一个参数会被修改然后返回,所以一般我们会传给第一个参数一个空对象,防止被意外修改。然后,传你想复制的对象给第二个参数。

    const a = {
        en: 'Bye',
        de: 'Tschüss'
    }
    let b = Object.assign({}, a)
    b.de = 'Ciao'
    console.log(b.de) // Ciao
    console.log(a.de) // Tschüss
    
    陷阱:嵌套的 Object 对象

    在复制一个对象时有个很大的陷阱,你也许也发现了,这个陷阱存在于上述的两种拷贝方法:当你有一个嵌套的对象(数组)并试图深拷贝它们时。该对象内部的对象并不会以同样的方式被拷贝下来——它们会被浅拷贝。因此,如果你更改得到的拷贝里的对象,原对象里的对象也将改变。下面是此错误的示例:

    const a = {
        foods: {
          dinner: 'Pasta'
        }
    }
    let b = {...a}
    b.foods.dinner = 'Soup' // dinner 并未被深拷贝
    console.log(b.foods.dinner) // Soup
    console.log(a.foods.dinner) // Soup
    

    要得到让对象里的对象得到预期的深拷贝,你必须手动复制所有嵌套对象:

    const a = {
        foods: {
          dinner: 'Pasta'
        }
    }
    let b = {foods: {...a.foods}}
    b.foods.dinner = 'Soup'
    console.log(b.foods.dinner) // Soup
    console.log(a.foods.dinner) // Pasta
    

    如果要拷贝的对象里不止一个对象( foods),可以再次利用一下展开运算符。也就是这样:const b = {... a,foods:{... a.foods}}

    简单粗暴的深拷贝方式

    如果你不知道对象有多少层嵌套呢?手动遍历对象并手动复制每个嵌套对象可十分繁琐。有一种方法能粗暴地拷贝下对象。只需将对象转换为字符串(stringify),然后解析一下(parse)它就完事啦:

    const a = {
        foods: {
          dinner: 'Pasta'
        }
    }
    let b = JSON.parse(JSON.stringify(a))
    b.foods.dinner = 'Soup'
    console.log(b.foods.dinner) // Soup
    console.log(a.foods.dinner) // Pasta
    

    如果使用这种方法,你得明白这是无法完全复制自定义类实例的。所以只有拷贝仅有 本地JavaScript值(native JavaScript values) 的对象时才可以使用此方式。

    水平不够,翻译不好?,放下原文:

    建议先不纠结,后文有细说。

    数组

    拷贝数组和拷贝对象相仿,因为数组本质上也是一种对象。

    展开运算符

    操作起来和对象一样:

    const a = [1,2,3]
    let b = [...a]
    b[1] = 4
    console.log(b[1]) // 4
    console.log(a[1]) // 2
    
    数组方法:map, filter, reduce

    运用这些方法可以得到一个新的数组,里面包含原数组里的所有值(或部分)。在拷贝过程中还可以修改你想修改的值,上帝啊,这也太方便了吧。

    const a = [1,2,3]
    let b = a.map(el => el)
    b[1] = 4
    console.log(b[1]) // 4
    console.log(a[1]) // 2
    

    或者在复制时修改所需的元素:

    const a = [1,2,3]
    const b = a.map((el, index) => index === 1 ? 4 : el)
    console.log(b[1]) // 4
    console.log(a[1]) // 2
    
    Array.slice

    slice 方法通常用于返回数组的子集。数组的子集从数组的特定下标开始,也可以自定义结束的位置。使用 array.slice() array.slice(0) 时,可以得到 array 数组的拷贝。

    const a = [1,2,3]
    let b = a.slice(0)
    b[1] = 4
    console.log(b[1]) // 4
    console.log(a[1]) // 2
    
    多维数组(Nested arrays,嵌套数组)

    和 Object 一样,使用上面的方法并不会将内部元素进行同样的深拷贝。为了防止意外,可以使用JSON.parse(JSON.stringify(someArray))

    奖励(BONUS):复制自定义类的实例

    当你已是专业的 JavaScript 开发人员,并也要复制自定义构造函数或类时,前面已有提到:你不能简单地将他们转为字符串然后解析,否则实例的方法会遗失。Don't panic!可以自己定义一个 Copy 方法来得到一个具有所有原对象值的新对象,看看具体实现:

    class Counter {
        constructor() {
            this.count = 5
        }
        copy() {
            const copy = new Counter()
            copy.count = this.count
            return copy
        }
    }
    const originalCounter = new Counter()
    const copiedCounter = originalCounter.copy()
    console.log(originalCounter.count) // 5
    console.log(copiedCounter.count) // 5
    copiedCounter.count = 7
    console.log(originalCounter.count) // 5
    console.log(copiedCounter.count) // 7
    

    如果要将对象内部的对象也运用深拷贝,你得灵活使用有关深拷贝的新技能。我将为自定义构造函数的拷贝方法添加最终的解决方法,使它更加动态。

    使用此拷贝方法,你可以在构造函数中防止任意数量地值,而不再需要一一赋值。

    (译:我觉得这奖励是个作业)

    结尾:

    原文作者:Lukas Gisder-Dubé

    原文:How to differentiate between deep and shallow copies in JavaScript

    译文和原文有所出入(大意一致),翻译不易,点个赞bie~嘿嘿嘿


    起源地下载网 » [译]如何区分浅拷贝和深拷贝并实现深拷贝?

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元