V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
wunonglin
V2EX  ›  程序员

js 如何实现对象值复制?

  •  
  •   wunonglin · 2021-12-08 10:32:10 +08:00 · 2698 次点击
    这是一个创建于 841 天前的主题,其中的信息可能已经有所发展或是发生改变。

    go 的实现

    package main
    
    import "fmt"
    
    type M struct {
    	Num int64
    }
    
    func T1() {
    	fmt.Printf("----T1----\n")
    	a := M{1}
    	b := a
    	fmt.Printf("%+v\n", a)
    	fmt.Printf("%+v\n", b)
    	a.Num = 2
    	fmt.Printf("%+v\n", a)
    	fmt.Printf("%+v\n", b)
    }
    
    func T2() {
    	fmt.Printf("----T2----\n")
    	a := &M{1}
    	b := a
    	fmt.Printf("%+v\n", a)
    	fmt.Printf("%+v\n", b)
    	a.Num = 2
    	fmt.Printf("%+v\n", a)
    	fmt.Printf("%+v\n", b)
    }
    
    func main() {
    	T1()
    	T2()
    }
    

    输出结果

    ----T1----
    {Num:1}
    {Num:1}
    {Num:2}
    {Num:1}
    ----T2----
    &{Num:1}
    &{Num:1}
    &{Num:2}
    &{Num:2}
    

    js 的实现

    function T2(){
        let a = {num: 1}
        let b = a
        console.log(a)
        console.log(b)
        a.num = 2
        console.log(a)
        console.log(b)
    }
    

    输出结果

    {num: 1}
    {num: 1}
    {num: 2}
    {num: 2}
    

    如何使用 js 实现 golang 的 T1 方法?除了深拷贝。

    就类似于“从源 Object 创建一个新的 Object ,内存地址完全是新的,新 Object 也和源完全一样,不会丢失各种属性”,js 自带的好像没看到类似的方法

    第 1 条附言  ·  2021-12-08 12:50:26 +08:00

    最新测试用例

    const _ = require('./lodash.min.js')
    
    class M {
      get K(){
        return this.Num + 1
      }
    
      constructor(value) {
        this.Num = value
      }
    }
    
    function T1(){
      console.log('----T1----')
      const a = new M(1)
      const b = a
      console.log(a)
      console.log(b)
      a.Num = 2
      console.log(a, a.K, a instanceof M)
      console.log(b, a.K, b instanceof M)
    }
    
    function T1_JSON(){
      console.log('----T1_JSON----')
      const a = new M(1)
      const b = JSON.parse(JSON.stringify(a))
      console.log(a)
      console.log(b)
      a.Num = 2
      console.log(a, a.K, a instanceof M)
      console.log(b, a.K, b instanceof M)
    }
    
    function T1_Assign(){
      console.log('----T1_Assign----')
      const a = new M(1)
      const b = Object.assign({}, a)
      console.log(a)
      console.log(b)
      a.Num = 2
      console.log(a, a.K, a instanceof M)
      console.log(b, a.K, b instanceof M)
    }
    
    function T1_Spread(){
      console.log('----T1_Spread----')
      const a = new M(1)
      const b = {...a}
      console.log(a)
      console.log(b)
      a.Num = 2
      console.log(a, a.K, a instanceof M)
      console.log(b, a.K, b instanceof M)
    }
    
    function T1_CloneDeep(){
      console.log('----T1_CloneDeep----')
      const a = new M(1)
      const b = _.cloneDeep(a)
      console.log(a)
      console.log(b)
      a.Num = 2
      console.log(a, a.K, a instanceof M)
      console.log(b, a.K, b instanceof M)
    }
    
    function T1_StructuredClone(){
      console.log('----T1_StructuredClone----')
      const a = new M(1)
      const b = structuredClone(a)
      console.log(a)
      console.log(b)
      a.Num = 2
      console.log(a, a.K, a instanceof M)
      console.log(b, a.K, b instanceof M)
    }
    
    function T1_Create(){
      console.log('----T1_Create----')
      const a = new M(1)
      const b = Object.create(a)
      console.log(a)
      console.log(b)
      a.Num = 2
      console.log(a, a.K, a instanceof M)
      console.log(b, a.K, b instanceof M)
    }
    
    T1()
    T1_JSON()
    T1_Assign()
    T1_Spread()
    T1_CloneDeep()
    T1_StructuredClone()
    T1_Create()
    
    第 2 条附言  ·  2021-12-08 12:50:48 +08:00

    期望返回结果

    M { Num: 1 }
    M { Num: 1 }
    M { Num: 2 } 3 true
    M { Num: 1 } 2 true
    

    实际返回结果

    ----T1----
    M { Num: 1 }
    M { Num: 1 }
    M { Num: 2 } 3 true
    M { Num: 2 } 3 true
    ----T1_JSON----
    M { Num: 1 }
    { Num: 1 }
    M { Num: 2 } 3 true
    { Num: 1 } 3 false
    ----T1_Assign----
    M { Num: 1 }
    { Num: 1 }
    M { Num: 2 } 3 true
    { Num: 1 } 3 false
    ----T1_Spread----
    M { Num: 1 }
    { Num: 1 }
    M { Num: 2 } 3 true
    { Num: 1 } 3 false
    ----T1_CloneDeep----
    M { Num: 1 }
    M { Num: 1 }
    M { Num: 2 } 3 true
    M { Num: 1 } 3 true
    ----T1_StructuredClone----
    M { Num: 1 }
    { Num: 1 }
    M { Num: 2 } 3 true
    { Num: 1 } 3 false
    ----T1_Create----
    M { Num: 1 }
    M {}
    M { Num: 2 } 3 true
    M {} 3 true
    
    39 条回复    2021-12-08 16:56:10 +08:00
    NathanDo
        2
    NathanDo  
       2021-12-08 10:38:19 +08:00
    const b = JSON.parse(JSON.stringify(a));
    anjianshi
        3
    anjianshi  
       2021-12-08 10:38:43 +08:00
    如果是普通对象,没有 method 之类的东西,且不考虑性能,可以 JSON 化。

    ```javascript
    const objectA = {
    a: {
    b: { c: 1 },
    d: 2,
    },
    e: 3
    }

    const objectB = JSON.parse(JSON.stringify(objectA))

    objectA.a.d = 100
    objectB.a.d = 200

    console.log(objectA.a.d) // 100
    console.log(objectB.a.d) // 200
    ```
    anjianshi
        4
    anjianshi  
       2021-12-08 10:39:00 +08:00
    哈哈哈哈 一楼抢答了
    viewweiwu
        5
    viewweiwu  
       2021-12-08 10:39:00 +08:00
    1. let b = Object.assign({}. a)
    2. let b = {...a}
    3. JSON.parse(JSON.stringify(a))

    这三种方法都可以
    viewweiwu
        6
    viewweiwu  
       2021-12-08 10:39:50 +08:00   ❤️ 1
    1. let b = Object.assign({}, a)
    2. let b = {...a}
    3. JSON.parse(JSON.stringify(a))

    这三种方法都可以
    tsanie
        7
    tsanie  
       2021-12-08 10:44:30 +08:00   ❤️ 1
    Object.assign 和{...a}是浅拷贝。
    JSON.parse(JSON.stringify(a))是深拷贝但没法处理函数。
    自己写递归或者用 lodash 的 cloneDeep 吧。
    wunonglin
        8
    wunonglin  
    OP
       2021-12-08 10:48:14 +08:00
    @NathanDo #2
    @anjianshi #3
    @anjianshi #4
    @viewweiwu #5

    ```js
    class M {
    get K(){
    return this.Num + 1
    }

    constructor(value) {
    this.Num = value
    }
    }

    function T1(){
    console.log('----T1----')
    const a = new M(1)
    const b = a
    console.log(a)
    console.log(b)
    a.Num = 2
    console.log(a, a.K, a instanceof M)
    console.log(b, a.K, b instanceof M)
    }

    function T2_JSON(){
    console.log('----T2_JSON----')
    const a = new M(1)
    const b = JSON.parse(JSON.stringify(a))
    console.log(a)
    console.log(b)
    a.Num = 2
    console.log(a, a.K, a instanceof M)
    console.log(b, a.K, b instanceof M)
    }

    function T2_Assign(){
    console.log('----T2_Assign----')
    const a = new M(1)
    const b = Object.assign({}, a)
    console.log(a)
    console.log(b)
    a.Num = 2
    console.log(a, a.K, a instanceof M)
    console.log(b, a.K, b instanceof M)
    }

    function T2_Spread(){
    console.log('----T2_Spread----')
    const a = new M(1)
    const b = {...a}
    console.log(a)
    console.log(b)
    a.Num = 2
    console.log(a, a.K, a instanceof M)
    console.log(b, a.K, b instanceof M)
    }

    T1()
    T2_JSON()
    T2_Assign()
    T2_Spread()
    ```

    使用 json ,assign ,spread 后原型丢了。不符合需求
    wunonglin
        9
    wunonglin  
    OP
       2021-12-08 10:48:46 +08:00
    ```
    ----T1----
    M { Num: 1 }
    M { Num: 1 }
    M { Num: 2 } 3 true
    M { Num: 2 } 3 true
    ----T2_JSON----
    M { Num: 1 }
    { Num: 1 }
    M { Num: 2 } 3 true
    { Num: 1 } 3 false
    ----T2_Assign----
    M { Num: 1 }
    { Num: 1 }
    M { Num: 2 } 3 true
    { Num: 1 } 3 false
    ----T2_Spread----
    M { Num: 1 }
    { Num: 1 }
    M { Num: 2 } 3 true
    { Num: 1 } 3 false
    ```
    yaphets666
        10
    yaphets666  
       2021-12-08 10:50:52 +08:00
    除了深拷贝?为啥要把他除了啊.jsonstringify 我劝你别用 会丢失数据结构
    wunonglin
        11
    wunonglin  
    OP
       2021-12-08 10:52:37 +08:00
    @yaphets666 #10

    类似于 b = Object.from(a),这样能够实现 golang 的效果,这个 from 是不存在的,举例而已
    yaphets666
        13
    yaphets666  
       2021-12-08 10:55:29 +08:00
    @wunonglin js 没有原生的这种功能,如果想完美的实现一个复杂数据结构对象的值复制,必须使用深拷贝,自己实现一个或者使用第三方库.上面说的 object.assign 和 JSON.stringify 都有各自的局限性,你可以自行百度.
    gadfly3173
        14
    gadfly3173  
       2021-12-08 10:58:02 +08:00
    “从源 Object 创建一个新的 Object ,内存地址完全是新的,新 Object 也和源完全一样,不会丢失各种属性”
    这个需求不就是深拷贝么,,,除了深拷贝还能咋实现呢
    anjianshi
        15
    anjianshi  
       2021-12-08 10:58:32 +08:00
    要保留原型,就只能用笨方法了。
    wunonglin
        16
    wunonglin  
    OP
       2021-12-08 11:03:00 +08:00
    @gadfly3173 #14 原生方法,非第三方
    anjianshi
        17
    anjianshi  
       2021-12-08 11:05:08 +08:00
    涉及到 class 的东西,是没法做到“复制”的。go 里我记得是没有 class 之类的概念,你才能用系统提供的能力去复制。
    只有“普通对象”,可以快捷复制。
    wunonglin
        18
    wunonglin  
    OP
       2021-12-08 11:05:22 +08:00
    @yaphets666 #13
    @gadfly3173 #12

    lodash 的 cloneDeep 可以实现。但是我想知道有没有原生的方法
    anjianshi
        19
    anjianshi  
       2021-12-08 11:05:47 +08:00
    所以,要么不要带 class ,要么自己手动深拷贝。
    WhiteHu
        20
    WhiteHu  
       2021-12-08 11:08:07 +08:00
    structuredClone()

    有自带的方法哈 不过是试验性 api

    https://developer.mozilla.org/en-US/docs/Web/API/structuredClone
    joshua7v
        21
    joshua7v  
       2021-12-08 11:10:58 +08:00
    structured clone 快来了
    虽然浏览器还未开放这个 api
    但可以借用浏览器的某些基于此 api 的接口
    比如 postmessage 什么的
    wunonglin
        22
    wunonglin  
    OP
       2021-12-08 11:11:34 +08:00
    @WhiteHu #20 哈哈哈哈哈哈。还真有。至少可以在 node 开发的时候用,浏览器可以等未来吧
    3dwelcome
        23
    3dwelcome  
       2021-12-08 11:11:38 +08:00
    我总觉得 JS 并不是什么正经语言,早年发明之初,就没想过现代前端会那么复杂。

    JS 可以快速写逻辑,但是编写复杂的算法,还是比不上传统语言。

    学我,JS 语法不足,用 webasm 来补全。把你原生 JS 逼死,也只有用深拷贝。
    wunonglin
        24
    wunonglin  
    OP
       2021-12-08 11:12:23 +08:00
    @joshua7v #21 firefox 永远的神,第一个支持了
    yaphets666
        25
    yaphets666  
       2021-12-08 11:34:49 +08:00
    @3dwelcome 正经语言是正经语言,没考虑过会这么复杂是真的.
    libook
        26
    libook  
       2021-12-08 11:43:33 +08:00   ❤️ 2
    JS 这种高度抽象的语言的设计初衷之一,就是让使用者不需要去考虑内存细节,所以赋值操作,对于简单类型是赋值,对于复杂类型是引用。

    仅针对题主的例子,不考虑其他情况的话可以这样写:
    let b = Object.assgin({},a);
    这行代码的意思是把 a 里面的所有成员拿出来,一个一个地赋值给 b 的同名成员,这个赋值操作和等号的赋值操作一样,同样是简单类型赋值、复杂类型引用。因为 Num 是简单的数值型,所以执行了赋值而不是引用,导致修改 b.Num 不会让 a.Num 发生改变。

    但如果 Num 的值是一个对象,因为 b.Num 和 a.Num 引用的是同一个 Num 对象,所以修改 Num 对象内的成员后,读取 a 和 b 内的 Num 对像会发现发生了变化,此时如果还需要进一步的值复制,就需要深拷贝。

    如果对象是简单的、可以用 JSON 描述的对象,比如不含有 getter 、setter ,没用 Symbol 字段名、没用非 JSON 数据类型等,那么常用的方式是 JSON.stringify 序列化再 JSON.parse 反序列化,完成一个深拷贝。这个在本来就在上游使用 JSON 的场景用得很广泛,比如 HTTP 通信。

    JS 有个核心特性叫做原型链,对象值复制跟原型链的思想是矛盾的,前者希望尽可能复用代码,后者希望尽可能复制代码。这也就导致在 JS 里做深拷贝不那么方便。

    有不少第三方的 deep clone 库,可以拿来直接用。
    libook
        27
    libook  
       2021-12-08 11:46:00 +08:00
    @libook #26 勘误:
    JS 有个核心特性叫做原型链,对象值复制跟原型链的思想是矛盾的,前者希望尽可能复制代码,后者希望尽可能复用代码。这也就导致在 JS 里做深拷贝不那么方便。
    EPr2hh6LADQWqRVH
        28
    EPr2hh6LADQWqRVH  
       2021-12-08 11:56:13 +08:00
    基础不牢,地动山摇,都十几层楼了还没人抛出 `Object.create()`

    既然 js 那就上原型啊

    const a = {x: 1};

    const b = Object.create(a);

    b.x = 2;

    assert(a.x === 1);
    gadfly3173
        29
    gadfly3173  
       2021-12-08 12:11:35 +08:00
    @avastms #28 楼主想要的是属性完全相同,内存地址不同的结果,Object.create 只是把源对象作为原型生成了新对象,你对源对象的修改还是会影响到新的对象
    bnm965321
        30
    bnm965321  
       2021-12-08 12:12:26 +08:00
    @wunonglin 这种的原理也是深拷贝吧
    chenstack
        31
    chenstack  
       2021-12-08 12:13:25 +08:00
    @avastms #28 这种方式变成 b 往上找值,a 的改动还会影响 b ,而且 b 没有给字段赋值的话,JSON.stringify(b)返回空的"{}"
    gadfly3173
        32
    gadfly3173  
       2021-12-08 12:13:48 +08:00
    EPr2hh6LADQWqRVH
        33
    EPr2hh6LADQWqRVH  
       2021-12-08 12:25:31 +08:00
    那自己实现一下吧, 之后维护一下原型链。

    const b = Object.assign({}, a);

    Object.setPrototypeOf(b, Object.getPrototypeOf(a));
    codehz
        34
    codehz  
       2021-12-08 14:44:19 +08:00
    考虑到还有原生对象,精确的复制大概是不现实的(
    加上还可以 proxy
    SmallTeddy
        35
    SmallTeddy  
       2021-12-08 14:49:44 +08:00
    @NathanDo 正解
    yangzzzzzz
        36
    yangzzzzzz  
       2021-12-08 14:56:05 +08:00
    使用 lodash

    _.clone(value):浅拷贝。浅拷贝只是对指针的拷贝,拷贝后两个指针指向同一个内存空间。

    _.cloneDeep(value):深拷贝。深拷贝不但对指针进行拷贝,而且对指针指向的内容进行拷贝,经深拷贝后的指针是指向两个不同地址的指针。

    _.defaults(object, [sources]):只对比第一层。给对象添加字段,保持原来字段的值。

    _.defaultsDeep(object, [sources]):递归对比到最里层。给对象添加字段,保持原来字段的值。
    2i2Re2PLMaDnghL
        37
    2i2Re2PLMaDnghL  
       2021-12-08 15:09:45 +08:00   ❤️ 1
    你的验证代码写错了,console.log(b, a.K, b instanceof M)
    本该是 b.K
    2i2Re2PLMaDnghL
        38
    2i2Re2PLMaDnghL  
       2021-12-08 15:28:46 +08:00   ❤️ 1
    实际上有个挺严重的问题,所谓值复制可能不一定是可行的。

    function factory(value){
    let num = value;
    return {get Num(){return num}, set Num(val){num=val}}
    }

    let a=factory();
    let b=someValueClone(a);

    你不可能在这个情况下通用地分离 a.Num 和 b.Num ,因为涉及到作用域的分支性变化。
    所以说实话,不如学 Rust 搞 trait Clone ,你自己写的类你自己实现 Clone 去。
    hxse
        39
    hxse  
       2021-12-08 16:56:10 +08:00
    这个问题有那么复杂吗

    let a = {num: 1}
    let b = {...a}
    console.log(a)
    console.log(b)
    a.num = 2
    console.log(a)
    console.log(b)

    {num: 1}
    {num: 1}
    {num: 2}
    {num: 1}
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   5108 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 32ms · UTC 01:17 · PVG 09:17 · LAX 18:17 · JFK 21:17
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.